@leafarcade/cli 0.8.0 → 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/README.md +1 -1
- package/dist/cli.mjs +77 -28
- package/package.json +1 -1
- package/skills/leafarcade/SKILL.md +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.
|
|
5
|
+
**Scope of 0.9.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.
|
|
6
6
|
|
|
7
7
|
## Start a game from your agent
|
|
8
8
|
|
package/dist/cli.mjs
CHANGED
|
@@ -3754,6 +3754,10 @@ function asianLastMove(history) {
|
|
|
3754
3754
|
const from = a.findIndex((cell, i) => cell.startsWith(mine) && !b[i].startsWith(mine));
|
|
3755
3755
|
return { from: from < 0 ? null : from, to };
|
|
3756
3756
|
}
|
|
3757
|
+
function drawReason(history) {
|
|
3758
|
+
const last = history.at(-1);
|
|
3759
|
+
return history.filter((key2) => key2 === last).length >= 3 ? "Draw \xB7 the same position occurred three times." : "Draw \xB7 too many moves without progress (no capture or man move).";
|
|
3760
|
+
}
|
|
3757
3761
|
|
|
3758
3762
|
// lib/random.ts
|
|
3759
3763
|
function randomStep(seed) {
|
|
@@ -4271,13 +4275,22 @@ function createGin(entropy2) {
|
|
|
4271
4275
|
}
|
|
4272
4276
|
function playGin(s, type, at, entropy2) {
|
|
4273
4277
|
if (type === "next" && s.phase === "round")
|
|
4274
|
-
return
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
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
|
+
};
|
|
4281
4294
|
if (["finished", "round"].includes(s.phase))
|
|
4282
4295
|
throw Error("This round is finished.");
|
|
4283
4296
|
const t = structuredClone(s), seat = s.turn;
|
|
@@ -4396,7 +4409,7 @@ var multiplayerGames = [
|
|
|
4396
4409
|
symbol: "1000",
|
|
4397
4410
|
players: "2\u20134 players",
|
|
4398
4411
|
description: "Bid your hand. Marry a new trump.",
|
|
4399
|
-
rules: "A 24-card deck, nine through ace; strength is nine, jack, queen, king, ten, ace. Three active players receive seven cards and a three-card kitty. With four seats the dealer sits out and scores revealed kitty aces (50 each) and marriages. Two players receive ten cards each and choose one of two two-card kitties. The first active player opens at 100; bid in tens, no higher than 120 plus held marriages. Winner takes a kitty, gives each other active player a card (or discards two with two seats), then declares at least the auction bid. Follow suit and beat the winner when possible; if void, trump when held. Leading a held queen-king pair with the marriage declaration earns clubs 60, diamonds 80, hearts 100 or spades 40 and changes trump. Card points: ace 11, ten 10, king 4, queen 3, jack 2.
|
|
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."
|
|
4400
4413
|
},
|
|
4401
4414
|
{
|
|
4402
4415
|
id: "binokel",
|
|
@@ -4516,7 +4529,7 @@ var multiplayerGames = [
|
|
|
4516
4529
|
symbol: "\u2654",
|
|
4517
4530
|
players: "2 players",
|
|
4518
4531
|
description: "Claim sixty-six. Close the stock with care.",
|
|
4519
|
-
rules: "Six cards each from a 24-card deck. Rank order: nine, jack, queen, king, ten, ace; values 0, 2, 3, 4, 10, 11. While stock is open, play freely; trick winner draws first. Exchange trump nine after winning a trick. Before leading, you may close the stock; then follow suit, overtake if possible, otherwise trump. A queen\u2013king marriage declared while leading with open stock earns 20, or 40 in trump, once you have won a trick (including the one it leads). Claim 66 before leading, or immediately with a marriage. False claims or failed closure award the opponent 2/3 match points. Otherwise round wins earn 1\u20133 based on opponent points. An unclosed last trick adds ten. First to seven match points wins."
|
|
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."
|
|
4520
4533
|
},
|
|
4521
4534
|
{
|
|
4522
4535
|
id: "spades",
|
|
@@ -4628,7 +4641,7 @@ var multiplayerGames = [
|
|
|
4628
4641
|
symbol: "\u25CC",
|
|
4629
4642
|
players: "2 players",
|
|
4630
4643
|
description: "Sow seeds. Capture twos and threes.",
|
|
4631
|
-
rules: "Each of twelve pits starts with four seeds. Sow counterclockwise, skipping the starting pit on a full lap. Ending on an opposing pit with two or three seeds captures it and preceding opposing pits with two or three. Captures that empty the entire opposing row are cancelled. Feed an empty opponent if possible. More than 24 captured seeds wins. No legal feeding move or three repetitions ends play; remaining seeds go to their owners."
|
|
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."
|
|
4632
4645
|
},
|
|
4633
4646
|
{
|
|
4634
4647
|
id: "mills",
|
|
@@ -8708,7 +8721,7 @@ var adapter5 = defineMatchAdapter({
|
|
|
8708
8721
|
return {
|
|
8709
8722
|
finished: state.winner !== null || state.draw,
|
|
8710
8723
|
winner: state.winner,
|
|
8711
|
-
label: state.draw ?
|
|
8724
|
+
label: state.draw ? drawReason(state.history) : state.winner !== null ? `Player ${state.winner + 1} wins \xB7 the opponent has no legal move.` : ""
|
|
8712
8725
|
};
|
|
8713
8726
|
},
|
|
8714
8727
|
apply(state, _seat, action, _entropy, _now) {
|
|
@@ -8726,7 +8739,7 @@ var adapter6 = defineMatchAdapter({
|
|
|
8726
8739
|
actions: ["move", "sequence"],
|
|
8727
8740
|
create: () => createInternational(),
|
|
8728
8741
|
turn: (s) => s.turn,
|
|
8729
|
-
outcome: (s) => ({ finished: s.winner !== null || s.draw, winner: s.winner, label: s.draw ?
|
|
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.` : "" }),
|
|
8730
8743
|
apply(s, _seat, a) {
|
|
8731
8744
|
if (a.type === "move" && typeof a.from === "number" && typeof a.to === "number") return moveInternational(s, a.from, a.to);
|
|
8732
8745
|
if (a.type === "sequence" && Array.isArray(a.cards) && a.cards.length >= 2 && a.cards.length <= 21) {
|
|
@@ -9061,6 +9074,7 @@ function playThousand(s, a, entropy2) {
|
|
|
9061
9074
|
if (a.type === "next" && s.phase === "round") {
|
|
9062
9075
|
t.round++;
|
|
9063
9076
|
t.dealer = (s.dealer + 1) % s.hands.length;
|
|
9077
|
+
t.previous = s.result;
|
|
9064
9078
|
return deal6(t, entropy2);
|
|
9065
9079
|
}
|
|
9066
9080
|
if (a.type === "redeal" && s.phase === "auction" && s.hands[seat].filter((c) => cardRank(c) === 9).length === 4)
|
|
@@ -9107,9 +9121,9 @@ function playThousand(s, a, entropy2) {
|
|
|
9107
9121
|
return t;
|
|
9108
9122
|
}
|
|
9109
9123
|
if (a.type === "bomb" && ["kitty", "exchange", "declare"].includes(s.phase)) {
|
|
9110
|
-
|
|
9111
|
-
|
|
9112
|
-
t.result =
|
|
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)"}.`;
|
|
9113
9127
|
finish4(t);
|
|
9114
9128
|
return t;
|
|
9115
9129
|
}
|
|
@@ -9154,10 +9168,12 @@ function playThousand(s, a, entropy2) {
|
|
|
9154
9168
|
(n, c) => n + pointValue(cardRank(c)),
|
|
9155
9169
|
0
|
|
9156
9170
|
);
|
|
9171
|
+
const before = t.scores;
|
|
9157
9172
|
t.scores = t.scores.map(
|
|
9158
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
|
|
9159
9174
|
);
|
|
9160
|
-
t.
|
|
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(", ")}.`;
|
|
9161
9177
|
finish4(t);
|
|
9162
9178
|
}
|
|
9163
9179
|
return t;
|
|
@@ -10413,6 +10429,7 @@ function playMakao(s, a, entropy2) {
|
|
|
10413
10429
|
const t = structuredClone(s), seat = s.turn;
|
|
10414
10430
|
if (a.type === "catch" && s.unannounced !== null && s.unannounced !== seat) {
|
|
10415
10431
|
draw(t, s.unannounced, 5, entropy2);
|
|
10432
|
+
t.last = { seat: s.unannounced, type: "caught", count: 5 };
|
|
10416
10433
|
t.unannounced = null;
|
|
10417
10434
|
t.result = "Missed Makao: draw five.";
|
|
10418
10435
|
return t;
|
|
@@ -10421,12 +10438,13 @@ function playMakao(s, a, entropy2) {
|
|
|
10421
10438
|
t.unannounced = null;
|
|
10422
10439
|
if (s.wait) {
|
|
10423
10440
|
t.blocked[seat] = s.wait - 1;
|
|
10441
|
+
t.last = { seat, type: "wait", count: s.wait };
|
|
10424
10442
|
t.wait = 0;
|
|
10425
10443
|
advance3(t);
|
|
10426
10444
|
return t;
|
|
10427
10445
|
}
|
|
10428
10446
|
if (s.penalty) {
|
|
10429
|
-
draw(t, seat, s.penalty, entropy2);
|
|
10447
|
+
t.last = { seat, type: "penalty", count: draw(t, seat, s.penalty, entropy2).length };
|
|
10430
10448
|
t.penalty = 0;
|
|
10431
10449
|
t.suit = null;
|
|
10432
10450
|
advance3(t);
|
|
@@ -10434,11 +10452,13 @@ function playMakao(s, a, entropy2) {
|
|
|
10434
10452
|
}
|
|
10435
10453
|
const received = draw(t, seat, 1, entropy2);
|
|
10436
10454
|
t.drawn = received[0] ?? null;
|
|
10455
|
+
t.last = { seat, type: "draw", count: received.length };
|
|
10437
10456
|
t.phase = "drawn";
|
|
10438
10457
|
if (t.drawn === null) advance3(t);
|
|
10439
10458
|
return t;
|
|
10440
10459
|
}
|
|
10441
10460
|
if (a.type === "pass" && s.phase === "drawn") {
|
|
10461
|
+
t.last = { seat, type: "keep", count: 1 };
|
|
10442
10462
|
t.unannounced = null;
|
|
10443
10463
|
t.suit = null;
|
|
10444
10464
|
advance3(t);
|
|
@@ -10453,6 +10473,7 @@ function playMakao(s, a, entropy2) {
|
|
|
10453
10473
|
throw Error("Request an ordinary rank, or choose no demand.");
|
|
10454
10474
|
t.unannounced = null;
|
|
10455
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 } : {} };
|
|
10456
10477
|
t.pile.push(...cards);
|
|
10457
10478
|
t.suit = null;
|
|
10458
10479
|
if (rank2 === 14) t.suit = a.amount;
|
|
@@ -11406,7 +11427,7 @@ function panLegal(s, cards) {
|
|
|
11406
11427
|
}
|
|
11407
11428
|
function playPan(s, type, cards, entropy2) {
|
|
11408
11429
|
if (type === "next" && s.phase === "round")
|
|
11409
|
-
return deal12(s.hands.length, entropy2, s.letters, s.round + 1);
|
|
11430
|
+
return { ...deal12(s.hands.length, entropy2, s.letters, s.round + 1), previous: s.result };
|
|
11410
11431
|
if (s.phase !== "play") throw Error("This round is finished.");
|
|
11411
11432
|
const t = structuredClone(s), seat = s.turn;
|
|
11412
11433
|
if (type === "play" && panLegal(s, cards)) {
|
|
@@ -11560,7 +11581,7 @@ function award2(t, winner2, failure = false) {
|
|
|
11560
11581
|
}
|
|
11561
11582
|
function playSixtySix(s, type, at, marriage = false, claimMarriage = false, entropy2) {
|
|
11562
11583
|
if (type === "next" && s.phase === "round")
|
|
11563
|
-
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 };
|
|
11564
11585
|
if (s.phase !== "play") throw Error("This round is finished.");
|
|
11565
11586
|
const t = structuredClone(s), seat = s.turn;
|
|
11566
11587
|
if (type === "close" && !s.trick.length && s.closed === null && s.upcard !== null) {
|
|
@@ -11605,6 +11626,7 @@ function playSixtySix(s, type, at, marriage = false, claimMarriage = false, entr
|
|
|
11605
11626
|
t.tricks[winner2]++;
|
|
11606
11627
|
t.turn = winner2;
|
|
11607
11628
|
t.lastTrick = t.trick;
|
|
11629
|
+
t.played = [...s.played ?? [], ...t.trick.map((c) => c.card)];
|
|
11608
11630
|
t.trick = [];
|
|
11609
11631
|
if (t.closed === null && t.upcard !== null)
|
|
11610
11632
|
for (const i of [winner2, 1 - winner2]) {
|
|
@@ -12425,11 +12447,16 @@ function agentPosition(room) {
|
|
|
12425
12447
|
knock_limit: 10
|
|
12426
12448
|
};
|
|
12427
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;
|
|
12428
12455
|
const asianMove = match?.kind === "shogi" || match?.kind === "xiangqi" ? asianLastMove(match.state.history) : null;
|
|
12429
12456
|
const asianLast = asianMove && (match?.kind === "shogi" || match?.kind === "xiangqi") ? { ...asianMove, by: match.state.history.at(-1).mover } : null;
|
|
12430
12457
|
const millLast = match?.kind === "mills" ? millLastTurn(match.state) : null;
|
|
12431
|
-
const
|
|
12432
|
-
const cribbageLast =
|
|
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;
|
|
12433
12460
|
return {
|
|
12434
12461
|
protocol_version: 1,
|
|
12435
12462
|
room: room.code,
|
|
@@ -12447,13 +12474,15 @@ function agentPosition(room) {
|
|
|
12447
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,
|
|
12448
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." } : {},
|
|
12449
12476
|
// Chess and its variants record the last move in SAN.
|
|
12450
|
-
last_move: (match?.kind === "chess" || match?.kind === "atomic-chess" || match?.kind === "three-check") && match.state.last ? { san: match.state.last } : checkerLast ? { from: square(checkerLast.from), to: square(checkerLast.to) } : cribbageLast ? cribbageLast : asianLast ? { by_seat: asianLast.by, ...asianLast.from === null ? { type: "drop" } : { from: asianLast.from }, to: asianLast.to } : match?.kind === "oware" && match.state.last !== null ? { last_seed_in_pit: match.state.last + 1 } : millLast ? { by_seat: millLast.seat, ...millLast.from === null ? { type: "place" } : { type: "move", from: millLast.from }, at: millLast.to, ...millLast.captured.length ? { captured: millLast.captured } : {} } : last === null ? null : room.game === "connect-four" ? { column: last % columns + 1, row_from_top: Math.floor(last / columns) + 1 } : { row: Math.floor(last / columns) + 1, column: last % columns + 1 },
|
|
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 },
|
|
12451
12478
|
...room.game === "chess" ? { fen } : {},
|
|
12452
12479
|
// The side to move is in check (chess family, xiangqi, shogi).
|
|
12453
12480
|
...match && room.phase === "playing" && (match.kind === "chess" ? chessPosition(match.state).inCheck() : "check" in match.state && match.state.check === true) ? { in_check: true } : {},
|
|
12454
12481
|
visible_state: match?.state ?? null,
|
|
12455
12482
|
cards_labeled: match ? cardsLabeled(room.game, match.state) : null,
|
|
12456
12483
|
...ginHint ? { gin_deadwood: ginHint } : {},
|
|
12484
|
+
...sixtyUnseen ? { sixty_six_unseen: sixtyUnseen } : {},
|
|
12485
|
+
...ruleNotes.length ? { rule_notes: ruleNotes } : {},
|
|
12457
12486
|
// Timed rounds (drawing, categories): agents cannot measure time, so give the remaining seconds.
|
|
12458
12487
|
...typeof match?.state?.deadline === "number" && room.serverNow ? { deadline_seconds_left: Math.max(0, Math.ceil((match.state.deadline - room.serverNow) / 1e3)) } : {},
|
|
12459
12488
|
action_types: action_catalog_default[room.game],
|
|
@@ -12795,7 +12824,7 @@ function nextStep(position, event, brief = false, server) {
|
|
|
12795
12824
|
const play = (move2) => `leafarcade game play ${room} --revision ${revision} --action ${move2}`;
|
|
12796
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}`;
|
|
12797
12826
|
if (position.turn?.yours) {
|
|
12798
|
-
if (wire?.length === 1 && wire[0].type === "next" && Object.keys(wire[0]).length === 1) return `The hand is over
|
|
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}`;
|
|
12799
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). " : "";
|
|
12800
12829
|
const move2 = wire ? "'<one legal_wire_actions entry>'" : position.legal_actions_complete ? "'<one legal_actions entry>'" : "'<wire action JSON from game rules>'";
|
|
12801
12830
|
const check = lineGames.includes(position.game) ? "Check your immediate win and your opponent's immediate threats (every line, including diagonals), then: " : "";
|
|
@@ -12803,12 +12832,19 @@ function nextStep(position, event, brief = false, server) {
|
|
|
12803
12832
|
state?.penalty ? `A ${state.penalty}-card penalty is active: answer with a matching attack card, or draw takes all ${state.penalty}. ` : "",
|
|
12804
12833
|
state?.wait ? `A ${state.wait}-turn wait is active: answer with a 4, or draw serves the wait (no cards drawn). ` : "",
|
|
12805
12834
|
state?.blocked?.[position.you.seat] ? `You are blocked for ${state.blocked[position.you.seat]} more turn(s). ` : "",
|
|
12806
|
-
state?.demand !== null && state?.demand !== void 0 ? `Jack demand in force: ${state.demand === 13 ? "king" : `rank ${state.demand}`}. ` : "",
|
|
12807
|
-
state?.suit !== null && state?.suit !== void 0 ? `Ace suit in force: ${["clubs", "diamonds", "hearts", "spades"][state.suit]}. ` : ""
|
|
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. ' : ""
|
|
12808
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). ` : "";
|
|
12809
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). " : "";
|
|
12810
|
-
return `Your move. ${mill}${makao}${partial}${check}${play(move2)} --wait${flag}`;
|
|
12844
|
+
return `Your move. ${capture}${mill}${makao}${partial}${check}${play(move2)} --wait${flag}`;
|
|
12811
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}`;
|
|
12812
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.`;
|
|
12813
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.";
|
|
12814
12850
|
const mover = position.turn ? ` (seat ${position.turn.seat}, ${position.players[position.turn.seat]?.name ?? "empty"}, to act)` : "";
|
|
@@ -12899,7 +12935,7 @@ async function installSkill(agent = "codex", update = false) {
|
|
|
12899
12935
|
// packages/cli/package.json
|
|
12900
12936
|
var package_default = {
|
|
12901
12937
|
name: "@leafarcade/cli",
|
|
12902
|
-
version: "0.
|
|
12938
|
+
version: "0.9.0",
|
|
12903
12939
|
private: false,
|
|
12904
12940
|
description: "Create private browser board games and play with your AI agent. Includes 53 games and an installable Codex/Claude skill.",
|
|
12905
12941
|
type: "module",
|
|
@@ -13195,6 +13231,15 @@ function stateChanges(game, before, after, limit = 30) {
|
|
|
13195
13231
|
const here = path.join(".");
|
|
13196
13232
|
const primitive = (v) => v === null || typeof v !== "object";
|
|
13197
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
|
+
}
|
|
13198
13243
|
if (a.length === b.length && a.length > 4 && a.every(primitive) && b.every(primitive)) {
|
|
13199
13244
|
a.forEach((v, i) => {
|
|
13200
13245
|
if (v !== b[i] && out2.length <= limit) out2.push(`${here}[${i}]: ${show(v)} \u2192 ${show(b[i])}`);
|
|
@@ -13666,6 +13711,7 @@ var scores = (position) => {
|
|
|
13666
13711
|
const value = position.visible_state?.scores;
|
|
13667
13712
|
return Array.isArray(value) && value.length && value.every((n) => typeof n === "number") ? value : null;
|
|
13668
13713
|
};
|
|
13714
|
+
var withoutLegal = (state) => Object.fromEntries(Object.entries(state).filter(([key2]) => key2 !== "legalActions"));
|
|
13669
13715
|
function briefPosition(position) {
|
|
13670
13716
|
const turn = position.turn ? position.turn.yours ? "yours" : seatName(position, position.turn.seat) : null;
|
|
13671
13717
|
const wire = "legal_wire_actions" in position ? position.legal_wire_actions ?? [] : null;
|
|
@@ -13693,9 +13739,12 @@ function briefPosition(position) {
|
|
|
13693
13739
|
..."deadline_seconds_left" in position ? { deadline_seconds_left: position.deadline_seconds_left } : {},
|
|
13694
13740
|
..."board_legend" in position ? { board_legend: position.board_legend } : {},
|
|
13695
13741
|
// Only the classic convenience games are fully described by their labeled board.
|
|
13696
|
-
|
|
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) },
|
|
13697
13744
|
...position.cards_labeled ? { cards_labeled: position.cards_labeled } : {},
|
|
13698
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 } : {},
|
|
13699
13748
|
...position.access_requests?.length ? { access_requests: position.access_requests } : {}
|
|
13700
13749
|
};
|
|
13701
13750
|
}
|
package/package.json
CHANGED
|
@@ -35,7 +35,7 @@ When the human already has a room code, use `leafarcade join ROOMCODE --name "My
|
|
|
35
35
|
## The play loop
|
|
36
36
|
|
|
37
37
|
- **Wait on the table yourself.** After your move, use `play --wait` or `game wait`. Never ask the human to tell you they moved.
|
|
38
|
-
- Read `game rules --game GAME_ID` once. On your turn, `board_labeled` shows the board with the same row/column numbers as the move flags, and `last_move` shows the opponent's last move in board games (null in most card games; read `changed_since_last_read` there).
|
|
38
|
+
- Read `game rules --game GAME_ID` once. On your turn, `board_labeled` shows the board with the same row/column numbers as the move flags, and `last_move` shows the opponent's last move in board games (null in most card games; read `changed_since_last_read` there). Some games add hints: `rule_notes` (rules in force now, such as Thousand's 800 rule), `gin_deadwood` and `sixty_six_unseen` (cards you have not seen this deal).
|
|
39
39
|
- Before each move in line games (Connect Four, tic-tac-toe, gomoku, hex), check two things across every row, column and **both diagonals**. First, can you win now? Second, does the opponent threaten to win next turn?
|
|
40
40
|
- Most games list every legal move. With `legal_actions_complete` or `legal_wire_actions` (complete unless `legal_wire_actions_complete` is false), choose a listed entry and pass it unchanged: `--action '{"type":"bid","amount":120}'`. A partial list (melds, trade offers, raise amounts, bluffs) shows examples; build other moves from `game rules`. Wire indexes are zero-based; the convenience flags are one-based.
|
|
41
41
|
- Card games add `cards_labeled` (for example `"hands.1": ["Q♠ (id 49)", …]`): read card names there, not from raw IDs. Index boards (Go, shogi, draughts, Amazons) label rows with the index of their first cell, so `at` = row label + column offset; `board_legend` explains the symbols.
|