@leafarcade/cli 0.8.0 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -3754,6 +3754,21 @@ function asianLastMove(history) {
3754
3754
  const from = a.findIndex((cell, i) => cell.startsWith(mine) && !b[i].startsWith(mine));
3755
3755
  return { from: from < 0 ? null : from, to };
3756
3756
  }
3757
+ function drawReason(history) {
3758
+ const last = history.at(-1);
3759
+ return history.filter((key2) => key2 === last).length >= 3 ? "Draw \xB7 the same position occurred three times." : "Draw \xB7 too many moves without progress (no capture or man move).";
3760
+ }
3761
+ function snapshotTurn(before, after, owner4, movers) {
3762
+ if (!before || !after || before.length !== after.length) return null;
3763
+ for (const seat of movers) {
3764
+ const to = after.findIndex((p, i) => owner4(p) === seat && owner4(before[i]) !== seat);
3765
+ if (to < 0) continue;
3766
+ const from = before.findIndex((p, i) => owner4(p) === seat && owner4(after[i]) !== seat);
3767
+ const captured = before.flatMap((p, i) => owner4(p) === 1 - seat && owner4(after[i]) !== 1 - seat ? [i] : []);
3768
+ return { by_seat: seat, ...from >= 0 ? { from } : {}, to, ...captured.length ? { captured } : {} };
3769
+ }
3770
+ return null;
3771
+ }
3757
3772
 
3758
3773
  // lib/random.ts
