@leafarcade/cli 0.7.1 → 0.9.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,30 @@ 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
+ function drawReason(history) {
3758
+ const last = history.at(-1);
3759
+ return history.filter((key2) => key2 === last).length >= 3 ? "Draw \xB7 the same position occurred three times." : "Draw \xB7 too many moves without progress (no capture or man move).";
3760
+ }
3761
+
3738
3762
  // lib/random.ts
3739
3763
  function randomStep(seed) {
3740
3764
  const next3 = seed + 1831565813 >>> 0;
@@ -3767,180 +3791,745 @@ function shuffled(items, entropy2) {
3767
3791
  function index(value, length) {
3768
3792
  return Number.isInteger(value) && value >= 0 && value < length;
3769
3793
  }
3770
- function best(scores) {
3771
- const high = Math.max(...scores), winners = scores.flatMap((s, i) => s === high ? [i] : []);
3794
+ function best(scores2) {
3795
+ const high = Math.max(...scores2), winners = scores2.flatMap((s, i) => s === high ? [i] : []);
3772
3796
  return winners.length === 1 ? winners[0] : null;
3773
3797
  }
3774
3798
 
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 };
3799
+ // lib/multiplayer/mills.ts
3800
+ var millLines = [
3801
+ [0, 1, 2],
3802
+ [2, 3, 4],
3803
+ [4, 5, 6],
3804
+ [6, 7, 0],
3805
+ [8, 9, 10],
3806
+ [10, 11, 12],
3807
+ [12, 13, 14],
3808
+ [14, 15, 8],
3809
+ [16, 17, 18],
3810
+ [18, 19, 20],
3811
+ [20, 21, 22],
3812
+ [22, 23, 16],
3813
+ [1, 9, 17],
3814
+ [3, 11, 19],
3815
+ [5, 13, 21],
3816
+ [7, 15, 23]
3817
+ ];
3818
+ var millEdges = millLines.flatMap(([a, b, c]) => [
3819
+ [a, b],
3820
+ [b, c]
3821
+ ]);
3822
+ function createMills() {
3823
+ return {
3824
+ board: Array(24).fill(0),
3825
+ reserves: [9, 9],
3826
+ turn: 0,
3827
+ captures: 0,
3828
+ winner: null,
3829
+ draw: false,
3830
+ history: []
3831
+ };
3789
3832
  }
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] : []
3833
+ var linesAt = (board, at) => millLines.filter(
3834
+ (line) => line.includes(at) && line.every((i) => board[i] === board[at])
3835
+ );
3836
+ function millCaptures(s) {
3837
+ const opponents = s.board.flatMap((p, i) => p === 2 - s.turn ? [i] : []), loose = opponents.filter((at) => !linesAt(s.board, at).length);
3838
+ return loose.length ? loose : opponents;
3839
+ }
3840
+ function millMoves(s) {
3841
+ if (s.winner !== null || s.draw || s.captures) return [];
3842
+ const empty = s.board.flatMap((p, i) => !p ? [i] : []);
3843
+ if (s.reserves[s.turn]) return empty.map((to) => ({ from: null, to }));
3844
+ const own = s.board.flatMap((p, i) => p === s.turn + 1 ? [i] : []);
3845
+ return own.flatMap(
3846
+ (from) => empty.filter(
3847
+ (to) => own.length === 3 || millEdges.some(
3848
+ ([a, b]) => a === from && b === to || b === from && a === to
3849
+ )
3850
+ ).map((to) => ({ from, to }))
3851
+ );
3852
+ }
3853
+ function finishTurn(s) {
3854
+ const turn = s.turn === 0 ? 1 : 0, key2 = s.board.join(",") + ":" + turn + ":" + s.reserves.join(",");
3855
+ const next3 = {
3856
+ ...s,
3857
+ turn,
3858
+ captures: 0,
3859
+ history: [...s.history, key2]
3860
+ };
3861
+ const count = next3.board.filter((p) => p === turn + 1).length;
3862
+ if (!next3.reserves[turn] && (count < 3 || !millMoves(next3).length))
3863
+ next3.winner = s.turn;
3864
+ else next3.draw = next3.history.filter((k) => k === key2).length >= 3;
3865
+ return next3;
3866
+ }
3867
+ function playMills(s, type, to, from) {
3868
+ if (!index(to, 24) || s.winner !== null || s.draw)
3869
+ throw Error("Choose a legal point.");
3870
+ const board = [...s.board], reserves = [...s.reserves];
3871
+ if (type === "capture") {
3872
+ if (!s.captures || !millCaptures(s).includes(to))
3873
+ throw Error(
3874
+ "Capture a piece outside a mill, unless all pieces are in mills."
3875
+ );
3876
+ board[to] = 0;
3877
+ const next4 = { ...s, board, captures: s.captures - 1 };
3878
+ return next4.captures && millCaptures(next4).length ? next4 : finishTurn(next4);
3879
+ }
3880
+ if (!["place", "move"].includes(type) || !millMoves(s).some(
3881
+ (m) => m.to === to && m.from === (type === "place" ? null : from)
3882
+ ))
3883
+ throw Error(
3884
+ "Place from your reserve or move to a connected point. With three pieces you may fly."
3885
+ );
3886
+ if (type === "place") reserves[s.turn]--;
3887
+ else board[from] = 0;
3888
+ board[to] = s.turn + 1;
3889
+ const captures = Math.min(
3890
+ linesAt(board, to).length,
3891
+ board.filter((p) => p === 2 - s.turn).length
3795
3892
  );
3893
+ const next3 = { ...s, board, reserves, captures };
3894
+ return captures ? next3 : finishTurn(next3);
3895
+ }
3896
+ function millLastTurn(s) {
3897
+ const boards = s.history.slice(-2).map((key2) => key2.split(":")[0].split(",").map(Number));
3898
+ const after = boards.at(-1), before = boards.length === 2 ? boards[0] : Array(24).fill(0);
3899
+ if (!after) return null;
3900
+ const seat = Number(s.history.at(-1).split(":")[1]) === 0 ? 1 : 0;
3901
+ const to = after.findIndex((p, i) => p === seat + 1 && before[i] !== seat + 1);
3902
+ if (to < 0) return null;
3903
+ const from = before.findIndex((p, i) => p === seat + 1 && after[i] !== seat + 1);
3904
+ const captured = before.flatMap((p, i) => p === 2 - seat && after[i] !== p ? [i] : []);
3905
+ return { seat, from: from < 0 ? null : from, to, captured };
3796
3906
  }
3797
3907
 
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
- {
3938
- id: "sixty-six",
3908
+ // lib/multiplayer/cribbage.ts
3909
+ 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)];
3910
+
3911
+ // lib/multiplayer/dots-boxes.ts
3912
+ function boxEdges(size, box) {
3913
+ const x = box % size, y = Math.floor(box / size), offset = size * (size + 1);
3914
+ return [
3915
+ y * size + x,
3916
+ (y + 1) * size + x,
3917
+ offset + y * (size + 1) + x,
3918
+ offset + y * (size + 1) + x + 1
3919
+ ];
3920
+ }
3921
+
3922
+ // lib/multiplayer/trick-cards.ts
3923
+ var cardSuit = (card2) => Math.floor(card2 / 13);
3924
+ var cardRank = (card2) => card2 % 13 + 2;
3925
+ var cardLabel = (card2) => `${["", "", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"][cardRank(card2)]}${["\u2663", "\u2666", "\u2665", "\u2660"][cardSuit(card2)]}`;
3926
+ var penalty = (card2) => cardSuit(card2) === 2 ? 1 : card2 === 49 ? 13 : 0;
3927
+ function first(s) {
3928
+ return s.variant === "spades" ? s.dealer : (s.dealer + 1) % s.hands.length;
3929
+ }
3930
+ function deal(s, entropy2) {
3931
+ const count = s.hands.length, size = s.variant === "oh-hell" ? Math.max(1, (count === 4 ? 13 : 17) - s.round) : 13, deck = shuffled(
3932
+ Array.from({ length: 52 }, (_, i) => i),
3933
+ entropy2
3934
+ );
3935
+ const hands = Array.from(
3936
+ { length: count },
3937
+ (_, i) => deck.slice(i * size, (i + 1) * size).sort((a, b) => a - b)
3938
+ );
3939
+ const t = {
3940
+ ...s,
3941
+ size,
3942
+ hands,
3943
+ stock: deck.slice(count * size),
3944
+ trick: [],
3945
+ lastTrick: [],
3946
+ tricks: Array(count).fill(0),
3947
+ penalties: Array(count).fill(0),
3948
+ bids: Array(count).fill(null),
3949
+ blind: Array(count).fill(false),
3950
+ blindAsked: Array(count).fill(false),
3951
+ selected: Array.from({ length: count }, () => []),
3952
+ passed: Array(count).fill(false),
3953
+ broken: false,
3954
+ winner: null,
3955
+ result: ""
3956
+ };
3957
+ t.trump = s.variant === "spades" ? 3 : s.variant === "oh-hell" && s.round > 0 ? cardSuit(t.stock[0]) : null;
3958
+ 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";
3959
+ t.turn = s.variant === "hearts" ? t.phase === "play" ? hands.findIndex((h) => h.includes(0)) : 0 : first(t);
3960
+ return t;
3961
+ }
3962
+ function createTrickCards(variant, count, entropy2) {
3963
+ if (!index(count - 2, 3) || variant !== "oh-hell" && count !== 4)
3964
+ throw Error("Choose the required player count.");
3965
+ return deal(
3966
+ {
3967
+ variant,
3968
+ turn: 0,
3969
+ dealer: Math.floor(draws(entropy2, 1)[0] * count),
3970
+ round: 0,
3971
+ size: 13,
3972
+ phase: "bid",
3973
+ hands: Array.from({ length: count }, () => []),
3974
+ stock: [],
3975
+ trick: [],
3976
+ lastTrick: [],
3977
+ tricks: [],
3978
+ penalties: [],
3979
+ bids: [],
3980
+ blind: [],
3981
+ blindAsked: [],
3982
+ selected: [],
3983
+ passed: [],
3984
+ scores: Array(variant === "spades" ? 2 : count).fill(0),
3985
+ bags: [0, 0],
3986
+ trump: null,
3987
+ broken: false,
3988
+ winner: null,
3989
+ result: ""
3990
+ },
3991
+ entropy2
3992
+ );
3993
+ }
3994
+ function trickLegalCards(s) {
3995
+ if (s.phase !== "play") return [];
3996
+ const hand2 = s.hands[s.turn].filter((c) => c !== null), lead = s.trick[0];
3997
+ if (lead) {
3998
+ const suited = hand2.filter((c) => cardSuit(c) === cardSuit(lead.card));
3999
+ if (suited.length) return suited;
4000
+ if (s.variant === "hearts" && s.tricks.every((n) => n === 0)) {
4001
+ const clean = hand2.filter((c) => !penalty(c));
4002
+ if (clean.length) return clean;
4003
+ }
4004
+ return hand2;
4005
+ }
4006
+ if (s.variant === "hearts") {
4007
+ if (s.tricks.every((n) => n === 0)) return hand2.includes(0) ? [0] : [];
4008
+ if (!s.broken) {
4009
+ const clean = hand2.filter((c) => cardSuit(c) !== 2);
4010
+ if (clean.length) return clean;
4011
+ }
4012
+ }
4013
+ if (s.variant === "spades" && !s.broken) {
4014
+ const clean = hand2.filter((c) => cardSuit(c) !== 3);
4015
+ if (clean.length) return clean;
4016
+ }
4017
+ return hand2;
4018
+ }
4019
+ function finish(s) {
4020
+ if (s.variant === "hearts") {
4021
+ s.scores = s.scores.map((n, i) => n + s.penalties[i]);
4022
+ s.result = `Round penalties: ${s.penalties.join(" \xB7 ")}`;
4023
+ if (s.scores.some((n) => n >= 100)) {
4024
+ s.phase = "finished";
4025
+ s.winner = best(s.scores.map((n) => -n));
4026
+ } else s.phase = "round";
4027
+ } else if (s.variant === "spades") {
4028
+ const gains = [0, 0];
4029
+ for (const team2 of [0, 1]) {
4030
+ 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);
4031
+ gains[team2] = taken >= bid ? bid * 10 + Math.max(0, taken - bid) : -bid * 10;
4032
+ const bags = Math.max(0, taken - bid) + seats2.filter((i) => s.bids[i] === 0).reduce((n, i) => n + s.tricks[i], 0);
4033
+ s.bags[team2] += bags;
4034
+ gains[team2] -= 100 * Math.floor(s.bags[team2] / 10);
4035
+ s.bags[team2] %= 10;
4036
+ for (const i of seats2)
4037
+ if (s.bids[i] === 0)
4038
+ gains[team2] += (s.tricks[i] === 0 ? 1 : -1) * (s.blind[i] ? 200 : 100);
4039
+ }
4040
+ s.scores = s.scores.map((n, i) => n + gains[i]);
4041
+ s.result = `Team 1 & 3: ${gains[0]} \xB7 Team 2 & 4: ${gains[1]}`;
4042
+ if (s.scores.some((n) => n >= 500 || n <= -200)) {
4043
+ s.phase = "finished";
4044
+ s.winner = best(s.scores);
4045
+ } else s.phase = "round";
4046
+ } else {
4047
+ s.scores = s.scores.map(
4048
+ (n, i) => n + (s.bids[i] === s.tricks[i] ? 10 + s.tricks[i] : 0)
4049
+ );
4050
+ s.result = `Tricks taken: ${s.tricks.join(" \xB7 ")}`;
4051
+ const rounds = (s.hands.length === 4 ? 13 : 17) + s.hands.length - 1;
4052
+ if (s.round + 1 >= rounds) {
4053
+ s.phase = "finished";
4054
+ s.winner = best(s.scores);
4055
+ } else s.phase = "round";
4056
+ }
4057
+ s.turn = first(s);
4058
+ }
4059
+ function playTrickCards(s, type, at, amount, entropy2) {
4060
+ if (s.phase === "finished") throw Error("This match is finished.");
4061
+ if (type === "next" && s.phase === "round")
4062
+ return deal(
4063
+ { ...s, round: s.round + 1, dealer: (s.dealer + 1) % s.hands.length },
4064
+ entropy2
4065
+ );
4066
+ const t = structuredClone(s), seat = s.turn, count = s.hands.length;
4067
+ if (s.phase === "pass") {
4068
+ if (type === "select" && at !== void 0 && s.hands[seat].includes(at)) {
4069
+ const selected = t.selected[seat];
4070
+ if (selected.includes(at))
4071
+ t.selected[seat] = selected.filter((c) => c !== at);
4072
+ else if (selected.length < 3) selected.push(at);
4073
+ else throw Error("Select three cards.");
4074
+ return t;
4075
+ }
4076
+ if (type !== "pass" || t.selected[seat].length !== 3)
4077
+ throw Error("Choose three cards to pass.");
4078
+ t.passed[seat] = true;
4079
+ if (t.passed.every(Boolean)) {
4080
+ const offset = [1, 3, 2][s.round % 4];
4081
+ t.hands = t.hands.map(
4082
+ (h, i) => h.filter((c) => !t.selected[i].includes(c))
4083
+ );
4084
+ t.selected.forEach(
4085
+ (cards, i) => t.hands[(i + offset) % 4].push(...cards)
4086
+ );
4087
+ t.hands.forEach((h) => h.sort((a, b) => a - b));
4088
+ t.selected = t.selected.map(() => []);
4089
+ t.phase = "play";
4090
+ t.turn = t.hands.findIndex((h) => h.includes(0));
4091
+ } else t.turn = t.passed.findIndex((n) => !n);
4092
+ return t;
4093
+ }
4094
+ if (s.phase === "blind") {
4095
+ const eligible = s.scores[1 - seat % 2] - s.scores[seat % 2] >= 200;
4096
+ if (type === "blind" && eligible) {
4097
+ t.bids[seat] = 0;
4098
+ t.blind[seat] = true;
4099
+ } else if (type !== "look")
4100
+ throw Error("Choose blind nil or look at your cards.");
4101
+ t.blindAsked[seat] = true;
4102
+ if (t.blindAsked.every(Boolean)) {
4103
+ t.phase = "bid";
4104
+ t.turn = first(t);
4105
+ while (t.bids[t.turn] !== null) t.turn = (t.turn + 1) % count;
4106
+ } else t.turn = (seat + 1) % count;
4107
+ return t;
4108
+ }
4109
+ if (s.phase === "bid") {
4110
+ if (type !== "bid" || amount === void 0 || !index(amount, s.size + 1))
4111
+ throw Error("Choose a legal bid.");
4112
+ 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)
4113
+ throw Error("The last bid cannot make the total equal the hand size.");
4114
+ t.bids[seat] = amount;
4115
+ if (t.bids.every((b) => b !== null)) {
4116
+ t.phase = "play";
4117
+ t.turn = first(t);
4118
+ } else {
4119
+ t.turn = (seat + 1) % count;
4120
+ while (t.bids[t.turn] !== null) t.turn = (t.turn + 1) % count;
4121
+ }
4122
+ return t;
4123
+ }
4124
+ if (s.phase === "moon") {
4125
+ if (type !== "moon" || ![0, 1].includes(amount ?? -1))
4126
+ throw Error("Choose how to score the moon.");
4127
+ t.penalties = t.penalties.map(
4128
+ (_, i) => amount === 0 ? i === seat ? -26 : 0 : i === seat ? 0 : 26
4129
+ );
4130
+ finish(t);
4131
+ return t;
4132
+ }
4133
+ if (type !== "play" || at === void 0 || !trickLegalCards(s).includes(at))
4134
+ throw Error("Choose a legal card.");
4135
+ t.hands[seat] = t.hands[seat].filter((c) => c !== at);
4136
+ t.trick.push({ seat, card: at });
4137
+ 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))
4138
+ t.broken = true;
4139
+ t.turn = (seat + 1) % count;
4140
+ if (t.trick.length === count) {
4141
+ const lead = cardSuit(t.trick[0].card), strength = (c) => (cardSuit(c) === s.trump ? 100 : cardSuit(c) === lead ? 50 : 0) + cardRank(c);
4142
+ const winner2 = t.trick.reduce(
4143
+ (a, b) => strength(b.card) > strength(a.card) ? b : a
4144
+ ).seat;
4145
+ t.tricks[winner2]++;
4146
+ t.penalties[winner2] += t.trick.reduce((n, c) => n + penalty(c.card), 0);
4147
+ t.lastTrick = t.trick;
4148
+ t.trick = [];
4149
+ t.turn = winner2;
4150
+ if (t.hands.every((h) => !h.length)) {
4151
+ const moon = s.variant === "hearts" ? t.penalties.findIndex((n) => n === 26) : -1;
4152
+ if (moon >= 0) {
4153
+ t.phase = "moon";
4154
+ t.turn = moon;
4155
+ } else finish(t);
4156
+ }
4157
+ }
4158
+ return t;
4159
+ }
4160
+ function trickView(s, seat) {
4161
+ const { stock, selected, ...publicState } = s;
4162
+ return {
4163
+ ...publicState,
4164
+ stockCount: stock.length,
4165
+ selected: selected.map((cards, i) => i === seat ? cards : []),
4166
+ hands: s.hands.map(
4167
+ (h, i) => i === seat && !(s.phase === "blind" && !s.blindAsked[i]) ? h : h.map(() => null)
4168
+ )
4169
+ };
4170
+ }
4171
+
4172
+ // lib/multiplayer/gin-rummy.ts
4173
+ var ginRank = (c) => cardRank(c) === 14 ? 1 : cardRank(c);
4174
+ var ginValue = (c) => Math.min(10, ginRank(c));
4175
+ function ginMelds(hand2) {
4176
+ const melds = [];
4177
+ for (let rank2 = 1; rank2 <= 13; rank2++) {
4178
+ const cards = hand2.filter((c) => ginRank(c) === rank2);
4179
+ if (cards.length >= 3) {
4180
+ if (cards.length === 4) {
4181
+ melds.push(cards);
4182
+ for (let i = 0; i < 4; i++) melds.push(cards.filter((_, j) => j !== i));
4183
+ } else melds.push(cards);
4184
+ }
4185
+ }
4186
+ for (let suit = 0; suit < 4; suit++) {
4187
+ const cards = hand2.filter((c) => cardSuit(c) === suit).sort((a, b) => ginRank(a) - ginRank(b));
4188
+ for (let start = 0; start < cards.length; start++)
4189
+ for (let end = start + 2; end < cards.length; end++) {
4190
+ const slice = cards.slice(start, end + 1);
4191
+ if (slice.every(
4192
+ (c, i) => i === 0 || ginRank(c) === ginRank(slice[i - 1]) + 1
4193
+ ))
4194
+ melds.push(slice);
4195
+ else break;
4196
+ }
4197
+ }
4198
+ return melds;
4199
+ }
4200
+ function partitions(hand2) {
4201
+ const melds = ginMelds(hand2), out2 = [];
4202
+ function visit(start, used, chosen) {
4203
+ const deadwood2 = hand2.filter((c) => !used.has(c));
4204
+ out2.push({
4205
+ melds: chosen,
4206
+ deadwood: deadwood2,
4207
+ value: deadwood2.reduce((n, c) => n + ginValue(c), 0)
4208
+ });
4209
+ for (let i = start; i < melds.length; i++)
4210
+ if (melds[i].every((c) => !used.has(c)))
4211
+ visit(i + 1, /* @__PURE__ */ new Set([...used, ...melds[i]]), [...chosen, melds[i]]);
4212
+ }
4213
+ visit(0, /* @__PURE__ */ new Set(), []);
4214
+ return out2;
4215
+ }
4216
+ function ginLayout(hand2) {
4217
+ return partitions(hand2).reduce((a, b) => b.value < a.value ? b : a);
4218
+ }
4219
+ function ginLayoffValue(hand2, onto) {
4220
+ const cache = /* @__PURE__ */ new Map();
4221
+ function attach(cards, melds) {
4222
+ const key2 = cards.join(",") + "|" + melds.map((m) => m.join(",")).join(";"), cached = cache.get(key2);
4223
+ if (cached !== void 0) return cached;
4224
+ let best2 = cards.reduce((n, c) => n + ginValue(c), 0);
4225
+ cards.forEach(
4226
+ (c, i) => melds.forEach((m, j) => {
4227
+ 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);
4228
+ if (fits) {
4229
+ const next3 = melds.map(
4230
+ (m2, k) => k === j ? [...m2, c].sort((a, b) => ginRank(a) - ginRank(b)) : m2
4231
+ );
4232
+ best2 = Math.min(
4233
+ best2,
4234
+ attach(
4235
+ cards.filter((_, k) => k !== i),
4236
+ next3
4237
+ )
4238
+ );
4239
+ }
4240
+ })
4241
+ );
4242
+ cache.set(key2, best2);
4243
+ return best2;
4244
+ }
4245
+ return Math.min(...partitions(hand2).map((p) => attach(p.deadwood, onto)));
4246
+ }
4247
+ function deal2(dealer, scores2, wins, round, entropy2) {
4248
+ const deck = shuffled(
4249
+ Array.from({ length: 52 }, (_, i) => i),
4250
+ entropy2
4251
+ );
4252
+ return {
4253
+ hands: [
4254
+ deck.slice(0, 10).sort((a, b) => a - b),
4255
+ deck.slice(10, 20).sort((a, b) => a - b)
4256
+ ],
4257
+ stock: deck.slice(21),
4258
+ discard: [deck[20]],
4259
+ turn: 1 - dealer,
4260
+ dealer,
4261
+ phase: "offer",
4262
+ offers: 0,
4263
+ drawnDiscard: null,
4264
+ scores: scores2,
4265
+ wins,
4266
+ winner: null,
4267
+ round,
4268
+ result: "",
4269
+ revealed: [],
4270
+ margin: null
4271
+ };
4272
+ }
4273
+ function createGin(entropy2) {
4274
+ return deal2(Math.floor(draws(entropy2, 1)[0] * 2), [0, 0], [0, 0], 0, entropy2);
4275
+ }
4276
+ function playGin(s, type, at, entropy2) {
4277
+ if (type === "next" && s.phase === "round")
4278
+ return {
4279
+ ...deal2(
4280
+ s.result.startsWith("Stock") ? s.dealer : 1 - s.dealer,
4281
+ s.scores,
4282
+ s.wins,
4283
+ s.round + 1,
4284
+ entropy2
4285
+ ),
4286
+ previous: [
4287
+ s.result,
4288
+ ...s.hands.map((h, i) => {
4289
+ const layout = ginLayout(h);
4290
+ return `Player ${i + 1}: melds ${layout.melds.map((m) => m.map(cardLabel).join(" ")).join(" \xB7 ") || "none"}; deadwood ${layout.deadwood.map(cardLabel).join(" ") || "none"} (${layout.value}).`;
4291
+ })
4292
+ ].join(" ")
4293
+ };
4294
+ if (["finished", "round"].includes(s.phase))
4295
+ throw Error("This round is finished.");
4296
+ const t = structuredClone(s), seat = s.turn;
4297
+ if (type === "pass" && s.phase === "offer") {
4298
+ t.offers++;
4299
+ if (t.offers === 2) {
4300
+ t.phase = "draw";
4301
+ t.turn = 1 - s.dealer;
4302
+ } else t.turn = 1 - seat;
4303
+ return t;
4304
+ }
4305
+ if (["draw", "offer"].includes(s.phase) && ["stock", "upcard"].includes(type)) {
4306
+ if (s.phase === "offer" && type === "stock")
4307
+ throw Error("Take the upcard or pass the opening offer.");
4308
+ if (s.phase === "draw" && s.offers === 2 && s.round === t.round && type === "upcard")
4309
+ throw Error("Both players passed: draw from the stock.");
4310
+ if (s.stock.length <= 2) {
4311
+ t.phase = "round";
4312
+ t.result = "Stock exhausted: round cancelled, same dealer.";
4313
+ t.turn = s.dealer;
4314
+ return t;
4315
+ }
4316
+ const card2 = type === "upcard" ? t.discard.pop() : t.stock.shift();
4317
+ t.drawnDiscard = type === "upcard" ? card2 : null;
4318
+ t.hands[seat].push(card2);
4319
+ t.hands[seat].sort((a, b) => a - b);
4320
+ t.phase = "discard";
4321
+ t.offers = 0;
4322
+ return t;
4323
+ }
4324
+ if (s.phase === "discard" && ["discard", "knock"].includes(type) && at !== void 0 && s.hands[seat].includes(at) && at !== s.drawnDiscard) {
4325
+ t.hands[seat] = t.hands[seat].filter((c) => c !== at);
4326
+ t.discard.push(at);
4327
+ t.drawnDiscard = null;
4328
+ if (type === "knock") {
4329
+ const layout = ginLayout(t.hands[seat]);
4330
+ if (layout.value > 10)
4331
+ throw Error("Knock only with ten or fewer deadwood points.");
4332
+ 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;
4333
+ const points = layout.value === 0 ? opp + 25 : winner2 === seat ? opp - layout.value : layout.value - opp + 25;
4334
+ t.scores[winner2] += points;
4335
+ t.wins[winner2]++;
4336
+ t.result = `${layout.value === 0 ? "Gin" : winner2 === seat ? "Knock" : "Undercut"}: Player ${winner2 + 1} scores ${points}. Deadwood ${layout.value} vs ${opp}.`;
4337
+ t.revealed = t.hands.map((h) => ginLayout(h).melds);
4338
+ t.phase = t.scores[winner2] >= 100 ? "finished" : "round";
4339
+ t.turn = winner2;
4340
+ if (t.phase === "finished") {
4341
+ t.winner = winner2;
4342
+ t.margin = t.scores[winner2] + 100 + (t.scores[other] === 0 ? 100 : 0) + 25 * t.wins[winner2] - t.scores[other] - 25 * t.wins[other];
4343
+ t.result += ` Final margin including game and hand bonuses: ${t.margin}.`;
4344
+ }
4345
+ } else {
4346
+ t.phase = "draw";
4347
+ t.turn = 1 - seat;
4348
+ }
4349
+ return t;
4350
+ }
4351
+ throw Error("Choose a legal draw, discard or knock.");
4352
+ }
4353
+ function ginView(s, seat) {
4354
+ const { stock, ...rest } = s;
4355
+ return {
4356
+ ...rest,
4357
+ stockCount: stock.length,
4358
+ hands: s.hands.map(
4359
+ (h, i) => i === seat || ["round", "finished"].includes(s.phase) ? h : h.map(() => null)
4360
+ )
4361
+ };
4362
+ }
4363
+
4364
+ // lib/multiplayer/oware.ts
4365
+ var side = (at) => at < 6 ? 0 : 1;
4366
+ function sow(pits, at) {
4367
+ const board = [...pits];
4368
+ let seeds = board[at], last = at;
4369
+ board[at] = 0;
4370
+ while (seeds > 0) {
4371
+ last = (last + 1) % 12;
4372
+ if (last !== at) {
4373
+ board[last]++;
4374
+ seeds--;
4375
+ }
4376
+ }
4377
+ return { board, last };
4378
+ }
4379
+ function owareMoves(s) {
4380
+ if (s.finished) return [];
4381
+ const opponentEmpty = s.pits.every((n, i) => side(i) === s.turn || n === 0);
4382
+ return s.pits.flatMap(
4383
+ (n, at) => n > 0 && side(at) === s.turn && (!opponentEmpty || sow(s.pits, at).board.some((n2, i) => side(i) !== s.turn && n2 > 0)) ? [at] : []
4384
+ );
4385
+ }
4386
+
4387
+ // lib/multiplayer/catalog.ts
4388
+ var multiplayerGames = [
4389
+ { "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." },
4390
+ { "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." },
4391
+ { "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." },
4392
+ { 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." },
4393
+ { 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." },
4394
+ { 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." },
4395
+ { "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." },
4396
+ { "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." },
4397
+ { "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." },
4398
+ { "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." },
4399
+ { 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." },
4400
+ { 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." },
4401
+ { 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." },
4402
+ { 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." },
4403
+ { 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." },
4404
+ { 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." },
4405
+ { 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." },
4406
+ {
4407
+ id: "thousand",
4408
+ name: "Thousand",
4409
+ symbol: "1000",
4410
+ players: "2\u20134 players",
4411
+ description: "Bid your hand. Marry a new trump.",
4412
+ 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. Cards left from the kitties, including the declarer\u2019s two discards in a two-player game, score for the last trick winner. Declarer makes or loses their contract; others round their points to tens. At 800, only declarer earns further points. First to 1000 wins, with declarer winning a simultaneous finish. The first bomb cancels freely; subsequent bombs award opponents 60 (40 with four seats). Four nines can request a redeal."
4413
+ },
4414
+ {
4415
+ id: "binokel",
4416
+ name: "Binokel",
4417
+ symbol: "AA",
4418
+ players: "4 players",
4419
+ description: "Four copies. Two partners. One contract.",
4420
+ 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."
4421
+ },
4422
+ {
4423
+ id: "skat",
4424
+ name: "Skat",
4425
+ symbol: "J",
4426
+ players: "3 players",
4427
+ description: "Win the bidding duel. Play alone against two.",
4428
+ 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."
4429
+ },
4430
+ {
4431
+ id: "baska",
4432
+ name: "Baska",
4433
+ symbol: "DQ",
4434
+ players: "4 players",
4435
+ description: "Find your hidden partner in sixteen cards.",
4436
+ 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."
4437
+ },
4438
+ {
4439
+ id: "canasta",
4440
+ name: "Canasta",
4441
+ symbol: "77",
4442
+ players: "2 or 4 players",
4443
+ description: "Build seven-card melds. Break a frozen pile.",
4444
+ 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."
4445
+ },
4446
+ {
4447
+ id: "bridge",
4448
+ name: "Bridge",
4449
+ symbol: "NT",
4450
+ players: "4 players",
4451
+ description: "Bid a contract. Plan two hands together.",
4452
+ 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."
4453
+ },
4454
+ {
4455
+ id: "polish-hearts",
4456
+ name: "Polish Hearts",
4457
+ symbol: "\u2665",
4458
+ players: "3\u20134 players",
4459
+ description: "Avoid seven penalties, then win points back.",
4460
+ 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."
4461
+ },
4462
+ {
4463
+ id: "durak",
4464
+ name: "Durak",
4465
+ symbol: "\u2660",
4466
+ players: "2\u20134 players",
4467
+ description: "Beat the attack. Empty your hand.",
4468
+ 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."
4469
+ },
4470
+ {
4471
+ id: "makao",
4472
+ name: "Makao",
4473
+ symbol: "Q",
4474
+ players: "2\u20134 players",
4475
+ description: "Match suits and ranks. Counter the demand.",
4476
+ 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."
4477
+ },
4478
+ {
4479
+ id: "three-five-eight",
4480
+ name: "3-5-8",
4481
+ symbol: "358",
4482
+ players: "3 players",
4483
+ description: "Meet your quota. Exchange for stronger cards.",
4484
+ 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."
4485
+ },
4486
+ {
4487
+ id: "property-trading",
4488
+ name: "Property Trading",
4489
+ symbol: "\u2302",
4490
+ players: "2\u20134 players",
4491
+ description: "Build a garden estate. Bargain for a complete street.",
4492
+ 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."
4493
+ },
4494
+ {
4495
+ id: "draw-guess",
4496
+ name: "Draw & Guess",
4497
+ symbol: "\u270E",
4498
+ players: "2\u201320 players",
4499
+ description: "A private word. A shared sketch.",
4500
+ 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."
4501
+ },
4502
+ {
4503
+ id: "letter-tiles",
4504
+ name: "Letter Tiles",
4505
+ symbol: "A",
4506
+ players: "2\u20134 players",
4507
+ description: "Build a crossword. Make every letter count.",
4508
+ 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."
4509
+ },
4510
+ {
4511
+ id: "pan",
4512
+ name: "Pan",
4513
+ symbol: "\u2667",
4514
+ players: "2\u20134 players",
4515
+ description: "Shed your cards before the pile turns.",
4516
+ 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."
4517
+ },
4518
+ {
4519
+ id: "gin-rummy",
4520
+ name: "Gin Rummy",
4521
+ symbol: "\u2666",
4522
+ players: "2 players",
4523
+ description: "Build runs and sets. Know when to knock.",
4524
+ 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."
4525
+ },
4526
+ {
4527
+ id: "sixty-six",
3939
4528
  name: "Sixty-Six",
3940
4529
  symbol: "\u2654",
3941
4530
  players: "2 players",
3942
4531
  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."
4532
+ 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, and the trump upcard is the last card drawn. Exchange trump nine after winning a trick. Before leading, you may close the stock; then follow suit, overtake if possible, otherwise trump. A queen\u2013king marriage declared while leading with open stock earns 20, or 40 in trump, once you have won a trick (including the one it leads). Claim 66 before leading, or immediately with a marriage. False claims or failed closure award the opponent 2/3 match points. Otherwise round wins earn 1\u20133 based on opponent points. An unclosed last trick adds ten. First to seven match points wins."
3944
4533
  },
3945
4534
  {
3946
4535
  id: "spades",
@@ -4052,7 +4641,7 @@ var multiplayerGames = [
4052
4641
  symbol: "\u25CC",
4053
4642
  players: "2 players",
4054
4643
  description: "Sow seeds. Capture twos and threes.",
4055
- rules: "Each of twelve pits starts with four seeds. Sow counterclockwise, skipping the starting pit on a full lap. Ending on an opposing pit with two or three seeds captures it and preceding opposing pits with two or three. Captures that empty the entire opposing row are cancelled. Feed an empty opponent if possible. More than 24 captured seeds wins. No legal feeding move or three repetitions ends play; remaining seeds go to their owners."
4644
+ rules: "Each of twelve pits starts with four seeds. Sow counterclockwise, skipping the starting pit on a full lap. Ending on an opposing pit with two or three seeds captures it and preceding opposing pits with two or three. Captures that would empty the entire opposing row (a grand slam) are cancelled. Feed an empty opponent if possible. More than 24 captured seeds wins. No legal feeding move or three repetitions ends play; remaining seeds go to their owners."
4056
4645
  },
4057
4646
  {
4058
4647
  id: "mills",
@@ -4413,421 +5002,168 @@ var action_catalog_default = {
4413
5002
  "pass",
4414
5003
  "choose"
4415
5004
  ]
4416
- };
4417
-
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
- }
5005
+ };
4578
5006
 
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
- }
5007
+ // lib/multiplayer/dice.ts
5008
+ var diceCategories = [
5009
+ "Ones",
5010
+ "Twos",
5011
+ "Threes",
5012
+ "Fours",
5013
+ "Fives",
5014
+ "Sixes",
5015
+ "Three of a kind",
5016
+ "Four of a kind",
5017
+ "Full house",
5018
+ "Small straight",
5019
+ "Large straight",
5020
+ "Five of a kind",
5021
+ "Chance"
5022
+ ];
5023
+ function createDice(count) {
5024
+ if (!index(count - 2, 3)) throw Error("Choose two to four players.");
5025
+ return {
5026
+ dice: [0, 0, 0, 0, 0],
5027
+ held: Array(5).fill(false),
5028
+ rolls: 0,
5029
+ sheets: Array.from({ length: count }, () => Array(13).fill(null)),
5030
+ bonuses: Array(count).fill(0),
5031
+ turn: 0,
5032
+ finished: false,
5033
+ winner: null
5034
+ };
5035
+ }
5036
+ function diceScore(dice, category, joker = false) {
5037
+ const counts = Array.from(
5038
+ { length: 6 },
5039
+ (_, i) => dice.filter((v) => v === i + 1).length
5040
+ ), sum = dice.reduce((a, b) => a + b, 0);
5041
+ if (category < 6) return counts[category] * (category + 1);
5042
+ switch (category) {
5043
+ case 6:
5044
+ return Math.max(...counts) >= 3 ? sum : 0;
5045
+ case 7:
5046
+ return Math.max(...counts) >= 4 ? sum : 0;
5047
+ case 8:
5048
+ return joker || counts.includes(3) && counts.includes(2) ? 25 : 0;
5049
+ case 9:
5050
+ return joker || [0, 1, 2].some((start) => counts.slice(start, start + 4).every(Boolean)) ? 30 : 0;
5051
+ case 10:
5052
+ return joker || [0, 1].some((start) => counts.slice(start, start + 5).every(Boolean)) ? 40 : 0;
5053
+ case 11:
5054
+ return counts.includes(5) ? 50 : 0;
5055
+ case 12:
5056
+ return sum;
5057
+ default:
5058
+ throw Error("Choose a score category.");
4814
5059
  }
4815
- return t;
4816
5060
  }
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
- )
5061
+ function diceTotal(s, seat) {
5062
+ const sheet = s.sheets[seat], upper = sheet.slice(0, 6).reduce((sum, n) => sum + (n ?? 0), 0);
5063
+ return sheet.reduce((sum, n) => sum + (n ?? 0), 0) + (upper >= 63 ? 35 : 0) + s.bonuses[seat];
5064
+ }
5065
+ function playDice(s, type, at, entropy2) {
5066
+ if (s.finished) throw Error("This match is finished.");
5067
+ if (type === "roll") {
5068
+ if (s.rolls >= 3) throw Error("Record a score after three rolls.");
5069
+ const random = draws(entropy2, 5);
5070
+ return {
5071
+ ...s,
5072
+ rolls: s.rolls + 1,
5073
+ dice: s.dice.map(
5074
+ (v, i) => s.rolls && s.held[i] ? v : 1 + Math.floor(random[i] * 6)
5075
+ )
5076
+ };
5077
+ }
5078
+ if (at === void 0 || !index(at, type === "hold" ? 5 : 13) || !s.rolls)
5079
+ throw Error("Roll first, then choose a die or score category.");
5080
+ if (type === "hold")
5081
+ return { ...s, held: s.held.map((v, i) => i === at ? !v : v) };
5082
+ if (type !== "score" || s.sheets[s.turn][at] !== null)
5083
+ throw Error("Choose an unused score category.");
5084
+ const sheets = s.sheets.map((row) => [...row]), bonuses = [...s.bonuses];
5085
+ const five = s.dice.every((v) => v === s.dice[0]), upper = s.dice[0] - 1;
5086
+ const joker = five && sheets[s.turn][11] !== null && sheets[s.turn][upper] !== null;
5087
+ if (joker && sheets[s.turn].slice(6).some((n) => n === null) && at < 6)
5088
+ throw Error(
5089
+ "Use the five-of-a-kind joker in an unused lower category first."
5090
+ );
5091
+ sheets[s.turn][at] = diceScore(s.dice, at, joker);
5092
+ if (joker && sheets[s.turn][11] === 50) bonuses[s.turn] += 100;
5093
+ const next3 = {
5094
+ ...s,
5095
+ sheets,
5096
+ bonuses,
5097
+ turn: (s.turn + 1) % s.sheets.length,
5098
+ rolls: 0,
5099
+ held: Array(5).fill(false),
5100
+ dice: [0, 0, 0, 0, 0]
4826
5101
  };
5102
+ next3.finished = sheets.every((row) => row.every((n) => n !== null));
5103
+ if (next3.finished)
5104
+ next3.winner = best(sheets.map((_, i) => diceTotal(next3, i)));
5105
+ return next3;
4827
5106
  }
4828
5107
 
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)];
5108
+ // lib/agents/action-help.ts
5109
+ var help = {
5110
+ amazons: "move: numeric from/to 0\u201399 and amount for the arrow square. Select an exact legal_wire_actions entry.",
5111
+ "lines-of-action": "move: numeric from/to 0\u201363. Move the count of all stones along the full line; enemy stones block jumping.",
5112
+ "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.",
5113
+ 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.",
5114
+ 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.",
5115
+ 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.",
5116
+ 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.`,
5117
+ 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.",
5118
+ 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.",
5119
+ 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.",
5120
+ "connect-four": "place: at is a zero-based column (0\u20136). Convenience: --column 1\u20137.",
5121
+ "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.",
5122
+ chess: "move: from/to are algebraic squares; promotion is q/r/b/n.",
5123
+ "atomic-chess": "move: from/to are algebraic squares; promotion is q/r/b/n. Explosions follow the game rules.",
5124
+ "three-check": "move: from/to are algebraic squares; promotion is q/r/b/n. Check counts are in visible_state.",
5125
+ 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.",
5126
+ "brazilian-checkers": "move: from/to are zero-based squares (0\u201363); maximum captures apply.",
5127
+ "international-draughts": "move: from/to are zero-based squares (0\u201399). sequence: cards is a complete legal path of square indexes, including the origin.",
5128
+ reversi: "place: at is a zero-based row-major square (0\u201363). Unavailable turns pass automatically.",
5129
+ gomoku: "place: at is a zero-based row-major intersection (0\u2013224). Exactly five wins.",
5130
+ 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.",
5131
+ "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.',
5132
+ 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.",
5133
+ 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.",
5134
+ 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.",
5135
+ 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.",
5136
+ 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.`,
5137
+ 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.",
5138
+ 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.",
5139
+ football: "place: at is a zero-based destination point. The state records ball, used edges and continuing rebounds.",
5140
+ 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.",
5141
+ 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.",
5142
+ xiangqi: "move: numeric zero-based row-major from/to on the 9\xD710 board. promotion is unused.",
5143
+ 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.",
5144
+ durak: "play: at is your card ID from the visible hand; take/pass: no payload. Follow attack/defence roles and visible trump/table.",
5145
+ 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.",
5146
+ 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.",
5147
+ "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.",
5148
+ "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.",
5149
+ 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.",
5150
+ 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.",
5151
+ "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.",
5152
+ "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.",
5153
+ 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.",
5154
+ "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.",
5155
+ 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).",
5156
+ 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.",
5157
+ 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).",
5158
+ 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.",
5159
+ 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.",
5160
+ "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.",
5161
+ "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.",
5162
+ "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."
5163
+ };
5164
+ function actionHelp(game) {
5165
+ 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." };
5166
+ }
4831
5167
 
4832
5168
  // lib/multiplayer/euchre.ts
4833
5169
  var euchreSuit = (card2) => Math.floor(card2 / 6);
@@ -4856,7 +5192,7 @@ function redFromHand(t, seat, replace) {
4856
5192
  }
4857
5193
  return true;
4858
5194
  }
4859
- function deal2(s, entropy2) {
5195
+ function deal3(s, entropy2) {
4860
5196
  const t = structuredClone(s), deck = shuffled(
4861
5197
  Array.from({ length: 108 }, (_, i) => i),
4862
5198
  entropy2
@@ -4879,7 +5215,7 @@ function deal2(s, entropy2) {
4879
5215
  }
4880
5216
  function createCanasta(count, entropy2) {
4881
5217
  if (count !== 2 && count !== 4) throw Error("Choose two or four players.");
4882
- return deal2(
5218
+ return deal3(
4883
5219
  {
4884
5220
  hands: Array.from({ length: count }, () => []),
4885
5221
  stock: [],
@@ -4958,7 +5294,7 @@ function playCanasta(s, a, entropy2) {
4958
5294
  if (a.type === "next" && s.phase === "round") {
4959
5295
  t.round++;
4960
5296
  t.dealer = (t.dealer + 1) % s.hands.length;
4961
- return deal2(t, entropy2);
5297
+ return deal3(t, entropy2);
4962
5298
  }
4963
5299
  if (a.type === "stock" && s.phase === "draw") {
4964
5300
  t.openingTurn = !s.opened[i];
@@ -5102,7 +5438,7 @@ function binokelMeld(hand2, trump) {
5102
5438
  add([16, 5], [0, 4, 30, 60, 90], "binokel");
5103
5439
  return { score, labels: labels2, shown: [...shown] };
5104
5440
  }
5105
- function deal3(s, entropy2) {
5441
+ function deal4(s, entropy2) {
5106
5442
  const t = structuredClone(s), deck = shuffled(
5107
5443
  Array.from({ length: 80 }, (_, i) => i),
5108
5444
  entropy2
@@ -5125,7 +5461,7 @@ function deal3(s, entropy2) {
5125
5461
  return t;
5126
5462
  }
5127
5463
  function createBinokel(entropy2) {
5128
- return deal3(
5464
+ return deal4(
5129
5465
  {
5130
5466
  hands: [[], [], [], []],
5131
5467
  turn: 0,
@@ -5173,7 +5509,7 @@ function playBinokel(s, a, entropy2) {
5173
5509
  if (a.type === "next" && s.phase === "round") {
5174
5510
  t.round++;
5175
5511
  t.dealer = (t.dealer + 1) % 4;
5176
- return deal3(t, entropy2);
5512
+ return deal4(t, entropy2);
5177
5513
  }
5178
5514
  if (s.phase === "auction") {
5179
5515
  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 +5683,7 @@ function nextSeat(s, from, eligible) {
5347
5683
  return -1;
5348
5684
  }
5349
5685
  var canAct = (p) => !p.folded && !p.out && p.stack > 0;
5350
- function deal4(s, n) {
5686
+ function deal5(s, n) {
5351
5687
  s.deck.pop();
5352
5688
  for (let i = 0; i < n; i++) s.community.push(s.deck.pop());
5353
5689
  }
@@ -5412,13 +5748,13 @@ function advance(s) {
5412
5748
  }
5413
5749
  if (s.phase === "river") return award(s);
5414
5750
  if (s.phase === "preflop") {
5415
- deal4(s, 3);
5751
+ deal5(s, 3);
5416
5752
  s.phase = "flop";
5417
5753
  } else if (s.phase === "flop") {
5418
- deal4(s, 1);
5754
+ deal5(s, 1);
5419
5755
  s.phase = "turn";
5420
5756
  } else {
5421
- deal4(s, 1);
5757
+ deal5(s, 1);
5422
5758
  s.phase = "river";
5423
5759
  }
5424
5760
  s.bet = 0;
@@ -7981,127 +8317,29 @@ function playBackgammon(s, type, from, to, die, entropy2) {
7981
8317
  return backgammonMoves(next4).length ? next4 : {
7982
8318
  ...next4,
7983
8319
  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);
8320
+ turn: turn === 0 ? 1 : 0,
8321
+ result: "All moves are blocked. Turn passes."
8322
+ };
8089
8323
  }
8090
- if (!["place", "move"].includes(type) || !millMoves(s).some(
8091
- (m) => m.to === to && m.from === (type === "place" ? null : from)
8092
- ))
8324
+ const move2 = backgammonMoves(s).find(
8325
+ (m) => m.from === from && m.to === to && m.die === die
8326
+ );
8327
+ if (type !== "move" || !move2)
8093
8328
  throw Error(
8094
- "Place from your reserve or move to a connected point. With three pieces you may fly."
8329
+ "Use the maximum number of dice; if only one can be used, use the higher die."
8095
8330
  );
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);
8331
+ const next3 = step(s, move2);
8332
+ if (next3.off[s.turn] === 15) {
8333
+ const opponent = 1 - s.turn, backgammon = next3.bar[opponent] > 0 || next3.points.some(
8334
+ (n, i) => (s.turn === 0 ? i >= 18 : i < 6) && n * (s.turn === 0 ? -1 : 1) > 0
8335
+ );
8336
+ next3.winner = s.turn;
8337
+ next3.result = next3.off[opponent] ? "Single game." : backgammon ? "Backgammon \xB7 3 points." : "Gammon \xB7 2 points.";
8338
+ } else if (!backgammonMoves(next3).length) {
8339
+ next3.dice = [];
8340
+ next3.turn = s.turn === 0 ? 1 : 0;
8341
+ }
8342
+ return next3;
8105
8343
  }
8106
8344
 
8107
8345
  // lib/multiplayer/football.ts
@@ -8372,13 +8610,13 @@ function goAction(s, seat, action, at) {
8372
8610
  }
8373
8611
  const accepted = [.../* @__PURE__ */ new Set([...s.accepted, seat])];
8374
8612
  if (accepted.length < 2) return { ...s, accepted };
8375
- const scores = scoreGo(s);
8613
+ const scores2 = scoreGo(s);
8376
8614
  return {
8377
8615
  ...s,
8378
8616
  accepted,
8379
- scores,
8617
+ scores: scores2,
8380
8618
  phase: "finished",
8381
- winner: scores[0] > scores[1] ? 0 : 1
8619
+ winner: scores2[0] > scores2[1] ? 0 : 1
8382
8620
  };
8383
8621
  }
8384
8622
  function goProposalKey(s) {
@@ -8483,7 +8721,7 @@ var adapter5 = defineMatchAdapter({
8483
8721
  return {
8484
8722
  finished: state.winner !== null || state.draw,
8485
8723
  winner: state.winner,
8486
- label: state.draw ? "Draw" : state.winner !== null ? `Player ${state.winner + 1} wins \xB7 the opponent has no legal move.` : ""
8724
+ label: state.draw ? drawReason(state.history) : state.winner !== null ? `Player ${state.winner + 1} wins \xB7 the opponent has no legal move.` : ""
8487
8725
  };
8488
8726
  },
8489
8727
  apply(state, _seat, action, _entropy, _now) {
@@ -8501,7 +8739,7 @@ var adapter6 = defineMatchAdapter({
8501
8739
  actions: ["move", "sequence"],
8502
8740
  create: () => createInternational(),
8503
8741
  turn: (s) => s.turn,
8504
- outcome: (s) => ({ finished: s.winner !== null || s.draw, winner: s.winner, label: s.draw ? "Draw" : s.winner !== null ? `Player ${s.winner + 1} wins \xB7 the opponent has no legal move.` : "" }),
8742
+ outcome: (s) => ({ finished: s.winner !== null || s.draw, winner: s.winner, label: s.draw ? drawReason(s.history) : s.winner !== null ? `Player ${s.winner + 1} wins \xB7 the opponent has no legal move.` : "" }),
8505
8743
  apply(s, _seat, a) {
8506
8744
  if (a.type === "move" && typeof a.from === "number" && typeof a.to === "number") return moveInternational(s, a.from, a.to);
8507
8745
  if (a.type === "sequence" && Array.isArray(a.cards) && a.cards.length >= 2 && a.cards.length <= 21) {
@@ -8546,7 +8784,7 @@ var adapter8 = defineMatchAdapter({
8546
8784
  return {
8547
8785
  finished: state.winner !== null || state.draw,
8548
8786
  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.` : ""
8787
+ 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
8788
  };
8551
8789
  },
8552
8790
  apply(state, _seat, action, _entropy, _now) {
@@ -8733,9 +8971,9 @@ function loserResult(count, loser) {
8733
8971
  validSeat(count, loser);
8734
8972
  return { sides: Array.from({ length: count }, (_, seat) => ({ seats: [seat], place: seat === loser ? 1 : 0 })) };
8735
8973
  }
8736
- function scoreResult(scores, winner2, lowerWins = false) {
8737
- validSeat(scores.length, winner2);
8738
- const values = scores.map((n) => lowerWins ? -n : n);
8974
+ function scoreResult(scores2, winner2, lowerWins = false) {
8975
+ validSeat(scores2.length, winner2);
8976
+ const values = scores2.map((n) => lowerWins ? -n : n);
8739
8977
  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
8978
  }
8741
8979
  function partnershipResult(count, winner2) {
@@ -8752,7 +8990,7 @@ function next(s, seat) {
8752
8990
  const i = s.players.indexOf(seat);
8753
8991
  return s.players[(i + 1) % s.players.length];
8754
8992
  }
8755
- function deal5(s, entropy2) {
8993
+ function deal6(s, entropy2) {
8756
8994
  const t = structuredClone(s), count = s.hands.length, deck = shuffled(
8757
8995
  Array.from({ length: 52 }, (_, i) => i).filter((c) => cardRank(c) >= 9),
8758
8996
  entropy2
@@ -8784,7 +9022,7 @@ function deal5(s, entropy2) {
8784
9022
  }
8785
9023
  function createThousand(count, entropy2) {
8786
9024
  if (!index(count - 2, 3)) throw Error("Choose two to four players.");
8787
- return deal5(
9025
+ return deal6(
8788
9026
  {
8789
9027
  hands: Array.from({ length: count }, () => []),
8790
9028
  kitties: [],
@@ -8836,10 +9074,11 @@ function playThousand(s, a, entropy2) {
8836
9074
  if (a.type === "next" && s.phase === "round") {
8837
9075
  t.round++;
8838
9076
  t.dealer = (s.dealer + 1) % s.hands.length;
8839
- return deal5(t, entropy2);
9077
+ t.previous = s.result;
9078
+ return deal6(t, entropy2);
8840
9079
  }
8841
9080
  if (a.type === "redeal" && s.phase === "auction" && s.hands[seat].filter((c) => cardRank(c) === 9).length === 4)
8842
- return deal5(t, entropy2);
9081
+ return deal6(t, entropy2);
8843
9082
  if (s.phase === "auction") {
8844
9083
  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
9084
  t.bid = a.amount;
@@ -8882,9 +9121,9 @@ function playThousand(s, a, entropy2) {
8882
9121
  return t;
8883
9122
  }
8884
9123
  if (a.type === "bomb" && ["kitty", "exchange", "declare"].includes(s.phase)) {
8885
- if (t.bombs[seat]++ > 0)
8886
- t.players.filter((i) => i !== seat && t.scores[i] < 800).forEach((i) => t.scores[i] += t.hands.length === 4 ? 40 : 60);
8887
- t.result = "Declarer cancels the hand with a bomb.";
9124
+ const penalty2 = t.bombs[seat]++ > 0 ? t.hands.length === 4 ? 40 : 60 : 0;
9125
+ t.players.filter((i) => i !== seat && t.scores[i] < 800).forEach((i) => t.scores[i] += penalty2);
9126
+ t.result = `Player ${seat + 1} cancels the hand with a bomb${penalty2 ? `; the other players score ${penalty2}` : " (the first bomb is free)"}.`;
8888
9127
  finish4(t);
8889
9128
  return t;
8890
9129
  }
@@ -8929,10 +9168,12 @@ function playThousand(s, a, entropy2) {
8929
9168
  (n, c) => n + pointValue(cardRank(c)),
8930
9169
  0
8931
9170
  );
9171
+ const before = t.scores;
8932
9172
  t.scores = t.scores.map(
8933
9173
  (n, i) => i === t.bidder ? n + (t.points[i] >= t.contract ? t.contract : -t.contract) : t.players.includes(i) && n < 800 ? n + Math.round(t.points[i] / 10) * 10 : n
8934
9174
  );
8935
- t.result = `Hand points ${t.points.join(" / ")} \xB7 Contract ${t.contract}.`;
9175
+ const made = t.points[t.bidder] >= t.contract, change = (i) => `${t.scores[i] - before[i] >= 0 ? "+" : ""}${t.scores[i] - before[i]}`;
9176
+ t.result = `Player ${t.bidder + 1} ${made ? "makes" : "fails"} ${t.contract} with ${t.points[t.bidder]} (${change(t.bidder)}). ${t.players.filter((i) => i !== t.bidder).map((i) => `Player ${i + 1} ${t.points[i]} points (${before[i] >= 800 ? "not scored: 800 rule" : change(i)})`).join(", ")}.`;
8936
9177
  finish4(t);
8937
9178
  }
8938
9179
  return t;
@@ -9009,7 +9250,7 @@ var skatBids = [
9009
9250
  )
9010
9251
  ])
9011
9252
  ].filter((n) => n <= 264).sort((a, b) => a - b);
9012
- function deal6(s, entropy2) {
9253
+ function deal7(s, entropy2) {
9013
9254
  const t = structuredClone(s), deck = shuffled(
9014
9255
  Array.from({ length: 52 }, (_, i) => i).filter((c) => cardRank(c) >= 7),
9015
9256
  entropy2
@@ -9043,7 +9284,7 @@ function deal6(s, entropy2) {
9043
9284
  return t;
9044
9285
  }
9045
9286
  function createSkat(entropy2) {
9046
- return deal6(
9287
+ return deal7(
9047
9288
  {
9048
9289
  hands: [[], [], []],
9049
9290
  skat: [],
@@ -9144,7 +9385,7 @@ function playSkat(s, a, entropy2) {
9144
9385
  if (a.type === "next" && s.phase === "round") {
9145
9386
  t.round++;
9146
9387
  t.dealer = (s.dealer + 1) % 3;
9147
- return deal6(t, entropy2);
9388
+ return deal7(t, entropy2);
9148
9389
  }
9149
9390
  if (s.phase === "auction") {
9150
9391
  if (s.offer === null) {
@@ -9305,7 +9546,7 @@ function sides(t) {
9305
9546
  t.old = t.hands.map((_, i) => i === t.declarer);
9306
9547
  else t.old = t.hands.map((h) => queens.some((c) => h.includes(c)));
9307
9548
  }
9308
- function deal7(s, entropy2) {
9549
+ function deal8(s, entropy2) {
9309
9550
  const t = structuredClone(s), deck = shuffled(
9310
9551
  Array.from({ length: 52 }, (_, i) => i).filter(
9311
9552
  (c) => [10, 11, 12, 14].includes(cardRank(c))
@@ -9346,7 +9587,7 @@ function deal7(s, entropy2) {
9346
9587
  return t;
9347
9588
  }
9348
9589
  function createBaska(entropy2) {
9349
- return deal7(
9590
+ return deal8(
9350
9591
  {
9351
9592
  hands: [[], [], [], []],
9352
9593
  turn: 0,
@@ -9415,7 +9656,7 @@ function playBaska(s, a, entropy2) {
9415
9656
  if (a.type === "next" && s.phase === "round") {
9416
9657
  t.round++;
9417
9658
  t.dealer = (t.dealer + 1) % 4;
9418
- return deal7(t, entropy2);
9659
+ return deal8(t, entropy2);
9419
9660
  }
9420
9661
  if (a.type === "announce" && s.phase === "honor") {
9421
9662
  finish5(t, true);
@@ -9535,7 +9776,7 @@ var adapter17 = defineMatchAdapter({
9535
9776
 
9536
9777
  // lib/multiplayer/bridge.ts
9537
9778
  var vulnerability = [0, 1, 2, 3, 1, 2, 3, 0, 2, 3, 0, 1, 3, 0, 1, 2];
9538
- function deal8(s, entropy2) {
9779
+ function deal9(s, entropy2) {
9539
9780
  const t = structuredClone(s), deck = shuffled(
9540
9781
  Array.from({ length: 52 }, (_, i) => i),
9541
9782
  entropy2
@@ -9563,7 +9804,7 @@ function deal8(s, entropy2) {
9563
9804
  return t;
9564
9805
  }
9565
9806
  function createBridge(entropy2) {
9566
- return deal8(
9807
+ return deal9(
9567
9808
  {
9568
9809
  hands: [[], [], [], []],
9569
9810
  turn: 0,
@@ -9616,7 +9857,7 @@ function playBridge(s, a, entropy2) {
9616
9857
  if (a.type === "next" && s.phase === "round") {
9617
9858
  t.board++;
9618
9859
  t.dealer = (t.dealer + 1) % 4;
9619
- return deal8(t, entropy2);
9860
+ return deal9(t, entropy2);
9620
9861
  }
9621
9862
  if (s.phase === "auction") {
9622
9863
  if (a.type === "bid" && index((a.amount ?? 0) - 1, 35)) {
@@ -9722,7 +9963,7 @@ var adapter18 = defineMatchAdapter({
9722
9963
  });
9723
9964
 
9724
9965
  // lib/multiplayer/polish-hearts.ts
9725
- function deal9(s, entropy2) {
9966
+ function deal10(s, entropy2) {
9726
9967
  const t = structuredClone(s), count = s.hands.length, deck = shuffled(
9727
9968
  Array.from({ length: 52 }, (_, i) => i).filter(
9728
9969
  (c) => count !== 3 || c !== 0
@@ -9749,7 +9990,7 @@ function deal9(s, entropy2) {
9749
9990
  }
9750
9991
  function createPolish(count, entropy2) {
9751
9992
  if (count !== 3 && count !== 4) throw Error("Choose three or four players.");
9752
- return deal9(
9993
+ return deal10(
9753
9994
  {
9754
9995
  hands: Array.from({ length: count }, () => []),
9755
9996
  rest: [],
@@ -9813,7 +10054,7 @@ function playPolish(s, a, entropy2) {
9813
10054
  if (a.type === "next" && s.phase === "round") {
9814
10055
  t.round++;
9815
10056
  t.dealer = (t.dealer + 1) % count;
9816
- return deal9(t, entropy2);
10057
+ return deal10(t, entropy2);
9817
10058
  }
9818
10059
  if (a.type === "trump" && s.phase === "trump" && index(a.amount ?? -1, 4)) {
9819
10060
  t.trump = a.amount;
@@ -10188,6 +10429,7 @@ function playMakao(s, a, entropy2) {
10188
10429
  const t = structuredClone(s), seat = s.turn;
10189
10430
  if (a.type === "catch" && s.unannounced !== null && s.unannounced !== seat) {
10190
10431
  draw(t, s.unannounced, 5, entropy2);
10432
+ t.last = { seat: s.unannounced, type: "caught", count: 5 };
10191
10433
  t.unannounced = null;
10192
10434
  t.result = "Missed Makao: draw five.";
10193
10435
  return t;
@@ -10196,12 +10438,13 @@ function playMakao(s, a, entropy2) {
10196
10438
  t.unannounced = null;
10197
10439
  if (s.wait) {
10198
10440
  t.blocked[seat] = s.wait - 1;
10441
+ t.last = { seat, type: "wait", count: s.wait };
10199
10442
  t.wait = 0;
10200
10443
  advance3(t);
10201
10444
  return t;
10202
10445
  }
10203
10446
  if (s.penalty) {
10204
- draw(t, seat, s.penalty, entropy2);
10447
+ t.last = { seat, type: "penalty", count: draw(t, seat, s.penalty, entropy2).length };
10205
10448
  t.penalty = 0;
10206
10449
  t.suit = null;
10207
10450
  advance3(t);
@@ -10209,11 +10452,13 @@ function playMakao(s, a, entropy2) {
10209
10452
  }
10210
10453
  const received = draw(t, seat, 1, entropy2);
10211
10454
  t.drawn = received[0] ?? null;
10455
+ t.last = { seat, type: "draw", count: received.length };
10212
10456
  t.phase = "drawn";
10213
10457
  if (t.drawn === null) advance3(t);
10214
10458
  return t;
10215
10459
  }
10216
10460
  if (a.type === "pass" && s.phase === "drawn") {
10461
+ t.last = { seat, type: "keep", count: 1 };
10217
10462
  t.unannounced = null;
10218
10463
  t.suit = null;
10219
10464
  advance3(t);
@@ -10228,6 +10473,7 @@ function playMakao(s, a, entropy2) {
10228
10473
  throw Error("Request an ordinary rank, or choose no demand.");
10229
10474
  t.unannounced = null;
10230
10475
  t.hands[seat] = t.hands[seat].filter((c) => !cards.includes(c));
10476
+ t.last = { seat, type: "play", cards: [...cards], ...t.hands[seat].length === 1 && a.promotion === "makao" ? { announced: true } : {} };
10231
10477
  t.pile.push(...cards);
10232
10478
  t.suit = null;
10233
10479
  if (rank2 === 14) t.suit = a.amount;
@@ -10286,7 +10532,7 @@ var adapter21 = defineMatchAdapter({
10286
10532
  });
10287
10533
 
10288
10534
  // lib/multiplayer/three-five-eight.ts
10289
- function deal10(s, entropy2) {
10535
+ function deal11(s, entropy2) {
10290
10536
  const t = structuredClone(s), deck = shuffled(
10291
10537
  Array.from({ length: 52 }, (_, i) => i),
10292
10538
  entropy2
@@ -10323,7 +10569,7 @@ function deal10(s, entropy2) {
10323
10569
  return t;
10324
10570
  }
10325
10571
  function createThree(entropy2) {
10326
- return deal10(
10572
+ return deal11(
10327
10573
  {
10328
10574
  hands: [[], [], []],
10329
10575
  kitty: [],
@@ -10362,7 +10608,7 @@ function playThree(s, a, entropy2) {
10362
10608
  if (a.type === "next" && s.phase === "round") {
10363
10609
  t.round++;
10364
10610
  t.dealer = (s.dealer + 1) % 3;
10365
- return deal10(t, entropy2);
10611
+ return deal11(t, entropy2);
10366
10612
  }
10367
10613
  if (s.phase === "exchange" && a.type === "exchange" && a.at !== void 0 && s.hands[s.turn].includes(a.at)) {
10368
10614
  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 +11389,7 @@ var adapter26 = defineMatchAdapter({
11143
11389
 
11144
11390
  // lib/multiplayer/pan.ts
11145
11391
  var nineHearts = 33;
11146
- function deal11(count, entropy2, letters, round) {
11392
+ function deal12(count, entropy2, letters, round) {
11147
11393
  const cards = shuffled(
11148
11394
  Array.from({ length: 52 }, (_, i) => i).filter((c) => cardRank(c) >= 9),
11149
11395
  entropy2
@@ -11165,7 +11411,7 @@ function deal11(count, entropy2, letters, round) {
11165
11411
  }
11166
11412
  function createPan(count, entropy2) {
11167
11413
  if (!index(count - 2, 3)) throw Error("Choose two to four players.");
11168
- return deal11(count, entropy2, Array(count).fill(0), 0);
11414
+ return deal12(count, entropy2, Array(count).fill(0), 0);
11169
11415
  }
11170
11416
  function panLegal(s, cards) {
11171
11417
  const hand2 = s.hands[s.turn];
@@ -11181,7 +11427,7 @@ function panLegal(s, cards) {
11181
11427
  }
11182
11428
  function playPan(s, type, cards, entropy2) {
11183
11429
  if (type === "next" && s.phase === "round")
11184
- return deal11(s.hands.length, entropy2, s.letters, s.round + 1);
11430
+ return { ...deal12(s.hands.length, entropy2, s.letters, s.round + 1), previous: s.result };
11185
11431
  if (s.phase !== "play") throw Error("This round is finished.");
11186
11432
  const t = structuredClone(s), seat = s.turn;
11187
11433
  if (type === "play" && panLegal(s, cards)) {
@@ -11251,189 +11497,6 @@ var adapter27 = defineMatchAdapter({
11251
11497
  }
11252
11498
  });
11253
11499
 
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
11500
  // lib/multiplayer/adapters/gin-rummy.ts
11438
11501
  var adapter28 = defineMatchAdapter({
11439
11502
  actions: ["next", "pass", "stock", "upcard", "discard", "knock"],
@@ -11459,7 +11522,7 @@ var adapter28 = defineMatchAdapter({
11459
11522
  // lib/multiplayer/sixty-six.ts
11460
11523
  var sixtyStrength = (c) => [9, 11, 12, 13, 10, 14].indexOf(cardRank(c));
11461
11524
  var sixtyPoints = (c) => [0, 2, 3, 4, 10, 11][sixtyStrength(c)];
11462
- function deal13(dealer, scores, round, entropy2) {
11525
+ function deal13(dealer, scores2, round, entropy2) {
11463
11526
  const deck = shuffled(
11464
11527
  Array.from({ length: 52 }, (_, i) => i).filter((c) => cardRank(c) >= 9),
11465
11528
  entropy2
@@ -11481,7 +11544,7 @@ function deal13(dealer, scores, round, entropy2) {
11481
11544
  points: [0, 0],
11482
11545
  pending: [0, 0],
11483
11546
  tricks: [0, 0],
11484
- scores,
11547
+ scores: scores2,
11485
11548
  phase: "play",
11486
11549
  winner: null,
11487
11550
  result: "",
@@ -11518,7 +11581,7 @@ function award2(t, winner2, failure = false) {
11518
11581
  }
11519
11582
  function playSixtySix(s, type, at, marriage = false, claimMarriage = false, entropy2) {
11520
11583
  if (type === "next" && s.phase === "round")
11521
- return deal13(1 - s.dealer, s.scores, s.round + 1, entropy2);
11584
+ return { ...deal13(1 - s.dealer, s.scores, s.round + 1, entropy2), previous: s.result };
11522
11585
  if (s.phase !== "play") throw Error("This round is finished.");
11523
11586
  const t = structuredClone(s), seat = s.turn;
11524
11587
  if (type === "close" && !s.trick.length && s.closed === null && s.upcard !== null) {
@@ -11563,6 +11626,7 @@ function playSixtySix(s, type, at, marriage = false, claimMarriage = false, entr
11563
11626
  t.tricks[winner2]++;
11564
11627
  t.turn = winner2;
11565
11628
  t.lastTrick = t.trick;
11629
+ t.played = [...s.played ?? [], ...t.trick.map((c) => c.card)];
11566
11630
  t.trick = [];
11567
11631
  if (t.closed === null && t.upcard !== null)
11568
11632
  for (const i of [winner2, 1 - winner2]) {
@@ -11705,13 +11769,13 @@ function dominoMoves(s) {
11705
11769
  });
11706
11770
  }
11707
11771
  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);
11772
+ const scores2 = [...s.scores];
11773
+ if (winner2 !== null) scores2[winner2] += Math.round(bonus / 5) * 5;
11774
+ const finished = scores2.some((n) => n >= 100);
11711
11775
  return {
11712
11776
  ...s,
11713
- scores,
11714
- winner: finished ? best(scores) : null,
11777
+ scores: scores2,
11778
+ winner: finished ? best(scores2) : null,
11715
11779
  phase: finished ? "finished" : "round",
11716
11780
  turn: winner2 ?? s.turn,
11717
11781
  result: winner2 === null ? "Blocked round: equal pip totals." : `Player ${winner2 + 1} wins the round.`
@@ -11727,7 +11791,7 @@ function playDomino(s, type, at, end, entropy2) {
11727
11791
  turn: s.turn
11728
11792
  };
11729
11793
  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];
11794
+ const hands = s.hands.map((h) => [...h]), stock = [...s.stock], chain = [...s.chain], scores2 = [...s.scores];
11731
11795
  if (type === "draw") {
11732
11796
  if (dominoMoves(s).length)
11733
11797
  throw Error("Play a matching tile before drawing.");
@@ -11752,20 +11816,20 @@ function playDomino(s, type, at, end, entropy2) {
11752
11816
  }
11753
11817
  const left = chain[0], right = chain.at(-1);
11754
11818
  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;
11819
+ if (total % 5 === 0) scores2[s.turn] += total;
11756
11820
  const next3 = {
11757
11821
  ...s,
11758
11822
  hands,
11759
11823
  stock,
11760
11824
  chain,
11761
- scores,
11825
+ scores: scores2,
11762
11826
  passes: 0,
11763
11827
  turn: 1 - s.turn
11764
11828
  };
11765
11829
  if (!hands[s.turn].length)
11766
11830
  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) };
11831
+ if (scores2.some((n) => n >= 100))
11832
+ return { ...next3, phase: "finished", winner: best(scores2) };
11769
11833
  return next3;
11770
11834
  }
11771
11835
  function dominoView(s, seat) {
@@ -11983,11 +12047,12 @@ function playRummy(s, seat, a, entropy2) {
11983
12047
  return t;
11984
12048
  }
11985
12049
  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)) {
12050
+ const keepsDiscard = (cards) => s.hands[seat].some((c) => !cards.includes(c) && c !== s.taken);
12051
+ if (a.type === "meld" && ownedCards(s.hands[seat], a.cards) && keepsDiscard(a.cards) && rummyMeld(a.cards)) {
11987
12052
  t.melds.push({ seat, cards: [...a.cards].sort((a2, b) => rummyRank(a2) - rummyRank(b)) });
11988
12053
  t.hands[seat] = t.hands[seat].filter((c) => !a.cards.includes(c));
11989
12054
  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])) {
12055
+ } 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
12056
  t.melds[a.at].cards.push(...a.cards);
11992
12057
  t.melds[a.at].cards.sort((a2, b) => rummyRank(a2) - rummyRank(b));
11993
12058
  t.hands[seat] = t.hands[seat].filter((c) => !a.cards.includes(c));
@@ -12079,7 +12144,8 @@ var promotions = ["marriage", "marriage-claim", "makao", "normal", "schneider",
12079
12144
  function hand(state, seat) {
12080
12145
  for (const key2 of ["hands", "hand"]) {
12081
12146
  const hands = state[key2];
12082
- if (Array.isArray(hands) && Array.isArray(hands[seat])) return hands[seat].filter((c) => typeof c === "number" && c >= 0);
12147
+ const held = typeof state.playSeat === "number" && state.turn === seat ? state.playSeat : seat;
12148
+ if (Array.isArray(hands) && Array.isArray(hands[held])) return hands[held].filter((c) => typeof c === "number" && c >= 0);
12083
12149
  }
12084
12150
  return [];
12085
12151
  }
@@ -12150,6 +12216,10 @@ function specific(game, state, seat) {
12150
12216
  const s = state;
12151
12217
  return s.die ? ludoMoves(s).map((at) => ({ type: "move", at })) : [{ type: "roll" }];
12152
12218
  }
12219
+ if (game === "makao") return [...generic(adapters.makao, state, seat), ...cardGroups(hand(state, seat)).flatMap((cards) => [
12220
+ { type: "play", cards, promotion: "makao" },
12221
+ ...[0, 1, 2, 3, 5, 6, 7, 8, 9, 10, 13].flatMap((amount) => [{ type: "play", cards, amount }, { type: "play", cards, amount, promotion: "makao" }])
12222
+ ])];
12153
12223
  if (game === "go") {
12154
12224
  const s = state;
12155
12225
  if (s.phase === "scoring" && s.accepted.includes(seat)) return [];
@@ -12249,6 +12319,35 @@ function millsBoard(board) {
12249
12319
  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
12320
  };
12251
12321
  }
12322
+ function battleshipsBoard(v) {
12323
+ const ships = new Set(v.ownFleet.flat()), sunk = new Set(v.sunk.flat());
12324
+ const target = (at) => sunk.has(at) ? "#" : v.hits.includes(at) ? "x" : v.fired.includes(at) ? "o" : ".";
12325
+ const fleet = (at) => ships.has(at) ? v.incoming.includes(at) ? "X" : "S" : v.incoming.includes(at) ? "o" : ".";
12326
+ const row = (r, cell) => Array.from({ length: 10 }, (_, c) => cell(r * 10 + c)).join(" ");
12327
+ return {
12328
+ rows: 10,
12329
+ columns: 10,
12330
+ labeled: [
12331
+ ` target grid your fleet`,
12332
+ ` +0 1 2 3 4 5 6 7 8 9 +0 1 2 3 4 5 6 7 8 9`,
12333
+ ...Array.from({ length: 10 }, (_, r) => `${String(r * 10).padStart(2)} ${row(r, target)} ${row(r, fleet)}`)
12334
+ ],
12335
+ 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."
12336
+ };
12337
+ }
12338
+ function backgammonBoard(v) {
12339
+ const cell = (at) => `${String(at).padStart(2)}:${v.points[at] ? `${Math.abs(v.points[at])}${v.points[at] > 0 ? "X" : "O"}` : "."}`.padEnd(6);
12340
+ return {
12341
+ rows: 2,
12342
+ columns: 12,
12343
+ labeled: [
12344
+ Array.from({ length: 12 }, (_, i) => cell(12 + i)).join(" ").trimEnd(),
12345
+ Array.from({ length: 12 }, (_, i) => cell(11 - i)).join(" ").trimEnd(),
12346
+ `bar: X ${v.bar[0]}, O ${v.bar[1]} \xB7 off: X ${v.off[0]}/15, O ${v.off[1]}/15`
12347
+ ],
12348
+ 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."
12349
+ };
12350
+ }
12252
12351
  function fenBoard(fen) {
12253
12352
  const ranks = fen.split(" ")[0].split("/").map((rank2) => rank2.replace(/\d/g, (n) => ".".repeat(Number(n))));
12254
12353
  return {
@@ -12261,6 +12360,8 @@ function fenBoard(fen) {
12261
12360
  function indexedBoard(game, state) {
12262
12361
  const grid = grids[game], board = state?.board;
12263
12362
  if (game === "mills" && Array.isArray(board) && board.length === 24) return millsBoard(board);
12363
+ if (game === "backgammon" && state && typeof state === "object" && "points" in state) return backgammonBoard(state);
12364
+ if (game === "battleships" && state && typeof state === "object" && "ownFleet" in state) return battleshipsBoard(state);
12264
12365
  const fen = state?.fen;
12265
12366
  if ((game === "atomic-chess" || game === "three-check") && typeof fen === "string") return fenBoard(fen);
12266
12367
  if (!grid || !Array.isArray(board)) return null;
@@ -12322,12 +12423,40 @@ function agentPosition(room) {
12322
12423
  if (match.kind === "hex" && match.state.moves === 1 && match.state.turn === 1) legal.push({ type: "swap" });
12323
12424
  }
12324
12425
  }
12325
- const last = match?.kind === "connect-four" || match?.kind === "tic-tac-toe" || match?.kind === "gomoku" ? match.state.last : null;
12426
+ 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
12427
  const agreed = match?.kind === "go" && match.state.phase === "scoring" && match.state.accepted.includes(room.seat);
12327
12428
  const yours = room.phase === "playing" && !!match && turn === room.seat && !agreed && !room.players[room.seat].left && !ended && !room.players.some((p) => p.paused);
12328
12429
  const active = room.phase === "playing" && !!match && !room.players[room.seat].left && !ended && !room.players.some((p) => p.paused);
12329
12430
  const grid = !complete && match ? indexedBoard(room.game, match.state) : null;
12330
12431
  const wire = !complete && match && enumeratedAgentGames.includes(room.game) ? active ? agentLegalActions(room.game, match.state, room.seat, room.capacity) ?? [] : [] : null;
12432
+ const checkerLast = match?.kind === "checkers" || match?.kind === "brazilian-checkers" ? checkerLastMove(match.state.history) : null;
12433
+ const dotsLabeled = match?.kind === "dots-boxes" ? match.state.boxes.map((owner4, box) => {
12434
+ const { size, edges } = match.state, sides2 = boxEdges(size, box), drawn = sides2.filter((e) => edges[e] >= 0).length;
12435
+ const named = ["top", "bottom", "left", "right"].map((side2, i) => `${side2} ${sides2[i] + 1}${edges[sides2[i]] >= 0 ? "*" : ""}`).join(", ");
12436
+ return `box row ${Math.floor(box / size) + 1} col ${box % size + 1} [${owner4 < 0 ? "." : "XO"[owner4]}]: ${named}${owner4 < 0 && drawn === 3 ? " (three sides: takeable)" : ""}`;
12437
+ }) : null;
12438
+ const ginHand = match?.kind === "gin-rummy" && match.state.phase !== "finished" ? match.state.hands[room.seat]?.filter((c) => c !== null) ?? [] : [];
12439
+ const ginHint = ginHand.length ? (() => {
12440
+ const layout = ginLayout(ginHand), names = (cards) => cards.map((c) => `${cardLabel(c)} (id ${c})`);
12441
+ 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) : [];
12442
+ return {
12443
+ deadwood: layout.value,
12444
+ melds: layout.melds.map(names),
12445
+ deadwood_cards: names(layout.deadwood),
12446
+ ...discards.length ? { best_discard: `${cardLabel(discards[0].discard)} (id ${discards[0].discard})`, deadwood_after_best_discard: discards[0].deadwood } : {},
12447
+ knock_limit: 10
12448
+ };
12449
+ })() : null;
12450
+ const ruleNotes = match?.kind === "thousand" ? match.state.scores.flatMap((score, seat) => match.state.phase !== "finished" && score >= 800 ? [`Seat ${seat} (${room.players[seat]?.name ?? "empty"}) has ${score} points: from 800, defending points no longer score; only a made contract adds points.`] : []) : [];
12451
+ const sixtyUnseen = match?.kind === "sixty-six" && match.state.phase === "play" ? (() => {
12452
+ const s = match.state, seen = /* @__PURE__ */ new Set([...(s.hands[room.seat] ?? []).filter((c) => c !== null), ...s.played ?? [], ...s.trick.map((c) => c.card), ...s.upcard === null ? [] : [s.upcard]]);
12453
+ return Array.from({ length: 24 }, (_, i) => Math.floor(i / 6) * 13 + 7 + i % 6).filter((c) => !seen.has(c)).map((c) => `${cardLabel(c)} (id ${c})`);
12454
+ })() : null;
12455
+ const asianMove = match?.kind === "shogi" || match?.kind === "xiangqi" ? asianLastMove(match.state.history) : null;
12456
+ const asianLast = asianMove && (match?.kind === "shogi" || match?.kind === "xiangqi") ? { ...asianMove, by: match.state.history.at(-1).mover } : null;
12457
+ const millLast = match?.kind === "mills" ? millLastTurn(match.state) : null;
12458
+ const peg = match?.kind === "cribbage" && match.state.phase === "peg" ? match.state.lastPeg : void 0;
12459
+ const cribbageLast = peg ? { by_seat: peg.seat, ...peg.go ? { type: "go" } : { played: `${cribbageLabel(peg.card)} (id ${peg.card})` }, count: peg.count, ...peg.count === 31 || match?.kind === "cribbage" && match.state.total === 0 && peg.count > 0 ? { count_reset: true } : {} } : null;
12331
12460
  return {
12332
12461
  protocol_version: 1,
12333
12462
  room: room.code,
@@ -12342,15 +12471,18 @@ function agentPosition(room) {
12342
12471
  players: room.players.map((p, seat) => ({ seat, name: p.name, left: p.left, ...p.agent ? { agent: true } : {}, ...p.paused ? { paused: true } : {} })),
12343
12472
  board_size: complete ? { rows, columns } : grid ? { rows: grid.rows, columns: grid.columns } : { rows: 0, columns: 0 },
12344
12473
  board_top_to_bottom: board,
12345
- board_labeled: labeledBoard(room.game, board) ?? grid?.labeled ?? null,
12346
- ...grid ? { board_legend: grid.legend } : {},
12474
+ 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,
12475
+ ...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
12476
  // 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 },
12477
+ 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 : match?.kind === "makao" && match.state.last ? { by_seat: match.state.last.seat, type: match.state.last.type, ...match.state.last.cards ? { cards: match.state.last.cards.map((c) => `${cardLabel(c)} (id ${c})`) } : {}, ...match.state.last.count !== void 0 && match.state.last.type !== "play" ? { count: match.state.last.count } : {}, ...match.state.last.announced ? { announced_makao: true } : {} } : asianLast ? { by_seat: asianLast.by, ...asianLast.from === null ? { type: "drop" } : { from: asianLast.from }, to: asianLast.to } : match?.kind === "oware" && match.state.last !== null ? { last_seed_in_pit: match.state.last + 1 } : millLast ? { by_seat: millLast.seat, ...millLast.from === null ? { type: "place" } : { type: "move", from: millLast.from }, at: millLast.to, ...millLast.captured.length ? { captured: millLast.captured } : {} } : last === null ? null : room.game === "connect-four" ? { column: last % columns + 1, row_from_top: Math.floor(last / columns) + 1 } : { row: Math.floor(last / columns) + 1, column: last % columns + 1 },
12349
12478
  ...room.game === "chess" ? { fen } : {},
12350
12479
  // The side to move is in check (chess family, xiangqi, shogi).
12351
12480
  ...match && room.phase === "playing" && (match.kind === "chess" ? chessPosition(match.state).inCheck() : "check" in match.state && match.state.check === true) ? { in_check: true } : {},
12352
12481
  visible_state: match?.state ?? null,
12353
12482
  cards_labeled: match ? cardsLabeled(room.game, match.state) : null,
12483
+ ...ginHint ? { gin_deadwood: ginHint } : {},
12484
+ ...sixtyUnseen ? { sixty_six_unseen: sixtyUnseen } : {},
12485
+ ...ruleNotes.length ? { rule_notes: ruleNotes } : {},
12354
12486
  // Timed rounds (drawing, categories): agents cannot measure time, so give the remaining seconds.
12355
12487
  ...typeof match?.state?.deadline === "number" && room.serverNow ? { deadline_seconds_left: Math.max(0, Math.ceil((match.state.deadline - room.serverNow) / 1e3)) } : {},
12356
12488
  action_types: action_catalog_default[room.game],
@@ -12358,7 +12490,8 @@ function agentPosition(room) {
12358
12490
  legal_actions_complete: complete,
12359
12491
  legal_actions: yours ? legal : [],
12360
12492
  ...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,
12493
+ // Engine labels count players from 1; name the zero-based seat as everywhere else in the CLI.
12494
+ 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
12495
  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
12496
  terminal_reason: room.phase === "finished" ? room.players.some((p) => p.left) ? "resignation_or_leave" : "game_result" : null,
12364
12497
  rematch_ready: room.rematch.includes(room.seat),
@@ -12367,13 +12500,14 @@ function agentPosition(room) {
12367
12500
  }
12368
12501
  function labeledBoard(game, board) {
12369
12502
  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]}`];
12503
+ 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
12504
  const algebraic = game === "chess" || game === "checkers";
12372
12505
  const rowWidth = String(board.length).length, cell = algebraic ? 1 : String(board[0].length).length;
12373
12506
  const files2 = algebraic ? [..."abcdefgh"] : [...board[0]].map((_, c) => String(c + 1));
12374
12507
  return [
12375
12508
  `${" ".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(" ")}`)
12509
+ // Hex rows shift right one step per row, so the six neighbours sit around each cell.
12510
+ ...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
12511
  ];
12378
12512
  }
12379
12513
  function agentMove(position, input) {
@@ -12403,7 +12537,7 @@ function agentRules(game = "connect-four") {
12403
12537
  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
12538
  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
12539
  };
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.`;
12540
+ 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
12541
  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
12542
  }
12409
12543
  var connectFourAgentRules = agentRules();
@@ -12690,12 +12824,27 @@ function nextStep(position, event, brief = false, server) {
12690
12824
  const play = (move2) => `leafarcade game play ${room} --revision ${revision} --action ${move2}`;
12691
12825
  if (wire?.length && !position.turn?.yours) return `You may act now (phase action outside the normal turn): ${play("'<one legal_wire_actions entry>'")}${flag}. Otherwise wait: ${wait}`;
12692
12826
  if (position.turn?.yours) {
12693
- if (wire?.length === 1 && wire[0].type === "next" && Object.keys(wire[0]).length === 1) return `The hand is over; start the next one (the deal moves on): ${play(`'{"type":"next"}'`)} --wait${flag}`;
12827
+ if (wire?.length === 1 && wire[0].type === "next" && Object.keys(wire[0]).length === 1) return `The hand is over and you deal the next one: ${play(`'{"type":"next"}'`)} --wait${flag}`;
12694
12828
  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
12829
  const move2 = wire ? "'<one legal_wire_actions entry>'" : position.legal_actions_complete ? "'<one legal_actions entry>'" : "'<wire action JSON from game rules>'";
12696
12830
  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}`;
12698
- }
12831
+ const makao = position.game === "makao" ? ((state) => [
12832
+ state?.penalty ? `A ${state.penalty}-card penalty is active: answer with a matching attack card, or draw takes all ${state.penalty}. ` : "",
12833
+ state?.wait ? `A ${state.wait}-turn wait is active: answer with a 4, or draw serves the wait (no cards drawn). ` : "",
12834
+ state?.blocked?.[position.you.seat] ? `You are blocked for ${state.blocked[position.you.seat]} more turn(s). ` : "",
12835
+ state?.demand !== null && state?.demand !== void 0 ? `Jack demand in force: ${state.demand === 13 ? "king" : `rank ${state.demand}`}, or answer with a jack${state.demandTurns ? ` (${state.demandTurns} turn(s) left)` : ""}. ` : "",
12836
+ state?.suit !== null && state?.suit !== void 0 ? `Ace suit in force: ${["clubs", "diamonds", "hearts", "spades"][state.suit]}. ` : "",
12837
+ state?.phase === "drawn" && state.drawn !== null && state.drawn !== void 0 ? `You drew ${cardLabel(state.drawn)} (id ${state.drawn}): play it if it is listed, otherwise pass to keep it. ` : "",
12838
+ wire?.some((action) => action.type === "play" && Array.isArray(action.cards) && action.cards.length === (state?.hands?.[position.you.seat]?.length ?? 0) - 1) ? 'A play that leaves you one card must use its entry with "promotion":"makao", or the opponent can make you draw five. ' : ""
12839
+ ].join(""))(position.visible_state) : "";
12840
+ const chain = position.visible_state?.chain;
12841
+ const jumper = (position.legal_actions?.[0] ?? wire?.[0])?.from;
12842
+ const capture = chain !== null && chain !== void 0 && jumper !== void 0 ? `Your capture continues: jump again with the piece on ${String(jumper)} (one jump per call). ` : "";
12843
+ 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). " : "";
12844
+ return `Your move. ${capture}${mill}${makao}${partial}${check}${play(move2)} --wait${flag}`;
12845
+ }
12846
+ const dealing = ["round", "show"].includes(String(position.visible_state?.phase));
12847
+ if (dealing && position.turn) return `The hand is over; seat ${position.turn.seat} (${position.players[position.turn.seat]?.name ?? "empty"}) deals the next one. Do not send next yourself. Wait: ${wait}`;
12699
12848
  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
12849
  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.";
12701
12850
  const mover = position.turn ? ` (seat ${position.turn.seat}, ${position.players[position.turn.seat]?.name ?? "empty"}, to act)` : "";
@@ -12786,7 +12935,7 @@ async function installSkill(agent = "codex", update = false) {
12786
12935
  // packages/cli/package.json
12787
12936
  var package_default = {
12788
12937
  name: "@leafarcade/cli",
12789
- version: "0.7.1",
12938
+ version: "0.9.0",
12790
12939
  private: false,
12791
12940
  description: "Create private browser board games and play with your AI agent. Includes 53 games and an installable Codex/Claude skill.",
12792
12941
  type: "module",
@@ -13082,6 +13231,15 @@ function stateChanges(game, before, after, limit = 30) {
13082
13231
  const here = path.join(".");
13083
13232
  const primitive = (v) => v === null || typeof v !== "object";
13084
13233
  if (Array.isArray(a) && Array.isArray(b)) {
13234
+ if (a.length && b.length && [...a, ...b].every((v) => typeof v === "string")) {
13235
+ for (let k = 0; k < a.length; k++) {
13236
+ const kept = a.slice(k);
13237
+ if (kept.every((v, i) => v === b[i]) && b.length > kept.length) {
13238
+ out2.push(`${here}: + ${b.slice(kept.length).map((v) => show(v)).join(", ")}`);
13239
+ return;
13240
+ }
13241
+ }
13242
+ }
13085
13243
  if (a.length === b.length && a.length > 4 && a.every(primitive) && b.every(primitive)) {
13086
13244
  a.forEach((v, i) => {
13087
13245
  if (v !== b[i] && out2.length <= limit) out2.push(`${here}[${i}]: ${show(v)} \u2192 ${show(b[i])}`);
@@ -13137,7 +13295,14 @@ async function withChanges(result, path) {
13137
13295
  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
13296
  const square2 = (at) => "abcdefgh"[at % 8] + (8 - Math.floor(at / 8));
13139
13297
  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 } };
13298
+ const piles = [before.state, position.visible_state].map((state) => state?.discard);
13299
+ const top = position.game === "gin-rummy" && Array.isArray(piles[0]) && Array.isArray(piles[1]) ? piles[0].at(-1) : void 0;
13300
+ const taken = typeof top === "number" && !piles[1].includes(top) ? `${cardLabel(top)} (id ${top})` : null;
13301
+ const annotated = {
13302
+ ...position,
13303
+ ...position.last_move === null && derived.length && derived.length <= 12 ? { last_move: { board_changes: derived } } : {},
13304
+ changed_since_last_read: { from_revision: before.revision, ...taken ? { taken_from_discard: taken } : {}, changes }
13305
+ };
13141
13306
  return "position" in result ? { ...result, position: annotated } : annotated;
13142
13307
  }
13143
13308
 
@@ -13542,6 +13707,11 @@ var winner = (position) => {
13542
13707
  return seat === void 0 || seat === null ? null : seat === position.you.seat ? "you" : seatName(position, seat);
13543
13708
  };
13544
13709
  var seatName = (position, seat) => `seat ${seat} (${position.players[seat]?.name ?? "empty"})`;
13710
+ var scores = (position) => {
13711
+ const value = position.visible_state?.scores;
13712
+ return Array.isArray(value) && value.length && value.every((n) => typeof n === "number") ? value : null;
13713
+ };
13714
+ var withoutLegal = (state) => Object.fromEntries(Object.entries(state).filter(([key2]) => key2 !== "legalActions"));
13545
13715
  function briefPosition(position) {
13546
13716
  const turn = position.turn ? position.turn.yours ? "yours" : seatName(position, position.turn.seat) : null;
13547
13717
  const wire = "legal_wire_actions" in position ? position.legal_wire_actions ?? [] : null;
@@ -13554,6 +13724,8 @@ function briefPosition(position) {
13554
13724
  you: position.you.piece,
13555
13725
  turn,
13556
13726
  ...position.paused ? { paused: true } : {},
13727
+ // Match or capture scores by zero-based seat, when the game keeps them.
13728
+ ...scores(position) ? { scores: scores(position) } : {},
13557
13729
  ..."in_check" in position ? { in_check: true } : {},
13558
13730
  ...position.you.left ? { left: true } : {},
13559
13731
  ...position.phase === "waiting" ? { seated: position.players.filter((player) => !player.left).length, capacity: position.capacity } : {},
@@ -13567,8 +13739,12 @@ function briefPosition(position) {
13567
13739
  ..."deadline_seconds_left" in position ? { deadline_seconds_left: position.deadline_seconds_left } : {},
13568
13740
  ..."board_legend" in position ? { board_legend: position.board_legend } : {},
13569
13741
  // Only the classic convenience games are fully described by their labeled board.
13570
- ...position.legal_actions_complete && position.board_labeled || !position.visible_state ? {} : { visible_state: position.visible_state },
13742
+ // The legal moves are listed once at the top level, not again inside the state.
13743
+ ...position.legal_actions_complete && position.board_labeled || !position.visible_state ? {} : { visible_state: withoutLegal(position.visible_state) },
13571
13744
  ...position.cards_labeled ? { cards_labeled: position.cards_labeled } : {},
13745
+ ..."gin_deadwood" in position ? { gin_deadwood: position.gin_deadwood } : {},
13746
+ ..."sixty_six_unseen" in position ? { sixty_six_unseen: position.sixty_six_unseen } : {},
13747
+ ..."rule_notes" in position ? { rule_notes: position.rule_notes } : {},
13572
13748
  ...position.access_requests?.length ? { access_requests: position.access_requests } : {}
13573
13749
  };
13574
13750
  }
@@ -13718,7 +13894,7 @@ try {
13718
13894
  result = { event: "browser_requested", room, url, browser: await openBrowser(url) };
13719
13895
  } else if (command === "start" || command === "leave" || command === "resign" || command === "rematch" || command === "pause" || command === "resume") {
13720
13896
  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");
13897
+ if (revision !== view.revision) throw new ServiceError(`The room changed (now revision ${view.revision}). Read game status before changing the room.`, "STALE_REVISION");
13722
13898
  if (command === "start") {
13723
13899
  await rpc(session, "start", { revision }, false, controller.signal);
13724
13900
  result = { event: "started", position: agentPosition(await readRoom(session, controller.signal)) };
@@ -13740,7 +13916,7 @@ try {
13740
13916
  const revision = integer(values.revision, "revision", 0, Number.MAX_SAFE_INTEGER);
13741
13917
  const column = values.column === void 0 ? void 0 : integer(values.column, "column", 1, position.board_size.columns);
13742
13918
  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");
13919
+ if (revision !== position.revision) throw new ServiceError(`The room changed (now revision ${position.revision}). Read game status before playing.`, "STALE_REVISION");
13744
13920
  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
13921
  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
13922
  const waitSeconds = integer(values.timeout ?? String(defaultWaitSeconds), "timeout", 1, 3600);