@leafarcade/cli 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.mjs CHANGED
@@ -3735,6 +3735,26 @@ function checkerMoves(s) {
3735
3735
  return moves.some((m) => m.capture !== null) ? moves.filter((m) => m.capture !== null) : moves;
3736
3736
  }
3737
3737
 
3738
+ // lib/multiplayer/room-last-move.ts
3739
+ function checkerLastMove(history) {
3740
+ const [before, after] = history.slice(-2).map((key2) => key2.split(":")[0].split(",").map(Number));
3741
+ if (!before || !after || before.length !== after.length) return null;
3742
+ const to = after.findIndex((p, i) => p !== 0 && before[i] === 0);
3743
+ if (to < 0) return null;
3744
+ const from = before.findIndex((p, i) => p !== 0 && after[i] === 0 && Math.abs(p) === Math.abs(after[to]));
3745
+ return from < 0 ? null : { from, to };
3746
+ }
3747
+ function asianLastMove(history) {
3748
+ const [before, after] = history.slice(-2);
3749
+ if (!before || !after || after.mover < 0) return null;
3750
+ const cells = (key2) => key2.split(":")[1]?.split(",") ?? [];
3751
+ const a = cells(before.key), b = cells(after.key), mine = String(after.mover);
3752
+ const to = b.findIndex((cell, i) => cell.startsWith(mine) && cell !== a[i]);
3753
+ if (to < 0) return null;
3754
+ const from = a.findIndex((cell, i) => cell.startsWith(mine) && !b[i].startsWith(mine));
3755
+ return { from: from < 0 ? null : from, to };
3756
+ }
3757
+
3738
3758
  // lib/random.ts
