@leafarcade/cli 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.mjs +147 -107
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Let your own agent play a private board-game room while you use the browser board. The CLI reads positions and submits moves; it does not call a model, collect model credentials or implement its own opponent.
4
4
 
5
- **Scope of 0.7.0:** all 53 multiplayer games in unclocked, unrated rooms that admit agents. Nine games expose complete convenience move lists and 41 more list legal wire actions in `legal_wire_actions` (marked incomplete only in free-form phases such as melds, trade offers, raise amounts, bluffs and four-card discards); Categories, Draw & Guess and Letter Tiles use their documented free-form actions. Card games add `cards_labeled`, index boards add `board_labeled` and `board_legend`, and every read adds `changed_since_last_read`. Local automatic play is available through the watcher below. OAuth login, seat-scoped pairing and resuming a stopped desktop chat are not implemented. Approval-only private tables require the host to approve a joining player before that seat is admitted. Invite-only tables admit players holding the invitation. Spectator and agent admission follow the host’s settings.
5
+ **Scope of 0.7.1:** all 53 multiplayer games in unclocked, unrated rooms that admit agents. Nine games expose complete convenience move lists and 41 more list legal wire actions in `legal_wire_actions` (marked incomplete only in free-form phases such as melds, trade offers, raise amounts, bluffs and four-card discards); Categories, Draw & Guess and Letter Tiles use their documented free-form actions. Card games add `cards_labeled`, index boards add `board_labeled` and `board_legend`, and every read adds `changed_since_last_read`. Local automatic play is available through the watcher below. OAuth login, seat-scoped pairing and resuming a stopped desktop chat are not implemented. Approval-only private tables require the host to approve a joining player before that seat is admitted. Invite-only tables admit players holding the invitation. Spectator and agent admission follow the host’s settings.
6
6
 
7
7
  ## Start a game from your agent
8
8
 
package/dist/cli.mjs CHANGED
@@ -4415,6 +4415,107 @@ var action_catalog_default = {
4415
4415
  ]
4416
4416
  };
4417
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
+
4418
4519
  // lib/agents/action-help.ts
