{ 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) { 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(); } } runAfterLoadList.push(() => { if (!localStorage.getItem("chessjs-tutorial")) { // "might break" // i know damn well it will 100% break on mobile alert("To start or restart chess board press 'u'. Might break on different resolutions and on mobile."); localStorage.setItem("chessjs-tutorial", true); } }) window.addEventListener("keydown", (ev) => { if (ev.key == "u") { if (!running) { 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 = true; } game.resetBoard(); } else { game.onKey(ev); } }) window.addEventListener("mousedown", (ev) => { game.onMouseClick(ev); }) }