@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.
- package/README.md +1 -1
- package/dist/cli.mjs +147 -107
- 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.
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 ? "
|
|
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 ? "
|
|
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:
|
|
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
|
|
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 === "
|
|
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
|
|
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;
|
|
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.
|
|
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
|
|
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) ?
|
|
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
|
|
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