3739
3759
  function randomStep(seed) {
3740
3760
  const next3 = seed + 1831565813 >>> 0;
@@ -3767,180 +3787,736 @@ function shuffled(items, entropy2) {
3767
3787
  function index(value, length) {
3768
3788
  return Number.isInteger(value) && value >= 0 && value < length;
3769
3789
  }
3770
- function best(scores) {
3771
- const high = Math.max(...scores), winners = scores.flatMap((s, i) => s === high ? [i] : []);
3790
+ function best(scores2) {
3791
+ const high = Math.max(...scores2), winners = scores2.flatMap((s, i) => s === high ? [i] : []);
3772
3792
  return winners.length === 1 ? winners[0] : null;
3773
3793
  }
3774
3794
 
3775
- // lib/multiplayer/oware.ts
3776
- var side = (at) => at < 6 ? 0 : 1;
3777
- function sow(pits, at) {
3778
- const board = [...pits];
3779
- let seeds = board[at], last = at;
3780
- board[at] = 0;
3781
- while (seeds > 0) {
3782
- last = (last + 1) % 12;
3783
- if (last !== at) {
3784
- board[last]++;
3785
- seeds--;
3786
- }
3787
- }
3788
- return { board, last };
3795
+ // lib/multiplayer/mills.ts
3796
+ var millLines = [
3797
+ [0, 1, 2],
3798
+ [2, 3, 4],
3799
+ [4, 5, 6],
3800
+ [6, 7, 0],
3801
+ [8, 9, 10],
3802
+ [10, 11, 12],
3803
+ [12, 13, 14],
3804
+ [14, 15, 8],
3805
+ [16, 17, 18],
3806
+ [18, 19, 20],
3807
+ [20, 21, 22],
3808
+ [22, 23, 16],
3809
+ [1, 9, 17],
3810
+ [3, 11, 19],
3811
+ [5, 13, 21],
3812
+ [7, 15, 23]
3813
+ ];
3814
+ var millEdges = millLines.flatMap(([a, b, c]) => [
3815
+ [a, b],
3816
+ [b, c]
3817
+ ]);
3818
+ function createMills() {
3819
+ return {
3820
+ board: Array(24).fill(0),
3821
+ reserves: [9, 9],
3822
+ turn: 0,
3823
+ captures: 0,
3824
+ winner: null,
3825
+ draw: false,
3826
+ history: []
3827
+ };
3789
3828
  }
3790
- function owareMoves(s) {
3791
- if (s.finished) return [];
3792
- const opponentEmpty = s.pits.every((n, i) => side(i) === s.turn || n === 0);
3793
- return s.pits.flatMap(
3794
- (n, at) => n > 0 && side(at) === s.turn && (!opponentEmpty || sow(s.pits, at).board.some((n2, i) => side(i) !== s.turn && n2 > 0)) ? [at] : []
3829
+ var linesAt = (board, at) => millLines.filter(
3830
+ (line) => line.includes(at) && line.every((i) => board[i] === board[at])
3831
+ );
3832
+ function millCaptures(s) {
3833
+ const opponents = s.board.flatMap((p, i) => p === 2 - s.turn ? [i] : []), loose = opponents.filter((at) => !linesAt(s.board, at).length);
3834
+ return loose.length ? loose : opponents;
3835
+ }
3836
+ function millMoves(s) {
3837
+ if (s.winner !== null || s.draw || s.captures) return [];
3838
+ const empty = s.board.flatMap((p, i) => !p ? [i] : []);
3839
+ if (s.reserves[s.turn]) return empty.map((to) => ({ from: null, to }));
3840
+ const own = s.board.flatMap((p, i) => p === s.turn + 1 ? [i] : []);
3841
+ return own.flatMap(
3842
+ (from) => empty.filter(
3843
+ (to) => own.length === 3 || millEdges.some(
3844
+ ([a, b]) => a === from && b === to || b === from && a === to
3845
+ )
3846
+ ).map((to) => ({ from, to }))
3847
+ );
3848
+ }
3849
+ function finishTurn(s) {
3850
+ const turn = s.turn === 0 ? 1 : 0, key2 = s.board.join(",") + ":" + turn + ":" + s.reserves.join(",");
3851
+ const next3 = {
3852
+ ...s,
3853
+ turn,
3854
+ captures: 0,
3855
+ history: [...s.history, key2]
3856
+ };
3857
+ const count = next3.board.filter((p) => p === turn + 1).length;
3858
+ if (!next3.reserves[turn] && (count < 3 || !millMoves(next3).length))
3859
+ next3.winner = s.turn;
3860
+ else next3.draw = next3.history.filter((k) => k === key2).length >= 3;
3861
+ return next3;
3862
+ }
3863
+ function playMills(s, type, to, from) {
3864
+ if (!index(to, 24) || s.winner !== null || s.draw)
3865
+ throw Error("Choose a legal point.");
3866
+ const board = [...s.board], reserves = [...s.reserves];
3867
+ if (type === "capture") {
3868
+ if (!s.captures || !millCaptures(s).includes(to))
3869
+ throw Error(
3870
+ "Capture a piece outside a mill, unless all pieces are in mills."
3871
+ );
3872
+ board[to] = 0;
3873
+ const next4 = { ...s, board, captures: s.captures - 1 };
3874
+ return next4.captures && millCaptures(next4).length ? next4 : finishTurn(next4);
3875
+ }
3876
+ if (!["place", "move"].includes(type) || !millMoves(s).some(
3877
+ (m) => m.to === to && m.from === (type === "place" ? null : from)
3878
+ ))
3879
+ throw Error(
3880
+ "Place from your reserve or move to a connected point. With three pieces you may fly."
3881
+ );
3882
+ if (type === "place") reserves[s.turn]--;
3883
+ else board[from] = 0;
3884
+ board[to] = s.turn + 1;
3885
+ const captures = Math.min(
3886
+ linesAt(board, to).length,
3887
+ board.filter((p) => p === 2 - s.turn).length
3795
3888
  );
3889
+ const next3 = { ...s, board, reserves, captures };
3890
+ return captures ? next3 : finishTurn(next3);
3891
+ }
3892
+ function millLastTurn(s) {
3893
+ const boards = s.history.slice(-2).map((key2) => key2.split(":")[0].split(",").map(Number));
3894
+ const after = boards.at(-1), before = boards.length === 2 ? boards[0] : Array(24).fill(0);
3895
+ if (!after) return null;
3896
+ const seat = Number(s.history.at(-1).split(":")[1]) === 0 ? 1 : 0;
3897
+ const to = after.findIndex((p, i) => p === seat + 1 && before[i] !== seat + 1);
3898
+ if (to < 0) return null;
3899
+ const from = before.findIndex((p, i) => p === seat + 1 && after[i] !== seat + 1);
3900
+ const captured = before.flatMap((p, i) => p === 2 - seat && after[i] !== p ? [i] : []);
3901
+ return { seat, from: from < 0 ? null : from, to, captured };
3796
3902
  }
3797
3903
 
3798
- // lib/multiplayer/catalog.ts
3799
- var multiplayerGames = [
3800
- { "id": "amazons", "name": "Amazons", "symbol": "\u265B", "players": "2 players", "description": "Claim territory with queens and arrows. Every move reshapes the board.", "rules": "Four queens per player on a 10 by 10 board. Player 1 starts at A4,D1,G1,J4; Player 2 at A7,D10,G10,J7. Move a queen any unobstructed distance horizontally, vertically or diagonally, then shoot an arrow from its destination along another unobstructed queen line. The arrow permanently blocks its square; it can land on the square just vacated. Complete both steps as one turn. A player with no queen move loses. JSON move uses numeric zero-based row-major from/to and amount for the arrow, 0\u201399. Exact legal wire actions are supplied to the active player." },
3801
- { "id": "lines-of-action", "name": "Lines of Action", "symbol": "\u25CF", "players": "2 players", "description": "Bring your scattered stones together in a tactical race for connection.", "rules": "Claude Soucie\u2019s 8 by 8 connection game. Player 1 has six stones on each horizontal edge, Player 2 six on each vertical edge, with corners empty. Move any stone in one straight line exactly as many squares as there are stones of either color on that entire line. Jump over friendly stones but never enemies. Capture an enemy at the destination; never land on a friendly stone. Connect all your remaining stones into one group using orthogonal or diagonal adjacency to win; a single stone is connected. This edition awards simultaneous connection to the mover. An immobile player passes automatically; both immobile or three repetitions with the same player to move draw. This bounded-table edition also draws after 512 moves without connection. JSON move uses numeric row-major from/to, 0\u201363. Exact legal actions are listed." },
3802
- { "id": "last-greenhouse", "name": "The Last Greenhouse", "symbol": "\u2667", "players": "2 players \xB7 cooperative", "description": "Two private journals. Five locked rooms. Save the last seed bank together.", "rules": "An original cooperative escape-room story for two keepers. Read your private journal and compare it with your partner\u2019s observation to solve each room. Share publishes your current observation to both players; Pass hands the controls to your partner; Choose selects setting at 0\u20133 from the visible room options. Every action hands over the controls. Wrong answers are safe to retry and are included in the legal-action list. Complete the atrium, prism gallery, seed archive, pump room and dawn canopy together. Only your own private journal and shared notes are projected; spectators see no private journal. Local handoff conceals journals between turns. No competitive rating or Quick Play." },
3803
- { id: "euchre", name: "Euchre", symbol: "J", players: "4 players \xB7 2 teams", description: "Call trump, trust your partner, or take all five tricks alone.", rules: "Four players in opposite-seat partnerships. A 24-card deck uses 9 through ace. Each player receives five cards and one card is turned up. Starting left of dealer, order the turned suit or pass. If ordered, the dealer picks up the upcard and discards one card face down. After four passes, call any other suit; four more passes produce a fresh deal with the next dealer. A caller may go alone: their partner sits out. Left of dealer leads, skipping a sitting partner. Follow suit when possible. The trump jack is highest, followed by the other jack of the same color (the left bower, which belongs to trump), then A,K,Q,10,9 of trump. Other suits rank A,K,Q,J,10,9. Trick winner leads. Makers score 1 for three or four tricks, 2 for all five together, or 4 for all five alone. Defenders score 2 if makers take fewer than three. First team to 10 wins. No stick-the-dealer, joker or defensive lone hands. Hands and kitty remain private. Legal actions are enumerated for the active seat." },
3804
- { id: "cribbage", name: "Cribbage", symbol: "121", players: "2 players", description: "Choose your crib. Peg the count. Race to 121.", rules: "Two-player six-card cribbage with a standard deck. Aces are low. Each player discards two cards face down to the dealer's crib; the starter is revealed after both discard. A starter jack gives the dealer two points. Non-dealer leads pegging: play one card without exceeding 31, or say Go only when no card fits. Fifteen and thirty-one score two; consecutive pairs, triples and quads score two, six and twelve; the longest trailing run scores its length. The last card before a reset scores one except at 31. After a Go/reset the opponent of the last player leads if holding cards. Count non-dealer's hand, dealer's hand, then crib: fifteens, pairs, longest runs with multiplicities, four/five-card hand flushes, five-card-only crib flush, and one for a jack matching the starter's suit. Counting is automatic; no muggins. Dealer alternates. First to reach 121 at any scoring event wins immediately. Hidden cards and crib stay private until counting; spectators see only public cards. Exact legal JSON actions are supplied in visible_state.legalActions for the active player." },
3805
- { id: "fanorona", name: "Fanorona", symbol: "\u2194", players: "2 players", description: "Approach or withdraw. One move can capture a whole line.", rules: "Two players on the traditional 9 by 5 connected-point board, twenty-two stones each. Player 1 starts. Move one stone along a drawn line to an adjacent empty point. Captures are compulsory when available: approach captures the consecutive enemy line beyond the destination; withdrawal captures the consecutive enemy line behind the starting point. Choose one when both apply. After a capture you may continue with the same stone or Stop. A capture sequence cannot revisit a point or repeat the immediately preceding direction. A non-capturing move is allowed only when no capture exists. Capture every opposing stone or leave the opponent without a move to win. This implementation draws at three repetitions of the same position with the same player to move, or eighty completed turns without a capture. Intersections are zero-based row-major: top-left 0, bottom-right 44. JSON move uses from/to plus amount 0 for approach or a quiet move, 1 for withdrawal; stop ends an optional continuation. Exact legal actions are supplied in visible_state.legalActions." },
3806
- { "id": "categories", "name": "Categories", "symbol": "A\u2013Z", "players": "2\u20138 players", "description": "One letter. Six categories. Race your friends to a unique answer.", "rules": "Five English-language rounds with Country, City, Animal, Food, Occupation and Object. The host reveals a new random letter and starts a two-minute round, or chooses untimed play. Submit one locked answer sheet; blank answers are allowed. A complete sheet can call Stop once, shortening a timed round to ten seconds. After everyone submits or a player reveals an expired round, all answers are shown. Unsubmitted answers are blank. Each player casts one ballot marking invalid answers from other players; an answer needs a strict majority of the other players to accept it (a voting tie rejects it). Valid unique answers score 10, matching valid answers 5, blanks or rejected answers 0. Highest total after five rounds wins; equal totals draw. Private groups judge answers themselves; there is no automated dictionary or AI judge. Local play uses untimed private handoffs." },
3807
- { "id": "cheat", "name": "Cheat", "symbol": "?!", "players": "3\u20136 players", "description": "Play face down. Tell a convincing story. Call the bluff.", "rules": "Deal all 52 cards clockwise. Player 1 claims aces, then claims cycle through 2 to king and back to ace. On your turn place one to four cards face down, claiming the required rank; bluffing is allowed. Before the next play, any other player may challenge, or the next player may accept. A challenge reveals only the latest cards: if any is the wrong rank the claimant takes the whole pile; otherwise the challenger takes it. Accepted cards stay hidden. The next clockwise player then takes their turn with the next required rank. An empty hand wins only after its final claim is accepted or survives a challenge. This table uses an explicit challenge window without a reaction timer." },
3808
- { "id": "president", "name": "President", "symbol": "3\u21922", "players": "3\u20136 players", "description": "Beat the group. Pass at the right moment. Finish first.", "rules": "A single deal with a standard 52-card deck, all cards dealt clockwise. Ranks rise from 3 through ace, with 2 highest. The holder of 3 of clubs opens with a group containing it. Lead one to four equal-rank cards; following plays must contain the same number of cards at a strictly higher rank. You may pass, but cannot re-enter until that pile clears. When all other active players pass, the last player to play leads again; if they have finished, the next active player leads. Finishers leave play and receive places in order. Play continues until one player remains; the first finisher is President. No jokers, suit tie-breaks, special twos, revolution or between-deal tribute." },
3809
- { "id": "rummy", "name": "Classic Rummy", "symbol": "777", "players": "2\u20134 players", "description": "Draw a card. Build sets and runs. Empty your hand.", "rules": "One deal, no jokers. Two players receive ten cards each; three or four receive seven. Aces are low only. Draw one card from the stock or take the top discard. Then optionally meld sets of three or four equal ranks, or runs of three or more consecutive cards of one suit. You may add held cards to either end of any public run or complete a public set. Keep at least one card and end your turn by discarding; a taken upcard cannot be discarded immediately. First to discard their last card wins and scores the total value of cards held by opponents: ace 1, faces 10, other cards face value. Recycle discards beneath the top card when stock runs out. If stock and recyclable discards are both empty and the next player chooses stock, the deal ends; lowest remaining card value wins, with ties drawn. No initial meld threshold, knock or Rummy 500 scoring." },
3810
- { id: "tic-tac-toe", name: "Tic-tac-toe", symbol: "XO", players: "2 players", description: "Nine squares. Three in a row. Your next quick match.", rules: "X starts on a 3 by 3 board. Take turns placing your mark on an empty square. Three marks in a horizontal, vertical or diagonal line wins. A full board without a winner draws." },
3811
- { id: "international-draughts", name: "International Draughts", symbol: "\u265B", players: "2 players", description: "Twenty pieces. Flying kings. Capture the longest chain.", rules: "White starts on a 10\xD710 board with twenty pieces per side. Men move one diagonal square forward and capture in either direction. Kings slide diagonally and land on any empty square beyond a captured piece. Captures are compulsory: choose a sequence taking the greatest number of pieces. Captured pieces remain as blockers until the entire sequence ends. A man becomes a king only if its final square is on the promotion row. No pieces or no legal move loses. Threefold repetition, twenty-five successive king moves per player without captures or man moves, and the official sixteen/five-move king endgame limits draw automatically." },
3812
- { id: "atomic-chess", name: "Atomic Chess", symbol: "\u265C", players: "2 players", description: "Every capture explodes. Protect your king from the blast.", rules: "Standard chess setup and moves, with explosive captures: the capturing piece, captured piece and adjacent non-pawns disappear. Adjacent pawns survive. En passant explodes at the capturing pawn\u2019s destination. A move that explodes your own king is illegal. Kings cannot capture, but may stand next to one another. Explode the opposing king or deliver checkmate to win. Stalemate, insufficient material, threefold repetition or fifty moves without a pawn move or capture draws. Castling and all four promotions are supported." },
3813
- { id: "three-check", name: "Three-check", symbol: "\u2654", players: "2 players", description: "Give three checks to win. Every threat counts.", rules: "Standard chess rules, plus a second way to win: give your opponent check three times. A double check counts as one checking move. Checkmate also wins. Each player\u2019s checks are shown beside the board. Standard castling, en passant and all four promotions apply. Stalemate, insufficient material, threefold repetition or fifty moves without a pawn move or capture draws." },
3814
- { id: "connect-four", name: "Connect Four", symbol: "\u25CF", players: "2 players", description: "Drop a piece. Connect four. Watch the next column.", rules: "Two players alternate dropping pieces into one of seven columns on a six-row board. Each piece falls to the lowest empty space. Four or more pieces of your color in a horizontal, vertical or diagonal line wins. A full board without a winner draws." },
3815
- { id: "dots-boxes", name: "Dots & Boxes", symbol: "\u25A6", players: "2 players", description: "Complete a square. Claim another turn.", rules: "Players alternate drawing one unused edge on a 3 \xD7 3 box board. Completing the fourth side of a box claims it and gives another turn. An edge can complete two boxes. When all nine boxes are owned, most boxes wins. All information is public." },
3816
- { id: "battleships", name: "Battleships", symbol: "\u2693", players: "2 players", description: "Hide your fleet. Read the hits. Find the last ship.", rules: "Each player secretly places five straight ships of lengths 5, 4, 3, 3 and 2 on a 10\xD710 grid. Ships may touch but cannot overlap or leave the grid. Tap a starting square to place the next ship; switch horizontal/vertical or arrange automatically, then lock the fleet. Player 1 sets up first, then Player 2. Player 1 fires first. Take exactly one shot per turn, including after a hit. The target grid marks hits and misses, and reveals a ship when every square is hit. Sink all five enemy ships to win. On one device, hide your fleet before passing it. Online, only your own ships and already-known shot results are sent to your browser." },
3817
- {
3818
- id: "thousand",
3819
- name: "Thousand",
3820
- symbol: "1000",
3821
- players: "2\u20134 players",
3822
- description: "Bid your hand. Marry a new trump.",
3823
- 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."
3824
- },
3825
- {
3826
- id: "binokel",
3827
- name: "Binokel",
3828
- symbol: "AA",
3829
- players: "4 players",
3830
- description: "Four copies. Two partners. One contract.",
3831
- rules: "Partners sit opposite. Four copies of jack, queen, king, ten and ace in every suit form an 80-card deck; twenty cards each. Auction starts at 50, rises by one below 60 and by five thereafter. Passed players leave the auction. If the first three pass, dealer plays 50. Declarer chooses trump only from a held queen-king pair; no pair loses the contract. The declaring partnership needs at least 20 meld points. Melds score automatically and their cards are revealed: aces/kings/queens/jacks around; ordinary and royal marriages; spade queen with diamond jack; and a trump A-10-K-Q-J run. Duplicate melds use the full single/double/triple/quadruple table. A royal marriage already in a run is not counted twice. Follow suit and beat the current winner when possible; when void, trump and overtrump when possible. Equal copies favor the first played. Each ace, ten or king taken earns one; last trick earns two. At least 20 trick points are required to score a hand, and at least 20 meld points to add melds. Declarers reaching their bid score their total, otherwise lose the bid. Opponents score qualifying totals. First team to 500 wins; if both reach 500, the declaring team wins."
3832
- },
3833
- {
3834
- id: "skat",
3835
- name: "Skat",
3836
- symbol: "J",
3837
- players: "3 players",
3838
- description: "Win the bidding duel. Play alone against two.",
3839
- rules: "Thirty-two cards, seven through ace. Middlehand speaks values to forehand, who holds or passes; rearhand then speaks to the survivor. If both speakers pass without a bid, forehand plays 18 or passes out. Declarer picks up the two-card skat and discards two, or plays from hand. Choose a suit, Grand (only jacks trump) or Null (lose every trick). Jacks rank clubs, spades, hearts, diamonds above suit trumps. Other suit/Grand cards rank A-10-K-Q-9-8-7; Null ranks A-K-Q-J-10-9-8-7. Follow the effective suit; jacks belong to trump in suit/Grand games. Forehand always leads. Suit/Grand need 61 card points, including skat; Null ends in loss on declarer\u2019s first trick. Value multiplies suit base (diamonds 9, hearts 10, spades 11, clubs 12, Grand 24) by matadors, game, hand and Schneider/Schwarz/open levels. Hand games can announce 90 points or all tricks. Null values are 23, hand 35, open 46, open hand 59. Lost or overbid games subtract twice their value, rounding overbids up to a sufficient base multiple. Highest score after eighteen hands wins. No Ramsch, Kontra or tournament bonus variant."
3840
- },
3841
- {
3842
- id: "baska",
3843
- name: "Baska",
3844
- symbol: "DQ",
3845
- players: "4 players",
3846
- description: "Find your hidden partner in sixteen cards.",
3847
- rules: "The red (heart-ace) variant, sixteen cards: ace, ten, queen and jack in each suit. Trump strength: heart ace, heart ten, queens clubs-spades-hearts-diamonds, jacks in that order, diamond ace, diamond ten. Other cards keep their printed suits. Old partners hold club and spade queens; holding both creates a silent solo. Wedding pairs that holder with the first available club, spade or heart jack holder. Auction offers Grand, Zolo and their all-trick Du contracts, with alternating counters up to redouble. Grand has only jack trumps. Four tens, aces or queens automatically claim Zolo Bieda, Master or Baska. Follow suit and beat the winner when possible. Card points are A11, 10=10, Q3, J2. Old side needs 53; with 52\u201352, the side that last countered loses. Ordinary/wedding stakes rise for failure to reach 26 (young) or 27 (old), or failure to take a trick; old losses cost twice. Silent solo stakes four, Grand/Zolo five, Du/honor ten, all doubled by counters. Paired winners gain the stake each; a solo winner gains it from each opponent; highest total after sixteen hands wins. Black and three-tens variants are outside this table."
3848
- },
3849
- {
3850
- id: "canasta",
3851
- name: "Canasta",
3852
- symbol: "77",
3853
- players: "2 or 4 players",
3854
- description: "Build seven-card melds. Break a frozen pile.",
3855
- rules: "Two 54-card decks, including four jokers. Two players get fifteen cards and draw two; four players get eleven and draw one, sharing melds with the opposite partner. Meld matching natural ranks with at least two natural cards and at least three total; at most three wild twos/jokers, with no more wild than natural. Seven or more cards form a canasta. Opening melds total 15 below zero score, 50 below 1500, 90 below 3000, then 120. Take the entire discard pile only by immediately melding its top; a frozen pile requires two matching natural cards held. Wild cards block pickup and freeze the pile; black threes block pickup except a final meld. Red threes are set aside and replaced when drawn from stock. Black threes meld only when going out. End a turn by discarding one. Going out requires two canastas with two players, one with partners; premature going out loses the match. Natural canastas earn 500, mixed 300; going out 100, concealed opening 200. Red threes earn 100 each plus 400 for all four, negative without a meld. Melded cards score positive, held cards negative. Card values: joker50, A/2=20, 8\u2013K=10, others5. Highest score reaching 5000 wins. Optional partner permission is not required."
3856
- },
3857
- {
3858
- id: "bridge",
3859
- name: "Bridge",
3860
- symbol: "NT",
3861
- players: "4 players",
3862
- description: "Bid a contract. Plan two hands together.",
3863
- rules: "Partners sit opposite. A sixteen-board match uses standard duplicate contract scores and the rotating vulnerability cycle. Each player has thirteen cards. Bids increase by level and strain: clubs, diamonds, hearts, spades, no trump. Three passes after a bid end the auction; four opening passes pass out the board. Opponents may double; the declaring side may redouble. Declarer is the first player in the winning partnership to bid the final strain. The player to declarer\u2019s left leads. Dummy is exposed after that lead and declarer controls its plays. Follow suit when possible; highest trump or lead-suit card wins. Contract, game, slam, overtrick and undertrick scores account for doubling and vulnerability. Highest net team score after sixteen boards wins. This table uses raw duplicate scores; Kurnik\u2019s HCP-adjusted IMP table, conventions, claims and tournament director rulings are outside this variant."
3864
- },
3865
- {
3866
- id: "polish-hearts",
3867
- name: "Polish Hearts",
3868
- symbol: "\u2665",
3869
- players: "3\u20134 players",
3870
- description: "Avoid seven penalties, then win points back.",
3871
- rules: "Seven penalty hands: each trick costs 20; each heart 20; queen 60; king or jack 30; heart king 150; seventh and last tricks 75 each; the seventh hand combines all penalties. Follow suit, with no obligation to beat the current winner. Hearts cannot lead while another suit is held in the heart, heart-king and combined hands. Next, each player deals a trump hand and chooses trump from the first five cards, before the rest are dealt; each trick earns 25. Finally, build four suit columns around the dealer\u2019s opening rank; extend a column by one rank, or start another at that opening rank. Play whenever possible. First out earns 800, second 500. Three-player games omit the club two, use six cards for the trump choice, cost 15 per trick, 140 for the heart king and 70 per special trick, earn 20 per trump trick and award 790/485 for the final layout. Highest total wins."
3872
- },
3873
- {
3874
- id: "durak",
3875
- name: "Durak",
3876
- symbol: "\u2660",
3877
- players: "2\u20134 players",
3878
- description: "Beat the attack. Empty your hand.",
3879
- rules: "Individual throw-in Durak with 36 cards, six through ace. Deal six each; the bottom stock card shows trump and is drawn last. The lowest dealt trump holder opens. Attack with one card, then add ranks already on the table, including defended ranks. The defender beats a card with a higher card of the same suit or any trump over a nontrump. Attacks are capped at six cards or the defender\u2019s starting hand size. The main attacker has first priority; finishing offers other attackers a clockwise turn. Taking opens a final throw-in window, then the defender collects all table cards and skips their next attack. Successful defense discards the table and the defender attacks next. Refill the main attacker first, other players clockwise and defender last. Empty hands leave once the stock runs out. The last player holding cards loses; simultaneous empty hands draw. This table has no transfer attacks, teams or optional trump-six exchange."
3880
- },
3881
- {
3882
- id: "makao",
3883
- name: "Makao",
3884
- symbol: "Q",
3885
- players: "2\u20134 players",
3886
- description: "Match suits and ranks. Counter the demand.",
3887
- rules: "Five cards each from a 52-card deck. Match the pile\u2019s suit or rank, or play a queen on an ordinary card; any ordinary card fits a queen. Groups of the same rank can be played together, with the first card matching. Twos and threes add draw penalties; the heart king adds five to the next player, the spade king to the previous player. Penalties can be stacked with any fighting card. Fours add waiting turns, countered with fours. Jacks request an ordinary rank for one complete cycle, or cancel a request; aces request a suit for the next player. Queens do not override demands. A normal draw offers only that new card for immediate play. Say Makao with the checkbox when one card remains; the next player can catch a missed declaration for five cards. When both stock and reusable discards run out, every player returns a random half of their hand, rounded down. The first empty hand wins. No jokers or sequence-chain variant."
3888
- },
3889
- {
3890
- id: "three-five-eight",
3891
- name: "3-5-8",
3892
- symbol: "358",
3893
- players: "3 players",
3894
- description: "Meet your quota. Exchange for stronger cards.",
3895
- rules: "Eighteen hands with a 52-card deck. Sixteen cards each and a four-card kitty. Dealer targets eight tricks, the next player five, the third three. Each player chooses each of the six contracts once on their own deals: four trump suits, no trump and no tricks. Discard four before taking the kitty. Follow suit; if void, play trump when held. Highest trump or highest lead suit wins. Ordinary hands score tricks minus quota; no-tricks reverses that score. Before subsequent hands, players above their previous quota exchange one unwanted card per surplus trick with players below quota; the receiver automatically returns their highest card in its suit. Higher current quotas exchange first. Highest total after eighteen hands wins. This uses the eighteen-hand contract variant rather than the twelve-trick sudden-win variant."
3896
- },
3897
- {
3898
- id: "property-trading",
3899
- name: "Property Trading",
3900
- symbol: "\u2302",
3901
- players: "2\u20134 players",
3902
- description: "Build a garden estate. Bargain for a complete street.",
3903
- rules: "An original forty-space estate board with classic property trading rules. Each player starts with 1,500 points; passing Start earns 200. Roll two dice, buy unowned properties or auction them to all players. Doubles earn another roll; three consecutive doubles send you to jail. Collect rents, complete color groups and build evenly using the finite bank of 32 houses and 12 hotels. Mortgage unbuilt properties for half their price; redeem for 60%. Trade unbuilt properties and cash by an explicit offer and acceptance; receiving a mortgaged property costs 10% of its price. Jail ends with doubles, a release card or a payment of 50; the third failed roll forces payment. Income tax is 200 or 10% of estate value; luxury tax is 75. Free Park pays nothing. Resolve debts by selling buildings or mortgaging assets; bankruptcy transfers an estate to the creditor, or auctions it for bank debts. The last solvent player wins. Free points only."
3904
- },
3905
- {
3906
- id: "draw-guess",
3907
- name: "Draw & Guess",
3908
- symbol: "\u270E",
3909
- players: "2\u201320 players",
3910
- description: "A private word. A shared sketch.",
3911
- rules: "Take turns drawing one of three private English prompts. You have two minutes. Other players guess while the drawing updates. A correct guess earns 1\u201345 points based on remaining time, and the drawer earns up to five more, capped at 45, for that guess. Each player can score once per round. If everyone guesses, the round ends early. No correct guesses costs the drawer ten. First to 100 ends the match; highest score wins. Draw pictures rather than writing the word. Local play offers a player switch and hides the prompt from guessers."
3912
- },
3913
- {
3914
- id: "letter-tiles",
3915
- name: "Letter Tiles",
3916
- symbol: "A",
3917
- players: "2\u20134 players",
3918
- description: "Build a crossword. Make every letter count.",
3919
- rules: "American English Scrabble-style home scoring on the standard 15 \xD7 15 premium-square layout, with 100 tiles and seven per rack. First word: at least two letters covering the center double-word square. Place connected tiles in one row or column without gaps; every new or crossed word scores, including existing letters. English values: A E I L N O R S T U 1; D G 2; B C M P 3; F H V W Y 4; K 5; J X 8; Q Z 10. Blanks stay zero and keep their chosen letter. DL/TL multiply each newly placed letter; DW/TW multiply its whole word after letter bonuses. New premiums apply to every word using that tile, once on its placement turn; multiple word multipliers multiply together. Using seven new tiles adds 50 after all multipliers. Next player accepts or challenges against our ENABLE English word-game list with QI/ZA additions; accepted words differ from official NWL/Collins lists. Invalid words return the tiles and lose the proposer\u2019s turn; a failed challenge loses the challenger\u2019s next turn. Exchange any number of rack tiles as often as needed, using a turn, when at least seven remain in the bag. Everyone passing twice ends play, as does emptying a rack with an empty bag. Deduct unplayed tiles; the player going out also receives opponents\u2019 deductions. Highest final score wins; a final tie favors the higher pre-adjustment score. Earlier rounds retain their original rules until restarted."
3920
- },
3921
- {
3922
- id: "pan",
3923
- name: "Pan",
3924
- symbol: "\u2667",
3925
- players: "2\u20134 players",
3926
- description: "Shed your cards before the pile turns.",
3927
- rules: "A 24-card deck, nine through ace. The holder of the heart nine opens with that card or all four nines. Play one card or all four of a rank, no lower than the pile top. With only the heart nine on the pile, the other three nines can be played together. Instead of playing, take up to three top cards, leaving the heart nine. Empty hands leave the round. The last player receives a P, then A, then N; three letters loses the match. A last player who can immediately shed one or four aces draws the round. This table uses the ordinary clockwise variant."
3928
- },
3929
- {
3930
- id: "gin-rummy",
3931
- name: "Gin Rummy",
3932
- symbol: "\u2666",
3933
- players: "2 players",
3934
- description: "Build runs and sets. Know when to knock.",
3935
- rules: "Ten cards each. The dealer\u2019s opponent may take the opening upcard; if passed, the dealer may take it. If both pass, draw from stock. Draw one, then discard another; you cannot return the upcard just drawn. Meld sets of three/four equal ranks or same-suit runs of at least three, ace low. Knock when the remaining deadwood is at most ten: ace one, face cards ten. Melds and opponent layoffs are optimized automatically. Gin earns deadwood difference plus 25; an equal or lower opponent deadwood undercuts for 25 plus difference. At 100, the game ends with game and hand bonuses. Two cards left in stock cancels the round with the same dealer."
3936
- },
3937
- {
3904
+ // lib/multiplayer/cribbage.ts
3905
+ var cribbageLabel = (c) => ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"][c % 13] + ["\u2663", "\u2666", "\u2665", "\u2660"][Math.floor(c / 13)];
3906
+
3907
+ // lib/multiplayer/dots-boxes.ts
3908
+ function boxEdges(size, box) {
3909
+ const x = box % size, y = Math.floor(box / size), offset = size * (size + 1);
3910
+ return [
3911
+ y * size + x,
3912
+ (y + 1) * size + x,
3913
+ offset + y * (size + 1) + x,
3914
+ offset + y * (size + 1) + x + 1
3915
+ ];
3916
+ }
3917
+
3918
+ // lib/multiplayer/trick-cards.ts
3919
+ var cardSuit = (card2) => Math.floor(card2 / 13);
3920
+ var cardRank = (card2) => card2 % 13 + 2;
3921
+ var cardLabel = (card2) => `${["", "", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"][cardRank(card2)]}${["\u2663", "\u2666", "\u2665", "\u2660"][cardSuit(card2)]}`;
3922
+ var penalty = (card2) => cardSuit(card2) === 2 ? 1 : card2 === 49 ? 13 : 0;
3923
+ function first(s) {
3924
+ return s.variant === "spades" ? s.dealer : (s.dealer + 1) % s.hands.length;
3925
+ }
3926
+ function deal(s, entropy2) {
3927
+ const count = s.hands.length, size = s.variant === "oh-hell" ? Math.max(1, (count === 4 ? 13 : 17) - s.round) : 13, deck = shuffled(
3928
+ Array.from({ length: 52 }, (_, i) => i),
3929
+ entropy2
3930
+ );
3931
+ const hands = Array.from(
3932
+ { length: count },
3933
+ (_, i) => deck.slice(i * size, (i + 1) * size).sort((a, b) => a - b)
3934
+ );
3935
+ const t = {
3936
+ ...s,
3937
+ size,
3938
+ hands,
3939
+ stock: deck.slice(count * size),
3940
+ trick: [],
3941
+ lastTrick: [],
3942
+ tricks: Array(count).fill(0),
3943
+ penalties: Array(count).fill(0),
3944
+ bids: Array(count).fill(null),
3945
+ blind: Array(count).fill(false),
3946
+ blindAsked: Array(count).fill(false),
3947
+ selected: Array.from({ length: count }, () => []),
3948
+ passed: Array(count).fill(false),
3949
+ broken: false,
3950
+ winner: null,
3951
+ result: ""
3952
+ };
3953
+ t.trump = s.variant === "spades" ? 3 : s.variant === "oh-hell" && s.round > 0 ? cardSuit(t.stock[0]) : null;
3954
+ t.phase = s.variant === "hearts" ? s.round % 4 === 3 ? "play" : "pass" : s.variant === "spades" && Math.abs(s.scores[0] - s.scores[1]) >= 200 ? "blind" : "bid";
3955
+ t.turn = s.variant === "hearts" ? t.phase === "play" ? hands.findIndex((h) => h.includes(0)) : 0 : first(t);
3956
+ return t;
3957
+ }
3958
+ function createTrickCards(variant, count, entropy2) {
3959
+ if (!index(count - 2, 3) || variant !== "oh-hell" && count !== 4)
3960
+ throw Error("Choose the required player count.");
3961
+ return deal(
3962
+ {
3963
+ variant,
3964
+ turn: 0,
3965
+ dealer: Math.floor(draws(entropy2, 1)[0] * count),
3966
+ round: 0,
3967
+ size: 13,
3968
+ phase: "bid",
3969
+ hands: Array.from({ length: count }, () => []),
3970
+ stock: [],
3971
+ trick: [],
3972
+ lastTrick: [],
3973
+ tricks: [],
3974
+ penalties: [],
3975
+ bids: [],
3976
+ blind: [],
3977
+ blindAsked: [],
3978
+ selected: [],
3979
+ passed: [],
3980
+ scores: Array(variant === "spades" ? 2 : count).fill(0),
3981
+ bags: [0, 0],
3982
+ trump: null,
3983
+ broken: false,
3984
+ winner: null,
3985
+ result: ""
3986
+ },
3987
+ entropy2
3988
+ );
3989
+ }
3990
+ function trickLegalCards(s) {
3991
+ if (s.phase !== "play") return [];
3992
+ const hand2 = s.hands[s.turn].filter((c) => c !== null), lead = s.trick[0];
3993
+ if (lead) {
3994
+ const suited = hand2.filter((c) => cardSuit(c) === cardSuit(lead.card));
3995
+ if (suited.length) return suited;
3996
+ if (s.variant === "hearts" && s.tricks.every((n) => n === 0)) {
3997
+ const clean = hand2.filter((c) => !penalty(c));
3998
+ if (clean.length) return clean;
3999
+ }
4000
+ return hand2;
4001
+ }
4002
+ if (s.variant === "hearts") {
4003
+ if (s.tricks.every((n) => n === 0)) return hand2.includes(0) ? [0] : [];
4004
+ if (!s.broken) {
4005
+ const clean = hand2.filter((c) => cardSuit(c) !== 2);
4006
+ if (clean.length) return clean;
4007
+ }
4008
+ }
4009
+ if (s.variant === "spades" && !s.broken) {
4010
+ const clean = hand2.filter((c) => cardSuit(c) !== 3);
4011
+ if (clean.length) return clean;
4012
+ }
4013
+ return hand2;
4014
+ }
4015
+ function finish(s) {
4016
+ if (s.variant === "hearts") {
4017
+ s.scores = s.scores.map((n, i) => n + s.penalties[i]);
4018
+ s.result = `Round penalties: ${s.penalties.join(" \xB7 ")}`;
4019
+ if (s.scores.some((n) => n >= 100)) {
4020
+ s.phase = "finished";
4021
+ s.winner = best(s.scores.map((n) => -n));
4022
+ } else s.phase = "round";
4023
+ } else if (s.variant === "spades") {
4024
+ const gains = [0, 0];
4025
+ for (const team2 of [0, 1]) {
4026
+ const seats2 = [team2, team2 + 2], bid = seats2.reduce((n, i) => n + s.bids[i], 0), taken = seats2.filter((i) => s.bids[i] !== 0).reduce((n, i) => n + s.tricks[i], 0);
4027
+ gains[team2] = taken >= bid ? bid * 10 + Math.max(0, taken - bid) : -bid * 10;
4028
+ const bags = Math.max(0, taken - bid) + seats2.filter((i) => s.bids[i] === 0).reduce((n, i) => n + s.tricks[i], 0);
4029
+ s.bags[team2] += bags;
4030
+ gains[team2] -= 100 * Math.floor(s.bags[team2] / 10);
4031
+ s.bags[team2] %= 10;
4032
+ for (const i of seats2)
4033
+ if (s.bids[i] === 0)
4034
+ gains[team2] += (s.tricks[i] === 0 ? 1 : -1) * (s.blind[i] ? 200 : 100);
4035
+ }
4036
+ s.scores = s.scores.map((n, i) => n + gains[i]);
4037
+ s.result = `Team 1 & 3: ${gains[0]} \xB7 Team 2 & 4: ${gains[1]}`;
4038
+ if (s.scores.some((n) => n >= 500 || n <= -200)) {
4039
+ s.phase = "finished";
4040
+ s.winner = best(s.scores);
4041
+ } else s.phase = "round";
4042
+ } else {
4043
+ s.scores = s.scores.map(
4044
+ (n, i) => n + (s.bids[i] === s.tricks[i] ? 10 + s.tricks[i] : 0)
4045
+ );
4046
+ s.result = `Tricks taken: ${s.tricks.join(" \xB7 ")}`;
4047
+ const rounds = (s.hands.length === 4 ? 13 : 17) + s.hands.length - 1;
4048
+ if (s.round + 1 >= rounds) {
4049
+ s.phase = "finished";
4050
+ s.winner = best(s.scores);
4051
+ } else s.phase = "round";
4052
+ }
4053
+ s.turn = first(s);
4054
+ }
4055
+ function playTrickCards(s, type, at, amount, entropy2) {
4056
+ if (s.phase === "finished") throw Error("This match is finished.");
4057
+ if (type === "next" && s.phase === "round")
4058
+ return deal(
4059
+ { ...s, round: s.round + 1, dealer: (s.dealer + 1) % s.hands.length },
4060
+ entropy2
4061
+ );
4062
+ const t = structuredClone(s), seat = s.turn, count = s.hands.length;
4063
+ if (s.phase === "pass") {
4064
+ if (type === "select" && at !== void 0 && s.hands[seat].includes(at)) {
4065
+ const selected = t.selected[seat];
4066
+ if (selected.includes(at))
4067
+ t.selected[seat] = selected.filter((c) => c !== at);
4068
+ else if (selected.length < 3) selected.push(at);
4069
+ else throw Error("Select three cards.");
4070
+ return t;
4071
+ }
4072
+ if (type !== "pass" || t.selected[seat].length !== 3)
4073
+ throw Error("Choose three cards to pass.");
4074
+ t.passed[seat] = true;
4075
+ if (t.passed.every(Boolean)) {
4076
+ const offset = [1, 3, 2][s.round % 4];
4077
+ t.hands = t.hands.map(
4078
+ (h, i) => h.filter((c) => !t.selected[i].includes(c))
4079
+ );
4080
+ t.selected.forEach(
4081
+ (cards, i) => t.hands[(i + offset) % 4].push(...cards)
4082
+ );
4083
+ t.hands.forEach((h) => h.sort((a, b) => a - b));
4084
+ t.selected = t.selected.map(() => []);
4085
+ t.phase = "play";
4086
+ t.turn = t.hands.findIndex((h) => h.includes(0));
4087
+ } else t.turn = t.passed.findIndex((n) => !n);
4088
+ return t;
4089
+ }
4090
+ if (s.phase === "blind") {
4091
+ const eligible = s.scores[1 - seat % 2] - s.scores[seat % 2] >= 200;
4092
+ if (type === "blind" && eligible) {
4093
+ t.bids[seat] = 0;
4094
+ t.blind[seat] = true;
4095
+ } else if (type !== "look")
4096
+ throw Error("Choose blind nil or look at your cards.");
4097
+ t.blindAsked[seat] = true;
4098
+ if (t.blindAsked.every(Boolean)) {
4099
+ t.phase = "bid";
4100
+ t.turn = first(t);
4101
+ while (t.bids[t.turn] !== null) t.turn = (t.turn + 1) % count;
4102
+ } else t.turn = (seat + 1) % count;
4103
+ return t;
4104
+ }
4105
+ if (s.phase === "bid") {
4106
+ if (type !== "bid" || amount === void 0 || !index(amount, s.size + 1))
4107
+ throw Error("Choose a legal bid.");
4108
+ if (s.variant === "oh-hell" && s.bids.filter((b) => b !== null).length === count - 1 && s.bids.reduce((n, b) => n + (b ?? 0), 0) + amount === s.size)
4109
+ throw Error("The last bid cannot make the total equal the hand size.");
4110
+ t.bids[seat] = amount;
4111
+ if (t.bids.every((b) => b !== null)) {
4112
+ t.phase = "play";
4113
+ t.turn = first(t);
4114
+ } else {
4115
+ t.turn = (seat + 1) % count;
4116
+ while (t.bids[t.turn] !== null) t.turn = (t.turn + 1) % count;
4117
+ }
4118
+ return t;
4119
+ }
4120
+ if (s.phase === "moon") {
4121
+ if (type !== "moon" || ![0, 1].includes(amount ?? -1))
4122
+ throw Error("Choose how to score the moon.");
4123
+ t.penalties = t.penalties.map(
4124
+ (_, i) => amount === 0 ? i === seat ? -26 : 0 : i === seat ? 0 : 26
4125
+ );
4126
+ finish(t);
4127
+ return t;
4128
+ }
4129
+ if (type !== "play" || at === void 0 || !trickLegalCards(s).includes(at))
4130
+ throw Error("Choose a legal card.");
4131
+ t.hands[seat] = t.hands[seat].filter((c) => c !== at);
4132
+ t.trick.push({ seat, card: at });
4133
+ if (s.variant === "spades" && cardSuit(at) === 3 && s.trick.length > 0 && cardSuit(s.trick[0].card) !== 3 || s.variant === "hearts" && (cardSuit(at) === 2 || at === 49))
4134
+ t.broken = true;
4135
+ t.turn = (seat + 1) % count;
4136
+ if (t.trick.length === count) {
4137
+ const lead = cardSuit(t.trick[0].card), strength = (c) => (cardSuit(c) === s.trump ? 100 : cardSuit(c) === lead ? 50 : 0) + cardRank(c);
4138
+ const winner2 = t.trick.reduce(
4139
+ (a, b) => strength(b.card) > strength(a.card) ? b : a
4140
+ ).seat;
4141
+ t.tricks[winner2]++;
4142
+ t.penalties[winner2] += t.trick.reduce((n, c) => n + penalty(c.card), 0);
4143
+ t.lastTrick = t.trick;
4144
+ t.trick = [];
4145
+ t.turn = winner2;
4146
+ if (t.hands.every((h) => !h.length)) {
4147
+ const moon = s.variant === "hearts" ? t.penalties.findIndex((n) => n === 26) : -1;
4148
+ if (moon >= 0) {
4149
+ t.phase = "moon";
4150
+ t.turn = moon;
4151
+ } else finish(t);
4152
+ }
4153
+ }
4154
+ return t;
4155
+ }
4156
+ function trickView(s, seat) {
4157
+ const { stock, selected, ...publicState } = s;
4158
+ return {
4159
+ ...publicState,
4160
+ stockCount: stock.length,
4161
+ selected: selected.map((cards, i) => i === seat ? cards : []),
4162
+ hands: s.hands.map(
4163
+ (h, i) => i === seat && !(s.phase === "blind" && !s.blindAsked[i]) ? h : h.map(() => null)
4164
+ )
4165
+ };
4166
+ }
4167
+
4168
+ // lib/multiplayer/gin-rummy.ts
4169
+ var ginRank = (c) => cardRank(c) === 14 ? 1 : cardRank(c);
4170
+ var ginValue = (c) => Math.min(10, ginRank(c));
4171
+ function ginMelds(hand2) {
4172
+ const melds = [];
4173
+ for (let rank2 = 1; rank2 <= 13; rank2++) {
4174
+ const cards = hand2.filter((c) => ginRank(c) === rank2);
4175
+ if (cards.length >= 3) {
4176
+ if (cards.length === 4) {
4177
+ melds.push(cards);
4178
+ for (let i = 0; i < 4; i++) melds.push(cards.filter((_, j) => j !== i));
4179
+ } else melds.push(cards);
4180
+ }
4181
+ }
4182
+ for (let suit = 0; suit < 4; suit++) {
4183
+ const cards = hand2.filter((c) => cardSuit(c) === suit).sort((a, b) => ginRank(a) - ginRank(b));
4184
+ for (let start = 0; start < cards.length; start++)
4185
+ for (let end = start + 2; end < cards.length; end++) {
4186
+ const slice = cards.slice(start, end + 1);
4187
+ if (slice.every(
4188
+ (c, i) => i === 0 || ginRank(c) === ginRank(slice[i - 1]) + 1
4189
+ ))
4190
+ melds.push(slice);
4191
+ else break;
4192
+ }
4193
+ }
4194
+ return melds;
4195
+ }
4196
+ function partitions(hand2) {
4197
+ const melds = ginMelds(hand2), out2 = [];
4198
+ function visit(start, used, chosen) {
4199
+ const deadwood2 = hand2.filter((c) => !used.has(c));
4200
+ out2.push({
4201
+ melds: chosen,
4202
+ deadwood: deadwood2,
4203
+ value: deadwood2.reduce((n, c) => n + ginValue(c), 0)
4204
+ });
4205
+ for (let i = start; i < melds.length; i++)
4206
+ if (melds[i].every((c) => !used.has(c)))
4207
+ visit(i + 1, /* @__PURE__ */ new Set([...used, ...melds[i]]), [...chosen, melds[i]]);
4208
+ }
4209
+ visit(0, /* @__PURE__ */ new Set(), []);
4210
+ return out2;
4211
+ }
4212
+ function ginLayout(hand2) {
4213
+ return partitions(hand2).reduce((a, b) => b.value < a.value ? b : a);
4214
+ }
4215
+ function ginLayoffValue(hand2, onto) {
4216
+ const cache = /* @__PURE__ */ new Map();
4217
+ function attach(cards, melds) {
4218
+ const key2 = cards.join(",") + "|" + melds.map((m) => m.join(",")).join(";"), cached = cache.get(key2);
4219
+ if (cached !== void 0) return cached;
4220
+ let best2 = cards.reduce((n, c) => n + ginValue(c), 0);
4221
+ cards.forEach(
4222
+ (c, i) => melds.forEach((m, j) => {
4223
+ const set = m.every((x) => ginRank(x) === ginRank(m[0])), ranks = m.map(ginRank), fits = set ? m.length === 3 && ginRank(c) === ranks[0] : cardSuit(c) === cardSuit(m[0]) && (ginRank(c) === Math.min(...ranks) - 1 || ginRank(c) === Math.max(...ranks) + 1);
4224
+ if (fits) {
4225
+ const next3 = melds.map(
4226
+ (m2, k) => k === j ? [...m2, c].sort((a, b) => ginRank(a) - ginRank(b)) : m2
4227
+ );
4228
+ best2 = Math.min(
4229
+ best2,
4230
+ attach(
4231
+ cards.filter((_, k) => k !== i),
4232
+ next3
4233
+ )
4234
+ );
4235
+ }
4236
+ })
4237
+ );
4238
+ cache.set(key2, best2);
4239
+ return best2;
4240
+ }
4241
+ return Math.min(...partitions(hand2).map((p) => attach(p.deadwood, onto)));
4242
+ }
4243
+ function deal2(dealer, scores2, wins, round, entropy2) {
4244
+ const deck = shuffled(
4245
+ Array.from({ length: 52 }, (_, i) => i),
4246
+ entropy2
4247
+ );
4248
+ return {
4249
+ hands: [
4250
+ deck.slice(0, 10).sort((a, b) => a - b),
4251
+ deck.slice(10, 20).sort((a, b) => a - b)
4252
+ ],
4253
+ stock: deck.slice(21),
4254
+ discard: [deck[20]],
4255
+ turn: 1 - dealer,
4256
+ dealer,
4257
+ phase: "offer",
4258
+ offers: 0,
4259
+ drawnDiscard: null,
4260
+ scores: scores2,
4261
+ wins,
4262
+ winner: null,
4263
+ round,
4264
+ result: "",
4265
+ revealed: [],
4266
+ margin: null
4267
+ };
4268
+ }
4269
+ function createGin(entropy2) {
4270
+ return deal2(Math.floor(draws(entropy2, 1)[0] * 2), [0, 0], [0, 0], 0, entropy2);
4271
+ }
4272
+ function playGin(s, type, at, entropy2) {
4273
+ 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
+ );
4281
+ if (["finished", "round"].includes(s.phase))
4282
+ throw Error("This round is finished.");
4283
+ const t = structuredClone(s), seat = s.turn;
4284
+ if (type === "pass" && s.phase === "offer") {
4285
+ t.offers++;
4286
+ if (t.offers === 2) {
4287
+ t.phase = "draw";
4288
+ t.turn = 1 - s.dealer;
4289
+ } else t.turn = 1 - seat;
4290
+ return t;
4291
+ }
4292
+ if (["draw", "offer"].includes(s.phase) && ["stock", "upcard"].includes(type)) {
4293
+ if (s.phase === "offer" && type === "stock")
4294
+ throw Error("Take the upcard or pass the opening offer.");
4295
+ if (s.phase === "draw" && s.offers === 2 && s.round === t.round && type === "upcard")
4296
+ throw Error("Both players passed: draw from the stock.");
4297
+ if (s.stock.length <= 2) {
4298
+ t.phase = "round";
4299
+ t.result = "Stock exhausted: round cancelled, same dealer.";
4300
+ t.turn = s.dealer;
4301
+ return t;
4302
+ }
4303
+ const card2 = type === "upcard" ? t.discard.pop() : t.stock.shift();
4304
+ t.drawnDiscard = type === "upcard" ? card2 : null;
4305
+ t.hands[seat].push(card2);
4306
+ t.hands[seat].sort((a, b) => a - b);
4307
+ t.phase = "discard";
4308
+ t.offers = 0;
4309
+ return t;
4310
+ }
4311
+ if (s.phase === "discard" && ["discard", "knock"].includes(type) && at !== void 0 && s.hands[seat].includes(at) && at !== s.drawnDiscard) {
4312
+ t.hands[seat] = t.hands[seat].filter((c) => c !== at);
4313
+ t.discard.push(at);
4314
+ t.drawnDiscard = null;
4315
+ if (type === "knock") {
4316
+ const layout = ginLayout(t.hands[seat]);
4317
+ if (layout.value > 10)
4318
+ throw Error("Knock only with ten or fewer deadwood points.");
4319
+ const other = 1 - seat, opp = layout.value === 0 ? ginLayout(t.hands[other]).value : ginLayoffValue(t.hands[other], layout.melds), winner2 = layout.value === 0 || layout.value < opp ? seat : other;
4320
+ const points = layout.value === 0 ? opp + 25 : winner2 === seat ? opp - layout.value : layout.value - opp + 25;
4321
+ t.scores[winner2] += points;
4322
+ t.wins[winner2]++;
4323
+ t.result = `${layout.value === 0 ? "Gin" : winner2 === seat ? "Knock" : "Undercut"}: Player ${winner2 + 1} scores ${points}. Deadwood ${layout.value} vs ${opp}.`;
4324
+ t.revealed = t.hands.map((h) => ginLayout(h).melds);
4325
+ t.phase = t.scores[winner2] >= 100 ? "finished" : "round";
4326
+ t.turn = winner2;
4327
+ if (t.phase === "finished") {
4328
+ t.winner = winner2;
4329
+ t.margin = t.scores[winner2] + 100 + (t.scores[other] === 0 ? 100 : 0) + 25 * t.wins[winner2] - t.scores[other] - 25 * t.wins[other];
4330
+ t.result += ` Final margin including game and hand bonuses: ${t.margin}.`;
4331
+ }
4332
+ } else {
4333
+ t.phase = "draw";
4334
+ t.turn = 1 - seat;
4335
+ }
4336
+ return t;
4337
+ }
4338
+ throw Error("Choose a legal draw, discard or knock.");
4339
+ }
4340
+ function ginView(s, seat) {
4341
+ const { stock, ...rest } = s;
4342
+ return {
4343
+ ...rest,
4344
+ stockCount: stock.length,
4345
+ hands: s.hands.map(
4346
+ (h, i) => i === seat || ["round", "finished"].includes(s.phase) ? h : h.map(() => null)
4347
+ )
4348
+ };
4349
+ }
4350
+
4351
+ // lib/multiplayer/oware.ts
4352
+ var side = (at) => at < 6 ? 0 : 1;
4353
+ function sow(pits, at) {
4354
+ const board = [...pits];
4355
+ let seeds = board[at], last = at;
4356
+ board[at] = 0;
4357
+ while (seeds > 0) {
4358
+ last = (last + 1) % 12;
4359
+ if (last !== at) {
4360
+ board[last]++;
4361
+ seeds--;
4362
+ }
4363
+ }
4364
+ return { board, last };
4365
+ }
4366
+ function owareMoves(s) {
4367
+ if (s.finished) return [];
4368
+ const opponentEmpty = s.pits.every((n, i) => side(i) === s.turn || n === 0);
4369
+ return s.pits.flatMap(
4370
+ (n, at) => n > 0 && side(at) === s.turn && (!opponentEmpty || sow(s.pits, at).board.some((n2, i) => side(i) !== s.turn && n2 > 0)) ? [at] : []
4371
+ );
4372
+ }
4373
+
4374
+ // lib/multiplayer/catalog.ts
4375
+ var multiplayerGames = [
4376
+ { "id": "amazons", "name": "Amazons", "symbol": "\u265B", "players": "2 players", "description": "Claim territory with queens and arrows. Every move reshapes the board.", "rules": "Four queens per player on a 10 by 10 board. Player 1 starts at A4,D1,G1,J4; Player 2 at A7,D10,G10,J7. Move a queen any unobstructed distance horizontally, vertically or diagonally, then shoot an arrow from its destination along another unobstructed queen line. The arrow permanently blocks its square; it can land on the square just vacated. Complete both steps as one turn. A player with no queen move loses. JSON move uses numeric zero-based row-major from/to and amount for the arrow, 0\u201399. Exact legal wire actions are supplied to the active player." },
4377
+ { "id": "lines-of-action", "name": "Lines of Action", "symbol": "\u25CF", "players": "2 players", "description": "Bring your scattered stones together in a tactical race for connection.", "rules": "Claude Soucie\u2019s 8 by 8 connection game. Player 1 has six stones on each horizontal edge, Player 2 six on each vertical edge, with corners empty. Move any stone in one straight line exactly as many squares as there are stones of either color on that entire line. Jump over friendly stones but never enemies. Capture an enemy at the destination; never land on a friendly stone. Connect all your remaining stones into one group using orthogonal or diagonal adjacency to win; a single stone is connected. This edition awards simultaneous connection to the mover. An immobile player passes automatically; both immobile or three repetitions with the same player to move draw. This bounded-table edition also draws after 512 moves without connection. JSON move uses numeric row-major from/to, 0\u201363. Exact legal actions are listed." },
4378
+ { "id": "last-greenhouse", "name": "The Last Greenhouse", "symbol": "\u2667", "players": "2 players \xB7 cooperative", "description": "Two private journals. Five locked rooms. Save the last seed bank together.", "rules": "An original cooperative escape-room story for two keepers. Read your private journal and compare it with your partner\u2019s observation to solve each room. Share publishes your current observation to both players; Pass hands the controls to your partner; Choose selects setting at 0\u20133 from the visible room options. Every action hands over the controls. Wrong answers are safe to retry and are included in the legal-action list. Complete the atrium, prism gallery, seed archive, pump room and dawn canopy together. Only your own private journal and shared notes are projected; spectators see no private journal. Local handoff conceals journals between turns. No competitive rating or Quick Play." },
4379
+ { id: "euchre", name: "Euchre", symbol: "J", players: "4 players \xB7 2 teams", description: "Call trump, trust your partner, or take all five tricks alone.", rules: "Four players in opposite-seat partnerships. A 24-card deck uses 9 through ace. Each player receives five cards and one card is turned up. Starting left of dealer, order the turned suit or pass. If ordered, the dealer picks up the upcard and discards one card face down. After four passes, call any other suit; four more passes produce a fresh deal with the next dealer. A caller may go alone: their partner sits out. Left of dealer leads, skipping a sitting partner. Follow suit when possible. The trump jack is highest, followed by the other jack of the same color (the left bower, which belongs to trump), then A,K,Q,10,9 of trump. Other suits rank A,K,Q,J,10,9. Trick winner leads. Makers score 1 for three or four tricks, 2 for all five together, or 4 for all five alone. Defenders score 2 if makers take fewer than three. First team to 10 wins. No stick-the-dealer, joker or defensive lone hands. Hands and kitty remain private. Legal actions are enumerated for the active seat." },
4380
+ { id: "cribbage", name: "Cribbage", symbol: "121", players: "2 players", description: "Choose your crib. Peg the count. Race to 121.", rules: "Two-player six-card cribbage with a standard deck. Aces are low. Each player discards two cards face down to the dealer's crib; the starter is revealed after both discard. A starter jack gives the dealer two points. Non-dealer leads pegging: play one card without exceeding 31, or say Go only when no card fits. Fifteen and thirty-one score two; consecutive pairs, triples and quads score two, six and twelve; the longest trailing run scores its length. The last card before a reset scores one except at 31. After a Go/reset the opponent of the last player leads if holding cards. Count non-dealer's hand, dealer's hand, then crib: fifteens, pairs, longest runs with multiplicities, four/five-card hand flushes, five-card-only crib flush, and one for a jack matching the starter's suit. Counting is automatic; no muggins. Dealer alternates. First to reach 121 at any scoring event wins immediately. Hidden cards and crib stay private until counting; spectators see only public cards. Exact legal JSON actions are supplied in visible_state.legalActions for the active player." },
4381
+ { id: "fanorona", name: "Fanorona", symbol: "\u2194", players: "2 players", description: "Approach or withdraw. One move can capture a whole line.", rules: "Two players on the traditional 9 by 5 connected-point board, twenty-two stones each. Player 1 starts. Move one stone along a drawn line to an adjacent empty point. Captures are compulsory when available: approach captures the consecutive enemy line beyond the destination; withdrawal captures the consecutive enemy line behind the starting point. Choose one when both apply. After a capture you may continue with the same stone or Stop. A capture sequence cannot revisit a point or repeat the immediately preceding direction. A non-capturing move is allowed only when no capture exists. Capture every opposing stone or leave the opponent without a move to win. This implementation draws at three repetitions of the same position with the same player to move, or eighty completed turns without a capture. Intersections are zero-based row-major: top-left 0, bottom-right 44. JSON move uses from/to plus amount 0 for approach or a quiet move, 1 for withdrawal; stop ends an optional continuation. Exact legal actions are supplied in visible_state.legalActions." },
4382
+ { "id": "categories", "name": "Categories", "symbol": "A\u2013Z", "players": "2\u20138 players", "description": "One letter. Six categories. Race your friends to a unique answer.", "rules": "Five English-language rounds with Country, City, Animal, Food, Occupation and Object. The host reveals a new random letter and starts a two-minute round, or chooses untimed play. Submit one locked answer sheet; blank answers are allowed. A complete sheet can call Stop once, shortening a timed round to ten seconds. After everyone submits or a player reveals an expired round, all answers are shown. Unsubmitted answers are blank. Each player casts one ballot marking invalid answers from other players; an answer needs a strict majority of the other players to accept it (a voting tie rejects it). Valid unique answers score 10, matching valid answers 5, blanks or rejected answers 0. Highest total after five rounds wins; equal totals draw. Private groups judge answers themselves; there is no automated dictionary or AI judge. Local play uses untimed private handoffs." },
4383
+ { "id": "cheat", "name": "Cheat", "symbol": "?!", "players": "3\u20136 players", "description": "Play face down. Tell a convincing story. Call the bluff.", "rules": "Deal all 52 cards clockwise. Player 1 claims aces, then claims cycle through 2 to king and back to ace. On your turn place one to four cards face down, claiming the required rank; bluffing is allowed. Before the next play, any other player may challenge, or the next player may accept. A challenge reveals only the latest cards: if any is the wrong rank the claimant takes the whole pile; otherwise the challenger takes it. Accepted cards stay hidden. The next clockwise player then takes their turn with the next required rank. An empty hand wins only after its final claim is accepted or survives a challenge. This table uses an explicit challenge window without a reaction timer." },
4384
+ { "id": "president", "name": "President", "symbol": "3\u21922", "players": "3\u20136 players", "description": "Beat the group. Pass at the right moment. Finish first.", "rules": "A single deal with a standard 52-card deck, all cards dealt clockwise. Ranks rise from 3 through ace, with 2 highest. The holder of 3 of clubs opens with a group containing it. Lead one to four equal-rank cards; following plays must contain the same number of cards at a strictly higher rank. You may pass, but cannot re-enter until that pile clears. When all other active players pass, the last player to play leads again; if they have finished, the next active player leads. Finishers leave play and receive places in order. Play continues until one player remains; the first finisher is President. No jokers, suit tie-breaks, special twos, revolution or between-deal tribute." },
4385
+ { "id": "rummy", "name": "Classic Rummy", "symbol": "777", "players": "2\u20134 players", "description": "Draw a card. Build sets and runs. Empty your hand.", "rules": "One deal, no jokers. Two players receive ten cards each; three or four receive seven. Aces are low only. Draw one card from the stock or take the top discard. Then optionally meld sets of three or four equal ranks, or runs of three or more consecutive cards of one suit. You may add held cards to either end of any public run or complete a public set. Keep at least one card and end your turn by discarding; a taken upcard cannot be discarded immediately. First to discard their last card wins and scores the total value of cards held by opponents: ace 1, faces 10, other cards face value. Recycle discards beneath the top card when stock runs out. If stock and recyclable discards are both empty and the next player chooses stock, the deal ends; lowest remaining card value wins, with ties drawn. No initial meld threshold, knock or Rummy 500 scoring." },
4386
+ { id: "tic-tac-toe", name: "Tic-tac-toe", symbol: "XO", players: "2 players", description: "Nine squares. Three in a row. Your next quick match.", rules: "X starts on a 3 by 3 board. Take turns placing your mark on an empty square. Three marks in a horizontal, vertical or diagonal line wins. A full board without a winner draws." },
4387
+ { id: "international-draughts", name: "International Draughts", symbol: "\u265B", players: "2 players", description: "Twenty pieces. Flying kings. Capture the longest chain.", rules: "White starts on a 10\xD710 board with twenty pieces per side. Men move one diagonal square forward and capture in either direction. Kings slide diagonally and land on any empty square beyond a captured piece. Captures are compulsory: choose a sequence taking the greatest number of pieces. Captured pieces remain as blockers until the entire sequence ends. A man becomes a king only if its final square is on the promotion row. No pieces or no legal move loses. Threefold repetition, twenty-five successive king moves per player without captures or man moves, and the official sixteen/five-move king endgame limits draw automatically." },
4388
+ { id: "atomic-chess", name: "Atomic Chess", symbol: "\u265C", players: "2 players", description: "Every capture explodes. Protect your king from the blast.", rules: "Standard chess setup and moves, with explosive captures: the capturing piece, captured piece and adjacent non-pawns disappear. Adjacent pawns survive. En passant explodes at the capturing pawn\u2019s destination. A move that explodes your own king is illegal. Kings cannot capture, but may stand next to one another. Explode the opposing king or deliver checkmate to win. Stalemate, insufficient material, threefold repetition or fifty moves without a pawn move or capture draws. Castling and all four promotions are supported." },
4389
+ { id: "three-check", name: "Three-check", symbol: "\u2654", players: "2 players", description: "Give three checks to win. Every threat counts.", rules: "Standard chess rules, plus a second way to win: give your opponent check three times. A double check counts as one checking move. Checkmate also wins. Each player\u2019s checks are shown beside the board. Standard castling, en passant and all four promotions apply. Stalemate, insufficient material, threefold repetition or fifty moves without a pawn move or capture draws." },
4390
+ { id: "connect-four", name: "Connect Four", symbol: "\u25CF", players: "2 players", description: "Drop a piece. Connect four. Watch the next column.", rules: "Two players alternate dropping pieces into one of seven columns on a six-row board. Each piece falls to the lowest empty space. Four or more pieces of your color in a horizontal, vertical or diagonal line wins. A full board without a winner draws." },
4391
+ { id: "dots-boxes", name: "Dots & Boxes", symbol: "\u25A6", players: "2 players", description: "Complete a square. Claim another turn.", rules: "Players alternate drawing one unused edge on a 3 \xD7 3 box board. Completing the fourth side of a box claims it and gives another turn. An edge can complete two boxes. When all nine boxes are owned, most boxes wins. All information is public." },
4392
+ { id: "battleships", name: "Battleships", symbol: "\u2693", players: "2 players", description: "Hide your fleet. Read the hits. Find the last ship.", rules: "Each player secretly places five straight ships of lengths 5, 4, 3, 3 and 2 on a 10\xD710 grid. Ships may touch but cannot overlap or leave the grid. Tap a starting square to place the next ship; switch horizontal/vertical or arrange automatically, then lock the fleet. Player 1 sets up first, then Player 2. Player 1 fires first. Take exactly one shot per turn, including after a hit. The target grid marks hits and misses, and reveals a ship when every square is hit. Sink all five enemy ships to win. On one device, hide your fleet before passing it. Online, only your own ships and already-known shot results are sent to your browser." },
4393
+ {
4394
+ id: "thousand",
4395
+ name: "Thousand",
4396
+ symbol: "1000",
4397
+ players: "2\u20134 players",
4398
+ 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."
4400
+ },
4401
+ {
4402
+ id: "binokel",
4403
+ name: "Binokel",
4404
+ symbol: "AA",
4405
+ players: "4 players",
4406
+ description: "Four copies. Two partners. One contract.",
4407
+ rules: "Partners sit opposite. Four copies of jack, queen, king, ten and ace in every suit form an 80-card deck; twenty cards each. Auction starts at 50, rises by one below 60 and by five thereafter. Passed players leave the auction. If the first three pass, dealer plays 50. Declarer chooses trump only from a held queen-king pair; no pair loses the contract. The declaring partnership needs at least 20 meld points. Melds score automatically and their cards are revealed: aces/kings/queens/jacks around; ordinary and royal marriages; spade queen with diamond jack; and a trump A-10-K-Q-J run. Duplicate melds use the full single/double/triple/quadruple table. A royal marriage already in a run is not counted twice. Follow suit and beat the current winner when possible; when void, trump and overtrump when possible. Equal copies favor the first played. Each ace, ten or king taken earns one; last trick earns two. At least 20 trick points are required to score a hand, and at least 20 meld points to add melds. Declarers reaching their bid score their total, otherwise lose the bid. Opponents score qualifying totals. First team to 500 wins; if both reach 500, the declaring team wins."
4408
+ },
4409
+ {
4410
+ id: "skat",
4411
+ name: "Skat",
4412
+ symbol: "J",
4413
+ players: "3 players",
4414
+ description: "Win the bidding duel. Play alone against two.",
4415
+ rules: "Thirty-two cards, seven through ace. Middlehand speaks values to forehand, who holds or passes; rearhand then speaks to the survivor. If both speakers pass without a bid, forehand plays 18 or passes out. Declarer picks up the two-card skat and discards two, or plays from hand. Choose a suit, Grand (only jacks trump) or Null (lose every trick). Jacks rank clubs, spades, hearts, diamonds above suit trumps. Other suit/Grand cards rank A-10-K-Q-9-8-7; Null ranks A-K-Q-J-10-9-8-7. Follow the effective suit; jacks belong to trump in suit/Grand games. Forehand always leads. Suit/Grand need 61 card points, including skat; Null ends in loss on declarer\u2019s first trick. Value multiplies suit base (diamonds 9, hearts 10, spades 11, clubs 12, Grand 24) by matadors, game, hand and Schneider/Schwarz/open levels. Hand games can announce 90 points or all tricks. Null values are 23, hand 35, open 46, open hand 59. Lost or overbid games subtract twice their value, rounding overbids up to a sufficient base multiple. Highest score after eighteen hands wins. No Ramsch, Kontra or tournament bonus variant."
4416
+ },
4417
+ {
4418
+ id: "baska",
4419
+ name: "Baska",
4420
+ symbol: "DQ",
4421
+ players: "4 players",
4422
+ description: "Find your hidden partner in sixteen cards.",
4423
+ rules: "The red (heart-ace) variant, sixteen cards: ace, ten, queen and jack in each suit. Trump strength: heart ace, heart ten, queens clubs-spades-hearts-diamonds, jacks in that order, diamond ace, diamond ten. Other cards keep their printed suits. Old partners hold club and spade queens; holding both creates a silent solo. Wedding pairs that holder with the first available club, spade or heart jack holder. Auction offers Grand, Zolo and their all-trick Du contracts, with alternating counters up to redouble. Grand has only jack trumps. Four tens, aces or queens automatically claim Zolo Bieda, Master or Baska. Follow suit and beat the winner when possible. Card points are A11, 10=10, Q3, J2. Old side needs 53; with 52\u201352, the side that last countered loses. Ordinary/wedding stakes rise for failure to reach 26 (young) or 27 (old), or failure to take a trick; old losses cost twice. Silent solo stakes four, Grand/Zolo five, Du/honor ten, all doubled by counters. Paired winners gain the stake each; a solo winner gains it from each opponent; highest total after sixteen hands wins. Black and three-tens variants are outside this table."
4424
+ },
4425
+ {
4426
+ id: "canasta",
4427
+ name: "Canasta",
4428
+ symbol: "77",
4429
+ players: "2 or 4 players",
4430
+ description: "Build seven-card melds. Break a frozen pile.",
4431
+ rules: "Two 54-card decks, including four jokers. Two players get fifteen cards and draw two; four players get eleven and draw one, sharing melds with the opposite partner. Meld matching natural ranks with at least two natural cards and at least three total; at most three wild twos/jokers, with no more wild than natural. Seven or more cards form a canasta. Opening melds total 15 below zero score, 50 below 1500, 90 below 3000, then 120. Take the entire discard pile only by immediately melding its top; a frozen pile requires two matching natural cards held. Wild cards block pickup and freeze the pile; black threes block pickup except a final meld. Red threes are set aside and replaced when drawn from stock. Black threes meld only when going out. End a turn by discarding one. Going out requires two canastas with two players, one with partners; premature going out loses the match. Natural canastas earn 500, mixed 300; going out 100, concealed opening 200. Red threes earn 100 each plus 400 for all four, negative without a meld. Melded cards score positive, held cards negative. Card values: joker50, A/2=20, 8\u2013K=10, others5. Highest score reaching 5000 wins. Optional partner permission is not required."
4432
+ },
4433
+ {
4434
+ id: "bridge",
4435
+ name: "Bridge",
4436
+ symbol: "NT",
4437
+ players: "4 players",
4438
+ description: "Bid a contract. Plan two hands together.",
4439
+ rules: "Partners sit opposite. A sixteen-board match uses standard duplicate contract scores and the rotating vulnerability cycle. Each player has thirteen cards. Bids increase by level and strain: clubs, diamonds, hearts, spades, no trump. Three passes after a bid end the auction; four opening passes pass out the board. Opponents may double; the declaring side may redouble. Declarer is the first player in the winning partnership to bid the final strain. The player to declarer\u2019s left leads. Dummy is exposed after that lead and declarer controls its plays. Follow suit when possible; highest trump or lead-suit card wins. Contract, game, slam, overtrick and undertrick scores account for doubling and vulnerability. Highest net team score after sixteen boards wins. This table uses raw duplicate scores; Kurnik\u2019s HCP-adjusted IMP table, conventions, claims and tournament director rulings are outside this variant."
4440
+ },
4441
+ {
4442
+ id: "polish-hearts",
4443
+ name: "Polish Hearts",
4444
+ symbol: "\u2665",
4445
+ players: "3\u20134 players",
4446
+ description: "Avoid seven penalties, then win points back.",
4447
+ rules: "Seven penalty hands: each trick costs 20; each heart 20; queen 60; king or jack 30; heart king 150; seventh and last tricks 75 each; the seventh hand combines all penalties. Follow suit, with no obligation to beat the current winner. Hearts cannot lead while another suit is held in the heart, heart-king and combined hands. Next, each player deals a trump hand and chooses trump from the first five cards, before the rest are dealt; each trick earns 25. Finally, build four suit columns around the dealer\u2019s opening rank; extend a column by one rank, or start another at that opening rank. Play whenever possible. First out earns 800, second 500. Three-player games omit the club two, use six cards for the trump choice, cost 15 per trick, 140 for the heart king and 70 per special trick, earn 20 per trump trick and award 790/485 for the final layout. Highest total wins."
4448
+ },
4449
+ {
4450
+ id: "durak",
4451
+ name: "Durak",
4452
+ symbol: "\u2660",
4453
+ players: "2\u20134 players",
4454
+ description: "Beat the attack. Empty your hand.",
4455
+ rules: "Individual throw-in Durak with 36 cards, six through ace. Deal six each; the bottom stock card shows trump and is drawn last. The lowest dealt trump holder opens. Attack with one card, then add ranks already on the table, including defended ranks. The defender beats a card with a higher card of the same suit or any trump over a nontrump. Attacks are capped at six cards or the defender\u2019s starting hand size. The main attacker has first priority; finishing offers other attackers a clockwise turn. Taking opens a final throw-in window, then the defender collects all table cards and skips their next attack. Successful defense discards the table and the defender attacks next. Refill the main attacker first, other players clockwise and defender last. Empty hands leave once the stock runs out. The last player holding cards loses; simultaneous empty hands draw. This table has no transfer attacks, teams or optional trump-six exchange."
4456
+ },
4457
+ {
4458
+ id: "makao",
4459
+ name: "Makao",
4460
+ symbol: "Q",
4461
+ players: "2\u20134 players",
4462
+ description: "Match suits and ranks. Counter the demand.",
4463
+ rules: "Five cards each from a 52-card deck. Match the pile\u2019s suit or rank, or play a queen on an ordinary card; any ordinary card fits a queen. Groups of the same rank can be played together, with the first card matching. Twos and threes add draw penalties; the heart king adds five to the next player, the spade king to the previous player. Penalties can be stacked with any fighting card. Fours add waiting turns, countered with fours. Jacks request an ordinary rank for one complete cycle, or cancel a request; aces request a suit for the next player. Queens do not override demands. A normal draw offers only that new card for immediate play. Say Makao with the checkbox when one card remains; the next player can catch a missed declaration for five cards. When both stock and reusable discards run out, every player returns a random half of their hand, rounded down. The first empty hand wins. No jokers or sequence-chain variant."
4464
+ },
4465
+ {
4466
+ id: "three-five-eight",
4467
+ name: "3-5-8",
4468
+ symbol: "358",
4469
+ players: "3 players",
4470
+ description: "Meet your quota. Exchange for stronger cards.",
4471
+ rules: "Eighteen hands with a 52-card deck. Sixteen cards each and a four-card kitty. Dealer targets eight tricks, the next player five, the third three. Each player chooses each of the six contracts once on their own deals: four trump suits, no trump and no tricks. Discard four before taking the kitty. Follow suit; if void, play trump when held. Highest trump or highest lead suit wins. Ordinary hands score tricks minus quota; no-tricks reverses that score. Before subsequent hands, players above their previous quota exchange one unwanted card per surplus trick with players below quota; the receiver automatically returns their highest card in its suit. Higher current quotas exchange first. Highest total after eighteen hands wins. This uses the eighteen-hand contract variant rather than the twelve-trick sudden-win variant."
4472
+ },
4473
+ {
4474
+ id: "property-trading",
4475
+ name: "Property Trading",
4476
+ symbol: "\u2302",
4477
+ players: "2\u20134 players",
4478
+ description: "Build a garden estate. Bargain for a complete street.",
4479
+ rules: "An original forty-space estate board with classic property trading rules. Each player starts with 1,500 points; passing Start earns 200. Roll two dice, buy unowned properties or auction them to all players. Doubles earn another roll; three consecutive doubles send you to jail. Collect rents, complete color groups and build evenly using the finite bank of 32 houses and 12 hotels. Mortgage unbuilt properties for half their price; redeem for 60%. Trade unbuilt properties and cash by an explicit offer and acceptance; receiving a mortgaged property costs 10% of its price. Jail ends with doubles, a release card or a payment of 50; the third failed roll forces payment. Income tax is 200 or 10% of estate value; luxury tax is 75. Free Park pays nothing. Resolve debts by selling buildings or mortgaging assets; bankruptcy transfers an estate to the creditor, or auctions it for bank debts. The last solvent player wins. Free points only."
4480
+ },
4481
+ {
4482
+ id: "draw-guess",
4483
+ name: "Draw & Guess",
4484
+ symbol: "\u270E",
4485
+ players: "2\u201320 players",
4486
+ description: "A private word. A shared sketch.",
4487
+ rules: "Take turns drawing one of three private English prompts. You have two minutes. Other players guess while the drawing updates. A correct guess earns 1\u201345 points based on remaining time, and the drawer earns up to five more, capped at 45, for that guess. Each player can score once per round. If everyone guesses, the round ends early. No correct guesses costs the drawer ten. First to 100 ends the match; highest score wins. Draw pictures rather than writing the word. Local play offers a player switch and hides the prompt from guessers."
4488
+ },
4489
+ {
4490
+ id: "letter-tiles",
4491
+ name: "Letter Tiles",
4492
+ symbol: "A",
4493
+ players: "2\u20134 players",
4494
+ description: "Build a crossword. Make every letter count.",
4495
+ rules: "American English Scrabble-style home scoring on the standard 15 \xD7 15 premium-square layout, with 100 tiles and seven per rack. First word: at least two letters covering the center double-word square. Place connected tiles in one row or column without gaps; every new or crossed word scores, including existing letters. English values: A E I L N O R S T U 1; D G 2; B C M P 3; F H V W Y 4; K 5; J X 8; Q Z 10. Blanks stay zero and keep their chosen letter. DL/TL multiply each newly placed letter; DW/TW multiply its whole word after letter bonuses. New premiums apply to every word using that tile, once on its placement turn; multiple word multipliers multiply together. Using seven new tiles adds 50 after all multipliers. Next player accepts or challenges against our ENABLE English word-game list with QI/ZA additions; accepted words differ from official NWL/Collins lists. Invalid words return the tiles and lose the proposer\u2019s turn; a failed challenge loses the challenger\u2019s next turn. Exchange any number of rack tiles as often as needed, using a turn, when at least seven remain in the bag. Everyone passing twice ends play, as does emptying a rack with an empty bag. Deduct unplayed tiles; the player going out also receives opponents\u2019 deductions. Highest final score wins; a final tie favors the higher pre-adjustment score. Earlier rounds retain their original rules until restarted."
4496
+ },
4497
+ {
4498
+ id: "pan",
4499
+ name: "Pan",
4500
+ symbol: "\u2667",
4501
+ players: "2\u20134 players",
4502
+ description: "Shed your cards before the pile turns.",
4503
+ rules: "A 24-card deck, nine through ace. The holder of the heart nine opens with that card or all four nines. Play one card or all four of a rank, no lower than the pile top. With only the heart nine on the pile, the other three nines can be played together. Instead of playing, take up to three top cards, leaving the heart nine. Empty hands leave the round. The last player receives a P, then A, then N; three letters loses the match. A last player who can immediately shed one or four aces draws the round. This table uses the ordinary clockwise variant."
4504
+ },
4505
+ {
4506
+ id: "gin-rummy",
4507
+ name: "Gin Rummy",
4508
+ symbol: "\u2666",
4509
+ players: "2 players",
4510
+ description: "Build runs and sets. Know when to knock.",
4511
+ rules: "Ten cards each. The dealer\u2019s opponent may take the opening upcard; if passed, the dealer may take it. If both pass, draw from stock. Draw one, then discard another; you cannot return the upcard just drawn. Meld sets of three/four equal ranks or same-suit runs of at least three, ace low. Knock when the remaining deadwood is at most ten: ace one, face cards ten. Melds and opponent layoffs are optimized automatically. Gin earns deadwood difference plus 25; an equal or lower opponent deadwood undercuts for 25 plus difference. At 100, the game ends with game and hand bonuses. Two cards left in stock cancels the round with the same dealer."
4512
+ },
4513
+ {
3938
4514
  id: "sixty-six",
3939
4515
  name: "Sixty-Six",
3940
4516
  symbol: "\u2654",
3941
4517
  players: "2 players",
3942
4518
  description: "Claim sixty-six. Close the stock with care.",
3943
- 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, after a subsequent won trick. 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."
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."
3944
4520
  },
3945
4521
  {
3946
4522
  id: "spades",
@@ -4415,419 +4991,166 @@ var action_catalog_default = {
4415
4991
  ]
4416
4992
  };
4417
4993
 
4418
- // lib/multiplayer/dice.ts
4419
- var diceCategories = [
4420
- "Ones",
4421
- "Twos",
4422
- "Threes",
4423
- "Fours",
4424
- "Fives",
4425
- "Sixes",
4426
- "Three of a kind",
4427
- "Four of a kind",
4428
- "Full house",
4429
- "Small straight",
4430
- "Large straight",
4431
- "Five of a kind",
4432
- "Chance"
4433
- ];
4434
- function createDice(count) {
4435
- if (!index(count - 2, 3)) throw Error("Choose two to four players.");
4436
- return {
4437
- dice: [0, 0, 0, 0, 0],
4438
- held: Array(5).fill(false),
4439
- rolls: 0,
4440
- sheets: Array.from({ length: count }, () => Array(13).fill(null)),
4441
- bonuses: Array(count).fill(0),
4442
- turn: 0,
4443
- finished: false,
4444
- winner: null
4445
- };
4446
- }
4447
- function diceScore(dice, category, joker = false) {
4448
- const counts = Array.from(
4449
- { length: 6 },
4450
- (_, i) => dice.filter((v) => v === i + 1).length
4451
- ), sum = dice.reduce((a, b) => a + b, 0);
4452
- if (category < 6) return counts[category] * (category + 1);
4453
- switch (category) {
4454
- case 6:
4455
- return Math.max(...counts) >= 3 ? sum : 0;
4456
- case 7:
4457
- return Math.max(...counts) >= 4 ? sum : 0;
4458
- case 8:
4459
- return joker || counts.includes(3) && counts.includes(2) ? 25 : 0;
4460
- case 9:
4461
- return joker || [0, 1, 2].some((start) => counts.slice(start, start + 4).every(Boolean)) ? 30 : 0;
4462
- case 10:
4463
- return joker || [0, 1].some((start) => counts.slice(start, start + 5).every(Boolean)) ? 40 : 0;
4464
- case 11:
4465
- return counts.includes(5) ? 50 : 0;
4466
- case 12:
4467
- return sum;
4468
- default:
4469
- throw Error("Choose a score category.");
4470
- }
4471
- }
4472
- function diceTotal(s, seat) {
4473
- const sheet = s.sheets[seat], upper = sheet.slice(0, 6).reduce((sum, n) => sum + (n ?? 0), 0);
4474
- return sheet.reduce((sum, n) => sum + (n ?? 0), 0) + (upper >= 63 ? 35 : 0) + s.bonuses[seat];
4475
- }
4476
- function playDice(s, type, at, entropy2) {
4477
- if (s.finished) throw Error("This match is finished.");
4478
- if (type === "roll") {
4479
- if (s.rolls >= 3) throw Error("Record a score after three rolls.");
4480
- const random = draws(entropy2, 5);
4481
- return {
4482
- ...s,
4483
- rolls: s.rolls + 1,
4484
- dice: s.dice.map(
4485
- (v, i) => s.rolls && s.held[i] ? v : 1 + Math.floor(random[i] * 6)
4486
- )
4487
- };
4488
- }
4489
- if (at === void 0 || !index(at, type === "hold" ? 5 : 13) || !s.rolls)
4490
- throw Error("Roll first, then choose a die or score category.");
4491
- if (type === "hold")
4492
- return { ...s, held: s.held.map((v, i) => i === at ? !v : v) };
4493
- if (type !== "score" || s.sheets[s.turn][at] !== null)
4494
- throw Error("Choose an unused score category.");
4495
- const sheets = s.sheets.map((row) => [...row]), bonuses = [...s.bonuses];
4496
- const five = s.dice.every((v) => v === s.dice[0]), upper = s.dice[0] - 1;
4497
- const joker = five && sheets[s.turn][11] !== null && sheets[s.turn][upper] !== null;
4498
- if (joker && sheets[s.turn].slice(6).some((n) => n === null) && at < 6)
4499
- throw Error(
4500
- "Use the five-of-a-kind joker in an unused lower category first."
4501
- );
4502
- sheets[s.turn][at] = diceScore(s.dice, at, joker);
4503
- if (joker && sheets[s.turn][11] === 50) bonuses[s.turn] += 100;
4504
- const next3 = {
4505
- ...s,
4506
- sheets,
4507
- bonuses,
4508
- turn: (s.turn + 1) % s.sheets.length,
4509
- rolls: 0,
4510
- held: Array(5).fill(false),
4511
- dice: [0, 0, 0, 0, 0]
4512
- };
4513
- next3.finished = sheets.every((row) => row.every((n) => n !== null));
4514
- if (next3.finished)
4515
- next3.winner = best(sheets.map((_, i) => diceTotal(next3, i)));
4516
- return next3;
4517
- }
4518
-
4519
- // lib/agents/action-help.ts
4520
- var help = {
4521
- amazons: "move: numeric from/to 0\u201399 and amount for the arrow square. Select an exact legal_wire_actions entry.",
4522
- "lines-of-action": "move: numeric from/to 0\u201363. Move the count of all stones along the full line; enemy stones block jumping.",
4523
- "last-greenhouse": "share: publish your current private journal observation. pass: hand controls to partner. choose: at 0\u20133 selects a room setting. All choices are listed, including incorrect answers. Cooperate using both journals.",
4524
- 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.",
4525
- 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.",
4526
- 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.",
4527
- 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.`,
4528
- 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.",
4529
- 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.",
4530
- 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.",
4531
- "connect-four": "place: at is a zero-based column (0\u20136). Convenience: --column 1\u20137.",
4532
- "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.",
4533
- chess: "move: from/to are algebraic squares; promotion is q/r/b/n.",
4534
- "atomic-chess": "move: from/to are algebraic squares; promotion is q/r/b/n. Explosions follow the game rules.",
4535
- "three-check": "move: from/to are algebraic squares; promotion is q/r/b/n. Check counts are in visible_state.",
4536
- checkers: "move: from/to are zero-based row-major squares (0\u201363); continue a mandatory capture chain. Convenience flags use algebraic squares.",
4537
- "brazilian-checkers": "move: from/to are zero-based squares (0\u201363); maximum captures apply.",
4538
- "international-draughts": "move: from/to are zero-based squares (0\u201399). sequence: cards is a complete legal path of square indexes, including the origin.",
4539
- reversi: "place: at is a zero-based row-major square (0\u201363). Unavailable turns pass automatically.",
4540
- gomoku: "place: at is a zero-based row-major intersection (0\u2013224). Exactly five wins.",
4541
- 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.",
4542
- "dots-boxes": "place: at is a zero-based edge index; legal_actions and --edge use the same edge one-based (edge N is visible_state.edges[N-1]). Horizontal edges precede vertical edges in row-major order. Edges and box owners are in visible_state. Convenience: --edge is one-based.",
4543
- 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.",
4544
- 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.",
4545
- 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.",
4546
- 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.",
4547
- 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.`,
4548
- 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.",
4549
- 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.",
4550
- football: "place: at is a zero-based destination point. The state records ball, used edges and continuing rebounds.",
4551
- 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.",
4552
- 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.",
4553
- xiangqi: "move: numeric zero-based row-major from/to on the 9\xD710 board. promotion is unused.",
4554
- 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.",
4555
- durak: "play: at is your card ID from the visible hand; take/pass: no payload. Follow attack/defence roles and visible trump/table.",
4556
- 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.",
4557
- 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.",
4558
- "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.",
4559
- "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.",
4560
- 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.",
4561
- 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.",
4562
- "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.",
4563
- "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.",
4564
- 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.",
4565
- "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.",
4566
- 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).",
4567
- 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.",
4568
- 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).",
4569
- 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.",
4570
- 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.",
4571
- "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.",
4572
- "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.",
4573
- "property-trading": "roll/end/buy/auction/pay/bankrupt/pass/reject have phase-specific effects. bid: amount is the bid. mortgage/redeem/build/sell: at selects a property. tax: amount is 0 or 1. release: amount 0 pays fifty, 1 uses a release card. offer: tradeOffer contains seat,give,take,giveCash,takeCash; accept confirms the current trade."
4574
- };
4575
- function actionHelp(game) {
4576
- return { format: `game play --room CODE --revision R --action '{"type":"...",...}'`, indexing: "Board and hand-position indexes are zero-based. Card IDs are the actual values in visible_state, not offsets into a hand array. Convenience --row/--column/--edge/--pit flags are one-based. Inspect the exact player-visible state; do not guess hidden cards or board indexes.", game: help[game], validation: "action_types lists permitted command families, not currently legal moves. The server enforces phases, turns, ownership and legality. After a rejected or uncertain action read status again; never retry blindly." };
4577
- }
4578
-
4579
- // lib/multiplayer/trick-cards.ts
4580
- var cardSuit = (card2) => Math.floor(card2 / 13);
4581
- var cardRank = (card2) => card2 % 13 + 2;
4582
- var cardLabel = (card2) => `${["", "", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"][cardRank(card2)]}${["\u2663", "\u2666", "\u2665", "\u2660"][cardSuit(card2)]}`;
4583
- var penalty = (card2) => cardSuit(card2) === 2 ? 1 : card2 === 49 ? 13 : 0;
4584
- function first(s) {
4585
- return s.variant === "spades" ? s.dealer : (s.dealer + 1) % s.hands.length;
4586
- }
4587
- function deal(s, entropy2) {
4588
- const count = s.hands.length, size = s.variant === "oh-hell" ? Math.max(1, (count === 4 ? 13 : 17) - s.round) : 13, deck = shuffled(
4589
- Array.from({ length: 52 }, (_, i) => i),
4590
- entropy2
4591
- );
4592
- const hands = Array.from(
4593
- { length: count },
4594
- (_, i) => deck.slice(i * size, (i + 1) * size).sort((a, b) => a - b)
4595
- );
4596
- const t = {
4597
- ...s,
4598
- size,
4599
- hands,
4600
- stock: deck.slice(count * size),
4601
- trick: [],
4602
- lastTrick: [],
4603
- tricks: Array(count).fill(0),
4604
- penalties: Array(count).fill(0),
4605
- bids: Array(count).fill(null),
4606
- blind: Array(count).fill(false),
4607
- blindAsked: Array(count).fill(false),
4608
- selected: Array.from({ length: count }, () => []),
4609
- passed: Array(count).fill(false),
4610
- broken: false,
4611
- winner: null,
4612
- result: ""
4613
- };
4614
- t.trump = s.variant === "spades" ? 3 : s.variant === "oh-hell" && s.round > 0 ? cardSuit(t.stock[0]) : null;
4615
- t.phase = s.variant === "hearts" ? s.round % 4 === 3 ? "play" : "pass" : s.variant === "spades" && Math.abs(s.scores[0] - s.scores[1]) >= 200 ? "blind" : "bid";
4616
- t.turn = s.variant === "hearts" ? t.phase === "play" ? hands.findIndex((h) => h.includes(0)) : 0 : first(t);
4617
- return t;
4618
- }
4619
- function createTrickCards(variant, count, entropy2) {
4620
- if (!index(count - 2, 3) || variant !== "oh-hell" && count !== 4)
4621
- throw Error("Choose the required player count.");
4622
- return deal(
4623
- {
4624
- variant,
4625
- turn: 0,
4626
- dealer: Math.floor(draws(entropy2, 1)[0] * count),
4627
- round: 0,
4628
- size: 13,
4629
- phase: "bid",
4630
- hands: Array.from({ length: count }, () => []),
4631
- stock: [],
4632
- trick: [],
4633
- lastTrick: [],
4634
- tricks: [],
4635
- penalties: [],
4636
- bids: [],
4637
- blind: [],
4638
- blindAsked: [],
4639
- selected: [],
4640
- passed: [],
4641
- scores: Array(variant === "spades" ? 2 : count).fill(0),
4642
- bags: [0, 0],
4643
- trump: null,
4644
- broken: false,
4645
- winner: null,
4646
- result: ""
4647
- },
4648
- entropy2
4649
- );
4650
- }
4651
- function trickLegalCards(s) {
4652
- if (s.phase !== "play") return [];
4653
- const hand2 = s.hands[s.turn].filter((c) => c !== null), lead = s.trick[0];
4654
- if (lead) {
4655
- const suited = hand2.filter((c) => cardSuit(c) === cardSuit(lead.card));
4656
- if (suited.length) return suited;
4657
- if (s.variant === "hearts" && s.tricks.every((n) => n === 0)) {
4658
- const clean = hand2.filter((c) => !penalty(c));
4659
- if (clean.length) return clean;
4660
- }
4661
- return hand2;
4662
- }
4663
- if (s.variant === "hearts") {
4664
- if (s.tricks.every((n) => n === 0)) return hand2.includes(0) ? [0] : [];
4665
- if (!s.broken) {
4666
- const clean = hand2.filter((c) => cardSuit(c) !== 2);
4667
- if (clean.length) return clean;
4668
- }
4669
- }
4670
- if (s.variant === "spades" && !s.broken) {
4671
- const clean = hand2.filter((c) => cardSuit(c) !== 3);
4672
- if (clean.length) return clean;
4673
- }
4674
- return hand2;
4675
- }
4676
- function finish(s) {
4677
- if (s.variant === "hearts") {
4678
- s.scores = s.scores.map((n, i) => n + s.penalties[i]);
4679
- s.result = `Round penalties: ${s.penalties.join(" \xB7 ")}`;
4680
- if (s.scores.some((n) => n >= 100)) {
4681
- s.phase = "finished";
4682
- s.winner = best(s.scores.map((n) => -n));
4683
- } else s.phase = "round";
4684
- } else if (s.variant === "spades") {
4685
- const gains = [0, 0];
4686
- for (const team2 of [0, 1]) {
4687
- const seats2 = [team2, team2 + 2], bid = seats2.reduce((n, i) => n + s.bids[i], 0), taken = seats2.filter((i) => s.bids[i] !== 0).reduce((n, i) => n + s.tricks[i], 0);
4688
- gains[team2] = taken >= bid ? bid * 10 + Math.max(0, taken - bid) : -bid * 10;
4689
- const bags = Math.max(0, taken - bid) + seats2.filter((i) => s.bids[i] === 0).reduce((n, i) => n + s.tricks[i], 0);
4690
- s.bags[team2] += bags;
4691
- gains[team2] -= 100 * Math.floor(s.bags[team2] / 10);
4692
- s.bags[team2] %= 10;
4693
- for (const i of seats2)
4694
- if (s.bids[i] === 0)
4695
- gains[team2] += (s.tricks[i] === 0 ? 1 : -1) * (s.blind[i] ? 200 : 100);
4696
- }
4697
- s.scores = s.scores.map((n, i) => n + gains[i]);
4698
- s.result = `Team 1 & 3: ${gains[0]} \xB7 Team 2 & 4: ${gains[1]}`;
4699
- if (s.scores.some((n) => n >= 500 || n <= -200)) {
4700
- s.phase = "finished";
4701
- s.winner = best(s.scores);
4702
- } else s.phase = "round";
4703
- } else {
4704
- s.scores = s.scores.map(
4705
- (n, i) => n + (s.bids[i] === s.tricks[i] ? 10 + s.tricks[i] : 0)
4706
- );
4707
- s.result = `Tricks taken: ${s.tricks.join(" \xB7 ")}`;
4708
- const rounds = (s.hands.length === 4 ? 13 : 17) + s.hands.length - 1;
4709
- if (s.round + 1 >= rounds) {
4710
- s.phase = "finished";
4711
- s.winner = best(s.scores);
4712
- } else s.phase = "round";
4713
- }
4714
- s.turn = first(s);
4715
- }
4716
- function playTrickCards(s, type, at, amount, entropy2) {
4717
- if (s.phase === "finished") throw Error("This match is finished.");
4718
- if (type === "next" && s.phase === "round")
4719
- return deal(
4720
- { ...s, round: s.round + 1, dealer: (s.dealer + 1) % s.hands.length },
4721
- entropy2
4722
- );
4723
- const t = structuredClone(s), seat = s.turn, count = s.hands.length;
4724
- if (s.phase === "pass") {
4725
- if (type === "select" && at !== void 0 && s.hands[seat].includes(at)) {
4726
- const selected = t.selected[seat];
4727
- if (selected.includes(at))
4728
- t.selected[seat] = selected.filter((c) => c !== at);
4729
- else if (selected.length < 3) selected.push(at);
4730
- else throw Error("Select three cards.");
4731
- return t;
4732
- }
4733
- if (type !== "pass" || t.selected[seat].length !== 3)
4734
- throw Error("Choose three cards to pass.");
4735
- t.passed[seat] = true;
4736
- if (t.passed.every(Boolean)) {
4737
- const offset = [1, 3, 2][s.round % 4];
4738
- t.hands = t.hands.map(
4739
- (h, i) => h.filter((c) => !t.selected[i].includes(c))
4740
- );
4741
- t.selected.forEach(
4742
- (cards, i) => t.hands[(i + offset) % 4].push(...cards)
4743
- );
4744
- t.hands.forEach((h) => h.sort((a, b) => a - b));
4745
- t.selected = t.selected.map(() => []);
4746
- t.phase = "play";
4747
- t.turn = t.hands.findIndex((h) => h.includes(0));
4748
- } else t.turn = t.passed.findIndex((n) => !n);
4749
- return t;
4750
- }
4751
- if (s.phase === "blind") {
4752
- const eligible = s.scores[1 - seat % 2] - s.scores[seat % 2] >= 200;
4753
- if (type === "blind" && eligible) {
4754
- t.bids[seat] = 0;
4755
- t.blind[seat] = true;
4756
- } else if (type !== "look")
4757
- throw Error("Choose blind nil or look at your cards.");
4758
- t.blindAsked[seat] = true;
4759
- if (t.blindAsked.every(Boolean)) {
4760
- t.phase = "bid";
4761
- t.turn = first(t);
4762
- while (t.bids[t.turn] !== null) t.turn = (t.turn + 1) % count;
4763
- } else t.turn = (seat + 1) % count;
4764
- return t;
4765
- }
4766
- if (s.phase === "bid") {
4767
- if (type !== "bid" || amount === void 0 || !index(amount, s.size + 1))
4768
- throw Error("Choose a legal bid.");
4769
- if (s.variant === "oh-hell" && s.bids.filter((b) => b !== null).length === count - 1 && s.bids.reduce((n, b) => n + (b ?? 0), 0) + amount === s.size)
4770
- throw Error("The last bid cannot make the total equal the hand size.");
4771
- t.bids[seat] = amount;
4772
- if (t.bids.every((b) => b !== null)) {
4773
- t.phase = "play";
4774
- t.turn = first(t);
4775
- } else {
4776
- t.turn = (seat + 1) % count;
4777
- while (t.bids[t.turn] !== null) t.turn = (t.turn + 1) % count;
4778
- }
4779
- return t;
4780
- }
4781
- if (s.phase === "moon") {
4782
- if (type !== "moon" || ![0, 1].includes(amount ?? -1))
4783
- throw Error("Choose how to score the moon.");
4784
- t.penalties = t.penalties.map(
4785
- (_, i) => amount === 0 ? i === seat ? -26 : 0 : i === seat ? 0 : 26
4786
- );
4787
- finish(t);
4788
- return t;
4789
- }
4790
- if (type !== "play" || at === void 0 || !trickLegalCards(s).includes(at))
4791
- throw Error("Choose a legal card.");
4792
- t.hands[seat] = t.hands[seat].filter((c) => c !== at);
4793
- t.trick.push({ seat, card: at });
4794
- if (s.variant === "spades" && cardSuit(at) === 3 && s.trick.length > 0 && cardSuit(s.trick[0].card) !== 3 || s.variant === "hearts" && (cardSuit(at) === 2 || at === 49))
4795
- t.broken = true;
4796
- t.turn = (seat + 1) % count;
4797
- if (t.trick.length === count) {
4798
- const lead = cardSuit(t.trick[0].card), strength = (c) => (cardSuit(c) === s.trump ? 100 : cardSuit(c) === lead ? 50 : 0) + cardRank(c);
4799
- const winner2 = t.trick.reduce(
4800
- (a, b) => strength(b.card) > strength(a.card) ? b : a
4801
- ).seat;
4802
- t.tricks[winner2]++;
4803
- t.penalties[winner2] += t.trick.reduce((n, c) => n + penalty(c.card), 0);
4804
- t.lastTrick = t.trick;
4805
- t.trick = [];
4806
- t.turn = winner2;
4807
- if (t.hands.every((h) => !h.length)) {
4808
- const moon = s.variant === "hearts" ? t.penalties.findIndex((n) => n === 26) : -1;
4809
- if (moon >= 0) {
4810
- t.phase = "moon";
4811
- t.turn = moon;
4812
- } else finish(t);
4813
- }
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.");
4814
5046
  }
4815
- return t;
4816
5047
  }
4817
- function trickView(s, seat) {
4818
- const { stock, selected, ...publicState } = s;
4819
- return {
4820
- ...publicState,
4821
- stockCount: stock.length,
4822
- selected: selected.map((cards, i) => i === seat ? cards : []),
4823
- hands: s.hands.map(
4824
- (h, i) => i === seat && !(s.phase === "blind" && !s.blindAsked[i]) ? h : h.map(() => null)
4825
- )
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]
4826
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;
4827
5093
  }
4828
5094
 
4829
- // lib/multiplayer/cribbage.ts
4830
- var cribbageLabel = (c) => ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"][c % 13] + ["\u2663", "\u2666", "\u2665", "\u2660"][Math.floor(c / 13)];
5095
+ // lib/agents/action-help.ts
5096
+ var help = {
5097
+ amazons: "move: numeric from/to 0\u201399 and amount for the arrow square. Select an exact legal_wire_actions entry.",
5098
+ "lines-of-action": "move: numeric from/to 0\u201363. Move the count of all stones along the full line; enemy stones block jumping.",
5099
+ "last-greenhouse": "share: publish your current private journal observation. pass: hand controls to partner. choose: at 0\u20133 selects a room setting. All choices are listed, including incorrect answers. Cooperate using both journals.",
5100
+ 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
+ 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
+ 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.`,
5104
+ 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
+ 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
+ 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.",
5108
+ "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
+ chess: "move: from/to are algebraic squares; promotion is q/r/b/n.",
5110
+ "atomic-chess": "move: from/to are algebraic squares; promotion is q/r/b/n. Explosions follow the game rules.",
5111
+ "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.",
5113
+ "brazilian-checkers": "move: from/to are zero-based squares (0\u201363); maximum captures apply.",
5114
+ "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.",
5118
+ "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.",
5120
+ 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
+ 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
+ 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
+ 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
+ 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
+ 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.",
5127
+ 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
+ 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
+ xiangqi: "move: numeric zero-based row-major from/to on the 9\xD710 board. promotion is unused.",
5130
+ 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.",
5132
+ 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
+ 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
+ "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
+ "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
+ 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.",
5138
+ "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.",
5140
+ 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
+ "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
+ 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.",
5144
+ 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.",
5146
+ 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
+ "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
+ "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.",
5149
+ "property-trading": "roll/end/buy/auction/pay/bankrupt/pass/reject have phase-specific effects. bid: amount is the bid. mortgage/redeem/build/sell: at selects a property. tax: amount is 0 or 1. release: amount 0 pays fifty, 1 uses a release card. offer: tradeOffer contains seat,give,take,giveCash,takeCash; accept confirms the current trade."
5150
+ };
5151
+ function actionHelp(game) {
5152
+ return { format: `game play --room CODE --revision R --action '{"type":"...",...}'`, indexing: "Board and hand-position indexes are zero-based. Card IDs are the actual values in visible_state, not offsets into a hand array. Convenience --row/--column/--edge/--pit flags are one-based. Inspect the exact player-visible state; do not guess hidden cards or board indexes.", game: help[game], validation: "action_types lists permitted command families, not currently legal moves. The server enforces phases, turns, ownership and legality. After a rejected or uncertain action read status again; never retry blindly." };
5153
+ }
4831
5154
 
4832
5155
  // lib/multiplayer/euchre.ts
4833
5156
  var euchreSuit = (card2) => Math.floor(card2 / 6);
@@ -4856,7 +5179,7 @@ function redFromHand(t, seat, replace) {
4856
5179
  }
4857
5180
  return true;
4858
5181
  }
4859
- function deal2(s, entropy2) {
5182
+ function deal3(s, entropy2) {
4860
5183
  const t = structuredClone(s), deck = shuffled(
4861
5184
  Array.from({ length: 108 }, (_, i) => i),
4862
5185
  entropy2
@@ -4879,7 +5202,7 @@ function deal2(s, entropy2) {
4879
5202
  }
4880
5203
  function createCanasta(count, entropy2) {
4881
5204
  if (count !== 2 && count !== 4) throw Error("Choose two or four players.");
4882
- return deal2(
5205
+ return deal3(
4883
5206
  {
4884
5207
  hands: Array.from({ length: count }, () => []),
4885
5208
  stock: [],
@@ -4958,7 +5281,7 @@ function playCanasta(s, a, entropy2) {
4958
5281
  if (a.type === "next" && s.phase === "round") {
4959
5282
  t.round++;
4960
5283
  t.dealer = (t.dealer + 1) % s.hands.length;
4961
- return deal2(t, entropy2);
5284
+ return deal3(t, entropy2);
4962
5285
  }
4963
5286
  if (a.type === "stock" && s.phase === "draw") {
4964
5287
  t.openingTurn = !s.opened[i];
@@ -5102,7 +5425,7 @@ function binokelMeld(hand2, trump) {
5102
5425
  add([16, 5], [0, 4, 30, 60, 90], "binokel");
5103
5426
  return { score, labels: labels2, shown: [...shown] };
5104
5427
  }
5105
- function deal3(s, entropy2) {
5428
+ function deal4(s, entropy2) {
5106
5429
  const t = structuredClone(s), deck = shuffled(
5107
5430
  Array.from({ length: 80 }, (_, i) => i),
5108
5431
  entropy2
@@ -5125,7 +5448,7 @@ function deal3(s, entropy2) {
5125
5448
  return t;
5126
5449
  }
5127
5450
  function createBinokel(entropy2) {
5128
- return deal3(
5451
+ return deal4(
5129
5452
  {
5130
5453
  hands: [[], [], [], []],
5131
5454
  turn: 0,
@@ -5173,7 +5496,7 @@ function playBinokel(s, a, entropy2) {
5173
5496
  if (a.type === "next" && s.phase === "round") {
5174
5497
  t.round++;
5175
5498
  t.dealer = (t.dealer + 1) % 4;
5176
- return deal3(t, entropy2);
5499
+ return deal4(t, entropy2);
5177
5500
  }
5178
5501
  if (s.phase === "auction") {
5179
5502
  if (a.type === "bid" && a.amount !== void 0 && Number.isInteger(a.amount) && a.amount > s.bid && (a.amount < 60 || a.amount % 5 === 0) && a.amount <= 1e3) {
@@ -5347,7 +5670,7 @@ function nextSeat(s, from, eligible) {
5347
5670
  return -1;
5348
5671
  }
5349
5672
  var canAct = (p) => !p.folded && !p.out && p.stack > 0;
5350
- function deal4(s, n) {
5673
+ function deal5(s, n) {
5351
5674
  s.deck.pop();
5352
5675
  for (let i = 0; i < n; i++) s.community.push(s.deck.pop());
5353
5676
  }
@@ -5412,13 +5735,13 @@ function advance(s) {
5412
5735
  }
5413
5736
  if (s.phase === "river") return award(s);
5414
5737
  if (s.phase === "preflop") {
5415
- deal4(s, 3);
5738
+ deal5(s, 3);
5416
5739
  s.phase = "flop";
5417
5740
  } else if (s.phase === "flop") {
5418
- deal4(s, 1);
5741
+ deal5(s, 1);
5419
5742
  s.phase = "turn";
5420
5743
  } else {
5421
- deal4(s, 1);
5744
+ deal5(s, 1);
5422
5745
  s.phase = "river";
5423
5746
  }
5424
5747
  s.bet = 0;
@@ -7980,128 +8303,30 @@ function playBackgammon(s, type, from, to, die, entropy2) {
7980
8303
  };
7981
8304
  return backgammonMoves(next4).length ? next4 : {
7982
8305
  ...next4,
7983
- dice: [],
7984
- turn: turn === 0 ? 1 : 0,
7985
- result: "All moves are blocked. Turn passes."
7986
- };
7987
- }
7988
- const move2 = backgammonMoves(s).find(
7989
- (m) => m.from === from && m.to === to && m.die === die
7990
- );
7991
- if (type !== "move" || !move2)
7992
- throw Error(
7993
- "Use the maximum number of dice; if only one can be used, use the higher die."
7994
- );
7995
- const next3 = step(s, move2);
7996
- if (next3.off[s.turn] === 15) {
7997
- const opponent = 1 - s.turn, backgammon = next3.bar[opponent] > 0 || next3.points.some(
7998
- (n, i) => (s.turn === 0 ? i >= 18 : i < 6) && n * (s.turn === 0 ? -1 : 1) > 0
7999
- );
8000
- next3.winner = s.turn;
8001
- next3.result = next3.off[opponent] ? "Single game." : backgammon ? "Backgammon \xB7 3 points." : "Gammon \xB7 2 points.";
8002
- } else if (!backgammonMoves(next3).length) {
8003
- next3.dice = [];
8004
- next3.turn = s.turn === 0 ? 1 : 0;
8005
- }
8006
- return next3;
8007
- }
8008
-
8009
- // lib/multiplayer/mills.ts
8010
- var millLines = [
8011
- [0, 1, 2],
8012
- [2, 3, 4],
8013
- [4, 5, 6],
8014
- [6, 7, 0],
8015
- [8, 9, 10],
8016
- [10, 11, 12],
8017
- [12, 13, 14],
8018
- [14, 15, 8],
8019
- [16, 17, 18],
8020
- [18, 19, 20],
8021
- [20, 21, 22],
8022
- [22, 23, 16],
8023
- [1, 9, 17],
8024
- [3, 11, 19],
8025
- [5, 13, 21],
8026
- [7, 15, 23]
8027
- ];
8028
- var millEdges = millLines.flatMap(([a, b, c]) => [
8029
- [a, b],
8030
- [b, c]
8031
- ]);
8032
- function createMills() {
8033
- return {
8034
- board: Array(24).fill(0),
8035
- reserves: [9, 9],
8036
- turn: 0,
8037
- captures: 0,
8038
- winner: null,
8039
- draw: false,
8040
- history: []
8041
- };
8042
- }
8043
- var linesAt = (board, at) => millLines.filter(
8044
- (line) => line.includes(at) && line.every((i) => board[i] === board[at])
8045
- );
8046
- function millCaptures(s) {
8047
- const opponents = s.board.flatMap((p, i) => p === 2 - s.turn ? [i] : []), loose = opponents.filter((at) => !linesAt(s.board, at).length);
8048
- return loose.length ? loose : opponents;
8049
- }
8050
- function millMoves(s) {
8051
- if (s.winner !== null || s.draw || s.captures) return [];
8052
- const empty = s.board.flatMap((p, i) => !p ? [i] : []);
8053
- if (s.reserves[s.turn]) return empty.map((to) => ({ from: null, to }));
8054
- const own = s.board.flatMap((p, i) => p === s.turn + 1 ? [i] : []);
8055
- return own.flatMap(
8056
- (from) => empty.filter(
8057
- (to) => own.length === 3 || millEdges.some(
8058
- ([a, b]) => a === from && b === to || b === from && a === to
8059
- )
8060
- ).map((to) => ({ from, to }))
8061
- );
8062
- }
8063
- function finishTurn(s) {
8064
- const turn = s.turn === 0 ? 1 : 0, key2 = s.board.join(",") + ":" + turn + ":" + s.reserves.join(",");
8065
- const next3 = {
8066
- ...s,
8067
- turn,
8068
- captures: 0,
8069
- history: [...s.history, key2]
8070
- };
8071
- const count = next3.board.filter((p) => p === turn + 1).length;
8072
- if (!next3.reserves[turn] && (count < 3 || !millMoves(next3).length))
8073
- next3.winner = s.turn;
8074
- else next3.draw = next3.history.filter((k) => k === key2).length >= 3;
8075
- return next3;
8076
- }
8077
- function playMills(s, type, to, from) {
8078
- if (!index(to, 24) || s.winner !== null || s.draw)
8079
- throw Error("Choose a legal point.");
8080
- const board = [...s.board], reserves = [...s.reserves];
8081
- if (type === "capture") {
8082
- if (!s.captures || !millCaptures(s).includes(to))
8083
- throw Error(
8084
- "Capture a piece outside a mill, unless all pieces are in mills."
8085
- );
8086
- board[to] = 0;
8087
- const next4 = { ...s, board, captures: s.captures - 1 };
8088
- return next4.captures && millCaptures(next4).length ? next4 : finishTurn(next4);
8306
+ dice: [],
8307
+ turn: turn === 0 ? 1 : 0,
8308
+ result: "All moves are blocked. Turn passes."
8309
+ };
8089
8310
  }
8090
- if (!["place", "move"].includes(type) || !millMoves(s).some(
8091
- (m) => m.to === to && m.from === (type === "place" ? null : from)
8092
- ))
8311
+ const move2 = backgammonMoves(s).find(
8312
+ (m) => m.from === from && m.to === to && m.die === die
8313
+ );
8314
+ if (type !== "move" || !move2)
8093
8315
  throw Error(
8094
- "Place from your reserve or move to a connected point. With three pieces you may fly."
8316
+ "Use the maximum number of dice; if only one can be used, use the higher die."
8095
8317
  );
8096
- if (type === "place") reserves[s.turn]--;
8097
- else board[from] = 0;
8098
- board[to] = s.turn + 1;
8099
- const captures = Math.min(
8100
- linesAt(board, to).length,
8101
- board.filter((p) => p === 2 - s.turn).length
8102
- );
8103
- const next3 = { ...s, board, reserves, captures };
8104
- return captures ? next3 : finishTurn(next3);
8318
+ const next3 = step(s, move2);
8319
+ if (next3.off[s.turn] === 15) {
8320
+ const opponent = 1 - s.turn, backgammon = next3.bar[opponent] > 0 || next3.points.some(
8321
+ (n, i) => (s.turn === 0 ? i >= 18 : i < 6) && n * (s.turn === 0 ? -1 : 1) > 0
8322
+ );
8323
+ next3.winner = s.turn;
8324
+ next3.result = next3.off[opponent] ? "Single game." : backgammon ? "Backgammon \xB7 3 points." : "Gammon \xB7 2 points.";
8325
+ } else if (!backgammonMoves(next3).length) {
8326
+ next3.dice = [];
8327
+ next3.turn = s.turn === 0 ? 1 : 0;
8328
+ }
8329
+ return next3;
8105
8330
  }
8106
8331
 
8107
8332
  // lib/multiplayer/football.ts
@@ -8372,13 +8597,13 @@ function goAction(s, seat, action, at) {
8372
8597
  }
8373
8598
  const accepted = [.../* @__PURE__ */ new Set([...s.accepted, seat])];
8374
8599
  if (accepted.length < 2) return { ...s, accepted };
8375
- const scores = scoreGo(s);
8600
+ const scores2 = scoreGo(s);
8376
8601
  return {
8377
8602
  ...s,
8378
8603
  accepted,
8379
- scores,
8604
+ scores: scores2,
8380
8605
  phase: "finished",
8381
- winner: scores[0] > scores[1] ? 0 : 1
8606
+ winner: scores2[0] > scores2[1] ? 0 : 1
8382
8607
  };
8383
8608
  }
8384
8609
  function goProposalKey(s) {
@@ -8546,7 +8771,7 @@ var adapter8 = defineMatchAdapter({
8546
8771
  return {
8547
8772
  finished: state.winner !== null || state.draw,
8548
8773
  winner: state.winner,
8549
- label: state.draw ? "Draw" : state.winner !== null ? `Player ${state.winner + 1} wins \xB7 the opponent is down to two pieces or blocked.` : ""
8774
+ label: state.draw ? "Draw" : state.winner !== null ? `Player ${state.winner + 1} wins \xB7 ${state.board.filter((p) => p === 2 - state.winner).length < 3 ? "the opponent is down to two pieces" : "the opponent has no legal move"}.` : ""
8550
8775
  };
8551
8776
  },
8552
8777
  apply(state, _seat, action, _entropy, _now) {
@@ -8733,9 +8958,9 @@ function loserResult(count, loser) {
8733
8958
  validSeat(count, loser);
8734
8959
  return { sides: Array.from({ length: count }, (_, seat) => ({ seats: [seat], place: seat === loser ? 1 : 0 })) };
8735
8960
  }
8736
- function scoreResult(scores, winner2, lowerWins = false) {
8737
- validSeat(scores.length, winner2);
8738
- const values = scores.map((n) => lowerWins ? -n : n);
8961
+ function scoreResult(scores2, winner2, lowerWins = false) {
8962
+ validSeat(scores2.length, winner2);
8963
+ const values = scores2.map((n) => lowerWins ? -n : n);
8739
8964
  return { sides: values.map((value, seat) => ({ seats: [seat], place: seat === winner2 ? 0 : Number(winner2 !== null) + values.filter((n, i) => i !== winner2 && n > value).length })) };
8740
8965
  }
8741
8966
  function partnershipResult(count, winner2) {
@@ -8752,7 +8977,7 @@ function next(s, seat) {
8752
8977
  const i = s.players.indexOf(seat);
8753
8978
  return s.players[(i + 1) % s.players.length];
8754
8979
  }
8755
- function deal5(s, entropy2) {
8980
+ function deal6(s, entropy2) {
8756
8981
  const t = structuredClone(s), count = s.hands.length, deck = shuffled(
8757
8982
  Array.from({ length: 52 }, (_, i) => i).filter((c) => cardRank(c) >= 9),
8758
8983
  entropy2
@@ -8784,7 +9009,7 @@ function deal5(s, entropy2) {
8784
9009
  }
8785
9010
  function createThousand(count, entropy2) {
8786
9011
  if (!index(count - 2, 3)) throw Error("Choose two to four players.");
8787
- return deal5(
9012
+ return deal6(
8788
9013
  {
8789
9014
  hands: Array.from({ length: count }, () => []),
8790
9015
  kitties: [],
@@ -8836,10 +9061,10 @@ function playThousand(s, a, entropy2) {
8836
9061
  if (a.type === "next" && s.phase === "round") {
8837
9062
  t.round++;
8838
9063
  t.dealer = (s.dealer + 1) % s.hands.length;
8839
- return deal5(t, entropy2);
9064
+ return deal6(t, entropy2);
8840
9065
  }
8841
9066
  if (a.type === "redeal" && s.phase === "auction" && s.hands[seat].filter((c) => cardRank(c) === 9).length === 4)
8842
- return deal5(t, entropy2);
9067
+ return deal6(t, entropy2);
8843
9068
  if (s.phase === "auction") {
8844
9069
  if (a.type === "bid" && a.amount !== void 0 && Number.isInteger(a.amount) && a.amount % 10 === 0 && a.amount > s.bid && a.amount <= thousandMaxBid(s.hands[seat])) {
8845
9070
  t.bid = a.amount;
@@ -9009,7 +9234,7 @@ var skatBids = [
9009
9234
  )
9010
9235
  ])
9011
9236
  ].filter((n) => n <= 264).sort((a, b) => a - b);
9012
- function deal6(s, entropy2) {
9237
+ function deal7(s, entropy2) {
9013
9238
  const t = structuredClone(s), deck = shuffled(
9014
9239
  Array.from({ length: 52 }, (_, i) => i).filter((c) => cardRank(c) >= 7),
9015
9240
  entropy2
@@ -9043,7 +9268,7 @@ function deal6(s, entropy2) {
9043
9268
  return t;
9044
9269
  }
9045
9270
  function createSkat(entropy2) {
9046
- return deal6(
9271
+ return deal7(
9047
9272
  {
9048
9273
  hands: [[], [], []],
9049
9274
  skat: [],
@@ -9144,7 +9369,7 @@ function playSkat(s, a, entropy2) {
9144
9369
  if (a.type === "next" && s.phase === "round") {
9145
9370
  t.round++;
9146
9371
  t.dealer = (s.dealer + 1) % 3;
9147
- return deal6(t, entropy2);
9372
+ return deal7(t, entropy2);
9148
9373
  }
9149
9374
  if (s.phase === "auction") {
9150
9375
  if (s.offer === null) {
@@ -9305,7 +9530,7 @@ function sides(t) {
9305
9530
  t.old = t.hands.map((_, i) => i === t.declarer);
9306
9531
  else t.old = t.hands.map((h) => queens.some((c) => h.includes(c)));
9307
9532
  }
9308
- function deal7(s, entropy2) {
9533
+ function deal8(s, entropy2) {
9309
9534
  const t = structuredClone(s), deck = shuffled(
9310
9535
  Array.from({ length: 52 }, (_, i) => i).filter(
9311
9536
  (c) => [10, 11, 12, 14].includes(cardRank(c))
@@ -9346,7 +9571,7 @@ function deal7(s, entropy2) {
9346
9571
  return t;
9347
9572
  }
9348
9573
  function createBaska(entropy2) {
9349
- return deal7(
9574
+ return deal8(
9350
9575
  {
9351
9576
  hands: [[], [], [], []],
9352
9577
  turn: 0,
@@ -9415,7 +9640,7 @@ function playBaska(s, a, entropy2) {
9415
9640
  if (a.type === "next" && s.phase === "round") {
9416
9641
  t.round++;
9417
9642
  t.dealer = (t.dealer + 1) % 4;
9418
- return deal7(t, entropy2);
9643
+ return deal8(t, entropy2);
9419
9644
  }
9420
9645
  if (a.type === "announce" && s.phase === "honor") {
9421
9646
  finish5(t, true);
@@ -9535,7 +9760,7 @@ var adapter17 = defineMatchAdapter({
9535
9760
 
9536
9761
  // lib/multiplayer/bridge.ts
9537
9762
  var vulnerability = [0, 1, 2, 3, 1, 2, 3, 0, 2, 3, 0, 1, 3, 0, 1, 2];
9538
- function deal8(s, entropy2) {
9763
+ function deal9(s, entropy2) {
9539
9764
  const t = structuredClone(s), deck = shuffled(
9540
9765
  Array.from({ length: 52 }, (_, i) => i),
9541
9766
  entropy2
@@ -9563,7 +9788,7 @@ function deal8(s, entropy2) {
9563
9788
  return t;
9564
9789
  }
9565
9790
  function createBridge(entropy2) {
9566
- return deal8(
9791
+ return deal9(
9567
9792
  {
9568
9793
  hands: [[], [], [], []],
9569
9794
  turn: 0,
@@ -9616,7 +9841,7 @@ function playBridge(s, a, entropy2) {
9616
9841
  if (a.type === "next" && s.phase === "round") {
9617
9842
  t.board++;
9618
9843
  t.dealer = (t.dealer + 1) % 4;
9619
- return deal8(t, entropy2);
9844
+ return deal9(t, entropy2);
9620
9845
  }
9621
9846
  if (s.phase === "auction") {
9622
9847
  if (a.type === "bid" && index((a.amount ?? 0) - 1, 35)) {
@@ -9722,7 +9947,7 @@ var adapter18 = defineMatchAdapter({
9722
9947
  });
9723
9948
 
9724
9949
  // lib/multiplayer/polish-hearts.ts
9725
- function deal9(s, entropy2) {
9950
+ function deal10(s, entropy2) {
9726
9951
  const t = structuredClone(s), count = s.hands.length, deck = shuffled(
9727
9952
  Array.from({ length: 52 }, (_, i) => i).filter(
9728
9953
  (c) => count !== 3 || c !== 0
@@ -9749,7 +9974,7 @@ function deal9(s, entropy2) {
9749
9974
  }
9750
9975
  function createPolish(count, entropy2) {
9751
9976
  if (count !== 3 && count !== 4) throw Error("Choose three or four players.");
9752
- return deal9(
9977
+ return deal10(
9753
9978
  {
9754
9979
  hands: Array.from({ length: count }, () => []),
9755
9980
  rest: [],
@@ -9813,7 +10038,7 @@ function playPolish(s, a, entropy2) {
9813
10038
  if (a.type === "next" && s.phase === "round") {
9814
10039
  t.round++;
9815
10040
  t.dealer = (t.dealer + 1) % count;
9816
- return deal9(t, entropy2);
10041
+ return deal10(t, entropy2);
9817
10042
  }
9818
10043
  if (a.type === "trump" && s.phase === "trump" && index(a.amount ?? -1, 4)) {
9819
10044
  t.trump = a.amount;
@@ -10286,7 +10511,7 @@ var adapter21 = defineMatchAdapter({
10286
10511
  });
10287
10512
 
10288
10513
  // lib/multiplayer/three-five-eight.ts
10289
- function deal10(s, entropy2) {
10514
+ function deal11(s, entropy2) {
10290
10515
  const t = structuredClone(s), deck = shuffled(
10291
10516
  Array.from({ length: 52 }, (_, i) => i),
10292
10517
  entropy2
@@ -10323,7 +10548,7 @@ function deal10(s, entropy2) {
10323
10548
  return t;
10324
10549
  }
10325
10550
  function createThree(entropy2) {
10326
- return deal10(
10551
+ return deal11(
10327
10552
  {
10328
10553
  hands: [[], [], []],
10329
10554
  kitty: [],
@@ -10362,7 +10587,7 @@ function playThree(s, a, entropy2) {
10362
10587
  if (a.type === "next" && s.phase === "round") {
10363
10588
  t.round++;
10364
10589
  t.dealer = (s.dealer + 1) % 3;
10365
- return deal10(t, entropy2);
10590
+ return deal11(t, entropy2);
10366
10591
  }
10367
10592
  if (s.phase === "exchange" && a.type === "exchange" && a.at !== void 0 && s.hands[s.turn].includes(a.at)) {
10368
10593
  const q = t.exchange[0], suit = cardSuit(a.at), other = t.hands[q.to].filter((c) => cardSuit(c) === suit).sort((a2, b) => cardRank(b) - cardRank(a2)), returned = other[0];
@@ -11143,7 +11368,7 @@ var adapter26 = defineMatchAdapter({
11143
11368
 
11144
11369
  // lib/multiplayer/pan.ts
11145
11370
  var nineHearts = 33;
11146
- function deal11(count, entropy2, letters, round) {
11371
+ function deal12(count, entropy2, letters, round) {
11147
11372
  const cards = shuffled(
11148
11373
  Array.from({ length: 52 }, (_, i) => i).filter((c) => cardRank(c) >= 9),
11149
11374
  entropy2
@@ -11165,7 +11390,7 @@ function deal11(count, entropy2, letters, round) {
11165
11390
  }
11166
11391
  function createPan(count, entropy2) {
11167
11392
  if (!index(count - 2, 3)) throw Error("Choose two to four players.");
11168
- return deal11(count, entropy2, Array(count).fill(0), 0);
11393
+ return deal12(count, entropy2, Array(count).fill(0), 0);
11169
11394
  }
11170
11395
  function panLegal(s, cards) {
11171
11396
  const hand2 = s.hands[s.turn];
@@ -11181,7 +11406,7 @@ function panLegal(s, cards) {
11181
11406
  }
11182
11407
  function playPan(s, type, cards, entropy2) {
11183
11408
  if (type === "next" && s.phase === "round")
11184
- return deal11(s.hands.length, entropy2, s.letters, s.round + 1);
11409
+ return deal12(s.hands.length, entropy2, s.letters, s.round + 1);
11185
11410
  if (s.phase !== "play") throw Error("This round is finished.");
11186
11411
  const t = structuredClone(s), seat = s.turn;
11187
11412
  if (type === "play" && panLegal(s, cards)) {
@@ -11251,189 +11476,6 @@ var adapter27 = defineMatchAdapter({
11251
11476
  }
11252
11477
  });
11253
11478
 
11254
- // lib/multiplayer/gin-rummy.ts
11255
- var ginRank = (c) => cardRank(c) === 14 ? 1 : cardRank(c);
11256
- var ginValue = (c) => Math.min(10, ginRank(c));
11257
- function ginMelds(hand2) {
11258
- const melds = [];
11259
- for (let rank2 = 1; rank2 <= 13; rank2++) {
11260
- const cards = hand2.filter((c) => ginRank(c) === rank2);
11261
- if (cards.length >= 3) {
11262
- if (cards.length === 4) {
11263
- melds.push(cards);
11264
- for (let i = 0; i < 4; i++) melds.push(cards.filter((_, j) => j !== i));
11265
- } else melds.push(cards);
11266
- }
11267
- }
11268
- for (let suit = 0; suit < 4; suit++) {
11269
- const cards = hand2.filter((c) => cardSuit(c) === suit).sort((a, b) => ginRank(a) - ginRank(b));
11270
- for (let start = 0; start < cards.length; start++)
11271
- for (let end = start + 2; end < cards.length; end++) {
11272
- const slice = cards.slice(start, end + 1);
11273
- if (slice.every(
11274
- (c, i) => i === 0 || ginRank(c) === ginRank(slice[i - 1]) + 1
11275
- ))
11276
- melds.push(slice);
11277
- else break;
11278
- }
11279
- }
11280
- return melds;
11281
- }
11282
- function partitions(hand2) {
11283
- const melds = ginMelds(hand2), out2 = [];
11284
- function visit(start, used, chosen) {
11285
- const deadwood2 = hand2.filter((c) => !used.has(c));
11286
- out2.push({
11287
- melds: chosen,
11288
- deadwood: deadwood2,
11289
- value: deadwood2.reduce((n, c) => n + ginValue(c), 0)
11290
- });
11291
- for (let i = start; i < melds.length; i++)
11292
- if (melds[i].every((c) => !used.has(c)))
11293
- visit(i + 1, /* @__PURE__ */ new Set([...used, ...melds[i]]), [...chosen, melds[i]]);
11294
- }
11295
- visit(0, /* @__PURE__ */ new Set(), []);
11296
- return out2;
11297
- }
11298
- function ginLayout(hand2) {
11299
- return partitions(hand2).reduce((a, b) => b.value < a.value ? b : a);
11300
- }
11301
- function ginLayoffValue(hand2, onto) {
11302
- const cache = /* @__PURE__ */ new Map();
11303
- function attach(cards, melds) {
11304
- const key2 = cards.join(",") + "|" + melds.map((m) => m.join(",")).join(";"), cached = cache.get(key2);
11305
- if (cached !== void 0) return cached;
11306
- let best2 = cards.reduce((n, c) => n + ginValue(c), 0);
11307
- cards.forEach(
11308
- (c, i) => melds.forEach((m, j) => {
11309
- const set = m.every((x) => ginRank(x) === ginRank(m[0])), ranks = m.map(ginRank), fits = set ? m.length === 3 && ginRank(c) === ranks[0] : cardSuit(c) === cardSuit(m[0]) && (ginRank(c) === Math.min(...ranks) - 1 || ginRank(c) === Math.max(...ranks) + 1);
11310
- if (fits) {
11311
- const next3 = melds.map(
11312
- (m2, k) => k === j ? [...m2, c].sort((a, b) => ginRank(a) - ginRank(b)) : m2
11313
- );
11314
- best2 = Math.min(
11315
- best2,
11316
- attach(
11317
- cards.filter((_, k) => k !== i),
11318
- next3
11319
- )
11320
- );
11321
- }
11322
- })
11323
- );
11324
- cache.set(key2, best2);
11325
- return best2;
11326
- }
11327
- return Math.min(...partitions(hand2).map((p) => attach(p.deadwood, onto)));
11328
- }
11329
- function deal12(dealer, scores, wins, round, entropy2) {
11330
- const deck = shuffled(
11331
- Array.from({ length: 52 }, (_, i) => i),
11332
- entropy2
11333
- );
11334
- return {
11335
- hands: [
11336
- deck.slice(0, 10).sort((a, b) => a - b),
11337
- deck.slice(10, 20).sort((a, b) => a - b)
11338
- ],
11339
- stock: deck.slice(21),
11340
- discard: [deck[20]],
11341
- turn: 1 - dealer,
11342
- dealer,
11343
- phase: "offer",
11344
- offers: 0,
11345
- drawnDiscard: null,
11346
- scores,
11347
- wins,
11348
- winner: null,
11349
- round,
11350
- result: "",
11351
- revealed: [],
11352
- margin: null
11353
- };
11354
- }
11355
- function createGin(entropy2) {
11356
- return deal12(Math.floor(draws(entropy2, 1)[0] * 2), [0, 0], [0, 0], 0, entropy2);
11357
- }
11358
- function playGin(s, type, at, entropy2) {
11359
- if (type === "next" && s.phase === "round")
11360
- return deal12(
11361
- s.result.startsWith("Stock") ? s.dealer : 1 - s.dealer,
11362
- s.scores,
11363
- s.wins,
11364
- s.round + 1,
11365
- entropy2
11366
- );
11367
- if (["finished", "round"].includes(s.phase))
11368
- throw Error("This round is finished.");
11369
- const t = structuredClone(s), seat = s.turn;
11370
- if (type === "pass" && s.phase === "offer") {
11371
- t.offers++;
11372
- if (t.offers === 2) {
11373
- t.phase = "draw";
11374
- t.turn = 1 - s.dealer;
11375
- } else t.turn = 1 - seat;
11376
- return t;
11377
- }
11378
- if (["draw", "offer"].includes(s.phase) && ["stock", "upcard"].includes(type)) {
11379
- if (s.phase === "offer" && type === "stock")
11380
- throw Error("Take the upcard or pass the opening offer.");
11381
- if (s.phase === "draw" && s.offers === 2 && s.round === t.round && type === "upcard")
11382
- throw Error("Both players passed: draw from the stock.");
11383
- if (s.stock.length <= 2) {
11384
- t.phase = "round";
11385
- t.result = "Stock exhausted: round cancelled, same dealer.";
11386
- t.turn = s.dealer;
11387
- return t;
11388
- }
11389
- const card2 = type === "upcard" ? t.discard.pop() : t.stock.shift();
11390
- t.drawnDiscard = type === "upcard" ? card2 : null;
11391
- t.hands[seat].push(card2);
11392
- t.hands[seat].sort((a, b) => a - b);
11393
- t.phase = "discard";
11394
- t.offers = 0;
11395
- return t;
11396
- }
11397
- if (s.phase === "discard" && ["discard", "knock"].includes(type) && at !== void 0 && s.hands[seat].includes(at) && at !== s.drawnDiscard) {
11398
- t.hands[seat] = t.hands[seat].filter((c) => c !== at);
11399
- t.discard.push(at);
11400
- t.drawnDiscard = null;
11401
- if (type === "knock") {
11402
- const layout = ginLayout(t.hands[seat]);
11403
- if (layout.value > 10)
11404
- throw Error("Knock only with ten or fewer deadwood points.");
11405
- const other = 1 - seat, opp = layout.value === 0 ? ginLayout(t.hands[other]).value : ginLayoffValue(t.hands[other], layout.melds), winner2 = layout.value === 0 || layout.value < opp ? seat : other;
11406
- const points = layout.value === 0 ? opp + 25 : winner2 === seat ? opp - layout.value : layout.value - opp + 25;
11407
- t.scores[winner2] += points;
11408
- t.wins[winner2]++;
11409
- t.result = `${layout.value === 0 ? "Gin" : winner2 === seat ? "Knock" : "Undercut"}: Player ${winner2 + 1} scores ${points}. Deadwood ${layout.value} vs ${opp}.`;
11410
- t.revealed = t.hands.map((h) => ginLayout(h).melds);
11411
- t.phase = t.scores[winner2] >= 100 ? "finished" : "round";
11412
- t.turn = winner2;
11413
- if (t.phase === "finished") {
11414
- t.winner = winner2;
11415
- t.margin = t.scores[winner2] + 100 + (t.scores[other] === 0 ? 100 : 0) + 25 * t.wins[winner2] - t.scores[other] - 25 * t.wins[other];
11416
- t.result += ` Final margin including game and hand bonuses: ${t.margin}.`;
11417
- }
11418
- } else {
11419
- t.phase = "draw";
11420
- t.turn = 1 - seat;
11421
- }
11422
- return t;
11423
- }
11424
- throw Error("Choose a legal draw, discard or knock.");
11425
- }
11426
- function ginView(s, seat) {
11427
- const { stock, ...rest } = s;
11428
- return {
11429
- ...rest,
11430
- stockCount: stock.length,
11431
- hands: s.hands.map(
11432
- (h, i) => i === seat || ["round", "finished"].includes(s.phase) ? h : h.map(() => null)
11433
- )
11434
- };
11435
- }
11436
-
11437
11479
  // lib/multiplayer/adapters/gin-rummy.ts
11438
11480
  var adapter28 = defineMatchAdapter({
11439
11481
  actions: ["next", "pass", "stock", "upcard", "discard", "knock"],
@@ -11459,7 +11501,7 @@ var adapter28 = defineMatchAdapter({
11459
11501
  // lib/multiplayer/sixty-six.ts
11460
11502
  var sixtyStrength = (c) => [9, 11, 12, 13, 10, 14].indexOf(cardRank(c));
11461
11503
  var sixtyPoints = (c) => [0, 2, 3, 4, 10, 11][sixtyStrength(c)];
11462
- function deal13(dealer, scores, round, entropy2) {
11504
+ function deal13(dealer, scores2, round, entropy2) {
11463
11505
  const deck = shuffled(
11464
11506
  Array.from({ length: 52 }, (_, i) => i).filter((c) => cardRank(c) >= 9),
11465
11507
  entropy2
@@ -11481,7 +11523,7 @@ function deal13(dealer, scores, round, entropy2) {
11481
11523
  points: [0, 0],
11482
11524
  pending: [0, 0],
11483
11525
  tricks: [0, 0],
11484
- scores,
11526
+ scores: scores2,
11485
11527
  phase: "play",
11486
11528
  winner: null,
11487
11529
  result: "",
@@ -11705,13 +11747,13 @@ function dominoMoves(s) {
11705
11747
  });
11706
11748
  }
11707
11749
  function endRound(s, winner2, bonus) {
11708
- const scores = [...s.scores];
11709
- if (winner2 !== null) scores[winner2] += Math.round(bonus / 5) * 5;
11710
- const finished = scores.some((n) => n >= 100);
11750
+ const scores2 = [...s.scores];
11751
+ if (winner2 !== null) scores2[winner2] += Math.round(bonus / 5) * 5;
11752
+ const finished = scores2.some((n) => n >= 100);
11711
11753
  return {
11712
11754
  ...s,
11713
- scores,
11714
- winner: finished ? best(scores) : null,
11755
+ scores: scores2,
11756
+ winner: finished ? best(scores2) : null,
11715
11757
  phase: finished ? "finished" : "round",
11716
11758
  turn: winner2 ?? s.turn,
11717
11759
  result: winner2 === null ? "Blocked round: equal pip totals." : `Player ${winner2 + 1} wins the round.`
@@ -11727,7 +11769,7 @@ function playDomino(s, type, at, end, entropy2) {
11727
11769
  turn: s.turn
11728
11770
  };
11729
11771
  if (s.phase !== "play") throw Error("This round is finished.");
11730
- const hands = s.hands.map((h) => [...h]), stock = [...s.stock], chain = [...s.chain], scores = [...s.scores];
11772
+ const hands = s.hands.map((h) => [...h]), stock = [...s.stock], chain = [...s.chain], scores2 = [...s.scores];
11731
11773
  if (type === "draw") {
11732
11774
  if (dominoMoves(s).length)
11733
11775
  throw Error("Play a matching tile before drawing.");
@@ -11752,20 +11794,20 @@ function playDomino(s, type, at, end, entropy2) {
11752
11794
  }
11753
11795
  const left = chain[0], right = chain.at(-1);
11754
11796
  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]);
11755
- if (total % 5 === 0) scores[s.turn] += total;
11797
+ if (total % 5 === 0) scores2[s.turn] += total;
11756
11798
  const next3 = {
11757
11799
  ...s,
11758
11800
  hands,
11759
11801
  stock,
11760
11802
  chain,
11761
- scores,
11803
+ scores: scores2,
11762
11804
  passes: 0,
11763
11805
  turn: 1 - s.turn
11764
11806
  };
11765
11807
  if (!hands[s.turn].length)
11766
11808
  return endRound(next3, s.turn, pipSum(hands[1 - s.turn]));
11767
- if (scores.some((n) => n >= 100))
11768
- return { ...next3, phase: "finished", winner: best(scores) };
11809
+ if (scores2.some((n) => n >= 100))
11810
+ return { ...next3, phase: "finished", winner: best(scores2) };
11769
11811
  return next3;
11770
11812
  }
11771
11813
  function dominoView(s, seat) {
@@ -11983,11 +12025,12 @@ function playRummy(s, seat, a, entropy2) {
11983
12025
  return t;
11984
12026
  }
11985
12027
  if (s.phase !== "meld") throw Error("Draw from the stock or take the top discard first.");
11986
- if (a.type === "meld" && ownedCards(s.hands[seat], a.cards) && a.cards.length < s.hands[seat].length && rummyMeld(a.cards)) {
12028
+ const keepsDiscard = (cards) => s.hands[seat].some((c) => !cards.includes(c) && c !== s.taken);
12029
+ if (a.type === "meld" && ownedCards(s.hands[seat], a.cards) && keepsDiscard(a.cards) && rummyMeld(a.cards)) {
11987
12030
  t.melds.push({ seat, cards: [...a.cards].sort((a2, b) => rummyRank(a2) - rummyRank(b)) });
11988
12031
  t.hands[seat] = t.hands[seat].filter((c) => !a.cards.includes(c));
11989
12032
  t.result = `Player ${seat + 1} melds ${a.cards.length} cards.`;
11990
- } else if (a.type === "layoff" && index(a.at, s.melds.length) && ownedCards(s.hands[seat], a.cards) && a.cards.length < s.hands[seat].length && rummyMeld([...s.melds[a.at].cards, ...a.cards])) {
12033
+ } else if (a.type === "layoff" && index(a.at, s.melds.length) && ownedCards(s.hands[seat], a.cards) && keepsDiscard(a.cards) && rummyMeld([...s.melds[a.at].cards, ...a.cards])) {
11991
12034
  t.melds[a.at].cards.push(...a.cards);
11992
12035
  t.melds[a.at].cards.sort((a2, b) => rummyRank(a2) - rummyRank(b));
11993
12036
  t.hands[seat] = t.hands[seat].filter((c) => !a.cards.includes(c));
@@ -12079,7 +12122,8 @@ var promotions = ["marriage", "marriage-claim", "makao", "normal", "schneider",
12079
12122
  function hand(state, seat) {
12080
12123
  for (const key2 of ["hands", "hand"]) {
12081
12124
  const hands = state[key2];
12082
- if (Array.isArray(hands) && Array.isArray(hands[seat])) return hands[seat].filter((c) => typeof c === "number" && c >= 0);
12125
+ const held = typeof state.playSeat === "number" && state.turn === seat ? state.playSeat : seat;
12126
+ if (Array.isArray(hands) && Array.isArray(hands[held])) return hands[held].filter((c) => typeof c === "number" && c >= 0);
12083
12127
  }
12084
12128
  return [];
12085
12129
  }
@@ -12150,6 +12194,10 @@ function specific(game, state, seat) {
12150
12194
  const s = state;
12151
12195
  return s.die ? ludoMoves(s).map((at) => ({ type: "move", at })) : [{ type: "roll" }];
12152
12196
  }
12197
+ if (game === "makao") return [...generic(adapters.makao, state, seat), ...cardGroups(hand(state, seat)).flatMap((cards) => [
12198
+ { type: "play", cards, promotion: "makao" },
12199
+ ...[0, 1, 2, 3, 5, 6, 7, 8, 9, 10, 13].flatMap((amount) => [{ type: "play", cards, amount }, { type: "play", cards, amount, promotion: "makao" }])
12200
+ ])];
12153
12201
  if (game === "go") {
12154
12202
  const s = state;
12155
12203
  if (s.phase === "scoring" && s.accepted.includes(seat)) return [];
@@ -12249,6 +12297,35 @@ function millsBoard(board) {
12249
12297
  legend: "Each point shows its index then X (seat 0), O (seat 1) or . (empty). Lines connect neighbours along each ring and the midpoints 1-9-17, 3-11-19, 5-13-21, 7-15-23; mills are three in a line."
12250
12298
  };
12251
12299
  }
12300
+ function battleshipsBoard(v) {
12301
+ const ships = new Set(v.ownFleet.flat()), sunk = new Set(v.sunk.flat());
12302
+ const target = (at) => sunk.has(at) ? "#" : v.hits.includes(at) ? "x" : v.fired.includes(at) ? "o" : ".";
12303
+ const fleet = (at) => ships.has(at) ? v.incoming.includes(at) ? "X" : "S" : v.incoming.includes(at) ? "o" : ".";
12304
+ const row = (r, cell) => Array.from({ length: 10 }, (_, c) => cell(r * 10 + c)).join(" ");
12305
+ return {
12306
+ rows: 10,
12307
+ columns: 10,
12308
+ labeled: [
12309
+ ` target grid your fleet`,
12310
+ ` +0 1 2 3 4 5 6 7 8 9 +0 1 2 3 4 5 6 7 8 9`,
12311
+ ...Array.from({ length: 10 }, (_, r) => `${String(r * 10).padStart(2)} ${row(r, target)} ${row(r, fleet)}`)
12312
+ ],
12313
+ legend: "Target grid: x hit, # sunk ship, o miss, . not fired. Your fleet: S ship, X hit ship, o enemy miss. Index = row label + column offset."
12314
+ };
12315
+ }
12316
+ function backgammonBoard(v) {
12317
+ const cell = (at) => `${String(at).padStart(2)}:${v.points[at] ? `${Math.abs(v.points[at])}${v.points[at] > 0 ? "X" : "O"}` : "."}`.padEnd(6);
12318
+ return {
12319
+ rows: 2,
12320
+ columns: 12,
12321
+ labeled: [
12322
+ Array.from({ length: 12 }, (_, i) => cell(12 + i)).join(" ").trimEnd(),
12323
+ Array.from({ length: 12 }, (_, i) => cell(11 - i)).join(" ").trimEnd(),
12324
+ `bar: X ${v.bar[0]}, O ${v.bar[1]} \xB7 off: X ${v.off[0]}/15, O ${v.off[1]}/15`
12325
+ ],
12326
+ legend: "index:count then X (seat 0, moves 0\u219223, bears off past 23 to 24) or O (seat 1, moves 23\u21920, bears off to 24). from -1 enters from the bar. Index + 1 is the point number the browser shows."
12327
+ };
12328
+ }
12252
12329
  function fenBoard(fen) {
12253
12330
  const ranks = fen.split(" ")[0].split("/").map((rank2) => rank2.replace(/\d/g, (n) => ".".repeat(Number(n))));
12254
12331
  return {
@@ -12261,6 +12338,8 @@ function fenBoard(fen) {
12261
12338
  function indexedBoard(game, state) {
12262
12339
  const grid = grids[game], board = state?.board;
12263
12340
  if (game === "mills" && Array.isArray(board) && board.length === 24) return millsBoard(board);
12341
+ if (game === "backgammon" && state && typeof state === "object" && "points" in state) return backgammonBoard(state);
12342
+ if (game === "battleships" && state && typeof state === "object" && "ownFleet" in state) return battleshipsBoard(state);
12264
12343
  const fen = state?.fen;
12265
12344
  if ((game === "atomic-chess" || game === "three-check") && typeof fen === "string") return fenBoard(fen);
12266
12345
  if (!grid || !Array.isArray(board)) return null;
@@ -12322,12 +12401,35 @@ function agentPosition(room) {
12322
12401
  if (match.kind === "hex" && match.state.moves === 1 && match.state.turn === 1) legal.push({ type: "swap" });
12323
12402
  }
12324
12403
  }
12325
- const last = match?.kind === "connect-four" || match?.kind === "tic-tac-toe" || match?.kind === "gomoku" ? match.state.last : null;
12404
+ const last = match?.kind === "connect-four" || match?.kind === "tic-tac-toe" || match?.kind === "gomoku" ? match.state.last : match?.kind === "reversi" || match?.kind === "hex" ? match.state.last ?? null : null;
12326
12405
  const agreed = match?.kind === "go" && match.state.phase === "scoring" && match.state.accepted.includes(room.seat);
12327
12406
  const yours = room.phase === "playing" && !!match && turn === room.seat && !agreed && !room.players[room.seat].left && !ended && !room.players.some((p) => p.paused);
12328
12407
  const active = room.phase === "playing" && !!match && !room.players[room.seat].left && !ended && !room.players.some((p) => p.paused);
12329
12408
  const grid = !complete && match ? indexedBoard(room.game, match.state) : null;
12330
12409
  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;
12411
+ const dotsLabeled = match?.kind === "dots-boxes" ? match.state.boxes.map((owner4, box) => {
12412
+ const { size, edges } = match.state, sides2 = boxEdges(size, box), drawn = sides2.filter((e) => edges[e] >= 0).length;
12413
+ const named = ["top", "bottom", "left", "right"].map((side2, i) => `${side2} ${sides2[i] + 1}${edges[sides2[i]] >= 0 ? "*" : ""}`).join(", ");
12414
+ 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
+ }) : null;
12416
+ const ginHand = match?.kind === "gin-rummy" && match.state.phase !== "finished" ? match.state.hands[room.seat]?.filter((c) => c !== null) ?? [] : [];
12417
+ const ginHint = ginHand.length ? (() => {
12418
+ const layout = ginLayout(ginHand), names = (cards) => cards.map((c) => `${cardLabel(c)} (id ${c})`);
12419
+ const discards = ginHand.length > 10 ? ginHand.map((c) => ({ discard: c, deadwood: ginLayout(ginHand.filter((x) => x !== c)).value })).sort((a, b) => a.deadwood - b.deadwood) : [];
12420
+ return {
12421
+ deadwood: layout.value,
12422
+ melds: layout.melds.map(names),
12423
+ deadwood_cards: names(layout.deadwood),
12424
+ ...discards.length ? { best_discard: `${cardLabel(discards[0].discard)} (id ${discards[0].discard})`, deadwood_after_best_discard: discards[0].deadwood } : {},
12425
+ knock_limit: 10
12426
+ };
12427
+ })() : null;
12428
+ const asianMove = match?.kind === "shogi" || match?.kind === "xiangqi" ? asianLastMove(match.state.history) : null;
12429
+ const asianLast = asianMove && (match?.kind === "shogi" || match?.kind === "xiangqi") ? { ...asianMove, by: match.state.history.at(-1).mover } : null;
12430
+ 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;
12331
12433
  return {
12332
12434
  protocol_version: 1,
12333
12435
  room: room.code,
@@ -12342,15 +12444,16 @@ function agentPosition(room) {
12342
12444
  players: room.players.map((p, seat) => ({ seat, name: p.name, left: p.left, ...p.agent ? { agent: true } : {}, ...p.paused ? { paused: true } : {} })),
12343
12445
  board_size: complete ? { rows, columns } : grid ? { rows: grid.rows, columns: grid.columns } : { rows: 0, columns: 0 },
12344
12446
  board_top_to_bottom: board,
12345
- board_labeled: labeledBoard(room.game, board) ?? grid?.labeled ?? null,
12346
- ...grid ? { board_legend: grid.legend } : {},
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,
12448
+ ...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." } : {},
12347
12449
  // Chess and its variants record the last move in SAN.
12348
- last_move: (match?.kind === "chess" || match?.kind === "atomic-chess" || match?.kind === "three-check") && match.state.last ? { san: match.state.last } : 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 },
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 },
12349
12451
  ...room.game === "chess" ? { fen } : {},
12350
12452
  // The side to move is in check (chess family, xiangqi, shogi).
12351
12453
  ...match && room.phase === "playing" && (match.kind === "chess" ? chessPosition(match.state).inCheck() : "check" in match.state && match.state.check === true) ? { in_check: true } : {},
12352
12454
  visible_state: match?.state ?? null,
12353
12455
  cards_labeled: match ? cardsLabeled(room.game, match.state) : null,
12456
+ ...ginHint ? { gin_deadwood: ginHint } : {},
12354
12457
  // Timed rounds (drawing, categories): agents cannot measure time, so give the remaining seconds.
12355
12458
  ...typeof match?.state?.deadline === "number" && room.serverNow ? { deadline_seconds_left: Math.max(0, Math.ceil((match.state.deadline - room.serverNow) / 1e3)) } : {},
12356
12459
  action_types: action_catalog_default[room.game],
@@ -12358,7 +12461,8 @@ function agentPosition(room) {
12358
12461
  legal_actions_complete: complete,
12359
12462
  legal_actions: yours ? legal : [],
12360
12463
  ...match?.kind === "amazons" || match?.kind === "lines-of-action" || match?.kind === "last-greenhouse" || match?.kind === "euchre" || match?.kind === "cribbage" || match?.kind === "fanorona" ? { legal_wire_actions_complete: true, legal_wire_actions: yours ? match.state.legalActions : [] } : wire ? { legal_wire_actions_complete: agentListComplete(room.game, match?.state, wire), legal_wire_actions: wire } : {},
12361
- outcome: room.outcome ? { winner: room.outcome.winner, label: room.outcome.label } : null,
12464
+ // Engine labels count players from 1; name the zero-based seat as everywhere else in the CLI.
12465
+ outcome: room.outcome ? { winner: room.outcome.winner, label: room.outcome.label.replace(/Player (\d+)/g, (whole, n) => room.players[Number(n) - 1] ? `seat ${Number(n) - 1} (${room.players[Number(n) - 1].name})` : whole) } : null,
12362
12466
  instructions_for_resume: "Resume your agent chat with this room code and read status. A timeout is not resignation. To rematch, use game rematch; never start one without the human asking.",
12363
12467
  terminal_reason: room.phase === "finished" ? room.players.some((p) => p.left) ? "resignation_or_leave" : "game_result" : null,
12364
12468
  rematch_ready: room.rematch.includes(room.seat),
@@ -12367,13 +12471,14 @@ function agentPosition(room) {
12367
12471
  }
12368
12472
  function labeledBoard(game, board) {
12369
12473
  if (!board?.length || game === "dots-boxes") return null;
12370
- if (game === "oware") return [`seat 0 pits 1-6: ${board[0]}`, `seat 1 pits 7-12: ${board[1]}`];
12474
+ if (game === "oware") return [`seat 0 pits 1-6: ${board[0]}`, `seat 1 pits 7-12: ${board[1]}`, "Sowing runs 1\u219212 and wraps to 1, skipping the origin pit after a full lap."];
12371
12475
  const algebraic = game === "chess" || game === "checkers";
12372
12476
  const rowWidth = String(board.length).length, cell = algebraic ? 1 : String(board[0].length).length;
12373
12477
  const files2 = algebraic ? [..."abcdefgh"] : [...board[0]].map((_, c) => String(c + 1));
12374
12478
  return [
12375
12479
  `${" ".repeat(rowWidth)} ${files2.map((f) => f.padStart(cell)).join(" ")}`,
12376
- ...board.map((row, r) => `${String(algebraic ? board.length - r : r + 1).padStart(rowWidth)} ${[...row].map((p) => p.padStart(cell)).join(" ")}`)
12480
+ // Hex rows shift right one step per row, so the six neighbours sit around each cell.
12481
+ ...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(" ")}`)
12377
12482
  ];
12378
12483
  }
12379
12484
  function agentMove(position, input) {
@@ -12403,7 +12508,7 @@ function agentRules(game = "connect-four") {
12403
12508
  oware: "Sow the seeds from a nonempty pit on your side, skipping the origin after a full lap. Feed an empty opponent if possible. Capture opposing pits with two or three seeds, except when doing so would starve the opponent. Scores, feeding and end conditions follow the supplied rules and state.",
12404
12509
  gomoku: "Place on an empty intersection of the 15 by 15 board. Exactly five contiguous stones horizontally, vertically or diagonally wins. A line longer than five does not win in this variant. A full board without a winner draws."
12405
12510
  };
12406
- const coordinates = !completeAgentGames.includes(game) || game === "dots-boxes" || game === "oware" ? actionHelp(game).indexing : game === "hex" ? "One-based rows and columns on the size in visible_state. Use --swap only when listed. Seat 0 connects top/bottom; seat 1 left/right." : game === "chess" ? "Use --from e2 --to e4. Board rows are ranks 8 to 1, columns a to h. Uppercase pieces are White, lowercase Black; dot is empty. FEN describes the full position." : game === "checkers" ? "Use --from b3 --to a4. Board rows are ranks 8 to 1, columns a to h. x/X are seat 0 men/kings; o/O are seat 1 men/kings; dot is empty." : game === "connect-four" ? "Columns are 1\u20137, left to right; use --column. X is seat 0, O is seat 1, dot is empty." : `Rows and columns are 1\u2013${game === "reversi" ? 8 : game === "gomoku" ? 15 : 3}, top to bottom and left to right; use --row and --column. X is seat 0, O is seat 1, dot is empty.`;
12511
+ const coordinates = !completeAgentGames.includes(game) || game === "dots-boxes" || game === "oware" ? actionHelp(game).indexing : game === "hex" ? "One-based rows and columns on the size in visible_state. Use --swap only when listed. Seat 0 (X, Black) connects top/bottom; seat 1 (O, White) left/right. board_labeled indents each row one step, so (r,c) touches the six cells (r,c-1), (r,c+1), (r-1,c), (r-1,c+1), (r+1,c-1) and (r+1,c)." : game === "chess" ? "Use --from e2 --to e4. Board rows are ranks 8 to 1, columns a to h. Uppercase pieces are White, lowercase Black; dot is empty. FEN describes the full position." : game === "checkers" ? "Use --from b3 --to a4. Board rows are ranks 8 to 1, columns a to h. x/X are seat 0 men/kings; o/O are seat 1 men/kings; dot is empty." : game === "connect-four" ? "Columns are 1\u20137, left to right; use --column. X is seat 0, O is seat 1, dot is empty." : `Rows and columns are 1\u2013${game === "reversi" ? 8 : game === "gomoku" ? 15 : 3}, top to bottom and left to right; use --row and --column. X is seat 0, O is seat 1, dot is empty.`;
12407
12512
  return { protocol_version: 1, game, rules: multiplayerGames.find((g) => g.id === game).rules, guidance: rules[game] ?? "Use the player-visible state and documented wire actions. The server validates legality; an empty partial action list does not mean there are no legal moves.", action_types: action_catalog_default[game], action_help: actionHelp(game), coordinates, instructions };
12408
12513
  }
12409
12514
  var connectFourAgentRules = agentRules();
@@ -12694,7 +12799,15 @@ function nextStep(position, event, brief = false, server) {
12694
12799
  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). " : "";
12695
12800
  const move2 = wire ? "'<one legal_wire_actions entry>'" : position.legal_actions_complete ? "'<one legal_actions entry>'" : "'<wire action JSON from game rules>'";
12696
12801
  const check = lineGames.includes(position.game) ? "Check your immediate win and your opponent's immediate threats (every line, including diagonals), then: " : "";
12697
- return `Your move. ${partial}${check}${play(move2)} --wait${flag}`;
12802
+ const makao = position.game === "makao" ? ((state) => [
12803
+ state?.penalty ? `A ${state.penalty}-card penalty is active: answer with a matching attack card, or draw takes all ${state.penalty}. ` : "",
12804
+ state?.wait ? `A ${state.wait}-turn wait is active: answer with a 4, or draw serves the wait (no cards drawn). ` : "",
12805
+ 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]}. ` : ""
12808
+ ].join(""))(position.visible_state) : "";
12809
+ 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}`;
12698
12811
  }
12699
12812
  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.`;
12700
12813
  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.";
@@ -12786,7 +12899,7 @@ async function installSkill(agent = "codex", update = false) {
12786
12899
  // packages/cli/package.json
12787
12900
  var package_default = {
12788
12901
  name: "@leafarcade/cli",
12789
- version: "0.7.1",
12902
+ version: "0.8.0",
12790
12903
  private: false,
12791
12904
  description: "Create private browser board games and play with your AI agent. Includes 53 games and an installable Codex/Claude skill.",
12792
12905
  type: "module",
@@ -13137,7 +13250,14 @@ async function withChanges(result, path) {
13137
13250
  const cells = Array.isArray(boards[0]) && Array.isArray(boards[1]) && boards[0].length === boards[1].length ? boards[1].flatMap((value, at) => JSON.stringify(value) === JSON.stringify(boards[0][at]) ? [] : [{ at, before: boards[0][at], after: value }]) : [];
13138
13251
  const square2 = (at) => "abcdefgh"[at % 8] + (8 - Math.floor(at / 8));
13139
13252
  const derived = position.game === "checkers" ? cells.map((cell) => ({ ...cell, at: square2(cell.at) })) : "board_legend" in position ? cells : [];
13140
- const annotated = { ...position, ...position.last_move === null && derived.length && derived.length <= 12 ? { last_move: { board_changes: derived } } : {}, changed_since_last_read: { from_revision: before.revision, changes } };
13253
+ const piles = [before.state, position.visible_state].map((state) => state?.discard);
13254
+ const top = position.game === "gin-rummy" && Array.isArray(piles[0]) && Array.isArray(piles[1]) ? piles[0].at(-1) : void 0;
13255
+ const taken = typeof top === "number" && !piles[1].includes(top) ? `${cardLabel(top)} (id ${top})` : null;
13256
+ const annotated = {
13257
+ ...position,
13258
+ ...position.last_move === null && derived.length && derived.length <= 12 ? { last_move: { board_changes: derived } } : {},
13259
+ changed_since_last_read: { from_revision: before.revision, ...taken ? { taken_from_discard: taken } : {}, changes }
13260
+ };
13141
13261
  return "position" in result ? { ...result, position: annotated } : annotated;
13142
13262
  }
13143
13263
 
@@ -13542,6 +13662,10 @@ var winner = (position) => {
13542
13662
  return seat === void 0 || seat === null ? null : seat === position.you.seat ? "you" : seatName(position, seat);
13543
13663
  };
13544
13664
  var seatName = (position, seat) => `seat ${seat} (${position.players[seat]?.name ?? "empty"})`;
13665
+ var scores = (position) => {
13666
+ const value = position.visible_state?.scores;
13667
+ return Array.isArray(value) && value.length && value.every((n) => typeof n === "number") ? value : null;
13668
+ };
13545
13669
  function briefPosition(position) {
13546
13670
  const turn = position.turn ? position.turn.yours ? "yours" : seatName(position, position.turn.seat) : null;
13547
13671
  const wire = "legal_wire_actions" in position ? position.legal_wire_actions ?? [] : null;
@@ -13554,6 +13678,8 @@ function briefPosition(position) {
13554
13678
  you: position.you.piece,
13555
13679
  turn,
13556
13680
  ...position.paused ? { paused: true } : {},
13681
+ // Match or capture scores by zero-based seat, when the game keeps them.
13682
+ ...scores(position) ? { scores: scores(position) } : {},
13557
13683
  ..."in_check" in position ? { in_check: true } : {},
13558
13684
  ...position.you.left ? { left: true } : {},
13559
13685
  ...position.phase === "waiting" ? { seated: position.players.filter((player) => !player.left).length, capacity: position.capacity } : {},
@@ -13569,6 +13695,7 @@ function briefPosition(position) {
13569
13695
  // Only the classic convenience games are fully described by their labeled board.
13570
13696
  ...position.legal_actions_complete && position.board_labeled || !position.visible_state ? {} : { visible_state: position.visible_state },
13571
13697
  ...position.cards_labeled ? { cards_labeled: position.cards_labeled } : {},
13698
+ ..."gin_deadwood" in position ? { gin_deadwood: position.gin_deadwood } : {},
13572
13699
  ...position.access_requests?.length ? { access_requests: position.access_requests } : {}
13573
13700
  };
13574
13701
  }
@@ -13718,7 +13845,7 @@ try {
13718
13845
  result = { event: "browser_requested", room, url, browser: await openBrowser(url) };
13719
13846
  } else if (command === "start" || command === "leave" || command === "resign" || command === "rematch" || command === "pause" || command === "resume") {
13720
13847
  const revision = integer(values.revision, "revision", 0, Number.MAX_SAFE_INTEGER);
13721
- if (revision !== view.revision) throw new ServiceError("The room changed. Read game status before changing the room.", "STALE_REVISION");
13848
+ if (revision !== view.revision) throw new ServiceError(`The room changed (now revision ${view.revision}). Read game status before changing the room.`, "STALE_REVISION");
13722
13849
  if (command === "start") {
13723
13850
  await rpc(session, "start", { revision }, false, controller.signal);
13724
13851
  result = { event: "started", position: agentPosition(await readRoom(session, controller.signal)) };
@@ -13740,7 +13867,7 @@ try {
13740
13867
  const revision = integer(values.revision, "revision", 0, Number.MAX_SAFE_INTEGER);
13741
13868
  const column = values.column === void 0 ? void 0 : integer(values.column, "column", 1, position.board_size.columns);
13742
13869
  const row = values.row === void 0 ? void 0 : integer(values.row, "row", 1, position.board_size.rows);
13743
- if (revision !== position.revision) throw new ServiceError("The room changed. Read game status before playing.", "STALE_REVISION");
13870
+ if (revision !== position.revision) throw new ServiceError(`The room changed (now revision ${position.revision}). Read game status before playing.`, "STALE_REVISION");
13744
13871
  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.");
13745
13872
  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) });
13746
13873
  const waitSeconds = integer(values.timeout ?? String(defaultWaitSeconds), "timeout", 1, 3600);