@journey-to-code/games 0.0.0-stage → 0.1.1
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/goboard.js
ADDED
|
@@ -0,0 +1,880 @@
|
|
|
1
|
+
// The Go board the rules and the computer player both stand on: stones in flat arrays, groups that
|
|
2
|
+
// know their own liberties as stones come and go, a hash of the position that changes with each
|
|
3
|
+
// stone (Zobrist), the count at the end, and the search (playouts and a tree over them).
|
|
4
|
+
//
|
|
5
|
+
// It is built to be fast before anything else, because the computer player plays whole games out
|
|
6
|
+
// to the end thousands of times a move, so it reads more like arithmetic than like the rest of
|
|
7
|
+
// the kit. go.ts is the game; nothing here knows about seats, rules presets or views.
|
|
8
|
+
//
|
|
9
|
+
// The board is kept with a border one point wide all round it (so a point's four neighbours are
|
|
10
|
+
// always there to look at, and the edge is just a colour nothing ever plays on). A "point" in this
|
|
11
|
+
// file is an index into that padded board; a "cell" is the kit's row-major index into the board
|
|
12
|
+
// itself, which is what states, moves and views use.
|
|
13
|
+
import { mulberry32 } from "@journey-to-code/rules";
|
|
14
|
+
export const EMPTY = 0;
|
|
15
|
+
export const BLACK = 1;
|
|
16
|
+
export const WHITE = 2;
|
|
17
|
+
const EDGE = 3;
|
|
18
|
+
/** The lines the star points are on: the third on a small board, the fourth otherwise, and the middle. */
|
|
19
|
+
function starLines(n) {
|
|
20
|
+
const near = n < 13 ? 2 : 3;
|
|
21
|
+
return [near, (n - 1) / 2, n - 1 - near];
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The nine star points of a board, as cells, in the order handicap stones are put down: two
|
|
25
|
+
* opposite corners, the third and fourth corner, then the sides; the middle is used for 5, 7 and 9.
|
|
26
|
+
*/
|
|
27
|
+
export function starCells(n) {
|
|
28
|
+
const [a, m, z] = starLines(n);
|
|
29
|
+
const at = (r, c) => r * n + c;
|
|
30
|
+
return { corners: [at(a, z), at(z, a), at(z, z), at(a, a)], sides: [at(m, a), at(m, z), at(a, m), at(z, m)], centre: at(m, m) };
|
|
31
|
+
}
|
|
32
|
+
/** Where `count` handicap stones go (2 to 9), as cells. */
|
|
33
|
+
export function handicapCells(n, count) {
|
|
34
|
+
const { corners, sides, centre } = starCells(n);
|
|
35
|
+
const k = Math.max(0, Math.min(9, Math.floor(count)));
|
|
36
|
+
if (k < 2)
|
|
37
|
+
return [];
|
|
38
|
+
if (k <= 4)
|
|
39
|
+
return corners.slice(0, k);
|
|
40
|
+
// An odd number takes the middle; the rest go on the corners and then the sides, in opposite pairs.
|
|
41
|
+
const rest = [...corners, ...sides].slice(0, k - (k % 2));
|
|
42
|
+
return k % 2 ? [...rest, centre] : rest;
|
|
43
|
+
}
|
|
44
|
+
/** The star points drawn on a board: all nine on the full board, the corners and the middle on the smaller ones. */
|
|
45
|
+
export function starPoints(n) {
|
|
46
|
+
const { corners, sides, centre } = starCells(n);
|
|
47
|
+
return n >= 19 ? [...corners, ...sides, centre] : [...corners, centre];
|
|
48
|
+
}
|
|
49
|
+
const GEOMETRY = new Map();
|
|
50
|
+
export function goGeometry(n) {
|
|
51
|
+
let g = GEOMETRY.get(n);
|
|
52
|
+
if (g)
|
|
53
|
+
return g;
|
|
54
|
+
const w = n + 2;
|
|
55
|
+
const size = w * w;
|
|
56
|
+
const at = new Int16Array(n * n);
|
|
57
|
+
const cell = new Int16Array(size).fill(-1);
|
|
58
|
+
const line = new Uint8Array(size);
|
|
59
|
+
const star = new Uint8Array(size);
|
|
60
|
+
for (let r = 0; r < n; r++) {
|
|
61
|
+
for (let c = 0; c < n; c++) {
|
|
62
|
+
const p = (r + 1) * w + c + 1;
|
|
63
|
+
at[r * n + c] = p;
|
|
64
|
+
cell[p] = r * n + c;
|
|
65
|
+
line[p] = Math.min(r, c, n - 1 - r, n - 1 - c) + 1;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
const stars = starCells(n);
|
|
69
|
+
for (const s of [...stars.corners, ...stars.sides, stars.centre])
|
|
70
|
+
star[at[s]] = 1;
|
|
71
|
+
// A fixed table, from a fixed seed: the same position has the same hash in every game, on every
|
|
72
|
+
// machine, for ever (hashes are kept in saved games).
|
|
73
|
+
const rand = mulberry32(0x60b0a2d ^ (n * 0x9e3779b1));
|
|
74
|
+
const zobHi = new Int32Array(size * 2);
|
|
75
|
+
const zobLo = new Int32Array(size * 2);
|
|
76
|
+
for (let i = 0; i < size * 2; i++) {
|
|
77
|
+
zobHi[i] = Math.floor(rand() * 4294967296) | 0;
|
|
78
|
+
zobLo[i] = Math.floor(rand() * 4294967296) | 0;
|
|
79
|
+
}
|
|
80
|
+
g = { n, w, size, at, cell, line, star, dirs: Int16Array.of(-w, -1, 1, w), diags: Int16Array.of(-w - 1, -w + 1, w - 1, w + 1), zobHi, zobLo };
|
|
81
|
+
GEOMETRY.set(n, g);
|
|
82
|
+
return g;
|
|
83
|
+
}
|
|
84
|
+
/** A quick, seedable source of whole numbers for playouts (xorshift): a call to `Math.random` per stone would be most of the cost. */
|
|
85
|
+
export class GoDice {
|
|
86
|
+
s;
|
|
87
|
+
constructor(seed) {
|
|
88
|
+
this.s = seed | 0 || 0x2545f491;
|
|
89
|
+
}
|
|
90
|
+
/** A whole number from 0 up to (not including) `below`. */
|
|
91
|
+
roll(below) {
|
|
92
|
+
let x = this.s;
|
|
93
|
+
x ^= x << 13;
|
|
94
|
+
x ^= x >>> 17;
|
|
95
|
+
x ^= x << 5;
|
|
96
|
+
this.s = x;
|
|
97
|
+
return (x >>> 0) % below;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* A position. Stones of one colour that touch are a group: every stone knows its group (`head`),
|
|
102
|
+
* the stones of a group are a ring (`next`), and the group keeps a count of its liberties, counted
|
|
103
|
+
* once for every stone that touches each (so a liberty two stones share counts twice), with the
|
|
104
|
+
* sum and the sum of squares of where they are. That is enough to tell at once when a group has
|
|
105
|
+
* none (it is captured) or exactly one (it is in atari, and where), without ever walking it.
|
|
106
|
+
*/
|
|
107
|
+
export class GoBoard {
|
|
108
|
+
g;
|
|
109
|
+
color;
|
|
110
|
+
head;
|
|
111
|
+
next;
|
|
112
|
+
libs;
|
|
113
|
+
libSum;
|
|
114
|
+
libSq;
|
|
115
|
+
count;
|
|
116
|
+
/** The empty points, in no order, and where each is in that list. */
|
|
117
|
+
empties;
|
|
118
|
+
where;
|
|
119
|
+
nEmpty = 0;
|
|
120
|
+
/** The point that can't be played next because it would take back a ko; 0 for none. */
|
|
121
|
+
ko = 0;
|
|
122
|
+
/** The position's hash, in two halves. */
|
|
123
|
+
hi = 0;
|
|
124
|
+
lo = 0;
|
|
125
|
+
/** Where the last stone went down; 0 for none, or after a pass. */
|
|
126
|
+
last = 0;
|
|
127
|
+
/** How many stones the last `play` took off (the other side's, or with suicide its own). */
|
|
128
|
+
taken = 0;
|
|
129
|
+
constructor(g) {
|
|
130
|
+
this.g = g;
|
|
131
|
+
const { size } = g;
|
|
132
|
+
this.color = new Uint8Array(size).fill(EDGE);
|
|
133
|
+
this.head = new Int16Array(size);
|
|
134
|
+
this.next = new Int16Array(size);
|
|
135
|
+
this.libs = new Int16Array(size);
|
|
136
|
+
this.libSum = new Int32Array(size);
|
|
137
|
+
this.libSq = new Int32Array(size);
|
|
138
|
+
this.count = new Int16Array(size);
|
|
139
|
+
this.empties = new Int16Array(g.n * g.n);
|
|
140
|
+
this.where = new Int16Array(size);
|
|
141
|
+
for (let i = 0; i < g.at.length; i++) {
|
|
142
|
+
const p = g.at[i];
|
|
143
|
+
this.color[p] = EMPTY;
|
|
144
|
+
this.where[p] = i;
|
|
145
|
+
this.empties[i] = p;
|
|
146
|
+
}
|
|
147
|
+
this.nEmpty = g.at.length;
|
|
148
|
+
}
|
|
149
|
+
copyFrom(b) {
|
|
150
|
+
this.color.set(b.color);
|
|
151
|
+
this.head.set(b.head);
|
|
152
|
+
this.next.set(b.next);
|
|
153
|
+
this.libs.set(b.libs);
|
|
154
|
+
this.libSum.set(b.libSum);
|
|
155
|
+
this.libSq.set(b.libSq);
|
|
156
|
+
this.count.set(b.count);
|
|
157
|
+
this.empties.set(b.empties);
|
|
158
|
+
this.where.set(b.where);
|
|
159
|
+
this.nEmpty = b.nEmpty;
|
|
160
|
+
this.ko = b.ko;
|
|
161
|
+
this.hi = b.hi;
|
|
162
|
+
this.lo = b.lo;
|
|
163
|
+
this.last = b.last;
|
|
164
|
+
this.taken = b.taken;
|
|
165
|
+
return this;
|
|
166
|
+
}
|
|
167
|
+
clone() {
|
|
168
|
+
return new GoBoard(this.g).copyFrom(this);
|
|
169
|
+
}
|
|
170
|
+
/** The position's hash as one whole number (53 bits: as many as a number keeps exactly). */
|
|
171
|
+
get hash() {
|
|
172
|
+
return (this.hi >>> 11) * 4294967296 + (this.lo >>> 0);
|
|
173
|
+
}
|
|
174
|
+
/** What the hash would be with a stone of `c` added at `p` and nothing taken off. */
|
|
175
|
+
hashWith(p, c) {
|
|
176
|
+
const z = p * 2 + c - 1;
|
|
177
|
+
return ((this.hi ^ this.g.zobHi[z]) >>> 11) * 4294967296 + ((this.lo ^ this.g.zobLo[z]) >>> 0);
|
|
178
|
+
}
|
|
179
|
+
/** How many stones are in the group at `p` (a point with a stone on it). */
|
|
180
|
+
groupSize(p) {
|
|
181
|
+
return this.count[this.head[p]];
|
|
182
|
+
}
|
|
183
|
+
/** Whether the group at `p` has exactly one liberty left. */
|
|
184
|
+
inAtari(p) {
|
|
185
|
+
const h = this.head[p];
|
|
186
|
+
const l = this.libs[h];
|
|
187
|
+
return l > 0 && l * this.libSq[h] === this.libSum[h] * this.libSum[h];
|
|
188
|
+
}
|
|
189
|
+
/** The one liberty of a group in atari. */
|
|
190
|
+
lastLiberty(p) {
|
|
191
|
+
const h = this.head[p];
|
|
192
|
+
return this.libSum[h] / this.libs[h];
|
|
193
|
+
}
|
|
194
|
+
/** Whether two points with stones on them are in the same group. */
|
|
195
|
+
sameGroup(p, q) {
|
|
196
|
+
return this.head[p] === this.head[q];
|
|
197
|
+
}
|
|
198
|
+
/** The stones of the group at `p`, as points. */
|
|
199
|
+
groupAt(p) {
|
|
200
|
+
const out = [p];
|
|
201
|
+
for (let s = this.next[p]; s !== p; s = this.next[s])
|
|
202
|
+
out.push(s);
|
|
203
|
+
return out;
|
|
204
|
+
}
|
|
205
|
+
setStone(p, c) {
|
|
206
|
+
const { zobHi, zobLo } = this.g;
|
|
207
|
+
this.color[p] = c;
|
|
208
|
+
const z = p * 2 + c - 1;
|
|
209
|
+
this.hi ^= zobHi[z];
|
|
210
|
+
this.lo ^= zobLo[z];
|
|
211
|
+
// Off the list of empty points: the last one takes its place.
|
|
212
|
+
const i = this.where[p];
|
|
213
|
+
const moved = this.empties[--this.nEmpty];
|
|
214
|
+
this.empties[i] = moved;
|
|
215
|
+
this.where[moved] = i;
|
|
216
|
+
}
|
|
217
|
+
/** How many of `p`'s four neighbours are stones of the group headed `h`. */
|
|
218
|
+
touches(p, h) {
|
|
219
|
+
const { dirs } = this.g;
|
|
220
|
+
let k = 0;
|
|
221
|
+
for (let d = 0; d < 4; d++) {
|
|
222
|
+
const q = p + dirs[d];
|
|
223
|
+
const cq = this.color[q];
|
|
224
|
+
if ((cq === BLACK || cq === WHITE) && this.head[q] === h)
|
|
225
|
+
k++;
|
|
226
|
+
}
|
|
227
|
+
return k;
|
|
228
|
+
}
|
|
229
|
+
/** Put a stone down and join it to its neighbours. Takes nothing off: `play` does that. */
|
|
230
|
+
place(p, c) {
|
|
231
|
+
const { dirs } = this.g;
|
|
232
|
+
this.setStone(p, c);
|
|
233
|
+
this.head[p] = p;
|
|
234
|
+
this.next[p] = p;
|
|
235
|
+
this.count[p] = 1;
|
|
236
|
+
this.libs[p] = 0;
|
|
237
|
+
this.libSum[p] = 0;
|
|
238
|
+
this.libSq[p] = 0;
|
|
239
|
+
for (let d = 0; d < 4; d++) {
|
|
240
|
+
const q = p + dirs[d];
|
|
241
|
+
const cq = this.color[q];
|
|
242
|
+
if (cq === EDGE)
|
|
243
|
+
continue;
|
|
244
|
+
if (cq === EMPTY) {
|
|
245
|
+
const me = this.head[p];
|
|
246
|
+
this.libs[me]++;
|
|
247
|
+
this.libSum[me] += q;
|
|
248
|
+
this.libSq[me] += q * q;
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
// A stone beside it has lost this liberty.
|
|
252
|
+
const h = this.head[q];
|
|
253
|
+
this.libs[h]--;
|
|
254
|
+
this.libSum[h] -= p;
|
|
255
|
+
this.libSq[h] -= p * p;
|
|
256
|
+
if (cq === c && h !== this.head[p])
|
|
257
|
+
this.merge(this.head[p], h);
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
/** Two groups of one colour become one: the smaller takes the larger's name. */
|
|
261
|
+
merge(a, b) {
|
|
262
|
+
if (this.count[a] < this.count[b])
|
|
263
|
+
[a, b] = [b, a];
|
|
264
|
+
let s = b;
|
|
265
|
+
do {
|
|
266
|
+
this.head[s] = a;
|
|
267
|
+
s = this.next[s];
|
|
268
|
+
} while (s !== b);
|
|
269
|
+
// Two rings cut and joined to each other are one ring.
|
|
270
|
+
const t = this.next[a];
|
|
271
|
+
this.next[a] = this.next[b];
|
|
272
|
+
this.next[b] = t;
|
|
273
|
+
this.count[a] += this.count[b];
|
|
274
|
+
this.libs[a] += this.libs[b];
|
|
275
|
+
this.libSum[a] += this.libSum[b];
|
|
276
|
+
this.libSq[a] += this.libSq[b];
|
|
277
|
+
}
|
|
278
|
+
/** Take the group at `p` off the board. Returns how many stones that was, and remembers one of them in `lifted`. */
|
|
279
|
+
lifted = 0;
|
|
280
|
+
remove(p) {
|
|
281
|
+
const { dirs, zobHi, zobLo } = this.g;
|
|
282
|
+
const c = this.color[p];
|
|
283
|
+
let n = 0;
|
|
284
|
+
let s = p;
|
|
285
|
+
do {
|
|
286
|
+
const after = this.next[s];
|
|
287
|
+
this.color[s] = EMPTY;
|
|
288
|
+
const z = s * 2 + c - 1;
|
|
289
|
+
this.hi ^= zobHi[z];
|
|
290
|
+
this.lo ^= zobLo[z];
|
|
291
|
+
this.where[s] = this.nEmpty;
|
|
292
|
+
this.empties[this.nEmpty++] = s;
|
|
293
|
+
n++;
|
|
294
|
+
// Every stone left standing beside it has a liberty back. (Stones of the group being lifted
|
|
295
|
+
// get one too; they are gone by the time anyone looks.)
|
|
296
|
+
for (let d = 0; d < 4; d++) {
|
|
297
|
+
const q = s + dirs[d];
|
|
298
|
+
const cq = this.color[q];
|
|
299
|
+
if (cq !== BLACK && cq !== WHITE)
|
|
300
|
+
continue;
|
|
301
|
+
const h = this.head[q];
|
|
302
|
+
this.libs[h]++;
|
|
303
|
+
this.libSum[h] += s;
|
|
304
|
+
this.libSq[h] += s * s;
|
|
305
|
+
}
|
|
306
|
+
s = after;
|
|
307
|
+
} while (s !== p);
|
|
308
|
+
this.lifted = p;
|
|
309
|
+
return n;
|
|
310
|
+
}
|
|
311
|
+
/** Set a stone on the board with no questions asked: for building a position from a saved one. */
|
|
312
|
+
put(p, c) {
|
|
313
|
+
this.place(p, c);
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* Whether `c` may play at `p` as far as the board alone can say: the point is empty, isn't the ko
|
|
317
|
+
* point, and the stone would have a liberty (its own, a friend's, or one made by capturing).
|
|
318
|
+
* Positions that may not be repeated are the game's business (go.ts keeps the history).
|
|
319
|
+
*/
|
|
320
|
+
legal(p, c) {
|
|
321
|
+
if (this.color[p] !== EMPTY || p === this.ko)
|
|
322
|
+
return false;
|
|
323
|
+
const { dirs } = this.g;
|
|
324
|
+
for (let d = 0; d < 4; d++)
|
|
325
|
+
if (this.color[p + dirs[d]] === EMPTY)
|
|
326
|
+
return true;
|
|
327
|
+
for (let d = 0; d < 4; d++) {
|
|
328
|
+
const q = p + dirs[d];
|
|
329
|
+
const cq = this.color[q];
|
|
330
|
+
if (cq === EDGE)
|
|
331
|
+
continue;
|
|
332
|
+
const h = this.head[q];
|
|
333
|
+
const k = this.touches(p, h);
|
|
334
|
+
// A friend with a liberty somewhere else, or an enemy whose last liberty this is.
|
|
335
|
+
if (cq === c ? this.libs[h] > k : this.libs[h] === k)
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
/** Whether a stone of `c` at the empty point `p` would take anything off. */
|
|
341
|
+
captures(p, c) {
|
|
342
|
+
const { dirs } = this.g;
|
|
343
|
+
const them = 3 - c;
|
|
344
|
+
for (let d = 0; d < 4; d++) {
|
|
345
|
+
const q = p + dirs[d];
|
|
346
|
+
if (this.color[q] !== them)
|
|
347
|
+
continue;
|
|
348
|
+
const h = this.head[q];
|
|
349
|
+
if (this.libs[h] === this.touches(p, h))
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Play a stone: it goes down, any of the other side's groups left without a liberty come off,
|
|
356
|
+
* and then (only when `legal` would have said no: a table that allows suicide) its own group if
|
|
357
|
+
* that has none. Sets `taken`, `ko` and `last`.
|
|
358
|
+
*/
|
|
359
|
+
play(p, c) {
|
|
360
|
+
const { dirs } = this.g;
|
|
361
|
+
const them = 3 - c;
|
|
362
|
+
this.place(p, c);
|
|
363
|
+
let taken = 0;
|
|
364
|
+
for (let d = 0; d < 4; d++) {
|
|
365
|
+
const q = p + dirs[d];
|
|
366
|
+
if (this.color[q] === them && this.libs[this.head[q]] === 0)
|
|
367
|
+
taken += this.remove(q);
|
|
368
|
+
}
|
|
369
|
+
const me = this.head[p];
|
|
370
|
+
// Taking one stone with one stone that is then itself in atari on that very point: a ko.
|
|
371
|
+
this.ko = taken === 1 && this.count[me] === 1 && this.libs[me] === 1 ? this.lifted : 0;
|
|
372
|
+
if (taken === 0 && this.libs[me] === 0)
|
|
373
|
+
taken = this.remove(p);
|
|
374
|
+
this.taken = taken;
|
|
375
|
+
this.last = this.color[p] === EMPTY ? 0 : p;
|
|
376
|
+
}
|
|
377
|
+
pass() {
|
|
378
|
+
this.ko = 0;
|
|
379
|
+
this.last = 0;
|
|
380
|
+
this.taken = 0;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Whether a stone of `c` at `p` would leave its own group with one liberty or none, taking
|
|
384
|
+
* nothing: a stone thrown away, usually. Gives the size the group would be (0 when it's safe).
|
|
385
|
+
*/
|
|
386
|
+
selfAtari(p, c) {
|
|
387
|
+
const { dirs } = this.g;
|
|
388
|
+
let liberty = 0;
|
|
389
|
+
let size = 1;
|
|
390
|
+
let g0 = -1;
|
|
391
|
+
let g1 = -1;
|
|
392
|
+
let g2 = -1;
|
|
393
|
+
for (let d = 0; d < 4; d++) {
|
|
394
|
+
const q = p + dirs[d];
|
|
395
|
+
const cq = this.color[q];
|
|
396
|
+
if (cq === EDGE)
|
|
397
|
+
continue;
|
|
398
|
+
let found = 0;
|
|
399
|
+
if (cq === EMPTY)
|
|
400
|
+
found = q;
|
|
401
|
+
else {
|
|
402
|
+
const h = this.head[q];
|
|
403
|
+
const k = this.touches(p, h);
|
|
404
|
+
const l = this.libs[h] - k;
|
|
405
|
+
if (cq !== c) {
|
|
406
|
+
if (l === 0)
|
|
407
|
+
return 0; // it captures
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
if (h !== g0 && h !== g1 && h !== g2) {
|
|
411
|
+
size += this.count[h];
|
|
412
|
+
if (g0 < 0)
|
|
413
|
+
g0 = h;
|
|
414
|
+
else if (g1 < 0)
|
|
415
|
+
g1 = h;
|
|
416
|
+
else
|
|
417
|
+
g2 = h;
|
|
418
|
+
}
|
|
419
|
+
if (l === 0)
|
|
420
|
+
continue;
|
|
421
|
+
const sum = this.libSum[h] - k * p;
|
|
422
|
+
if (l * (this.libSq[h] - k * p * p) !== sum * sum)
|
|
423
|
+
return 0; // two liberties of its own elsewhere
|
|
424
|
+
found = sum / l;
|
|
425
|
+
}
|
|
426
|
+
if (liberty === 0)
|
|
427
|
+
liberty = found;
|
|
428
|
+
else if (liberty !== found)
|
|
429
|
+
return 0;
|
|
430
|
+
}
|
|
431
|
+
return size;
|
|
432
|
+
}
|
|
433
|
+
/**
|
|
434
|
+
* Whether the empty point `p` is an eye of `c`'s: every neighbour is `c`'s, and the other side
|
|
435
|
+
* doesn't hold enough of the corners to make it a false one. Nobody fills their own eyes.
|
|
436
|
+
*/
|
|
437
|
+
isEye(p, c) {
|
|
438
|
+
const { dirs, diags } = this.g;
|
|
439
|
+
let edge = false;
|
|
440
|
+
for (let d = 0; d < 4; d++) {
|
|
441
|
+
const cq = this.color[p + dirs[d]];
|
|
442
|
+
if (cq === EDGE)
|
|
443
|
+
edge = true;
|
|
444
|
+
else if (cq !== c)
|
|
445
|
+
return false;
|
|
446
|
+
}
|
|
447
|
+
const them = 3 - c;
|
|
448
|
+
let bad = 0;
|
|
449
|
+
for (let d = 0; d < 4; d++)
|
|
450
|
+
if (this.color[p + diags[d]] === them)
|
|
451
|
+
bad++;
|
|
452
|
+
return edge ? bad === 0 : bad <= 1;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* The move a quick player makes: a capture or an escape by the stone just played if there is one,
|
|
456
|
+
* and otherwise any point that isn't its own eye and doesn't throw three or more stones away.
|
|
457
|
+
* 0 means pass.
|
|
458
|
+
*/
|
|
459
|
+
quickMove(c, dice) {
|
|
460
|
+
const { dirs } = this.g;
|
|
461
|
+
const last = this.last;
|
|
462
|
+
if (last) {
|
|
463
|
+
// The stone just played can be taken: take it.
|
|
464
|
+
if (this.inAtari(last)) {
|
|
465
|
+
const l = this.lastLiberty(last);
|
|
466
|
+
if (this.legal(l, c))
|
|
467
|
+
return l;
|
|
468
|
+
}
|
|
469
|
+
// It put one of mine in atari: run, if running gets anywhere.
|
|
470
|
+
for (let d = 0; d < 4; d++) {
|
|
471
|
+
const q = last + dirs[d];
|
|
472
|
+
if (this.color[q] !== c || !this.inAtari(q))
|
|
473
|
+
continue;
|
|
474
|
+
const l = this.lastLiberty(q);
|
|
475
|
+
if (this.legal(l, c) && this.selfAtari(l, c) === 0)
|
|
476
|
+
return l;
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
const n = this.nEmpty;
|
|
480
|
+
if (n === 0)
|
|
481
|
+
return 0;
|
|
482
|
+
const start = dice.roll(n);
|
|
483
|
+
for (let k = 0; k < n; k++) {
|
|
484
|
+
const p = this.empties[start + k < n ? start + k : start + k - n];
|
|
485
|
+
if (p === this.ko || this.isEye(p, c) || !this.legal(p, c))
|
|
486
|
+
continue;
|
|
487
|
+
if (this.selfAtari(p, c) >= 3)
|
|
488
|
+
continue;
|
|
489
|
+
return p;
|
|
490
|
+
}
|
|
491
|
+
return 0;
|
|
492
|
+
}
|
|
493
|
+
/**
|
|
494
|
+
* Play the game out from here with quick moves until both sides pass. `first` (if given) notes
|
|
495
|
+
* the colour that first played each point, for the search's "all moves as first" figures.
|
|
496
|
+
*/
|
|
497
|
+
playOut(toMove, dice, first) {
|
|
498
|
+
let c = toMove;
|
|
499
|
+
let passes = 0;
|
|
500
|
+
// Captures can make a playout long; three boards' worth of moves is past any real game.
|
|
501
|
+
for (let left = this.g.n * this.g.n * 3; left > 0 && passes < 2; left--) {
|
|
502
|
+
const p = this.quickMove(c, dice);
|
|
503
|
+
if (p === 0) {
|
|
504
|
+
passes++;
|
|
505
|
+
this.pass();
|
|
506
|
+
}
|
|
507
|
+
else {
|
|
508
|
+
passes = 0;
|
|
509
|
+
this.play(p, c);
|
|
510
|
+
if (first && first[p] === 0)
|
|
511
|
+
first[p] = c;
|
|
512
|
+
}
|
|
513
|
+
c = (3 - c);
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
/**
|
|
517
|
+
* Black's points minus White's, counting the board as it stands (stones, and empty points only
|
|
518
|
+
* one colour touches): right for a playout's last position, where everything that could be taken
|
|
519
|
+
* has been. `owner`, if given, has each point added to it: +1 Black's, -1 White's.
|
|
520
|
+
*/
|
|
521
|
+
areaMargin(owner) {
|
|
522
|
+
const { at, dirs } = this.g;
|
|
523
|
+
let margin = 0;
|
|
524
|
+
for (let i = 0; i < at.length; i++) {
|
|
525
|
+
const p = at[i];
|
|
526
|
+
let c = this.color[p];
|
|
527
|
+
if (c === EMPTY) {
|
|
528
|
+
let black = false;
|
|
529
|
+
let white = false;
|
|
530
|
+
for (let d = 0; d < 4; d++) {
|
|
531
|
+
const cq = this.color[p + dirs[d]];
|
|
532
|
+
if (cq === BLACK)
|
|
533
|
+
black = true;
|
|
534
|
+
else if (cq === WHITE)
|
|
535
|
+
white = true;
|
|
536
|
+
}
|
|
537
|
+
c = black && !white ? BLACK : white && !black ? WHITE : EMPTY;
|
|
538
|
+
}
|
|
539
|
+
if (c === BLACK) {
|
|
540
|
+
margin++;
|
|
541
|
+
if (owner)
|
|
542
|
+
owner[i]++;
|
|
543
|
+
}
|
|
544
|
+
else if (c === WHITE) {
|
|
545
|
+
margin--;
|
|
546
|
+
if (owner)
|
|
547
|
+
owner[i]--;
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
return margin;
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
/** A board from its cells (0 empty, 1 black, 2 white), row by row. */
|
|
554
|
+
export function boardFromCells(n, cells) {
|
|
555
|
+
const g = goGeometry(n);
|
|
556
|
+
const b = new GoBoard(g);
|
|
557
|
+
// In any order: a group never has fewer liberties half built than it has in the finished position.
|
|
558
|
+
for (let i = 0; i < cells.length; i++)
|
|
559
|
+
if (cells[i] === BLACK || cells[i] === WHITE)
|
|
560
|
+
b.put(g.at[i], cells[i]);
|
|
561
|
+
return b;
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Count a finished position. Stones marked dead come off first; then every stretch of empty points
|
|
565
|
+
* that touches stones of only one colour is that colour's. Stones that share liberties neither
|
|
566
|
+
* dares fill (a seki) are simply left standing: they count as stones, and the points between them
|
|
567
|
+
* are nobody's.
|
|
568
|
+
*/
|
|
569
|
+
export function countGo(n, cells, dead = []) {
|
|
570
|
+
const board = Uint8Array.from(cells);
|
|
571
|
+
const out = { owner: new Uint8Array(n * n), stones: [0, 0, 0], territory: [0, 0, 0], dead: [0, 0, 0] };
|
|
572
|
+
for (const cell of dead) {
|
|
573
|
+
if (board[cell] === BLACK || board[cell] === WHITE) {
|
|
574
|
+
out.dead[board[cell]]++;
|
|
575
|
+
board[cell] = EMPTY;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
const seen = new Uint8Array(n * n);
|
|
579
|
+
const stack = [];
|
|
580
|
+
for (let start = 0; start < board.length; start++) {
|
|
581
|
+
if (board[start] !== EMPTY) {
|
|
582
|
+
out.owner[start] = board[start];
|
|
583
|
+
out.stones[board[start]]++;
|
|
584
|
+
continue;
|
|
585
|
+
}
|
|
586
|
+
if (seen[start])
|
|
587
|
+
continue;
|
|
588
|
+
// One stretch of empty points, and the colours on its edge.
|
|
589
|
+
const region = [];
|
|
590
|
+
let touches = 0;
|
|
591
|
+
seen[start] = 1;
|
|
592
|
+
stack.push(start);
|
|
593
|
+
while (stack.length) {
|
|
594
|
+
const cell = stack.pop();
|
|
595
|
+
region.push(cell);
|
|
596
|
+
const r = Math.floor(cell / n);
|
|
597
|
+
const c = cell % n;
|
|
598
|
+
for (let d = 0; d < 4; d++) {
|
|
599
|
+
const rr = r + (d === 0 ? -1 : d === 1 ? 1 : 0);
|
|
600
|
+
const cc = c + (d === 2 ? -1 : d === 3 ? 1 : 0);
|
|
601
|
+
if (rr < 0 || rr >= n || cc < 0 || cc >= n)
|
|
602
|
+
continue;
|
|
603
|
+
const q = rr * n + cc;
|
|
604
|
+
if (board[q] !== EMPTY)
|
|
605
|
+
touches |= board[q];
|
|
606
|
+
else if (!seen[q]) {
|
|
607
|
+
seen[q] = 1;
|
|
608
|
+
stack.push(q);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
if (touches === BLACK || touches === WHITE) {
|
|
613
|
+
out.territory[touches] += region.length;
|
|
614
|
+
for (const cell of region)
|
|
615
|
+
out.owner[cell] = touches;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
return out;
|
|
619
|
+
}
|
|
620
|
+
/** The stones of the group at `cell` (a cell with a stone on it), as cells. */
|
|
621
|
+
export function groupCells(n, cells, cell) {
|
|
622
|
+
const colour = cells[cell];
|
|
623
|
+
const seen = new Set([cell]);
|
|
624
|
+
const stack = [cell];
|
|
625
|
+
while (stack.length) {
|
|
626
|
+
const at = stack.pop();
|
|
627
|
+
const r = Math.floor(at / n);
|
|
628
|
+
const c = at % n;
|
|
629
|
+
for (const [rr, cc] of [[r - 1, c], [r + 1, c], [r, c - 1], [r, c + 1]]) {
|
|
630
|
+
if (rr < 0 || rr >= n || cc < 0 || cc >= n)
|
|
631
|
+
continue;
|
|
632
|
+
const q = rr * n + cc;
|
|
633
|
+
if (cells[q] === colour && !seen.has(q)) {
|
|
634
|
+
seen.add(q);
|
|
635
|
+
stack.push(q);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
return [...seen].sort((a, b) => a - b);
|
|
640
|
+
}
|
|
641
|
+
// ---- Who owns what: an estimate -----------------------------------------------------------------
|
|
642
|
+
/**
|
|
643
|
+
* Play the position out `playouts` times and say how each cell tends to end up: from +1 (always
|
|
644
|
+
* Black's) to -1 (always White's). The dice come from `seed`, so the same position always gets the
|
|
645
|
+
* same answer: the game uses this to propose which stones are dead, and that has to replay.
|
|
646
|
+
*/
|
|
647
|
+
export function estimateOwners(board, toMove, playouts, seed) {
|
|
648
|
+
const cells = board.g.at.length;
|
|
649
|
+
const sum = new Int32Array(cells);
|
|
650
|
+
const work = new GoBoard(board.g);
|
|
651
|
+
const dice = new GoDice(seed);
|
|
652
|
+
for (let i = 0; i < playouts; i++) {
|
|
653
|
+
work.copyFrom(board);
|
|
654
|
+
work.playOut(toMove, dice);
|
|
655
|
+
work.areaMargin(sum);
|
|
656
|
+
}
|
|
657
|
+
const out = new Float32Array(cells);
|
|
658
|
+
for (let i = 0; i < cells; i++)
|
|
659
|
+
out[i] = playouts ? sum[i] / playouts : 0;
|
|
660
|
+
return out;
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* The stones an estimate says are dead: every group whose points end up the other side's more
|
|
664
|
+
* often than not, by a clear margin. A group nobody can say much about is left alive (the players
|
|
665
|
+
* can mark it themselves).
|
|
666
|
+
*/
|
|
667
|
+
export function deadFromOwners(n, cells, owners, margin = 0.3) {
|
|
668
|
+
const dead = [];
|
|
669
|
+
const done = new Uint8Array(n * n);
|
|
670
|
+
for (let cell = 0; cell < n * n; cell++) {
|
|
671
|
+
if (done[cell] || (cells[cell] !== BLACK && cells[cell] !== WHITE))
|
|
672
|
+
continue;
|
|
673
|
+
const group = groupCells(n, cells, cell);
|
|
674
|
+
const sign = cells[cell] === BLACK ? 1 : -1;
|
|
675
|
+
let mean = 0;
|
|
676
|
+
for (const s of group) {
|
|
677
|
+
done[s] = 1;
|
|
678
|
+
mean += owners[s] * sign;
|
|
679
|
+
}
|
|
680
|
+
if (mean / group.length < -margin)
|
|
681
|
+
dead.push(...group);
|
|
682
|
+
}
|
|
683
|
+
return dead.sort((a, b) => a - b);
|
|
684
|
+
}
|
|
685
|
+
/** How many games a move's own results need before they count for as much as the guess. */
|
|
686
|
+
const RAVE_WEIGHT = 2000;
|
|
687
|
+
/** How many made-up games a move starts with. */
|
|
688
|
+
const PRIOR_GAMES = 12;
|
|
689
|
+
function worth(k) {
|
|
690
|
+
const guess = k.rw / k.rn;
|
|
691
|
+
if (k.n === 0)
|
|
692
|
+
return guess;
|
|
693
|
+
const beta = k.rn / (k.rn + k.n + (k.rn * k.n) / RAVE_WEIGHT);
|
|
694
|
+
return (1 - beta) * (k.w / k.n) + beta * guess;
|
|
695
|
+
}
|
|
696
|
+
/**
|
|
697
|
+
* How good a move looks before any reading, from 0 to 1 (0.5 is "no opinion"): what a beginner is
|
|
698
|
+
* told in the first lesson, and enough to keep a short search from looking foolish.
|
|
699
|
+
*/
|
|
700
|
+
export function goPrior(b, p, c) {
|
|
701
|
+
const { dirs, diags, line, star, n } = b.g;
|
|
702
|
+
const them = 3 - c;
|
|
703
|
+
let v = 0.5;
|
|
704
|
+
let near = 0;
|
|
705
|
+
let saves = false;
|
|
706
|
+
let takes = 0;
|
|
707
|
+
for (let d = 0; d < 4; d++) {
|
|
708
|
+
const q = p + dirs[d];
|
|
709
|
+
const cq = b.color[q];
|
|
710
|
+
if (cq === BLACK || cq === WHITE) {
|
|
711
|
+
near++;
|
|
712
|
+
if (b.inAtari(q)) {
|
|
713
|
+
if (cq === them)
|
|
714
|
+
takes += b.groupSize(q);
|
|
715
|
+
else
|
|
716
|
+
saves = true;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
const dq = b.color[p + diags[d]];
|
|
720
|
+
if (dq === BLACK || dq === WHITE)
|
|
721
|
+
near++;
|
|
722
|
+
}
|
|
723
|
+
const thrown = b.selfAtari(p, c);
|
|
724
|
+
if (takes)
|
|
725
|
+
v += takes > 1 ? 0.35 : 0.25;
|
|
726
|
+
if (thrown)
|
|
727
|
+
v -= thrown > 1 ? 0.4 : 0.3;
|
|
728
|
+
else if (saves)
|
|
729
|
+
v += 0.25;
|
|
730
|
+
const l = line[p];
|
|
731
|
+
if (near === 0) {
|
|
732
|
+
// Open country: the third and fourth lines are where a game starts, the edge is where it ends.
|
|
733
|
+
if (l === 1)
|
|
734
|
+
v -= 0.3;
|
|
735
|
+
else if (l === 2)
|
|
736
|
+
v -= 0.15;
|
|
737
|
+
else if (l === 3 || (l === 4 && n > 9))
|
|
738
|
+
v += 0.08;
|
|
739
|
+
if (star[p] && n > 9)
|
|
740
|
+
v += 0.12;
|
|
741
|
+
}
|
|
742
|
+
else {
|
|
743
|
+
v += 0.05;
|
|
744
|
+
if (l === 1 && !takes && !saves)
|
|
745
|
+
v -= 0.1;
|
|
746
|
+
}
|
|
747
|
+
// An answer close to the stone just played is usually wanted.
|
|
748
|
+
if (b.last) {
|
|
749
|
+
const w = b.g.w;
|
|
750
|
+
const dr = Math.abs(Math.floor(p / w) - Math.floor(b.last / w));
|
|
751
|
+
const dc = Math.abs((p % w) - (b.last % w));
|
|
752
|
+
const far = Math.max(dr, dc);
|
|
753
|
+
if (far <= 1)
|
|
754
|
+
v += 0.1;
|
|
755
|
+
else if (far === 2)
|
|
756
|
+
v += 0.05;
|
|
757
|
+
}
|
|
758
|
+
return v < 0.05 ? 0.05 : v > 0.95 ? 0.95 : v;
|
|
759
|
+
}
|
|
760
|
+
/**
|
|
761
|
+
* Think about `toMove`'s move. `legal` is every cell the rules allow (the game works that out, with
|
|
762
|
+
* its history of positions); its own eyes are left out here, since filling one is never the move.
|
|
763
|
+
* With no time or games at all it still answers, with the move that looks best at a glance.
|
|
764
|
+
*/
|
|
765
|
+
export function searchGo(root, toMove, legal, o) {
|
|
766
|
+
const g = root.g;
|
|
767
|
+
const dice = new GoDice(o.seed);
|
|
768
|
+
const cells = g.at.length;
|
|
769
|
+
const kids = [];
|
|
770
|
+
for (const cell of legal) {
|
|
771
|
+
const p = g.at[cell];
|
|
772
|
+
if (root.isEye(p, toMove))
|
|
773
|
+
continue;
|
|
774
|
+
const prior = goPrior(root, p, toMove);
|
|
775
|
+
kids.push({ p, c: toMove, n: 0, w: 0, rn: PRIOR_GAMES, rw: PRIOR_GAMES * prior, kids: null });
|
|
776
|
+
}
|
|
777
|
+
// Moves that look alike at a glance are looked at in a different order each time.
|
|
778
|
+
for (let i = kids.length - 1; i > 0; i--) {
|
|
779
|
+
const j = dice.roll(i + 1);
|
|
780
|
+
[kids[i], kids[j]] = [kids[j], kids[i]];
|
|
781
|
+
}
|
|
782
|
+
const pass = o.canPass || kids.length === 0 ? { p: 0, c: toMove, n: 0, w: 0, rn: PRIOR_GAMES, rw: PRIOR_GAMES * 0.4, kids: null } : null;
|
|
783
|
+
if (pass)
|
|
784
|
+
kids.push(pass);
|
|
785
|
+
const top = { p: 0, c: (3 - toMove), n: 0, w: 0, rn: 1, rw: 0, kids };
|
|
786
|
+
const owners = new Float32Array(cells);
|
|
787
|
+
const none = (cell) => ({ cell, playouts: 0, winning: 0.5, owners, passing: null });
|
|
788
|
+
if (kids.length === 1)
|
|
789
|
+
return none(kids[0].p ? g.cell[kids[0].p] : null);
|
|
790
|
+
const work = new GoBoard(g);
|
|
791
|
+
const first = new Uint8Array(g.size);
|
|
792
|
+
const sum = new Int32Array(cells);
|
|
793
|
+
const path = [];
|
|
794
|
+
// A new position joins the tree once it has been through this many games: sooner on a small board.
|
|
795
|
+
const grow = g.n > 9 ? 2 : 1;
|
|
796
|
+
function expand(node, c) {
|
|
797
|
+
const list = [];
|
|
798
|
+
for (let i = 0; i < work.nEmpty; i++) {
|
|
799
|
+
const p = work.empties[i];
|
|
800
|
+
if (p === work.ko || work.isEye(p, c) || !work.legal(p, c))
|
|
801
|
+
continue;
|
|
802
|
+
list.push({ p, c, n: 0, w: 0, rn: PRIOR_GAMES, rw: PRIOR_GAMES * goPrior(work, p, c), kids: null });
|
|
803
|
+
}
|
|
804
|
+
if (!list.length)
|
|
805
|
+
list.push({ p: 0, c, n: 0, w: 0, rn: PRIOR_GAMES, rw: PRIOR_GAMES * 0.5, kids: null });
|
|
806
|
+
node.kids = list;
|
|
807
|
+
}
|
|
808
|
+
let played = 0;
|
|
809
|
+
while (played < o.playouts) {
|
|
810
|
+
if (o.deadline !== undefined && performance.now() >= o.deadline)
|
|
811
|
+
break;
|
|
812
|
+
work.copyFrom(root);
|
|
813
|
+
first.fill(0);
|
|
814
|
+
path.length = 0;
|
|
815
|
+
path.push(top);
|
|
816
|
+
let node = top;
|
|
817
|
+
let c = toMove;
|
|
818
|
+
let passes = o.passed ? 1 : 0;
|
|
819
|
+
while (passes < 2) {
|
|
820
|
+
if (!node.kids) {
|
|
821
|
+
if (node.n < grow)
|
|
822
|
+
break;
|
|
823
|
+
expand(node, c);
|
|
824
|
+
}
|
|
825
|
+
const list = node.kids;
|
|
826
|
+
let best = list[0];
|
|
827
|
+
let bestWorth = -1;
|
|
828
|
+
for (let i = 0; i < list.length; i++) {
|
|
829
|
+
const v = worth(list[i]);
|
|
830
|
+
if (v > bestWorth) {
|
|
831
|
+
bestWorth = v;
|
|
832
|
+
best = list[i];
|
|
833
|
+
}
|
|
834
|
+
}
|
|
835
|
+
if (best.p === 0) {
|
|
836
|
+
passes++;
|
|
837
|
+
work.pass();
|
|
838
|
+
}
|
|
839
|
+
else {
|
|
840
|
+
passes = 0;
|
|
841
|
+
work.play(best.p, c);
|
|
842
|
+
if (first[best.p] === 0)
|
|
843
|
+
first[best.p] = c;
|
|
844
|
+
}
|
|
845
|
+
path.push(best);
|
|
846
|
+
node = best;
|
|
847
|
+
c = (3 - c);
|
|
848
|
+
}
|
|
849
|
+
if (passes < 2)
|
|
850
|
+
work.playOut(c, dice, first);
|
|
851
|
+
const margin = work.areaMargin(sum) - o.komi;
|
|
852
|
+
const winner = margin > 0 ? BLACK : WHITE;
|
|
853
|
+
for (let i = 1; i < path.length; i++) {
|
|
854
|
+
const k = path[i];
|
|
855
|
+
k.n++;
|
|
856
|
+
if (k.c === winner)
|
|
857
|
+
k.w++;
|
|
858
|
+
// Its brothers hear about it too: any of them whose point this player went on to play.
|
|
859
|
+
for (const s of path[i - 1].kids) {
|
|
860
|
+
if (s.p !== 0 && first[s.p] === s.c) {
|
|
861
|
+
s.rn++;
|
|
862
|
+
if (s.c === winner)
|
|
863
|
+
s.rw++;
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
top.n++;
|
|
868
|
+
played++;
|
|
869
|
+
}
|
|
870
|
+
if (played)
|
|
871
|
+
for (let i = 0; i < cells; i++)
|
|
872
|
+
owners[i] = sum[i] / played;
|
|
873
|
+
// The move tried most; with nothing tried, the one that looked best at a glance.
|
|
874
|
+
let best = kids[0];
|
|
875
|
+
for (const k of kids)
|
|
876
|
+
if (k.n > best.n || (k.n === best.n && worth(k) > worth(best)))
|
|
877
|
+
best = k;
|
|
878
|
+
return { cell: best.p ? g.cell[best.p] : null, playouts: played, winning: best.n ? best.w / best.n : 0.5, owners, passing: pass && pass.n ? pass.w / pass.n : null };
|
|
879
|
+
}
|
|
880
|
+
//# sourceMappingURL=goboard.js.map
|