4419
4520
  var help = {
4420
4521
  amazons: "move: numeric from/to 0\u201399 and amount for the arrow square. Select an exact legal_wire_actions entry.",
@@ -4443,8 +4544,8 @@ var help = {
4443
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.",
4444
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.",
4445
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.",
4446
- dice: "roll: no payload. hold: at is a die index (0\u20134) to toggle its hold after rolling. score: at is a category index. Inspect the roll phase and score sheet.",
4447
- 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 leaves the yard and keeps the turn.",
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.",
4448
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.",
4449
4550
  football: "place: at is a zero-based destination point. The state records ball, used edges and continuing rebounds.",
4450
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.",
@@ -4456,17 +4557,17 @@ var help = {
4456
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.",
4457
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.",
4458
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.",
4459
- hearts: "select: at is a card ID from the visible hand for passing, 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.",
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.",
4460
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.",
4461
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.",
4462
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.",
4463
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.",
4464
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.",
4465
- 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 declarer deals the next hand.",
4466
- 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 about 240 points per hand, so bids far above that rarely make. The declaring team needs 20 meld points.",
4467
- 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 previous declarer deals the next hand.",
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).",
4468
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.",
4469
- 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.",
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.",
4470
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.",
4471
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.",
4472
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."
@@ -5538,6 +5639,7 @@ var labels = {
5538
5639
  var singleCard = /^(upcard|starter|top|trumpCard|card|attack|defen[cs]e|turned)$/;
5539
5640
  var suits = ["\u2663 clubs", "\u2666 diamonds", "\u2665 hearts", "\u2660 spades"];
5540
5641
  var cardField = /^(hands?|hole|trick|lastTrick|tricks?Won|pile|table|attack|defen[cs]e|discards?|kitty|kitties|shownKitty|skat|crib|starter|upcard|top|community|board|melds?|layout|red|revealed|played|dummy|selected|cards?|exchange|stock|widow|talon|turned|trumpCard|last)$/;
5642
+ var hasCardLabels = (game) => game in labels;
5541
5643
  function cardsLabeled(game, state) {
5542
5644
  const label = labels[game];
5543
5645
  if (!label || !state || typeof state !== "object") return null;
@@ -8333,7 +8435,7 @@ var adapter3 = defineMatchAdapter({
8333
8435
  return {
8334
8436
  finished: state.winner !== null || state.draw,
8335
8437
  winner: state.winner,
8336
- label: state.draw ? "Repetition or insufficient material" : state.winner !== null ? `${state.check ? "Checkmate" : "No legal move"} \xB7 Player ${state.winner + 1} wins.` : state.check ? "Check" : ""
8438
+ label: state.draw ? "Draw \xB7 repetition, insufficient material or the move limit" : state.winner !== null ? `${state.check ? "Checkmate" : "No legal move"} \xB7 Player ${state.winner + 1} wins.` : state.check ? "Check" : ""
8337
8439
  };
8338
8440
  },
8339
8441
  apply(state, _seat, action, _entropy, _now) {
@@ -8357,7 +8459,7 @@ var adapter4 = defineMatchAdapter({
8357
8459
  return {
8358
8460
  finished: state.winner !== null || state.draw,
8359
8461
  winner: state.winner,
8360
- label: state.draw ? "Repetition or insufficient material" : state.winner !== null ? `${state.check ? "Checkmate" : "No legal move"} \xB7 Player ${state.winner + 1} wins.` : state.check ? "Check" : ""
8462
+ label: state.draw ? "Draw \xB7 repetition, insufficient material or the move limit" : state.winner !== null ? `${state.check ? "Checkmate" : "No legal move"} \xB7 Player ${state.winner + 1} wins.` : state.check ? "Check" : ""
8361
8463
  };
8362
8464
  },
8363
8465
  apply(state, _seat, action, _entropy, _now) {
@@ -8492,7 +8594,7 @@ var adapter10 = defineMatchAdapter({
8492
8594
  return {
8493
8595
  finished: state.winner !== null,
8494
8596
  winner: state.winner,
8495
- label: state.last
8597
+ label: state.winner !== null ? `Player ${state.winner + 1} brings all four tokens home and wins.` : state.last
8496
8598
  };
8497
8599
  },
8498
8600
  apply(state, _seat, action, entropy2, _now) {
@@ -10652,6 +10754,7 @@ function settle(s, entropy2) {
10652
10754
  s.bills.shift();
10653
10755
  }
10654
10756
  if (["buy", "tax", "trade", "auction"].includes(s.phase)) return;
10757
+ if (s.players[s.active].bankrupt) s.pendingMove = null;
10655
10758
  if (s.pendingMove !== null) {
10656
10759
  const distance = s.pendingMove;
10657
10760
  s.pendingMove = null;
@@ -11548,92 +11651,6 @@ var adapter30 = defineMatchAdapter({
11548
11651
  leave: leavePoker
11549
11652
  });
11550
11653
 
11551
- // lib/multiplayer/dice.ts
11552
- function createDice(count) {
11553
- if (!index(count - 2, 3)) throw Error("Choose two to four players.");
11554
- return {
11555
- dice: [0, 0, 0, 0, 0],
11556
- held: Array(5).fill(false),
11557
- rolls: 0,
11558
- sheets: Array.from({ length: count }, () => Array(13).fill(null)),
11559
- bonuses: Array(count).fill(0),
11560
- turn: 0,
11561
- finished: false,
11562
- winner: null
11563
- };
11564
- }
11565
- function diceScore(dice, category, joker = false) {
11566
- const counts = Array.from(
11567
- { length: 6 },
11568
- (_, i) => dice.filter((v) => v === i + 1).length
11569
- ), sum = dice.reduce((a, b) => a + b, 0);
11570
- if (category < 6) return counts[category] * (category + 1);
11571
- switch (category) {
11572
- case 6:
11573
- return Math.max(...counts) >= 3 ? sum : 0;
11574
- case 7:
11575
- return Math.max(...counts) >= 4 ? sum : 0;
11576
- case 8:
11577
- return joker || counts.includes(3) && counts.includes(2) ? 25 : 0;
11578
- case 9:
11579
- return joker || [0, 1, 2].some((start) => counts.slice(start, start + 4).every(Boolean)) ? 30 : 0;
11580
- case 10:
11581
- return joker || [0, 1].some((start) => counts.slice(start, start + 5).every(Boolean)) ? 40 : 0;
11582
- case 11:
11583
- return counts.includes(5) ? 50 : 0;
11584
- case 12:
11585
- return sum;
11586
- default:
11587
- throw Error("Choose a score category.");
11588
- }
11589
- }
11590
- function diceTotal(s, seat) {
11591
- const sheet = s.sheets[seat], upper = sheet.slice(0, 6).reduce((sum, n) => sum + (n ?? 0), 0);
11592
- return sheet.reduce((sum, n) => sum + (n ?? 0), 0) + (upper >= 63 ? 35 : 0) + s.bonuses[seat];
11593
- }
11594
- function playDice(s, type, at, entropy2) {
11595
- if (s.finished) throw Error("This match is finished.");
11596
- if (type === "roll") {
11597
- if (s.rolls >= 3) throw Error("Record a score after three rolls.");
11598
- const random = draws(entropy2, 5);
11599
- return {
11600
- ...s,
11601
- rolls: s.rolls + 1,
11602
- dice: s.dice.map(
11603
- (v, i) => s.rolls && s.held[i] ? v : 1 + Math.floor(random[i] * 6)
11604
- )
11605
- };
11606
- }
11607
- if (at === void 0 || !index(at, type === "hold" ? 5 : 13) || !s.rolls)
11608
- throw Error("Roll first, then choose a die or score category.");
11609
- if (type === "hold")
11610
- return { ...s, held: s.held.map((v, i) => i === at ? !v : v) };
11611
- if (type !== "score" || s.sheets[s.turn][at] !== null)
11612
- throw Error("Choose an unused score category.");
11613
- const sheets = s.sheets.map((row) => [...row]), bonuses = [...s.bonuses];
11614
- const five = s.dice.every((v) => v === s.dice[0]), upper = s.dice[0] - 1;
11615
- const joker = five && sheets[s.turn][11] !== null && sheets[s.turn][upper] !== null;
11616
- if (joker && sheets[s.turn].slice(6).some((n) => n === null) && at < 6)
11617
- throw Error(
11618
- "Use the five-of-a-kind joker in an unused lower category first."
11619
- );
11620
- sheets[s.turn][at] = diceScore(s.dice, at, joker);
11621
- if (joker && sheets[s.turn][11] === 50) bonuses[s.turn] += 100;
11622
- const next3 = {
11623
- ...s,
11624
- sheets,
11625
- bonuses,
11626
- turn: (s.turn + 1) % s.sheets.length,
11627
- rolls: 0,
11628
- held: Array(5).fill(false),
11629
- dice: [0, 0, 0, 0, 0]
11630
- };
11631
- next3.finished = sheets.every((row) => row.every((n) => n !== null));
11632
- if (next3.finished)
11633
- next3.winner = best(sheets.map((_, i) => diceTotal(next3, i)));
11634
- return next3;
11635
- }
11636
-
11637
11654
  // lib/multiplayer/adapters/dice.ts
11638
11655
  var adapter31 = defineMatchAdapter({
11639
11656
  ranking: (state) => scoreResult(state.sheets.map((_, seat) => diceTotal(state, seat)), state.winner),
@@ -12135,6 +12152,7 @@ function specific(game, state, seat) {
12135
12152
  }
12136
12153
  if (game === "go") {
12137
12154
  const s = state;
12155
+ if (s.phase === "scoring" && s.accepted.includes(seat)) return [];
12138
12156
  if (s.phase === "scoring") return [...s.board.flatMap((p, at) => p ? [{ type: "dead", at }] : []), { type: "accept", scoreKey: goProposalKey(s) }, { type: "resume" }];
12139
12157
  return s.phase === "play" && s.turn === seat ? [...s.board.flatMap((p, at) => p ? [] : [{ type: "place", at }]), { type: "pass" }] : [];
12140
12158
  }
@@ -12188,7 +12206,7 @@ function reconstruct(game, view, seat) {
12188
12206
  const own = view.ownFleet, fired = view.fired, incoming = view.incoming;
12189
12207
  return { ...view, fleets: seat === 0 ? [own, []] : [[], own], shots: seat === 0 ? [fired, incoming] : [incoming, fired] };
12190
12208
  }
12191
- var entropy = Array.from({ length: 128 }, (_, i) => i * 7919 + 1);
12209
+ var entropy = Array.from({ length: 512 }, (_, i) => i * 7919 + 1);
12192
12210
  function hydrate(adapter36, view, seats2) {
12193
12211
  let template;
12194
12212
  try {
@@ -12305,7 +12323,8 @@ function agentPosition(room) {
12305
12323
  }
12306
12324
  }
12307
12325
  const last = match?.kind === "connect-four" || match?.kind === "tic-tac-toe" || match?.kind === "gomoku" ? match.state.last : null;
12308
- const yours = room.phase === "playing" && !!match && turn === room.seat && !room.players[room.seat].left && !ended && !room.players.some((p) => p.paused);
12326
+ const agreed = match?.kind === "go" && match.state.phase === "scoring" && match.state.accepted.includes(room.seat);
12327
+ const yours = room.phase === "playing" && !!match && turn === room.seat && !agreed && !room.players[room.seat].left && !ended && !room.players.some((p) => p.paused);
12309
12328
  const active = room.phase === "playing" && !!match && !room.players[room.seat].left && !ended && !room.players.some((p) => p.paused);
12310
12329
  const grid = !complete && match ? indexedBoard(room.game, match.state) : null;
12311
12330
  const wire = !complete && match && enumeratedAgentGames.includes(room.game) ? active ? agentLegalActions(room.game, match.state, room.seat, room.capacity) ?? [] : [] : null;
@@ -12347,7 +12366,8 @@ function agentPosition(room) {
12347
12366
  };
12348
12367
  }
12349
12368
  function labeledBoard(game, board) {
12350
- if (!board?.length || game === "oware" || game === "dots-boxes") return null;
12369
+ 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]}`];
12351
12371
  const algebraic = game === "chess" || game === "checkers";
12352
12372
  const rowWidth = String(board.length).length, cell = algebraic ? 1 : String(board[0].length).length;
12353
12373
  const files2 = algebraic ? [..."abcdefgh"] : [...board[0]].map((_, c) => String(c + 1));
@@ -12358,6 +12378,10 @@ function labeledBoard(game, board) {
12358
12378
  }
12359
12379
  function agentMove(position, input) {
12360
12380
  const action = position.legal_actions.find((a) => a.type === "swap" ? input.swap === true && Object.entries(input).every(([k, v]) => k === "swap" || v === void 0) : a.type === "edge" ? input.edge === a.edge && Object.entries(input).every(([k, v]) => k === "edge" || v === void 0) : a.type === "sow" ? input.pit === a.pit && Object.entries(input).every(([k, v]) => k === "pit" || v === void 0) : !input.swap && input.edge === void 0 && input.pit === void 0 && a.type === "move" ? input.column === void 0 && input.row === void 0 && a.from === input.from && a.to === input.to && a.promotion === input.promotion : a.type === "place" || a.type === "drop" ? !input.swap && input.edge === void 0 && input.pit === void 0 && input.from === void 0 && input.to === void 0 && input.promotion === void 0 && a.column === input.column && (a.type === "drop" ? input.row === void 0 : a.row === input.row) : false);
12381
+ if (!action && input.from !== void 0 && input.to !== void 0 && "legal_wire_actions" in position) {
12382
+ const wire = position.legal_wire_actions?.find((a) => a.type === "move" && String(a.from) === input.from && String(a.to) === input.to && (a.promotion === void 0 || a.promotion === input.promotion));
12383
+ if (wire) return wire;
12384
+ }
12361
12385
  if (!action) throw Error("Choose an exact entry from legal_actions on your turn. Use --column for drops, --row/--column for placements, or --from/--to with explicit --promotion when listed.");
12362
12386
  if (action.type === "swap") return { type: "swap" };
12363
12387
  if (action.type === "edge") return { type: "place", at: action.edge - 1 };
@@ -12371,7 +12395,7 @@ function agentRules(game = "connect-four") {
12371
12395
  const rules = {
12372
12396
  "connect-four": "Drop into an unfilled column. Gravity places the piece in the lowest empty cell. Four or more matching pieces horizontally, vertically or diagonally wins; a full board without a winner draws.",
12373
12397
  chess: "Standard chess, including castling, en passant, check, checkmate, stalemate, repetition and fifty-move draws. Seat 0 is White. Pick a listed from/to move. Promotions explicitly list q, r, b and n; supply --promotion. FEN is included for complete castling and en passant state.",
12374
- checkers: "English draughts on 8 by 8. Seat 0 moves from the bottom towards row 1. Captures are compulsory; kings move one diagonal square in either direction. A multiple jump is submitted one step at a time: read status and continue with the same piece while it remains your turn. Crowning ends a turn. No pieces or no legal moves loses; repetition and 80 quiet half-moves draw.",
12398
+ checkers: "English draughts on 8 by 8. Seat 0 (x) starts on ranks 1\u20133 at the bottom and moves up towards rank 8; seat 1 (o) moves down. Men move and capture forward only, as in English draughts. Captures are compulsory; kings move one diagonal square in either direction. A multiple jump is submitted one step at a time: read status and continue with the same piece while it remains your turn. Crowning ends a turn. No pieces or no legal moves loses; repetition and 80 quiet half-moves draw.",
12375
12399
  "tic-tac-toe": "X (seat 0) starts on a 3 by 3 board. Place on an empty square. Three matching marks horizontally, vertically or diagonally wins; a full board without a winner draws.",
12376
12400
  reversi: "Bracket opposing discs in a straight line; all bracketed discs flip. Seat 0 is Black and starts. The engine automatically skips players with no legal move; the same seat may move twice. No pass command is needed. When neither can move, the most discs wins; equal counts draw.",
12377
12401
  hex: "Connect opposite edges on the hexagonal board: seat 0 top to bottom, seat 1 left to right. On seat 1's first turn it may swap; this transposes the first stone and changes its owner. No draws.",
@@ -12558,6 +12582,7 @@ function networkCause(error) {
12558
12582
  var readRoom = (session, signal) => rpc(session, "read", {}, true, signal);
12559
12583
  var continuousGames = ["draw-guess", "categories"];
12560
12584
  function waitForRoom(session, after, seconds, signal, untilAction = false) {
12585
+ const sawRevision = after !== void 0;
12561
12586
  return new Promise((resolve, reject) => {
12562
12587
  let socket, closed = false, reconnects = 0, received = false, opened = false;
12563
12588
  let ping, retry;
@@ -12599,7 +12624,7 @@ function waitForRoom(session, after, seconds, signal, untilAction = false) {
12599
12624
  received = true;
12600
12625
  after ??= latest.revision;
12601
12626
  if (latest.revision < after) throw new ServiceError("Your revision is ahead of this room. Read game status again.", "STALE_REVISION");
12602
- const canAct2 = !continuousGames.includes(latest.game) && (latest.turn?.yours || !!(latest.legal_wire_actions_complete && latest.legal_wire_actions?.length));
12627
+ const canAct2 = !continuousGames.includes(latest.game) && (latest.turn?.yours || !sawRevision && !!(latest.legal_wire_actions_complete && latest.legal_wire_actions?.length));
12603
12628
  if (untilAction && latest.revision > after && !canAct2 && latest.phase !== "finished" && !latest.you.left && !latest.paused) after = latest.revision;
12604
12629
  if (latest.revision > after || canAct2 || latest.phase === "finished" || latest.you.left || latest.paused) finish6({ event: latest.phase === "finished" ? "finished" : latest.you.left ? "left" : latest.paused ? "paused" : latest.turn?.yours ? "your_turn" : canAct2 ? "can_act" : "changed", position: latest });
12605
12630
  } catch (error) {
@@ -12665,7 +12690,7 @@ function nextStep(position, event, brief = false, server) {
12665
12690
  const play = (move2) => `leafarcade game play ${room} --revision ${revision} --action ${move2}`;
12666
12691
  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}`;
12667
12692
  if (position.turn?.yours) {
12668
- if (wire?.length === 1 && wire[0].type === "next" && Object.keys(wire[0]).length === 1) return `The hand is over; you deal the next one: ${play(`'{"type":"next"}'`)} --wait${flag}`;
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}`;
12669
12694
  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). " : "";
12670
12695
  const move2 = wire ? "'<one legal_wire_actions entry>'" : position.legal_actions_complete ? "'<one legal_actions entry>'" : "'<wire action JSON from game rules>'";
12671
12696
  const check = lineGames.includes(position.game) ? "Check your immediate win and your opponent's immediate threats (every line, including diagonals), then: " : "";
@@ -12761,7 +12786,7 @@ async function installSkill(agent = "codex", update = false) {
12761
12786
  // packages/cli/package.json
12762
12787
  var package_default = {
12763
12788
  name: "@leafarcade/cli",
12764
- version: "0.7.0",
12789
+ version: "0.7.1",
12765
12790
  private: false,
12766
12791
  description: "Create private browser board games and play with your AI agent. Includes 53 games and an installable Codex/Claude skill.",
12767
12792
  type: "module",
@@ -12822,7 +12847,7 @@ var commands = {
12822
12847
  status: { usage: "game status --room ROOMCODE", description: "Read your seat's visible state, phase, turn, revision, outcome and action completeness. Hidden information belonging to other players stays hidden. Includes a labeled board, last_move and a concrete next command.", options: ["room", "brief", "server"] },
12823
12848
  "legal-actions": { usage: "game legal-actions --room ROOMCODE", description: "Read actions and legal_actions_complete. An empty partial list is not proof that no move exists. Use game rules and visible_state for JSON actions.", options: ["room", "server"] },
12824
12849
  play: { usage: "game play --room ROOMCODE --revision R MOVE_FLAGS [--wait]", description: "Submit one move at the exact latest revision. Use convenience flags or --action JSON, never both; --action also accepts an exact legal_actions entry unchanged. --wait then waits for your next turn or the end, like game wait. The service validates legality. On stale or uncertain results, read status before acting again.", options: ["room", "revision", "action", "column", "row", "from", "to", "promotion", "swap", "edge", "pit", "wait", "timeout", "brief", "server"] },
12825
- wait: { usage: "game wait --room ROOMCODE [--after-revision R] [--timeout 90]", description: "Wait for changes through WebSocket (1\u20133600 seconds, default 90). Without --after-revision it waits for the next change after the current one. Returns at once when you can already act. Returns your_turn, can_act (a phase action outside your turn), changed, paused, finished, left or waiting. Use it after every move instead of asking the human to report theirs. A timeout is normal, not resignation. This command cannot restart a stopped agent.", options: ["room", "after-revision", "timeout", "brief", "server"] },
12850
+ wait: { usage: "game wait --room ROOMCODE [--after-revision R] [--timeout 90]", description: "Wait for changes through WebSocket (1\u20133600 seconds, default 90). Without --after-revision it waits for the next change after the current one. Returns at once when it is already your turn; optional phase actions outside your turn (such as a challenge window) return at once only without --after-revision, so you can wait through them. Returns your_turn, can_act (a phase action outside your turn), changed, paused, finished, left or waiting. Use it after every move instead of asking the human to report theirs. A timeout is normal, not resignation. This command cannot restart a stopped agent.", options: ["room", "after-revision", "timeout", "brief", "server"] },
12826
12851
  pause: { usage: "game pause --room ROOMCODE --revision R", description: "Save your explicit pause in an active, unclocked private match. Any paused player blocks moves. Room expiry is not extended.", options: ["room", "revision", "brief", "server"] },
12827
12852
  resume: { usage: "game resume --room ROOMCODE --revision R", description: "Clear your own pause. Each player who paused must resume before play continues.", options: ["room", "revision", "brief", "server"] },
12828
12853
  rematch: { usage: "game rematch --room ROOMCODE --revision R", description: "Request another round after a normal finish. All players must agree and remain at the table. After resignation or leaving, create a fresh room. Never request a rematch without the human asking.", options: ["room", "revision", "brief", "server"] },
@@ -12958,7 +12983,7 @@ Coordinates
12958
12983
  Actions
12959
12984
  ${rules.action_types.join(", ")}
12960
12985
  ${rules.action_help.game}
12961
- ${complete ? "Complete legal-action list: choose an exact legal_actions entry on your turn (pass it to --action unchanged, or use the flags)." : wireListed.includes(game) ? "Complete legal_wire_actions list in status, including phase actions outside your turn: pass one entry to --action unchanged. Card names are in cards_labeled." : "Partial legal-action list: inspect visible_state and game phase; use --action JSON. An empty list does not mean there are no moves."}
12986
+ ${complete ? "Complete legal-action list: choose an exact legal_actions entry on your turn (pass it to --action unchanged, or use the flags)." : wireListed.includes(game) ? `Complete legal_wire_actions list in status, including phase actions outside your turn: pass one entry to --action unchanged.${hasCardLabels(game) ? " Card names are in cards_labeled." : ""}` : "Partial legal-action list: inspect visible_state and game phase; use --action JSON. An empty list does not mean there are no moves."}
12962
12987
 
12963
12988
  Move syntax example (illustrative; read status and verify legality first)
12964
12989
  leafarcade game play --room ROOMCODE --revision R ${examples[game]} --wait
@@ -13042,6 +13067,11 @@ var show = (value) => JSON.stringify(value)?.slice(0, 120) ?? "undefined";
13042
13067
  function stateChanges(game, before, after, limit = 30) {
13043
13068
  const out2 = [];
13044
13069
  const labels2 = [cardsLabeled(game, before) ?? {}, cardsLabeled(game, after) ?? {}];
13070
+ const names = /* @__PURE__ */ new Map();
13071
+ for (const side2 of labels2) for (const value of Object.values(side2)) for (const label of [value].flat()) {
13072
+ const id = /\(id (\d+)\)$/.exec(label)?.[1];
13073
+ if (id !== void 0) names.set(Number(id), label);
13074
+ }
13045
13075
  const named = (path, value, side2) => {
13046
13076
  const label = labels2[side2][path];
13047
13077
  return label === void 0 ? show(value) : Array.isArray(label) ? label.join(", ") || "[]" : label;
@@ -13062,6 +13092,14 @@ function stateChanges(game, before, after, limit = 30) {
13062
13092
  a.forEach((v, i) => walk(v, b[i], [...path, String(i)]));
13063
13093
  return;
13064
13094
  }
13095
+ const cardName = (card2) => names.get(card2) ?? show(card2);
13096
+ if (labels2[0][here] !== void 0 || labels2[1][here] !== void 0) {
13097
+ const added = b.filter((card2) => !a.includes(card2)), removed = a.filter((card2) => !b.includes(card2));
13098
+ if (added.length + removed.length < Math.max(a.length, b.length)) {
13099
+ out2.push(`${here}: ${[...added.map((c) => `+${cardName(c)}`), ...removed.map((c) => `-${cardName(c)}`)].join(", ")}`);
13100
+ return;
13101
+ }
13102
+ }
13065
13103
  out2.push(`${here}: ${named(here, a, 0)} \u2192 ${named(here, b, 1)}`);
13066
13104
  return;
13067
13105
  }
@@ -13097,7 +13135,9 @@ async function withChanges(result, path) {
13097
13135
  if (!changes.length) return result;
13098
13136
  const boards = [before.state, position.visible_state].map((state) => state?.board);
13099
13137
  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 }]) : [];
13100
- const annotated = { ...position, ...position.last_move === null && "board_legend" in position && cells.length && cells.length <= 12 ? { last_move: { board_changes: cells } } : {}, changed_since_last_read: { from_revision: before.revision, changes } };
13138
+ const square2 = (at) => "abcdefgh"[at % 8] + (8 - Math.floor(at / 8));
13139
+ 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 } };
13101
13141
  return "position" in result ? { ...result, position: annotated } : annotated;
13102
13142
  }
13103
13143
 
@@ -13527,7 +13567,7 @@ function briefPosition(position) {
13527
13567
  ..."deadline_seconds_left" in position ? { deadline_seconds_left: position.deadline_seconds_left } : {},
13528
13568
  ..."board_legend" in position ? { board_legend: position.board_legend } : {},
13529
13569
  // Only the classic convenience games are fully described by their labeled board.
13530
- ...position.legal_actions_complete || !position.visible_state ? {} : { visible_state: position.visible_state },
13570
+ ...position.legal_actions_complete && position.board_labeled || !position.visible_state ? {} : { visible_state: position.visible_state },
13531
13571
  ...position.cards_labeled ? { cards_labeled: position.cards_labeled } : {},
13532
13572
  ...position.access_requests?.length ? { access_requests: position.access_requests } : {}
13533
13573
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leafarcade/cli",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "private": false,
5
5
  "description": "Create private browser board games and play with your AI agent. Includes 53 games and an installable Codex/Claude skill.",
6
6
  "type": "module",