diff --git a/mods/chess.js b/mods/chess.js new file mode 100644 index 00000000..0cfc230c --- /dev/null +++ b/mods/chess.js @@ -0,0 +1,1457 @@ + +elements.screen = { + name: "Screen", + color: "#000000" +} + +let running = false; + +const offsetX = 0; +const offsetY = 0; +const screenWidth = 166 - (2 * offsetX); +const screenHeight = 82 - (2 * offsetY); + +const splitHex = (hex) => hex.slice(1).match(/../g).map(a => Math.floor(parseInt(a, 16))); +const hexify = (rgb) => rgb.map(a => Math.floor(a).toString(16).padStart(2, "0")).join(""); +function colorLerp(color_, color2_, t) { + const color = splitHex(color_); + const color2 = splitHex(color2_); + const r = (1 - t) * color[0] + t * color2[0]; + const g = (1 - t) * color[1] + t * color2[1]; + const b = (1 - t) * color[2] + t * color2[2]; + return hexify([r, g, b]); +} + +function clamp(x, min, max) { + return Math.max(min, Math.min(x, max)); +} + +// 5x5 +const font = { + a: [ + 0, 1, 1, 1, 0, + 0, 0, 0, 0, 1, + 0, 1, 1, 1, 1, + 1, 0, 0, 0, 1, + 0, 1, 1, 1, 1 + ], + b: [ + 1, 1, 1, 1, 0, + 1, 0, 0, 0, 1, + 1, 1, 1, 1, 0, + 1, 0, 0, 0, 1, + 1, 1, 1, 1, 0 + ], + c: [ + 0, 1, 1, 1, 1, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 0, 1, 1, 1, 1 + ], + d: [ + 1, 1, 1, 1, 0, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 1, 1, 1, 1, 0 + ], + e: [ + 1, 1, 1, 1, 1, + 1, 0, 0, 0, 0, + 1, 1, 1, 1, 1, + 1, 0, 0, 0, 0, + 1, 1, 1, 1, 1 + ], + f: [ + 1, 1, 1, 1, 1, + 1, 0, 0, 0, 0, + 1, 1, 1, 1, 0, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0 + ], + g: [ + 1, 1, 1, 1, 1, + 1, 0, 0, 0, 0, + 1, 0, 1, 1, 1, + 1, 0, 0, 0, 1, + 1, 1, 1, 1, 1 + ], + h: [ + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 1, 1, 1, 1, 1, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1 + ], + i: [ + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0 + ], + j: [ + 0, 0, 0, 0, 1, + 0, 0, 0, 0, 1, + 0, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 0, 1, 1, 1, 0 + ], + k: [ + 1, 0, 0, 1, 0, + 1, 0, 0, 1, 0, + 1, 1, 1, 0, 0, + 1, 0, 0, 1, 0, + 1, 0, 0, 1, 0 + ], + l: [ + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 1, 1, 1, 0 + ], + m: [ + 1, 1, 1, 1, 0, + 1, 0, 1, 0, 1, + 1, 0, 1, 0, 1, + 1, 0, 1, 0, 1, + 1, 0, 1, 0, 1 + ], + n: [ + 1, 1, 1, 1, 0, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1 + ], + o: [ + 0, 1, 1, 1, 0, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 0, 1, 1, 1, 0 + ], + p: [ + 1, 1, 1, 1, 0, + 1, 0, 0, 0, 1, + 1, 1, 1, 1, 0, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0 + ], + q: [ + 0, 1, 1, 1, 1, + 1, 0, 0, 0, 1, + 0, 1, 1, 1, 1, + 0, 0, 0, 0, 1, + 0, 0, 0, 0, 1 + ], + r: [ + 1, 0, 1, 1, 1, + 1, 1, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0 + ], + s: [ + 0, 1, 1, 1, 1, + 1, 0, 0, 0, 0, + 0, 1, 1, 1, 0, + 0, 0, 0, 0, 1, + 1, 1, 1, 1, 0 + ], + t: [ + 1, 1, 1, 1, 1, + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0 + ], + u: [ + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 1, 1, 1, 1, 1 + ], + v: [ + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 0, 1, 0, 1, 0, + 0, 1, 0, 1, 0, + 0, 0, 1, 0, 0 + ], + w: [ + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 1, 0, 1, 0, 1, + 1, 0, 1, 0, 1, + 0, 1, 0, 1, 0 + ], + x: [ + 1, 0, 0, 0, 1, + 0, 1, 0, 1, 0, + 0, 0, 1, 0, 0, + 0, 1, 0, 1, 0, + 1, 0, 0, 0, 1 + ], + y: [ + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 0, 1, 0, 1, 0, + 0, 0, 1, 0, 0, + 0, 0, 1, 0, 0 + ], + z: [ + 1, 1, 1, 1, 0, + 0, 0, 0, 1, 0, + 0, 1, 1, 0, 0, + 1, 0, 0, 0, 0, + 1, 1, 1, 1, 0 + ], + "0": [ + 1, 1, 1, 1, 0, + 1, 0, 0, 1, 0, + 1, 0, 0, 1, 0, + 1, 0, 0, 1, 0, + 1, 1, 1, 1, 0 + ], + "1": [ + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0 + ], + "2": [ + 1, 1, 1, 1, 1, + 0, 0, 0, 0, 1, + 1, 1, 1, 1, 1, + 1, 0, 0, 0, 0, + 1, 1, 1, 1, 1 + ], + "3": [ + 1, 1, 1, 1, 1, + 0, 0, 0, 0, 1, + 1, 1, 1, 1, 1, + 0, 0, 0, 0, 1, + 1, 1, 1, 1, 1 + ], + "4": [ + 1, 0, 0, 0, 1, + 1, 0, 0, 0, 1, + 1, 1, 1, 1, 1, + 0, 0, 0, 0, 1, + 0, 0, 0, 0, 1 + ], + "5": [ + 1, 1, 1, 1, 1, + 1, 0, 0, 0, 0, + 1, 1, 1, 1, 1, + 0, 0, 0, 0, 1, + 1, 1, 1, 1, 1 + ], + "6": [ + 1, 1, 1, 1, 1, + 1, 0, 0, 0, 0, + 1, 1, 1, 1, 1, + 1, 0, 0, 0, 1, + 1, 1, 1, 1, 1 + ], + "7": [ + 1, 1, 1, 1, 1, + 0, 0, 0, 0, 1, + 0, 0, 0, 0, 1, + 0, 0, 0, 0, 1, + 0, 0, 0, 0, 1 + ], + "8": [ + 1, 1, 1, 1, 1, + 1, 0, 0, 0, 1, + 1, 1, 1, 1, 1, + 1, 0, 0, 0, 1, + 1, 1, 1, 1, 1 + ], + "9": [ + 1, 1, 1, 1, 1, + 1, 0, 0, 0, 1, + 1, 1, 1, 1, 1, + 0, 0, 0, 0, 1, + 1, 1, 1, 1, 1 + ], + ".": [ + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0 + ], + ":": [ + 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0 + ], + "-": [ + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 1, 1, 1, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0 + ], + "+": [ + 0, 0, 0, 0, 0, + 0, 1, 0, 0, 0, + 1, 1, 1, 0, 0, + 0, 1, 0, 0, 0, + 0, 0, 0, 0, 0, + ], + ",": [ + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + ], + "[": [ + 1, 1, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 1, 0, 0, 0 + ], + "]": [ + 1, 1, 0, 0, 0, + 0, 1, 0, 0, 0, + 0, 1, 0, 0, 0, + 0, 1, 0, 0, 0, + 1, 1, 0, 0, 0 + ], + "(": [ + 0, 1, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 0, 1, 0, 0, 0 + ], + ")": [ + 1, 0, 0, 0, 0, + 0, 1, 0, 0, 0, + 0, 1, 0, 0, 0, + 0, 1, 0, 0, 0, + 1, 0, 0, 0, 0 + ], + ";": [ + 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0 + ], + "!": [ + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 1, 0, 0, 0, 0 + ], + "{": [ + 0, 1, 1, 0, 0, + 0, 1, 0, 0, 0, + 1, 0, 0, 0, 0, + 0, 1, 0, 0, 0, + 0, 1, 1, 0, 0 + ], + "}": [ + 1, 1, 0, 0, 0, + 0, 1, 0, 0, 0, + 0, 0, 1, 0, 0, + 0, 1, 0, 0, 0, + 1, 1, 0, 0, 0 + ], + "_": [ + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, + 1, 1, 1, 0, 0 + ], + "°": [ + 1, 1, 1, 0, 0, + 1, 0, 1, 0, 0, + 1, 1, 1, 0, 0, + 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0 + ], + "|": [ + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0, + 1, 0, 0, 0, 0 + ] +} + +const customWidth = { + i: 1, + l: 4, + z: 4, + "0": 4, + "1": 1, + ".": 1, + ":": 1, + "-": 3, + "+": 3, + ",": 1, + "[": 2, + "]": 2, + "(": 2, + ")": 2, + ";": 1, + "!": 1, + "{": 3, + "}": 3, + "_": 3, + "°": 3, + " ": 3, + "|": 1, +} + +class TextRenderer { + static getCharWidth(char) { + return customWidth[char] ?? 5; + } + + static drawChar(char, x, y, color) { + if (!font[char]) return; + const width = this.getCharWidth(); + for (let i = 0; i < width; i++) { + for (let j = 0; j < 5; j++) { + if (font[char][j * 5 + i] == 1) { + pixelMap[x + offsetX + i][y + offsetY + j].color = color; + } + } + } + } + + static drawText(text, x, y, color) { + let offset = 0; + for (const char of text.toLowerCase().split("")) { + const width = this.getCharWidth(char); + this.drawChar(char, x + offset, y, color); + offset += width + 1; + } + } + + static getStringWidth(text) { + return text.split("").map(a => this.getCharWidth(a)).reduce((a, b) => a + b, 0) + text.length - 1; + } + + static drawCenteredText(text, x1, y, color) { + const x = Math.floor(x1 - (this.getStringWidth(text) / 2)); + this.drawText(text, x, y, color); + } +} + +class ButtonRegistry { + constructor (screen, buttons) { + this.screen = screen; + this.buttons = buttons; + this.currentButton = 0; + if (this.buttons.length > 0) { + this.buttons[this.currentButton].toggleSelection(); + } + } + + next() { + this.buttons[this.currentButton].toggleSelection(); + this.currentButton++; + if (this.currentButton >= this.buttons.length) { + this.currentButton %= this.buttons.length; + } + this.buttons[this.currentButton].toggleSelection(); + } + + prev() { + this.buttons[this.currentButton].toggleSelection(); + this.currentButton--; + if (this.currentButton < 0) { + this.currentButton = this.buttons.length - 1; + } + this.buttons[this.currentButton].toggleSelection(); + } + + current() { + return this.buttons[this.currentButton]; + } +} + +class GameMenu { + constructor (name, screen, width, height, offsets = null) { + this.name = name; + this.screen = screen; + this.width = width; + this.height = height; + this.offsetX = Math.floor((screenWidth - width - (offsets ? offsets.x : 0)) / 2) + (offsets ? offsets.x : 0); + this.offsetY = Math.floor((screenHeight - height - 10 - (offsets ? offsets.y : 0)) / 2) + (offsets ? offsets.y : 0) + 10; + this.buttons = []; + this.buttonRegistry = new ButtonRegistry(this, []); + } + + draw() { + for (const button of this.buttons) { + button.draw(); + } + } + + getOffsetX() { + return offsetX + this.offsetX; + } + + getOffsetY() { + return offsetY + this.offsetY; + } + + onKey(ev) { + if (ev.key == "b") { // up + this.buttonRegistry.prev(); + } else if (ev.key == "n") { // down + this.buttonRegistry.next(); + } else if (ev.key == "Enter") { + this.buttonRegistry.current().click(); + } + } + + onClick(ev) {} + + addButtons(...buttons) { + this.buttons.push(...buttons); + this.updateButtonRegistry(); + } + + getButtons() { + return this.buttons; + } + + updateButtonRegistry() { + this.buttonRegistry = new ButtonRegistry(this, this.buttons); + } +} + +class GuiButton { + constructor (x, y, width, height, text, color, screen) { + this.x = x + screen.getOffsetX(); + this.y = y + screen.getOffsetY(); + this.width = width; + this.height = height; + this.text = text; + this.color = color; + this.onPressed = () => {}; + this.selected = false; + this.screen = screen; + } + + draw() { + GuiUtils.drawRect(this.x, this.y, this.x + this.width, this.y + this.height, this.color); + TextRenderer.drawCenteredText(this.text, this.x + (this.width / 2), Math.floor(this.y + (this.height / 2) - 5/2), "#ffffff"); + if (this.selected) { + GuiUtils.drawOutline(this.x, this.y, this.x + this.width, this.y + this.height, "#000000"); + } + } + + onClick(cb) { + this.onPressed = cb; + } + + click() { + this.onPressed(); + } + + toggleSelection() { + this.selected = !this.selected; + } +} + +class GuiUtils { + static drawRect(x1, y1, x2, y2, color) { + for (let i = Math.max(Math.min(x1, x2), 0); i < Math.min(Math.max(x1, x2), width); i++) { + for (let j = Math.max(Math.min(y1, y2), 0); j < Math.min(Math.max(y1, y2), height); j++) { + pixelMap[i][j].color = color; + } + } + } + + static drawOutline(x1, y1, x2, y2, color) { + const initI = Math.max(Math.min(x1, x2), 0); + const endI = Math.min(Math.max(x1, x2), width); + const initJ = Math.max(Math.min(y1, y2), 0); + const endJ = Math.min(Math.max(y1, y2), height); + for (let i = initI; i < endI; i++) { + for (let j = initJ; j < endJ; j++) { + if (i == initI || i == endI - 1 || j == initJ || j == endJ - 1) { + pixelMap[i][j].color = color; + } + } + } + } + + static drawVerticalLine(x, y1, y2, color) { + for (let i = Math.max(Math.min(y1, y2), 0); i <= Math.min(Math.max(y1, y2), height); i++) { + if (!pixelMap[x][i]) continue; + pixelMap[x][i].color = color; + } + } +} + +const closestDivisible = (n, m) => m * Math.floor(n / m); +const xor = (a, b) => (!a && b) || (a && !b) + +const colorSettings = { + color1: "#eeeed5", + color2: "#7d945d", + white: { + 1: "#A4A9AD", + 2: "#BDC3C7", + 3: "#EDF3F7" + }, + black: { + 1: "#000000", + 2: "#232323", + 3: "#7A7D7F" + } +} + +const sprites = { + "pawn": [ + 0,0,0,1,1,0,0,0, + 0,0,1,3,3,1,0,0, + 0,0,1,3,3,1,0,0, + 0,0,0,1,1,0,0,0, + 0,0,1,3,3,1,0,0, + 0,0,1,3,3,1,0,0, + 0,1,3,3,3,3,1,0, + 0,1,3,3,3,3,1,0 + ], + "bishop": [ + 0,0,0,1,1,0,0,0, + 0,0,1,3,3,1,0,0, + 0,1,3,1,1,3,1,0, + 0,1,3,3,3,3,1,0, + 0,0,1,1,1,1,0,0, + 0,0,1,3,3,1,0,0, + 0,1,3,3,3,3,1,0, + 0,1,3,3,3,3,1,0 + ], + "knight": [ + 0,0,0,2,1,1,2,0, + 0,0,1,1,1,1,1,2, + 0,1,1,3,1,1,1,2, + 0,1,1,1,1,1,1,2, + 0,0,0,1,1,1,1,2, + 0,0,1,1,1,1,1,2, + 0,0,1,1,1,1,0,2, + 0,1,1,1,1,1,1,0 + ], + "rook": [ + 1,0,1,0,0,1,0,1, + 1,1,1,1,1,1,1,1, + 0,1,3,3,3,3,1,0, + 0,1,3,3,3,3,1,0, + 0,1,3,3,3,3,1,0, + 1,1,3,1,1,3,1,1, + 1,3,3,2,1,3,3,1, + 1,3,3,2,2,3,3,1 + ], + "queen": [ + 0,0,0,0,0,0,0,0, + 0,0,0,1,1,0,0,0, + 1,0,1,3,3,1,0,1, + 1,1,3,3,3,3,1,1, + 1,3,3,3,3,3,3,1, + 1,2,3,2,2,3,2,1, + 1,2,3,2,2,3,2,1, + 1,3,3,3,3,3,3,1 + ], + "king": [ + 1,0,0,0,0,0,0,1, + 1,1,0,0,0,0,1,1, + 1,3,1,0,0,1,3,1, + 1,3,3,1,1,3,3,1, + 1,3,3,3,3,3,3,1, + 1,3,3,3,3,3,3,1, + 1,3,3,3,3,3,3,1, + 1,3,3,3,3,3,3,1 + ], + move: [ + 0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0, + 0,0,0,1,1,0,0,0, + 0,0,1,1,1,1,0,0, + 0,0,1,1,1,1,0,0, + 0,0,0,1,1,0,0,0, + 0,0,0,0,0,0,0,0, + 0,0,0,0,0,0,0,0 + ] +} + +function checkBounds(x, y) { + return x >= 0 && x <= 7 && y >= 0 && y <= 7; +} + +class Piece { + constructor (color, type, position) { + this.color = color; + this.type = type; + this.position = position; + this.moved = false; + } + + moveTo(position) { + this.position = { + x: position[0], + y: position[1] + } + } + + draw(screen) { + for (let i = 0; i < 8; i++) { + for (let j = 0; j < 8; j++) { + const x = i + 1 + offsetX + screen.offsetX + this.position.x * screen.n; + const y = j + 1 + offsetY + screen.offsetY + this.position.y * screen.n; + if (sprites[this.type][j * 8 + i]) pixelMap[x][y].color = colorSettings[this.color][sprites[this.type][j * 8 + i]]; + } + } + if (screen.selected && screen.selected[0] == this.position.x && screen.selected[1] == this.position.y) { + const moves = this.getLegalMoves(screen.getBoard(), screen); + for (const move of moves) { + for (let i = 0; i < 8; i++) { + for (let j = 0; j < 8; j++) { + const x = move[0] * screen.n + screen.offsetX + offsetX + i + 1; + const y = move[1] * screen.n + screen.offsetY + offsetY + j + 1; + if (sprites.move[j * 8 + i]) pixelMap[x][y].color = "#dddddd"; + } + } + } + } + } + // -2 + // -1 + // origin + + getPossibleMoves(board, screen) { + const moves = []; + const direction = this.color == "black" ? 1 : -1; + const opponent = this.color == "black" ? "white" : "black"; + switch (this.type) { + case "pawn": + if (!board[this.position.x][this.position.y + direction]) { + moves.push([0, direction]); + if (!board[this.position.x][this.position.y + direction * 2] && !this.moved) moves.push([0, direction * 2, false, true]); + } + if (board[this.position.x - 1] && board[this.position.x - 1][this.position.y + direction] && board[this.position.x - 1][this.position.y + direction].color == opponent) { + moves.push([-1, direction]); + } + if (board[this.position.x + 1] && board[this.position.x + 1][this.position.y + direction] && board[this.position.x + 1][this.position.y + direction].color == opponent) { + moves.push([1, direction]); + } + // en crossaint + if (screen.enPassantTargetSquare != null && (this.position.x == screen.enPassantTargetSquare[0] - 1 || this.position.x == screen.enPassantTargetSquare[0] + 1) && this.position.y == screen.enPassantTargetSquare[1]) { + moves.push([screen.enPassantTargetSquare[0] - this.position.x, direction, false, false, true]); + } + break; + case "knight": { + const possibleMoves = [[-2, -1], [-2, 1], [-1, -2], [-1, 2], [1, -2], [1, 2], [2, -1], [2, 1]]; + for (const move of possibleMoves) { + const newX = move[0] + this.position.x; + const newY = move[1] + this.position.y; + if (newX >= 0 && newX < 8 && newY >= 0 && newY < 8 && (!board[newX][newY] || board[newX][newY].color == opponent)) moves.push(move); + } + break; + } + case "rook": { + for (const coord of adjacentCoords) { + let currentX = this.position.x + coord[0]; + let currentY = this.position.y + coord[1]; + while (checkBounds(currentX, currentY)) { + if (board[currentX][currentY]) { + if (board[currentX][currentY].color == opponent) moves.push([currentX - this.position.x, currentY - this.position.y]); + break; + } else moves.push([currentX - this.position.x, currentY - this.position.y]); + currentX += coord[0]; + currentY += coord[1]; + } + } + break; + } + case "bishop": { + for (let i = this.position.x - 1, j = this.position.y + 1; i >= 0 && j < 8; i--, j++) { + if (board[i][j]) { + if (board[i][j].color == opponent) moves.push([i - this.position.x, j - this.position.y]); + break; + } + moves.push([i - this.position.x, j - this.position.y]); + } + for (let i = this.position.x - 1, j = this.position.y - 1; i >= 0 && j >= 0; i--, j--) { + if (board[i][j]) { + if (board[i][j].color == opponent) moves.push([i - this.position.x, j - this.position.y]); + break; + } + moves.push([i - this.position.x, j - this.position.y]); + } + for (let i = this.position.x + 1, j = this.position.y + 1; i < 8 && j < 8; i++, j++) { + if (board[i][j]) { + if (board[i][j].color == opponent) moves.push([i - this.position.x, j - this.position.y]); + break; + } + moves.push([i - this.position.x, j - this.position.y]); + } + for (let i = this.position.x + 1, j = this.position.y - 1; i < 8 && j >= 0; i++, j--) { + if (board[i][j]) { + if (board[i][j].color == opponent) moves.push([i - this.position.x, j - this.position.y]); + break; + } + moves.push([i - this.position.x, j - this.position.y]); + } + break; + } + case "queen": { + for (const coord of squareCoords) { + let currentX = this.position.x + coord[0]; + let currentY = this.position.y + coord[1]; + while (checkBounds(currentX, currentY)) { + if (board[currentX][currentY]) { + if (board[currentX][currentY].color == opponent) moves.push([currentX - this.position.x, currentY - this.position.y]); + break; + } else moves.push([currentX - this.position.x, currentY - this.position.y]); + currentX += coord[0]; + currentY += coord[1]; + } + } + break; + } + case "king": { + const potentialMoves = []; + for (const coord of squareCoords) { + const x = coord[0] + this.position.x; + const y = coord[1] + this.position.y; + if (checkBounds(x, y) && (!board[x][y] || board[x][y].color == opponent)) { + potentialMoves.push([coord[0], coord[1]]); + } + } + + if (!this.moved) { + const ksRook = board[7][this.position.y]; + const qsRook = board[0][this.position.y]; + if (ksRook && ksRook.type == "rook" && ksRook.color == this.color && !ksRook.moved && !board[5][this.position.y] && screen.getAttackersAt([5, this.position.y], opponent).length == 0 && !board[6][this.position.y] && screen.getAttackersAt([6, this.position.y], opponent).length == 0) { + potentialMoves.push([2, 0, true]); + } + const cond = screen.getAttackersAt([1, this.position.y], opponent).length == 0 && screen.getAttackersAt([2, this.position.y], opponent).length == 0 && screen.getAttackersAt([3, this.position.y], opponent).length == 0; + if (qsRook && qsRook.type == "rook" && qsRook.color == this.color && !qsRook.moved && !board[1][this.position.y] && !board[2][this.position.y] && !board[3][this.position.y] && cond) { + potentialMoves.push([-2, 0, true]); + } + } + + for (const move of potentialMoves) { + if (screen.getAttackersAt([move[0] + this.position.x, move[1] + this.position.y], opponent).length == 0) { + moves.push(move); + } + } + } + } + return moves.map(m => [m[0] + this.position.x, m[1] + this.position.y, ...m.slice(2)]).filter(a => a[0] < 8 && a[0] >= 0 && a[1] < 8 && a[1] >= 0); + } + + getLegalMoves(board, screen) { + const possibleMoves = this.getPossibleMoves(board, screen); + if (this.type == "king") return possibleMoves; + const legalMoves = []; + for (const move of possibleMoves) { + const screen2 = screen.clone(); + const piece2 = screen2.pieces.find(p => p.position.x == this.position.x && p.position.y == this.position.y); + screen2.pieceMove(piece2, move, true, false); + if (!screen2.isKingAttacked(this.color)) { + legalMoves.push(move); + } + } + return legalMoves; + } + + handleSelection(pos, screen) { + const found = this.getLegalMoves(screen.getBoard(), screen).find(a => a[0] == pos[0] && a[1] == pos[1]); + if (found) { + screen.pieceMove(this, found); + this.moved = true; + return true + } else return false; + } +} + +const types = { + p: "pawn", + n: "knight", + b: "bishop", + r: "rook", + q: "queen", + k: "king" +} + +const loadFEN = (fen) => { + const split = fen.split(" ")[0].split("/"); + const pieces = []; + let posY = 0; + for (const part of split) { + let posX = 0; + for (const char of part.split("")) { + if (!isNaN(parseInt(char))) { + posX += parseInt(char); + continue; + } + const white = char.toUpperCase() == char; + const type = types[char.toLowerCase()]; + pieces.push(new Piece(white ? "white" : "black", type, { + x: posX, + y: posY + })) + posX++ + } + posY++; + } + return pieces; +} + +class UpgradeScreen extends GameMenu { + constructor (screen, width, height, color, piece) { + super("upgrade", screen, width, height); + this.color = color; + this.piece = piece; + this.current = -1; + } + + draw() { + const separator = 4; + const panelWidth = 12; + const pieces = ["queen", "rook", "bishop", "knight"]; + for (let p = 0; p < 4; p++) { + const iOffset = this.getOffsetX() + 2 + (panelWidth + separator) * p; + const jOffset = this.getOffsetY() + separator; + const hovered = mousePos.x >= iOffset && mousePos.x <= iOffset + panelWidth && mousePos.y >= jOffset && mousePos.y <= jOffset + 24; + if (hovered) this.current = p; + const color = this.color == "white" ? (hovered ? "#3c3c3c" : "#1e1e1e") : (hovered ? "#aaaaaa" : "#cccccc") + for (let i = this.getOffsetX() + 2 + (panelWidth + separator) * p; i < panelWidth * (p + 1) + separator * p + this.getOffsetX() + 2; i++) { + for (let j = this.getOffsetY() + separator; j < this.getOffsetY() + separator + 24; j++) { + const x = i - iOffset; + const y = j - jOffset; + if (x < 10 && x >= 2 && y < 12 && y >= 4) { + if (sprites[pieces[p]][(y - 4) * 8 + (x - 2)]) pixelMap[i][j].color = colorSettings[this.color][sprites[pieces[p]][(y - 4) * 8 + (x - 2)]]; + else pixelMap[i][j].color = color; + } else { + pixelMap[i][j].color = color; + } + } + } + } + super.draw(); + } + + onClick(_) { + if (this.current == -1) return; + this.screen.upgrade(this.piece, this.current); + } +} + +class WinScreen extends GameMenu { + constructor (screen, width, height, winningColor, timeout = false) { + super(`${winningColor} won`, screen, width, height); + this.winningColor = winningColor; + this.timeout = timeout; + const button = new GuiButton(1, 10, Math.ceil(width - 2), 20, "reset", "#555555", this); + button.onClick(() => { + screen.resetBoard(); + }) + this.addButtons(button); + } + + draw() { + for (let i = this.getOffsetX(); i < this.width + this.getOffsetX(); i++) { + for (let j = this.getOffsetY(); j < this.height + this.getOffsetY(); j++) { + pixelMap[i][j].color = "#ffffff"; + } + } + TextRenderer.drawCenteredText(`${this.winningColor} won ${this.timeout ? "on time" : "by checkmate"}`, Math.floor(this.getOffsetX() + this.width / 2), this.getOffsetY() + 1, "#000000") + super.draw(); + } + + onClick(_) { + if (mousePos.x >= this.buttons[0].x + && mousePos.x <= this.buttons[0].x + this.buttons[0].width + && mousePos.y >= this.buttons[0].y + && mousePos.y <= this.buttons[0].y + this.buttons[0].height) this.buttons[0].click(); + } +} + +class DrawScreen extends GameMenu { + constructor (screen, width, height, type) { + super(`Game drawn`, screen, width, height); + this.type = type; + const button = new GuiButton(1, 10, Math.ceil(width - 2), 20, "reset", "#555555", this); + button.onClick(() => { + screen.resetBoard(); + }) + this.addButtons(button); + } + + draw() { + for (let i = this.getOffsetX(); i < this.width + this.getOffsetX(); i++) { + for (let j = this.getOffsetY(); j < this.height + this.getOffsetY(); j++) { + pixelMap[i][j].color = "#ffffff"; + } + } + const types = { + 0: "50-move rule", + 1: "insuff. mat.", + 2: "stalemate", + 3: "repetition" + } + TextRenderer.drawCenteredText(`Draw by ${types[this.type]}`, Math.floor(this.getOffsetX() + this.width / 2), this.getOffsetY() + 1, "#000000") + super.draw(); + } + + onClick(_) { + if (mousePos.x >= this.buttons[0].x + && mousePos.x <= this.buttons[0].x + this.buttons[0].width + && mousePos.y >= this.buttons[0].y + && mousePos.y <= this.buttons[0].y + this.buttons[0].height) this.buttons[0].click(); + } +} + +const pieceValue = { + "pawn": 1, + "knight": 3, + "bishop": 3, + "rook": 5, + "queen": 9, + "king": Infinity +} + +class GameScreen { + constructor () { + this.selected = null; + this.pieces = loadFEN("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"); + this.castlingWhite = true; + this.castlingBlack = true; + this.capturedWhite = []; + this.capturedBlack = []; + this.nonActionMoves = 0; + this.positions = []; + this.enPassantTargetSquare = null; + // true = white + this.turn = true; + this.chessClock = { + white: 60 * 5, + black: 60 * 5 + } + this.inCheck = null; + this.positions.push(this.positionToHash()); + } + + clock() { + this.fps = this.currentFrames; + this.currentFrames = 0; + if (!this.paused) this.turn ? this.chessClock.white-- : this.chessClock.black--; + if (!this.paused && (this.turn ? this.chessClock.white : this.chessClock.black) <= 0) { + this.timeout(this.turn ? "black" : "white"); + } + } + + clear() { + for (let i = offsetX; i < width - offsetX; i++) { + for (let j = offsetY; j < height - offsetY; j++) { + pixelMap[i][j].color = "#000000"; + } + } + } + + pause() { + this.paused = true; + } + + unpause() { + this.paused = false; + } + + draw() { + if (!this.boardSize) { + this.boardSize = closestDivisible(Math.min(width, height), 8); + this.offsetX = (width - this.boardSize) / 2; + this.offsetY = (height - this.boardSize) / 2; + this.n = this.boardSize / 8; + } + if (!this.currentFrames) this.currentFrames = 0; + this.currentFrames++; + this.clear(); + this.drawBoard(); + this.drawPieces(); + this.drawCaptured(); + TextRenderer.drawText(`${Math.floor(this.chessClock.white / 60)}:${(this.chessClock.white % 60).toString().padStart(2, "0")}`, 2, 2, "#ffffff"); + TextRenderer.drawText(`${Math.floor(this.chessClock.black / 60)}:${(this.chessClock.black % 60).toString().padStart(2, "0")}`, width - offsetX - 2 - TextRenderer.getStringWidth(`${Math.floor(this.chessClock.black / 60)}:${(this.chessClock.black % 60).toString().padStart(2, "0")}`), 2, "#ffffff"); + if (this.menuScreen) { + this.drawMenu(); + } + } + + drawBoard() { + GuiUtils.drawOutline(this.offsetX + offsetY - 1, this.offsetY + offsetY - 1, width - this.offsetX - offsetX + 1, height - this.offsetY - offsetY + 1, "#ff0000"); + // totally not confusing + for (let i = this.offsetX + offsetX; i < width - this.offsetX - offsetX; i++) { + for (let j = this.offsetY + offsetY; j < height - this.offsetY - offsetY; j++) { + const x = Math.floor((i - this.offsetX - offsetX) / this.n); + const y = Math.floor((j - this.offsetY - offsetY) / this.n); + if (this.pieces.find(p => p.position.x == x && p.position.y == y && p.type == "king" && this.inCheck == p.color)) { + pixelMap[i][j].color = "#ff0000"; + } else if (this.selected && this.selected[0] == x && this.selected[1] == y) { + pixelMap[i][j].color = "#ffff00"; + } else if (xor(x % 2 != 0, y % 2 != 0)) { + pixelMap[i][j].color = colorSettings.color2; + } else { + pixelMap[i][j].color = colorSettings.color1; + } + } + } + } + + drawPieces() { + for (const piece of this.pieces) { + piece.draw(this); + } + } + + drawCaptured() { + const whitePieces = [...new Set(this.capturedWhite.sort((a, b) => pieceValue[b.type] - pieceValue[a.type]).map(a => a.type))]; + const blackPieces = [...new Set(this.capturedBlack.sort((a, b) => pieceValue[b.type] - pieceValue[a.type]).map(a => a.type))]; + for (let k = 0; k < whitePieces.length; k++) { + const piece = whitePieces[k]; + for (let i = 0; i < 8; i++) { + for (let j = 0; j < 8; j++) { + const x = i - 11 + offsetX + this.offsetX; + const y = j + 1 + offsetY + this.offsetY + k * 9; + if (sprites[piece][j * 8 + i]) pixelMap[x][y].color = colorSettings.white[sprites[piece][j * 8 + i]]; + } + } + const amount = this.capturedWhite.filter(p => p.type == piece); + if (amount > 1) TextRenderer.drawText(offsetX + this.offsetX - 11, 1 + offsetY + this.offsetY + (k + 1) * 9); + } + for (let k = 0; k < blackPieces.length; k++) { + const piece = blackPieces[k]; + for (let i = 0; i < 8; i++) { + for (let j = 0; j < 8; j++) { + const x = i + 3 + offsetX + this.boardSize + this.offsetX; + const y = j + 1 + offsetY + this.offsetY + k * 9; + if (sprites[piece][j * 8 + i]) pixelMap[x][y].color = colorSettings.black[sprites[piece][j * 8 + i]]; + } + } + const amount = this.capturedBlack.filter(p => p.type == piece).length; + if (amount > 1) TextRenderer.drawText(`${amount}`, 12 + offsetX + this.boardSize + this.offsetX, 2 + offsetY + this.offsetY + k * 9, "#ffffff"); + } + } + + openUpgradeScreen(piece) { + this.menuScreen = new UpgradeScreen(this, 64, 28, piece.color, piece); + } + + upgrade(piece, newType) { + const types = ["queen", "rook", "bishop", "knight"] + piece.type = types[newType]; + this.pieces.find(p => p.position.x == piece.position.x && p.position.y == piece.position.y).type = types[newType]; + this.menuScreen = null; + const kings = { + white: this.pieces.find(p => p.type == "king" && p.color == "white"), + black: this.pieces.find(p => p.type == "king" && p.color == "black") + } + let checked = ""; + for (const king in kings) { + const isChecked = this.isKingAttacked(king); + if (isChecked) { + checked = king; + this.nonActionMoves = 0; + this.inCheck = king; + const isCheckmate = this.checkForCheckmate(king); + if (isCheckmate) this.checkmate(king == "white" ? "black" : "white"); + } + } + if (!checked && this.inCheck) this.inCheck = null; + } + + handleCastling(piece, pos) { + this.turn = !this.turn; + piece.moveTo([pos[0], pos[1]]); + this.pieces.find(p => p.color == piece.color && p.type == "rook" && p.position.y == piece.position.y && p.position.x == (pos[0] == 6 ? 7 : 0)).moveTo([pos[0] == 6 ? 5 : 3, pos[1]]); + } + + pieceMove(piece, pos, ignoreChecks = false, checkForDraw = true) { + // castling + if (pos[2]) return this.handleCastling(piece, pos); + const captured = this.pieces.find(p => p.position.x == pos[0] && p.position.y == pos[1]) ?? ((pos[4] && !!this.enPassantTargetSquare) ? this.pieces.find(p => p.position.x == this.enPassantTargetSquare[0] && p.position.y == this.enPassantTargetSquare[1] && p.color != piece.color && p.type == "pawn") : null); + if (!captured) { + this.turn = !this.turn; + piece.moveTo(pos); + this.nonActionMoves++; + if (piece.type == "pawn" && ((piece.color == "white" && piece.position.y == 0) || (piece.color == "black" && piece.position.y == 7))) { + this.openUpgradeScreen(piece); + } + } else if (captured.type != "king") { + if (piece.color == "white") this.capturedBlack.push(captured); + else this.capturedWhite.push(captured); + this.nonActionMoves = 0; + this.pieces = this.pieces.filter(p => p.position.x != captured.position.x || p.position.y != captured.position.y) + piece.moveTo(pos); + this.turn = !this.turn; + if (piece.type == "pawn" && ((piece.color == "white" && piece.position.y == 0) || (piece.color == "black" && piece.position.y == 7))) { + this.openUpgradeScreen(piece); + } + } + if (pos[3]) { + this.enPassantTargetSquare = pos; + // reset after every move that doesnt allow en passant + } else if (this.enPassantTargetSquare || pos[4]) this.enPassantTargetSquare = null; + const kings = { + white: this.pieces.find(p => p.type == "king" && p.color == "white"), + black: this.pieces.find(p => p.type == "king" && p.color == "black") + } + let checked = ""; + if (!ignoreChecks) { + for (const king in kings) { + const isChecked = this.isKingAttacked(king); + if (isChecked) { + checked = king; + this.nonActionMoves = 0; + this.inCheck = king; + const isCheckmate = this.checkForCheckmate(king); + if (isCheckmate) this.checkmate(king == "white" ? "black" : "white"); + } + } + } + if (!checked && this.inCheck) this.inCheck = null; + if (checkForDraw) { + this.positions.push(this.positionToHash()); + const draw = this.checkForDraw(); + if (draw[0]) this._draw(draw[1]); + } + } + + positionToHash() { + const board = this.getBoard(); + const pieceTypes = { + "pawn": BigInt(1 << 0), + "knight": BigInt(1 << 1), + "bishop": BigInt(1 << 2), + "rook": BigInt(1 << 3), + "queen": BigInt(1 << 4), + "king": BigInt(1 << 5), + } + const colors = { + "white": BigInt(1 << 6), + "black": BigInt(1 << 7) + } + let hash = BigInt(0); + let flag = false; + for (let i = 0; i < 8; i++) { + for (let j = 0; j < 8; j++) { + const piece = board[i][j]; + if (flag) hash <<= BigInt(8); + if (!piece) { + hash >>= BigInt(7); + hash |= BigInt(0); + } + else hash |= (colors[piece.color] | pieceTypes[piece.type]); + flag = true; + } + } + return hash; + } + + isKingAttacked(color) { + const king = this.pieces.find(p => p.type == "king" && p.color == color); + const pos = [king.position.x, king.position.y]; + const attackers = this.getAttackersAt(pos, color == "white" ? "black" : "white"); + return attackers.length > 0; + } + + // deep copies in javascript are so fucking annoying + clone() { + const newScreen = new GameScreen(); + newScreen.selected = structuredClone(this.selected); + newScreen.pieces = structuredClone(this.pieces); + newScreen.pieces.forEach(p => Object.setPrototypeOf(p, Piece.prototype)); + newScreen.enPassantTargetSquare = structuredClone(this.enPassantTargetSquare); + // true = white + newScreen.turn = structuredClone(this.turn); + newScreen.chessClock = structuredClone(this.chessClock); + newScreen.inCheck = structuredClone(this.inCheck); + return newScreen; + } + + resetBoard() { + this.menuScreen = null; + this.selected = null; + this.pieces = loadFEN("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"); + this.castlingWhite = true; + this.castlingBlack = true; + this.capturedWhite = []; + this.capturedBlack = []; + this.nonActionMoves = 0; + this.positions = []; + this.enPassantTargetSquare = null; + // true = white + this.turn = true; + this.chessClock = { + white: 60 * 5, + black: 60 * 5 + } + this.inCheck = null; + this.unpause(); + } + + timeout(winningColor) { + this.menuScreen = new WinScreen(this, 0.85 * screenWidth, 0.5 * screenHeight, winningColor, true); + this.pause(); + } + + checkmate(winningColor) { + this.menuScreen = new WinScreen(this, 0.85 * screenWidth, 0.5 * screenHeight, winningColor); + this.pause(); + } + + _draw(type) { + this.menuScreen = new DrawScreen(this, 0.85 * screenWidth, 0.5 * screenHeight, type); + this.pause(); + } + + checkForDraw() { + // 50-move rule + if (this.nonActionMoves >= 100) return [true, 0]; + const whitePieces = this.pieces.filter(p => p.color == "white"); + const blackPieces = this.pieces.filter(p => p.color == "black"); + const cond1 = whitePieces.length == 1 || (whitePieces.length == 2 && whitePieces.find(p => p.type == "rook" || p.type == "bishop")); + const cond2 = blackPieces.length == 1 || (blackPieces.length == 2 && blackPieces.find(p => p.type == "rook" || p.type == "bishop")); + // insufficient material + if (cond1 && cond2) return [true, 1]; + // stalemate + if (this.pieces.filter(p => p.color == this.turn ? "white" : "black" && p.getLegalMoves(this.getBoard(), this).length > 0).length == 0 && (this.inCheck != this.turn ? "white" : "black")) return [true, 2]; + // threefold repetition + if (this.positions.filter(p => p == this.positionToHash()).length >= 3) return [true, 3]; + return [false]; + } + + checkForCheckmate(color) { + if (!this.isKingAttacked(color)) return false; + const pieces = this.pieces.filter(p => p.color == color); + const savingMoves = []; + for (const piece of pieces) { + for (const move of piece.getPossibleMoves(this.getBoard(), this)) { + const screen = this.clone(); + const piece2 = screen.pieces.find(p => p.position.x == piece.position.x && p.position.y == piece.position.y); + screen.pieceMove(piece2, move, true, false); + if (!screen.isKingAttacked(color)) { + savingMoves.push([piece, move]); + } + } + } + return savingMoves.length == 0; + } + + getBoard() { + const board = []; + for (let i = 0; i < 8; i++) { + board[i] = []; + for (let j = 0; j < 8; j++) { + board[i][j] = this.pieces.find(p => p.position.x == i && p.position.y == j) ?? null; + } + } + return board; + } + + getAttackersAt(pos, color) { + const attackers = []; + for (const piece of this.pieces.filter(p => p.color == color && p.type != "king")) { + if (piece.getPossibleMoves(this.getBoard(), this).find(m => m[0] == pos[0] && m[1] == pos[1])) attackers.push(piece); + } + return attackers; + } + + drawMenu() { + this.drawOverlay(); + GuiUtils.drawRect(this.menuScreen.getOffsetX(), Math.floor((screenHeight - this.menuScreen.height) / 2) - 2, this.menuScreen.getOffsetX() + this.menuScreen.width, this.menuScreen.getOffsetY(), "#1e1e1e"); + TextRenderer.drawCenteredText(this.menuScreen.name, (this.menuScreen.width / 2) + this.menuScreen.getOffsetX(), Math.floor((screenHeight - this.menuScreen.height) / 2) - 1, "#ffffff"); + this.menuScreen.draw(); + } + + drawOverlay() { + for (let i = offsetX; i < width - offsetX; i++) { + for (let j = offsetY; j < height - offsetY; j++) { + pixelMap[i][j].color = "#" + colorLerp(pixelMap[i][j].color, "#000000", 0.5); + } + } + } + + onKey(ev) { + if (!this.menuScreen) { + player.update(ev); + } else { + this.menuScreen.onKey(ev); + } + } + + onMouseClick(ev) { + if (outOfBounds(mousePos.x, mousePos.y)) return; + if (!this.offsetX || !this.offsetY) return; + if (this.menuScreen) this.menuScreen.onClick(ev); + if (mousePos.x <= offsetX + this.offsetX || mousePos.x >= offsetX + this.offsetX + this.boardSize || mousePos.y <= offsetY + this.offsetY || mousePos.y >= offsetY + this.offsetY + this.boardSize) return; + const newPos = [Math.floor((mousePos.x - this.offsetX - offsetX) / this.n), Math.floor((mousePos.y - this.offsetY - offsetY) / this.n)]; + if (!this.selected) { + const piece = this.pieces.find(p => p.position.x == newPos[0] && p.position.y == newPos[1]); + if (piece && ((piece.color == "white" && this.turn) || (piece.color == "black" && !this.turn))) this.selected = newPos; + } else { + const piece = this.pieces.find(p => p.position.x == this.selected[0] && p.position.y == this.selected[1]); + if (piece && ((piece.color == "white" && this.turn) || (piece.color == "black" && !this.turn))) { + piece.handleSelection(newPos, this); + this.selected = null; + } else this.selected = null; + } + } +} + +const game = new GameScreen(); + +setInterval(cellTick, (1000/(tps*4))); +setInterval(() => {game.clock()}, 1000); + +function cellTick() { + if (running && !paused) { + game.draw(); + } +} + +window.addEventListener("keydown", (ev) => { + if (ev.key == "u") { + if (!running) { + videoFrame = 0; + + for (let i = offsetX; i < width - offsetX; i++) { + for (let j = offsetY; j < height - offsetY; j++) { + if (pixelMap[i][j]) deletePixel(i, j); + createPixel("screen", i, j); + } + } + } + running = !running; + game.resetBoard(); + } else { + game.onKey(ev); + } +}) + +window.addEventListener("mousedown", (ev) => { + game.onMouseClick(ev); +})