@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
@@ -0,0 +1,1221 @@
1
+ // Backgammon: fifteen checkers a side, two dice, and a race round the board in opposite
2
+ // directions. A checker lands on any point the other side doesn't hold with two or more; landing on
3
+ // one alone (a blot) sends it to the bar, and it has to come back in before anything else moves.
4
+ // Once all fifteen are in your home board you bear them off; the first to bear off all of them wins:
5
+ // a single game, a gammon (double) if the loser has borne off none, a backgammon (triple) if the
6
+ // loser also still has a checker on the bar or in the winner's home board.
7
+ //
8
+ // A turn is several moves to the server, as a run of jumps is in Checkers: roll, then one checker
9
+ // a die, the seat keeping the turn until its dice are played or nothing more can be. What may be
10
+ // played at each step comes from one generator of whole legal turns, so "you must play both dice if
11
+ // you can" and "if only one can be played it must be the bigger" hold a step at a time. Until the
12
+ // turn is over the steps made so far can be taken back: the dice are already on the table, so
13
+ // nothing is learned by it. A roll that can't be played passes on its own.
14
+ //
15
+ // The doubling cube and match play are house rules: a friendly game without the cube, a money game
16
+ // (with the Jacoby rule and automatic doubles as options), or a match to a number of points with
17
+ // the Crawford rule.
18
+ //
19
+ // The board is kept one way whoever is looking: the points are numbered as the first seat counts
20
+ // them, 24 down to 1, and the second seat travels them the other way. Each seat's view asks the
21
+ // table to draw the board from its own side.
22
+ //
23
+ // Resigning isn't here yet: when the kit's shared offers arrive, Backgammon opts in for resign only
24
+ // (never a draw), and what a resignation gives is the cube times what the position could still lose
25
+ // (a single game, or a gammon or backgammon while those are still possible).
26
+ import { mulberry32 } from "@journey-to-code/rules";
27
+ import { careless, clone, GameError, markReady, nextHandChoices, nextRandom, who } from "./table.js";
28
+ export const friendlyBackgammon = { id: "backgammon-friendly", name: "Friendly", match: 0, cube: false, crawford: false, jacoby: false, autoDoubles: false, start: "standard", confirm: false, quick: false };
29
+ export const moneyBackgammon = { ...friendlyBackgammon, id: "backgammon-money", name: "Money game (with the cube)", cube: true, jacoby: true };
30
+ const matchTo = (n) => ({ ...friendlyBackgammon, id: `backgammon-match${n}`, name: `Match to ${n}`, match: n, cube: true, crawford: true });
31
+ export const match3Backgammon = matchTo(3);
32
+ export const match5Backgammon = matchTo(5);
33
+ export const match7Backgammon = matchTo(7);
34
+ export const nackgammon = { ...friendlyBackgammon, id: "backgammon-nack", name: "Nackgammon", start: "nack" };
35
+ const PRESETS = [friendlyBackgammon, moneyBackgammon, match3Backgammon, match5Backgammon, match7Backgammon, nackgammon];
36
+ const MATCHES = [0, 3, 5, 7, 9, 11];
37
+ const CUBE_MAX = 64;
38
+ /** Where the fifteen checkers start, by the point as their own side counts it: [point, how many]. */
39
+ const STARTS = {
40
+ standard: [[24, 2], [13, 5], [8, 3], [6, 5]],
41
+ nack: [[24, 2], [23, 2], [13, 4], [8, 3], [6, 4]],
42
+ };
43
+ const other = (seat) => 1 - seat;
44
+ const ownerOf = (checker) => (checker < 15 ? 0 : 1);
45
+ // ---- The board as the table sees it -------------------------------------------------------------
46
+ // Two rows of fourteen cells: six points, the bar, six points, a tray. The first seat's home board
47
+ // is the bottom right and its tray below the second seat's.
48
+ const COLS = 14;
49
+ const BAR_CELL = [20, 6];
50
+ const HOME_CELL = [27, 13];
51
+ /** The cell a place of `stacks` is drawn in. */
52
+ function cellOfPlace(place) {
53
+ if (place === 0)
54
+ return BAR_CELL[1];
55
+ if (place === 25)
56
+ return BAR_CELL[0];
57
+ if (place >= 19)
58
+ return place - 12;
59
+ if (place >= 13)
60
+ return place - 13;
61
+ if (place >= 7)
62
+ return 26 - place;
63
+ return 27 - place;
64
+ }
65
+ const PLACE_OF_CELL = Array.from({ length: COLS * 2 }, (_, cell) => {
66
+ for (let place = 0; place <= 25; place++)
67
+ if (cellOfPlace(place) === cell)
68
+ return place;
69
+ return null;
70
+ });
71
+ /** The points in the order the first seat travels them: 24 down to 1. */
72
+ const PATH = Array.from({ length: 24 }, (_, i) => cellOfPlace(24 - i));
73
+ /** A place of `stacks` as a seat counts it: its own points 1 to 24, 25 for its own bar, 0 for the other side's. */
74
+ const pointOf = (seat, place) => (seat === 0 ? place : 25 - place);
75
+ const placeOf = pointOf;
76
+ function sidesOf(s, seat) {
77
+ const me = new Int8Array(26);
78
+ const op = new Int8Array(26);
79
+ for (let place = 0; place <= 25; place++) {
80
+ const pile = s.stacks[place];
81
+ if (!pile.length)
82
+ continue;
83
+ if (ownerOf(pile[0]) === seat)
84
+ me[pointOf(seat, place)] = pile.length;
85
+ else
86
+ op[pointOf(seat, place)] = pile.length;
87
+ }
88
+ me[0] = s.home[seat].length;
89
+ op[25] = s.home[other(seat)].length;
90
+ return { me, op };
91
+ }
92
+ /** The same position from the other side of the board. */
93
+ function turned(side, into = new Int8Array(26)) {
94
+ for (let i = 0; i <= 25; i++)
95
+ into[i] = side[25 - i];
96
+ return into;
97
+ }
98
+ /** My rearmost checker: the highest point I'm on (25 for the bar), or 0 with everything off. */
99
+ function rearmost(me) {
100
+ for (let p = 25; p >= 1; p--)
101
+ if (me[p])
102
+ return p;
103
+ return 0;
104
+ }
105
+ /**
106
+ * Whether a checker may go from `from` by `die`, given my rearmost checker `back`. Bearing off
107
+ * needs everything home, and a die bigger than the point only takes off the rearmost checker.
108
+ */
109
+ function canHop(op, back, from, die) {
110
+ if (back === 25 && from !== 25)
111
+ return false;
112
+ const to = from - die;
113
+ if (to > 0)
114
+ return op[to] < 2;
115
+ return back <= 6 && (to === 0 || from === back);
116
+ }
117
+ /** Make a hop; says whether it hit a blot. */
118
+ function hop(me, op, from, die) {
119
+ const to = from - die;
120
+ me[from]--;
121
+ if (to <= 0) {
122
+ me[0]++;
123
+ return false;
124
+ }
125
+ me[to]++;
126
+ if (op[to] !== 1)
127
+ return false;
128
+ op[to] = 0;
129
+ op[0]++;
130
+ return true;
131
+ }
132
+ function unhop(me, op, from, die, hit) {
133
+ const to = from - die;
134
+ if (to <= 0)
135
+ me[0]--;
136
+ else {
137
+ me[to]--;
138
+ if (hit) {
139
+ op[to] = 1;
140
+ op[0]--;
141
+ }
142
+ }
143
+ me[from]++;
144
+ }
145
+ /** Whether anything at all can be played with `die`. */
146
+ function anyHop(me, op, die) {
147
+ const back = rearmost(me);
148
+ for (let from = back; from >= 1; from--)
149
+ if (me[from] && canHop(op, back, from, die))
150
+ return true;
151
+ return false;
152
+ }
153
+ /**
154
+ * The most hops that can be made with `left` dice all showing `die`. With every die the same, any
155
+ * way of playing them can be put in order from the rearmost checker forwards (a checker only ever
156
+ * gets to a point from a higher one), so only those orders need looking at: `from` never rises.
157
+ */
158
+ function mostSame(me, op, die, left, max) {
159
+ if (!left)
160
+ return 0;
161
+ const back = rearmost(me);
162
+ let best = 0;
163
+ for (let from = back === 25 ? 25 : Math.min(back, max); from >= 1; from--) {
164
+ if (!me[from] || !canHop(op, back, from, die))
165
+ continue;
166
+ const hit = hop(me, op, from, die);
167
+ const n = 1 + mostSame(me, op, die, left - 1, from);
168
+ unhop(me, op, from, die, hit);
169
+ if (n > best)
170
+ best = n;
171
+ if (best === left)
172
+ break;
173
+ }
174
+ return best;
175
+ }
176
+ /** The most of `dice` (the ones still to play) that can be played from here. */
177
+ function mostHops(me, op, dice) {
178
+ if (!dice.length)
179
+ return 0;
180
+ if (dice.every((d) => d === dice[0]))
181
+ return mostSame(me, op, dice[0], dice.length, 25);
182
+ let best = 0;
183
+ for (const [first, second] of [dice, [dice[1], dice[0]]]) {
184
+ const back = rearmost(me);
185
+ for (let from = back; from >= 1 && best < 2; from--) {
186
+ if (!me[from] || !canHop(op, back, from, first))
187
+ continue;
188
+ const hit = hop(me, op, from, first);
189
+ best = Math.max(best, anyHop(me, op, second) ? 2 : 1);
190
+ unhop(me, op, from, first, hit);
191
+ }
192
+ }
193
+ return best;
194
+ }
195
+ /**
196
+ * The hops that may be played next with the dice still to play: the ones that begin a way of
197
+ * playing as many dice as can be played, and, when only one of two different dice can be, the
198
+ * bigger of them if it can.
199
+ */
200
+ function legalHops(me, op, dice) {
201
+ const values = [...new Set(dice)];
202
+ const found = [];
203
+ for (const die of values) {
204
+ const rest = [...dice];
205
+ rest.splice(rest.indexOf(die), 1);
206
+ const back = rearmost(me);
207
+ for (let from = back; from >= 1; from--) {
208
+ if (!me[from] || !canHop(op, back, from, die))
209
+ continue;
210
+ const hit = hop(me, op, from, die);
211
+ found.push({ from, die, n: 1 + mostHops(me, op, rest) });
212
+ unhop(me, op, from, die, hit);
213
+ }
214
+ }
215
+ const most = Math.max(0, ...found.map((h) => h.n));
216
+ let legal = found.filter((h) => h.n === most);
217
+ if (most === 1 && values.length === 2) {
218
+ const high = Math.max(...values);
219
+ if (legal.some((h) => h.die === high))
220
+ legal = legal.filter((h) => h.die === high);
221
+ }
222
+ return legal.map(({ from, die }) => ({ from, die }));
223
+ }
224
+ const keyOf = (me, op) => String.fromCharCode(...me, ...op);
225
+ /**
226
+ * How much looking the computer players have done: one for every hop tried while listing turns and
227
+ * one for every position judged. A look ahead is rationed by this and not by the clock, so it plays
228
+ * the same on a slow machine, and a board full of doubles can't make it slow.
229
+ */
230
+ let work = 0;
231
+ /**
232
+ * Every whole legal turn with these dice, one for each different position that can be reached
233
+ * (two orders of the same two hops are one turn). None when nothing can be played.
234
+ */
235
+ function wholeTurns(me, op, dice) {
236
+ const most = mostHops(me, op, dice);
237
+ if (!most)
238
+ return [];
239
+ const seen = new Map();
240
+ const hops = [];
241
+ const done = () => {
242
+ const key = keyOf(me, op);
243
+ if (!seen.has(key))
244
+ seen.set(key, { hops: hops.map((h) => ({ ...h })), me: me.slice(), op: op.slice() });
245
+ };
246
+ const same = (die, left, max) => {
247
+ if (!left)
248
+ return done();
249
+ const back = rearmost(me);
250
+ for (let from = back === 25 ? 25 : Math.min(back, max); from >= 1; from--) {
251
+ if (!me[from] || !canHop(op, back, from, die))
252
+ continue;
253
+ const hit = hop(me, op, from, die);
254
+ work++;
255
+ hops.push({ from, die });
256
+ same(die, left - 1, from);
257
+ hops.pop();
258
+ unhop(me, op, from, die, hit);
259
+ }
260
+ };
261
+ if (dice.every((d) => d === dice[0]))
262
+ same(dice[0], most, 25);
263
+ else if (most === 1) {
264
+ // One die only: the bigger if it can be played at all.
265
+ const high = Math.max(...dice);
266
+ same(anyHop(me, op, high) ? high : Math.min(...dice), 1, 25);
267
+ }
268
+ else {
269
+ for (const [first, second] of [dice, [dice[1], dice[0]]]) {
270
+ const back = rearmost(me);
271
+ for (let from = back; from >= 1; from--) {
272
+ if (!me[from] || !canHop(op, back, from, first))
273
+ continue;
274
+ const hit = hop(me, op, from, first);
275
+ hops.push({ from, die: first });
276
+ same(second, 1, 25);
277
+ hops.pop();
278
+ unhop(me, op, from, first, hit);
279
+ }
280
+ }
281
+ }
282
+ return [...seen.values()];
283
+ }
284
+ /** The dice still to be played this turn. */
285
+ const diceLeft = (s) => s.dice.filter((_, i) => !s.used[i]);
286
+ /**
287
+ * What the player to move may do next: one checker by one die, each as the cell it leaves and the
288
+ * cell it lands in (the tray for a checker borne off). Two dice that would do the same thing are
289
+ * one option: the one that fits exactly is used, and failing that the bigger.
290
+ */
291
+ export function backgammonSteps(s) {
292
+ if (s.phase !== "move")
293
+ return [];
294
+ const seat = s.turn;
295
+ const { me, op } = sidesOf(s, seat);
296
+ const out = new Map();
297
+ for (const h of legalHops(me, op, diceLeft(s))) {
298
+ const to = h.from - h.die;
299
+ const from = cellOfPlace(placeOf(seat, h.from));
300
+ const option = { from, to: to <= 0 ? HOME_CELL[seat] : cellOfPlace(placeOf(seat, to)), die: s.dice.findIndex((d, i) => d === h.die && !s.used[i]), value: h.die, exact: to >= 0 };
301
+ const key = `${option.from}/${option.to}`;
302
+ const had = out.get(key);
303
+ if (!had || (option.exact && !had.exact) || (option.exact === had.exact && option.value > had.value))
304
+ out.set(key, option);
305
+ }
306
+ return [...out.values()].map(({ from, to, die }) => ({ from, to, die }));
307
+ }
308
+ /** Every whole turn the player to move could play with the dice still to play: one for each position it could leave. */
309
+ export function backgammonTurns(s) {
310
+ if (s.phase !== "move")
311
+ return [];
312
+ const seat = s.turn;
313
+ const { me, op } = sidesOf(s, seat);
314
+ return wholeTurns(me, op, diceLeft(s)).map((t) => ({
315
+ steps: t.hops.map((h) => ({ from: cellOfPlace(placeOf(seat, h.from)), to: h.from - h.die <= 0 ? HOME_CELL[seat] : cellOfPlace(placeOf(seat, h.from - h.die)), die: h.die })),
316
+ }));
317
+ }
318
+ /** How far a seat still has to go: every checker's distance to being off, added up. */
319
+ export function backgammonPips(s, seat) {
320
+ let pips = 0;
321
+ for (let place = 0; place <= 25; place++) {
322
+ const pile = s.stacks[place];
323
+ if (pile.length && ownerOf(pile[0]) === seat)
324
+ pips += pile.length * pointOf(seat, place);
325
+ }
326
+ return pips;
327
+ }
328
+ // ---- A game ------------------------------------------------------------------------------------
329
+ function startGame(s) {
330
+ s.stacks = Array.from({ length: 26 }, () => []);
331
+ s.home = [[], []];
332
+ for (const seat of [0, 1]) {
333
+ let checker = seat * 15;
334
+ for (const [point, count] of STARTS[s.rules.start])
335
+ for (let i = 0; i < count; i++)
336
+ s.stacks[placeOf(seat, point)].push(checker++);
337
+ }
338
+ s.phase = "opening";
339
+ s.turn = 0;
340
+ s.opening = [null, null];
341
+ s.dice = [];
342
+ s.used = [];
343
+ s.thrower = [];
344
+ s.steps = [];
345
+ s.last = [];
346
+ s.cube = { value: 1, owner: null, turned: false };
347
+ s.autos = 0;
348
+ s.result = null;
349
+ s.ready = [false, false];
350
+ s.narration = null;
351
+ }
352
+ export function newBackgammon(rules, _seats, seed) {
353
+ const s = { game: "backgammon", rules, seats: 2, rng: seed >>> 0, round: 1, rolls: 0, crawford: false, crawfordDone: false, score: [0, 0] };
354
+ startGame(s);
355
+ return s;
356
+ }
357
+ const throwDie = (s) => 1 + Math.floor(nextRandom(s)() * 6);
358
+ /** Whether `seat` may offer a double now. */
359
+ function canDouble(s, seat) {
360
+ return s.rules.cube && s.phase === "roll" && s.turn === seat && !s.crawford && (s.cube.owner === null || s.cube.owner === seat) && s.cube.value < CUBE_MAX;
361
+ }
362
+ /** A point as it is said and written: "bar", "off", or its number. */
363
+ const said = (point) => (point === 25 ? "bar" : point <= 0 ? "off" : String(point));
364
+ /** A turn in the usual notation, from the side that played it: "24/18, 13/11*". */
365
+ function notation(steps) {
366
+ return steps.map((st) => `${said(pointOf(st.seat, st.from))}/${st.to === null ? "off" : said(pointOf(st.seat, st.to))}${st.hit ? "*" : ""}`).join(", ");
367
+ }
368
+ /** A roll as it is said: the bigger die first. */
369
+ const rolled = (dice) => (dice.length === 4 ? `double ${dice[0]}s` : `${Math.max(dice[0], dice[1])}-${Math.min(dice[0], dice[1])}`);
370
+ function endGame(s, winner, dropped) {
371
+ const loser = other(winner);
372
+ let kind = "single";
373
+ if (!dropped && s.home[loser].length === 0) {
374
+ const { op } = sidesOf(s, winner);
375
+ // The loser's checkers on my points 1 to 6 are in my home board; `op[0]` is their bar.
376
+ kind = op[0] + op[1] + op[2] + op[3] + op[4] + op[5] + op[6] > 0 ? "backgammon" : "gammon";
377
+ }
378
+ const jacoby = kind !== "single" && s.rules.jacoby && !s.cube.turned;
379
+ const points = s.cube.value * (kind === "single" || jacoby ? 1 : kind === "gammon" ? 2 : 3);
380
+ s.score[winner] += points;
381
+ s.result = { winner, kind, dropped, jacoby, points };
382
+ s.last = s.steps.length ? s.steps : s.last;
383
+ s.steps = [];
384
+ s.used = s.dice.map(() => true);
385
+ // (The table turns "{0} wins" into "You win" for the player it's about: one plain verb after each name.)
386
+ const what = dropped ? `${who(loser)} passes on the double: ${who(winner)} wins` : `${who(winner)} wins${kind === "single" ? "" : ` a ${kind}`} with the last checker off`;
387
+ s.narration = `${what}${s.rules.cube || kind !== "single" ? ` (${points} point${points === 1 ? "" : "s"}${jacoby ? ": the cube was never turned" : ""})` : ""}`;
388
+ s.phase = s.rules.match > 0 && s.score[winner] < s.rules.match ? "scored" : "finished";
389
+ }
390
+ /** The dice are thrown and the turn begins; a roll that can't be played passes at once. Changes `s`. */
391
+ function begin(s, seat, lead) {
392
+ s.turn = seat;
393
+ s.phase = "move";
394
+ s.steps = [];
395
+ s.used = s.dice.map(() => false);
396
+ s.narration = lead;
397
+ const { me, op } = sidesOf(s, seat);
398
+ if (!legalHops(me, op, s.dice).length) {
399
+ s.narration = `${lead} and can't move`;
400
+ pass(s);
401
+ }
402
+ }
403
+ /** The turn is over: the other seat is to roll. Changes `s`. */
404
+ function pass(s) {
405
+ if (s.steps.length)
406
+ s.last = s.steps;
407
+ s.steps = [];
408
+ s.used = s.dice.map(() => true);
409
+ s.turn = other(s.turn);
410
+ s.phase = "roll";
411
+ }
412
+ function apply(state, seat, move) {
413
+ const type = move?.type;
414
+ if (type !== "roll" && type !== "move" && type !== "undo" && type !== "confirm" && type !== "double" && type !== "take" && type !== "drop" && type !== "next")
415
+ throw new GameError("That's not a move in Backgammon.");
416
+ if (seat !== 0 && seat !== 1)
417
+ throw new GameError("You're not in this game.");
418
+ if (type === "next") {
419
+ if (state.phase !== "scored")
420
+ throw new GameError("There's a game going on.");
421
+ const s = clone(state);
422
+ if (markReady(s.ready, seat)) {
423
+ s.round += 1;
424
+ // The Crawford game is the one after somebody first gets to within a point of the match.
425
+ s.crawford = s.rules.crawford && !s.crawfordDone && s.score.some((v) => v === s.rules.match - 1);
426
+ if (s.crawford)
427
+ s.crawfordDone = true;
428
+ startGame(s);
429
+ }
430
+ return s;
431
+ }
432
+ if (state.phase === "scored" || state.phase === "finished")
433
+ throw new GameError("This game is over.");
434
+ if (state.phase === "opening") {
435
+ if (type !== "roll")
436
+ throw new GameError("Roll to see who goes first.");
437
+ if (state.opening[seat] !== null)
438
+ throw new GameError("You've rolled: wait for the other die.");
439
+ const s = clone(state);
440
+ s.opening[seat] = throwDie(s);
441
+ s.rolls += 1;
442
+ const a = s.opening[0] ?? null;
443
+ const b = s.opening[1] ?? null;
444
+ if (a === null || b === null) {
445
+ s.narration = `${who(seat)} rolls a ${s.opening[seat]}`;
446
+ return s;
447
+ }
448
+ if (a === b) {
449
+ // A tie: roll again, and with automatic doubles the stakes go up (once).
450
+ const auto = s.rules.autoDoubles && s.autos < 1;
451
+ if (auto) {
452
+ s.autos += 1;
453
+ s.cube.value *= 2;
454
+ }
455
+ s.opening = [null, null];
456
+ s.narration = `Both roll a ${a}${auto ? `: the cube goes to ${s.cube.value}` : ""}. Roll again`;
457
+ return s;
458
+ }
459
+ const first = a > b ? 0 : 1;
460
+ s.dice = [a, b];
461
+ s.thrower = [0, 1];
462
+ begin(s, first, `${who(first)} rolls the higher die: ${rolled([a, b])} to start`);
463
+ return s;
464
+ }
465
+ if (state.phase === "offer") {
466
+ if (seat !== other(state.turn))
467
+ throw new GameError(type === "take" || type === "drop" ? "The double isn't yours to answer." : "Waiting for an answer to the double.");
468
+ if (type !== "take" && type !== "drop")
469
+ throw new GameError("Take the double or drop it.");
470
+ const s = clone(state);
471
+ if (type === "drop") {
472
+ endGame(s, s.turn, true);
473
+ return s;
474
+ }
475
+ s.cube = { value: s.cube.value * 2, owner: seat, turned: true };
476
+ s.phase = "roll";
477
+ s.narration = `${who(seat)} takes: the cube is on ${s.cube.value}`;
478
+ return s;
479
+ }
480
+ if (state.turn !== seat)
481
+ throw new GameError("It's not your turn.");
482
+ if (state.phase === "roll") {
483
+ if (type === "double") {
484
+ if (!state.rules.cube)
485
+ throw new GameError("There's no doubling cube in this game.");
486
+ if (state.crawford)
487
+ throw new GameError("No doubling in the Crawford game.");
488
+ if (!canDouble(state, seat))
489
+ throw new GameError(state.cube.value >= CUBE_MAX ? "The cube can't go any higher." : "The cube is on the other side: only they can double.");
490
+ const s = clone(state);
491
+ s.phase = "offer";
492
+ s.narration = `${who(seat)} is doubling to ${s.cube.value * 2}`;
493
+ return s;
494
+ }
495
+ if (type !== "roll")
496
+ throw new GameError("Roll the dice first.");
497
+ const s = clone(state);
498
+ const [a, b] = [throwDie(s), throwDie(s)];
499
+ s.dice = a === b ? [a, a, a, a] : [a, b];
500
+ s.thrower = s.dice.map(() => seat);
501
+ s.rolls += 1;
502
+ begin(s, seat, `${who(seat)} rolls ${rolled(s.dice)}`);
503
+ return s;
504
+ }
505
+ // Playing the dice.
506
+ if (type === "roll")
507
+ throw new GameError("You've rolled: play your dice.");
508
+ if (type === "double" || type === "take" || type === "drop")
509
+ throw new GameError("You can only double before you roll.");
510
+ const options = backgammonSteps(state);
511
+ if (type === "undo") {
512
+ if (!state.steps.length)
513
+ throw new GameError("There's nothing to take back.");
514
+ const s = clone(state);
515
+ const st = s.steps.pop();
516
+ const checker = (st.to === null ? s.home[seat] : s.stacks[st.to]).pop();
517
+ if (st.hit)
518
+ s.stacks[st.to].push(s.stacks[placeOf(seat, 0)].pop());
519
+ s.stacks[st.from].push(checker);
520
+ s.used[st.die] = false;
521
+ s.narration = `${who(seat)} takes a move back`;
522
+ return s;
523
+ }
524
+ if (type === "confirm") {
525
+ if (options.length)
526
+ throw new GameError("You still have dice to play.");
527
+ const s = clone(state);
528
+ finishTurn(s);
529
+ return s;
530
+ }
531
+ const from = Number(move.from);
532
+ const to = Number(move.to);
533
+ const option = options.find((o) => o.from === from && o.to === to);
534
+ if (!option) {
535
+ const place = Number.isInteger(from) ? (PLACE_OF_CELL[from] ?? null) : null;
536
+ const pile = place === null ? null : state.stacks[place];
537
+ if (!Number.isInteger(to) || to < 0 || to >= COLS * 2 || place === null)
538
+ throw new GameError("That's not on the board.");
539
+ if (!pile.length || ownerOf(pile[0]) !== seat)
540
+ throw new GameError("That's not your checker.");
541
+ if (!options.length)
542
+ throw new GameError("You have no more moves: finish your turn.");
543
+ if (state.stacks[placeOf(seat, 25)].length && pointOf(seat, place) !== 25)
544
+ throw new GameError("Your checker on the bar has to come in first.");
545
+ if (!options.some((o) => o.from === from))
546
+ throw new GameError("That checker can't move with these dice (both have to be played if they can be).");
547
+ throw new GameError("That checker can't go there.");
548
+ }
549
+ const s = clone(state);
550
+ const fromPlace = PLACE_OF_CELL[from];
551
+ const off = to === HOME_CELL[seat];
552
+ const toPlace = off ? null : PLACE_OF_CELL[to];
553
+ const checker = s.stacks[fromPlace].pop();
554
+ let hit = false;
555
+ if (toPlace === null)
556
+ s.home[seat].push(checker);
557
+ else {
558
+ const pile = s.stacks[toPlace];
559
+ if (pile.length === 1 && ownerOf(pile[0]) !== seat) {
560
+ s.stacks[placeOf(seat, 0)].push(pile.pop());
561
+ hit = true;
562
+ }
563
+ pile.push(checker);
564
+ }
565
+ s.used[option.die] = true;
566
+ s.steps.push({ seat, from: fromPlace, to: toPlace, die: option.die, hit });
567
+ s.narration = `${who(seat)} plays ${notation(s.steps)}`;
568
+ if (s.home[seat].length === 15)
569
+ endGame(s, seat, false);
570
+ else if (!s.rules.confirm && !backgammonSteps(s).length)
571
+ finishTurn(s);
572
+ return s;
573
+ }
574
+ /** The dice are played (or no more can be): say what was played and pass the turn. Changes `s`. */
575
+ function finishTurn(s) {
576
+ const left = diceLeft(s);
577
+ s.narration = `${who(s.turn)} plays ${notation(s.steps)}${left.length ? ` and can't play the ${left.length > 1 ? "rest" : left[0]}` : ""}`;
578
+ pass(s);
579
+ }
580
+ // ---- What a seat sees ---------------------------------------------------------------------------
581
+ const TONES = ["light", "dark"];
582
+ function checkerOf(seat, pile) {
583
+ return { owner: seat, kind: "checker", glyph: "●", label: `${TONES[seat]} checker`, tone: TONES[seat], ...(pile ? { count: pile.length, ids: pile.map((c) => `c${c}`) } : {}) };
584
+ }
585
+ function resultOf(s) {
586
+ const r = s.result;
587
+ if (!r || !s.rules.match)
588
+ return null;
589
+ const how = r.dropped ? "the double was dropped" : r.kind === "single" ? "a single game" : `a ${r.kind}${r.jacoby ? " (not counted)" : ""}`;
590
+ return {
591
+ key: `game-${s.round}`,
592
+ title: `${who(r.winner)} wins game ${s.round}: ${how}`,
593
+ sides: [[0], [1]],
594
+ rows: [
595
+ { label: `Game ${s.round}`, values: [0, 1].map((seat) => (seat === r.winner ? `+${r.points}` : "")) },
596
+ { label: `Match to ${s.rules.match}`, values: s.score.map(String), tone: "total" },
597
+ ],
598
+ };
599
+ }
600
+ function view(s, seat) {
601
+ const over = s.phase === "finished";
602
+ const playing = s.phase === "roll" || s.phase === "move" || s.phase === "offer" || s.phase === "opening";
603
+ const moving = s.phase === "move" && s.turn === seat;
604
+ const options = moving ? backgammonSteps(s) : [];
605
+ const cells = Array(COLS * 2).fill(null);
606
+ for (let place = 0; place <= 25; place++) {
607
+ const pile = s.stacks[place];
608
+ if (pile.length)
609
+ cells[cellOfPlace(place)] = checkerOf(ownerOf(pile[0]), pile);
610
+ }
611
+ for (const o of [0, 1])
612
+ if (s.home[o].length)
613
+ cells[HOME_CELL[o]] = checkerOf(o, s.home[o]);
614
+ // Where this turn has been so far, or (before its first move) where the turn before went.
615
+ const marks = [];
616
+ for (const st of s.steps.length ? s.steps : s.last) {
617
+ marks.push({ cell: cellOfPlace(st.from), mark: "from" });
618
+ marks.push({ cell: st.to === null ? HOME_CELL[st.seat] : cellOfPlace(st.to), mark: "last" });
619
+ }
620
+ // A checker on the bar has to come in before anything else moves: lit for both players.
621
+ if (s.phase === "move" && s.stacks[placeOf(s.turn, 25)].length)
622
+ marks.push({ cell: BAR_CELL[s.turn], mark: "must" });
623
+ const sources = new Map();
624
+ for (const o of options)
625
+ sources.set(o.from, [...(sources.get(o.from) ?? []), o.to]);
626
+ const grid = {
627
+ id: "board",
628
+ label: "Board",
629
+ rows: 2,
630
+ cols: COLS,
631
+ cells,
632
+ track: { kind: "points", path: PATH, bar: BAR_CELL, home: HOME_CELL },
633
+ // The first seat's home board is the bottom right of `cells`; the second seat sees the board turned over, so theirs is too.
634
+ flip: seat === 1 ? "rows" : false,
635
+ marks,
636
+ select: moving ? { type: "move", sources: [...sources].map(([from, to]) => ({ from, to })), quick: s.rules.quick } : null,
637
+ };
638
+ const match = s.rules.match;
639
+ const mine = s.turn === seat;
640
+ let choices = null;
641
+ if (s.phase === "scored")
642
+ choices = nextHandChoices(s.ready, seat, false, "Next game");
643
+ else if (s.phase === "opening" && s.opening[seat] === null)
644
+ choices = { prompt: "", options: [{ label: "Roll for the first move", move: { type: "roll" }, primary: true }] };
645
+ else if (s.phase === "roll" && mine) {
646
+ const list = [{ label: "Roll", move: { type: "roll" }, primary: true }];
647
+ if (canDouble(s, seat))
648
+ list.push({ label: `Double to ${s.cube.value * 2}`, move: { type: "double" }, hint: "Offer to double the stakes before you roll" });
649
+ choices = { prompt: "", options: list };
650
+ }
651
+ else if (s.phase === "offer" && !mine) {
652
+ choices = {
653
+ // What is on offer is in the narration, and on the cube.
654
+ prompt: "",
655
+ options: [
656
+ { label: "Take", move: { type: "take" }, primary: true, hint: `Play on for ${s.cube.value * 2}: the cube is yours` },
657
+ { label: "Drop", move: { type: "drop" }, hint: `Give up this game for ${s.cube.value} point${s.cube.value === 1 ? "" : "s"}` },
658
+ ],
659
+ };
660
+ }
661
+ else if (moving && s.steps.length) {
662
+ choices = {
663
+ prompt: "",
664
+ options: [...(options.length ? [] : [{ label: "Done", move: { type: "confirm" }, primary: true, hint: "End your turn" }]), { label: "Undo", move: { type: "undo" }, hint: "Take back your last move" }],
665
+ };
666
+ }
667
+ const cube = s.rules.cube ? { value: s.cube.value, owner: s.cube.owner, offered: s.phase === "offer" ? s.turn : null, dead: s.crawford } : null;
668
+ const r = s.result;
669
+ const info = [
670
+ ...(match ? [`Match to ${match} · game ${s.round}`] : []),
671
+ ...(s.crawford && playing ? ["The Crawford game: no doubling"] : []),
672
+ ...(s.rules.jacoby && playing && !s.cube.turned ? ["Jacoby rule: gammons count once the cube is turned"] : []),
673
+ ];
674
+ return {
675
+ game: "backgammon",
676
+ seats: 2,
677
+ me: seat,
678
+ teams: false,
679
+ phase: s.phase,
680
+ turn: s.phase === "opening" || !playing ? null : s.phase === "offer" ? other(s.turn) : s.turn,
681
+ grids: [grid],
682
+ reserves: [],
683
+ dice: {
684
+ dice: s.phase === "opening"
685
+ ? [0, 1].map((o) => ({ id: o, value: s.opening[o] ?? null, used: false, owner: o }))
686
+ : s.dice.map((value, id) => ({ id, value, used: s.phase !== "move" || s.used[id], owner: s.thrower[id] })),
687
+ roll: s.rolls,
688
+ },
689
+ cube,
690
+ choices,
691
+ players: [0, 1].map((o) => {
692
+ const pips = backgammonPips(s, o);
693
+ const off = s.home[o].length;
694
+ return {
695
+ line: [`${pips} pip${pips === 1 ? "" : "s"}`, ...(off ? [`${off} off`] : []), ...(match ? [`${s.score[o]} of ${match}`] : [])].join(" · "),
696
+ piece: checkerOf(o),
697
+ badge: s.phase === "offer" && s.turn === o ? "Doubles" : s.cube.owner === o ? `Cube ${s.cube.value}` : undefined,
698
+ };
699
+ }),
700
+ info,
701
+ narration: s.narration ?? (s.phase === "opening" ? "Each roll a die: the higher goes first, with those two dice" : null),
702
+ scores: s.score.map((value, o) => ({ seats: [o], value })),
703
+ result: playing ? null : resultOf(s),
704
+ over: over && r
705
+ ? {
706
+ winners: [r.winner],
707
+ title: match
708
+ ? `${who(r.winner)} wins the match ${s.score[r.winner]} to ${s.score[other(r.winner)]}!`
709
+ : `${who(r.winner)} wins${r.dropped || r.kind === "single" ? "" : ` a ${r.kind}`}${s.rules.cube || r.points > 1 ? `: ${r.points} point${r.points === 1 ? "" : "s"}` : ""}!`,
710
+ }
711
+ : null,
712
+ };
713
+ }
714
+ // ---- Computer players ---------------------------------------------------------------------------
715
+ // A computer player looks at every whole turn its dice allow and judges the position each leaves,
716
+ // with a hand-built judgement and no table of positions behind it. While the two sides can still
717
+ // hit each other it weighs the race, blots by how many of the other side's 36 rolls hit them and
718
+ // what being hit would cost, points made, a run of points in front of the other side's back
719
+ // checkers, anchors, its home board against a checker on the bar, and checkers piled up or buried.
720
+ // Once the sides have passed each other it is a pure race, judged by a pip count with the usual
721
+ // corrections for a ragged bear-off and turned into a chance of winning.
722
+ //
723
+ // The sharpest player then looks a move further for its best few turns: the other side's best
724
+ // answer to each of the 21 rolls, averaged. Middling players judge without looking further, and
725
+ // beginners count pips, blots and points and little else. A careless moment (see `careless`) plays
726
+ // any legal turn.
727
+ //
728
+ // The cube goes by the same judgement: in a race the 8-9-12 rule (double from 8% ahead, redouble
729
+ // from 9%, take up to 12% behind), otherwise the chance of winning it makes of the position.
730
+ //
731
+ // What it doesn't know: it has no bear-off database, no opening book and no idea of match equity
732
+ // beyond "the cube is dead when it already wins the match" and "double at once when you're losing
733
+ // the match anyway"; and it never plays on for a gammon by holding the cube back.
734
+ /** The 21 different rolls, and how many of the 36 ways of throwing two dice each is. */
735
+ const ROLLS = [];
736
+ for (let a = 6; a >= 1; a--)
737
+ for (let b = a; b >= 1; b--)
738
+ ROLLS.push({ a, b, ways: a === b ? 1 : 2 });
739
+ const SHOTS = Array.from({ length: 25 }, (_, d) => {
740
+ const out = [];
741
+ ROLLS.forEach(({ a, b }, roll) => {
742
+ if (a === d || b === d)
743
+ out.push({ roll, a: d, b: a === d ? b : a, steps: 1 });
744
+ else if (a !== b && a + b === d)
745
+ out.push({ roll, a, b, steps: 2 });
746
+ else if (a === b && d % a === 0 && d / a >= 2 && d / a <= 4)
747
+ out.push({ roll, a, b, steps: d / a });
748
+ });
749
+ return out;
750
+ });
751
+ /** What a made point is worth, by the point, in pips: the home board and the points just outside it most. */
752
+ const POINT_WORTH = [0, 1, 2.5, 5, 8, 10, 9, 8, 5, 4, 3.5, 3, 2, 2.5, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0];
753
+ /** What an anchor in the other side's home board (or on their bar point) is worth. */
754
+ const ANCHOR_WORTH = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 7, 6, 10, 9, 6, 4, 3, 0];
755
+ /** A run of made points in a row in front of the other side's back checkers, by its length. */
756
+ const PRIME_WORTH = [0, 0, 3, 9, 18, 30, 46];
757
+ /** A home board with this many points made, against each checker the other side has on the bar. */
758
+ const BOARD_WORTH = [0, 1, 3, 7, 13, 22, 40];
759
+ /** What being hit costs on top of the pips lost, by how many points the other side's home board has. */
760
+ const HIT_COST = [5, 6, 7, 9, 13, 19, 27];
761
+ /** How many pips of advantage it takes to turn an even game into about three to one. */
762
+ const SCALE = 26;
763
+ /** Scratch lists, so judging a position makes nothing new. */
764
+ const WORST = new Float64Array(ROLLS.length);
765
+ const THEM = new Int8Array(26);
766
+ const US = new Int8Array(26);
767
+ /**
768
+ * What my blots stand to lose to the other side's next roll, in pips: for each roll, the dearest
769
+ * blot it hits, weighted by how often the roll comes. `op` are the checkers that would do the
770
+ * hitting: they travel up my numbering, from their bar at 0.
771
+ */
772
+ function blotRisk(me, op, cost) {
773
+ let any = false;
774
+ const onBar = op[0];
775
+ for (let p = 1; p <= 24; p++) {
776
+ if (me[p] !== 1)
777
+ continue;
778
+ let first = true;
779
+ for (let j = p - 1; j >= 0; j--) {
780
+ if (!op[j])
781
+ continue;
782
+ // With checkers on the bar they come in first: with two or more there, nothing else moves.
783
+ if (onBar > 1 && j > 0)
784
+ continue;
785
+ if (first) {
786
+ if (!any)
787
+ WORST.fill(0);
788
+ any = true;
789
+ first = false;
790
+ }
791
+ const loss = 25 - p + cost;
792
+ for (const shot of SHOTS[p - j]) {
793
+ if (WORST[shot.roll] >= loss)
794
+ continue;
795
+ let hits;
796
+ if (shot.steps === 1)
797
+ hits = !onBar || j === 0 || me[shot.b] < 2;
798
+ else if (onBar && (j > 0 || onBar > 1))
799
+ hits = false;
800
+ else if (shot.steps === 2)
801
+ hits = me[j + shot.a] < 2 || me[j + shot.b] < 2;
802
+ else {
803
+ hits = true;
804
+ for (let k = 1; k < shot.steps; k++)
805
+ if (me[j + shot.a * k] >= 2)
806
+ hits = false;
807
+ }
808
+ if (hits)
809
+ WORST[shot.roll] = loss;
810
+ }
811
+ }
812
+ }
813
+ if (!any)
814
+ return 0;
815
+ let total = 0;
816
+ for (let r = 0; r < ROLLS.length; r++)
817
+ total += WORST[r] * ROLLS[r].ways;
818
+ return total / 36;
819
+ }
820
+ /** What my points, primes, anchors and piles are worth, in pips, against the other side's checkers `op`. */
821
+ function shape(me, op) {
822
+ let total = 0;
823
+ let home = 0;
824
+ let run = 0;
825
+ let prime = 0;
826
+ // How many of their checkers are still behind each point: on their bar, or on a lower point of mine.
827
+ let behind = op[0];
828
+ let behindRun = 0;
829
+ for (let p = 1; p <= 24; p++) {
830
+ const n = me[p];
831
+ if (n >= 2) {
832
+ total += POINT_WORTH[p] + ANCHOR_WORTH[p];
833
+ if (p <= 6)
834
+ home++;
835
+ if (p <= 12) {
836
+ if (!run)
837
+ behindRun = behind;
838
+ run++;
839
+ // A prime is only worth something with a checker stuck behind it.
840
+ const held = PRIME_WORTH[Math.min(run, 6)] * (behindRun >= 2 ? 1 : behindRun === 1 ? 0.6 : 0.08);
841
+ if (held > prime)
842
+ prime = held;
843
+ }
844
+ // Checkers piled high are doing nothing, and ones deep in the home board early are out of the game.
845
+ if (n > 4)
846
+ total -= (n - 4) * 2;
847
+ if (p <= 3 && n > 2)
848
+ total -= (n - 2) * (4 - p) * 0.6;
849
+ }
850
+ else
851
+ run = 0;
852
+ // A checker still in their home board has a long way to come and can be shut in: an anchor
853
+ // there is worth having, but less than having got away.
854
+ if (p >= 19)
855
+ total -= n * 4;
856
+ behind += op[p];
857
+ }
858
+ return total + prime + BOARD_WORTH[home] * Math.min(op[0], 2);
859
+ }
860
+ /** How many points of their home board a side has made. `side` in its own numbering. */
861
+ const homePoints = (side) => (side[1] >= 2 ? 1 : 0) + (side[2] >= 2 ? 1 : 0) + (side[3] >= 2 ? 1 : 0) + (side[4] >= 2 ? 1 : 0) + (side[5] >= 2 ? 1 : 0) + (side[6] >= 2 ? 1 : 0);
862
+ /**
863
+ * A race count: the pips, plus what a ragged bear-off wastes (checkers heaped on the lowest points
864
+ * and gaps on the high ones, after Tom Keith), and never less than the rolls it takes to bear
865
+ * the checkers off two at a time.
866
+ */
867
+ function raceCount(me) {
868
+ let pips = 0;
869
+ for (let p = 1; p <= 25; p++)
870
+ pips += me[p] * p;
871
+ const waste = Math.max(0, me[1] - 1) * 2 + Math.max(0, me[2] - 1) + Math.max(0, me[3] - 3) + (me[4] ? 0 : 1) + (me[5] ? 0 : 1) + (me[6] ? 0 : 1);
872
+ return Math.max(pips + waste, Math.ceil((15 - me[0]) / 2) * 7);
873
+ }
874
+ /** The chance a standard normal value is below `z`. */
875
+ function normal(z) {
876
+ const t = 1 / (1 + 0.2316419 * Math.abs(z));
877
+ const tail = 0.3989423 * Math.exp((-z * z) / 2) * t * (0.3193815 + t * (-0.3565638 + t * (1.781478 + t * (-1.821256 + t * 1.330274))));
878
+ return z >= 0 ? 1 - tail : tail;
879
+ }
880
+ /** The chance of winning a race for the side about to roll, from the two counts (Kleinman's count). */
881
+ const raceChance = (roller, waiter) => normal((waiter - roller + 4) / Math.sqrt(2 * Math.max(roller + waiter - 4, 1)));
882
+ /** How many pips a side needs to get every checker home and one of them off, if it has none off yet. */
883
+ function toSave(me) {
884
+ let pips = 1;
885
+ for (let p = 7; p <= 25; p++)
886
+ pips += me[p] * (p - 6);
887
+ return pips;
888
+ }
889
+ /**
890
+ * What a position is worth to me with the other side about to roll: about +1 for a certain win and
891
+ * −1 for a certain loss, a little more either way for a likely gammon. Also the chance of winning
892
+ * it stands for, in `chance`.
893
+ */
894
+ let chance = 0.5;
895
+ function judge(me, op, stakes) {
896
+ work++;
897
+ if (me[0] === 15) {
898
+ chance = 1;
899
+ return 1 + (op[25] ? 0 : stakes.gammonMe);
900
+ }
901
+ let back = 25;
902
+ while (!me[back])
903
+ back--;
904
+ let front = 0;
905
+ while (!op[front])
906
+ front++;
907
+ if (back < front) {
908
+ // Past each other: a race, with them to roll.
909
+ const mine = raceCount(me);
910
+ const theirs = raceCount(turned(op, THEM));
911
+ const p = 1 - raceChance(theirs, mine);
912
+ chance = p;
913
+ // The counts themselves still tell turns apart when the race is as good as won or lost.
914
+ let value = 2 * p - 1 + (theirs - mine) * 0.003;
915
+ // A gammon either way: whoever has nothing off needs to get home and bear one off in time.
916
+ if (!op[25] && stakes.gammonMe)
917
+ value += (p * stakes.gammonMe) / (1 + Math.exp(((mine - toSave(turned(op, THEM))) / 8.17 + 1) * 1.6));
918
+ if (!me[0] && stakes.gammonOp)
919
+ value -= ((1 - p) * stakes.gammonOp) / (1 + Math.exp(((theirs - toSave(me)) / 8.17 + 0.5) * 1.6));
920
+ return value;
921
+ }
922
+ let pipsMe = 0;
923
+ let pipsOp = 0;
924
+ for (let p = 1; p <= 25; p++) {
925
+ pipsMe += me[p] * p;
926
+ pipsOp += op[25 - p] * p;
927
+ }
928
+ let x = pipsOp - pipsMe - 4;
929
+ if (stakes.crude) {
930
+ for (let p = 1; p <= 24; p++) {
931
+ if (me[p] >= 2 && p <= 12)
932
+ x += 3;
933
+ else if (me[p] === 1 && p > front)
934
+ x -= 7;
935
+ }
936
+ }
937
+ else {
938
+ const them = turned(op, THEM);
939
+ const us = turned(me, US);
940
+ x += shape(me, op) - shape(them, us) - blotRisk(me, op, HIT_COST[homePoints(them)]);
941
+ // Their blots are only in danger if they leave them there, and they move first.
942
+ x += 0.3 * blotRisk(them, us, HIT_COST[homePoints(me)]);
943
+ }
944
+ const p = 1 / (1 + Math.exp(-x / SCALE));
945
+ chance = p;
946
+ return 2 * p - 1 + x * 0.0004;
947
+ }
948
+ /** The dice a roll puts on the table: four of them for a double. */
949
+ const diceOf = (a, b) => (a === b ? [a, a, a, a] : [a, b]);
950
+ /**
951
+ * What a position is worth to me (they are about to roll) after their best answer to each roll,
952
+ * averaged over the rolls. Null if it would take more looking than `cap` allows.
953
+ */
954
+ function lookAhead(me, op, stakes, cap) {
955
+ const them = turned(op);
956
+ const us = turned(me);
957
+ const theirs = { gammonMe: stakes.gammonOp, gammonOp: stakes.gammonMe, crude: stakes.crude };
958
+ let total = 0;
959
+ for (const { a, b, ways } of ROLLS) {
960
+ if (work > cap)
961
+ return null;
962
+ const replies = wholeTurns(them, us, diceOf(a, b));
963
+ let best = -Infinity;
964
+ if (!replies.length)
965
+ best = judge(them, us, theirs);
966
+ else
967
+ for (const r of replies)
968
+ best = Math.max(best, judge(r.me, r.op, theirs));
969
+ total -= best * ways;
970
+ }
971
+ return total / 36;
972
+ }
973
+ /** How hard a computer player thinks: 2 looks a move further, 1 judges every turn, 0 is a beginner. */
974
+ const tierOf = (style) => ((style.skill ?? 1) >= 0.9 ? 2 : (style.skill ?? 1) >= 0.5 ? 1 : 0);
975
+ /**
976
+ * How much luck is in a player's judgement, by tier: none at the top. It counts for most where a
977
+ * game is in the balance, and for next to nothing where it is as good as over, so nobody dawdles
978
+ * over bearing off a won game.
979
+ */
980
+ const LUCK = [0.08, 0.025, 0];
981
+ /**
982
+ * How many of the best turns the sharpest player looks further into, how close to the best they
983
+ * must be, and the most looking it may do over one turn (see `work`). Measured over a few hundred
984
+ * games a turn took 2 to 3 ms on average and 40 ms at the slowest, which used half this ration.
985
+ */
986
+ const DEEP = 4;
987
+ const DEEP_WITHIN = 0.14;
988
+ const DEEP_WORK = 40_000;
989
+ function stakesFor(s, seat, tier) {
990
+ // A gammon is worth nothing under the Jacoby rule until the cube is turned, or when a single game already wins the match.
991
+ const counts = !(s.rules.jacoby && !s.cube.turned);
992
+ const enough = (o) => s.rules.match > 0 && s.score[o] + s.cube.value >= s.rules.match;
993
+ return { gammonMe: counts && !enough(seat) ? 0.6 : 0, gammonOp: counts && !enough(other(seat)) ? 0.6 : 0, crude: tier === 0 };
994
+ }
995
+ /** A number from a position that is the same every time: where a player's luck comes from. */
996
+ function mix(seed, key) {
997
+ let h = seed >>> 0;
998
+ for (let i = 0; i < key.length; i++)
999
+ h = Math.imul(h ^ key.charCodeAt(i), 0x01000193) >>> 0;
1000
+ return mulberry32(h)();
1001
+ }
1002
+ /** The last turn a computer player worked out, so the steps after the first don't work it out again. */
1003
+ let planned = null;
1004
+ /**
1005
+ * The best whole turn for the seat to move with the dice still to play, at a tier of skill (see
1006
+ * `tierOf`): its hops in order. Null when nothing can be played.
1007
+ */
1008
+ function bestTurn(s, tier) {
1009
+ const seat = s.turn;
1010
+ const { me, op } = sidesOf(s, seat);
1011
+ const dice = diceLeft(s);
1012
+ const stakes = stakesFor(s, seat, tier);
1013
+ const about = `|${tier}|${stakes.gammonMe}${stakes.gammonOp}|${s.rng}`;
1014
+ /** Remember the rest of a turn for the next step: the position its first hop leaves is the key. */
1015
+ const keep = (hops) => {
1016
+ if (hops.length < 2)
1017
+ return hops;
1018
+ const hit = hop(me, op, hops[0].from, hops[0].die);
1019
+ const rest = [...dice];
1020
+ rest.splice(rest.indexOf(hops[0].die), 1);
1021
+ planned = { key: `${keyOf(me, op)}|${rest.join("")}${about}`, hops: hops.slice(1) };
1022
+ unhop(me, op, hops[0].from, hops[0].die, hit);
1023
+ return hops;
1024
+ };
1025
+ if (planned?.key === `${keyOf(me, op)}|${dice.join("")}${about}`)
1026
+ return keep(planned.hops);
1027
+ const turns = wholeTurns(me, op, dice);
1028
+ if (!turns.length)
1029
+ return null;
1030
+ // The same turn gets the same luck however many of its steps have been played.
1031
+ const seed = (s.rng ^ Math.imul(s.rolls + 1, 0x9e3779b1)) >>> 0;
1032
+ const scored = turns.map((turn) => {
1033
+ const value = judge(turn.me, turn.op, stakes);
1034
+ return { turn, value, luck: (mix(seed, keyOf(turn.me, turn.op)) - 0.5) * LUCK[tier] * (0.2 + 3.2 * chance * (1 - chance)) };
1035
+ });
1036
+ scored.sort((x, y) => y.value - x.value);
1037
+ let best = scored[0];
1038
+ if (tier === 2 && scored.length > 1) {
1039
+ const cap = work + DEEP_WORK;
1040
+ let top = -Infinity;
1041
+ for (const o of scored.slice(0, DEEP)) {
1042
+ if (o.value < scored[0].value - DEEP_WITHIN)
1043
+ break;
1044
+ const value = lookAhead(o.turn.me, o.turn.op, stakes, cap);
1045
+ // Out of looking: the best of the turns looked into so far stands.
1046
+ if (value === null)
1047
+ break;
1048
+ if (value > top)
1049
+ [top, best] = [value, o];
1050
+ }
1051
+ }
1052
+ else
1053
+ for (const o of scored)
1054
+ if (o.value + o.luck > best.value + best.luck)
1055
+ best = o;
1056
+ return keep(best.turn.hops);
1057
+ }
1058
+ /**
1059
+ * The seat to roll's chance of winning as a computer player of this tier sees it, and whether the
1060
+ * position is a race with `lead`: how far ahead the roller is, as a share of its own count.
1061
+ */
1062
+ function outlook(s, roller, tier) {
1063
+ const { me, op } = sidesOf(s, roller);
1064
+ // Judged from the other side, who has "just moved": the roller's chance is what is left.
1065
+ judge(turned(op), turned(me), { gammonMe: 0, gammonOp: 0, crude: tier === 0 });
1066
+ const p = 1 - chance;
1067
+ const race = rearmost(me) < 25 - rearmost(turned(op));
1068
+ const mine = raceCount(me);
1069
+ const theirs = raceCount(turned(op));
1070
+ return { chance: p, race, lead: (theirs - mine) / Math.max(mine, 1), count: mine };
1071
+ }
1072
+ /** Whether a computer player on roll offers a double. */
1073
+ function wantsToDouble(s, seat, tier) {
1074
+ if (!canDouble(s, seat))
1075
+ return false;
1076
+ const match = s.rules.match;
1077
+ if (match) {
1078
+ // The cube is dead once a single game wins the match: turning it only helps the other side.
1079
+ if (s.score[seat] + s.cube.value >= match)
1080
+ return false;
1081
+ // A game from losing the match anyway: there is nothing to lose by doubling at once.
1082
+ if (s.score[other(seat)] >= match - 1)
1083
+ return true;
1084
+ }
1085
+ const view = outlook(s, seat, tier);
1086
+ const again = s.cube.owner === seat;
1087
+ // A long race goes by the lead; a short one, and anything with hitting still in it, by the chance.
1088
+ if (view.race && view.count >= 30)
1089
+ return view.lead >= (again ? 0.09 : 0.08);
1090
+ return view.chance >= (again ? 0.72 : 0.69);
1091
+ }
1092
+ /** Whether a computer player offered a double takes it. */
1093
+ function wantsToTake(s, seat, tier) {
1094
+ const match = s.rules.match;
1095
+ // Dropping would lose the match: play on whatever the position.
1096
+ if (match && s.score[s.turn] + s.cube.value >= match)
1097
+ return true;
1098
+ const view = outlook(s, s.turn, tier);
1099
+ if (view.race && view.count >= 30)
1100
+ return view.lead <= 0.12;
1101
+ return view.chance <= 0.76;
1102
+ }
1103
+ function bot(s, seat, style) {
1104
+ if (s.phase === "scored")
1105
+ return s.ready[seat] ? null : { type: "next" };
1106
+ if (s.phase === "opening")
1107
+ return s.opening[seat] === null ? { type: "roll" } : null;
1108
+ if (s.phase === "finished")
1109
+ return null;
1110
+ const tier = tierOf(style);
1111
+ // A turn is several moves, so whether this is a careless moment has to be the same each time it's
1112
+ // asked between two rolls: it is decided by the dice thrown so far, not by the clock.
1113
+ const lapse = careless(style, mulberry32((s.rng ^ Math.imul(s.rolls + 1, 0x85ebca6b) ^ Math.imul(seat + 1, 0xc2b2ae35)) >>> 0));
1114
+ if (s.phase === "offer") {
1115
+ if (seat === s.turn)
1116
+ return null;
1117
+ // Without thinking, a double is taken: nobody likes to give a game up.
1118
+ return lapse || wantsToTake(s, seat, tier) ? { type: "take" } : { type: "drop" };
1119
+ }
1120
+ if (s.turn !== seat)
1121
+ return null;
1122
+ if (s.phase === "roll")
1123
+ return !lapse && wantsToDouble(s, seat, tier) ? { type: "double" } : { type: "roll" };
1124
+ const options = backgammonSteps(s);
1125
+ if (!options.length)
1126
+ return s.steps.length ? { type: "confirm" } : null;
1127
+ if (lapse) {
1128
+ // Whatever comes to hand.
1129
+ const { me, op } = sidesOf(s, seat);
1130
+ const o = options[Math.floor(mix(s.rng ^ s.steps.length, keyOf(me, op)) * options.length)];
1131
+ return { type: "move", from: o.from, to: o.to };
1132
+ }
1133
+ const first = bestTurn(s, tier)[0];
1134
+ const to = first.from - first.die;
1135
+ return { type: "move", from: cellOfPlace(placeOf(seat, first.from)), to: to <= 0 ? HOME_CELL[seat] : cellOfPlace(placeOf(seat, to)) };
1136
+ }
1137
+ /** How much looking the computer players have done so far: for measuring what a move costs. */
1138
+ export const backgammonWork = () => work;
1139
+ // ---- The game -----------------------------------------------------------------------------------
1140
+ function normalize(rules) {
1141
+ const given = (rules && typeof rules === "object" ? rules : {});
1142
+ const base = PRESETS.find((p) => p.id === given.id) ?? friendlyBackgammon;
1143
+ const r = { ...base, ...given };
1144
+ const match = MATCHES.includes(Number(r.match)) ? Number(r.match) : base.match;
1145
+ const cube = !!r.cube;
1146
+ return {
1147
+ id: String(r.id),
1148
+ name: String(r.name),
1149
+ match,
1150
+ cube,
1151
+ // Each of these only means something with the cube, in a match or outside one.
1152
+ crawford: cube && match > 0 && !!r.crawford,
1153
+ jacoby: cube && match === 0 && !!r.jacoby,
1154
+ autoDoubles: cube && match === 0 && !!r.autoDoubles,
1155
+ start: r.start === "nack" ? "nack" : "standard",
1156
+ confirm: !!r.confirm,
1157
+ quick: !!r.quick,
1158
+ };
1159
+ }
1160
+ export const backgammon = {
1161
+ id: "backgammon",
1162
+ name: "Backgammon",
1163
+ emoji: "🎲",
1164
+ blurb: "Race your checkers home, and hit theirs on the way",
1165
+ tagline: "Two dice and fifteen checkers each: a friendly game, a money game with the doubling cube, or a match to 3, 5 or 7 points.",
1166
+ seats: { min: 2, max: 2, default: 2 },
1167
+ teams: false,
1168
+ presets: PRESETS,
1169
+ fields: [
1170
+ { key: "match", label: "Play", kind: "choice", options: MATCHES.map((n) => [n, n ? `A match to ${n} points` : "A single game"]) },
1171
+ { key: "cube", label: "Doubling cube", hint: "Before rolling you can offer to double the stakes; the other player takes it, or drops and loses the game", kind: "toggle" },
1172
+ { key: "crawford", label: "Crawford rule", hint: "In a match with the cube: no doubling in the first game after someone gets to within a point of winning", kind: "toggle" },
1173
+ { key: "jacoby", label: "Jacoby rule", hint: "In a single game with the cube: gammons and backgammons only count once the cube has been turned", kind: "toggle" },
1174
+ { key: "autoDoubles", label: "Automatic doubles", hint: "In a single game with the cube: a tied opening roll doubles the stakes (once)", kind: "toggle" },
1175
+ { key: "start", label: "Starting position", kind: "choice", options: [["standard", "Standard"], ["nack", "Nackgammon (four checkers back)"]] },
1176
+ { key: "confirm", label: "Finish each turn with Done", hint: "Your turn isn't over until you say so, and you can take all of it back until then", kind: "toggle" },
1177
+ { key: "quick", label: "One tap moves", hint: "Tapping a checker that has only one place to go plays it at once", kind: "toggle" },
1178
+ ],
1179
+ normalize,
1180
+ validate: () => [],
1181
+ describe(r) {
1182
+ return [
1183
+ `${r.start === "nack" ? "Nackgammon: each side starts with two checkers on its 24 point, two on its 23, four on the 13, three on the 8 and four on the 6" : "Each side starts with two checkers on its 24 point, five on the 13, three on the 8 and five on the 6"}, and moves them round to its own home board (points 1 to 6) and off. To start, each player rolls one die: the higher goes first and plays those two dice; a tie is rolled again.`,
1184
+ "On your turn, roll and move one checker for each die (a double is played four times). A checker can land on any point the other side doesn't hold with two or more. Landing on a single checker hits it: it goes to the bar and has to come back in on your home board before that player moves anything else.",
1185
+ "You have to play both dice if there is any way to; if only one can be played it has to be the bigger. A roll you can't play at all passes by itself.",
1186
+ "With all fifteen checkers in your home board you bear them off: a die takes off a checker from the point it shows, or, when nothing is on a higher point, from the highest point you have.",
1187
+ `Tap a checker, then where it goes. Until your turn is over you can take your moves back with Undo.${r.confirm ? " Your turn ends when you press Done." : " Your turn ends by itself when your dice are played."}${r.quick ? " A checker with only one place to go moves as soon as you tap it." : ""}`,
1188
+ "The first to bear off every checker wins: a gammon (double) if the loser has borne off none, a backgammon (triple) if the loser also has a checker on the bar or in the winner's home board.",
1189
+ ...(r.cube
1190
+ ? [
1191
+ `The doubling cube: before you roll you may offer to double the stakes. The other player takes (and now owns the cube: only they can double next) or drops and loses the game at the stakes as they stood.${r.jacoby ? " Jacoby rule: gammons and backgammons only count once the cube has been turned." : ""}${r.autoDoubles ? " A tied opening roll doubles the stakes, once." : ""}`,
1192
+ ]
1193
+ : []),
1194
+ ...(r.match ? [`A match to ${r.match} points: each game scores the cube times one, two or three, and the first to ${r.match} wins.${r.crawford ? " Crawford rule: in the first game after a player comes within a point of winning, nobody may double." : ""}`] : []),
1195
+ ];
1196
+ },
1197
+ newGame: newBackgammon,
1198
+ apply,
1199
+ view,
1200
+ bot,
1201
+ // A roll comes quickly, a cube decision takes a moment, and the steps after a turn's first are already thought out.
1202
+ thinkingTime: (s) => (s.phase === "offer" ? 1800 : s.phase === "move" ? (s.steps.length ? 450 : 1000) : s.phase === "roll" ? 700 : 500),
1203
+ summary(s) {
1204
+ const over = s.phase === "finished";
1205
+ const winner = s.result?.winner ?? 0;
1206
+ return {
1207
+ game: "backgammon",
1208
+ rules: s.rules.name,
1209
+ phase: s.phase,
1210
+ over,
1211
+ hand: s.round,
1212
+ scoreLine: `${s.score[0]} to ${s.score[1]}`,
1213
+ // A win is a win, whatever it was won for.
1214
+ order: over ? [winner, other(winner)] : null,
1215
+ winners: over ? [winner] : null,
1216
+ teams: false,
1217
+ seats: s.seats,
1218
+ };
1219
+ },
1220
+ };
1221
+ //# sourceMappingURL=backgammon.js.map