sandboxels/mods/chess.js

1463 lines
47 KiB
JavaScript

{
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);
})
}