@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.
Files changed (207) hide show
  1. package/LICENSE +21 -0
  2. package/dist/backgammon.d.ts +146 -0
  3. package/dist/backgammon.js +1221 -0
  4. package/dist/backgammon.js.map +1 -0
  5. package/dist/blackjack.d.ts +66 -0
  6. package/dist/blackjack.js +452 -0
  7. package/dist/blackjack.js.map +1 -0
  8. package/dist/bluff.d.ts +60 -0
  9. package/dist/bluff.js +239 -0
  10. package/dist/bluff.js.map +1 -0
  11. package/dist/board.d.ts +387 -0
  12. package/dist/board.js +105 -0
  13. package/dist/board.js.map +1 -0
  14. package/dist/bridge.d.ts +86 -0
  15. package/dist/bridge.js +494 -0
  16. package/dist/bridge.js.map +1 -0
  17. package/dist/canasta.d.ts +88 -0
  18. package/dist/canasta.js +646 -0
  19. package/dist/canasta.js.map +1 -0
  20. package/dist/checkers.d.ts +108 -0
  21. package/dist/checkers.js +756 -0
  22. package/dist/checkers.js.map +1 -0
  23. package/dist/chess.d.ts +162 -0
  24. package/dist/chess.js +904 -0
  25. package/dist/chess.js.map +1 -0
  26. package/dist/chesscore.d.ts +136 -0
  27. package/dist/chesscore.js +1121 -0
  28. package/dist/chesscore.js.map +1 -0
  29. package/dist/combat.d.ts +102 -0
  30. package/dist/combat.js +772 -0
  31. package/dist/combat.js.map +1 -0
  32. package/dist/contractrummy.d.ts +77 -0
  33. package/dist/contractrummy.js +430 -0
  34. package/dist/contractrummy.js.map +1 -0
  35. package/dist/crazy8.d.ts +65 -0
  36. package/dist/crazy8.js +366 -0
  37. package/dist/crazy8.js.map +1 -0
  38. package/dist/cribbage.d.ts +75 -0
  39. package/dist/cribbage.js +428 -0
  40. package/dist/cribbage.js.map +1 -0
  41. package/dist/cuckoo.d.ts +43 -0
  42. package/dist/cuckoo.js +216 -0
  43. package/dist/cuckoo.js.map +1 -0
  44. package/dist/drinkingpyramid.d.ts +54 -0
  45. package/dist/drinkingpyramid.js +213 -0
  46. package/dist/drinkingpyramid.js.map +1 -0
  47. package/dist/drinks.d.ts +25 -0
  48. package/dist/drinks.js +40 -0
  49. package/dist/drinks.js.map +1 -0
  50. package/dist/dropfour.d.ts +103 -0
  51. package/dist/dropfour.js +577 -0
  52. package/dist/dropfour.js.map +1 -0
  53. package/dist/durak.d.ts +58 -0
  54. package/dist/durak.js +332 -0
  55. package/dist/durak.js.map +1 -0
  56. package/dist/euchre.d.ts +80 -0
  57. package/dist/euchre.js +453 -0
  58. package/dist/euchre.js.map +1 -0
  59. package/dist/fivedraw.d.ts +50 -0
  60. package/dist/fivedraw.js +289 -0
  61. package/dist/fivedraw.js.map +1 -0
  62. package/dist/fivehundred.d.ts +90 -0
  63. package/dist/fivehundred.js +504 -0
  64. package/dist/fivehundred.js.map +1 -0
  65. package/dist/freecell.d.ts +50 -0
  66. package/dist/freecell.js +327 -0
  67. package/dist/freecell.js.map +1 -0
  68. package/dist/fruitsalad.d.ts +69 -0
  69. package/dist/fruitsalad.js +427 -0
  70. package/dist/fruitsalad.js.map +1 -0
  71. package/dist/gin.d.ts +72 -0
  72. package/dist/gin.js +393 -0
  73. package/dist/gin.js.map +1 -0
  74. package/dist/go.d.ts +132 -0
  75. package/dist/go.js +659 -0
  76. package/dist/go.js.map +1 -0
  77. package/dist/goboard.d.ts +218 -0
  78. package/dist/goboard.js +880 -0
  79. package/dist/goboard.js.map +1 -0
  80. package/dist/gofish.d.ts +37 -0
  81. package/dist/gofish.js +235 -0
  82. package/dist/gofish.js.map +1 -0
  83. package/dist/golf.d.ts +64 -0
  84. package/dist/golf.js +343 -0
  85. package/dist/golf.js.map +1 -0
  86. package/dist/handfoot.d.ts +86 -0
  87. package/dist/handfoot.js +515 -0
  88. package/dist/handfoot.js.map +1 -0
  89. package/dist/hearts.d.ts +65 -0
  90. package/dist/hearts.js +390 -0
  91. package/dist/hearts.js.map +1 -0
  92. package/dist/holdem.d.ts +45 -0
  93. package/dist/holdem.js +286 -0
  94. package/dist/holdem.js.map +1 -0
  95. package/dist/horserace.d.ts +49 -0
  96. package/dist/horserace.js +207 -0
  97. package/dist/horserace.js.map +1 -0
  98. package/dist/index.d.ts +65 -0
  99. package/dist/index.js +66 -0
  100. package/dist/index.js.map +1 -0
  101. package/dist/kingscorner.d.ts +59 -0
  102. package/dist/kingscorner.js +374 -0
  103. package/dist/kingscorner.js.map +1 -0
  104. package/dist/kingscup.d.ts +44 -0
  105. package/dist/kingscup.js +185 -0
  106. package/dist/kingscup.js.map +1 -0
  107. package/dist/kit.d.ts +283 -0
  108. package/dist/kit.js +73 -0
  109. package/dist/kit.js.map +1 -0
  110. package/dist/klondike.d.ts +57 -0
  111. package/dist/klondike.js +314 -0
  112. package/dist/klondike.js.map +1 -0
  113. package/dist/liarspoker.d.ts +61 -0
  114. package/dist/liarspoker.js +298 -0
  115. package/dist/liarspoker.js.map +1 -0
  116. package/dist/memory.d.ts +36 -0
  117. package/dist/memory.js +181 -0
  118. package/dist/memory.js.map +1 -0
  119. package/dist/minigolf.d.ts +104 -0
  120. package/dist/minigolf.js +724 -0
  121. package/dist/minigolf.js.map +1 -0
  122. package/dist/minigolfcourse.d.ts +127 -0
  123. package/dist/minigolfcourse.js +470 -0
  124. package/dist/minigolfcourse.js.map +1 -0
  125. package/dist/offers.d.ts +136 -0
  126. package/dist/offers.js +279 -0
  127. package/dist/offers.js.map +1 -0
  128. package/dist/ohhell.d.ts +66 -0
  129. package/dist/ohhell.js +331 -0
  130. package/dist/ohhell.js.map +1 -0
  131. package/dist/oldmaid.d.ts +40 -0
  132. package/dist/oldmaid.js +186 -0
  133. package/dist/oldmaid.js.map +1 -0
  134. package/dist/pinochle.d.ts +94 -0
  135. package/dist/pinochle.js +466 -0
  136. package/dist/pinochle.js.map +1 -0
  137. package/dist/pitch.d.ts +77 -0
  138. package/dist/pitch.js +362 -0
  139. package/dist/pitch.js.map +1 -0
  140. package/dist/poker.d.ts +79 -0
  141. package/dist/poker.js +268 -0
  142. package/dist/poker.js.map +1 -0
  143. package/dist/president.d.ts +56 -0
  144. package/dist/president.js +339 -0
  145. package/dist/president.js.map +1 -0
  146. package/dist/pyramid.d.ts +39 -0
  147. package/dist/pyramid.js +200 -0
  148. package/dist/pyramid.js.map +1 -0
  149. package/dist/registry.d.ts +17 -0
  150. package/dist/registry.js +70 -0
  151. package/dist/registry.js.map +1 -0
  152. package/dist/reversi.d.ts +69 -0
  153. package/dist/reversi.js +584 -0
  154. package/dist/reversi.js.map +1 -0
  155. package/dist/ridethebus.d.ts +48 -0
  156. package/dist/ridethebus.js +257 -0
  157. package/dist/ridethebus.js.map +1 -0
  158. package/dist/rummy500.d.ts +89 -0
  159. package/dist/rummy500.js +474 -0
  160. package/dist/rummy500.js.map +1 -0
  161. package/dist/sevens.d.ts +48 -0
  162. package/dist/sevens.js +221 -0
  163. package/dist/sevens.js.map +1 -0
  164. package/dist/slapjack.d.ts +42 -0
  165. package/dist/slapjack.js +224 -0
  166. package/dist/slapjack.js.map +1 -0
  167. package/dist/speed.d.ts +44 -0
  168. package/dist/speed.js +201 -0
  169. package/dist/speed.js.map +1 -0
  170. package/dist/spider.d.ts +51 -0
  171. package/dist/spider.js +320 -0
  172. package/dist/spider.js.map +1 -0
  173. package/dist/spite.d.ts +75 -0
  174. package/dist/spite.js +372 -0
  175. package/dist/spite.js.map +1 -0
  176. package/dist/sport.d.ts +266 -0
  177. package/dist/sport.js +48 -0
  178. package/dist/sport.js.map +1 -0
  179. package/dist/switch.d.ts +69 -0
  180. package/dist/switch.js +393 -0
  181. package/dist/switch.js.map +1 -0
  182. package/dist/table.d.ts +284 -0
  183. package/dist/table.js +67 -0
  184. package/dist/table.js.map +1 -0
  185. package/dist/talk.d.ts +17 -0
  186. package/dist/talk.js +44 -0
  187. package/dist/talk.js.map +1 -0
  188. package/dist/thirtyone.d.ts +66 -0
  189. package/dist/thirtyone.js +372 -0
  190. package/dist/thirtyone.js.map +1 -0
  191. package/dist/tictactoe.d.ts +68 -0
  192. package/dist/tictactoe.js +541 -0
  193. package/dist/tictactoe.js.map +1 -0
  194. package/dist/tricks.d.ts +21 -0
  195. package/dist/tricks.js +48 -0
  196. package/dist/tricks.js.map +1 -0
  197. package/dist/war.d.ts +44 -0
  198. package/dist/war.js +185 -0
  199. package/dist/war.js.map +1 -0
  200. package/dist/whist.d.ts +52 -0
  201. package/dist/whist.js +218 -0
  202. package/dist/whist.js.map +1 -0
  203. package/dist/yacht.d.ts +96 -0
  204. package/dist/yacht.js +653 -0
  205. package/dist/yacht.js.map +1 -0
  206. package/package.json +41 -4
  207. package/README.md +0 -3
