1463 lines
47 KiB
JavaScript
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
|
|
promptText("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);
|
|
})
|
|
} |