@journey-to-code/games 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/dist/backgammon.d.ts +146 -0
- package/dist/backgammon.js +1221 -0
- package/dist/backgammon.js.map +1 -0
- package/dist/blackjack.d.ts +66 -0
- package/dist/blackjack.js +452 -0
- package/dist/blackjack.js.map +1 -0
- package/dist/bluff.d.ts +60 -0
- package/dist/bluff.js +239 -0
- package/dist/bluff.js.map +1 -0
- package/dist/board.d.ts +387 -0
- package/dist/board.js +105 -0
- package/dist/board.js.map +1 -0
- package/dist/bridge.d.ts +86 -0
- package/dist/bridge.js +494 -0
- package/dist/bridge.js.map +1 -0
- package/dist/canasta.d.ts +88 -0
- package/dist/canasta.js +646 -0
- package/dist/canasta.js.map +1 -0
- package/dist/checkers.d.ts +108 -0
- package/dist/checkers.js +756 -0
- package/dist/checkers.js.map +1 -0
- package/dist/chess.d.ts +162 -0
- package/dist/chess.js +904 -0
- package/dist/chess.js.map +1 -0
- package/dist/chesscore.d.ts +136 -0
- package/dist/chesscore.js +1121 -0
- package/dist/chesscore.js.map +1 -0
- package/dist/combat.d.ts +102 -0
- package/dist/combat.js +772 -0
- package/dist/combat.js.map +1 -0
- package/dist/contractrummy.d.ts +77 -0
- package/dist/contractrummy.js +430 -0
- package/dist/contractrummy.js.map +1 -0
- package/dist/crazy8.d.ts +65 -0
- package/dist/crazy8.js +366 -0
- package/dist/crazy8.js.map +1 -0
- package/dist/cribbage.d.ts +75 -0
- package/dist/cribbage.js +428 -0
- package/dist/cribbage.js.map +1 -0
- package/dist/cuckoo.d.ts +43 -0
- package/dist/cuckoo.js +216 -0
- package/dist/cuckoo.js.map +1 -0
- package/dist/drinkingpyramid.d.ts +54 -0
- package/dist/drinkingpyramid.js +213 -0
- package/dist/drinkingpyramid.js.map +1 -0
- package/dist/drinks.d.ts +25 -0
- package/dist/drinks.js +40 -0
- package/dist/drinks.js.map +1 -0
- package/dist/dropfour.d.ts +103 -0
- package/dist/dropfour.js +577 -0
- package/dist/dropfour.js.map +1 -0
- package/dist/durak.d.ts +58 -0
- package/dist/durak.js +332 -0
- package/dist/durak.js.map +1 -0
- package/dist/euchre.d.ts +80 -0
- package/dist/euchre.js +453 -0
- package/dist/euchre.js.map +1 -0
- package/dist/fivedraw.d.ts +50 -0
- package/dist/fivedraw.js +289 -0
- package/dist/fivedraw.js.map +1 -0
- package/dist/fivehundred.d.ts +90 -0
- package/dist/fivehundred.js +504 -0
- package/dist/fivehundred.js.map +1 -0
- package/dist/freecell.d.ts +50 -0
- package/dist/freecell.js +327 -0
- package/dist/freecell.js.map +1 -0
- package/dist/fruitsalad.d.ts +69 -0
- package/dist/fruitsalad.js +427 -0
- package/dist/fruitsalad.js.map +1 -0
- package/dist/gin.d.ts +72 -0
- package/dist/gin.js +393 -0
- package/dist/gin.js.map +1 -0
- package/dist/go.d.ts +132 -0
- package/dist/go.js +659 -0
- package/dist/go.js.map +1 -0
- package/dist/goboard.d.ts +218 -0
- package/dist/goboard.js +880 -0
- package/dist/goboard.js.map +1 -0
- package/dist/gofish.d.ts +37 -0
- package/dist/gofish.js +235 -0
- package/dist/gofish.js.map +1 -0
- package/dist/golf.d.ts +64 -0
- package/dist/golf.js +343 -0
- package/dist/golf.js.map +1 -0
- package/dist/handfoot.d.ts +86 -0
- package/dist/handfoot.js +515 -0
- package/dist/handfoot.js.map +1 -0
- package/dist/hearts.d.ts +65 -0
- package/dist/hearts.js +390 -0
- package/dist/hearts.js.map +1 -0
- package/dist/holdem.d.ts +45 -0
- package/dist/holdem.js +286 -0
- package/dist/holdem.js.map +1 -0
- package/dist/horserace.d.ts +49 -0
- package/dist/horserace.js +207 -0
- package/dist/horserace.js.map +1 -0
- package/dist/index.d.ts +65 -0
- package/dist/index.js +66 -0
- package/dist/index.js.map +1 -0
- package/dist/kingscorner.d.ts +59 -0
- package/dist/kingscorner.js +374 -0
- package/dist/kingscorner.js.map +1 -0
- package/dist/kingscup.d.ts +44 -0
- package/dist/kingscup.js +185 -0
- package/dist/kingscup.js.map +1 -0
- package/dist/kit.d.ts +283 -0
- package/dist/kit.js +73 -0
- package/dist/kit.js.map +1 -0
- package/dist/klondike.d.ts +57 -0
- package/dist/klondike.js +314 -0
- package/dist/klondike.js.map +1 -0
- package/dist/liarspoker.d.ts +61 -0
- package/dist/liarspoker.js +298 -0
- package/dist/liarspoker.js.map +1 -0
- package/dist/memory.d.ts +36 -0
- package/dist/memory.js +181 -0
- package/dist/memory.js.map +1 -0
- package/dist/minigolf.d.ts +104 -0
- package/dist/minigolf.js +724 -0
- package/dist/minigolf.js.map +1 -0
- package/dist/minigolfcourse.d.ts +127 -0
- package/dist/minigolfcourse.js +470 -0
- package/dist/minigolfcourse.js.map +1 -0
- package/dist/offers.d.ts +136 -0
- package/dist/offers.js +279 -0
- package/dist/offers.js.map +1 -0
- package/dist/ohhell.d.ts +66 -0
- package/dist/ohhell.js +331 -0
- package/dist/ohhell.js.map +1 -0
- package/dist/oldmaid.d.ts +40 -0
- package/dist/oldmaid.js +186 -0
- package/dist/oldmaid.js.map +1 -0
- package/dist/pinochle.d.ts +94 -0
- package/dist/pinochle.js +466 -0
- package/dist/pinochle.js.map +1 -0
- package/dist/pitch.d.ts +77 -0
- package/dist/pitch.js +362 -0
- package/dist/pitch.js.map +1 -0
- package/dist/poker.d.ts +79 -0
- package/dist/poker.js +268 -0
- package/dist/poker.js.map +1 -0
- package/dist/president.d.ts +56 -0
- package/dist/president.js +339 -0
- package/dist/president.js.map +1 -0
- package/dist/pyramid.d.ts +39 -0
- package/dist/pyramid.js +200 -0
- package/dist/pyramid.js.map +1 -0
- package/dist/registry.d.ts +17 -0
- package/dist/registry.js +70 -0
- package/dist/registry.js.map +1 -0
- package/dist/reversi.d.ts +69 -0
- package/dist/reversi.js +584 -0
- package/dist/reversi.js.map +1 -0
- package/dist/ridethebus.d.ts +48 -0
- package/dist/ridethebus.js +257 -0
- package/dist/ridethebus.js.map +1 -0
- package/dist/rummy500.d.ts +89 -0
- package/dist/rummy500.js +474 -0
- package/dist/rummy500.js.map +1 -0
- package/dist/sevens.d.ts +48 -0
- package/dist/sevens.js +221 -0
- package/dist/sevens.js.map +1 -0
- package/dist/slapjack.d.ts +42 -0
- package/dist/slapjack.js +224 -0
- package/dist/slapjack.js.map +1 -0
- package/dist/speed.d.ts +44 -0
- package/dist/speed.js +201 -0
- package/dist/speed.js.map +1 -0
- package/dist/spider.d.ts +51 -0
- package/dist/spider.js +320 -0
- package/dist/spider.js.map +1 -0
- package/dist/spite.d.ts +75 -0
- package/dist/spite.js +372 -0
- package/dist/spite.js.map +1 -0
- package/dist/sport.d.ts +266 -0
- package/dist/sport.js +48 -0
- package/dist/sport.js.map +1 -0
- package/dist/switch.d.ts +69 -0
- package/dist/switch.js +393 -0
- package/dist/switch.js.map +1 -0
- package/dist/table.d.ts +284 -0
- package/dist/table.js +67 -0
- package/dist/table.js.map +1 -0
- package/dist/talk.d.ts +17 -0
- package/dist/talk.js +44 -0
- package/dist/talk.js.map +1 -0
- package/dist/thirtyone.d.ts +66 -0
- package/dist/thirtyone.js +372 -0
- package/dist/thirtyone.js.map +1 -0
- package/dist/tictactoe.d.ts +68 -0
- package/dist/tictactoe.js +541 -0
- package/dist/tictactoe.js.map +1 -0
- package/dist/tricks.d.ts +21 -0
- package/dist/tricks.js +48 -0
- package/dist/tricks.js.map +1 -0
- package/dist/war.d.ts +44 -0
- package/dist/war.js +185 -0
- package/dist/war.js.map +1 -0
- package/dist/whist.d.ts +52 -0
- package/dist/whist.js +218 -0
- package/dist/whist.js.map +1 -0
- package/dist/yacht.d.ts +96 -0
- package/dist/yacht.js +653 -0
- package/dist/yacht.js.map +1 -0
- package/package.json +41 -4
- package/README.md +0 -3
package/dist/chess.js
ADDED
|
@@ -0,0 +1,904 @@
|
|
|
1
|
+
import { settleMatch, withOffers } from "./offers.js";
|
|
2
|
+
import { cellName, chessCoords } from "./board.js";
|
|
3
|
+
import { BISHOP, BLACK, FLAG_CASTLE_K, FLAG_CASTLE_Q, FLAG_DOUBLE, FLAG_EP, KING, KNIGHT, MATE, PAWN, Position, QUEEN, ROOK, WHITE, cellOf, evaluate, moveFlag, moveFrom, movePromo, moveTo, perft, positionKey, think } from "./chesscore.js";
|
|
4
|
+
import { careless, GameError, markReady, nextHandChoices, nextRandom, thinkFor, who } from "./table.js";
|
|
5
|
+
const base = { variant: "classic", white: "random", minutes: 0, increment: 0, days: 0, hints: true, games: 1 };
|
|
6
|
+
export const classicChess = { id: "chess-classic", name: "Classic", ...base };
|
|
7
|
+
export const chess960 = { id: "chess-960", name: "Chess960", ...base, variant: "960" };
|
|
8
|
+
export const bulletChess = { id: "chess-bullet", name: "Bullet 1+0", ...base, minutes: 1 };
|
|
9
|
+
export const blitzChess = { id: "chess-blitz", name: "Blitz 3+2", ...base, minutes: 3, increment: 2 };
|
|
10
|
+
export const blitz5Chess = { id: "chess-blitz5", name: "Blitz 5+0", ...base, minutes: 5 };
|
|
11
|
+
export const rapidChess = { id: "chess-rapid", name: "Rapid 10+0", ...base, minutes: 10 };
|
|
12
|
+
export const rapid15Chess = { id: "chess-rapid15", name: "Rapid 15+10", ...base, minutes: 15, increment: 10 };
|
|
13
|
+
export const correspondenceChess = { id: "chess-correspondence", name: "Correspondence (a day a move)", ...base, days: 1 };
|
|
14
|
+
const PRESETS = [classicChess, chess960, bulletChess, blitzChess, blitz5Chess, rapidChess, rapid15Chess, correspondenceChess];
|
|
15
|
+
const GAMES = [1, 2, 3, 4, 6];
|
|
16
|
+
const DAYS = [0, 1, 2, 3, 7];
|
|
17
|
+
const DAY_MS = 86_400_000;
|
|
18
|
+
const LETTERS = ["p", "n", "b", "r", "q", "k"];
|
|
19
|
+
const CODE = { p: PAWN, n: KNIGHT, b: BISHOP, r: ROOK, q: QUEEN, k: KING };
|
|
20
|
+
/** By the move generator's number for a kind (1 is a pawn). */
|
|
21
|
+
const LETTER_OF = ["p", "p", "n", "b", "r", "q", "k"];
|
|
22
|
+
const KIND = { p: "pawn", n: "knight", b: "bishop", r: "rook", q: "queen", k: "king" };
|
|
23
|
+
const GLYPH = { p: ["♙", "♟"], n: ["♘", "♞"], b: ["♗", "♝"], r: ["♖", "♜"], q: ["♕", "♛"], k: ["♔", "♚"] };
|
|
24
|
+
const POINTS = { p: 1, n: 3, b: 3, r: 5, q: 9, k: 0 };
|
|
25
|
+
const COLOUR = ["white", "black"];
|
|
26
|
+
const other = (seat) => 1 - seat;
|
|
27
|
+
/**
|
|
28
|
+
* A state to change, leaving the one given as it was. Pieces and recorded moves are never changed
|
|
29
|
+
* once made (a promoted pawn is a new piece with the old one's number, and the record only grows),
|
|
30
|
+
* so the copy shares them: a long game is many moves to copy otherwise, on every move tried.
|
|
31
|
+
*/
|
|
32
|
+
function copy(state) {
|
|
33
|
+
return { ...state, cells: state.cells.slice(), castling: state.castling.slice(), seen: { ...state.seen }, taken: state.taken.slice(), bank: state.bank ? state.bank.slice() : null, wins: state.wins.slice(), ready: state.ready.slice() };
|
|
34
|
+
}
|
|
35
|
+
const seatOf = (s, colour) => (colour === WHITE ? s.white : other(s.white));
|
|
36
|
+
const colourOf = (s, seat) => (seat === s.white ? WHITE : BLACK);
|
|
37
|
+
const GRID = { cols: 8, coords: chessCoords(8, 8) };
|
|
38
|
+
const at = (cell) => cellName(GRID, cell);
|
|
39
|
+
// ---- Starting rows, and FEN ---------------------------------------------------------------------
|
|
40
|
+
/**
|
|
41
|
+
* Chess960's starting row number `n` (0 to 959), a8 to h8, by the usual numbering: the bishops go
|
|
42
|
+
* on opposite colours, then the queen, then the knights, and the king goes between the rooks.
|
|
43
|
+
* 518 is the classic row.
|
|
44
|
+
*/
|
|
45
|
+
export function chess960Row(n) {
|
|
46
|
+
const row = Array(8).fill(null);
|
|
47
|
+
const free = () => row.flatMap((p, file) => (p ? [] : [file]));
|
|
48
|
+
row[(n % 4) * 2 + 1] = "b";
|
|
49
|
+
n = Math.floor(n / 4);
|
|
50
|
+
row[(n % 4) * 2] = "b";
|
|
51
|
+
n = Math.floor(n / 4);
|
|
52
|
+
row[free()[n % 6]] = "q";
|
|
53
|
+
n = Math.floor(n / 6);
|
|
54
|
+
const [a, b] = [[0, 1], [0, 2], [0, 3], [0, 4], [1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]][n % 10];
|
|
55
|
+
const left = free();
|
|
56
|
+
row[left[a]] = "n";
|
|
57
|
+
row[left[b]] = "n";
|
|
58
|
+
["r", "k", "r"].forEach((p, i) => (row[free()[0]] = p));
|
|
59
|
+
return row;
|
|
60
|
+
}
|
|
61
|
+
/** Read a FEN. Castling may be written KQkq, or with the rooks' files (HAha) as Chess960 needs. Throws a GameError for one that isn't a position. */
|
|
62
|
+
export function parseFen(fen) {
|
|
63
|
+
const bad = () => new GameError("That isn't a chess position (FEN).");
|
|
64
|
+
const [place, side = "w", castle = "-", ep = "-", half = "0", full = "1"] = String(fen).trim().split(/\s+/);
|
|
65
|
+
const rows = (place ?? "").split("/");
|
|
66
|
+
if (rows.length !== 8)
|
|
67
|
+
throw bad();
|
|
68
|
+
const cells = [];
|
|
69
|
+
for (const row of rows) {
|
|
70
|
+
const before = cells.length;
|
|
71
|
+
for (const ch of row) {
|
|
72
|
+
if (/[1-8]/.test(ch))
|
|
73
|
+
for (let i = 0; i < Number(ch); i++)
|
|
74
|
+
cells.push(null);
|
|
75
|
+
else if (/[pnbrqk]/i.test(ch))
|
|
76
|
+
cells.push({ c: ch === ch.toLowerCase() ? BLACK : WHITE, k: ch.toLowerCase() });
|
|
77
|
+
else
|
|
78
|
+
throw bad();
|
|
79
|
+
}
|
|
80
|
+
if (cells.length - before !== 8)
|
|
81
|
+
throw bad();
|
|
82
|
+
}
|
|
83
|
+
for (const c of [WHITE, BLACK])
|
|
84
|
+
if (cells.filter((p) => p?.k === "k" && p.c === c).length !== 1)
|
|
85
|
+
throw bad();
|
|
86
|
+
if (side !== "w" && side !== "b")
|
|
87
|
+
throw bad();
|
|
88
|
+
const castling = [-1, -1, -1, -1];
|
|
89
|
+
for (const ch of castle === "-" ? "" : castle) {
|
|
90
|
+
const c = ch === ch.toLowerCase() ? BLACK : WHITE;
|
|
91
|
+
const row = c === WHITE ? 56 : 0;
|
|
92
|
+
const king = cells.findIndex((p, cell) => cell >= row && cell < row + 8 && p?.k === "k" && p.c === c);
|
|
93
|
+
if (king < 0)
|
|
94
|
+
continue;
|
|
95
|
+
const rookAt = (cell) => cells[cell]?.k === "r" && cells[cell].c === c;
|
|
96
|
+
const low = ch.toLowerCase();
|
|
97
|
+
let rook = -1;
|
|
98
|
+
// K and Q mean the outermost rook on that side of the king; a file letter means that rook.
|
|
99
|
+
if (low === "k")
|
|
100
|
+
for (let cell = row + 7; cell > king && rook < 0; cell--)
|
|
101
|
+
if (rookAt(cell))
|
|
102
|
+
rook = cell;
|
|
103
|
+
if (low === "q")
|
|
104
|
+
for (let cell = row; cell < king && rook < 0; cell++)
|
|
105
|
+
if (rookAt(cell))
|
|
106
|
+
rook = cell;
|
|
107
|
+
if (low >= "a" && low <= "h" && low !== "k" && low !== "q" && rookAt(row + low.charCodeAt(0) - 97))
|
|
108
|
+
rook = row + low.charCodeAt(0) - 97;
|
|
109
|
+
if (rook >= 0)
|
|
110
|
+
castling[c * 2 + (rook > king ? 0 : 1)] = rook;
|
|
111
|
+
}
|
|
112
|
+
let epCell = -1;
|
|
113
|
+
if (ep !== "-") {
|
|
114
|
+
if (!/^[a-h][36]$/.test(ep))
|
|
115
|
+
throw bad();
|
|
116
|
+
epCell = (8 - Number(ep[1])) * 8 + ep.charCodeAt(0) - 97;
|
|
117
|
+
}
|
|
118
|
+
const n = (text, least) => (Number.isInteger(Number(text)) && Number(text) >= least ? Number(text) : least);
|
|
119
|
+
return { cells, side: side === "w" ? WHITE : BLACK, castling, ep: epCell, half: n(half, 0), full: n(full, 1) };
|
|
120
|
+
}
|
|
121
|
+
const fenLetter = (p) => (p.c === WHITE ? p.k.toUpperCase() : p.k);
|
|
122
|
+
function placement(cells) {
|
|
123
|
+
let out = "";
|
|
124
|
+
for (let row = 0; row < 8; row++) {
|
|
125
|
+
let gap = 0;
|
|
126
|
+
for (let col = 0; col < 8; col++) {
|
|
127
|
+
const p = cells[row * 8 + col];
|
|
128
|
+
if (!p)
|
|
129
|
+
gap++;
|
|
130
|
+
else {
|
|
131
|
+
out += (gap ? String(gap) : "") + fenLetter(p);
|
|
132
|
+
gap = 0;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
out += (gap ? String(gap) : "") + (row < 7 ? "/" : "");
|
|
136
|
+
}
|
|
137
|
+
return out;
|
|
138
|
+
}
|
|
139
|
+
/** Castling rights for a FEN: KQkq from the classic squares, the rooks' files otherwise. */
|
|
140
|
+
function castlingText(cells, castling) {
|
|
141
|
+
const classic = castling.every((rook, i) => rook < 0 || (rook === [63, 56, 7, 0][i] && cells[i < 2 ? 60 : 4]?.k === "k"));
|
|
142
|
+
const text = castling.map((rook, i) => (rook < 0 ? "" : classic ? "KQkq"[i] : i < 2 ? "ABCDEFGH"[rook % 8] : "abcdefgh"[rook % 8])).join("");
|
|
143
|
+
return text || "-";
|
|
144
|
+
}
|
|
145
|
+
/** The position on the board as FEN. */
|
|
146
|
+
export function chessFen(s) {
|
|
147
|
+
return `${placement(s.cells)} ${colourOf(s, s.turn) === WHITE ? "w" : "b"} ${castlingText(s.cells, s.castling)} ${s.ep < 0 ? "-" : at(s.ep)} ${s.half} ${s.full}`;
|
|
148
|
+
}
|
|
149
|
+
/** The position for the move generator. */
|
|
150
|
+
function setupOf(s) {
|
|
151
|
+
return { board: s.cells.map((p) => (p ? CODE[p.k] | (p.c << 3) : 0)), side: colourOf(s, s.turn), rooks: s.castling, ep: s.ep, half: s.half, full: s.full };
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* What makes two positions the same for the repetition rule: the same pieces on the same squares,
|
|
155
|
+
* the same side to move, the same castling rights, and the same en passant capture really there
|
|
156
|
+
* to be made.
|
|
157
|
+
*/
|
|
158
|
+
function keyOf(s, replies) {
|
|
159
|
+
const ep = s.ep >= 0 && replies.some((m) => moveFlag(m) === FLAG_EP) ? at(s.ep) : "-";
|
|
160
|
+
return `${placement(s.cells)} ${colourOf(s, s.turn) === WHITE ? "w" : "b"} ${castlingText(s.cells, s.castling)} ${ep}`;
|
|
161
|
+
}
|
|
162
|
+
function begin(s, parsed) {
|
|
163
|
+
let id = 0;
|
|
164
|
+
s.cells = parsed.cells.map((p) => (p ? { ...p, id: id++ } : null));
|
|
165
|
+
s.turn = seatOf(s, parsed.side);
|
|
166
|
+
s.castling = parsed.castling;
|
|
167
|
+
s.ep = parsed.ep;
|
|
168
|
+
s.half = parsed.half;
|
|
169
|
+
s.full = parsed.full;
|
|
170
|
+
s.start = chessFen(s);
|
|
171
|
+
s.record = [];
|
|
172
|
+
s.taken = ["", ""];
|
|
173
|
+
s.seen = { [keyOf(s, new Position(setupOf(s)).legal())]: 1 };
|
|
174
|
+
s.bank = s.rules.minutes > 0 ? [s.rules.minutes * 60_000, s.rules.minutes * 60_000] : null;
|
|
175
|
+
s.ticking = false;
|
|
176
|
+
s.roundWinner = null;
|
|
177
|
+
s.phase = "playing";
|
|
178
|
+
s.ready = Array(s.seats).fill(false);
|
|
179
|
+
s.narration = null;
|
|
180
|
+
}
|
|
181
|
+
function startRound(s) {
|
|
182
|
+
// A new starting row for each pair of games, so each player has it once with each colour.
|
|
183
|
+
if (s.rules.variant !== "960")
|
|
184
|
+
s.setup = 518;
|
|
185
|
+
else if (s.round % 2 === 1)
|
|
186
|
+
s.setup = Math.floor(nextRandom(s)() * 960);
|
|
187
|
+
const row = chess960Row(s.setup);
|
|
188
|
+
const rank = (c) => row.map((k) => ({ c, k }));
|
|
189
|
+
const pawns = (c) => Array.from({ length: 8 }, () => ({ c, k: "p" }));
|
|
190
|
+
const empty = Array(32).fill(null);
|
|
191
|
+
const rooks = row.flatMap((k, file) => (k === "r" ? [file] : []));
|
|
192
|
+
begin(s, { cells: [...rank(BLACK), ...pawns(BLACK), ...empty, ...pawns(WHITE), ...rank(WHITE)], side: WHITE, castling: [56 + rooks[1], 56 + rooks[0], rooks[1], rooks[0]], ep: -1, half: 0, full: 1 });
|
|
193
|
+
}
|
|
194
|
+
export function newChess(rules, _seats, seed) {
|
|
195
|
+
const s = { game: "chess", rules, seats: 2, rng: seed >>> 0, round: 1, wins: [0, 0], draws: 0, setup: 518 };
|
|
196
|
+
s.white = rules.white === "seat1" ? 0 : rules.white === "seat2" ? 1 : nextRandom(s)() < 0.5 ? 0 : 1;
|
|
197
|
+
startRound(s);
|
|
198
|
+
return s;
|
|
199
|
+
}
|
|
200
|
+
/** A game that starts from a position given as FEN, with `white` playing White: for study, and for tests. */
|
|
201
|
+
export function chessFromFen(fen, rules = classicChess, white = 0) {
|
|
202
|
+
const parsed = parseFen(fen);
|
|
203
|
+
const s = { game: "chess", rules, seats: 2, rng: 1, round: 1, wins: [0, 0], draws: 0, setup: 518, white: white === 1 ? 1 : 0 };
|
|
204
|
+
begin(s, parsed);
|
|
205
|
+
return s;
|
|
206
|
+
}
|
|
207
|
+
// ---- Moves, and how they are written ------------------------------------------------------------
|
|
208
|
+
/** Where a move is put down on the board: in Chess960 castling is the king onto its own rook, since the king's own square may be where it ends up, or a square it could simply step to. */
|
|
209
|
+
function target(s, colour, m) {
|
|
210
|
+
const flag = moveFlag(m);
|
|
211
|
+
if (s.rules.variant === "960" && (flag === FLAG_CASTLE_K || flag === FLAG_CASTLE_Q))
|
|
212
|
+
return s.castling[colour * 2 + flag - FLAG_CASTLE_K];
|
|
213
|
+
return cellOf(moveTo(m));
|
|
214
|
+
}
|
|
215
|
+
/** A move in standard algebraic notation, without the mark for check: "Nbd7", "exd5", "e8=Q", "O-O". */
|
|
216
|
+
function sanOf(pos, moves, m) {
|
|
217
|
+
const flag = moveFlag(m);
|
|
218
|
+
if (flag === FLAG_CASTLE_K)
|
|
219
|
+
return "O-O";
|
|
220
|
+
if (flag === FLAG_CASTLE_Q)
|
|
221
|
+
return "O-O-O";
|
|
222
|
+
const from = moveFrom(m);
|
|
223
|
+
const to = moveTo(m);
|
|
224
|
+
const kind = pos.b[from] & 7;
|
|
225
|
+
const takes = !!pos.b[to] || flag === FLAG_EP;
|
|
226
|
+
const square = (sq) => "abcdefgh"[sq & 7] + ((sq >> 4) + 1);
|
|
227
|
+
if (kind === PAWN)
|
|
228
|
+
return `${takes ? `${"abcdefgh"[from & 7]}x` : ""}${square(to)}${movePromo(m) ? `=${LETTER_OF[movePromo(m)].toUpperCase()}` : ""}`;
|
|
229
|
+
// When another piece of the same kind could go there too, say which: by file if that settles it,
|
|
230
|
+
// else by rank, else both.
|
|
231
|
+
const rivals = moves.filter((o) => o !== m && moveTo(o) === to && (pos.b[moveFrom(o)] & 7) === kind && moveFlag(o) !== FLAG_CASTLE_K && moveFlag(o) !== FLAG_CASTLE_Q).map(moveFrom);
|
|
232
|
+
let which = "";
|
|
233
|
+
if (rivals.length) {
|
|
234
|
+
if (!rivals.some((sq) => (sq & 7) === (from & 7)))
|
|
235
|
+
which = "abcdefgh"[from & 7];
|
|
236
|
+
else if (!rivals.some((sq) => sq >> 4 === from >> 4))
|
|
237
|
+
which = String((from >> 4) + 1);
|
|
238
|
+
else
|
|
239
|
+
which = square(from);
|
|
240
|
+
}
|
|
241
|
+
return `${LETTER_OF[kind].toUpperCase()}${which}${takes ? "x" : ""}${square(to)}`;
|
|
242
|
+
}
|
|
243
|
+
/** How a match stands once this game is in: finished when the games run out or one side can't be caught. */
|
|
244
|
+
function matchOver(s) {
|
|
245
|
+
const left = s.rules.games - s.round;
|
|
246
|
+
return left <= 0 || Math.abs(s.wins[0] - s.wins[1]) > left;
|
|
247
|
+
}
|
|
248
|
+
function endRound(s, winner, why) {
|
|
249
|
+
const many = s.rules.games > 1;
|
|
250
|
+
if (winner === null)
|
|
251
|
+
s.draws += 1;
|
|
252
|
+
else
|
|
253
|
+
s.wins[winner] += 1;
|
|
254
|
+
s.roundWinner = winner;
|
|
255
|
+
s.ticking = false;
|
|
256
|
+
s.narration = winner === null ? `${why}: ${many ? `game ${s.round} is a draw` : "it's a draw"}` : `${why}: ${who(winner)} wins${many ? ` game ${s.round}` : ""}!`;
|
|
257
|
+
s.phase = matchOver(s) ? "finished" : "scored";
|
|
258
|
+
}
|
|
259
|
+
/** A draw either player may claim now, and why; null when there is none. */
|
|
260
|
+
function claimable(s, replies) {
|
|
261
|
+
if (s.phase !== "playing")
|
|
262
|
+
return null;
|
|
263
|
+
if ((s.seen[keyOf(s, replies)] ?? 0) >= 3)
|
|
264
|
+
return "The same position three times";
|
|
265
|
+
if (s.half >= 100)
|
|
266
|
+
return "Fifty moves with no capture and no pawn moved";
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Whether `colour` could ever give mate with what it has, for when the other side's clock runs
|
|
271
|
+
* out: with no way to mate, running out of time is only a draw. A lone king can't; a king and one
|
|
272
|
+
* knight, or a king and bishops all on one colour of square, can only with the loser's own pieces
|
|
273
|
+
* in the way of their king, so it goes by what the loser has left (as the big chess sites do; the
|
|
274
|
+
* laws of chess ask whether any series of legal moves at all ends in mate, which this follows in
|
|
275
|
+
* every case that comes up in practice).
|
|
276
|
+
*/
|
|
277
|
+
function couldMate(s, colour) {
|
|
278
|
+
const mine = [];
|
|
279
|
+
const theirs = [];
|
|
280
|
+
s.cells.forEach((p, cell) => {
|
|
281
|
+
if (p && p.k !== "k")
|
|
282
|
+
(p.c === colour ? mine : theirs).push({ k: p.k, dark: ((cell >> 3) + (cell & 7)) % 2 === 1 });
|
|
283
|
+
});
|
|
284
|
+
if (mine.some((p) => p.k === "p" || p.k === "r" || p.k === "q"))
|
|
285
|
+
return true;
|
|
286
|
+
if (!mine.length)
|
|
287
|
+
return false;
|
|
288
|
+
const knights = mine.filter((p) => p.k === "n").length;
|
|
289
|
+
const bishops = mine.filter((p) => p.k === "b");
|
|
290
|
+
if (knights && bishops.length)
|
|
291
|
+
return true;
|
|
292
|
+
if (knights >= 2)
|
|
293
|
+
return true;
|
|
294
|
+
if (knights === 1)
|
|
295
|
+
return theirs.some((p) => p.k !== "q");
|
|
296
|
+
// Bishops only.
|
|
297
|
+
if (bishops.some((p) => p.dark) && bishops.some((p) => !p.dark))
|
|
298
|
+
return true;
|
|
299
|
+
return theirs.some((p) => p.k === "n" || p.k === "p" || (p.k === "b" && p.dark !== bishops[0].dark));
|
|
300
|
+
}
|
|
301
|
+
const describeMove = (piece, from, to, taken, promo) => `${KIND[piece]} ${at(from)} ${taken ? `takes the ${KIND[taken]} on` : "to"} ${at(to)}${promo ? `, and becomes a ${KIND[promo]}` : ""}`;
|
|
302
|
+
/** Make a legal move on the board, write it in the record, and see whether the game is over. Changes `s` and `pos`. */
|
|
303
|
+
function carryOut(s, pos, moves, m, seat) {
|
|
304
|
+
const colour = colourOf(s, seat);
|
|
305
|
+
const from = cellOf(moveFrom(m));
|
|
306
|
+
const to = cellOf(moveTo(m));
|
|
307
|
+
const flag = moveFlag(m);
|
|
308
|
+
const promo = movePromo(m);
|
|
309
|
+
const piece = s.cells[from];
|
|
310
|
+
const changes = [];
|
|
311
|
+
let san = sanOf(pos, moves, m);
|
|
312
|
+
let said;
|
|
313
|
+
if (flag === FLAG_CASTLE_K || flag === FLAG_CASTLE_Q) {
|
|
314
|
+
const rf = s.castling[colour * 2 + flag - FLAG_CASTLE_K];
|
|
315
|
+
const rt = (to & ~7) + (flag === FLAG_CASTLE_K ? 5 : 3);
|
|
316
|
+
const rook = s.cells[rf];
|
|
317
|
+
// Both off, then both on: in Chess960 either may land where the other stood.
|
|
318
|
+
s.cells[from] = null;
|
|
319
|
+
s.cells[rf] = null;
|
|
320
|
+
s.cells[to] = piece;
|
|
321
|
+
s.cells[rt] = rook;
|
|
322
|
+
changes.push({ id: rook.id, from: rf, to: rt }, { id: piece.id, from, to });
|
|
323
|
+
said = `castles ${flag === FLAG_CASTLE_K ? "king" : "queen"} side`;
|
|
324
|
+
}
|
|
325
|
+
else {
|
|
326
|
+
const where = flag === FLAG_EP ? to + (colour === WHITE ? 8 : -8) : to;
|
|
327
|
+
const captured = s.cells[where] ?? null;
|
|
328
|
+
if (captured) {
|
|
329
|
+
s.cells[where] = null;
|
|
330
|
+
s.taken[seat] += captured.k;
|
|
331
|
+
changes.push({ id: captured.id, from: where, to: -1, was: { c: captured.c, k: captured.k } });
|
|
332
|
+
}
|
|
333
|
+
s.cells[from] = null;
|
|
334
|
+
const becomes = promo ? LETTER_OF[promo] : null;
|
|
335
|
+
s.cells[to] = becomes ? { ...piece, k: becomes } : piece;
|
|
336
|
+
changes.push({ id: piece.id, from, to, ...(becomes ? { was: { c: piece.c, k: piece.k } } : {}) });
|
|
337
|
+
said = describeMove(piece.k, from, to, captured?.k ?? null, becomes) + (flag === FLAG_EP ? " in passing" : "");
|
|
338
|
+
}
|
|
339
|
+
pos.make(m);
|
|
340
|
+
const rights = pos.castle;
|
|
341
|
+
s.castling = s.castling.map((rook, i) => (rights & (1 << i) ? rook : -1));
|
|
342
|
+
// As a FEN has it: the square passed over by any double step, whether or not it can be taken.
|
|
343
|
+
s.ep = flag === FLAG_DOUBLE ? (from + to) >> 1 : -1;
|
|
344
|
+
s.half = pos.half;
|
|
345
|
+
s.full = pos.full;
|
|
346
|
+
s.turn = other(seat);
|
|
347
|
+
const replies = pos.legal();
|
|
348
|
+
const checked = pos.inCheck();
|
|
349
|
+
san += checked ? (replies.length ? "+" : "#") : "";
|
|
350
|
+
const kingCell = cellOf(pos.king[pos.side]);
|
|
351
|
+
s.record = [...s.record, { san, by: seat, from, to, changes, ...(checked ? { check: kingCell } : {}) }];
|
|
352
|
+
// Nothing before a capture or a pawn move can come round again.
|
|
353
|
+
if (s.half === 0)
|
|
354
|
+
s.seen = {};
|
|
355
|
+
const key = keyOf(s, replies);
|
|
356
|
+
s.seen[key] = (s.seen[key] ?? 0) + 1;
|
|
357
|
+
s.narration = `${who(seat)} plays ${san}: ${said}${checked && replies.length ? ", check" : ""}`;
|
|
358
|
+
if (!replies.length)
|
|
359
|
+
endRound(s, checked ? seat : null, checked ? `Checkmate (${san})` : `Stalemate: ${who(s.turn)} has no move and isn't in check`);
|
|
360
|
+
else if (pos.dead())
|
|
361
|
+
endRound(s, null, "Neither side has enough left to give mate");
|
|
362
|
+
else if (s.seen[key] >= 5)
|
|
363
|
+
endRound(s, null, "The same position five times");
|
|
364
|
+
else if (s.half >= 150)
|
|
365
|
+
endRound(s, null, "Seventy-five moves with no capture and no pawn moved");
|
|
366
|
+
}
|
|
367
|
+
function apply(state, seat, move, time) {
|
|
368
|
+
if (move?.type === "next") {
|
|
369
|
+
if (state.phase !== "scored")
|
|
370
|
+
throw new GameError("There's a game going on.");
|
|
371
|
+
const s = copy(state);
|
|
372
|
+
if (markReady(s.ready, seat)) {
|
|
373
|
+
// Colours change every game.
|
|
374
|
+
s.round += 1;
|
|
375
|
+
s.white = other(s.white);
|
|
376
|
+
startRound(s);
|
|
377
|
+
}
|
|
378
|
+
return s;
|
|
379
|
+
}
|
|
380
|
+
if (move?.type !== "move" && move?.type !== "claim" && move?.type !== "timeout")
|
|
381
|
+
throw new GameError("That's not a move in Chess.");
|
|
382
|
+
if (state.phase !== "playing")
|
|
383
|
+
throw new GameError("This game is over.");
|
|
384
|
+
if (seat !== 0 && seat !== 1)
|
|
385
|
+
throw new GameError("You're not playing at this table.");
|
|
386
|
+
const elapsed = Math.max(0, Number(time?.elapsed) || 0);
|
|
387
|
+
if (move.type === "claim") {
|
|
388
|
+
const why = claimable(state, new Position(setupOf(state)).legal());
|
|
389
|
+
if (!why)
|
|
390
|
+
throw new GameError("There's no draw to claim: it takes the same position three times, or fifty moves with no capture and no pawn moved.");
|
|
391
|
+
const s = copy(state);
|
|
392
|
+
endRound(s, null, `${who(seat)} claims a draw. ${why}`);
|
|
393
|
+
return s;
|
|
394
|
+
}
|
|
395
|
+
if (state.turn !== seat)
|
|
396
|
+
throw new GameError("It's not your turn.");
|
|
397
|
+
if (move.type === "timeout") {
|
|
398
|
+
const due = deadline(state);
|
|
399
|
+
if (!due)
|
|
400
|
+
throw new GameError("There's no clock running.");
|
|
401
|
+
// Only when the time really has gone. (A table that doesn't say how long a move took is taken at its word.)
|
|
402
|
+
if (time && elapsed < due.after - 1000)
|
|
403
|
+
throw new GameError("Your clock is still running.");
|
|
404
|
+
const s = copy(state);
|
|
405
|
+
if (s.bank)
|
|
406
|
+
s.bank[seat] = 0;
|
|
407
|
+
const winner = other(seat);
|
|
408
|
+
if (couldMate(s, colourOf(s, winner)))
|
|
409
|
+
endRound(s, winner, `${who(seat)} ran out of time`);
|
|
410
|
+
else
|
|
411
|
+
endRound(s, null, `${who(seat)} ran out of time, but ${who(winner)} has nothing to mate with`);
|
|
412
|
+
return s;
|
|
413
|
+
}
|
|
414
|
+
const from = Number(move.from);
|
|
415
|
+
const to = Number(move.to);
|
|
416
|
+
const on = (cell) => Number.isInteger(cell) && cell >= 0 && cell < 64;
|
|
417
|
+
if (!on(from) || !on(to))
|
|
418
|
+
throw new GameError("That's not on the board.");
|
|
419
|
+
const colour = colourOf(state, seat);
|
|
420
|
+
const pos = new Position(setupOf(state));
|
|
421
|
+
const moves = pos.legal();
|
|
422
|
+
const fits = moves.filter((m) => cellOf(moveFrom(m)) === from && target(state, colour, m) === to);
|
|
423
|
+
if (!fits.length) {
|
|
424
|
+
if (state.cells[from]?.c !== colour)
|
|
425
|
+
throw new GameError("That's not your piece.");
|
|
426
|
+
throw new GameError(pos.inCheck() ? "You're in check: that doesn't get you out of it." : "That piece can't go there.");
|
|
427
|
+
}
|
|
428
|
+
let m = fits[0];
|
|
429
|
+
if (movePromo(m)) {
|
|
430
|
+
const as = LETTERS.find((k) => KIND[k] === move.as);
|
|
431
|
+
const chosen = as ? fits.find((o) => movePromo(o) === CODE[as]) : undefined;
|
|
432
|
+
if (!chosen)
|
|
433
|
+
throw new GameError("Choose what the pawn becomes: a queen, a rook, a bishop or a knight.");
|
|
434
|
+
m = chosen;
|
|
435
|
+
}
|
|
436
|
+
const s = copy(state);
|
|
437
|
+
// The clock: the time the move took comes off the mover's bank, and the increment goes on. White's
|
|
438
|
+
// first move is free, and starts the clocks.
|
|
439
|
+
if (s.bank) {
|
|
440
|
+
if (s.ticking)
|
|
441
|
+
s.bank[seat] = Math.max(0, s.bank[seat] - elapsed);
|
|
442
|
+
s.bank[seat] += s.rules.increment * 1000;
|
|
443
|
+
}
|
|
444
|
+
s.ticking = true;
|
|
445
|
+
carryOut(s, pos, moves, m, seat);
|
|
446
|
+
return s;
|
|
447
|
+
}
|
|
448
|
+
/**
|
|
449
|
+
* The game's clock: when the side to move runs out of time, the table makes its "timeout" move.
|
|
450
|
+
* With a chess clock that is the time left in its bank; in correspondence play, so many days for
|
|
451
|
+
* the move. Nothing runs until White's first move has been made.
|
|
452
|
+
*/
|
|
453
|
+
function deadline(s) {
|
|
454
|
+
if (s.phase !== "playing" || !s.ticking)
|
|
455
|
+
return null;
|
|
456
|
+
if (s.bank)
|
|
457
|
+
return { after: s.bank[s.turn], seat: s.turn, move: { type: "timeout" }, strict: true };
|
|
458
|
+
if (s.rules.days > 0)
|
|
459
|
+
return { after: s.rules.days * DAY_MS, seat: s.turn, move: { type: "timeout" }, strict: true };
|
|
460
|
+
return null;
|
|
461
|
+
}
|
|
462
|
+
/**
|
|
463
|
+
* The move a line of notation means in this position, as the table would send it: "Nf3", "exd5",
|
|
464
|
+
* "O-O", "e8=Q+" (or the squares alone: "g1f3", "e7e8q"). Null if it is no legal move.
|
|
465
|
+
*/
|
|
466
|
+
export function chessMove(s, text) {
|
|
467
|
+
if (s.phase !== "playing")
|
|
468
|
+
return null;
|
|
469
|
+
const pos = new Position(setupOf(s));
|
|
470
|
+
const moves = pos.legal();
|
|
471
|
+
const colour = colourOf(s, s.turn);
|
|
472
|
+
const plain = (san) => san.replace(/0/g, "O").replace(/[+#!?]/g, "");
|
|
473
|
+
const wanted = plain(String(text).trim());
|
|
474
|
+
const square = (sq) => "abcdefgh"[sq & 7] + ((sq >> 4) + 1);
|
|
475
|
+
const m = moves.find((o) => plain(sanOf(pos, moves, o)) === wanted) ?? moves.find((o) => `${square(moveFrom(o))}${square(moveTo(o))}${movePromo(o) ? LETTER_OF[movePromo(o)] : ""}` === String(text).trim().toLowerCase());
|
|
476
|
+
if (m === undefined)
|
|
477
|
+
return null;
|
|
478
|
+
return { type: "move", from: cellOf(moveFrom(m)), to: target(s, colour, m), ...(movePromo(m) ? { as: KIND[LETTER_OF[movePromo(m)]] } : {}) };
|
|
479
|
+
}
|
|
480
|
+
/** How many positions are exactly `depth` moves from this one: the standard check of a move generator. */
|
|
481
|
+
export const chessPerft = (s, depth) => perft(new Position(setupOf(s)), depth);
|
|
482
|
+
/** "1-0", "0-1", "1/2-1/2", or "*" while the game on the board is being played. */
|
|
483
|
+
function resultOf(s) {
|
|
484
|
+
if (s.phase === "playing")
|
|
485
|
+
return "*";
|
|
486
|
+
return s.roundWinner === null ? "1/2-1/2" : s.roundWinner === s.white ? "1-0" : "0-1";
|
|
487
|
+
}
|
|
488
|
+
/**
|
|
489
|
+
* The game on the board as a PGN. Players are named {0} and {1} as in every text from an engine,
|
|
490
|
+
* unless `names` are given.
|
|
491
|
+
*/
|
|
492
|
+
export function chessPgn(s, names) {
|
|
493
|
+
const start = parseFen(s.start);
|
|
494
|
+
const classic = s.rules.variant !== "960" && s.start === "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1";
|
|
495
|
+
const name = (seat) => names?.[seat] ?? who(seat);
|
|
496
|
+
const control = s.rules.minutes > 0 ? `${s.rules.minutes * 60}${s.rules.increment ? `+${s.rules.increment}` : ""}` : s.rules.days > 0 ? `1/${s.rules.days * 86_400}` : "-";
|
|
497
|
+
const tags = [
|
|
498
|
+
["Event", "Card Room"],
|
|
499
|
+
["Site", "?"],
|
|
500
|
+
["Date", "????.??.??"],
|
|
501
|
+
["Round", String(s.round)],
|
|
502
|
+
["White", name(s.white)],
|
|
503
|
+
["Black", name(other(s.white))],
|
|
504
|
+
["Result", resultOf(s)],
|
|
505
|
+
...(s.rules.variant === "960" ? [["Variant", "Chess960"]] : []),
|
|
506
|
+
...(classic ? [] : [["SetUp", "1"], ["FEN", s.start]]),
|
|
507
|
+
["TimeControl", control],
|
|
508
|
+
];
|
|
509
|
+
const words = [];
|
|
510
|
+
let n = start.full;
|
|
511
|
+
let colour = start.side;
|
|
512
|
+
s.record.forEach((ply, i) => {
|
|
513
|
+
if (colour === WHITE)
|
|
514
|
+
words.push(`${n}.`);
|
|
515
|
+
else if (i === 0)
|
|
516
|
+
words.push(`${n}...`);
|
|
517
|
+
words.push(ply.san);
|
|
518
|
+
if (colour === BLACK)
|
|
519
|
+
n++;
|
|
520
|
+
colour ^= 1;
|
|
521
|
+
});
|
|
522
|
+
words.push(resultOf(s));
|
|
523
|
+
// Lines of movetext are kept under 80 characters.
|
|
524
|
+
const lines = [];
|
|
525
|
+
let line = "";
|
|
526
|
+
for (const word of words) {
|
|
527
|
+
if (line && line.length + 1 + word.length > 79) {
|
|
528
|
+
lines.push(line);
|
|
529
|
+
line = word;
|
|
530
|
+
}
|
|
531
|
+
else
|
|
532
|
+
line = line ? `${line} ${word}` : word;
|
|
533
|
+
}
|
|
534
|
+
if (line)
|
|
535
|
+
lines.push(line);
|
|
536
|
+
return `${tags.map(([tag, value]) => `[${tag} "${value.replace(/["\\]/g, "")}"]`).join("\n")}\n\n${lines.join("\n")}\n`;
|
|
537
|
+
}
|
|
538
|
+
/** Best first. A drawn match lists the player who had Black in the last game first. */
|
|
539
|
+
function order(s) {
|
|
540
|
+
const [a, b] = s.wins;
|
|
541
|
+
if (a !== b)
|
|
542
|
+
return a > b ? [0, 1] : [1, 0];
|
|
543
|
+
return [other(s.white), s.white];
|
|
544
|
+
}
|
|
545
|
+
const winners = (s) => (s.wins[0] === s.wins[1] ? [0, 1] : [s.wins[0] > s.wins[1] ? 0 : 1]);
|
|
546
|
+
// ---- What a seat sees ---------------------------------------------------------------------------
|
|
547
|
+
function pieceOf(s, p) {
|
|
548
|
+
return { owner: seatOf(s, p.c), kind: KIND[p.k], glyph: GLYPH[p.k][p.c], label: `${COLOUR[p.c]} ${KIND[p.k]}`, tone: p.c === WHITE ? "light" : "dark" };
|
|
549
|
+
}
|
|
550
|
+
const clockName = (r) => (r.minutes > 0 ? `${r.minutes}+${r.increment}` : r.days > 0 ? (r.days === 1 ? "a day a move" : `${r.days} days a move`) : "");
|
|
551
|
+
function recordOf(s) {
|
|
552
|
+
const start = parseFen(s.start);
|
|
553
|
+
let n = start.full;
|
|
554
|
+
let colour = start.side;
|
|
555
|
+
const moves = s.record.map((ply) => {
|
|
556
|
+
const turn = n;
|
|
557
|
+
if (colour === BLACK)
|
|
558
|
+
n++;
|
|
559
|
+
colour ^= 1;
|
|
560
|
+
const changes = ply.changes.map((c) => ({ id: `c${c.id}`, from: c.from < 0 ? null : c.from, to: c.to < 0 ? null : c.to, ...(c.was ? { was: { ...pieceOf(s, c.was), id: `c${c.id}` } } : {}) }));
|
|
561
|
+
return { label: ply.san, by: ply.by, turn, changes, ...(ply.check !== undefined ? { threat: ply.check } : {}) };
|
|
562
|
+
});
|
|
563
|
+
return { moves, export: { label: "PGN", text: chessPgn(s) } };
|
|
564
|
+
}
|
|
565
|
+
function view(s, seat) {
|
|
566
|
+
const over = s.phase === "finished";
|
|
567
|
+
const playing = s.phase === "playing";
|
|
568
|
+
const mine = playing && s.turn === seat;
|
|
569
|
+
const many = s.rules.games > 1;
|
|
570
|
+
const w = over ? winners(s) : [];
|
|
571
|
+
const pos = new Position(setupOf(s));
|
|
572
|
+
const moves = playing ? pos.legal() : [];
|
|
573
|
+
const colour = colourOf(s, s.turn);
|
|
574
|
+
const checked = playing && pos.inCheck();
|
|
575
|
+
const marks = [];
|
|
576
|
+
const last = s.record.at(-1);
|
|
577
|
+
if (last)
|
|
578
|
+
marks.push({ cell: last.from, mark: "from" }, { cell: last.to, mark: "last" });
|
|
579
|
+
const kingOf = (c) => s.cells.findIndex((p) => p?.k === "k" && p.c === c);
|
|
580
|
+
if (checked)
|
|
581
|
+
marks.push({ cell: kingOf(colour), mark: "threat" });
|
|
582
|
+
// A game that ended in mate leaves the mated king marked.
|
|
583
|
+
else if (!playing && last?.check !== undefined)
|
|
584
|
+
marks.push({ cell: last.check, mark: "threat" });
|
|
585
|
+
const sources = new Map();
|
|
586
|
+
const choose = [];
|
|
587
|
+
if (mine) {
|
|
588
|
+
for (const m of moves) {
|
|
589
|
+
const from = cellOf(moveFrom(m));
|
|
590
|
+
const to = target(s, colour, m);
|
|
591
|
+
const tos = sources.get(from) ?? [];
|
|
592
|
+
if (!tos.includes(to))
|
|
593
|
+
sources.set(from, [...tos, to]);
|
|
594
|
+
// Queen first: it is nearly always what is wanted.
|
|
595
|
+
if (movePromo(m) === QUEEN)
|
|
596
|
+
choose.push({ from, to, key: "as", options: ["q", "r", "b", "n"].map((k) => pieceOf(s, { c: colour, k })) });
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
const grid = {
|
|
600
|
+
id: "board",
|
|
601
|
+
label: "Board",
|
|
602
|
+
rows: 8,
|
|
603
|
+
...GRID,
|
|
604
|
+
shade: "checker",
|
|
605
|
+
// White is at the bottom of `cells`; the seat playing Black sees the board turned round.
|
|
606
|
+
flip: seat === seatOf(s, BLACK),
|
|
607
|
+
cells: s.cells.map((p) => (p ? { ...pieceOf(s, p), id: `c${p.id}` } : null)),
|
|
608
|
+
marks,
|
|
609
|
+
select: mine ? { type: "move", sources: [...sources].map(([from, to]) => ({ from, to })), ...(choose.length ? { choose } : {}), ...(s.rules.hints ? {} : { quiet: true }) } : null,
|
|
610
|
+
};
|
|
611
|
+
const material = [0, 1].map((seatNo) => s.cells.reduce((sum, p) => sum + (p && seatOf(s, p.c) === seatNo ? POINTS[p.k] : 0), 0));
|
|
612
|
+
const claim = playing ? claimable(s, moves) : null;
|
|
613
|
+
const left = 100 - s.half;
|
|
614
|
+
const scoreTitle = s.roundWinner === null ? `Game ${s.round} is a draw` : `${who(s.roundWinner)} wins game ${s.round}`;
|
|
615
|
+
return {
|
|
616
|
+
game: "chess",
|
|
617
|
+
seats: s.seats,
|
|
618
|
+
me: seat,
|
|
619
|
+
teams: false,
|
|
620
|
+
phase: s.phase,
|
|
621
|
+
turn: playing ? s.turn : null,
|
|
622
|
+
grids: [grid],
|
|
623
|
+
// What each side has taken, biggest first, the way they are lined up beside a board.
|
|
624
|
+
reserves: [0, 1].map((o) => ({
|
|
625
|
+
owner: o,
|
|
626
|
+
label: "Captured",
|
|
627
|
+
pieces: ["q", "r", "b", "n", "p"].map((k) => ({ piece: pieceOf(s, { c: colourOf(s, other(o)), k }), count: s.taken[o].split(k).length - 1 })),
|
|
628
|
+
})),
|
|
629
|
+
choices: s.phase === "scored"
|
|
630
|
+
? nextHandChoices(s.ready, seat, false, "Next game")
|
|
631
|
+
: claim && (seat === 0 || seat === 1)
|
|
632
|
+
? { prompt: "", options: [{ label: "Claim a draw", move: { type: "claim" }, hint: claim }] }
|
|
633
|
+
: null,
|
|
634
|
+
players: [0, 1].map((i) => {
|
|
635
|
+
const ahead = material[i] - material[other(i)];
|
|
636
|
+
const running = playing && s.ticking && s.turn === i;
|
|
637
|
+
return {
|
|
638
|
+
line: [COLOUR[colourOf(s, i)] === "white" ? "White" : "Black", ...(ahead > 0 ? [`+${ahead}`] : []), ...(many ? [`${s.wins[i]} win${s.wins[i] === 1 ? "" : "s"}`] : [])].join(" · "),
|
|
639
|
+
piece: pieceOf(s, { c: colourOf(s, i), k: "k" }),
|
|
640
|
+
badge: checked && s.turn === i ? "Check" : undefined,
|
|
641
|
+
...(s.bank ? { clock: { ms: s.bank[i], running } } : s.rules.days > 0 && running ? { clock: { ms: s.rules.days * DAY_MS, running } } : {}),
|
|
642
|
+
};
|
|
643
|
+
}),
|
|
644
|
+
info: [
|
|
645
|
+
...(many ? [`Game ${s.round} of ${s.rules.games}`] : []),
|
|
646
|
+
...(s.rules.variant === "960" ? [`Chess960, starting row ${s.setup}`] : []),
|
|
647
|
+
...(clockName(s.rules) ? [`Clock: ${clockName(s.rules)}${playing && !s.ticking ? ", from White's first move" : ""}`] : []),
|
|
648
|
+
...(claim ? [`${claim}: either player can claim a draw`] : playing && left <= 20 ? [`A draw can be claimed in ${Math.ceil(left / 2)} more move${left <= 2 ? "" : "s"} each without a capture or a pawn move`] : []),
|
|
649
|
+
],
|
|
650
|
+
narration: s.narration ?? (playing ? (mine ? "You're White: you move first" : `${who(s.turn)} is White, and moves first`) : null),
|
|
651
|
+
scores: s.wins.map((value, i) => ({ seats: [i], value })),
|
|
652
|
+
result: many && !playing
|
|
653
|
+
? { key: `game-${s.round}`, title: scoreTitle, sides: [[0], [1]], rows: [{ label: "Wins", values: s.wins.map(String), tone: "total" }, ...(s.draws ? [{ label: "Draws", values: [String(s.draws), String(s.draws)] }] : [])] }
|
|
654
|
+
: null,
|
|
655
|
+
over: over ? { winners: w, title: w.length > 1 ? (many ? "The match is drawn" : "It's a draw") : `${who(w[0])} wins${many ? ` ${s.wins[w[0]]} to ${s.wins[other(w[0])]}` : ""}!` } : null,
|
|
656
|
+
record: recordOf(s),
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
// ---- Computer players ---------------------------------------------------------------------------
|
|
660
|
+
// The search is in chesscore.ts. Here is how much of it each player gets, and when they stop.
|
|
661
|
+
//
|
|
662
|
+
// Skill sets how far ahead a player looks and how much luck is in its choice: the lowest looks two
|
|
663
|
+
// moves ahead and happily plays any move within most of a pawn of the best, the middle looks four
|
|
664
|
+
// and is choosier, and the top looks as far as its work or its time allows. A careless player
|
|
665
|
+
// (see `careless`) doesn't look ahead at all for that move: it plays what looks best on the board
|
|
666
|
+
// as it stands, so it leaves pieces where they can be taken, the way people do. From the middle
|
|
667
|
+
// skill up even a careless move never walks into mate on the move.
|
|
668
|
+
//
|
|
669
|
+
// When a search stops goes by which game is being played:
|
|
670
|
+
//
|
|
671
|
+
// - No chess clock. A think is rationed by work, the number of positions looked at, the way
|
|
672
|
+
// Checkers rations its own, so the same position, seat, style and dice always give the same
|
|
673
|
+
// move on any machine. The ration is sized from the budget the table grants (`WORK_PER_MS`
|
|
674
|
+
// positions for each ms of it): a table that allows two seconds gets a search that takes an
|
|
675
|
+
// ordinary machine a fifth of a second and a slow one most of the two. The budget is kept as
|
|
676
|
+
// a time limit as well, so a machine slower still stops on time (and only there is the move
|
|
677
|
+
// that machine's own).
|
|
678
|
+
// - A chess clock. The player is on the clock like anyone, so it thinks by the time: a share of
|
|
679
|
+
// what is left in its bank, plus most of the increment, never more than the table's budget.
|
|
680
|
+
// At the top skill the time is the only limit, and it is used.
|
|
681
|
+
//
|
|
682
|
+
// Either way a search asked to stop stops within a fraction of a millisecond and plays the best
|
|
683
|
+
// move it has, and a search with no time to look at anything plays a capture if there is one.
|
|
684
|
+
/**
|
|
685
|
+
* Positions looked at for each ms of the table's budget, when a think is rationed by work. A
|
|
686
|
+
* tenth of what an ordinary server does in the time, so the ration is used up long before the
|
|
687
|
+
* budget on anything but a very slow or very busy machine, and the move stays the same everywhere.
|
|
688
|
+
*/
|
|
689
|
+
const WORK_PER_MS = 50;
|
|
690
|
+
/** The budget assumed when a table gives none (a test, a script): a short think. */
|
|
691
|
+
const UNBUDGETED_MS = 12;
|
|
692
|
+
/** The most each skill may look at when its think is rationed: lowest, middle, top (no cap but the ration). */
|
|
693
|
+
const WORK_CAP = [6_000, 40_000, Infinity];
|
|
694
|
+
const DEPTH = [2, 4, 64];
|
|
695
|
+
/** How much luck is worth to each, in hundredths of a pawn; more in the first few moves, so no two games open alike. */
|
|
696
|
+
const LUCK = [70, 22, 8];
|
|
697
|
+
const OPENING_LUCK = 16;
|
|
698
|
+
const tierOf = (style) => ((style.skill ?? 1) >= 0.9 ? 2 : (style.skill ?? 1) >= 0.5 ? 1 : 0);
|
|
699
|
+
/** How long a player with a clock gives a move, in ms: a share of the bank and most of the increment, leaving time to get the move to the table. */
|
|
700
|
+
function allowance(s, seat) {
|
|
701
|
+
const bank = s.bank[seat];
|
|
702
|
+
return Math.max(4, Math.min(bank / 4, bank / 30 + s.rules.increment * 700) - 15);
|
|
703
|
+
}
|
|
704
|
+
/** The keys of the positions the game has been through since the last capture or pawn move, for the search. */
|
|
705
|
+
function seenKeys(s) {
|
|
706
|
+
const out = [];
|
|
707
|
+
for (const key of Object.keys(s.seen)) {
|
|
708
|
+
const parsed = parseFen(`${key} 0 1`);
|
|
709
|
+
out.push(positionKey({ board: parsed.cells.map((p) => (p ? CODE[p.k] | (p.c << 3) : 0)), side: parsed.side, rooks: parsed.castling, ep: parsed.ep, half: 0, full: 1 }));
|
|
710
|
+
}
|
|
711
|
+
return out;
|
|
712
|
+
}
|
|
713
|
+
/** After `m`, can the other side give mate at once? */
|
|
714
|
+
function allowsMate(pos, m) {
|
|
715
|
+
pos.make(m);
|
|
716
|
+
let mate = false;
|
|
717
|
+
for (const reply of pos.legal()) {
|
|
718
|
+
pos.make(reply);
|
|
719
|
+
if (pos.inCheck() && !pos.canMove())
|
|
720
|
+
mate = true;
|
|
721
|
+
pos.unmake(reply);
|
|
722
|
+
if (mate)
|
|
723
|
+
break;
|
|
724
|
+
}
|
|
725
|
+
pos.unmake(m);
|
|
726
|
+
return mate;
|
|
727
|
+
}
|
|
728
|
+
/** The move that looks best without looking ahead, give or take a pawn or so: what a careless player plays. */
|
|
729
|
+
function hasty(pos, moves, rand, safe) {
|
|
730
|
+
const scored = moves
|
|
731
|
+
.map((m) => {
|
|
732
|
+
pos.make(m);
|
|
733
|
+
const value = -evaluate(pos) + rand() * 120;
|
|
734
|
+
pos.unmake(m);
|
|
735
|
+
return { m, value };
|
|
736
|
+
})
|
|
737
|
+
.sort((a, b) => b.value - a.value);
|
|
738
|
+
if (safe)
|
|
739
|
+
for (const { m } of scored.slice(0, 6))
|
|
740
|
+
if (!allowsMate(pos, m))
|
|
741
|
+
return m;
|
|
742
|
+
return scored[0].m;
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* The computer player's choice in this position, with what the search made of it: for the bot
|
|
746
|
+
* below, and for tests and measurements. Null when there is no move to make.
|
|
747
|
+
*/
|
|
748
|
+
export function chessThink(s, style = {}, rand = Math.random, budget) {
|
|
749
|
+
const began = performance.now();
|
|
750
|
+
if (s.phase !== "playing")
|
|
751
|
+
return null;
|
|
752
|
+
const pos = new Position(setupOf(s));
|
|
753
|
+
const tier = tierOf(style);
|
|
754
|
+
if (careless(style, rand)) {
|
|
755
|
+
const moves = pos.legal();
|
|
756
|
+
if (!moves.length)
|
|
757
|
+
return null;
|
|
758
|
+
return { move: moves.length === 1 ? moves[0] : hasty(pos, moves, rand, tier > 0), score: 0, depth: 0, nodes: moves.length, timedOut: false, hasty: true };
|
|
759
|
+
}
|
|
760
|
+
// By the clock only at a table that keeps time: one that grants no budget (a test, a script) isn't timing anything.
|
|
761
|
+
const clocked = !!s.bank && !!budget;
|
|
762
|
+
const granted = budget?.ms ?? UNBUDGETED_MS;
|
|
763
|
+
const ms = clocked ? Math.min(granted, allowance(s, s.turn)) : budget ? granted : Infinity;
|
|
764
|
+
const nodes = clocked ? WORK_CAP[tier] : Math.min(WORK_CAP[tier], Math.round(granted * WORK_PER_MS));
|
|
765
|
+
const luck = LUCK[tier] + (s.record.length < 10 ? OPENING_LUCK : 0);
|
|
766
|
+
// Stop a little short of the time, so the answer is back inside it.
|
|
767
|
+
const stopIn = ms === Infinity ? Infinity : Math.max(1, ms - (ms >= 400 ? 25 : ms >= 40 ? 4 : 2) - (performance.now() - began));
|
|
768
|
+
const result = think(pos, { depth: DEPTH[tier], nodes, ms: stopIn, luck, rand, seen: seenKeys(s) });
|
|
769
|
+
return result.move ? { ...result, hasty: false } : null;
|
|
770
|
+
}
|
|
771
|
+
function botMove(s, seat, style, rand = Math.random, budget) {
|
|
772
|
+
if (s.phase === "scored")
|
|
773
|
+
return s.ready[seat] ? null : { type: "next" };
|
|
774
|
+
if (s.phase !== "playing" || s.turn !== seat)
|
|
775
|
+
return null;
|
|
776
|
+
// A draw there for the claiming is taken unless the game is going its way.
|
|
777
|
+
if (claimable(s, new Position(setupOf(s)).legal()) && (standing(s, seat) ?? 0) <= 0)
|
|
778
|
+
return { type: "claim" };
|
|
779
|
+
const thought = chessThink(s, style, rand, budget);
|
|
780
|
+
if (!thought)
|
|
781
|
+
return null;
|
|
782
|
+
const m = thought.move;
|
|
783
|
+
return { type: "move", from: cellOf(moveFrom(m)), to: target(s, colourOf(s, seat), m), ...(movePromo(m) ? { as: KIND[LETTER_OF[movePromo(m)]] } : {}) };
|
|
784
|
+
}
|
|
785
|
+
/** How the game stands for a seat, in pawns: nothing either way when it is close. */
|
|
786
|
+
function standing(s, seat) {
|
|
787
|
+
if (s.phase !== "playing")
|
|
788
|
+
return null;
|
|
789
|
+
const pos = new Position(setupOf(s));
|
|
790
|
+
const value = (evaluate(pos) - 8) * (s.turn === seat ? 1 : -1);
|
|
791
|
+
return Math.abs(value) < 80 ? 0 : value / 100;
|
|
792
|
+
}
|
|
793
|
+
/** Get the search warm (a thread's first think is its slowest): a table's worker calls this before the first move is wanted. */
|
|
794
|
+
function warmUp() {
|
|
795
|
+
const s = newChess(classicChess, 2, 1);
|
|
796
|
+
for (let i = 0; i < 3; i++)
|
|
797
|
+
chessThink(s, { skill: 1, sloppy: 0 }, () => 0.5, { ms: 30 });
|
|
798
|
+
}
|
|
799
|
+
// ---- The game -----------------------------------------------------------------------------------
|
|
800
|
+
function normalize(rules) {
|
|
801
|
+
const given = (rules && typeof rules === "object" ? rules : {});
|
|
802
|
+
const preset = PRESETS.find((p) => p.id === given.id) ?? classicChess;
|
|
803
|
+
const r = { ...preset, ...given };
|
|
804
|
+
const whole = (value, min, max, or) => (Number.isFinite(Number(value)) ? Math.min(max, Math.max(min, Math.round(Number(value)))) : or);
|
|
805
|
+
const minutes = whole(r.minutes, 0, 180, preset.minutes);
|
|
806
|
+
return {
|
|
807
|
+
id: String(r.id),
|
|
808
|
+
name: String(r.name),
|
|
809
|
+
variant: r.variant === "960" ? "960" : "classic",
|
|
810
|
+
white: r.white === "seat1" || r.white === "seat2" ? r.white : "random",
|
|
811
|
+
minutes,
|
|
812
|
+
// An increment is something a clock has, and a game has one kind of time limit.
|
|
813
|
+
increment: minutes > 0 ? whole(r.increment, 0, 60, preset.increment) : 0,
|
|
814
|
+
days: minutes > 0 ? 0 : DAYS.includes(Number(r.days)) ? Number(r.days) : 0,
|
|
815
|
+
hints: r.hints !== false,
|
|
816
|
+
games: GAMES.includes(Number(r.games)) ? Number(r.games) : 1,
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
const rules = {
|
|
820
|
+
id: "chess",
|
|
821
|
+
name: "Chess",
|
|
822
|
+
emoji: "♟️",
|
|
823
|
+
blurb: "Checkmate the king, against the clock if you like",
|
|
824
|
+
tagline: "The whole game: castling, en passant, promotion and every kind of draw, with a chess clock, Chess960, and the moves written down as you play.",
|
|
825
|
+
seats: { min: 2, max: 2, default: 2 },
|
|
826
|
+
teams: false,
|
|
827
|
+
presets: PRESETS,
|
|
828
|
+
fields: [
|
|
829
|
+
{ key: "variant", label: "Starting position", kind: "choice", options: [["classic", "Classic"], ["960", "Chess960: a starting row drawn by lot"]] },
|
|
830
|
+
{ key: "white", label: "Who plays White", kind: "choice", options: [["random", "Luck of the draw"], ["seat1", "The first seat"], ["seat2", "The second seat"]] },
|
|
831
|
+
{ key: "minutes", label: "Clock: minutes each", hint: "Each side's time for the whole game. 0 for no clock", kind: "number", min: 0, max: 180 },
|
|
832
|
+
{ key: "increment", label: "Clock: seconds added a move", hint: "Added to your time after each move you make", kind: "number", min: 0, max: 60 },
|
|
833
|
+
{ key: "days", label: "Correspondence: time for each move", hint: "Instead of a clock, for a game played over days", kind: "choice", options: [[0, "No limit"], [1, "A day"], [2, "Two days"], [3, "Three days"], [7, "A week"]] },
|
|
834
|
+
{ key: "hints", label: "Show where a piece can go", hint: "Off for purists: a piece put down where it can't go is simply refused", kind: "toggle" },
|
|
835
|
+
{ key: "games", label: "Games in a match", kind: "choice", options: [[1, "Just one"], [2, "Two"], [3, "Three"], [4, "Four"], [6, "Six"]] },
|
|
836
|
+
],
|
|
837
|
+
normalize,
|
|
838
|
+
validate(r) {
|
|
839
|
+
return [...(r.minutes > 0 && r.days > 0 ? ["Pick a clock or a time for each move, not both."] : []), ...(r.increment > 0 && !(r.minutes > 0) ? ["Seconds added a move need a clock to be added to."] : [])];
|
|
840
|
+
},
|
|
841
|
+
describe(r) {
|
|
842
|
+
return [
|
|
843
|
+
r.variant === "960"
|
|
844
|
+
? "Chess960: the pieces behind the pawns start in one of 960 rows, drawn by lot and the same for both sides. The bishops are on opposite colours and the king is between the rooks."
|
|
845
|
+
: "Chess, from the classic starting position.",
|
|
846
|
+
`White moves first${r.white === "random" ? "; who plays White is the luck of the draw" : `, and that's the ${r.white === "seat1" ? "first" : "second"} seat`}. Tap a piece and then where it goes, or drag it there.`,
|
|
847
|
+
"You win by checkmate: the king is attacked and has no way out. You may never leave your own king in check.",
|
|
848
|
+
r.variant === "960"
|
|
849
|
+
? "Castling: the king and a rook that haven't moved, nothing between or in the way of either, and the king not in check or crossing an attacked square. They finish where they would in classic chess (king on the g- or c-file, rook beside it). To castle, put your king on that rook."
|
|
850
|
+
: "Castling: the king moves two squares towards a rook and the rook hops over it, if neither has moved, nothing is between them, and the king isn't in check or crossing an attacked square.",
|
|
851
|
+
"A pawn that has just moved two squares can be taken as if it had moved one (en passant), on the next move only. A pawn that reaches the far row becomes a queen, rook, bishop or knight: you choose.",
|
|
852
|
+
"It's a draw by stalemate (no legal move, and not in check) or when neither side has enough left to mate. Either player can claim a draw when the same position has come up three times, or after fifty moves each with no capture and no pawn moved; at five times, or seventy-five moves, the game is drawn anyway.",
|
|
853
|
+
...(r.minutes > 0
|
|
854
|
+
? [`Each side has ${r.minutes} minute${r.minutes === 1 ? "" : "s"} for all its moves${r.increment ? `, and gets ${r.increment} second${r.increment === 1 ? "" : "s"} back after each one` : ""}. The clocks start when White has moved. Run out of time and you lose, unless the other side has nothing left to mate with: then it's a draw. Computer players are on the clock too.`]
|
|
855
|
+
: r.days > 0
|
|
856
|
+
? [`Correspondence: you have ${r.days === 1 ? "a day" : r.days === 7 ? "a week" : `${r.days} days`} for each move, from White's first. Run out and you lose (a draw if the other side has nothing left to mate with).`]
|
|
857
|
+
: []),
|
|
858
|
+
...(r.hints ? [] : ["The squares a piece can go to aren't shown."]),
|
|
859
|
+
...(r.games > 1 ? [`A match is ${r.games} games; most wins takes it. Colours change every game.`] : []),
|
|
860
|
+
];
|
|
861
|
+
},
|
|
862
|
+
newGame: newChess,
|
|
863
|
+
apply,
|
|
864
|
+
view,
|
|
865
|
+
bot: botMove,
|
|
866
|
+
// A search: off the table's main thread (see TableGame.heavy).
|
|
867
|
+
heavy: (s, seat) => s.phase === "playing" && s.turn === seat,
|
|
868
|
+
// A player on the clock doesn't sit and look thoughtful with its time running.
|
|
869
|
+
thinkingTime(s, seat) {
|
|
870
|
+
const usual = thinkFor(6, s.record.length < 6 ? 0.6 : 1.1);
|
|
871
|
+
return s.bank && s.phase === "playing" ? Math.round(Math.min(usual, allowance(s, seat) * 0.5)) : usual;
|
|
872
|
+
},
|
|
873
|
+
deadline,
|
|
874
|
+
summary(s) {
|
|
875
|
+
const over = s.phase === "finished";
|
|
876
|
+
return {
|
|
877
|
+
game: "chess",
|
|
878
|
+
rules: s.rules.name,
|
|
879
|
+
phase: s.phase,
|
|
880
|
+
over,
|
|
881
|
+
hand: s.round,
|
|
882
|
+
scoreLine: `${s.wins[0]} to ${s.wins[1]}${s.draws ? ` · ${s.draws} drawn` : ""}`,
|
|
883
|
+
order: over ? order(s) : null,
|
|
884
|
+
winners: over ? winners(s) : null,
|
|
885
|
+
teams: false,
|
|
886
|
+
seats: s.seats,
|
|
887
|
+
};
|
|
888
|
+
},
|
|
889
|
+
warmUp,
|
|
890
|
+
};
|
|
891
|
+
/**
|
|
892
|
+
* Chess, with resigning and draw offers, and taking a move back in a friendly game with no clock
|
|
893
|
+
* (see offers.ts): a move can't be had back once time has been spent on it.
|
|
894
|
+
*/
|
|
895
|
+
export const chess = withOffers(rules, {
|
|
896
|
+
settle(s, result) {
|
|
897
|
+
s.ticking = false;
|
|
898
|
+
settleMatch(s, result);
|
|
899
|
+
},
|
|
900
|
+
takeBack: (s) => !s.bank && !(s.rules.days > 0),
|
|
901
|
+
standing,
|
|
902
|
+
});
|
|
903
|
+
export { MATE as CHESS_MATE };
|
|
904
|
+
//# sourceMappingURL=chess.js.map
|