package/dist/go.js ADDED
@@ -0,0 +1,659 @@
1
+ import { settleMatch, withOffers } from "./offers.js";
2
+ import { BLACK, EMPTY, GoBoard, WHITE, boardFromCells, countGo, deadFromOwners, estimateOwners, goPrior, groupCells, handicapCells, searchGo, starPoints } from "./goboard.js";
3
+ import { careless, GameError, markReady, nextHandChoices, nextRandom, thinkFor, who } from "./table.js";
4
+ export const smallGo = { id: "go-small", name: "Small board (9×9)", size: 9, scoring: "area", komi: 7.5, handicap: 0, black: "random", ko: "superko", suicide: false, games: 1 };
5
+ export const mediumGo = { ...smallGo, id: "go-medium", name: "Medium board (13×13)", size: 13 };
6
+ export const fullGo = { ...smallGo, id: "go-full", name: "Full board (19×19)", size: 19 };
7
+ export const japaneseGo = { ...smallGo, id: "go-japanese", name: "Japanese rules (9×9)", scoring: "territory", komi: 6.5, ko: "simple" };
8
+ const PRESETS = [smallGo, mediumGo, fullGo, japaneseGo];
9
+ const SIZES = [9, 13, 19];
10
+ const KOMI = [0, 0.5, 5.5, 6, 6.5, 7, 7.5];
11
+ /** What White is actually given at a table: the komi, or half a point when Black has a handicap. */
12
+ export const komiOf = (r) => (r.handicap > 0 ? 0.5 : r.komi);
13
+ const other = (seat) => 1 - seat;
14
+ const colourOf = (s, seat) => (seat === s.black ? BLACK : WHITE);
15
+ const seatOf = (s, colour) => (colour === BLACK ? s.black : other(s.black));
16
+ const CHARS = ".XO";
17
+ const stoneAt = (board, cell) => (board.charCodeAt(cell) === 88 ? BLACK : board.charCodeAt(cell) === 79 ? WHITE : EMPTY);
18
+ const cellsOf = (board) => Uint8Array.from(board, (ch) => (ch === "X" ? BLACK : ch === "O" ? WHITE : EMPTY));
19
+ /** Columns are lettered left to right without an I (it reads as a 1 or a J), rows numbered from the bottom. */
20
+ const LETTERS = "ABCDEFGHJKLMNOPQRST";
21
+ const COORDS = new Map();
22
+ function coordsOf(n) {
23
+ let c = COORDS.get(n);
24
+ if (!c)
25
+ COORDS.set(n, (c = { cols: Array.from({ length: n }, (_, i) => LETTERS[i]), rows: Array.from({ length: n }, (_, r) => String(n - r)) }));
26
+ return c;
27
+ }
28
+ /** A crossing's name: "D4", "Q16". */
29
+ export const goPoint = (n, cell) => `${LETTERS[cell % n]}${n - Math.floor(cell / n)}`;
30
+ // ---- The board behind a state ---------------------------------------------------------------------
31
+ /** The last few boards built from a state's string: trying every move a view offers asks for the same one hundreds of times. */
32
+ const BUILT = new Map();
33
+ /** One board of each size that `boardOf` hands out again and again: a move tried is a board copied, hundreds of times a turn. */
34
+ const LENT = new Map();
35
+ /**
36
+ * A board for this state, to look at or play on. It is lent: the next call for the same size gets
37
+ * the same one, set up afresh. A caller that keeps its board across other calls here asks for its `own`.
38
+ */
39
+ function boardOf(s, own = false) {
40
+ const n = s.rules.size;
41
+ const key = `${n}:${s.board}`;
42
+ let made = BUILT.get(key);
43
+ if (!made) {
44
+ made = boardFromCells(n, cellsOf(s.board));
45
+ if (BUILT.size >= 4)
46
+ BUILT.delete(BUILT.keys().next().value);
47
+ BUILT.set(key, made);
48
+ }
49
+ let b;
50
+ if (own)
51
+ b = made.clone();
52
+ else {
53
+ if (!LENT.has(n))
54
+ LENT.set(n, new GoBoard(made.g));
55
+ b = LENT.get(n).copyFrom(made);
56
+ }
57
+ b.ko = s.ko === null ? 0 : b.g.at[s.ko];
58
+ b.last = s.last === null ? 0 : b.g.at[s.last];
59
+ return b;
60
+ }
61
+ const CODES = [CHARS.charCodeAt(0), CHARS.charCodeAt(1), CHARS.charCodeAt(2)];
62
+ function boardString(b) {
63
+ const { at } = b.g;
64
+ const codes = new Array(at.length);
65
+ for (let i = 0; i < at.length; i++)
66
+ codes[i] = CODES[b.color[at[i]]];
67
+ return String.fromCharCode.apply(null, codes);
68
+ }
69
+ /** A spare board of each size to try a move on: one is enough, since nothing here is ever in the middle of two things. */
70
+ const SCRATCH = new Map();
71
+ function scratchOf(b) {
72
+ let spare = SCRATCH.get(b.g.n);
73
+ if (!spare)
74
+ SCRATCH.set(b.g.n, (spare = new GoBoard(b.g)));
75
+ return spare;
76
+ }
77
+ /** The positions a state's board has been in, as a set (kept beside the list it was made from). */
78
+ const SEEN = new WeakMap();
79
+ function seenOf(s) {
80
+ let set = SEEN.get(s.seen);
81
+ if (!set)
82
+ SEEN.set(s.seen, (set = new Set(s.seen)));
83
+ return set;
84
+ }
85
+ /**
86
+ * Why `colour` can't play at `cell` on this board, in words for the player; null when it can.
87
+ * `b` is the state's board, and is left as it was.
88
+ */
89
+ function refusal(s, b, cell, colour) {
90
+ const p = b.g.at[cell];
91
+ if (b.color[p] !== EMPTY)
92
+ return "There's a stone there already.";
93
+ if (p === b.ko)
94
+ return "That would take the ko straight back: play somewhere else first.";
95
+ const alive = b.legal(p, colour);
96
+ if (!alive && !s.rules.suicide)
97
+ return "A stone there would have no liberties.";
98
+ const superko = s.rules.ko === "superko";
99
+ // A stone that takes nothing off just adds itself to the position: no need to play it to know the hash.
100
+ if (alive && !b.captures(p, colour))
101
+ return superko && seenOf(s).has(b.hashWith(p, colour)) ? "That would repeat a position the board has been in before." : null;
102
+ const scratch = scratchOf(b).copyFrom(b);
103
+ scratch.play(p, colour);
104
+ // (One stone that only takes itself off changes nothing, which is no move at all.)
105
+ if (!alive && scratch.hash === b.hash)
106
+ return "A stone there would have no liberties.";
107
+ return superko && seenOf(s).has(scratch.hash) ? "That would repeat a position the board has been in before." : null;
108
+ }
109
+ /** Every crossing `seat` may play now. */
110
+ export function legalGoMoves(s, seat) {
111
+ const b = boardOf(s);
112
+ const colour = colourOf(s, seat);
113
+ const out = [];
114
+ for (let cell = 0; cell < s.board.length; cell++)
115
+ if (s.board.charCodeAt(cell) === 46 && refusal(s, b, cell, colour) === null)
116
+ out.push(cell);
117
+ return out;
118
+ }
119
+ // ---- Dead stones and the count --------------------------------------------------------------------
120
+ /** How many games an estimate plays out: enough to tell a dead group from a live one, on each size. */
121
+ const estimateGames = (n) => (n <= 9 ? 160 : n <= 13 ? 120 : 80);
122
+ const PROPOSED = new Map();
123
+ /**
124
+ * The game's own view of which stones are dead: every group that ends up the other side's in most
125
+ * of a batch of games played out from here (goboard.ts). It depends on the position and nothing
126
+ * else, so it replays, and a computer player asked what it thinks gets the same answer.
127
+ */
128
+ export function proposedDead(s, toMove) {
129
+ const n = s.rules.size;
130
+ const key = `${n}:${toMove}:${s.board}`;
131
+ let dead = PROPOSED.get(key);
132
+ if (!dead) {
133
+ const b = boardOf({ ...s, ko: null, last: null }, true);
134
+ dead = deadFromOwners(n, cellsOf(s.board), estimateOwners(b, toMove, estimateGames(n), b.lo ^ 0x51ed27));
135
+ if (PROPOSED.size >= 8)
136
+ PROPOSED.delete(PROPOSED.keys().next().value);
137
+ PROPOSED.set(key, dead);
138
+ }
139
+ return dead;
140
+ }
141
+ const fmt = (points) => (Number.isInteger(points) ? String(points) : points.toFixed(1));
142
+ /** The count of a state's board with `dead` lifted off, under the table's scoring. */
143
+ export function scoreGo(s, dead) {
144
+ const count = countGo(s.rules.size, cellsOf(s.board), dead);
145
+ const komi = komiOf(s.rules);
146
+ const white = other(s.black);
147
+ const by = (black, whiteValue) => (s.black === 0 ? [black, whiteValue] : [whiteValue, black]);
148
+ const territory = by(count.territory[BLACK], count.territory[WHITE]);
149
+ const komiRow = { label: "Komi", values: by(0, komi).map((k) => (k ? fmt(k) : "")) };
150
+ let totals;
151
+ let rows;
152
+ if (s.rules.scoring === "area") {
153
+ const stones = by(count.stones[BLACK], count.stones[WHITE]);
154
+ totals = [0, 1].map((seat) => stones[seat] + territory[seat] + (seat === white ? komi : 0));
155
+ rows = [{ label: "Stones", values: stones.map(String) }, { label: "Territory", values: territory.map(String) }];
156
+ }
157
+ else {
158
+ // A dead stone is a prisoner as well as a point of territory once it is lifted.
159
+ const prisoners = by(s.captured[s.black] + count.dead[WHITE], s.captured[white] + count.dead[BLACK]);
160
+ totals = [0, 1].map((seat) => territory[seat] + prisoners[seat] + (seat === white ? komi : 0));
161
+ rows = [{ label: "Territory", values: territory.map(String) }, { label: "Prisoners", values: prisoners.map(String) }];
162
+ }
163
+ if (komi)
164
+ rows.push(komiRow);
165
+ rows.push({ label: "Total", values: totals.map(fmt), tone: "score" });
166
+ return { count, totals, rows };
167
+ }
168
+ // ---- Starting -------------------------------------------------------------------------------------
169
+ function startRound(s) {
170
+ const n = s.rules.size;
171
+ const cells = new Uint8Array(n * n);
172
+ for (const cell of handicapCells(n, s.rules.handicap))
173
+ cells[cell] = BLACK;
174
+ const b = boardFromCells(n, cells);
175
+ s.board = boardString(b);
176
+ // With stones given, White has the first move.
177
+ s.turn = s.rules.handicap >= 2 ? other(s.black) : s.black;
178
+ s.moves = 0;
179
+ s.passes = 0;
180
+ s.passed = false;
181
+ s.last = null;
182
+ s.ko = null;
183
+ s.captured = [0, 0];
184
+ s.seen = s.rules.ko === "superko" ? [b.hash] : [];
185
+ s.phase = "playing";
186
+ s.dead = [];
187
+ s.agreed = [false, false];
188
+ s.resumes = 0;
189
+ s.score = null;
190
+ s.roundWinner = null;
191
+ s.ready = [false, false];
192
+ s.narration = null;
193
+ }
194
+ export function newGo(rules, _seats, seed) {
195
+ const s = { game: "go", rules, seats: 2, rng: seed >>> 0, round: 1, wins: [0, 0], draws: 0 };
196
+ s.black = rules.black === "seat1" ? 0 : rules.black === "seat2" ? 1 : nextRandom(s)() < 0.5 ? 0 : 1;
197
+ startRound(s);
198
+ return s;
199
+ }
200
+ // ---- Moves ----------------------------------------------------------------------------------------
201
+ function matchOver(s) {
202
+ const left = s.rules.games - s.round;
203
+ return left <= 0 || Math.abs(s.wins[0] - s.wins[1]) > left;
204
+ }
205
+ const plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
206
+ /** Both have agreed: count the board and end the game. */
207
+ function finish(s) {
208
+ const { totals } = scoreGo(s, s.dead);
209
+ const [a, b] = totals;
210
+ const many = s.rules.games > 1;
211
+ s.score = totals;
212
+ if (a === b) {
213
+ s.draws += 1;
214
+ s.roundWinner = null;
215
+ s.narration = `${many ? `Game ${s.round} is a draw` : "It's a draw"}, ${fmt(a)} all`;
216
+ }
217
+ else {
218
+ const winner = a > b ? 0 : 1;
219
+ s.wins[winner] += 1;
220
+ s.roundWinner = winner;
221
+ s.narration = `${who(winner)} wins${many ? ` game ${s.round}` : ""} by ${plural(Math.abs(a - b), "point")}: ${fmt(Math.max(a, b))} to ${fmt(Math.min(a, b))}`;
222
+ }
223
+ s.phase = matchOver(s) ? "finished" : "scored";
224
+ }
225
+ function apply(state, seat, move) {
226
+ if (seat !== 0 && seat !== 1)
227
+ throw new GameError("You're not playing at this table.");
228
+ if (move?.type === "next") {
229
+ if (state.phase !== "scored")
230
+ throw new GameError("There's a game going on.");
231
+ const s = { ...state, ready: [...state.ready], wins: [...state.wins] };
232
+ if (markReady(s.ready, seat)) {
233
+ s.round += 1;
234
+ // Players take turns with Black, unless Black is the one being given stones.
235
+ if (s.rules.handicap === 0)
236
+ s.black = other(s.black);
237
+ startRound(s);
238
+ }
239
+ return s;
240
+ }
241
+ const n = state.rules.size;
242
+ if (move?.type === "toggle" || move?.type === "agree" || move?.type === "resume") {
243
+ if (state.phase !== "counting")
244
+ throw new GameError(state.phase === "playing" ? "The game isn't being counted yet: that starts when both players pass." : "This game is over.");
245
+ if (move.type === "resume") {
246
+ // Back to the board, and the player who wanted to stop there gets to answer first.
247
+ return { ...state, phase: "playing", turn: other(seat), passes: 0, dead: [], agreed: [false, false], resumes: state.resumes + 1, narration: `${who(seat)} plays on: ${who(other(seat))} to move` };
248
+ }
249
+ if (move.type === "toggle") {
250
+ const cell = Number(move.cell);
251
+ if (!Number.isInteger(cell) || cell < 0 || cell >= state.board.length)
252
+ throw new GameError("That's not on the board.");
253
+ if (stoneAt(state.board, cell) === EMPTY)
254
+ throw new GameError("There's no stone there to mark.");
255
+ const group = groupCells(n, cellsOf(state.board), cell);
256
+ const wasDead = state.dead.includes(cell);
257
+ const rest = state.dead.filter((d) => !group.includes(d));
258
+ // A changed marking is a new proposal: nobody has agreed to it yet.
259
+ return { ...state, dead: wasDead ? rest : [...rest, ...group].sort((a, b) => a - b), agreed: [false, false], narration: `${who(seat)} says the ${plural(group.length, "stone")} at ${goPoint(n, cell)} ${group.length === 1 ? "is" : "are"} ${wasDead ? "alive" : "dead"}` };
260
+ }
261
+ if (state.agreed[seat])
262
+ throw new GameError(`You've agreed already: waiting for ${who(other(seat))}.`);
263
+ const s = { ...state, agreed: state.agreed.map((yes, i) => yes || i === seat), wins: [...state.wins], narration: `${who(seat)} is happy with the count` };
264
+ if (s.agreed.every(Boolean))
265
+ finish(s);
266
+ return s;
267
+ }
268
+ if (move?.type !== "place" && move?.type !== "pass")
269
+ throw new GameError("That's not a move in Go.");
270
+ if (state.phase !== "playing")
271
+ throw new GameError(state.phase === "counting" ? "Play has stopped: agree the count, or say \"Play on\"." : "This game is over.");
272
+ if (state.turn !== seat)
273
+ throw new GameError("It's not your turn.");
274
+ if (move.type === "pass") {
275
+ const s = { ...state, moves: state.moves + 1, passes: state.passes + 1, passed: true, last: null, ko: null, turn: other(seat) };
276
+ if (s.passes < 2) {
277
+ s.narration = `${who(seat)} passes`;
278
+ return s;
279
+ }
280
+ // Two passes: stop, and work out what is dead. Whoever would have moved next is who a
281
+ // playout starts with.
282
+ s.phase = "counting";
283
+ s.dead = proposedDead(s, colourOf(s, other(seat)));
284
+ s.agreed = [false, false];
285
+ s.narration = `${who(seat)} passes too: time to count`;
286
+ return s;
287
+ }
288
+ const cell = Number(move.cell);
289
+ if (!Number.isInteger(cell) || cell < 0 || cell >= state.board.length)
290
+ throw new GameError("That's not on the board.");
291
+ const colour = colourOf(state, seat);
292
+ const b = boardOf(state);
293
+ const no = refusal(state, b, cell, colour);
294
+ if (no)
295
+ throw new GameError(no);
296
+ b.play(b.g.at[cell], colour);
297
+ const captured = [...state.captured];
298
+ // A stone that took its own group off (a table that allows it) hands those stones to the other side.
299
+ const own = b.last === 0;
300
+ captured[own ? other(seat) : seat] += b.taken;
301
+ return {
302
+ ...state,
303
+ board: boardString(b),
304
+ turn: other(seat),
305
+ moves: state.moves + 1,
306
+ passes: 0,
307
+ last: own ? null : cell,
308
+ ko: b.ko ? b.g.cell[b.ko] : null,
309
+ captured,
310
+ seen: state.rules.ko === "superko" ? [...state.seen, b.hash] : state.seen,
311
+ narration: `${who(seat)} plays ${goPoint(n, cell)}${b.taken ? (own ? `, and ${plural(b.taken, "stone")} of their own ${b.taken === 1 ? "comes" : "come"} off` : `, capturing ${plural(b.taken, "stone")}`) : ""}`,
312
+ };
313
+ }
314
+ /** Best first. A drawn match lists White of the last game first, as the other board games list the second player. */
315
+ function order(s) {
316
+ const [a, b] = s.wins;
317
+ if (a !== b)
318
+ return a > b ? [0, 1] : [1, 0];
319
+ return [other(s.black), s.black];
320
+ }
321
+ const winners = (s) => (s.wins[0] === s.wins[1] ? [0, 1] : [s.wins[0] > s.wins[1] ? 0 : 1]);
322
+ // ---- What a seat sees -----------------------------------------------------------------------------
323
+ const BLACK_STONE = { kind: "stone", glyph: "●", label: "black stone", tone: "dark" };
324
+ const WHITE_STONE = { kind: "stone", glyph: "●", label: "white stone", tone: "light" };
325
+ const stoneOf = (s, seat) => ({ ...(seat === s.black ? BLACK_STONE : WHITE_STONE), owner: seat });
326
+ function view(s, seat) {
327
+ const n = s.rules.size;
328
+ const over = s.phase === "finished";
329
+ const playing = s.phase === "playing";
330
+ const counting = s.phase === "counting";
331
+ const mine = playing && s.turn === seat;
332
+ const many = s.rules.games > 1;
333
+ const w = over ? winners(s) : [];
334
+ // The count is shown while it is being agreed, and after a game that was counted.
335
+ const counted = counting || s.score !== null;
336
+ const score = counted ? scoreGo(s, s.dead) : null;
337
+ const dead = new Set(counted ? s.dead : []);
338
+ const stars = new Set(starPoints(n));
339
+ const marks = [];
340
+ if (s.last !== null)
341
+ marks.push({ cell: s.last, mark: "last" });
342
+ if (playing && s.ko !== null)
343
+ marks.push({ cell: s.ko, mark: "barred" });
344
+ if (score)
345
+ for (let cell = 0; cell < n * n; cell++)
346
+ if (score.count.owner[cell] && (dead.has(cell) || stoneAt(s.board, cell) === EMPTY))
347
+ marks.push({ cell, mark: "claimed", owner: seatOf(s, score.count.owner[cell]) });
348
+ const stones = [];
349
+ const cells = [];
350
+ for (let cell = 0; cell < n * n; cell++) {
351
+ const colour = stoneAt(s.board, cell);
352
+ if (colour === EMPTY)
353
+ cells.push(null);
354
+ else {
355
+ stones.push(cell);
356
+ const piece = stoneOf(s, seatOf(s, colour));
357
+ cells.push(dead.has(cell) ? { ...piece, faded: true, label: `${piece.label}, dead` } : piece);
358
+ }
359
+ }
360
+ const grid = {
361
+ id: "board",
362
+ label: "Board",
363
+ rows: n,
364
+ cols: n,
365
+ points: true,
366
+ coords: coordsOf(n),
367
+ cells,
368
+ shade: Array.from({ length: n * n }, (_, cell) => (stars.has(cell) ? "spot" : "plain")),
369
+ marks,
370
+ // On your turn, every crossing the rules allow. While counting, either player may tap any group.
371
+ tap: mine ? { type: "place", cells: legalGoMoves(s, seat) } : counting && (seat === 0 || seat === 1) ? { type: "toggle", cells: stones } : null,
372
+ };
373
+ let choices = null;
374
+ if (mine) {
375
+ choices = { prompt: "", options: [{ label: "Pass", move: { type: "pass" }, hint: "Give up your turn. Two passes in a row end the game.", ...(s.passed ? {} : { confirm: "Pass your turn? When both players pass, play stops and the game is counted." }) }] };
376
+ }
377
+ else if (counting && (seat === 0 || seat === 1)) {
378
+ choices = {
379
+ prompt: s.agreed[seat] ? `Waiting for ${who(other(seat))} to agree` : "Faded stones are dead. Tap a group to change it, then agree the count.",
380
+ options: [...(s.agreed[seat] ? [] : [{ label: "Agree", move: { type: "agree" }, primary: true }]), { label: "Play on", move: { type: "resume" }, hint: "Carry on with the game instead. The other player moves first." }],
381
+ };
382
+ }
383
+ else if (s.phase === "scored")
384
+ choices = nextHandChoices(s.ready, seat, false, "Next game");
385
+ const komi = komiOf(s.rules);
386
+ const scoreTitle = s.roundWinner === null ? `Game ${s.round} is a draw` : `${who(s.roundWinner)} wins game ${s.round}`;
387
+ const title = () => {
388
+ if (w.length > 1)
389
+ return "It's a draw!";
390
+ const winner = w[0];
391
+ if (many)
392
+ return s.wins[winner] > s.wins[other(winner)] ? `${who(winner)} wins ${s.wins[winner]} to ${s.wins[other(winner)]}!` : `${who(winner)} wins!`;
393
+ return s.score ? `${who(winner)} wins by ${plural(Math.abs(s.score[0] - s.score[1]), "point")}!` : `${who(winner)} wins!`;
394
+ };
395
+ const done = s.phase === "scored" || over;
396
+ return {
397
+ game: "go",
398
+ seats: s.seats,
399
+ me: seat,
400
+ teams: false,
401
+ phase: s.phase,
402
+ turn: playing ? s.turn : null,
403
+ grids: [grid],
404
+ // The stones each player has taken, beside their name.
405
+ reserves: [0, 1].map((i) => ({ owner: i, label: "Captured", pieces: [{ piece: stoneOf(s, other(i)), count: s.captured[i] }] })),
406
+ choices,
407
+ players: [0, 1].map((i) => ({
408
+ // (The stones each has taken are beside the name already: see `reserves`.)
409
+ line: many ? plural(s.wins[i], "win") : "",
410
+ piece: stoneOf(s, i),
411
+ badge: counting ? (s.agreed[i] ? "Agreed" : undefined) : !playing ? undefined : s.passes === 1 && s.turn === other(i) ? "Passed" : s.moves === 0 && s.turn === i ? "Goes first" : undefined,
412
+ })),
413
+ info: [
414
+ `${s.rules.scoring === "area" ? "Chinese" : "Japanese"} counting${komi ? ` · White gets ${fmt(komi)}` : ""}`,
415
+ ...(s.rules.handicap >= 2 ? [`Black starts with ${s.rules.handicap} stones`] : []),
416
+ ...(many ? [`Game ${s.round} of ${s.rules.games}`] : []),
417
+ ],
418
+ narration: s.narration ?? (mine ? (s.rules.handicap >= 2 ? "Black has the handicap stones: you're White, and you go first" : "You're Black: you go first") : null),
419
+ // A match is scored in games won. One game shows the count once there is one, and until then the stones taken.
420
+ scores: (many ? s.wins : score ? score.totals : s.captured).map((value, i) => ({ seats: [i], value })),
421
+ result: done && (many || s.score)
422
+ ? {
423
+ key: `game-${s.round}`,
424
+ title: scoreTitle,
425
+ sides: [[0], [1]],
426
+ rows: [...(s.score && score ? score.rows : []), ...(many ? [{ label: "Wins", values: s.wins.map(String), tone: "total" }, ...(s.draws ? [{ label: "Draws", values: [String(s.draws), String(s.draws)] }] : [])] : [])],
427
+ }
428
+ : null,
429
+ over: over ? { winners: w, title: title() } : null,
430
+ };
431
+ }
432
+ // ---- The computer player --------------------------------------------------------------------------
433
+ // Monte Carlo tree search over quick playouts (goboard.ts has the search itself). What is decided
434
+ // here is how much of it a player gets, and when to stop playing.
435
+ //
436
+ // How much: a think is rationed in playouts, and the ration is so many for each millisecond the
437
+ // table grants (`PLAYOUTS_PER_MS`, a little under what a modest server manages), scaled down for a
438
+ // weaker player. So a table that gives two seconds gets a search eighty times the size of one that
439
+ // gives twenty-five milliseconds, and on any machine quick enough the ration, not the clock, is
440
+ // what ends it: the same dice then give the same move everywhere. The clock is the hard stop all
441
+ // the same. Three quarters of the budget and the search ends wherever it has got to, and the move
442
+ // tried most so far is played; with almost no time at all (the table's "answer at once") that is
443
+ // simply the move that looks best at a glance. Asked with no budget (a test, a replay) the ration
444
+ // is `UNTIMED_MS` worth and the clock is never read.
445
+ //
446
+ // When to stop: it passes when every stone and point on the board is clearly somebody's, both in
447
+ // its playouts and in the count the game would propose (so the borders are closed, and nothing is
448
+ // left for either side to take); when the other player has passed and that count is a win; when it
449
+ // has no move that isn't filling its own eye; and (so that two of them always finish) when the game
450
+ // has gone on absurdly long. It doesn't give up a lost game: a person who wants out resigns.
451
+ /**
452
+ * Playouts a millisecond for a full-strength player, by board size. A search manages about 11 to
453
+ * 14 on 9×9, 7 to 10 on 13×13 and 3.5 to 5.5 on 19×19 on the machine this was written on (two
454
+ * shared cores), from the opening to the endgame.
455
+ */
456
+ const PLAYOUTS_PER_MS = { 9: 8, 13: 5, 19: 2.4 };
457
+ /** The time a think is rationed for when nobody says how long it may take. */
458
+ const UNTIMED_MS = 15;
459
+ /** The share of a budget a think uses: the rest is for getting the move back to the table. */
460
+ const BUDGET_SHARE = 0.75;
461
+ /** A player at full skill gets the whole ration; one at no skill about an eightieth of it. */
462
+ export function goEffort(n, skill, budget) {
463
+ const ms = budget ? Math.max(0, budget.ms) * BUDGET_SHARE : UNTIMED_MS;
464
+ const playouts = Math.max(1, Math.round((PLAYOUTS_PER_MS[n] ?? 1) * ms * Math.pow(80, Math.min(1, Math.max(0, skill)) - 1)));
465
+ return budget ? { playouts, deadline: performance.now() + ms } : { playouts };
466
+ }
467
+ /** How sure a point's owner has to be, over the playouts, for it to count as settled. */
468
+ const SETTLED = 0.7;
469
+ /** Playouts needed before the picture they give of the board is believed. */
470
+ const ENOUGH = 16;
471
+ /** The most points that can be unsettled for the game still to be over: the liberties shared in a seki or two. */
472
+ const SHARED = 8;
473
+ /**
474
+ * The computer player's move in a game being played: a crossing, or a pass. `effort` says how hard
475
+ * to look (tests and measurements give their own); the dice come from `rand`.
476
+ */
477
+ export function goBotMove(s, seat, effort, rand = Math.random, sloppy = false) {
478
+ const n = s.rules.size;
479
+ // No game of Go is this long: two computer players that somehow got here stop.
480
+ if (s.moves >= n * n * 3)
481
+ return { type: "pass" };
482
+ const colour = colourOf(s, seat);
483
+ const b = boardOf(s, true);
484
+ const legal = legalGoMoves(s, seat);
485
+ const seed = Math.floor(rand() * 4294967296);
486
+ if (sloppy) {
487
+ // A careless move: somewhere that doesn't look silly, without reading anything.
488
+ const fair = legal.filter((cell) => !b.isEye(b.g.at[cell], colour) && goPrior(b, b.g.at[cell], colour) >= 0.45);
489
+ if (fair.length)
490
+ return { type: "place", cell: fair[Math.floor(rand() * fair.length)] };
491
+ }
492
+ const komi = komiOf(s.rules);
493
+ const thought = searchGo(b, colour, legal, { ...effort, komi, seed, passed: s.passes === 1, canPass: s.passes === 1 });
494
+ if (thought.cell === null)
495
+ return { type: "pass" };
496
+ if (thought.playouts >= ENOUGH) {
497
+ const cells = cellsOf(s.board);
498
+ let unsure = 0;
499
+ for (let cell = 0; cell < cells.length; cell++)
500
+ if (Math.abs(thought.owners[cell]) < SETTLED)
501
+ unsure++;
502
+ // What the count would be if play stopped here: the marking the game itself would propose
503
+ // (after this pass, and the other player's if they haven't yet).
504
+ const stopped = () => scoreGo(s, proposedDead(s, s.passes === 1 ? colourOf(s, other(seat)) : colour));
505
+ // They have passed: if the count as it would stand is a win, so does it.
506
+ if (s.passes === 1) {
507
+ const { totals } = stopped();
508
+ if (totals[seat] > totals[other(seat)])
509
+ return { type: "pass" };
510
+ }
511
+ // Nothing left to do: every stone and every point is clearly somebody's, in its playouts and in
512
+ // that count. A point the count gives to nobody is still to be played for, unless a stone there
513
+ // would only be thrown away (the liberties two groups share in a seki), which is nobody's for good.
514
+ if (unsure <= SHARED) {
515
+ const { count } = stopped();
516
+ let todo = false;
517
+ for (let cell = 0; cell < cells.length && !todo; cell++) {
518
+ if (cells[cell] === EMPTY && count.owner[cell] === EMPTY)
519
+ todo = b.legal(b.g.at[cell], colour) && b.selfAtari(b.g.at[cell], colour) === 0;
520
+ else
521
+ todo = Math.abs(thought.owners[cell]) < SETTLED;
522
+ }
523
+ if (!todo)
524
+ return { type: "pass" };
525
+ }
526
+ }
527
+ return { type: "place", cell: thought.cell };
528
+ }
529
+ function bot(s, seat, style, rand = Math.random, budget) {
530
+ if (seat !== 0 && seat !== 1)
531
+ return null;
532
+ if (s.phase === "scored")
533
+ return s.ready[seat] ? null : { type: "next" };
534
+ if (s.phase === "counting") {
535
+ if (s.agreed[seat])
536
+ return null;
537
+ // Its own estimate is the game's: the same playouts from the same position. It agrees to any
538
+ // marking that costs it nothing against that, and otherwise plays on, once. A second time it
539
+ // lets the marking stand: a person who is sure (and may be right: it can't read a seki) isn't
540
+ // held at the board for ever.
541
+ const own = scoreGo(s, proposedDead(s, colourOf(s, s.turn))).totals;
542
+ const marked = scoreGo(s, s.dead).totals;
543
+ const worse = marked[seat] - marked[other(seat)] < own[seat] - own[other(seat)];
544
+ return worse && s.resumes < 1 ? { type: "resume" } : { type: "agree" };
545
+ }
546
+ if (s.phase !== "playing" || s.turn !== seat)
547
+ return null;
548
+ const skill = style.skill ?? 1;
549
+ // Being careless is about where a stone goes, never about whether the game is over.
550
+ return goBotMove(s, seat, goEffort(s.rules.size, skill, budget), rand, s.passes === 0 && careless(style, rand));
551
+ }
552
+ /**
553
+ * How the game stands for a seat, in points, by a quick estimate that depends on the position
554
+ * alone: for a computer player asked for a draw. Close games are "level".
555
+ */
556
+ function standing(s, seat) {
557
+ if (s.phase !== "playing")
558
+ return null;
559
+ const n = s.rules.size;
560
+ const b = boardOf({ ...s, ko: null, last: null }, true);
561
+ const owners = estimateOwners(b, colourOf(s, s.turn), Math.round(estimateGames(n) / 2), b.lo ^ 0x51ed27);
562
+ let margin = -komiOf(s.rules);
563
+ for (let cell = 0; cell < owners.length; cell++)
564
+ margin += owners[cell];
565
+ const mine = colourOf(s, seat) === BLACK ? margin : -margin;
566
+ return Math.abs(mine) < 3 ? 0 : Math.round(mine);
567
+ }
568
+ // ---- The game -------------------------------------------------------------------------------------
569
+ function normalize(rules) {
570
+ const given = (rules && typeof rules === "object" ? rules : {});
571
+ const base = PRESETS.find((p) => p.id === given.id) ?? smallGo;
572
+ const r = { ...base, ...given };
573
+ const handicap = Math.floor(Number(r.handicap));
574
+ return {
575
+ id: String(r.id),
576
+ name: String(r.name),
577
+ size: SIZES.includes(Number(r.size)) ? Number(r.size) : 9,
578
+ scoring: r.scoring === "territory" ? "territory" : "area",
579
+ komi: KOMI.includes(Number(r.komi)) ? Number(r.komi) : 7.5,
580
+ handicap: handicap >= 1 && handicap <= 9 ? handicap : 0,
581
+ black: r.black === "seat1" || r.black === "seat2" ? r.black : "random",
582
+ ko: r.ko === "simple" ? "simple" : "superko",
583
+ suicide: r.suicide === true,
584
+ games: [1, 3, 5, 7].includes(Number(r.games)) ? Number(r.games) : 1,
585
+ };
586
+ }
587
+ const rules = {
588
+ id: "go",
589
+ name: "Go",
590
+ emoji: "⚪",
591
+ blurb: "Surround more of the board than they do (Weiqi, Baduk)",
592
+ tagline: "Go, also called Weiqi and Baduk: black and white stones on the crossings of a grid. Surround empty points to own them, and take away the last breathing space of their stones to capture them.",
593
+ seats: { min: 2, max: 2, default: 2 },
594
+ teams: false,
595
+ presets: PRESETS,
596
+ fields: [
597
+ { key: "size", label: "Board size", hint: "9×9 is a quick game and the place to learn. The computer plays a fair game there, and a weak one on the bigger boards.", kind: "choice", options: [[9, "9×9 (small)"], [13, "13×13"], [19, "19×19 (full)"]] },
598
+ { key: "scoring", label: "Counting", hint: "Chinese counts your stones and the points you surround. Japanese counts the points you surround and the stones you captured.", kind: "choice", options: [["area", "Chinese (area)"], ["territory", "Japanese (territory)"]] },
599
+ { key: "komi", label: "Komi: points White gets for moving second", hint: "A half point means no drawn games. With a handicap White gets just the half point.", kind: "choice", options: KOMI.map((k) => [k, k === 0 ? "None" : String(k)]) },
600
+ { key: "handicap", label: "Handicap stones for Black", hint: "Black starts with stones on the star points, and White moves first.", kind: "choice", options: [[0, "None"], [1, "Just no komi"], ...[2, 3, 4, 5, 6, 7, 8, 9].map((k) => [k, `${k} stones`])] },
601
+ { key: "black", label: "Who plays Black", hint: "Black moves first, and by custom is the less experienced player.", kind: "choice", options: [["random", "Luck of the draw"], ["seat1", "The first seat"], ["seat2", "The second seat"]] },
602
+ { key: "ko", label: "Repeating a position", hint: "Superko: the board may never look the way it has before. Simple ko only stops a stone being taken straight back.", kind: "choice", options: [["superko", "Never (superko)"], ["simple", "Not straight away (simple ko)"]] },
603
+ { key: "suicide", label: "Allow suicide", hint: "A stone played where it has no liberties takes its own group off the board. Most rules don't allow it.", kind: "toggle" },
604
+ { key: "games", label: "Games in a match", kind: "choice", options: [[1, "Just one"], [3, "Three"], [5, "Five"], [7, "Seven"]] },
605
+ ],
606
+ normalize,
607
+ validate: () => [],
608
+ describe(r) {
609
+ const komi = komiOf(r);
610
+ return [
611
+ `Black and White take turns putting a stone on any empty crossing of a ${r.size}×${r.size} board. ${r.handicap >= 2 ? `Black starts with ${r.handicap} stones on the star points, and White moves first.` : "Black moves first."} ${r.black === "random" ? "Who plays Black is the luck of the draw." : `The ${r.black === "seat1" ? "first" : "second"} seat plays Black.`}`,
612
+ "Stones of one colour that touch along the lines are a group. A group with no empty crossing beside it is captured and comes off the board.",
613
+ `${r.suicide ? "A stone played where it has no liberties takes its own group off." : "You can't play a stone where it would have no liberties, unless it captures."} ${r.ko === "superko" ? "You can't make the board look the way it has before in this game (ko)." : "You can't take a ko straight back: play somewhere else first."}`,
614
+ "You may pass. When both players pass, play stops and you agree which stones left on the board are dead: tap a group to mark it, and the count stands when you both agree. Either of you can play on instead.",
615
+ r.scoring === "area"
616
+ ? `Chinese counting: your stones on the board plus the empty points only you surround.${komi ? ` White gets ${fmt(komi)} more.` : ""}`
617
+ : `Japanese counting: the empty points only you surround, plus the stones you captured and their dead stones.${komi ? ` White gets ${fmt(komi)} more.` : ""}`,
618
+ r.size === 9 ? "The computer plays a sensible game on this board: a beginner will find it a real opponent at Hard, and should beat it at Easy." : "The computer is weak on a board this size: it knows the basics, but an experienced player will beat it easily. It plays its best on 9×9.",
619
+ ...(r.games > 1 ? [`A match is ${r.games} games; most wins takes it.${r.handicap ? "" : " Players take turns with Black."}`] : []),
620
+ ];
621
+ },
622
+ newGame: newGo,
623
+ apply,
624
+ view,
625
+ bot,
626
+ // A search: off the table's main thread (see TableGame.heavy). Agreeing a count takes no thought.
627
+ heavy: (s, seat) => s.phase === "playing" && s.turn === seat,
628
+ thinkingTime: (s) => (s.phase === "playing" ? thinkFor(6, s.moves < 2 ? 0.7 : 1) : 900),
629
+ summary(s) {
630
+ const over = s.phase === "finished";
631
+ return {
632
+ game: "go",
633
+ rules: s.rules.name,
634
+ phase: s.phase,
635
+ over,
636
+ hand: s.round,
637
+ // A match is told in games won; one game in its count, once it has been counted.
638
+ scoreLine: s.rules.games === 1 && s.score ? `${fmt(s.score[0])} to ${fmt(s.score[1])}` : `${s.wins[0]} to ${s.wins[1]}${s.draws ? ` · ${s.draws} drawn` : ""}`,
639
+ order: over ? order(s) : null,
640
+ winners: over ? winners(s) : null,
641
+ teams: false,
642
+ seats: s.seats,
643
+ };
644
+ },
645
+ };
646
+ /**
647
+ * Go, with resigning (the usual way a game of Go ends), taking a move back, and draw offers (see
648
+ * offers.ts). A draw by agreement is not really a thing in Go, but the kit's house rule offers
649
+ * draws and take-backs together or not at all, and a family table wants the take-back: a table
650
+ * that wants neither sets Offers to "Neither". Only the last turn and the reply to it can be had
651
+ * back (`history: 2`): every position kept is another copy of the game's list of hashes.
652
+ */
653
+ export const go = withOffers(rules, {
654
+ takeBack: true,
655
+ history: 2,
656
+ settle: settleMatch,
657
+ standing,
658
+ });
659
+ //# sourceMappingURL=go.js.map