@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/checkers.js
ADDED
|
@@ -0,0 +1,756 @@
|
|
|
1
|
+
import { settleMatch, withOffers } from "./offers.js";
|
|
2
|
+
import { cellName, chessCoords } from "./board.js";
|
|
3
|
+
import { careless, clone, GameError, markReady, nextHandChoices, nextRandom, pickRandom, thinkFor, who } from "./table.js";
|
|
4
|
+
export const americanCheckers = { id: "checkers-american", name: "American (8ร8)", size: 8, backward: false, flying: false, forced: true, most: false, crowning: "stop", opens: "dark", first: "random", quiet: 40, games: 1 };
|
|
5
|
+
export const internationalCheckers = { id: "checkers-international", name: "International (10ร10)", size: 10, backward: true, flying: true, forced: true, most: true, crowning: "pass", opens: "light", first: "random", quiet: 40, games: 1 };
|
|
6
|
+
export const russianCheckers = { id: "checkers-russian", name: "Russian (flying kings)", size: 8, backward: true, flying: true, forced: true, most: false, crowning: "king", opens: "light", first: "random", quiet: 40, games: 1 };
|
|
7
|
+
export const friendlyCheckers = { ...americanCheckers, id: "checkers-friendly", name: "Friendly (jumps optional)", forced: false };
|
|
8
|
+
const PRESETS = [americanCheckers, internationalCheckers, russianCheckers, friendlyCheckers];
|
|
9
|
+
const SIZES = [8, 10];
|
|
10
|
+
const other = (seat) => 1 - seat;
|
|
11
|
+
const geos = new Map();
|
|
12
|
+
function geoOf(n) {
|
|
13
|
+
let g = geos.get(n);
|
|
14
|
+
if (g)
|
|
15
|
+
return g;
|
|
16
|
+
const steps = [[-1, -1], [-1, 1], [1, -1], [1, 1]];
|
|
17
|
+
const row = new Int8Array(n * n);
|
|
18
|
+
const col = new Int8Array(n * n);
|
|
19
|
+
const dark = [];
|
|
20
|
+
const next = steps.map(() => new Int16Array(n * n));
|
|
21
|
+
for (let sq = 0; sq < n * n; sq++) {
|
|
22
|
+
const r = Math.floor(sq / n);
|
|
23
|
+
const c = sq % n;
|
|
24
|
+
row[sq] = r;
|
|
25
|
+
col[sq] = c;
|
|
26
|
+
if ((r + c) % 2 === 1)
|
|
27
|
+
dark.push(sq);
|
|
28
|
+
steps.forEach(([dr, dc], d) => {
|
|
29
|
+
const [r2, c2] = [r + dr, c + dc];
|
|
30
|
+
next[d][sq] = r2 >= 0 && r2 < n && c2 >= 0 && c2 < n ? r2 * n + c2 : -1;
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
g = { n, next, row, col, dark };
|
|
34
|
+
geos.set(n, g);
|
|
35
|
+
return g;
|
|
36
|
+
}
|
|
37
|
+
const genOf = (r) => ({ geo: geoOf(r.size), backward: r.backward, flying: r.flying, forced: r.forced, most: r.forced && r.most, crowning: r.crowning });
|
|
38
|
+
/** The row a side's men are crowned on. */
|
|
39
|
+
const crownRow = (g, side) => (side > 0 ? 0 : g.geo.n - 1);
|
|
40
|
+
/**
|
|
41
|
+
* How much looking for moves has been done: one for every piece looked at for jumps and every
|
|
42
|
+
* quiet move found (and a dozen for every position a search stops at). A computer player's thinking is rationed by this and not by the clock, so it
|
|
43
|
+
* plays the same on a slow machine, and a board full of flying kings can't make it slow.
|
|
44
|
+
*/
|
|
45
|
+
let work = 0;
|
|
46
|
+
/**
|
|
47
|
+
* The jumps `piece` can make from `at`, as [jumped square, landing square, โฆ]. `over` are pieces
|
|
48
|
+
* already jumped in this move: they are still on the board and in the way, but can't be taken again.
|
|
49
|
+
*/
|
|
50
|
+
function jumpsFrom(g, b, at, piece, over) {
|
|
51
|
+
work++;
|
|
52
|
+
const out = [];
|
|
53
|
+
const side = piece > 0 ? 1 : -1;
|
|
54
|
+
const king = piece === 2 || piece === -2;
|
|
55
|
+
for (let d = 0; d < 4; d++) {
|
|
56
|
+
// Up the board is forwards for the side that opened.
|
|
57
|
+
if (!king && !g.backward && d < 2 !== side > 0)
|
|
58
|
+
continue;
|
|
59
|
+
const next = g.geo.next[d];
|
|
60
|
+
let sq = next[at];
|
|
61
|
+
if (king && g.flying)
|
|
62
|
+
while (sq >= 0 && b[sq] === 0)
|
|
63
|
+
sq = next[sq];
|
|
64
|
+
if (sq < 0 || b[sq] * side >= 0 || over.includes(sq))
|
|
65
|
+
continue;
|
|
66
|
+
let land = next[sq];
|
|
67
|
+
while (land >= 0 && b[land] === 0) {
|
|
68
|
+
out.push(sq, land);
|
|
69
|
+
if (!(king && g.flying))
|
|
70
|
+
break;
|
|
71
|
+
land = next[land];
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
/** Follow every capture from `at` to its end, adding each finished line to `out`. The moving piece is off the board meanwhile. */
|
|
77
|
+
function follow(g, b, from, at, piece, wasMan, steps, over, out) {
|
|
78
|
+
const jumps = jumpsFrom(g, b, at, piece, over);
|
|
79
|
+
if (!jumps.length) {
|
|
80
|
+
if (steps.length)
|
|
81
|
+
out.push({ from, steps: [...steps], over: [...over], crown: wasMan && (Math.abs(piece) === 2 || g.geo.row[at] === crownRow(g, piece)) });
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
const side = piece > 0 ? 1 : -1;
|
|
85
|
+
for (let i = 0; i < jumps.length; i += 2) {
|
|
86
|
+
const victim = jumps[i];
|
|
87
|
+
const land = jumps[i + 1];
|
|
88
|
+
// A flying king that can land where the capture carries on has to (the biggest-capture rule
|
|
89
|
+
// settles it by itself). Landings past one piece come together in `jumps`.
|
|
90
|
+
if (g.forced && !g.most && Math.abs(piece) === 2 && g.flying) {
|
|
91
|
+
let carries = false;
|
|
92
|
+
for (let j = 0; j < jumps.length && !carries; j += 2)
|
|
93
|
+
if (jumps[j] === victim && jumpsFrom(g, b, jumps[j + 1], piece, [...over, victim]).length)
|
|
94
|
+
carries = true;
|
|
95
|
+
if (carries && !jumpsFrom(g, b, land, piece, [...over, victim]).length)
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
steps.push(land);
|
|
99
|
+
over.push(victim);
|
|
100
|
+
if (Math.abs(piece) === 1 && g.geo.row[land] === crownRow(g, side) && g.crowning !== "pass") {
|
|
101
|
+
if (g.crowning === "stop")
|
|
102
|
+
out.push({ from, steps: [...steps], over: [...over], crown: true });
|
|
103
|
+
else
|
|
104
|
+
follow(g, b, from, land, side * 2, true, steps, over, out);
|
|
105
|
+
}
|
|
106
|
+
else
|
|
107
|
+
follow(g, b, from, land, piece, wasMan, steps, over, out);
|
|
108
|
+
steps.pop();
|
|
109
|
+
over.pop();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Every move `side` may make. With `jump`, only ways to carry on the capture in progress (none
|
|
114
|
+
* when it is finished). Captures come first; when they are forced they are all there is.
|
|
115
|
+
*/
|
|
116
|
+
function linesFor(g, b, side, jump, capturesOnly = false) {
|
|
117
|
+
const caps = [];
|
|
118
|
+
const { dark, next, row } = g.geo;
|
|
119
|
+
if (jump) {
|
|
120
|
+
const piece = b[jump.at];
|
|
121
|
+
b[jump.at] = 0;
|
|
122
|
+
follow(g, b, jump.at, jump.at, piece, Math.abs(piece) === 1, [], [...jump.over], caps);
|
|
123
|
+
b[jump.at] = piece;
|
|
124
|
+
// The pieces jumped before now aren't part of what is still to come.
|
|
125
|
+
for (const line of caps)
|
|
126
|
+
line.over.splice(0, jump.over.length);
|
|
127
|
+
}
|
|
128
|
+
else {
|
|
129
|
+
for (const sq of dark) {
|
|
130
|
+
const piece = b[sq];
|
|
131
|
+
if (piece * side <= 0)
|
|
132
|
+
continue;
|
|
133
|
+
b[sq] = 0;
|
|
134
|
+
follow(g, b, sq, sq, piece, Math.abs(piece) === 1, [], [], caps);
|
|
135
|
+
b[sq] = piece;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (caps.length && g.most) {
|
|
139
|
+
const most = Math.max(...caps.map((l) => l.over.length));
|
|
140
|
+
return caps.filter((l) => l.over.length === most);
|
|
141
|
+
}
|
|
142
|
+
if (jump || capturesOnly || (caps.length && g.forced))
|
|
143
|
+
return caps;
|
|
144
|
+
for (const sq of dark) {
|
|
145
|
+
const piece = b[sq];
|
|
146
|
+
if (piece * side <= 0)
|
|
147
|
+
continue;
|
|
148
|
+
const king = Math.abs(piece) === 2;
|
|
149
|
+
for (let d = 0; d < 4; d++) {
|
|
150
|
+
if (!king && d < 2 !== side > 0)
|
|
151
|
+
continue;
|
|
152
|
+
let to = next[d][sq];
|
|
153
|
+
while (to >= 0 && b[to] === 0) {
|
|
154
|
+
caps.push({ from: sq, steps: [to], over: [], crown: !king && row[to] === crownRow(g, side) });
|
|
155
|
+
if (!(king && g.flying))
|
|
156
|
+
break;
|
|
157
|
+
to = next[d][to];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
work += caps.length;
|
|
162
|
+
return caps;
|
|
163
|
+
}
|
|
164
|
+
/** Whether `side` has a move that isn't a capture: enough to tell a quiet position from a lost one. */
|
|
165
|
+
function canStep(g, b, side) {
|
|
166
|
+
const { dark, next } = g.geo;
|
|
167
|
+
for (const sq of dark) {
|
|
168
|
+
const piece = b[sq];
|
|
169
|
+
if (piece * side <= 0)
|
|
170
|
+
continue;
|
|
171
|
+
const king = piece === 2 || piece === -2;
|
|
172
|
+
for (let d = 0; d < 4; d++) {
|
|
173
|
+
if (!king && d < 2 !== side > 0)
|
|
174
|
+
continue;
|
|
175
|
+
const to = next[d][sq];
|
|
176
|
+
if (to >= 0 && b[to] === 0)
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return false;
|
|
181
|
+
}
|
|
182
|
+
/** The side of the board a seat plays from: 1 moves up (it opened), โ1 moves down. */
|
|
183
|
+
const sideOf = (s, seat) => (seat === s.first ? 1 : -1);
|
|
184
|
+
const gridOf = (s) => s.cells.map((p) => (p ? sideOf(s, p.o) * (p.k ? 2 : 1) : 0));
|
|
185
|
+
/** Every whole move the player to move may make from here (or every way to carry on a capture in progress). */
|
|
186
|
+
export function checkersLines(s) {
|
|
187
|
+
if (s.phase !== "playing")
|
|
188
|
+
return [];
|
|
189
|
+
return linesFor(genOf(s.rules), gridOf(s), sideOf(s, s.turn), s.jump);
|
|
190
|
+
}
|
|
191
|
+
function startRound(s) {
|
|
192
|
+
const n = s.rules.size;
|
|
193
|
+
const rows = n / 2 - 1;
|
|
194
|
+
let id = 0;
|
|
195
|
+
s.cells = Array.from({ length: n * n }, (_, sq) => {
|
|
196
|
+
const r = Math.floor(sq / n);
|
|
197
|
+
if ((r + (sq % n)) % 2 === 0 || (r >= rows && r < n - rows))
|
|
198
|
+
return null;
|
|
199
|
+
return { o: r < rows ? other(s.first) : s.first, k: false, id: id++ };
|
|
200
|
+
});
|
|
201
|
+
s.turn = s.first;
|
|
202
|
+
s.jump = null;
|
|
203
|
+
s.last = null;
|
|
204
|
+
s.taken = [{ men: 0, kings: 0 }, { men: 0, kings: 0 }];
|
|
205
|
+
s.quiet = 0;
|
|
206
|
+
s.seen = { [keyOf(s)]: 1 };
|
|
207
|
+
s.roundWinner = null;
|
|
208
|
+
s.phase = "playing";
|
|
209
|
+
s.ready = Array(s.seats).fill(false);
|
|
210
|
+
s.narration = null;
|
|
211
|
+
}
|
|
212
|
+
export function newCheckers(rules, seats, seed) {
|
|
213
|
+
const s = { game: "checkers", rules, seats: 2, rng: seed >>> 0, round: 1, wins: [0, 0], draws: 0 };
|
|
214
|
+
s.first = rules.first === "seat1" ? 0 : rules.first === "seat2" ? 1 : nextRandom(s)() < 0.5 ? 0 : 1;
|
|
215
|
+
startRound(s);
|
|
216
|
+
return s;
|
|
217
|
+
}
|
|
218
|
+
/** A position, for telling when it has come round again: what is where, and whose move it is. */
|
|
219
|
+
const keyOf = (s) => s.cells.map((p) => (p ? "mkMK"[p.o * 2 + (p.k ? 1 : 0)] : ".")).join("") + s.turn;
|
|
220
|
+
// ---- Moves --------------------------------------------------------------------------------------
|
|
221
|
+
const coordsOf = (s) => ({ cols: s.rules.size, coords: chessCoords(s.rules.size, s.rules.size) });
|
|
222
|
+
const at = (s, sq) => cellName(coordsOf(s), sq);
|
|
223
|
+
/** How a match stands once this game is in: finished when the games run out or one side can't be caught. */
|
|
224
|
+
function matchOver(s) {
|
|
225
|
+
const left = s.rules.games - s.round;
|
|
226
|
+
return left <= 0 || Math.abs(s.wins[0] - s.wins[1]) > left;
|
|
227
|
+
}
|
|
228
|
+
function endRound(s, winner, why) {
|
|
229
|
+
const many = s.rules.games > 1;
|
|
230
|
+
if (winner === null)
|
|
231
|
+
s.draws += 1;
|
|
232
|
+
else
|
|
233
|
+
s.wins[winner] += 1;
|
|
234
|
+
s.roundWinner = winner;
|
|
235
|
+
s.narration = winner === null ? `${why}: ${many ? `game ${s.round} is a draw` : "it's a draw"}` : `${why}: ${who(winner)} wins${many ? ` game ${s.round}` : ""}!`;
|
|
236
|
+
s.phase = matchOver(s) ? "finished" : "scored";
|
|
237
|
+
}
|
|
238
|
+
/** The move is done: take off what was jumped, crown, pass the turn, and see whether the game is over. Changes `s`. */
|
|
239
|
+
function finish(s, seat, path, over, crownedOnTheWay) {
|
|
240
|
+
const g = genOf(s.rules);
|
|
241
|
+
const to = path.at(-1);
|
|
242
|
+
const piece = s.cells[to];
|
|
243
|
+
const wasMan = !piece.k || crownedOnTheWay;
|
|
244
|
+
for (const sq of over) {
|
|
245
|
+
s.taken[seat][s.cells[sq].k ? "kings" : "men"] += 1;
|
|
246
|
+
s.cells[sq] = null;
|
|
247
|
+
}
|
|
248
|
+
const crowned = crownedOnTheWay || (!piece.k && g.geo.row[to] === crownRow(g, sideOf(s, seat)));
|
|
249
|
+
if (crowned)
|
|
250
|
+
piece.k = true;
|
|
251
|
+
s.jump = null;
|
|
252
|
+
s.last = path;
|
|
253
|
+
s.turn = other(seat);
|
|
254
|
+
// Nothing before a capture or a man's move can come round again.
|
|
255
|
+
if (over.length || wasMan) {
|
|
256
|
+
s.quiet = 0;
|
|
257
|
+
s.seen = {};
|
|
258
|
+
}
|
|
259
|
+
else
|
|
260
|
+
s.quiet += 1;
|
|
261
|
+
const key = keyOf(s);
|
|
262
|
+
s.seen[key] = (s.seen[key] ?? 0) + 1;
|
|
263
|
+
s.narration = `${who(seat)} ${over.length ? `jumps ${path.map((sq) => at(s, sq)).join("ร")}${over.length > 1 ? `, taking ${over.length}` : ""}` : `moves ${at(s, path[0])} to ${at(s, to)}`}${crowned ? " and is crowned" : ""}`;
|
|
264
|
+
if (!linesFor(g, gridOf(s), sideOf(s, s.turn)).length)
|
|
265
|
+
endRound(s, seat, s.cells.some((p) => p?.o === s.turn) ? `${who(s.turn)} has no move left` : `${who(seat)} has taken every piece`);
|
|
266
|
+
else if (s.seen[key] >= 3)
|
|
267
|
+
endRound(s, null, "The same position three times");
|
|
268
|
+
else if (s.quiet >= s.rules.quiet * 2)
|
|
269
|
+
endRound(s, null, `${s.rules.quiet} moves each with no capture and no man moved`);
|
|
270
|
+
}
|
|
271
|
+
function apply(state, seat, move) {
|
|
272
|
+
if (move?.type === "next") {
|
|
273
|
+
if (state.phase !== "scored")
|
|
274
|
+
throw new GameError("There's a game going on.");
|
|
275
|
+
const s = clone(state);
|
|
276
|
+
if (markReady(s.ready, seat)) {
|
|
277
|
+
// Take turns moving first.
|
|
278
|
+
s.round += 1;
|
|
279
|
+
s.first = other(s.first);
|
|
280
|
+
startRound(s);
|
|
281
|
+
}
|
|
282
|
+
return s;
|
|
283
|
+
}
|
|
284
|
+
if (move?.type !== "move" && move?.type !== "stop")
|
|
285
|
+
throw new GameError("That's not a move in Checkers.");
|
|
286
|
+
if (state.phase !== "playing")
|
|
287
|
+
throw new GameError("This game is over.");
|
|
288
|
+
if (state.turn !== seat)
|
|
289
|
+
throw new GameError("It's not your turn.");
|
|
290
|
+
if (move.type === "stop") {
|
|
291
|
+
if (!state.jump)
|
|
292
|
+
throw new GameError("You haven't moved yet.");
|
|
293
|
+
if (state.rules.forced)
|
|
294
|
+
throw new GameError("You have to finish the capture.");
|
|
295
|
+
const s = clone(state);
|
|
296
|
+
finish(s, seat, s.jump.path, s.jump.over, s.jump.crowned);
|
|
297
|
+
return s;
|
|
298
|
+
}
|
|
299
|
+
const from = Number(move.from);
|
|
300
|
+
const to = Number(move.to);
|
|
301
|
+
const on = (sq) => Number.isInteger(sq) && sq >= 0 && sq < state.cells.length;
|
|
302
|
+
if (!on(from) || !on(to))
|
|
303
|
+
throw new GameError("That's not on the board.");
|
|
304
|
+
const lines = checkersLines(state);
|
|
305
|
+
const line = lines.find((l) => l.from === from && l.steps[0] === to);
|
|
306
|
+
if (!line) {
|
|
307
|
+
if (state.jump && from !== state.jump.at)
|
|
308
|
+
throw new GameError("Carry on with the piece that's jumping.");
|
|
309
|
+
if (state.cells[from]?.o !== seat)
|
|
310
|
+
throw new GameError("That's not your piece.");
|
|
311
|
+
if (state.rules.forced && lines[0]?.over.length)
|
|
312
|
+
throw new GameError(state.rules.most ? "You have to make the capture that takes the most." : "You have to capture.");
|
|
313
|
+
throw new GameError("That piece can't go there.");
|
|
314
|
+
}
|
|
315
|
+
const s = clone(state);
|
|
316
|
+
const g = genOf(s.rules);
|
|
317
|
+
const piece = s.cells[from];
|
|
318
|
+
s.cells[from] = null;
|
|
319
|
+
s.cells[to] = piece;
|
|
320
|
+
if (!line.over.length) {
|
|
321
|
+
finish(s, seat, [from, to], [], false);
|
|
322
|
+
return s;
|
|
323
|
+
}
|
|
324
|
+
const jump = s.jump ?? { from, at: from, path: [from], over: [], crowned: false };
|
|
325
|
+
jump.at = to;
|
|
326
|
+
jump.path.push(to);
|
|
327
|
+
jump.over.push(line.over[0]);
|
|
328
|
+
// A man on the far row with more to take: it stops there, carries on as a man, or carries on as a king.
|
|
329
|
+
const reached = !piece.k && g.geo.row[to] === crownRow(g, sideOf(s, seat));
|
|
330
|
+
if (reached && s.rules.crowning === "king") {
|
|
331
|
+
piece.k = true;
|
|
332
|
+
jump.crowned = true;
|
|
333
|
+
}
|
|
334
|
+
const more = reached && s.rules.crowning === "stop" ? [] : linesFor(g, gridOf(s), sideOf(s, seat), jump);
|
|
335
|
+
if (!more.length) {
|
|
336
|
+
finish(s, seat, jump.path, jump.over, jump.crowned);
|
|
337
|
+
return s;
|
|
338
|
+
}
|
|
339
|
+
s.jump = jump;
|
|
340
|
+
s.narration = `${who(seat)} jumps ${jump.path.map((sq) => at(s, sq)).join("ร")}โฆ`;
|
|
341
|
+
return s;
|
|
342
|
+
}
|
|
343
|
+
/** Best first. A drawn match lists the player who moved second in the last game first: holding the first move to a draw is the better result. */
|
|
344
|
+
function order(s) {
|
|
345
|
+
const [a, b] = s.wins;
|
|
346
|
+
if (a !== b)
|
|
347
|
+
return a > b ? [0, 1] : [1, 0];
|
|
348
|
+
return [other(s.first), s.first];
|
|
349
|
+
}
|
|
350
|
+
const winners = (s) => (s.wins[0] === s.wins[1] ? [0, 1] : [s.wins[0] > s.wins[1] ? 0 : 1]);
|
|
351
|
+
// ---- What a seat sees ---------------------------------------------------------------------------
|
|
352
|
+
/** The colour a seat plays: whoever moves first has the colour the rules say opens. */
|
|
353
|
+
const colourOf = (s, seat) => ((seat === s.first) === (s.rules.opens === "dark") ? "dark" : "light");
|
|
354
|
+
function pieceOf(s, seat, king) {
|
|
355
|
+
const colour = colourOf(s, seat);
|
|
356
|
+
return { owner: seat, kind: king ? "king" : "man", glyph: king ? "โ" : "โ", label: `${colour} ${king ? "king" : "man"}`, tone: colour };
|
|
357
|
+
}
|
|
358
|
+
function view(s, seat) {
|
|
359
|
+
const over = s.phase === "finished";
|
|
360
|
+
const playing = s.phase === "playing";
|
|
361
|
+
const mine = playing && s.turn === seat;
|
|
362
|
+
const many = s.rules.games > 1;
|
|
363
|
+
const n = s.rules.size;
|
|
364
|
+
const w = over ? winners(s) : [];
|
|
365
|
+
const lines = checkersLines(s);
|
|
366
|
+
const capturing = lines.length > 0 && lines[0].over.length > 0;
|
|
367
|
+
const marks = [];
|
|
368
|
+
const path = s.jump?.path ?? s.last;
|
|
369
|
+
if (path) {
|
|
370
|
+
for (const sq of path.slice(0, -1))
|
|
371
|
+
marks.push({ cell: sq, mark: "from" });
|
|
372
|
+
marks.push({ cell: path.at(-1), mark: "last" });
|
|
373
|
+
}
|
|
374
|
+
// When a capture has to be made, the pieces that have to make it are lit for both players.
|
|
375
|
+
if (s.rules.forced && capturing)
|
|
376
|
+
for (const from of new Set(lines.map((l) => l.from)))
|
|
377
|
+
marks.push({ cell: from, mark: "must" });
|
|
378
|
+
const sources = new Map();
|
|
379
|
+
if (mine)
|
|
380
|
+
for (const l of lines)
|
|
381
|
+
if (!sources.get(l.from)?.includes(l.steps[0]))
|
|
382
|
+
sources.set(l.from, [...(sources.get(l.from) ?? []), l.steps[0]]);
|
|
383
|
+
const grid = {
|
|
384
|
+
id: "board",
|
|
385
|
+
label: "Board",
|
|
386
|
+
rows: n,
|
|
387
|
+
...coordsOf(s),
|
|
388
|
+
shade: "checker",
|
|
389
|
+
// The side that opened is at the bottom of `cells`; the other seat sees the board turned round.
|
|
390
|
+
flip: seat !== s.first,
|
|
391
|
+
cells: s.cells.map((p, sq) => (p ? { ...pieceOf(s, p.o, p.k), id: `p${p.id}`, ...(s.jump?.over.includes(sq) ? { faded: true } : {}) } : null)),
|
|
392
|
+
marks,
|
|
393
|
+
select: mine ? { type: "move", sources: [...sources].map(([from, to]) => ({ from, to })), ...(s.jump ? { held: s.jump.at } : {}) } : null,
|
|
394
|
+
};
|
|
395
|
+
const left = s.rules.quiet * 2 - s.quiet;
|
|
396
|
+
const scoreTitle = s.roundWinner === null ? `Game ${s.round} is a draw` : `${who(s.roundWinner)} wins game ${s.round}`;
|
|
397
|
+
return {
|
|
398
|
+
game: "checkers",
|
|
399
|
+
seats: s.seats,
|
|
400
|
+
me: seat,
|
|
401
|
+
teams: false,
|
|
402
|
+
phase: s.phase,
|
|
403
|
+
turn: playing ? s.turn : null,
|
|
404
|
+
grids: [grid],
|
|
405
|
+
reserves: [0, 1].map((o) => ({
|
|
406
|
+
owner: o,
|
|
407
|
+
label: "Captured",
|
|
408
|
+
pieces: [
|
|
409
|
+
{ piece: pieceOf(s, other(o), false), count: s.taken[o].men },
|
|
410
|
+
{ piece: pieceOf(s, other(o), true), count: s.taken[o].kings },
|
|
411
|
+
],
|
|
412
|
+
})),
|
|
413
|
+
choices: s.phase === "scored"
|
|
414
|
+
? nextHandChoices(s.ready, seat, false, "Next game")
|
|
415
|
+
: mine && s.jump && !s.rules.forced
|
|
416
|
+
? { prompt: "", options: [{ label: "Stop here", move: { type: "stop" }, hint: "End your move without jumping again" }] }
|
|
417
|
+
: null,
|
|
418
|
+
players: [0, 1].map((i) => {
|
|
419
|
+
const count = s.cells.filter((p) => p?.o === i).length;
|
|
420
|
+
return {
|
|
421
|
+
line: [`${count} piece${count === 1 ? "" : "s"}`, ...(many ? [`${s.wins[i]} win${s.wins[i] === 1 ? "" : "s"}`] : [])].join(" ยท "),
|
|
422
|
+
piece: pieceOf(s, i, false),
|
|
423
|
+
badge: playing && s.last === null && !s.jump && i === s.first ? "Goes first" : undefined,
|
|
424
|
+
};
|
|
425
|
+
}),
|
|
426
|
+
info: [
|
|
427
|
+
...(many ? [`Game ${s.round} of ${s.rules.games}`] : []),
|
|
428
|
+
...(playing && s.rules.forced && capturing ? [s.rules.most ? "The biggest capture has to be taken" : s.jump ? "The capture has to be finished" : "A capture has to be taken"] : []),
|
|
429
|
+
...(playing && left <= Math.min(20, s.rules.quiet) ? [`A draw in ${Math.ceil(left / 2)} more move${left <= 2 ? "" : "s"} each without a capture`] : []),
|
|
430
|
+
],
|
|
431
|
+
narration: s.narration ?? (mine ? `You're ${colourOf(s, seat)}: you go first` : null),
|
|
432
|
+
scores: s.wins.map((value, i) => ({ seats: [i], value })),
|
|
433
|
+
result: many && !playing
|
|
434
|
+
? { 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)] }] : [])] }
|
|
435
|
+
: null,
|
|
436
|
+
over: over ? { winners: w, title: w.length > 1 ? "It's a draw!" : `${who(w[0])} wins${many ? ` ${s.wins[w[0]]} to ${s.wins[other(w[0])]}` : ""}!` } : null,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
// ---- Computer players ---------------------------------------------------------------------------
|
|
440
|
+
// Look ahead, alpha-beta, a whole move at a time (a multi-jump is one move to the search, however
|
|
441
|
+
// many taps it takes to play). The search never stops in the middle of an exchange: when the side
|
|
442
|
+
// to move can capture it keeps looking until the board is quiet. What is left is judged by
|
|
443
|
+
// material, with kings worth more where they fly, and a little for men that have got on, for men
|
|
444
|
+
// still guarding the back row, and for holding the middle.
|
|
445
|
+
//
|
|
446
|
+
// How far ahead goes by skill, and a ration of looking (see `work`) keeps the slowest move short on
|
|
447
|
+
// any board: a 10ร10 with flying kings has far more to look at than an 8ร8. A careless player (see
|
|
448
|
+
// `careless`) plays the first thing it sees.
|
|
449
|
+
const WIN = 1_000_000;
|
|
450
|
+
/** How much of a man (100) luck is worth when moves are about as good as each other. */
|
|
451
|
+
const LUCK = 6;
|
|
452
|
+
/**
|
|
453
|
+
* The most looking a computer player does over one move, in units of `work`. At this the slowest
|
|
454
|
+
* move measured was about 25 ms on every board (10ร10 with flying kings included) and the usual
|
|
455
|
+
* one 8 ms; a busy machine took three times that.
|
|
456
|
+
*/
|
|
457
|
+
const CAP = 150_000;
|
|
458
|
+
/** Thrown when a search has done all the looking it may; the last depth it finished stands. */
|
|
459
|
+
const SPENT = new Error("spent");
|
|
460
|
+
function judge(g, b, side) {
|
|
461
|
+
const { n, dark, row, col, next } = g.geo;
|
|
462
|
+
const kingWorth = g.flying ? 320 : 160;
|
|
463
|
+
let total = 0;
|
|
464
|
+
// Pieces, and squares they could step to, for the side moving up and the side moving down.
|
|
465
|
+
let up = 0;
|
|
466
|
+
let down = 0;
|
|
467
|
+
let upSteps = 0;
|
|
468
|
+
let downSteps = 0;
|
|
469
|
+
for (const sq of dark) {
|
|
470
|
+
const piece = b[sq];
|
|
471
|
+
if (!piece)
|
|
472
|
+
continue;
|
|
473
|
+
const own = piece > 0 ? 1 : -1;
|
|
474
|
+
const king = piece === 2 || piece === -2;
|
|
475
|
+
// How near the middle of the board, 0 at the edge.
|
|
476
|
+
const inCol = Math.min(col[sq], n - 1 - col[sq]);
|
|
477
|
+
let worth;
|
|
478
|
+
if (king)
|
|
479
|
+
worth = kingWorth + (inCol + Math.min(row[sq], n - 1 - row[sq])) * 2;
|
|
480
|
+
else {
|
|
481
|
+
const gone = own > 0 ? n - 1 - row[sq] : row[sq];
|
|
482
|
+
// Men are worth more as they get on, and the ones at home keep the other side from crowning.
|
|
483
|
+
worth = 100 + gone * 3 + (gone === 0 ? 8 : 0) + inCol;
|
|
484
|
+
}
|
|
485
|
+
total += own * worth;
|
|
486
|
+
let steps = 0;
|
|
487
|
+
for (let d = king ? 0 : own > 0 ? 0 : 2, end = king ? 4 : own > 0 ? 2 : 4; d < end; d++) {
|
|
488
|
+
const to = next[d][sq];
|
|
489
|
+
if (to >= 0 && b[to] === 0)
|
|
490
|
+
steps++;
|
|
491
|
+
}
|
|
492
|
+
if (own > 0)
|
|
493
|
+
[up, upSteps] = [up + 1, upSteps + steps];
|
|
494
|
+
else
|
|
495
|
+
[down, downSteps] = [down + 1, downSteps + steps];
|
|
496
|
+
}
|
|
497
|
+
// Ahead with little left on the board: swapping pieces off is winning.
|
|
498
|
+
total += ((up - down) * 240) / (up + down + 2);
|
|
499
|
+
// Room to move, and above all not running out of it: a side with nothing to move has lost.
|
|
500
|
+
total += (upSteps - downSteps) * 2 - 60 / (1 + upSteps) + 60 / (1 + downSteps);
|
|
501
|
+
return total * side;
|
|
502
|
+
}
|
|
503
|
+
function make(b, line, undo) {
|
|
504
|
+
const piece = b[line.from];
|
|
505
|
+
undo.length = 0;
|
|
506
|
+
for (const sq of line.over) {
|
|
507
|
+
undo.push(b[sq]);
|
|
508
|
+
b[sq] = 0;
|
|
509
|
+
}
|
|
510
|
+
b[line.from] = 0;
|
|
511
|
+
b[line.steps.length ? line.steps[line.steps.length - 1] : line.from] = line.crown ? (piece > 0 ? 2 : -2) : piece;
|
|
512
|
+
return piece;
|
|
513
|
+
}
|
|
514
|
+
function unmake(b, line, undo, piece) {
|
|
515
|
+
b[line.steps.length ? line.steps[line.steps.length - 1] : line.from] = 0;
|
|
516
|
+
b[line.from] = piece;
|
|
517
|
+
for (let i = 0; i < line.over.length; i++)
|
|
518
|
+
b[line.over[i]] = undo[i];
|
|
519
|
+
}
|
|
520
|
+
/** The value of the position for `side`, who is about to move, looking `depth` whole moves ahead. */
|
|
521
|
+
function search(g, b, side, depth, alpha, beta, ply, look) {
|
|
522
|
+
// Every position costs something to look at, however little is on the board.
|
|
523
|
+
if ((work += 12) > look.cap)
|
|
524
|
+
throw SPENT;
|
|
525
|
+
let best = -Infinity;
|
|
526
|
+
let lines;
|
|
527
|
+
if (depth <= 0) {
|
|
528
|
+
// As far as it looks, except that an exchange is followed to its end.
|
|
529
|
+
lines = ply > 40 ? [] : linesFor(g, b, side, null, true);
|
|
530
|
+
// Nothing to move loses, the sooner the worse.
|
|
531
|
+
if (!lines.length)
|
|
532
|
+
return canStep(g, b, side) || ply > 40 ? judge(g, b, side) : -WIN + ply;
|
|
533
|
+
if (!g.forced) {
|
|
534
|
+
// A capture that needn't be taken: the position is worth at least what it is as it stands.
|
|
535
|
+
best = judge(g, b, side);
|
|
536
|
+
if (best >= beta)
|
|
537
|
+
return best;
|
|
538
|
+
if (best > alpha)
|
|
539
|
+
alpha = best;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
else {
|
|
543
|
+
lines = linesFor(g, b, side);
|
|
544
|
+
if (!lines.length)
|
|
545
|
+
return -WIN + ply;
|
|
546
|
+
}
|
|
547
|
+
const size = g.geo.n * g.geo.n;
|
|
548
|
+
if (lines.length > 1) {
|
|
549
|
+
const good = look.good;
|
|
550
|
+
const rank = (l) => (l.over.length ? 1e9 + l.over.length * 10 : good[l.from * size + l.steps[0]]) + (l.crown ? 5e8 : 0);
|
|
551
|
+
lines.sort((x, y) => rank(y) - rank(x));
|
|
552
|
+
}
|
|
553
|
+
const undo = [];
|
|
554
|
+
for (const line of lines) {
|
|
555
|
+
const piece = make(b, line, undo);
|
|
556
|
+
const value = -search(g, b, -side, depth - 1, -beta, -alpha, ply + 1, look);
|
|
557
|
+
unmake(b, line, undo, piece);
|
|
558
|
+
if (value > best)
|
|
559
|
+
best = value;
|
|
560
|
+
if (best > alpha)
|
|
561
|
+
alpha = best;
|
|
562
|
+
if (alpha >= beta) {
|
|
563
|
+
if (!line.over.length && depth > 0)
|
|
564
|
+
look.good[line.from * size + line.steps[0]] += depth * depth;
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
return best;
|
|
569
|
+
}
|
|
570
|
+
/**
|
|
571
|
+
* The best whole move for the player to move, looking up to `depth` moves ahead and doing no more than
|
|
572
|
+
* `cap` of work (see `work`). In the middle of a capture it is the best way to carry on; a line with no steps
|
|
573
|
+
* means stopping where the piece stands (only when captures aren't forced). Null when there is
|
|
574
|
+
* nothing to do.
|
|
575
|
+
*/
|
|
576
|
+
export function bestCheckersLine(s, depth, cap, rand = Math.random) {
|
|
577
|
+
const g = genOf(s.rules);
|
|
578
|
+
// A copy to try moves on: a search that runs out of positions leaves it half played.
|
|
579
|
+
const b = Int8Array.from(gridOf(s));
|
|
580
|
+
const side = sideOf(s, s.turn);
|
|
581
|
+
const lines = linesFor(g, b, side, s.jump);
|
|
582
|
+
if (s.jump && !s.rules.forced)
|
|
583
|
+
lines.push({ from: s.jump.at, steps: [], over: [], crown: false });
|
|
584
|
+
if (lines.length < 2)
|
|
585
|
+
return lines[0] ?? null;
|
|
586
|
+
// What each move leaves on the board: with a capture under way, the pieces already jumped go too.
|
|
587
|
+
const before = s.jump?.over ?? [];
|
|
588
|
+
const crownRowHere = crownRow(g, side);
|
|
589
|
+
const options = lines.map((line) => ({
|
|
590
|
+
line,
|
|
591
|
+
whole: { ...line, over: [...before, ...line.over], crown: line.crown || (!!s.jump && Math.abs(b[s.jump.at]) === 1 && !line.steps.length && g.geo.row[s.jump.at] === crownRowHere) },
|
|
592
|
+
value: 0,
|
|
593
|
+
luck: rand(),
|
|
594
|
+
}));
|
|
595
|
+
// Going back to a position that has already come up is how a won game turns into a draw.
|
|
596
|
+
const repeats = (o) => {
|
|
597
|
+
if (o.whole.over.length)
|
|
598
|
+
return 0;
|
|
599
|
+
const undo = [];
|
|
600
|
+
const piece = make(b, o.whole, undo);
|
|
601
|
+
const key = Array.from(b, (v) => (v ? "mkMK"[(v * side > 0 ? s.turn : other(s.turn)) * 2 + (v === 2 || v === -2 ? 1 : 0)] : ".")).join("") + other(s.turn);
|
|
602
|
+
unmake(b, o.whole, undo, piece);
|
|
603
|
+
return s.seen[key] ?? 0;
|
|
604
|
+
};
|
|
605
|
+
const look = { cap: work + cap, good: new Int32Array(b.length * b.length) };
|
|
606
|
+
let best = options[0];
|
|
607
|
+
const undo = [];
|
|
608
|
+
try {
|
|
609
|
+
for (let d = 1; d <= depth; d++) {
|
|
610
|
+
let top = -Infinity;
|
|
611
|
+
let pick = options[0];
|
|
612
|
+
for (const o of options) {
|
|
613
|
+
const piece = make(b, o.whole, undo);
|
|
614
|
+
// A move is only scored exactly when it comes within a whisker of the best so far: that is
|
|
615
|
+
// all that's needed, and the whisker is where luck gets in, so no two games go the same way.
|
|
616
|
+
o.value = -search(g, b, -side, d - 1, -Infinity, -(top - LUCK), 1, look) + o.luck * LUCK;
|
|
617
|
+
unmake(b, o.whole, undo, piece);
|
|
618
|
+
if (o.value > 0 && o.value < WIN / 2)
|
|
619
|
+
o.value -= repeats(o) * 60;
|
|
620
|
+
if (o.value > top)
|
|
621
|
+
[top, pick] = [o.value, o];
|
|
622
|
+
}
|
|
623
|
+
best = pick;
|
|
624
|
+
// Next time round, look at the moves that did best first: it prunes far more.
|
|
625
|
+
options.sort((x, y) => y.value - x.value);
|
|
626
|
+
if (top > WIN / 2 || top < -WIN / 2)
|
|
627
|
+
break;
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
catch (e) {
|
|
631
|
+
if (e !== SPENT)
|
|
632
|
+
throw e;
|
|
633
|
+
}
|
|
634
|
+
return best.line;
|
|
635
|
+
}
|
|
636
|
+
/** How far ahead a computer player looks, and how much looking it may do, by skill. */
|
|
637
|
+
function reach(style) {
|
|
638
|
+
const skill = style.skill ?? 1;
|
|
639
|
+
return skill >= 0.9 ? [10, CAP] : skill >= 0.5 ? [4, CAP / 3] : [2, CAP / 8];
|
|
640
|
+
}
|
|
641
|
+
function bot(s, seat, style, rand = Math.random) {
|
|
642
|
+
if (s.phase === "scored")
|
|
643
|
+
return s.ready[seat] ? null : { type: "next" };
|
|
644
|
+
if (s.phase !== "playing" || s.turn !== seat)
|
|
645
|
+
return null;
|
|
646
|
+
if (careless(style, rand)) {
|
|
647
|
+
// Whatever comes to hand, though even a careless player finishes a capture it has started.
|
|
648
|
+
const line = pickRandom(checkersLines(s), rand);
|
|
649
|
+
return { type: "move", from: line.from, to: line.steps[0] };
|
|
650
|
+
}
|
|
651
|
+
const line = bestCheckersLine(s, ...reach(style), rand);
|
|
652
|
+
return line.steps.length ? { type: "move", from: line.from, to: line.steps[0] } : { type: "stop" };
|
|
653
|
+
}
|
|
654
|
+
// ---- The game -----------------------------------------------------------------------------------
|
|
655
|
+
function normalize(rules) {
|
|
656
|
+
const given = (rules && typeof rules === "object" ? rules : {});
|
|
657
|
+
const base = PRESETS.find((p) => p.id === given.id) ?? americanCheckers;
|
|
658
|
+
const r = { ...base, ...given };
|
|
659
|
+
const forced = !!r.forced;
|
|
660
|
+
return {
|
|
661
|
+
id: String(r.id),
|
|
662
|
+
name: String(r.name),
|
|
663
|
+
size: SIZES.includes(Number(r.size)) ? Number(r.size) : base.size,
|
|
664
|
+
backward: !!r.backward,
|
|
665
|
+
flying: !!r.flying,
|
|
666
|
+
forced,
|
|
667
|
+
// The biggest capture can only be insisted on when captures are.
|
|
668
|
+
most: forced && !!r.most,
|
|
669
|
+
crowning: r.crowning === "pass" || r.crowning === "king" ? r.crowning : "stop",
|
|
670
|
+
opens: r.opens === "light" ? "light" : "dark",
|
|
671
|
+
first: r.first === "seat1" || r.first === "seat2" ? r.first : "random",
|
|
672
|
+
quiet: Math.min(100, Math.max(10, Math.round(Number(r.quiet)) || 40)),
|
|
673
|
+
games: [1, 3, 5, 7].includes(Number(r.games)) ? Number(r.games) : 1,
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
const rules = {
|
|
677
|
+
id: "checkers",
|
|
678
|
+
name: "Checkers",
|
|
679
|
+
emoji: "๐",
|
|
680
|
+
blurb: "Jump their pieces and crown your kings",
|
|
681
|
+
tagline: "Draughts the way your family plays it: American, International on the big board, Russian, or friendly with no forced jumps.",
|
|
682
|
+
seats: { min: 2, max: 2, default: 2 },
|
|
683
|
+
teams: false,
|
|
684
|
+
presets: PRESETS,
|
|
685
|
+
fields: [
|
|
686
|
+
{ key: "size", label: "Board", kind: "choice", options: [[8, "8ร8, 12 men each"], [10, "10ร10, 20 men each"]] },
|
|
687
|
+
{ key: "forced", label: "Captures are forced", hint: "If you can jump, you have to", kind: "toggle" },
|
|
688
|
+
{ key: "most", label: "Must take the most", hint: "With forced captures: the jump that takes the most pieces is the one you have to make", kind: "toggle" },
|
|
689
|
+
{ key: "backward", label: "Men capture backwards", kind: "toggle" },
|
|
690
|
+
{ key: "flying", label: "Flying kings", hint: "Kings move and capture any distance along a diagonal", kind: "toggle" },
|
|
691
|
+
{ key: "crowning", label: "Reaching the far row mid-capture", kind: "choice", options: [["stop", "Crowned, and the move ends"], ["pass", "Carries on as a man"], ["king", "Crowned, and carries on as a king"]] },
|
|
692
|
+
{ key: "opens", label: "Which colour moves first", kind: "choice", options: [["dark", "Dark"], ["light", "Light"]] },
|
|
693
|
+
{ key: "first", label: "Who moves first", kind: "choice", options: [["random", "Luck of the draw"], ["seat1", "The first seat"], ["seat2", "The second seat"]] },
|
|
694
|
+
{ key: "quiet", label: "Draw after moves each with no capture", hint: "Moves by each side with no capture and no man moved", kind: "number", min: 10, max: 100, step: 5 },
|
|
695
|
+
{ key: "games", label: "Games in a match", kind: "choice", options: [[1, "Just one"], [3, "Three"], [5, "Five"], [7, "Seven"]] },
|
|
696
|
+
],
|
|
697
|
+
normalize,
|
|
698
|
+
validate(r) {
|
|
699
|
+
return r.most && !r.forced ? ["Taking the most only means something when captures are forced."] : [];
|
|
700
|
+
},
|
|
701
|
+
describe(r) {
|
|
702
|
+
const men = (r.size / 2 - 1) * (r.size / 2);
|
|
703
|
+
return [
|
|
704
|
+
`A ${r.size}ร${r.size} board, ${men} men each, played on the dark squares. ${r.opens === "dark" ? "Dark" : "Light"} moves first${r.first === "random" ? "; who that is is the luck of the draw" : `, and that's the ${r.first === "seat1" ? "first" : "second"} seat`}.`,
|
|
705
|
+
`Men move one square forwards on a slant. They capture by jumping a piece next to them into the empty square beyond: forwards${r.backward ? " or backwards" : " only"}.`,
|
|
706
|
+
r.flying ? "A man that reaches the far row is crowned a king. Kings fly: they move any distance along a diagonal, and capture a piece from any distance, landing anywhere clear beyond it." : "A man that reaches the far row is crowned a king. Kings move and capture one square at a time, forwards or backwards.",
|
|
707
|
+
r.forced
|
|
708
|
+
? `If you can capture, you have to, and you carry on jumping with the same piece while it can.${r.most ? " When there's a choice, you have to make the capture that takes the most pieces." : " When there's a choice of captures, take whichever you like."}`
|
|
709
|
+
: "Captures are never forced: jump if you want to, and stop a run of jumps whenever you like.",
|
|
710
|
+
"Tap each square in turn to make a run of jumps. Pieces you've jumped stay put until your move is done, and can't be jumped twice.",
|
|
711
|
+
...(r.backward || r.flying
|
|
712
|
+
? [r.crowning === "stop" ? "A man that reaches the far row during a capture is crowned and the move ends there." : r.crowning === "pass" ? "A man that passes through the far row during a capture carries on as a man: it's only crowned if the move ends there." : "A man that reaches the far row during a capture is crowned at once and carries on as a king."]
|
|
713
|
+
: []),
|
|
714
|
+
`You win by taking every piece or leaving the other player with no move. It's a draw after ${r.quiet} moves each with no capture and no man moved, or when the same position comes up three times.`,
|
|
715
|
+
...(r.games > 1 ? [`A match is ${r.games} games; most wins takes it. Players take turns moving first.`] : []),
|
|
716
|
+
];
|
|
717
|
+
},
|
|
718
|
+
newGame: newCheckers,
|
|
719
|
+
apply,
|
|
720
|
+
view,
|
|
721
|
+
bot,
|
|
722
|
+
// A search: off the table's main thread (see TableGame.heavy).
|
|
723
|
+
heavy: (s, seat) => s.phase === "playing" && s.turn === seat,
|
|
724
|
+
// A hop in the middle of a capture comes quickly: the thinking was done before the first one.
|
|
725
|
+
thinkingTime: (s) => (s.jump ? 450 : thinkFor(Math.min(new Set(checkersLines(s).map((l) => l.from)).size, 6), s.last === null ? 0.7 : 1)),
|
|
726
|
+
summary(s) {
|
|
727
|
+
const over = s.phase === "finished";
|
|
728
|
+
return {
|
|
729
|
+
game: "checkers",
|
|
730
|
+
rules: s.rules.name,
|
|
731
|
+
phase: s.phase,
|
|
732
|
+
over,
|
|
733
|
+
hand: s.round,
|
|
734
|
+
scoreLine: `${s.wins[0]} to ${s.wins[1]}${s.draws ? ` ยท ${s.draws} drawn` : ""}`,
|
|
735
|
+
order: over ? order(s) : null,
|
|
736
|
+
winners: over ? winners(s) : null,
|
|
737
|
+
teams: false,
|
|
738
|
+
seats: s.seats,
|
|
739
|
+
};
|
|
740
|
+
},
|
|
741
|
+
};
|
|
742
|
+
/** Checkers, with resigning, draw offers and taking a move back (see offers.ts). */
|
|
743
|
+
export const checkers = withOffers(rules, {
|
|
744
|
+
// A move is a whole turn: taking back in the middle of a run of jumps undoes the run so far.
|
|
745
|
+
takeBack: true,
|
|
746
|
+
settle(s, result) {
|
|
747
|
+
s.jump = null;
|
|
748
|
+
settleMatch(s, result);
|
|
749
|
+
},
|
|
750
|
+
// Material: a king is worth two men.
|
|
751
|
+
standing(s, seat) {
|
|
752
|
+
const worth = (o) => s.cells.reduce((n, p) => n + (p?.o === o ? (p.k ? 2 : 1) : 0), 0);
|
|
753
|
+
return worth(seat) - worth(other(seat));
|
|
754
|
+
},
|
|
755
|
+
});
|
|
756
|
+
//# sourceMappingURL=checkers.js.map
|