3759
3774
  function randomStep(seed) {
@@ -3792,6 +3807,261 @@ function best(scores2) {
3792
3807
  return winners.length === 1 ? winners[0] : null;
3793
3808
  }
3794
3809
 
3810
+ // lib/multiplayer/dice.ts
3811
+ var diceCategories = [
3812
+ "Ones",
3813
+ "Twos",
3814
+ "Threes",
3815
+ "Fours",
3816
+ "Fives",
3817
+ "Sixes",
3818
+ "Three of a kind",
3819
+ "Four of a kind",
3820
+ "Full house",
3821
+ "Small straight",
3822
+ "Large straight",
3823
+ "Five of a kind",
3824
+ "Chance"
3825
+ ];
3826
+ function createDice(count) {
3827
+ if (!index(count - 2, 3)) throw Error("Choose two to four players.");
3828
+ return {
3829
+ dice: [0, 0, 0, 0, 0],
3830
+ held: Array(5).fill(false),
3831
+ rolls: 0,
3832
+ sheets: Array.from({ length: count }, () => Array(13).fill(null)),
3833
+ bonuses: Array(count).fill(0),
3834
+ turn: 0,
3835
+ finished: false,
3836
+ winner: null
3837
+ };
3838
+ }
3839
+ function diceScore(dice, category, joker = false) {
3840
+ const counts = Array.from(
3841
+ { length: 6 },
3842
+ (_, i) => dice.filter((v) => v === i + 1).length
3843
+ ), sum = dice.reduce((a, b) => a + b, 0);
3844
+ if (category < 6) return counts[category] * (category + 1);
3845
+ switch (category) {
3846
+ case 6:
3847
+ return Math.max(...counts) >= 3 ? sum : 0;
3848
+ case 7:
3849
+ return Math.max(...counts) >= 4 ? sum : 0;
3850
+ case 8:
3851
+ return joker || counts.includes(3) && counts.includes(2) ? 25 : 0;
3852
+ case 9:
3853
+ return joker || [0, 1, 2].some((start) => counts.slice(start, start + 4).every(Boolean)) ? 30 : 0;
3854
+ case 10:
3855
+ return joker || [0, 1].some((start) => counts.slice(start, start + 5).every(Boolean)) ? 40 : 0;
3856
+ case 11:
3857
+ return counts.includes(5) ? 50 : 0;
3858
+ case 12:
3859
+ return sum;
3860
+ default:
3861
+ throw Error("Choose a score category.");
3862
+ }
3863
+ }
3864
+ function diceTotal(s, seat) {
3865
+ const sheet = s.sheets[seat], upper = sheet.slice(0, 6).reduce((sum, n) => sum + (n ?? 0), 0);
3866
+ return sheet.reduce((sum, n) => sum + (n ?? 0), 0) + (upper >= 63 ? 35 : 0) + s.bonuses[seat];
3867
+ }
3868
+ function playDice(s, type, at, entropy2) {
3869
+ if (s.finished) throw Error("This match is finished.");
3870
+ if (type === "roll") {
3871
+ if (s.rolls >= 3) throw Error("Record a score after three rolls.");
3872
+ if (s.rolls && s.held.every(Boolean)) throw Error("Release a die before rolling again, or record a score.");
3873
+ const random = draws(entropy2, 5);
3874
+ return {
3875
+ ...s,
3876
+ rolls: s.rolls + 1,
3877
+ dice: s.dice.map(
3878
+ (v, i) => s.rolls && s.held[i] ? v : 1 + Math.floor(random[i] * 6)
3879
+ )
3880
+ };
3881
+ }
3882
+ if (at === void 0 || !index(at, type === "hold" ? 5 : 13) || !s.rolls)
3883
+ throw Error("Roll first, then choose a die or score category.");
3884
+ if (type === "hold") {
3885
+ if (s.rolls >= 3) throw Error("No rolls are left: record a score.");
3886
+ return { ...s, held: s.held.map((v, i) => i === at ? !v : v) };
3887
+ }
3888
+ if (type !== "score" || s.sheets[s.turn][at] !== null)
3889
+ throw Error("Choose an unused score category.");
3890
+ const sheets = s.sheets.map((row) => [...row]), bonuses = [...s.bonuses];
3891
+ const five = s.dice.every((v) => v === s.dice[0]), upper = s.dice[0] - 1;
3892
+ const joker = five && sheets[s.turn][11] !== null && sheets[s.turn][upper] !== null;
3893
+ if (joker && sheets[s.turn].slice(6).some((n) => n === null) && at < 6)
3894
+ throw Error(
3895
+ "Use the five-of-a-kind joker in an unused lower category first."
3896
+ );
3897
+ sheets[s.turn][at] = diceScore(s.dice, at, joker);
3898
+ if (joker && sheets[s.turn][11] === 50) bonuses[s.turn] += 100;
3899
+ const next3 = {
3900
+ ...s,
3901
+ sheets,
3902
+ bonuses,
3903
+ turn: (s.turn + 1) % s.sheets.length,
3904
+ rolls: 0,
3905
+ held: Array(5).fill(false),
3906
+ dice: [0, 0, 0, 0, 0]
3907
+ };
3908
+ next3.finished = sheets.every((row) => row.every((n) => n !== null));
3909
+ if (next3.finished)
3910
+ next3.winner = best(sheets.map((_, i) => diceTotal(next3, i)));
3911
+ return next3;
3912
+ }
3913
+
3914
+ // lib/multiplayer/ludo.ts
3915
+ function createLudo(count) {
3916
+ if (!index(count - 2, 3)) throw Error("Choose two to four players.");
3917
+ return {
3918
+ pieces: Array.from({ length: count }, () => [-1, -1, -1, -1]),
3919
+ turn: 0,
3920
+ die: 0,
3921
+ streak: 0,
3922
+ attempts: 0,
3923
+ winner: null,
3924
+ last: "Roll a six to leave your yard."
3925
+ };
3926
+ }
3927
+ var ludoTrack = (seat, progress, count = 4) => (seat * (count === 2 ? 20 : 10) + progress) % 40;
3928
+ function ludoMoves(s) {
3929
+ if (!s.die || s.winner !== null) return [];
3930
+ return s.pieces[s.turn].flatMap((p, i) => {
3931
+ const to = p < 0 ? s.die === 6 ? 0 : -1 : p + s.die;
3932
+ return to >= 0 && to <= 43 && !s.pieces[s.turn].some((other, j) => j !== i && other === to) ? [i] : [];
3933
+ });
3934
+ }
3935
+ function advance(s) {
3936
+ return {
3937
+ ...s,
3938
+ turn: (s.turn + 1) % s.pieces.length,
3939
+ die: 0,
3940
+ streak: 0,
3941
+ attempts: 0
3942
+ };
3943
+ }
3944
+ function playLudo(s, type, at, entropy2) {
3945
+ if (s.winner !== null) throw Error("This match is finished.");
3946
+ if (type === "roll") {
3947
+ if (s.die) throw Error("Move a piece before rolling again.");
3948
+ const die = 1 + Math.floor(draws(entropy2, 1)[0] * 6), streak = die === 6 ? s.streak + 1 : 0;
3949
+ let next4 = {
3950
+ ...s,
3951
+ die,
3952
+ streak,
3953
+ attempts: s.attempts + 1,
3954
+ last: `Player ${s.turn + 1} rolled ${die}.`
3955
+ };
3956
+ if (streak > 3)
3957
+ return advance({
3958
+ ...next4,
3959
+ last: "Three extra moves is the limit. Turn passes."
3960
+ });
3961
+ if (!ludoMoves(next4).length) {
3962
+ const allHome = s.pieces[s.turn].every((p) => p === -1);
3963
+ next4 = allHome && next4.attempts < 3 || die === 6 ? { ...next4, die: 0, last: `Player ${s.turn + 1} rolled ${die}: no piece can move${die === 6 ? "; roll again" : allHome ? `; ${3 - next4.attempts} more tr${3 - next4.attempts === 1 ? "y" : "ies"} for a 6` : ""}.` } : advance({ ...next4, last: `Player ${s.turn + 1} rolled ${die}: no piece can move. Turn passes.` });
3964
+ }
3965
+ return next4;
3966
+ }
3967
+ if (type !== "move" || at === void 0 || !index(at, 4) || !ludoMoves(s).includes(at))
3968
+ throw Error(
3969
+ "Choose a highlighted piece. You need an exact roll to reach home."
3970
+ );
3971
+ const pieces = s.pieces.map((row) => [...row]), old = pieces[s.turn][at], to = old < 0 ? 0 : old + s.die;
3972
+ pieces[s.turn][at] = to;
3973
+ const captured = [];
3974
+ if (to < 40) {
3975
+ for (let seat = 0; seat < pieces.length; seat++)
3976
+ if (seat !== s.turn) {
3977
+ pieces[seat] = pieces[seat].map((p, piece2) => {
3978
+ const hit = p >= 0 && p < 40 && ludoTrack(seat, p, pieces.length) === ludoTrack(s.turn, to, pieces.length);
3979
+ if (hit) captured.push(`Player ${seat + 1}'s piece ${piece2 + 1}`);
3980
+ return hit ? -1 : p;
3981
+ });
3982
+ }
3983
+ }
3984
+ const won = pieces[s.turn].every((p) => p >= 40);
3985
+ const next3 = {
3986
+ ...s,
3987
+ pieces,
3988
+ die: 0,
3989
+ attempts: 0,
3990
+ winner: won ? s.turn : null,
3991
+ last: `Player ${s.turn + 1} moved piece ${at + 1} ${old < 0 ? "out of the yard to 0" : `from ${old} to ${to}`} with a ${s.die}${to >= 40 ? " (home lane)" : ""}${captured.length ? `, sending ${captured.join(" and ")} back to the yard` : ""}${!won && s.die === 6 && s.streak < 3 ? "; the 6 gives another roll" : ""}.`
3992
+ };
3993
+ return s.die === 6 && s.streak < 3 || won ? next3 : advance(next3);
3994
+ }
3995
+
3996
+ // lib/multiplayer/football.ts
3997
+ var footballPoint = (at) => [at % 9, Math.floor(at / 9)];
3998
+ var footballEdge = (a, b) => a < b ? `${a}:${b}` : `${b}:${a}`;
3999
+ var valid = (at) => index(at, 117) && (Math.floor(at / 9) > 0 && Math.floor(at / 9) < 12 || at % 9 >= 3 && at % 9 <= 5);
4000
+ var boundary = (at) => {
4001
+ const [x, y] = footballPoint(at);
4002
+ return x === 0 || x === 8 || y === 0 || y === 12 || (y === 1 || y === 11) && x !== 4;
4003
+ };
4004
+ function createFootball() {
4005
+ const edges = [];
4006
+ for (let y = 1; y <= 11; y++)
4007
+ for (const x of [0, 8])
4008
+ if (y < 11) edges.push(footballEdge(y * 9 + x, (y + 1) * 9 + x));
4009
+ for (const y of [1, 11])
4010
+ for (let x = 0; x < 8; x++)
4011
+ if (x < 3 || x >= 5) edges.push(footballEdge(y * 9 + x, y * 9 + x + 1));
4012
+ for (const y of [0, 12]) {
4013
+ edges.push(
4014
+ footballEdge(y * 9 + 3, y * 9 + 4),
4015
+ footballEdge(y * 9 + 4, y * 9 + 5)
4016
+ );
4017
+ for (const x of [3, 5])
4018
+ edges.push(footballEdge(y * 9 + x, (y === 0 ? 1 : 11) * 9 + x));
4019
+ }
4020
+ return { ball: 58, edges, turn: 0, winner: null, last: null };
4021
+ }
4022
+ function footballMoves(s) {
4023
+ if (s.winner !== null) return [];
4024
+ const [x, y] = footballPoint(s.ball), moves = [];
4025
+ for (let dy = -1; dy <= 1; dy++)
4026
+ for (let dx = -1; dx <= 1; dx++) {
4027
+ const nx = x + dx, ny = y + dy, at = ny * 9 + nx;
4028
+ if ((ny === 0 || ny === 12) && (x < 3 || x > 5)) continue;
4029
+ if ((dx || dy) && nx >= 0 && nx <= 8 && ny >= 0 && ny <= 12 && valid(at) && !s.edges.includes(footballEdge(s.ball, at)))
4030
+ moves.push(at);
4031
+ }
4032
+ return moves;
4033
+ }
4034
+ function playFootball(s, at) {
4035
+ if (!footballMoves(s).includes(at))
4036
+ throw Error("Choose an adjacent point along an unused line.");
4037
+ const bounced = boundary(at) || s.edges.some((edge) => edge.split(":").map(Number).includes(at));
4038
+ const y = Math.floor(at / 9);
4039
+ const next3 = {
4040
+ ...s,
4041
+ ball: at,
4042
+ edges: [...s.edges, footballEdge(s.ball, at)],
4043
+ turn: bounced ? s.turn : s.turn === 0 ? 1 : 0,
4044
+ winner: y === 0 ? 0 : y === 12 ? 1 : null,
4045
+ last: [s.ball, at]
4046
+ };
4047
+ if (next3.winner === null && !footballMoves(next3).length)
4048
+ next3.winner = 1 - next3.turn;
4049
+ return next3;
4050
+ }
4051
+ var compass = { "0,-1": "up", "1,-1": "up-right", "1,0": "right", "1,1": "down-right", "0,1": "down", "-1,1": "down-left", "-1,0": "left", "-1,-1": "up-left" };
4052
+ function footballLabeled(s) {
4053
+ const [x, y] = footballPoint(s.ball);
4054
+ return [
4055
+ `ball: point ${s.ball} (row ${y}, column ${x}); points are row*9+column on a 9-column, 13-row grid, rows 0 and 12 are the goals`,
4056
+ "seat 0 scores in the top goal (row 0: points 3, 4, 5); seat 1 in the bottom goal (row 12: points 111, 112, 113)",
4057
+ ...footballMoves(s).map((at) => {
4058
+ const [nx, ny] = footballPoint(at);
4059
+ const bounce = boundary(at) || s.edges.some((edge) => edge.split(":").map(Number).includes(at));
4060
+ return `move to ${at}: ${compass[`${nx - x},${ny - y}`]}${ny === 0 || ny === 12 ? ", goal" : bounce ? ", bounce: you move again" : ""}`;
4061
+ })
4062
+ ];
4063
+ }
4064
+
3795
4065
  // lib/multiplayer/mills.ts
3796
4066
  var millLines = [
3797
4067
  [0, 1, 2],
@@ -4271,13 +4541,22 @@ function createGin(entropy2) {
4271
4541
  }
4272
4542
  function playGin(s, type, at, entropy2) {
4273
4543
  if (type === "next" && s.phase === "round")
4274
- return deal2(
4275
- s.result.startsWith("Stock") ? s.dealer : 1 - s.dealer,
4276
- s.scores,
4277
- s.wins,
4278
- s.round + 1,
4279
- entropy2
4280
- );
4544
+ return {
4545
+ ...deal2(
4546
+ s.result.startsWith("Stock") ? s.dealer : 1 - s.dealer,
4547
+ s.scores,
4548
+ s.wins,
4549
+ s.round + 1,
4550
+ entropy2
4551
+ ),
4552
+ previous: [
4553
+ s.result,
4554
+ ...s.hands.map((h, i) => {
4555
+ const layout = ginLayout(h);
4556
+ return `Player ${i + 1}: melds ${layout.melds.map((m) => m.map(cardLabel).join(" ")).join(" \xB7 ") || "none"}; deadwood ${layout.deadwood.map(cardLabel).join(" ") || "none"} (${layout.value}).`;
4557
+ })
4558
+ ].join(" ")
4559
+ };
4281
4560
  if (["finished", "round"].includes(s.phase))
4282
4561
  throw Error("This round is finished.");
4283
4562
  const t = structuredClone(s), seat = s.turn;
@@ -4396,7 +4675,7 @@ var multiplayerGames = [
4396
4675
  symbol: "1000",
4397
4676
  players: "2\u20134 players",
4398
4677
  description: "Bid your hand. Marry a new trump.",
4399
- rules: "A 24-card deck, nine through ace; strength is nine, jack, queen, king, ten, ace. Three active players receive seven cards and a three-card kitty. With four seats the dealer sits out and scores revealed kitty aces (50 each) and marriages. Two players receive ten cards each and choose one of two two-card kitties. The first active player opens at 100; bid in tens, no higher than 120 plus held marriages. Winner takes a kitty, gives each other active player a card (or discards two with two seats), then declares at least the auction bid. Follow suit and beat the winner when possible; if void, trump when held. Leading a held queen-king pair with the marriage declaration earns clubs 60, diamonds 80, hearts 100 or spades 40 and changes trump. Card points: ace 11, ten 10, king 4, queen 3, jack 2. Remaining kitty cards score for the last trick winner. Declarer makes or loses their contract; others round their points to tens. At 800, only declarer earns further points. First to 1000 wins, with declarer winning a simultaneous finish. The first bomb cancels freely; subsequent bombs award opponents 60 (40 with four seats). Four nines can request a redeal."
4678
+ rules: "A 24-card deck, nine through ace; strength from lowest to highest is nine, jack, queen, king, ten, ace. Three active players receive seven cards and a three-card kitty. With four seats the dealer sits out and scores revealed kitty aces (50 each) and marriages. Two players receive ten cards each and choose one of two two-card kitties. The first active player opens at 100; bid in tens, no higher than 120 plus held marriages. Winner takes a kitty, gives each other active player a card (or discards two with two seats), then declares at least the auction bid. Follow suit and beat the winner when possible; if void, trump when held. Leading a held queen-king pair with the marriage declaration earns clubs 60, diamonds 80, hearts 100 or spades 40 and changes trump. Card points: ace 11, ten 10, king 4, queen 3, jack 2. Cards left from the kitties, including the declarer\u2019s two discards in a two-player game, score for the last trick winner. Declarer makes or loses their contract; others round their points to tens. At 800, only declarer earns further points. First to 1000 wins, with declarer winning a simultaneous finish. The first bomb cancels freely; subsequent bombs award opponents 60 (40 with four seats). Four nines can request a redeal."
4400
4679
  },
4401
4680
  {
4402
4681
  id: "binokel",
@@ -4516,7 +4795,7 @@ var multiplayerGames = [
4516
4795
  symbol: "\u2654",
4517
4796
  players: "2 players",
4518
4797
  description: "Claim sixty-six. Close the stock with care.",
4519
- rules: "Six cards each from a 24-card deck. Rank order: nine, jack, queen, king, ten, ace; values 0, 2, 3, 4, 10, 11. While stock is open, play freely; trick winner draws first. Exchange trump nine after winning a trick. Before leading, you may close the stock; then follow suit, overtake if possible, otherwise trump. A queen\u2013king marriage declared while leading with open stock earns 20, or 40 in trump, once you have won a trick (including the one it leads). Claim 66 before leading, or immediately with a marriage. False claims or failed closure award the opponent 2/3 match points. Otherwise round wins earn 1\u20133 based on opponent points. An unclosed last trick adds ten. First to seven match points wins."
4798
+ rules: "Six cards each from a 24-card deck. Rank order from lowest to highest: nine, jack, queen, king, ten, ace; values 0, 2, 3, 4, 10, 11. While stock is open, play freely; trick winner draws first, and the trump upcard is the last card drawn. Exchange trump nine after winning a trick. Before leading, you may close the stock; then follow suit, overtake if possible, otherwise trump. A queen\u2013king marriage declared while leading with open stock earns 20, or 40 in trump, once you have won a trick (including the one it leads). Claim 66 before leading, or immediately with a marriage. False claims or failed closure award the opponent 2/3 match points. Otherwise round wins earn 1\u20133 based on opponent points. An unclosed last trick adds ten. First to seven match points wins."
4520
4799
  },
4521
4800
  {
4522
4801
  id: "spades",
@@ -4628,7 +4907,7 @@ var multiplayerGames = [
4628
4907
  symbol: "\u25CC",
4629
4908
  players: "2 players",
4630
4909
  description: "Sow seeds. Capture twos and threes.",
4631
- rules: "Each of twelve pits starts with four seeds. Sow counterclockwise, skipping the starting pit on a full lap. Ending on an opposing pit with two or three seeds captures it and preceding opposing pits with two or three. Captures that empty the entire opposing row are cancelled. Feed an empty opponent if possible. More than 24 captured seeds wins. No legal feeding move or three repetitions ends play; remaining seeds go to their owners."
4910
+ rules: "Each of twelve pits starts with four seeds. Sow counterclockwise, skipping the starting pit on a full lap. Ending on an opposing pit with two or three seeds captures it and preceding opposing pits with two or three. Captures that would empty the entire opposing row (a grand slam) are cancelled. Feed an empty opponent if possible. More than 24 captured seeds wins. No legal feeding move or three repetitions ends play; remaining seeds go to their owners."
4632
4911
  },
4633
4912
  {
4634
4913
  id: "mills",
@@ -4991,107 +5270,6 @@ var action_catalog_default = {
4991
5270
  ]
4992
5271
  };
4993
5272
 
4994
- // lib/multiplayer/dice.ts
4995
- var diceCategories = [
4996
- "Ones",
4997
- "Twos",
4998
- "Threes",
4999
- "Fours",
5000
- "Fives",
5001
- "Sixes",
5002
- "Three of a kind",
5003
- "Four of a kind",
5004
- "Full house",
5005
- "Small straight",
5006
- "Large straight",
5007
- "Five of a kind",
5008
- "Chance"
5009
- ];
5010
- function createDice(count) {
5011
- if (!index(count - 2, 3)) throw Error("Choose two to four players.");
5012
- return {
5013
- dice: [0, 0, 0, 0, 0],
5014
- held: Array(5).fill(false),
5015
- rolls: 0,
5016
- sheets: Array.from({ length: count }, () => Array(13).fill(null)),
5017
- bonuses: Array(count).fill(0),
5018
- turn: 0,
5019
- finished: false,
5020
- winner: null
5021
- };
5022
- }
5023
- function diceScore(dice, category, joker = false) {
5024
- const counts = Array.from(
5025
- { length: 6 },
5026
- (_, i) => dice.filter((v) => v === i + 1).length
5027
- ), sum = dice.reduce((a, b) => a + b, 0);
5028
- if (category < 6) return counts[category] * (category + 1);
5029
- switch (category) {
5030
- case 6:
5031
- return Math.max(...counts) >= 3 ? sum : 0;
5032
- case 7:
5033
- return Math.max(...counts) >= 4 ? sum : 0;
5034
- case 8:
5035
- return joker || counts.includes(3) && counts.includes(2) ? 25 : 0;
5036
- case 9:
5037
- return joker || [0, 1, 2].some((start) => counts.slice(start, start + 4).every(Boolean)) ? 30 : 0;
5038
- case 10:
5039
- return joker || [0, 1].some((start) => counts.slice(start, start + 5).every(Boolean)) ? 40 : 0;
5040
- case 11:
5041
- return counts.includes(5) ? 50 : 0;
5042
- case 12:
5043
- return sum;
5044
- default:
5045
- throw Error("Choose a score category.");
5046
- }
5047
- }
5048
- function diceTotal(s, seat) {
5049
- const sheet = s.sheets[seat], upper = sheet.slice(0, 6).reduce((sum, n) => sum + (n ?? 0), 0);
5050
- return sheet.reduce((sum, n) => sum + (n ?? 0), 0) + (upper >= 63 ? 35 : 0) + s.bonuses[seat];
5051
- }
5052
- function playDice(s, type, at, entropy2) {
5053
- if (s.finished) throw Error("This match is finished.");
5054
- if (type === "roll") {
5055
- if (s.rolls >= 3) throw Error("Record a score after three rolls.");
5056
- const random = draws(entropy2, 5);
5057
- return {
5058
- ...s,
5059
- rolls: s.rolls + 1,
5060
- dice: s.dice.map(
5061
- (v, i) => s.rolls && s.held[i] ? v : 1 + Math.floor(random[i] * 6)
5062
- )
5063
- };
5064
- }
5065
- if (at === void 0 || !index(at, type === "hold" ? 5 : 13) || !s.rolls)
5066
- throw Error("Roll first, then choose a die or score category.");
5067
- if (type === "hold")
5068
- return { ...s, held: s.held.map((v, i) => i === at ? !v : v) };
5069
- if (type !== "score" || s.sheets[s.turn][at] !== null)
5070
- throw Error("Choose an unused score category.");
5071
- const sheets = s.sheets.map((row) => [...row]), bonuses = [...s.bonuses];
5072
- const five = s.dice.every((v) => v === s.dice[0]), upper = s.dice[0] - 1;
5073
- const joker = five && sheets[s.turn][11] !== null && sheets[s.turn][upper] !== null;
5074
- if (joker && sheets[s.turn].slice(6).some((n) => n === null) && at < 6)
5075
- throw Error(
5076
- "Use the five-of-a-kind joker in an unused lower category first."
5077
- );
5078
- sheets[s.turn][at] = diceScore(s.dice, at, joker);
5079
- if (joker && sheets[s.turn][11] === 50) bonuses[s.turn] += 100;
5080
- const next3 = {
5081
- ...s,
5082
- sheets,
5083
- bonuses,
5084
- turn: (s.turn + 1) % s.sheets.length,
5085
- rolls: 0,
5086
- held: Array(5).fill(false),
5087
- dice: [0, 0, 0, 0, 0]
5088
- };
5089
- next3.finished = sheets.every((row) => row.every((n) => n !== null));
5090
- if (next3.finished)
5091
- next3.winner = best(sheets.map((_, i) => diceTotal(next3, i)));
5092
- return next3;
5093
- }
5094
-
5095
5273
  // lib/agents/action-help.ts
5096
5274
  var help = {
5097
5275
  amazons: "move: numeric from/to 0\u201399 and amount for the arrow square. Select an exact legal_wire_actions entry.",
@@ -5100,49 +5278,49 @@ var help = {
5100
5278
  euchre: "Use visible_state.legalActions. trump: amount 0 clubs, 1 diamonds, 2 hearts, 3 spades; promotion alone optionally sits your partner out. pass declines trump. discard/play: at is a held card ID, not a hand index. IDs are suit*6+rank, ranks 0\u20135 mean 9,10,J,Q,K,A. The left bower belongs to trump for following suit. next deals after a hand. Partners are opposite seats; first team to 10 wins.",
5101
5279
  cribbage: "Use visible_state.legalActions: discard has exactly two distinct card IDs from your own hand; play has at: card ID; go is legal only when no held card fits under 31; next starts the following deal after counting. Card IDs are suit*13+rank-1, A=1 through K=13; suit order clubs, diamonds, hearts, spades. Crib and opponents' unplayed cards remain hidden.",
5102
5280
  fanorona: "Use visible_state.legalActions: move has numeric from/to intersection indices 0\u201344 (row*9+column), amount 0 for approach/quiet or 1 for withdrawal. stop ends an optional capture continuation. Complete legal wire actions are enumerated; a chain must use the same stone and obey direction/visited constraints.",
5103
- categories: `begin: host starts a timed round; promotion:'untimed' disables the timer. submit/stop: text is a string holding a JSON array of six English answers in category order, for example text set to the string '["Apple","Ant",...]' (not a raw array); stop needs a complete sheet. expire reveals a timed-out round. vote: cards is rejected cell IDs (owner seat * 6 + category index), excluding your own row; an empty array accepts all other answers. next: host advances. Answers and ballots lock once submitted. Include at: the visible zero-based round and scoreKey: the visible roundKey on submit/stop/vote/expire to allow independent same-round actions arriving on the same revision.`,
5281
+ categories: `begin: host starts a timed round; promotion:'untimed' disables the timer. submit/stop: text is a string holding a JSON array of six English answers in category order, for example text set to the string '["Apple","Ant",...]' (not a raw array); stop needs a complete sheet. expire reveals a timed-out round. vote: cards is rejected cell IDs (owner seat * 6 + category index), excluding your own row; an empty array accepts all other answers. next: host advances. Answers and ballots lock once submitted. The CLI fills at (the visible zero-based round) and scoreKey (the visible roundKey) on submit/stop/vote/expire when you leave them out, and accepts text as a plain array: {"type":"submit","text":["Spain","Seville","Seal","Soup","Singer","Spoon"]}.`,
5104
5282
  cheat: "play: cards is one to four card IDs from your hand, claiming the displayed rank. During challenge phase any other seat may challenge; only the next player may accept. Final-card claims must resolve before a win.",
5105
5283
  president: "play: cards is one to four equal-rank card IDs. Match pile size and beat its rank; 2 is high. Opening group includes 3 of clubs. pass sits out until the pile clears. Finished seats leave play; order gives final places.",
5106
5284
  rummy: "stock/upcard: draw first. meld: cards is a set or run of held card IDs. layoff: at is a public meld index, cards is held card IDs to add. discard: at is a held card ID. Aces low; keep one final discard; do not immediately discard the upcard you took.",
5107
- "connect-four": "place: at is a zero-based column (0\u20136). Convenience: --column 1\u20137.",
5285
+ "connect-four": 'legal_actions list {"type":"drop","column":1\u20137}, one-based; pass one unchanged or use --column. The CLI converts it to the wire action place with zero-based at = column-1.',
5108
5286
  "tic-tac-toe": "place: at is a zero-based row-major square (0\u20138). legal_actions use one-based row/column instead; pass either form.",
5109
5287
  chess: "move: from/to are algebraic squares; promotion is q/r/b/n.",
5110
5288
  "atomic-chess": "move: from/to are algebraic squares; promotion is q/r/b/n. Explosions follow the game rules.",
5111
5289
  "three-check": "move: from/to are algebraic squares; promotion is q/r/b/n. Check counts are in visible_state.",
5112
- checkers: "move: from/to are zero-based row-major squares (0\u201363); continue a mandatory capture chain. Men move and capture forward only; kings move both ways. Convenience flags use algebraic squares.",
5290
+ checkers: 'legal_actions use algebraic squares (from "c3" to "d4"); pass one unchanged or use --from/--to. The wire move uses zero-based row-major squares (0\u201363). Continue a mandatory capture chain. Men move and capture forward only; kings move both ways. Convenience flags use algebraic squares.',
5113
5291
  "brazilian-checkers": "move: from/to are zero-based squares (0\u201363); maximum captures apply.",
5114
5292
  "international-draughts": "move: from/to are zero-based squares (0\u201399). sequence: cards is a complete legal path of square indexes, including the origin.",
5115
- reversi: "place: at is a zero-based row-major square (0\u201363). Unavailable turns pass automatically.",
5116
- gomoku: "place: at is a zero-based row-major intersection (0\u2013224). Exactly five wins.",
5117
- hex: "place: at is a zero-based row-major hexagon. swap: no payload, only the second seat's first turn. Convenience: --row/--column or --swap.",
5293
+ reversi: "legal_actions use one-based row/column (1\u20138); pass one unchanged or use --row/--column. The wire place uses zero-based row-major at (0\u201363). Unavailable turns pass automatically.",
5294
+ gomoku: "legal_actions use one-based row/column (1\u201315); pass one unchanged or use --row/--column. The wire place uses zero-based row-major at (0\u2013224). Exactly five wins.",
5295
+ hex: "legal_actions use one-based row/column; pass one unchanged or use --row/--column. The wire place uses a zero-based row-major hexagon. swap: no payload, only the second seat's first turn. Convenience: --row/--column or --swap.",
5118
5296
  "dots-boxes": 'Send a legal_actions entry ({"type":"edge","edge":N}, one-based) unchanged or use --edge N; the CLI converts it to the wire action place with zero-based at = N-1 (visible_state.edges[N-1]). board_labeled lists each box with its edge numbers. Horizontal edges precede vertical edges in row-major order. Edges and box owners are in visible_state. Convenience: --edge is one-based.',
5119
- oware: "sow: at is a zero-based pit index, 0\u20135 for seat 0 and 6\u201311 for seat 1. Convenience: --pit is one-based.",
5297
+ oware: 'legal_actions list {"type":"sow","pit":N}, one-based: seat 0 owns pits 1\u20136, seat 1 pits 7\u201312; pass one unchanged or use --pit. The wire sow uses zero-based at = pit-1 (0\u20135 for seat 0, 6\u201311 for seat 1).',
5120
5298
  go: "place/dead: at is a zero-based row-major intersection (board_labeled shows row start indexes). pass/resume: no payload. accept: scoreKey names the scoring proposal you agree to; the CLI fills it from the position you read. Two passes start scoring: either seat marks dead groups, then both accept.",
5121
5299
  battleships: "auto/clear/ready: no payload. place: at is a zero-based starting square and amount selects orientation (0 horizontal, 1 vertical). fire: at is a zero-based target square. Inspect your fleet/setup phase; enemy ships remain hidden.",
5122
5300
  mills: "place/capture: at is a zero-based point index. move: from is the origin and at the destination. Completing a mill requires a subsequent capture action.",
5123
5301
  dice: `roll: no payload. hold: at is a die index (0\u20134) to toggle its hold after rolling. score: at is a category index: ${diceCategories.map((name, i) => `${i} ${name}`).join(", ")}. Inspect the roll phase and score sheet.`,
5124
5302
  ludo: "roll: no payload. move: at is the index (0\u20133) of your token. Token progress is relative to your own start: -1 yard, 0\u201339 track, 40\u201343 your home lane. Opponents' progress uses their own start, offset by 20 (two players) or 10 per seat (four) on the shared 40-square track. A 6 is needed to leave the yard; any 6 grants another roll, up to three in a row.",
5125
5303
  domino: "tile: at is your zero-based hand position (not a pip value) and amount selects the chain end: 0 left, 1 right. draw: draw until you can play, or pass when the stock is empty. next: start the following round. Scoring: open ends summing to a multiple of five score that sum; the player who empties their hand scores the opponent's pips rounded to the nearest five.",
5126
- football: "place: at is a zero-based destination point. The state records ball, used edges and continuing rebounds.",
5304
+ football: "place: at is the destination point, row*9+column on a 9-column, 13-row grid (0\u2013116; rows 0 and 12 hold only the goal points 3\u20135 and 111\u2013113). board_labeled names each legal step with its direction and whether it bounces (another move). Seat 0 attacks the top goal, seat 1 the bottom.",
5127
5305
  backgammon: "roll: no payload. move: numeric from/to and amount (die value), one die per action. Points are 0\u201323. Positive point counts are seat 0, negative seat 1. Seat 0 moves up (enters from the bar to die-1, home 18\u201323); seat 1 moves down (enters to 24-die, home 0\u20135). from -1 means the bar; to 24 bears off for both seats. Compulsory dice usage is enforced.",
5128
5306
  shogi: "move: from is a zero-based square or a hand-piece type for a drop; to is a zero-based square; promotion:'yes' promotes. Inspect your hands and piece owners.",
5129
5307
  xiangqi: "move: numeric zero-based row-major from/to on the 9\xD710 board. promotion is unused.",
5130
5308
  poker: "fold/check/call/next: no payload. raise: amount is the desired bet total. Follow phase, pot, current bet, your cards and stack. next is a between-hand host action.",
5131
- durak: "play: at is your card ID from the visible hand; take/pass: no payload. Follow attack/defence roles and visible trump/table.",
5309
+ durak: "play: at is your card ID from the visible hand (attack or add a card of a rank already on the table; defend by beating the open attack card). take: the defender picks up the table after the attackers finish adding. pass: an attacker stops adding cards; when every attacker has passed, beaten cards are discarded (or the defender takes them after take). Follow attack/defence roles and visible trump/table.",
5132
5310
  makao: "play: cards is 1\u20134 card IDs from your visible hand, all of the same rank. amount chooses an ace suit (0 clubs, 1 diamonds, 2 hearts, 3 spades) or a jack demand (rank 5\u201310 or 13 for king; 0 for no demand); visible_state.suit and demand use the same numbers; promotion:'makao' announces the last card. draw/pass/catch act according to phase. Inspect drawn-card and demand state.",
5133
5311
  pan: "play: cards is an array of card IDs from the visible hand; take/next: no payload. Follow the table rank and legal group rules.",
5134
5312
  "gin-rummy": "stock/upcard/pass/next: no payload. discard/knock: at is a card ID from the visible hand. Inspect draw/discard phase and deadwood rules.",
5135
5313
  "sixty-six": "play: at is a card ID from the visible hand; promotion:'marriage' or 'marriage-claim' declares. exchange/close/claim/next are phase-specific commands.",
5136
5314
  hearts: "select: at is a card ID from the visible hand for passing (selecting a chosen card again unselects it; visible_state.selected shows your choice), then pass. play: at is a card ID from the visible hand. moon: amount selects a scoring choice. Follow phase, lead suit and legal cards.",
5137
- spades: "bid: amount is the bid; blind/look for blind nil where offered. play: at is a card ID from the visible hand. Follow partner, phase and lead suit.",
5315
+ spades: "bid: amount is the bid; blind/look for blind nil where offered. play: at is a card ID from the visible hand. Partners are opposite: seats 0 and 2 against seats 1 and 3 (the rules text calls them Players 1 & 3 and 2 & 4). Follow partner, phase and lead suit.",
5138
5316
  "oh-hell": "bid: amount is the bid. play: at is a card ID from the visible hand. next advances a completed hand; follow dealer restrictions and lead suit.",
5139
- "three-five-eight": "exchange: at is one card ID from your visible hand. discard: cards is exactly four distinct card IDs from your visible hand. play: at is one card ID from your visible hand. contract: amount 0\u20133 chooses a suit, 4 no trumps, 5 misere. next advances the hand.",
5317
+ "three-five-eight": "exchange: at is one card ID from your visible hand. discard: cards is exactly four distinct card IDs from your visible hand; the listed sets are samples, any four of your cards are legal. play: at is one card ID from your visible hand. contract: amount 0 clubs, 1 diamonds, 2 hearts, 3 spades as trump, 4 no trumps, 5 no tricks (misere); each once per player. Cards rank 2 (low) to ace (high). next advances the hand.",
5140
5318
  bridge: "bid: amount 1\u201335 encodes level floor((amount-1)/5)+1 and strain (amount-1)%5; pass/double/redouble have no payload. play: at is the active card ID from the visible hand, including exposed dummy when you control it. Follow auction, strain and lead-suit rules.",
5141
5319
  "polish-hearts": "trump: amount is a suit index; pass: no payload, only when no layout card fits; play: at is a card ID from the visible hand. Follow the current penalty/trump/layout phase.",
5142
5320
  thousand: "bid/declare: amount is points. kitty: at selects a kitty. exchange: cards is card IDs from the visible hand. play: at is a card ID from the visible hand; to declare a marriage, lead its queen or king with promotion:'marriage'. pass/bomb/redeal/next depend on phase. Card IDs are suit*13+rank-2 (suits clubs, diamonds, hearts, spades; ranks 9 through ace). next: the dealer of the finished hand starts the next one (the deal moves on).",
5143
- binokel: "bid: amount is points; trump: amount is a suit; play: at is a card ID from the visible hand. pass/next have no payload. Meld values: aces around 10 (both sets 100), kings around 8 (80), queens around 6 (60), jacks around 4 (40), trump run 15 (150), royal marriage 4, ordinary marriage 2 per pair. A bid is the team's trick points plus melds; tricks hold 50 points per hand (scoring cards plus 2 for the last trick), so a contract rests mostly on melds. The declaring team needs 20 meld points.",
5321
+ binokel: "bid: amount is points; trump: amount is a suit; play: at is a card ID from the visible hand. pass/next have no payload. Meld values (one set, two sets): aces around 10, 100; kings around 8, 80; queens around 6, 60; jacks around 4, 40; trump run 15, 150; royal marriage 4, ordinary marriage 2 per pair. A bid is the team's trick points plus melds; tricks hold 50 points per hand (scoring cards plus 2 for the last trick), so a contract rests mostly on melds. The declaring team needs 20 meld points.",
5144
5322
  skat: "bid: amount is an auction value; hold/pass/hand/pickup act in auction/pickup phases (after a hold the bidder must bid higher or pass). discard: cards is exactly two card IDs from your visible hand. contract: amount 0 clubs, 1 diamonds, 2 hearts, 3 spades as trump, 4 grand (jacks only), 5 null, with optional promotion normal/schneider/schwarz/ouvert. play: at is a card ID from the visible hand. Jacks rank clubs, spades, hearts, diamonds. Card IDs are suit*13+rank-2 (suits clubs, diamonds, hearts, spades). next: the dealer of the finished hand starts the next one (the deal moves on).",
5145
- baska: "announce: no payload in the honor phase. contract: amount is one of 1,3,4,5,6 and must exceed the current contract. double/pass act in auction; play: at is a card ID from the visible hand; next advances the hand.",
5323
+ baska: "announce: no payload in the honor phase. contract: amount is one of 1 Wedding, 3 Grand, 4 Zolo, 5 Grand Du, 6 Zolo Du and must exceed the current contract (the other numbers name contracts not bid this way: 0 Ordinary, 2 Zolo Bieda, 7 Master, 8 Baska). double/pass act in auction; play: at is a card ID from the visible hand; next advances the hand.",
5146
5324
  canasta: "stock/next: no payload. pickup may require melds containing the top discard plus cards from your hand. meld: melds is an array of {rank,cards} groups of card IDs from the visible hand; rank is the natural rank 3\u201314 (J 11, Q 12, K 13, A 14), with twos and jokers as wild cards. cards_labeled names each card. discard: at is a card ID from the visible hand. Follow opening meld and canasta requirements. Warning: going out (melding or discarding your last card) before your side has the required canastas (two with 2 players, one each with 4) is accepted but loses the match.",
5147
5325
  "letter-tiles": "place: placements is an array of {at,rack,letter?}; at is a board index and rack a rack index, letter assigns a blank. exchange: cards is rack indexes. accept/challenge/pass/next depend on phase.",
5148
5326
  "draw-guess": "choose: at selects one of your offered prompts. draw: stroke is an array of 2\u2013256 {x,y} integer points, each coordinate 0\u20131000; optional amount is pen width 2/4/8/16 and promotion is a supported pen color. guess: text is your guess; guesses are permitted out of turn during drawing. clear/expire/next depend on phase. Hidden prompts stay hidden from guessers.",
@@ -5226,20 +5404,23 @@ function canastaCount(melds) {
5226
5404
  return melds.filter((m) => m.cards.length >= 7).length;
5227
5405
  }
5228
5406
  function finish2(t, out2, concealed = false) {
5407
+ const parts = [];
5229
5408
  for (let i = 0; i < 2; i++) {
5230
- const meld2 = t.melds[i], red = t.red[i].length, bonus = meld2.reduce(
5409
+ const meld2 = t.melds[i], red = t.red[i].length, canastas = meld2.reduce(
5231
5410
  (n, m) => n + (m.cards.length >= 7 ? m.cards.some(canastaWild) ? 300 : 500 : 0),
5232
5411
  0
5233
- ) + (red * 100 + (red === 4 ? 400 : 0)) * (meld2.length ? 1 : -1), cards = meld2.flatMap((m) => m.cards).reduce((n, c) => n + canastaValue(c), 0), held = t.hands.reduce(
5412
+ ), threes = (red * 100 + (red === 4 ? 400 : 0)) * (meld2.length ? 1 : -1), bonus = canastas + threes, cards = meld2.flatMap((m) => m.cards).reduce((n, c) => n + canastaValue(c), 0), held = t.hands.reduce(
5234
5413
  (n, h, seat) => n + (team(t, seat) === i ? h.reduce((n2, c) => n2 + canastaValue(c), 0) : 0),
5235
5414
  0
5236
5415
  );
5237
- t.scores[i] += bonus + cards - held + (out2 !== null && team(t, out2) === i ? concealed ? 200 : 100 : 0);
5416
+ const going = out2 !== null && team(t, out2) === i ? concealed ? 200 : 100 : 0;
5417
+ t.scores[i] += bonus + cards - held + going;
5418
+ parts.push(`Team ${i + 1}: melded cards ${cards}, canastas ${canastas}, red threes ${threes}${red && !meld2.length ? " (no meld, so negative)" : ""}, going out ${going}, cards left in hand -${held}`);
5238
5419
  }
5239
5420
  t.phase = t.scores.some((n) => n >= 5e3) ? "finished" : "round";
5240
5421
  t.winner = t.phase === "finished" ? best(t.scores) : null;
5241
5422
  t.turn = t.dealer;
5242
- t.result = `Hand complete. Scores ${t.scores.join(" / ")}.`;
5423
+ t.result = `Hand complete${out2 === null ? " (stock exhausted)" : ""}. ${parts.join("; ")}. Scores ${t.scores.join(" / ")}.`;
5243
5424
  }
5244
5425
  function out(t, seat) {
5245
5426
  if (t.hands[seat].length) return;
@@ -5726,7 +5907,7 @@ function award(s) {
5726
5907
  }
5727
5908
  return s;
5728
5909
  }
5729
- function advance(s) {
5910
+ function advance2(s) {
5730
5911
  const live = s.players.filter((p) => !p.folded && !p.out);
5731
5912
  if (live.length <= 1) return award(s);
5732
5913
  if (s.pending.length) {
@@ -5752,7 +5933,7 @@ function advance(s) {
5752
5933
  s.pending = s.players.flatMap((p, i) => canAct(p) ? [i] : []);
5753
5934
  if (s.pending.length <= 1) {
5754
5935
  s.pending = [];
5755
- return advance(s);
5936
+ return advance2(s);
5756
5937
  }
5757
5938
  s.turn = nextSeat(s, s.button, canAct);
5758
5939
  return s;
@@ -5801,7 +5982,7 @@ function begin(s, seed) {
5801
5982
  s.turn = nextSeat(s, big, canAct);
5802
5983
  if (!s.pending.length || s.pending.length === 1 && s.players[s.pending[0]].street >= Math.max(...s.players.map((p) => p.street))) {
5803
5984
  s.pending = [];
5804
- return advance(s);
5985
+ return advance2(s);
5805
5986
  }
5806
5987
  return s;
5807
5988
  }
@@ -5896,7 +6077,7 @@ function pokerAction(previous, seat, action, seed) {
5896
6077
  s.actedAt ??= s.players.map((player) => player.street);
5897
6078
  s.actedAt[seat] = s.bet;
5898
6079
  s.log = [...s.log, `Player ${seat + 1} ${label}`].slice(-80);
5899
- return advance(s);
6080
+ return advance2(s);
5900
6081
  }
5901
6082
  function leavePoker(previous, seat) {
5902
6083
  const s = structuredClone(previous);
@@ -5914,7 +6095,7 @@ function leavePoker(previous, seat) {
5914
6095
  return s;
5915
6096
  }
5916
6097
  if (s.turn === seat || s.players.filter((p) => !p.folded && !p.out).length <= 1)
5917
- return advance(s);
6098
+ return advance2(s);
5918
6099
  return s;
5919
6100
  }
5920
6101
  function pokerView(s, seat) {
@@ -7703,7 +7884,8 @@ function moveVariant(state, from, to, promotion = "q") {
7703
7884
  if (!parseUci(from + to)) throw Error("Choose a legal move.");
7704
7885
  const move2 = variantMoves(state).find((m) => m.from === from && m.to === to && (!m.promotion || m.promotion === promotion));
7705
7886
  if (!move2) throw Error("Choose a legal move.");
7706
- const pos = variantPosition(state), last = makeSanAndPlay(pos, move2.internal), setup = pos.toSetup();
7887
+ const pos = variantPosition(state), san = makeSanAndPlay(pos, move2.internal), setup = pos.toSetup();
7888
+ const last = san.endsWith("#") && !pos.isCheckmate() ? san.slice(0, -1) + (pos.isCheck() ? "+" : "") : san;
7707
7889
  const fen = makeFen(setup), history = [...state.history, repetitionKey(fen)];
7708
7890
  const outcome = pos.outcome();
7709
7891
  const repeated = history.filter((key2) => key2 === repetitionKey(fen)).length >= 3;
@@ -8299,13 +8481,14 @@ function playBackgammon(s, type, from, to, die, entropy2) {
8299
8481
  opening: false,
8300
8482
  rolled,
8301
8483
  dice: rolled[0] === rolled[1] ? Array(4).fill(rolled[0]) : rolled,
8302
- result: `Rolled ${rolled.join(" and ")}.`
8484
+ result: `Player ${turn + 1} rolled ${rolled.join(" and ")}.`,
8485
+ last: { by_seat: turn, rolled, moves: [] }
8303
8486
  };
8304
8487
  return backgammonMoves(next4).length ? next4 : {
8305
8488
  ...next4,
8306
8489
  dice: [],
8307
8490
  turn: turn === 0 ? 1 : 0,
8308
- result: "All moves are blocked. Turn passes."
8491
+ result: `Player ${turn + 1} rolled ${rolled.join(" and ")}: all moves are blocked. Turn passes.`
8309
8492
  };
8310
8493
  }
8311
8494
  const move2 = backgammonMoves(s).find(
@@ -8316,6 +8499,7 @@ function playBackgammon(s, type, from, to, die, entropy2) {
8316
8499
  "Use the maximum number of dice; if only one can be used, use the higher die."
8317
8500
  );
8318
8501
  const next3 = step(s, move2);
8502
+ next3.last = { by_seat: s.turn, rolled: s.last?.rolled ?? s.rolled, moves: [...s.last?.by_seat === s.turn ? s.last.moves : [], { ...move2, ...move2.to !== 24 && s.points[move2.to] === (s.turn === 0 ? -1 : 1) ? { hit: true } : {} }] };
8319
8503
  if (next3.off[s.turn] === 15) {
8320
8504
  const opponent = 1 - s.turn, backgammon = next3.bar[opponent] > 0 || next3.points.some(
8321
8505
  (n, i) => (s.turn === 0 ? i >= 18 : i < 6) && n * (s.turn === 0 ? -1 : 1) > 0
@@ -8323,147 +8507,13 @@ function playBackgammon(s, type, from, to, die, entropy2) {
8323
8507
  next3.winner = s.turn;
8324
8508
  next3.result = next3.off[opponent] ? "Single game." : backgammon ? "Backgammon \xB7 3 points." : "Gammon \xB7 2 points.";
8325
8509
  } else if (!backgammonMoves(next3).length) {
8510
+ if (next3.dice.length) next3.result = `Player ${s.turn + 1} cannot use the remaining ${next3.dice.join(" and ")}. Turn passes.`;
8326
8511
  next3.dice = [];
8327
8512
  next3.turn = s.turn === 0 ? 1 : 0;
8328
8513
  }
8329
8514
  return next3;
8330
8515
  }
8331
8516
 
8332
- // lib/multiplayer/football.ts
8333
- var footballPoint = (at) => [at % 9, Math.floor(at / 9)];
8334
- var footballEdge = (a, b) => a < b ? `${a}:${b}` : `${b}:${a}`;
8335
- var valid = (at) => index(at, 117) && (Math.floor(at / 9) > 0 && Math.floor(at / 9) < 12 || at % 9 >= 3 && at % 9 <= 5);
8336
- var boundary = (at) => {
8337
- const [x, y] = footballPoint(at);
8338
- return x === 0 || x === 8 || y === 0 || y === 12 || (y === 1 || y === 11) && x !== 4;
8339
- };
8340
- function createFootball() {
8341
- const edges = [];
8342
- for (let y = 1; y <= 11; y++)
8343
- for (const x of [0, 8])
8344
- if (y < 11) edges.push(footballEdge(y * 9 + x, (y + 1) * 9 + x));
8345
- for (const y of [1, 11])
8346
- for (let x = 0; x < 8; x++)
8347
- if (x < 3 || x >= 5) edges.push(footballEdge(y * 9 + x, y * 9 + x + 1));
8348
- for (const y of [0, 12]) {
8349
- edges.push(
8350
- footballEdge(y * 9 + 3, y * 9 + 4),
8351
- footballEdge(y * 9 + 4, y * 9 + 5)
8352
- );
8353
- for (const x of [3, 5])
8354
- edges.push(footballEdge(y * 9 + x, (y === 0 ? 1 : 11) * 9 + x));
8355
- }
8356
- return { ball: 58, edges, turn: 0, winner: null, last: null };
8357
- }
8358
- function footballMoves(s) {
8359
- if (s.winner !== null) return [];
8360
- const [x, y] = footballPoint(s.ball), moves = [];
8361
- for (let dy = -1; dy <= 1; dy++)
8362
- for (let dx = -1; dx <= 1; dx++) {
8363
- const nx = x + dx, ny = y + dy, at = ny * 9 + nx;
8364
- if ((ny === 0 || ny === 12) && (x < 3 || x > 5)) continue;
8365
- if ((dx || dy) && nx >= 0 && nx <= 8 && ny >= 0 && ny <= 12 && valid(at) && !s.edges.includes(footballEdge(s.ball, at)))
8366
- moves.push(at);
8367
- }
8368
- return moves;
8369
- }
8370
- function playFootball(s, at) {
8371
- if (!footballMoves(s).includes(at))
8372
- throw Error("Choose an adjacent point along an unused line.");
8373
- const bounced = boundary(at) || s.edges.some((edge) => edge.split(":").map(Number).includes(at));
8374
- const y = Math.floor(at / 9);
8375
- const next3 = {
8376
- ...s,
8377
- ball: at,
8378
- edges: [...s.edges, footballEdge(s.ball, at)],
8379
- turn: bounced ? s.turn : s.turn === 0 ? 1 : 0,
8380
- winner: y === 0 ? 0 : y === 12 ? 1 : null,
8381
- last: [s.ball, at]
8382
- };
8383
- if (next3.winner === null && !footballMoves(next3).length)
8384
- next3.winner = 1 - next3.turn;
8385
- return next3;
8386
- }
8387
-
8388
- // lib/multiplayer/ludo.ts
8389
- function createLudo(count) {
8390
- if (!index(count - 2, 3)) throw Error("Choose two to four players.");
8391
- return {
8392
- pieces: Array.from({ length: count }, () => [-1, -1, -1, -1]),
8393
- turn: 0,
8394
- die: 0,
8395
- streak: 0,
8396
- attempts: 0,
8397
- winner: null,
8398
- last: "Roll a six to leave your yard."
8399
- };
8400
- }
8401
- var ludoTrack = (seat, progress, count = 4) => (seat * (count === 2 ? 20 : 10) + progress) % 40;
8402
- function ludoMoves(s) {
8403
- if (!s.die || s.winner !== null) return [];
8404
- return s.pieces[s.turn].flatMap((p, i) => {
8405
- const to = p < 0 ? s.die === 6 ? 0 : -1 : p + s.die;
8406
- return to >= 0 && to <= 43 && !s.pieces[s.turn].some((other, j) => j !== i && other === to) ? [i] : [];
8407
- });
8408
- }
8409
- function advance2(s) {
8410
- return {
8411
- ...s,
8412
- turn: (s.turn + 1) % s.pieces.length,
8413
- die: 0,
8414
- streak: 0,
8415
- attempts: 0
8416
- };
8417
- }
8418
- function playLudo(s, type, at, entropy2) {
8419
- if (s.winner !== null) throw Error("This match is finished.");
8420
- if (type === "roll") {
8421
- if (s.die) throw Error("Move a piece before rolling again.");
8422
- const die = 1 + Math.floor(draws(entropy2, 1)[0] * 6), streak = die === 6 ? s.streak + 1 : 0;
8423
- let next4 = {
8424
- ...s,
8425
- die,
8426
- streak,
8427
- attempts: s.attempts + 1,
8428
- last: `Player ${s.turn + 1} rolled ${die}.`
8429
- };
8430
- if (streak > 3)
8431
- return advance2({
8432
- ...next4,
8433
- last: "Three extra moves is the limit. Turn passes."
8434
- });
8435
- if (!ludoMoves(next4).length) {
8436
- const allHome = s.pieces[s.turn].every((p) => p === -1);
8437
- next4 = allHome && next4.attempts < 3 || die === 6 ? { ...next4, die: 0 } : advance2(next4);
8438
- }
8439
- return next4;
8440
- }
8441
- if (type !== "move" || at === void 0 || !index(at, 4) || !ludoMoves(s).includes(at))
8442
- throw Error(
8443
- "Choose a highlighted piece. You need an exact roll to reach home."
8444
- );
8445
- const pieces = s.pieces.map((row) => [...row]), old = pieces[s.turn][at], to = old < 0 ? 0 : old + s.die;
8446
- pieces[s.turn][at] = to;
8447
- if (to < 40) {
8448
- for (let seat = 0; seat < pieces.length; seat++)
8449
- if (seat !== s.turn) {
8450
- pieces[seat] = pieces[seat].map(
8451
- (p) => p >= 0 && p < 40 && ludoTrack(seat, p, pieces.length) === ludoTrack(s.turn, to, pieces.length) ? -1 : p
8452
- );
8453
- }
8454
- }
8455
- const won = pieces[s.turn].every((p) => p >= 40);
8456
- const next3 = {
8457
- ...s,
8458
- pieces,
8459
- die: 0,
8460
- attempts: 0,
8461
- winner: won ? s.turn : null,
8462
- last: `Player ${s.turn + 1} moved piece ${at + 1}.`
8463
- };
8464
- return s.die === 6 && s.streak < 3 || won ? next3 : advance2(next3);
8465
- }
8466
-
8467
8517
  // lib/multiplayer/go.ts
8468
8518
  function createGo(size = 9) {
8469
8519
  const board = Array(size * size).fill(0);
@@ -8708,7 +8758,7 @@ var adapter5 = defineMatchAdapter({
8708
8758
  return {
8709
8759
  finished: state.winner !== null || state.draw,
8710
8760
  winner: state.winner,
8711
- label: state.draw ? "Draw" : state.winner !== null ? `Player ${state.winner + 1} wins \xB7 the opponent has no legal move.` : ""
8761
+ label: state.draw ? drawReason(state.history) : state.winner !== null ? `Player ${state.winner + 1} wins \xB7 the opponent has no legal move.` : ""
8712
8762
  };
8713
8763
  },
8714
8764
  apply(state, _seat, action, _entropy, _now) {
@@ -8726,7 +8776,7 @@ var adapter6 = defineMatchAdapter({
8726
8776
  actions: ["move", "sequence"],
8727
8777
  create: () => createInternational(),
8728
8778
  turn: (s) => s.turn,
8729
- outcome: (s) => ({ finished: s.winner !== null || s.draw, winner: s.winner, label: s.draw ? "Draw" : s.winner !== null ? `Player ${s.winner + 1} wins \xB7 the opponent has no legal move.` : "" }),
8779
+ outcome: (s) => ({ finished: s.winner !== null || s.draw, winner: s.winner, label: s.draw ? drawReason(s.history) : s.winner !== null ? `Player ${s.winner + 1} wins \xB7 the opponent has no legal move.` : "" }),
8730
8780
  apply(s, _seat, a) {
8731
8781
  if (a.type === "move" && typeof a.from === "number" && typeof a.to === "number") return moveInternational(s, a.from, a.to);
8732
8782
  if (a.type === "sequence" && Array.isArray(a.cards) && a.cards.length >= 2 && a.cards.length <= 21) {
@@ -9061,6 +9111,7 @@ function playThousand(s, a, entropy2) {
9061
9111
  if (a.type === "next" && s.phase === "round") {
9062
9112
  t.round++;
9063
9113
  t.dealer = (s.dealer + 1) % s.hands.length;
9114
+ t.previous = s.result;
9064
9115
  return deal6(t, entropy2);
9065
9116
  }
9066
9117
  if (a.type === "redeal" && s.phase === "auction" && s.hands[seat].filter((c) => cardRank(c) === 9).length === 4)
@@ -9107,9 +9158,9 @@ function playThousand(s, a, entropy2) {
9107
9158
  return t;
9108
9159
  }
9109
9160
  if (a.type === "bomb" && ["kitty", "exchange", "declare"].includes(s.phase)) {
9110
- if (t.bombs[seat]++ > 0)
9111
- t.players.filter((i) => i !== seat && t.scores[i] < 800).forEach((i) => t.scores[i] += t.hands.length === 4 ? 40 : 60);
9112
- t.result = "Declarer cancels the hand with a bomb.";
9161
+ const penalty2 = t.bombs[seat]++ > 0 ? t.hands.length === 4 ? 40 : 60 : 0;
9162
+ t.players.filter((i) => i !== seat && t.scores[i] < 800).forEach((i) => t.scores[i] += penalty2);
9163
+ t.result = `Player ${seat + 1} cancels the hand with a bomb${penalty2 ? `; the other players score ${penalty2}` : " (the first bomb is free)"}.`;
9113
9164
  finish4(t);
9114
9165
  return t;
9115
9166
  }
@@ -9154,10 +9205,12 @@ function playThousand(s, a, entropy2) {
9154
9205
  (n, c) => n + pointValue(cardRank(c)),
9155
9206
  0
9156
9207
  );
9208
+ const before = t.scores;
9157
9209
  t.scores = t.scores.map(
9158
9210
  (n, i) => i === t.bidder ? n + (t.points[i] >= t.contract ? t.contract : -t.contract) : t.players.includes(i) && n < 800 ? n + Math.round(t.points[i] / 10) * 10 : n
9159
9211
  );
9160
- t.result = `Hand points ${t.points.join(" / ")} \xB7 Contract ${t.contract}.`;
9212
+ const made = t.points[t.bidder] >= t.contract, change = (i) => `${t.scores[i] - before[i] >= 0 ? "+" : ""}${t.scores[i] - before[i]}`;
9213
+ t.result = `Player ${t.bidder + 1} ${made ? "makes" : "fails"} ${t.contract} with ${t.points[t.bidder]} (${change(t.bidder)}). ${t.players.filter((i) => i !== t.bidder).map((i) => `Player ${i + 1} ${t.points[i]} points (${before[i] >= 800 ? "not scored: 800 rule" : change(i)})`).join(", ")}.`;
9161
9214
  finish4(t);
9162
9215
  }
9163
9216
  return t;
@@ -10413,6 +10466,7 @@ function playMakao(s, a, entropy2) {
10413
10466
  const t = structuredClone(s), seat = s.turn;
10414
10467
  if (a.type === "catch" && s.unannounced !== null && s.unannounced !== seat) {
10415
10468
  draw(t, s.unannounced, 5, entropy2);
10469
+ t.last = { seat: s.unannounced, type: "caught", count: 5 };
10416
10470
  t.unannounced = null;
10417
10471
  t.result = "Missed Makao: draw five.";
10418
10472
  return t;
@@ -10421,12 +10475,13 @@ function playMakao(s, a, entropy2) {
10421
10475
  t.unannounced = null;
10422
10476
  if (s.wait) {
10423
10477
  t.blocked[seat] = s.wait - 1;
10478
+ t.last = { seat, type: "wait", count: s.wait };
10424
10479
  t.wait = 0;
10425
10480
  advance3(t);
10426
10481
  return t;
10427
10482
  }
10428
10483
  if (s.penalty) {
10429
- draw(t, seat, s.penalty, entropy2);
10484
+ t.last = { seat, type: "penalty", count: draw(t, seat, s.penalty, entropy2).length };
10430
10485
  t.penalty = 0;
10431
10486
  t.suit = null;
10432
10487
  advance3(t);
@@ -10434,11 +10489,13 @@ function playMakao(s, a, entropy2) {
10434
10489
  }
10435
10490
  const received = draw(t, seat, 1, entropy2);
10436
10491
  t.drawn = received[0] ?? null;
10492
+ t.last = { seat, type: "draw", count: received.length };
10437
10493
  t.phase = "drawn";
10438
10494
  if (t.drawn === null) advance3(t);
10439
10495
  return t;
10440
10496
  }
10441
10497
  if (a.type === "pass" && s.phase === "drawn") {
10498
+ t.last = { seat, type: "keep", count: 1 };
10442
10499
  t.unannounced = null;
10443
10500
  t.suit = null;
10444
10501
  advance3(t);
@@ -10453,6 +10510,7 @@ function playMakao(s, a, entropy2) {
10453
10510
  throw Error("Request an ordinary rank, or choose no demand.");
10454
10511
  t.unannounced = null;
10455
10512
  t.hands[seat] = t.hands[seat].filter((c) => !cards.includes(c));
10513
+ t.last = { seat, type: "play", cards: [...cards], ...t.hands[seat].length === 1 && a.promotion === "makao" ? { announced: true } : {} };
10456
10514
  t.pile.push(...cards);
10457
10515
  t.suit = null;
10458
10516
  if (rank2 === 14) t.suit = a.amount;
@@ -11187,7 +11245,7 @@ function playProperties(s, a, entropy2) {
11187
11245
  }
11188
11246
  if (s.phase === "tax") {
11189
11247
  if (a.type !== "tax" || ![0, 1].includes(a.amount ?? -1))
11190
- throw Error("Choose flat tax or ten percent of estate value.");
11248
+ throw Error("Choose the tax: amount 0 pays the flat 200, amount 1 pays ten percent of your estate value.");
11191
11249
  const estate = p.cash + s.assets.reduce(
11192
11250
  (n, a2, i) => n + (a2.owner === seat ? propertyFields[i].price + a2.houses * propertyFields[i].build : 0),
11193
11251
  0
@@ -11262,7 +11320,8 @@ function playProperties(s, a, entropy2) {
11262
11320
  t.dice = [];
11263
11321
  return t;
11264
11322
  }
11265
- throw Error("Choose an action for the current estate phase.");
11323
+ if (a.type === "release") throw Error(p.jail < 0 ? "You are not in jail." : "Leave jail at the start of your turn, in the roll phase: release with amount 0 pays 50, amount 1 uses a release card.");
11324
+ throw Error(`That action is not available in the ${s.phase} phase; choose one from the legal list.`);
11266
11325
  }
11267
11326
  function propertyView(s) {
11268
11327
  const { decks, ...publicState } = s;
@@ -11406,7 +11465,7 @@ function panLegal(s, cards) {
11406
11465
  }
11407
11466
  function playPan(s, type, cards, entropy2) {
11408
11467
  if (type === "next" && s.phase === "round")
11409
- return deal12(s.hands.length, entropy2, s.letters, s.round + 1);
11468
+ return { ...deal12(s.hands.length, entropy2, s.letters, s.round + 1), previous: s.result };
11410
11469
  if (s.phase !== "play") throw Error("This round is finished.");
11411
11470
  const t = structuredClone(s), seat = s.turn;
11412
11471
  if (type === "play" && panLegal(s, cards)) {
@@ -11560,7 +11619,7 @@ function award2(t, winner2, failure = false) {
11560
11619
  }
11561
11620
  function playSixtySix(s, type, at, marriage = false, claimMarriage = false, entropy2) {
11562
11621
  if (type === "next" && s.phase === "round")
11563
- return deal13(1 - s.dealer, s.scores, s.round + 1, entropy2);
11622
+ return { ...deal13(1 - s.dealer, s.scores, s.round + 1, entropy2), previous: s.result };
11564
11623
  if (s.phase !== "play") throw Error("This round is finished.");
11565
11624
  const t = structuredClone(s), seat = s.turn;
11566
11625
  if (type === "close" && !s.trick.length && s.closed === null && s.upcard !== null) {
@@ -11605,6 +11664,7 @@ function playSixtySix(s, type, at, marriage = false, claimMarriage = false, entr
11605
11664
  t.tricks[winner2]++;
11606
11665
  t.turn = winner2;
11607
11666
  t.lastTrick = t.trick;
11667
+ t.played = [...s.played ?? [], ...t.trick.map((c) => c.card)];
11608
11668
  t.trick = [];
11609
11669
  if (t.closed === null && t.upcard !== null)
11610
11670
  for (const i of [winner2, 1 - winner2]) {
@@ -11746,9 +11806,9 @@ function dominoMoves(s) {
11746
11806
  ].filter((m) => m !== null);
11747
11807
  });
11748
11808
  }
11749
- function endRound(s, winner2, bonus) {
11750
- const scores2 = [...s.scores];
11751
- if (winner2 !== null) scores2[winner2] += Math.round(bonus / 5) * 5;
11809
+ function endRound(s, winner2, bonus, out2 = false) {
11810
+ const scores2 = [...s.scores], points = Math.round(bonus / 5) * 5;
11811
+ if (winner2 !== null) scores2[winner2] += points;
11752
11812
  const finished = scores2.some((n) => n >= 100);
11753
11813
  return {
11754
11814
  ...s,
@@ -11756,7 +11816,7 @@ function endRound(s, winner2, bonus) {
11756
11816
  winner: finished ? best(scores2) : null,
11757
11817
  phase: finished ? "finished" : "round",
11758
11818
  turn: winner2 ?? s.turn,
11759
- result: winner2 === null ? "Blocked round: equal pip totals." : `Player ${winner2 + 1} wins the round.`
11819
+ result: winner2 === null ? "Blocked round: equal pip totals, nobody scores." : out2 ? `Player ${winner2 + 1} goes out and scores ${points} for the opponent's ${bonus} pips (rounded to five).` : `Blocked round: Player ${winner2 + 1} holds fewer pips and scores ${points} for the ${bonus}-pip difference (rounded to five).`
11760
11820
  };
11761
11821
  }
11762
11822
  var pipSum = (hand2) => hand2.reduce((n, [a, b]) => n + a + b, 0);
@@ -11775,8 +11835,9 @@ function playDomino(s, type, at, end, entropy2) {
11775
11835
  throw Error("Play a matching tile before drawing.");
11776
11836
  while (stock.length && !dominoMoves({ ...s, hands }).length)
11777
11837
  hands[s.turn].push(stock.pop());
11778
- if (dominoMoves({ ...s, hands }).length) return { ...s, hands, stock };
11779
- const next4 = { ...s, hands, stock, turn: 1 - s.turn, passes: s.passes + 1 };
11838
+ const drawn = hands[s.turn].length - s.hands[s.turn].length;
11839
+ if (dominoMoves({ ...s, hands }).length) return { ...s, hands, stock, result: `Player ${s.turn + 1} drew ${drawn} tile${drawn === 1 ? "" : "s"}.` };
11840
+ const next4 = { ...s, hands, stock, turn: 1 - s.turn, passes: s.passes + 1, result: `Player ${s.turn + 1} ${drawn ? `drew ${drawn} tile${drawn === 1 ? "" : "s"}, ` : ""}cannot play and passes.` };
11780
11841
  if (next4.passes < 2) return next4;
11781
11842
  const a2 = pipSum(hands[0]), b2 = pipSum(hands[1]);
11782
11843
  return endRound(next4, a2 === b2 ? null : a2 < b2 ? 0 : 1, Math.abs(a2 - b2));
@@ -11794,7 +11855,8 @@ function playDomino(s, type, at, end, entropy2) {
11794
11855
  }
11795
11856
  const left = chain[0], right = chain.at(-1);
11796
11857
  const total = chain.length === 1 ? left[0] + left[1] : (left[0] === left[1] ? left[0] * 2 : left[0]) + (right[0] === right[1] ? right[1] * 2 : right[1]);
11797
- if (total % 5 === 0) scores2[s.turn] += total;
11858
+ const scored = total % 5 === 0 && total > 0;
11859
+ if (scored) scores2[s.turn] += total;
11798
11860
  const next3 = {
11799
11861
  ...s,
11800
11862
  hands,
@@ -11802,10 +11864,11 @@ function playDomino(s, type, at, end, entropy2) {
11802
11864
  chain,
11803
11865
  scores: scores2,
11804
11866
  passes: 0,
11805
- turn: 1 - s.turn
11867
+ turn: 1 - s.turn,
11868
+ result: `Player ${s.turn + 1} played ${a}-${b}${scored ? ` and scores ${total}: the open ends total ${total}` : ""}.`
11806
11869
  };
11807
11870
  if (!hands[s.turn].length)
11808
- return endRound(next3, s.turn, pipSum(hands[1 - s.turn]));
11871
+ return endRound(next3, s.turn, pipSum(hands[1 - s.turn]), true);
11809
11872
  if (scores2.some((n) => n >= 100))
11810
11873
  return { ...next3, phase: "finished", winner: best(scores2) };
11811
11874
  return next3;
@@ -12117,7 +12180,7 @@ function agentListComplete(game, state, legal) {
12117
12180
  if (game === "poker") return !legal.some((action) => action.type === "raise");
12118
12181
  return false;
12119
12182
  }
12120
- var amounts = [.../* @__PURE__ */ new Set([...Array.from({ length: 301 }, (_, i) => i), ...Array.from({ length: 41 }, (_, i) => 300 + i * 10)])];
12183
+ var amounts = [.../* @__PURE__ */ new Set([...Array.from({ length: 301 }, (_, i) => i), ...Array.from({ length: 141 }, (_, i) => 300 + i * 5)])];
12121
12184
  var promotions = ["marriage", "marriage-claim", "makao", "normal", "schneider", "schwarz", "ouvert"];
12122
12185
  function hand(state, seat) {
12123
12186
  for (const key2 of ["hands", "hand"]) {
@@ -12144,6 +12207,15 @@ function cardGroups(cards) {
12144
12207
  else for (let i = 0; i + 4 <= cards.length; i += 4) add(cards.slice(i, i + 4));
12145
12208
  return [...groups.values()].map(([group]) => group);
12146
12209
  }
12210
+ function runGroups(cards) {
12211
+ const runs = [];
12212
+ for (const card2 of cards) for (let length = 3; ; length++) {
12213
+ const run = Array.from({ length }, (_, i) => card2 + i);
12214
+ if (card2 % 13 + length > 13 || !run.every((c) => cards.includes(c))) break;
12215
+ runs.push(run);
12216
+ }
12217
+ return runs;
12218
+ }
12147
12219
  var atTypes = /* @__PURE__ */ new Set(["fire", "sow", "hold", "score", "capture", "knock", "dead", "mortgage", "redeem", "build", "sell", "choose", "kitty", "place", "tile", "move"]);
12148
12220
  var amountTypes = /* @__PURE__ */ new Set(["bid", "raise", "declare", "tax", "release", "moon", "trump", "contract", "kitty", "announce", "choose", "tile", "place", "move"]);
12149
12221
  var cardTypes = /* @__PURE__ */ new Set(["play", "discard", "exchange", "select", "meld", "layoff"]);
@@ -12198,6 +12270,10 @@ function specific(game, state, seat) {
12198
12270
  { type: "play", cards, promotion: "makao" },
12199
12271
  ...[0, 1, 2, 3, 5, 6, 7, 8, 9, 10, 13].flatMap((amount) => [{ type: "play", cards, amount }, { type: "play", cards, amount, promotion: "makao" }])
12200
12272
  ])];
12273
+ if (game === "rummy") {
12274
+ const runs = runGroups(hand(state, seat));
12275
+ return [...generic(adapters.rummy, state, seat), ...runs.map((cards) => ({ type: "meld", cards })), ...runs.flatMap((cards) => Array.from({ length: 20 }, (_, at) => ({ type: "layoff", at, cards })))];
12276
+ }
12201
12277
  if (game === "go") {
12202
12278
  const s = state;
12203
12279
  if (s.phase === "scoring" && s.accepted.includes(seat)) return [];
@@ -12407,12 +12483,33 @@ function agentPosition(room) {
12407
12483
  const active = room.phase === "playing" && !!match && !room.players[room.seat].left && !ended && !room.players.some((p) => p.paused);
12408
12484
  const grid = !complete && match ? indexedBoard(room.game, match.state) : null;
12409
12485
  const wire = !complete && match && enumeratedAgentGames.includes(room.game) ? active ? agentLegalActions(room.game, match.state, room.seat, room.capacity) ?? [] : [] : null;
12410
- const checkerLast = match?.kind === "checkers" || match?.kind === "brazilian-checkers" ? checkerLastMove(match.state.history) : null;
12486
+ const checkerLast = match?.kind === "checkers" ? checkerLastMove(match.state.history) : null;
12487
+ const snapshotLast = (() => {
12488
+ if (!match) return null;
12489
+ const draughts2 = (key2) => key2?.split(":")[0].split(",").map(Number);
12490
+ const pieceOwner = (p) => p ? Math.abs(p) - 1 : -1, seatOwner = (p) => p;
12491
+ if (match.kind === "checkers" || match.kind === "brazilian-checkers" || match.kind === "international-draughts") {
12492
+ const [a, b] = match.state.history.slice(-2);
12493
+ return snapshotTurn(draughts2(a), draughts2(b), pieceOwner, [1 - Number(b?.split(":")[1])]);
12494
+ }
12495
+ if (match.kind === "fanorona") {
12496
+ const [a, b] = match.state.history.slice(-2).map((k) => k.split(":")[0].match(/-1|0|1/g)?.map(Number));
12497
+ return snapshotTurn(a, b, seatOwner, [1 - match.state.turn, match.state.turn]);
12498
+ }
12499
+ if (match.kind === "go") {
12500
+ if (match.state.passes > 0) return { by_seat: 1 - match.state.turn, type: "pass" };
12501
+ const [a, b] = match.state.history.slice(-2).map((k) => [...k].map(Number));
12502
+ return snapshotTurn(a, b, (p) => p - 1, [1 - match.state.turn]);
12503
+ }
12504
+ if ((match.kind === "amazons" || match.kind === "dots-boxes" || match.kind === "lines-of-action" || match.kind === "backgammon") && match.state.last) return match.state.last;
12505
+ return null;
12506
+ })();
12411
12507
  const dotsLabeled = match?.kind === "dots-boxes" ? match.state.boxes.map((owner4, box) => {
12412
12508
  const { size, edges } = match.state, sides2 = boxEdges(size, box), drawn = sides2.filter((e) => edges[e] >= 0).length;
12413
12509
  const named = ["top", "bottom", "left", "right"].map((side2, i) => `${side2} ${sides2[i] + 1}${edges[sides2[i]] >= 0 ? "*" : ""}`).join(", ");
12414
12510
  return `box row ${Math.floor(box / size) + 1} col ${box % size + 1} [${owner4 < 0 ? "." : "XO"[owner4]}]: ${named}${owner4 < 0 && drawn === 3 ? " (three sides: takeable)" : ""}`;
12415
12511
  }) : null;
12512
+ const ludoLabeled = match?.kind === "ludo" ? match.state.pieces.map((row, seat) => `seat ${seat} (${room.players[seat]?.name ?? "empty"}): ${row.map((p, i) => `piece ${i + 1} ${p < 0 ? "yard" : p >= 40 ? `home lane ${p - 39}/4` : `progress ${p}, track square ${ludoTrack(seat, p, match.state.pieces.length)}`}`).join("; ")}`) : null;
12416
12513
  const ginHand = match?.kind === "gin-rummy" && match.state.phase !== "finished" ? match.state.hands[room.seat]?.filter((c) => c !== null) ?? [] : [];
12417
12514
  const ginHint = ginHand.length ? (() => {
12418
12515
  const layout = ginLayout(ginHand), names = (cards) => cards.map((c) => `${cardLabel(c)} (id ${c})`);
@@ -12425,11 +12522,19 @@ function agentPosition(room) {
12425
12522
  knock_limit: 10
12426
12523
  };
12427
12524
  })() : null;
12525
+ const ruleNotes = match?.kind === "thousand" ? match.state.scores.flatMap((score, seat) => match.state.phase !== "finished" && score >= 800 ? [`Seat ${seat} (${room.players[seat]?.name ?? "empty"}) has ${score} points: from 800, defending points no longer score; only a made contract adds points.`] : []) : match?.kind === "dice" ? match.state.sheets.map((sheet, seat) => {
12526
+ const upper = sheet.slice(0, 6).reduce((n, v) => n + (v ?? 0), 0);
12527
+ return `Seat ${seat} (${room.players[seat]?.name ?? "empty"}): upper section ${upper}/63${upper >= 63 ? ", 35 bonus earned" : ""}; total ${diceTotal(match.state, seat)}.`;
12528
+ }) : match?.kind === "polish-hearts" && match.state.phase !== "finished" ? [polishHandNote(match.state.round, match.state.hands.length)] : match?.kind === "three-check" ? [`Checks given: seat 0 ${match.state.checks[0]}, seat 1 ${match.state.checks[1]}; the third check wins.`] : [];
12529
+ const sixtyUnseen = match?.kind === "sixty-six" && match.state.phase === "play" ? (() => {
12530
+ const s = match.state, seen = /* @__PURE__ */ new Set([...(s.hands[room.seat] ?? []).filter((c) => c !== null), ...s.played ?? [], ...s.trick.map((c) => c.card), ...s.upcard === null ? [] : [s.upcard]]);
12531
+ return Array.from({ length: 24 }, (_, i) => Math.floor(i / 6) * 13 + 7 + i % 6).filter((c) => !seen.has(c)).map((c) => `${cardLabel(c)} (id ${c})`);
12532
+ })() : null;
12428
12533
  const asianMove = match?.kind === "shogi" || match?.kind === "xiangqi" ? asianLastMove(match.state.history) : null;
12429
12534
  const asianLast = asianMove && (match?.kind === "shogi" || match?.kind === "xiangqi") ? { ...asianMove, by: match.state.history.at(-1).mover } : null;
12430
12535
  const millLast = match?.kind === "mills" ? millLastTurn(match.state) : null;
12431
- const cribbageCard = match?.kind === "cribbage" && match.state.phase === "peg" ? match.state.sequence.at(-1) : void 0;
12432
- const cribbageLast = match?.kind === "cribbage" && cribbageCard !== void 0 ? { by_seat: match.state.played.findIndex((cards) => cards.includes(cribbageCard)), played: `${cribbageLabel(cribbageCard)} (id ${cribbageCard})`, count: match.state.total } : null;
12536
+ const peg = match?.kind === "cribbage" && match.state.phase === "peg" ? match.state.lastPeg : void 0;
12537
+ const cribbageLast = peg ? { by_seat: peg.seat, ...peg.go ? { type: "go" } : { played: `${cribbageLabel(peg.card)} (id ${peg.card})` }, count: peg.count, ...peg.count === 31 || match?.kind === "cribbage" && match.state.total === 0 && peg.count > 0 ? { count_reset: true } : {} } : null;
12433
12538
  return {
12434
12539
  protocol_version: 1,
12435
12540
  room: room.code,
@@ -12444,16 +12549,18 @@ function agentPosition(room) {
12444
12549
  players: room.players.map((p, seat) => ({ seat, name: p.name, left: p.left, ...p.agent ? { agent: true } : {}, ...p.paused ? { paused: true } : {} })),
12445
12550
  board_size: complete ? { rows, columns } : grid ? { rows: grid.rows, columns: grid.columns } : { rows: 0, columns: 0 },
12446
12551
  board_top_to_bottom: board,
12447
- board_labeled: dotsLabeled ?? (match?.kind === "oware" ? [...labeledBoard(room.game, board) ?? [], `captured: seat 0 ${match.state.scores[0]}, seat 1 ${match.state.scores[1]}`] : null) ?? labeledBoard(room.game, board) ?? grid?.labeled ?? null,
12552
+ board_labeled: dotsLabeled ?? ludoLabeled ?? (match?.kind === "football" ? footballLabeled(match.state) : null) ?? (match?.kind === "oware" ? [...labeledBoard(room.game, board) ?? [], `captured: seat 0 ${match.state.scores[0]}, seat 1 ${match.state.scores[1]}`] : null) ?? labeledBoard(room.game, board) ?? grid?.labeled ?? null,
12448
12553
  ...grid ? { board_legend: grid.legend } : dotsLabeled ? { board_legend: "One line per box: owner (X seat 0, O seat 1, . open), then its four edges as one-based edge numbers (--edge N); * marks a drawn edge. A box with three drawn edges can be taken now." } : {},
12449
12554
  // Chess and its variants record the last move in SAN.
12450
- last_move: (match?.kind === "chess" || match?.kind === "atomic-chess" || match?.kind === "three-check") && match.state.last ? { san: match.state.last } : checkerLast ? { from: square(checkerLast.from), to: square(checkerLast.to) } : cribbageLast ? cribbageLast : asianLast ? { by_seat: asianLast.by, ...asianLast.from === null ? { type: "drop" } : { from: asianLast.from }, to: asianLast.to } : match?.kind === "oware" && match.state.last !== null ? { last_seed_in_pit: match.state.last + 1 } : millLast ? { by_seat: millLast.seat, ...millLast.from === null ? { type: "place" } : { type: "move", from: millLast.from }, at: millLast.to, ...millLast.captured.length ? { captured: millLast.captured } : {} } : last === null ? null : room.game === "connect-four" ? { column: last % columns + 1, row_from_top: Math.floor(last / columns) + 1 } : { row: Math.floor(last / columns) + 1, column: last % columns + 1 },
12555
+ last_move: (match?.kind === "chess" || match?.kind === "atomic-chess" || match?.kind === "three-check") && match.state.last ? { san: match.state.last } : checkerLast ? { by_seat: snapshotLast?.by_seat, from: square(checkerLast.from), to: square(checkerLast.to), ...snapshotLast && "captured" in snapshotLast && Array.isArray(snapshotLast.captured) ? { captured: snapshotLast.captured.map(square) } : {} } : snapshotLast ? snapshotLast : cribbageLast ? cribbageLast : match?.kind === "makao" && match.state.last ? { by_seat: match.state.last.seat, type: match.state.last.type, ...match.state.last.cards ? { cards: match.state.last.cards.map((c) => `${cardLabel(c)} (id ${c})`) } : {}, ...match.state.last.count !== void 0 && match.state.last.type !== "play" ? { count: match.state.last.count } : {}, ...match.state.last.announced ? { announced_makao: true } : {} } : asianLast ? { by_seat: asianLast.by, ...asianLast.from === null ? { type: "drop" } : { from: asianLast.from }, to: asianLast.to } : match?.kind === "oware" && match.state.last !== null ? { last_seed_in_pit: match.state.last + 1 } : millLast ? { by_seat: millLast.seat, ...millLast.from === null ? { type: "place" } : { type: "move", from: millLast.from }, at: millLast.to, ...millLast.captured.length ? { captured: millLast.captured } : {} } : last === null ? null : room.game === "connect-four" ? { column: last % columns + 1, row_from_top: Math.floor(last / columns) + 1 } : { row: Math.floor(last / columns) + 1, column: last % columns + 1 },
12451
12556
  ...room.game === "chess" ? { fen } : {},
12452
12557
  // The side to move is in check (chess family, xiangqi, shogi).
12453
12558
  ...match && room.phase === "playing" && (match.kind === "chess" ? chessPosition(match.state).inCheck() : "check" in match.state && match.state.check === true) ? { in_check: true } : {},
12454
12559
  visible_state: match?.state ?? null,
12455
12560
  cards_labeled: match ? cardsLabeled(room.game, match.state) : null,
12456
12561
  ...ginHint ? { gin_deadwood: ginHint } : {},
12562
+ ...sixtyUnseen ? { sixty_six_unseen: sixtyUnseen } : {},
12563
+ ...ruleNotes.length ? { rule_notes: ruleNotes } : {},
12457
12564
  // Timed rounds (drawing, categories): agents cannot measure time, so give the remaining seconds.
12458
12565
  ...typeof match?.state?.deadline === "number" && room.serverNow ? { deadline_seconds_left: Math.max(0, Math.ceil((match.state.deadline - room.serverNow) / 1e3)) } : {},
12459
12566
  action_types: action_catalog_default[room.game],
@@ -12481,13 +12588,29 @@ function labeledBoard(game, board) {
12481
12588
  ...board.map((row, r) => `${String(algebraic ? board.length - r : r + 1).padStart(rowWidth)} ${game === "hex" ? " ".repeat(Math.round(r * (cell + 1) / 2)) : ""}${[...row].map((p) => p.padStart(cell)).join(" ")}`)
12482
12589
  ];
12483
12590
  }
12591
+ function moveRejection(position, input) {
12592
+ if (!position.turn?.yours) return "It is not your turn. ";
12593
+ if (input.from === void 0) return "";
12594
+ const wire = "legal_wire_actions" in position ? position.legal_wire_actions ?? [] : [];
12595
+ const moves = [...position.legal_actions, ...wire].filter((a) => a.type === "move" && String(a.from) === input.from);
12596
+ if (!moves.length) return `No legal move starts on ${input.from}. `;
12597
+ const exact = moves.filter((a) => String(a.to) === input.to);
12598
+ if (exact.length) return `${input.from} to ${input.to} is listed with promotion ${[...new Set(exact.map((a) => String(a.promotion ?? "none")))].join(" or ")}; pass --promotion exactly as listed. `;
12599
+ return `${input.from} to ${input.to} is not legal now; from ${input.from} the legal destinations are ${[...new Set(moves.map((a) => String(a.to)))].join(", ")}. `;
12600
+ }
12601
+ function polishHandNote(round, count) {
12602
+ const three = count === 3, total = 8 + count;
12603
+ const hands = [`no tricks: ${three ? 15 : 20} off per trick taken`, "no hearts: 20 off per heart", "no queens: 60 off per queen", "no jacks or kings: 30 off each", `no king of hearts: ${three ? 140 : 150} off`, `no seventh or last trick: ${three ? 70 : 75} off each`, "robber: every penalty of hands 1\u20136 at once"];
12604
+ const name = round < 7 ? hands[round] : round < 7 + count ? `trump contract chosen by the dealer: each trick taken scores +${three ? 20 : 25}` : "layout: build the four suit rows outward from the base rank (scoring as in the rules)";
12605
+ return `Hand ${round + 1} of ${total}: ${name}.`;
12606
+ }
12484
12607
  function agentMove(position, input) {
12485
12608
  const action = position.legal_actions.find((a) => a.type === "swap" ? input.swap === true && Object.entries(input).every(([k, v]) => k === "swap" || v === void 0) : a.type === "edge" ? input.edge === a.edge && Object.entries(input).every(([k, v]) => k === "edge" || v === void 0) : a.type === "sow" ? input.pit === a.pit && Object.entries(input).every(([k, v]) => k === "pit" || v === void 0) : !input.swap && input.edge === void 0 && input.pit === void 0 && a.type === "move" ? input.column === void 0 && input.row === void 0 && a.from === input.from && a.to === input.to && a.promotion === input.promotion : a.type === "place" || a.type === "drop" ? !input.swap && input.edge === void 0 && input.pit === void 0 && input.from === void 0 && input.to === void 0 && input.promotion === void 0 && a.column === input.column && (a.type === "drop" ? input.row === void 0 : a.row === input.row) : false);
12486
12609
  if (!action && input.from !== void 0 && input.to !== void 0 && "legal_wire_actions" in position) {
12487
12610
  const wire = position.legal_wire_actions?.find((a) => a.type === "move" && String(a.from) === input.from && String(a.to) === input.to && (a.promotion === void 0 || a.promotion === input.promotion));
12488
12611
  if (wire) return wire;
12489
12612
  }
12490
- if (!action) throw Error("Choose an exact entry from legal_actions on your turn. Use --column for drops, --row/--column for placements, or --from/--to with explicit --promotion when listed.");
12613
+ if (!action) throw Error(`${moveRejection(position, input)}Choose an exact entry from legal_actions on your turn. Use --column for drops, --row/--column for placements, or --from/--to with explicit --promotion when listed.`);
12491
12614
  if (action.type === "swap") return { type: "swap" };
12492
12615
  if (action.type === "edge") return { type: "place", at: action.edge - 1 };
12493
12616
  if (action.type === "sow") return { type: "sow", at: action.pit - 1 };
@@ -12529,6 +12652,10 @@ function agentWireMove(position, json) {
12529
12652
  if (!position.action_types.includes(action.type)) throw Error(`Choose a wire action type (${position.action_types.join(", ")}) from game rules, or pass an exact legal_actions entry.`);
12530
12653
  if (position.phase !== "playing" || position.you.left || position.paused) throw Error("This match is not active for your seat.");
12531
12654
  if (position.game === "go" && action.type === "accept" && action.scoreKey === void 0 && position.visible_state) action = { ...action, scoreKey: goProposalKey(position.visible_state) };
12655
+ if (position.game === "categories" && ["submit", "stop", "vote", "expire"].includes(action.type) && position.visible_state) {
12656
+ const state = position.visible_state;
12657
+ action = { ...action, at: action.at ?? state.round, scoreKey: action.scoreKey ?? state.roundKey ?? void 0, ...Array.isArray(action.text) ? { text: JSON.stringify(action.text) } : {} };
12658
+ }
12532
12659
  return action;
12533
12660
  }
12534
12661
  function sameFields(a, b) {
@@ -12782,7 +12909,9 @@ function nextStep(position, event, brief = false, server) {
12782
12909
  const room = `--room ${position.room}${server && server !== defaultServer ? ` --server ${server}` : ""}`, revision = position.revision, flag = brief ? " --brief" : "";
12783
12910
  const wait = `leafarcade game wait ${room} --after-revision ${revision} --timeout ${defaultWaitSeconds}${flag}`;
12784
12911
  if (position.you.left) return "You left this room. Create or join another room only if the human asks.";
12785
- if (position.phase === "finished") return `Match finished${position.outcome?.label ? ` (${position.outcome.label})` : ""}. Tell the human the result. Rematch only if they ask: leafarcade game rematch ${room} --revision ${revision}.`;
12912
+ const won = position.outcome?.winner;
12913
+ const result = won === void 0 || won === null ? position.phase === "finished" ? " Nobody won." : "" : won === position.you.seat ? " You won." : ` Seat ${won} (${position.players[won]?.name ?? "empty"}) won.`;
12914
+ if (position.phase === "finished") return `Match finished${position.outcome?.label ? ` (${position.outcome.label})` : ""}.${result} Tell the human the result. Rematch only if they ask: leafarcade game rematch ${room} --revision ${revision}.`;
12786
12915
  if (position.paused) return `Paused. Stop and tell the human. Resume only when asked: leafarcade game resume ${room} --revision ${revision} (each player who paused must resume).`;
12787
12916
  if (position.phase === "waiting") {
12788
12917
  const host = position.you.seat === 0, seated = position.players.filter((p) => !p.left).length;
@@ -12795,7 +12924,7 @@ function nextStep(position, event, brief = false, server) {
12795
12924
  const play = (move2) => `leafarcade game play ${room} --revision ${revision} --action ${move2}`;
12796
12925
  if (wire?.length && !position.turn?.yours) return `You may act now (phase action outside the normal turn): ${play("'<one legal_wire_actions entry>'")}${flag}. Otherwise wait: ${wait}`;
12797
12926
  if (position.turn?.yours) {
12798
- if (wire?.length === 1 && wire[0].type === "next" && Object.keys(wire[0]).length === 1) return `The hand is over; start the next one (the deal moves on): ${play(`'{"type":"next"}'`)} --wait${flag}`;
12927
+ if (wire?.length === 1 && wire[0].type === "next" && Object.keys(wire[0]).length === 1) return `The hand is over and you deal the next one: ${play(`'{"type":"next"}'`)} --wait${flag}`;
12799
12928
  const partial = !wire && position.legal_wire_actions?.length ? "Some legal actions are listed in legal_wire_actions, but the list is not exhaustive (see game rules for melds, offers, amounts or card groups). " : "";
12800
12929
  const move2 = wire ? "'<one legal_wire_actions entry>'" : position.legal_actions_complete ? "'<one legal_actions entry>'" : "'<wire action JSON from game rules>'";
12801
12930
  const check = lineGames.includes(position.game) ? "Check your immediate win and your opponent's immediate threats (every line, including diagonals), then: " : "";
@@ -12803,12 +12932,19 @@ function nextStep(position, event, brief = false, server) {
12803
12932
  state?.penalty ? `A ${state.penalty}-card penalty is active: answer with a matching attack card, or draw takes all ${state.penalty}. ` : "",
12804
12933
  state?.wait ? `A ${state.wait}-turn wait is active: answer with a 4, or draw serves the wait (no cards drawn). ` : "",
12805
12934
  state?.blocked?.[position.you.seat] ? `You are blocked for ${state.blocked[position.you.seat]} more turn(s). ` : "",
12806
- state?.demand !== null && state?.demand !== void 0 ? `Jack demand in force: ${state.demand === 13 ? "king" : `rank ${state.demand}`}. ` : "",
12807
- state?.suit !== null && state?.suit !== void 0 ? `Ace suit in force: ${["clubs", "diamonds", "hearts", "spades"][state.suit]}. ` : ""
12935
+ state?.demand !== null && state?.demand !== void 0 ? `Jack demand in force: ${state.demand === 13 ? "king" : `rank ${state.demand}`}, or answer with a jack${state.demandTurns ? ` (${state.demandTurns} turn(s) left)` : ""}. ` : "",
12936
+ state?.suit !== null && state?.suit !== void 0 ? `Ace suit in force: ${["clubs", "diamonds", "hearts", "spades"][state.suit]}. ` : "",
12937
+ state?.phase === "drawn" && state.drawn !== null && state.drawn !== void 0 ? `You drew ${cardLabel(state.drawn)} (id ${state.drawn}): play it if it is listed, otherwise pass to keep it. ` : "",
12938
+ wire?.some((action) => action.type === "play" && Array.isArray(action.cards) && action.cards.length === (state?.hands?.[position.you.seat]?.length ?? 0) - 1) ? 'A play that leaves you one card must use its entry with "promotion":"makao", or the opponent can make you draw five. ' : ""
12808
12939
  ].join(""))(position.visible_state) : "";
12940
+ const chain = position.visible_state?.chain;
12941
+ const jumper = (position.legal_actions?.[0] ?? wire?.[0])?.from;
12942
+ const capture = chain !== null && chain !== void 0 && jumper !== void 0 ? `Your capture continues: jump again with the piece on ${String(jumper)} (one jump per call). ` : "";
12809
12943
  const mill = position.game === "mills" && wire?.length && wire.every((action) => action.type === "capture") ? "You completed a mill: remove one opposing piece (only pieces outside mills, unless all are in mills). " : "";
12810
- return `Your move. ${mill}${makao}${partial}${check}${play(move2)} --wait${flag}`;
12944
+ return `Your move. ${capture}${mill}${makao}${partial}${check}${play(move2)} --wait${flag}`;
12811
12945
  }
12946
+ const dealing = ["round", "show"].includes(String(position.visible_state?.phase));
12947
+ if (dealing && position.turn) return `The hand is over; seat ${position.turn.seat} (${position.players[position.turn.seat]?.name ?? "empty"}) deals the next one. Do not send next yourself. Wait: ${wait}`;
12812
12948
  if (event === "waiting") return `No change yet. If the other players are still playing, wait again: ${wait}. After several silent waits, stop and tell the human how to resume.`;
12813
12949
  const outOfTurn = position.legal_actions_complete || position.legal_wire_actions_complete ? "" : " Some phases allow out-of-turn actions; check game rules for this phase first.";
12814
12950
  const mover = position.turn ? ` (seat ${position.turn.seat}, ${position.players[position.turn.seat]?.name ?? "empty"}, to act)` : "";
@@ -12899,7 +13035,7 @@ async function installSkill(agent = "codex", update = false) {
12899
13035
  // packages/cli/package.json
12900
13036
  var package_default = {
12901
13037
  name: "@leafarcade/cli",
12902
- version: "0.8.0",
13038
+ version: "0.9.1",
12903
13039
  private: false,
12904
13040
  description: "Create private browser board games and play with your AI agent. Includes 53 games and an installable Codex/Claude skill.",
12905
13041
  type: "module",
@@ -13007,13 +13143,13 @@ var examples = {
13007
13143
  amazons: `--action '{"type":"move","from":30,"to":33,"amount":53}'`,
13008
13144
  "lines-of-action": `--action '{"type":"move","from":1,"to":17}'`,
13009
13145
  "last-greenhouse": `--action '{"type":"share"}'`,
13010
- euchre: "pass | trump --amount 0..3 [promotion alone] | discard/play --at CARD_ID | next. Use legal wire actions; card IDs use 24-card suit*6+rank encoding.",
13146
+ euchre: `--action '{"type":"trump","amount":2}'`,
13011
13147
  cribbage: `--action '{"type":"discard","cards":[0,1]}'`,
13012
13148
  fanorona: `--action '{"type":"move","from":13,"to":22,"amount":0}'`,
13013
- categories: '{"type":"begin","promotion":"untimed"}',
13014
- cheat: '{"type":"play","cards":[0]}',
13015
- president: '{"type":"play","cards":[1]}',
13016
- rummy: '{"type":"stock"}',
13149
+ categories: `--action '{"type":"begin","promotion":"untimed"}'`,
13150
+ cheat: `--action '{"type":"play","cards":[0]}'`,
13151
+ president: `--action '{"type":"play","cards":[1]}'`,
13152
+ rummy: `--action '{"type":"stock"}'`,
13017
13153
  "connect-four": "--column 4",
13018
13154
  chess: "--from e2 --to e4",
13019
13155
  checkers: "--from b6 --to a5",
@@ -13195,6 +13331,15 @@ function stateChanges(game, before, after, limit = 30) {
13195
13331
  const here = path.join(".");
13196
13332
  const primitive = (v) => v === null || typeof v !== "object";
13197
13333
  if (Array.isArray(a) && Array.isArray(b)) {
13334
+ if (a.length && b.length && [...a, ...b].every((v) => typeof v === "string")) {
13335
+ for (let k = 0; k < a.length; k++) {
13336
+ const kept = a.slice(k);
13337
+ if (kept.every((v, i) => v === b[i]) && b.length > kept.length) {
13338
+ out2.push(`${here}: + ${b.slice(kept.length).map((v) => show(v)).join(", ")}`);
13339
+ return;
13340
+ }
13341
+ }
13342
+ }
13198
13343
  if (a.length === b.length && a.length > 4 && a.every(primitive) && b.every(primitive)) {
13199
13344
  a.forEach((v, i) => {
13200
13345
  if (v !== b[i] && out2.length <= limit) out2.push(`${here}[${i}]: ${show(v)} \u2192 ${show(b[i])}`);
@@ -13666,6 +13811,28 @@ var scores = (position) => {
13666
13811
  const value = position.visible_state?.scores;
13667
13812
  return Array.isArray(value) && value.length && value.every((n) => typeof n === "number") ? value : null;
13668
13813
  };
13814
+ var withoutLegal = (state) => Object.fromEntries(Object.entries(state).filter(([key2]) => key2 !== "legalActions"));
13815
+ function groupedLegal(list) {
13816
+ if (list.length <= 80 || !list.every((a) => a && typeof a === "object")) return null;
13817
+ const groups = /* @__PURE__ */ new Map();
13818
+ for (const action of list) {
13819
+ const keys = Object.keys(action), last = keys.at(-1);
13820
+ if (keys.length < 2 || last === "type") return null;
13821
+ const head = Object.fromEntries(keys.slice(0, -1).map((k) => [k, action[k]])), id = JSON.stringify(head) + last;
13822
+ const group = groups.get(id) ?? { ...head, [`${last}_choices`]: [] };
13823
+ group[`${last}_choices`].push(action[last]);
13824
+ groups.set(id, group);
13825
+ }
13826
+ return [...groups.values()].map((group) => {
13827
+ const [name, choices] = Object.entries(group).find(([k]) => k.endsWith("_choices"));
13828
+ const numbers = choices.every((n) => Number.isInteger(n)) ? choices.toSorted((a, b) => a - b) : null;
13829
+ if (!numbers || numbers.length < 20 || numbers.some((n, i) => i && n !== numbers[i - 1] + 1)) return group;
13830
+ const { [name]: _list, ...rest } = group;
13831
+ void _list;
13832
+ return { ...rest, [name.replace(/_choices$/, "_range")]: [numbers[0], numbers.at(-1)] };
13833
+ });
13834
+ }
13835
+ var groupNote = "Grouped list: each entry gives the choices for its last field (*_choices, or *_range [min, max] inclusive). Send one action with a single value, e.g. column 4, not the list.";
13669
13836
  function briefPosition(position) {
13670
13837
  const turn = position.turn ? position.turn.yours ? "yours" : seatName(position, position.turn.seat) : null;
13671
13838
  const wire = "legal_wire_actions" in position ? position.legal_wire_actions ?? [] : null;
@@ -13688,14 +13855,18 @@ function briefPosition(position) {
13688
13855
  board_labeled: position.board_labeled,
13689
13856
  last_move: position.last_move,
13690
13857
  ..."changed_since_last_read" in position ? { changed_since_last_read: position.changed_since_last_read } : {},
13691
- ...wire ? { legal_wire_actions: wire, legal_wire_actions_complete: !!position.legal_wire_actions_complete } : { legal_actions: position.legal_actions, legal_actions_complete: position.legal_actions_complete },
13858
+ ...wire ? { legal_wire_actions: groupedLegal(wire) ?? wire, legal_wire_actions_complete: !!position.legal_wire_actions_complete } : { legal_actions: groupedLegal(position.legal_actions) ?? position.legal_actions, legal_actions_complete: position.legal_actions_complete },
13859
+ ...groupedLegal(wire ?? position.legal_actions) ? { legal_actions_grouped: groupNote } : {},
13692
13860
  // Card, tile and other games without a labeled board still need their seat-visible state.
13693
13861
  ..."deadline_seconds_left" in position ? { deadline_seconds_left: position.deadline_seconds_left } : {},
13694
13862
  ..."board_legend" in position ? { board_legend: position.board_legend } : {},
13695
13863
  // Only the classic convenience games are fully described by their labeled board.
13696
- ...position.legal_actions_complete && position.board_labeled || !position.visible_state ? {} : { visible_state: position.visible_state },
13864
+ // The legal moves are listed once at the top level, not again inside the state.
13865
+ ...position.legal_actions_complete && position.board_labeled || !position.visible_state ? {} : { visible_state: withoutLegal(position.visible_state) },
13697
13866
  ...position.cards_labeled ? { cards_labeled: position.cards_labeled } : {},
13698
13867
  ..."gin_deadwood" in position ? { gin_deadwood: position.gin_deadwood } : {},
13868
+ ..."sixty_six_unseen" in position ? { sixty_six_unseen: position.sixty_six_unseen } : {},
13869
+ ..."rule_notes" in position ? { rule_notes: position.rule_notes } : {},
13699
13870
  ...position.access_requests?.length ? { access_requests: position.access_requests } : {}
13700
13871
  };
13701
13872
  }
@@ -13794,8 +13965,10 @@ try {
13794
13965
  const goSize = integer(values["go-size"] ?? "9", "go-size", 9, 19);
13795
13966
  if (goSize !== 9 && goSize !== 13 && goSize !== 19) throw Error("go-size must be 9, 13 or 19.");
13796
13967
  result = await createRoom({ server, site: values.site ?? "https://leafarcade.com", game: values.game ?? "connect-four", name: values.name, capacity: values.capacity === void 0 ? void 0 : integer(values.capacity, "capacity", 2, 8), goSize, open: values.open, access: values.access, allowSpectators: !values["no-spectators"], brief: values.brief }, controller.signal);
13797
- } else if (command === "rules") result = agentRules(values.game);
13798
- else {
13968
+ } else if (command === "rules") {
13969
+ if (!values.game) throw Error("Pass --game GAME_ID (for example: leafarcade game rules --game thousand). leafarcade --help lists the game IDs.");
13970
+ result = agentRules(values.game);
13971
+ } else {
13799
13972
  const server = serviceUrl(values.server ?? defaultServer);
13800
13973
  if (command === "join" ? !positionals[1] : !values.room) throw Error(command === "join" ? 'Missing ROOMCODE. Usage: leafarcade join ROOMCODE --name "My agent"' : `Missing --room ROOMCODE. Usage: leafarcade game ${command} --room ROOMCODE. Saved rooms: leafarcade rooms.`);
13801
13974
  const room = roomCode(command === "join" ? positionals[1] : values.room);
@@ -13869,6 +14042,8 @@ try {
13869
14042
  const row = values.row === void 0 ? void 0 : integer(values.row, "row", 1, position.board_size.rows);
13870
14043
  if (revision !== position.revision) throw new ServiceError(`The room changed (now revision ${position.revision}). Read game status before playing.`, "STALE_REVISION");
13871
14044
  if (values.action && [values.column, values.row, values.from, values.to, values.promotion, values.edge, values.pit, values.swap].some((v) => v !== void 0)) throw Error("Use --action or convenience flags, not both.");
14045
+ const grouped = values.action?.match(/"(\w+)_(?:choices|range)"/);
14046
+ if (grouped) throw Error(`That entry is from a grouped list: replace the list with "${grouped[1]}" and one value from it.`);
13872
14047
  const action = values.action ? agentWireMove(position, values.action) : agentMove(position, { column, row, from: values.from, to: values.to, promotion: values.promotion, swap: values.swap, edge: values.edge === void 0 ? void 0 : integer(values.edge, "edge", 1, 1e3), pit: values.pit === void 0 ? void 0 : integer(values.pit, "pit", 1, 12) });
13873
14048
  const waitSeconds = integer(values.timeout ?? String(defaultWaitSeconds), "timeout", 1, 3600);
13874
14049
  await rpc(session, "play", { revision, action }, false, controller.signal);