@leafarcade/cli 0.9.0 → 0.9.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 +416 -290
- package/package.json +1 -1
- package/skills/leafarcade/SKILL.md +2 -2
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.9.
|
|
5
|
+
**Scope of 0.9.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
|
@@ -3758,6 +3758,17 @@ function drawReason(history) {
|
|
|
3758
3758
|
const last = history.at(-1);
|
|
3759
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
3760
|
}
|
|
3761
|
+
function snapshotTurn(before, after, owner4, movers) {
|
|
3762
|
+
if (!before || !after || before.length !== after.length) return null;
|
|
3763
|
+
for (const seat of movers) {
|
|
3764
|
+
const to = after.findIndex((p, i) => owner4(p) === seat && owner4(before[i]) !== seat);
|
|
3765
|
+
if (to < 0) continue;
|
|
3766
|
+
const from = before.findIndex((p, i) => owner4(p) === seat && owner4(after[i]) !== seat);
|
|
3767
|
+
const captured = before.flatMap((p, i) => owner4(p) === 1 - seat && owner4(after[i]) !== 1 - seat ? [i] : []);
|
|
3768
|
+
return { by_seat: seat, ...from >= 0 ? { from } : {}, to, ...captured.length ? { captured } : {} };
|
|
3769
|
+
}
|
|
3770
|
+
return null;
|
|
3771
|
+
}
|
|
3761
3772
|
|
|
3762
3773
|
// lib/random.ts
|
|
3763
3774
|
function randomStep(seed) {
|
|
@@ -3796,6 +3807,261 @@ function best(scores2) {
|
|
|
3796
3807
|
return winners.length === 1 ? winners[0] : null;
|
|
3797
3808
|
}
|
|
3798
3809
|
|
|
3810
|
+
// lib/multiplayer/dice.ts
|
|
3811
|
+
var diceCategories = [
|
|
3812
|
+
"Ones",
|
|
3813
|
+
"Twos",
|
|
3814
|
+
"Threes",
|
|
3815
|
+
"Fours",
|
|
3816
|
+
"Fives",
|
|
3817
|
+
"Sixes",
|
|
3818
|
+
"Three of a kind",
|
|
3819
|
+
"Four of a kind",
|
|
3820
|
+
"Full house",
|
|
3821
|
+
"Small straight",
|
|
3822
|
+
"Large straight",
|
|
3823
|
+
"Five of a kind",
|
|
3824
|
+
"Chance"
|
|
3825
|
+
];
|
|
3826
|
+
function createDice(count) {
|
|
3827
|
+
if (!index(count - 2, 3)) throw Error("Choose two to four players.");
|
|
3828
|
+
return {
|
|
3829
|
+
dice: [0, 0, 0, 0, 0],
|
|
3830
|
+
held: Array(5).fill(false),
|
|
3831
|
+
rolls: 0,
|
|
3832
|
+
sheets: Array.from({ length: count }, () => Array(13).fill(null)),
|
|
3833
|
+
bonuses: Array(count).fill(0),
|
|
3834
|
+
turn: 0,
|
|
3835
|
+
finished: false,
|
|
3836
|
+
winner: null
|
|
3837
|
+
};
|
|
3838
|
+
}
|
|
3839
|
+
function diceScore(dice, category, joker = false) {
|
|
3840
|
+
const counts = Array.from(
|
|
3841
|
+
{ length: 6 },
|
|
3842
|
+
(_, i) => dice.filter((v) => v === i + 1).length
|
|
3843
|
+
), sum = dice.reduce((a, b) => a + b, 0);
|
|
3844
|
+
if (category < 6) return counts[category] * (category + 1);
|
|
3845
|
+
switch (category) {
|
|
3846
|
+
case 6:
|
|
3847
|
+
return Math.max(...counts) >= 3 ? sum : 0;
|
|
3848
|
+
case 7:
|
|
3849
|
+
return Math.max(...counts) >= 4 ? sum : 0;
|
|
3850
|
+
case 8:
|
|
3851
|
+
return joker || counts.includes(3) && counts.includes(2) ? 25 : 0;
|
|
3852
|
+
case 9:
|
|
3853
|
+
return joker || [0, 1, 2].some((start) => counts.slice(start, start + 4).every(Boolean)) ? 30 : 0;
|
|
3854
|
+
case 10:
|
|
3855
|
+
return joker || [0, 1].some((start) => counts.slice(start, start + 5).every(Boolean)) ? 40 : 0;
|
|
3856
|
+
case 11:
|
|
3857
|
+
return counts.includes(5) ? 50 : 0;
|
|
3858
|
+
case 12:
|
|
3859
|
+
return sum;
|
|
3860
|
+
default:
|
|
3861
|
+
throw Error("Choose a score category.");
|
|
3862
|
+
}
|
|
3863
|
+
}
|
|
3864
|
+
function diceTotal(s, seat) {
|
|
3865
|
+
const sheet = s.sheets[seat], upper = sheet.slice(0, 6).reduce((sum, n) => sum + (n ?? 0), 0);
|
|
3866
|
+
return sheet.reduce((sum, n) => sum + (n ?? 0), 0) + (upper >= 63 ? 35 : 0) + s.bonuses[seat];
|
|
3867
|
+
}
|
|
3868
|
+
function playDice(s, type, at, entropy2) {
|
|
3869
|
+
if (s.finished) throw Error("This match is finished.");
|
|
3870
|
+
if (type === "roll") {
|
|
3871
|
+
if (s.rolls >= 3) throw Error("Record a score after three rolls.");
|
|
3872
|
+
if (s.rolls && s.held.every(Boolean)) throw Error("Release a die before rolling again, or record a score.");
|
|
3873
|
+
const random = draws(entropy2, 5);
|
|
3874
|
+
return {
|
|
3875
|
+
...s,
|
|
3876
|
+
rolls: s.rolls + 1,
|
|
3877
|
+
dice: s.dice.map(
|
|
3878
|
+
(v, i) => s.rolls && s.held[i] ? v : 1 + Math.floor(random[i] * 6)
|
|
3879
|
+
)
|
|
3880
|
+
};
|
|
3881
|
+
}
|
|
3882
|
+
if (at === void 0 || !index(at, type === "hold" ? 5 : 13) || !s.rolls)
|
|
3883
|
+
throw Error("Roll first, then choose a die or score category.");
|
|
3884
|
+
if (type === "hold") {
|
|
3885
|
+
if (s.rolls >= 3) throw Error("No rolls are left: record a score.");
|
|
3886
|
+
return { ...s, held: s.held.map((v, i) => i === at ? !v : v) };
|
|
3887
|
+
}
|
|
3888
|
+
if (type !== "score" || s.sheets[s.turn][at] !== null)
|
|
3889
|
+
throw Error("Choose an unused score category.");
|
|
3890
|
+
const sheets = s.sheets.map((row) => [...row]), bonuses = [...s.bonuses];
|
|
3891
|
+
const five = s.dice.every((v) => v === s.dice[0]), upper = s.dice[0] - 1;
|
|
3892
|
+
const joker = five && sheets[s.turn][11] !== null && sheets[s.turn][upper] !== null;
|
|
3893
|
+
if (joker && sheets[s.turn].slice(6).some((n) => n === null) && at < 6)
|
|
3894
|
+
throw Error(
|
|
3895
|
+
"Use the five-of-a-kind joker in an unused lower category first."
|
|
3896
|
+
);
|
|
3897
|
+
sheets[s.turn][at] = diceScore(s.dice, at, joker);
|
|
3898
|
+
if (joker && sheets[s.turn][11] === 50) bonuses[s.turn] += 100;
|
|
3899
|
+
const next3 = {
|
|
3900
|
+
...s,
|
|
3901
|
+
sheets,
|
|
3902
|
+
bonuses,
|
|
3903
|
+
turn: (s.turn + 1) % s.sheets.length,
|
|
3904
|
+
rolls: 0,
|
|
3905
|
+
held: Array(5).fill(false),
|
|
3906
|
+
dice: [0, 0, 0, 0, 0]
|
|
3907
|
+
};
|
|
3908
|
+
next3.finished = sheets.every((row) => row.every((n) => n !== null));
|
|
3909
|
+
if (next3.finished)
|
|
3910
|
+
next3.winner = best(sheets.map((_, i) => diceTotal(next3, i)));
|
|
3911
|
+
return next3;
|
|
3912
|
+
}
|
|
3913
|
+
|
|
3914
|
+
// lib/multiplayer/ludo.ts
|
|
3915
|
+
function createLudo(count) {
|
|
3916
|
+
if (!index(count - 2, 3)) throw Error("Choose two to four players.");
|
|
3917
|
+
return {
|
|
3918
|
+
pieces: Array.from({ length: count }, () => [-1, -1, -1, -1]),
|
|
3919
|
+
turn: 0,
|
|
3920
|
+
die: 0,
|
|
3921
|
+
streak: 0,
|
|
3922
|
+
attempts: 0,
|
|
3923
|
+
winner: null,
|
|
3924
|
+
last: "Roll a six to leave your yard."
|
|
3925
|
+
};
|
|
3926
|
+
}
|
|
3927
|
+
var ludoTrack = (seat, progress, count = 4) => (seat * (count === 2 ? 20 : 10) + progress) % 40;
|
|
3928
|
+
function ludoMoves(s) {
|
|
3929
|
+
if (!s.die || s.winner !== null) return [];
|
|
3930
|
+
return s.pieces[s.turn].flatMap((p, i) => {
|
|
3931
|
+
const to = p < 0 ? s.die === 6 ? 0 : -1 : p + s.die;
|
|
3932
|
+
return to >= 0 && to <= 43 && !s.pieces[s.turn].some((other, j) => j !== i && other === to) ? [i] : [];
|
|
3933
|
+
});
|
|
3934
|
+
}
|
|
3935
|
+
function advance(s) {
|
|
3936
|
+
return {
|
|
3937
|
+
...s,
|
|
3938
|
+
turn: (s.turn + 1) % s.pieces.length,
|
|
3939
|
+
die: 0,
|
|
3940
|
+
streak: 0,
|
|
3941
|
+
attempts: 0
|
|
3942
|
+
};
|
|
3943
|
+
}
|
|
3944
|
+
function playLudo(s, type, at, entropy2) {
|
|
3945
|
+
if (s.winner !== null) throw Error("This match is finished.");
|
|
3946
|
+
if (type === "roll") {
|
|
3947
|
+
if (s.die) throw Error("Move a piece before rolling again.");
|
|
3948
|
+
const die = 1 + Math.floor(draws(entropy2, 1)[0] * 6), streak = die === 6 ? s.streak + 1 : 0;
|
|
3949
|
+
let next4 = {
|
|
3950
|
+
...s,
|
|
3951
|
+
die,
|
|
3952
|
+
streak,
|
|
3953
|
+
attempts: s.attempts + 1,
|
|
3954
|
+
last: `Player ${s.turn + 1} rolled ${die}.`
|
|
3955
|
+
};
|
|
3956
|
+
if (streak > 3)
|
|
3957
|
+
return advance({
|
|
3958
|
+
...next4,
|
|
3959
|
+
last: "Three extra moves is the limit. Turn passes."
|
|
3960
|
+
});
|
|
3961
|
+
if (!ludoMoves(next4).length) {
|
|
3962
|
+
const allHome = s.pieces[s.turn].every((p) => p === -1);
|
|
3963
|
+
next4 = allHome && next4.attempts < 3 || die === 6 ? { ...next4, die: 0, last: `Player ${s.turn + 1} rolled ${die}: no piece can move${die === 6 ? "; roll again" : allHome ? `; ${3 - next4.attempts} more tr${3 - next4.attempts === 1 ? "y" : "ies"} for a 6` : ""}.` } : advance({ ...next4, last: `Player ${s.turn + 1} rolled ${die}: no piece can move. Turn passes.` });
|
|
3964
|
+
}
|
|
3965
|
+
return next4;
|
|
3966
|
+
}
|
|
3967
|
+
if (type !== "move" || at === void 0 || !index(at, 4) || !ludoMoves(s).includes(at))
|
|
3968
|
+
throw Error(
|
|
3969
|
+
"Choose a highlighted piece. You need an exact roll to reach home."
|
|
3970
|
+
);
|
|
3971
|
+
const pieces = s.pieces.map((row) => [...row]), old = pieces[s.turn][at], to = old < 0 ? 0 : old + s.die;
|
|
3972
|
+
pieces[s.turn][at] = to;
|
|
3973
|
+
const captured = [];
|
|
3974
|
+
if (to < 40) {
|
|
3975
|
+
for (let seat = 0; seat < pieces.length; seat++)
|
|
3976
|
+
if (seat !== s.turn) {
|
|
3977
|
+
pieces[seat] = pieces[seat].map((p, piece2) => {
|
|
3978
|
+
const hit = p >= 0 && p < 40 && ludoTrack(seat, p, pieces.length) === ludoTrack(s.turn, to, pieces.length);
|
|
3979
|
+
if (hit) captured.push(`Player ${seat + 1}'s piece ${piece2 + 1}`);
|
|
3980
|
+
return hit ? -1 : p;
|
|
3981
|
+
});
|
|
3982
|
+
}
|
|
3983
|
+
}
|
|
3984
|
+
const won = pieces[s.turn].every((p) => p >= 40);
|
|
3985
|
+
const next3 = {
|
|
3986
|
+
...s,
|
|
3987
|
+
pieces,
|
|
3988
|
+
die: 0,
|
|
3989
|
+
attempts: 0,
|
|
3990
|
+
winner: won ? s.turn : null,
|
|
3991
|
+
last: `Player ${s.turn + 1} moved piece ${at + 1} ${old < 0 ? "out of the yard to 0" : `from ${old} to ${to}`} with a ${s.die}${to >= 40 ? " (home lane)" : ""}${captured.length ? `, sending ${captured.join(" and ")} back to the yard` : ""}${!won && s.die === 6 && s.streak < 3 ? "; the 6 gives another roll" : ""}.`
|
|
3992
|
+
};
|
|
3993
|
+
return s.die === 6 && s.streak < 3 || won ? next3 : advance(next3);
|
|
3994
|
+
}
|
|
3995
|
+
|
|
3996
|
+
// lib/multiplayer/football.ts
|
|
3997
|
+
var footballPoint = (at) => [at % 9, Math.floor(at / 9)];
|
|
3998
|
+
var footballEdge = (a, b) => a < b ? `${a}:${b}` : `${b}:${a}`;
|
|
3999
|
+
var valid = (at) => index(at, 117) && (Math.floor(at / 9) > 0 && Math.floor(at / 9) < 12 || at % 9 >= 3 && at % 9 <= 5);
|
|
4000
|
+
var boundary = (at) => {
|
|
4001
|
+
const [x, y] = footballPoint(at);
|
|
4002
|
+
return x === 0 || x === 8 || y === 0 || y === 12 || (y === 1 || y === 11) && x !== 4;
|
|
4003
|
+
};
|
|
4004
|
+
function createFootball() {
|
|
4005
|
+
const edges = [];
|
|
4006
|
+
for (let y = 1; y <= 11; y++)
|
|
4007
|
+
for (const x of [0, 8])
|
|
4008
|
+
if (y < 11) edges.push(footballEdge(y * 9 + x, (y + 1) * 9 + x));
|
|
4009
|
+
for (const y of [1, 11])
|
|
4010
|
+
for (let x = 0; x < 8; x++)
|
|
4011
|
+
if (x < 3 || x >= 5) edges.push(footballEdge(y * 9 + x, y * 9 + x + 1));
|
|
4012
|
+
for (const y of [0, 12]) {
|
|
4013
|
+
edges.push(
|
|
4014
|
+
footballEdge(y * 9 + 3, y * 9 + 4),
|
|
4015
|
+
footballEdge(y * 9 + 4, y * 9 + 5)
|
|
4016
|
+
);
|
|
4017
|
+
for (const x of [3, 5])
|
|
4018
|
+
edges.push(footballEdge(y * 9 + x, (y === 0 ? 1 : 11) * 9 + x));
|
|
4019
|
+
}
|
|
4020
|
+
return { ball: 58, edges, turn: 0, winner: null, last: null };
|
|
4021
|
+
}
|
|
4022
|
+
function footballMoves(s) {
|
|
4023
|
+
if (s.winner !== null) return [];
|
|
4024
|
+
const [x, y] = footballPoint(s.ball), moves = [];
|
|
4025
|
+
for (let dy = -1; dy <= 1; dy++)
|
|
4026
|
+
for (let dx = -1; dx <= 1; dx++) {
|
|
4027
|
+
const nx = x + dx, ny = y + dy, at = ny * 9 + nx;
|
|
4028
|
+
if ((ny === 0 || ny === 12) && (x < 3 || x > 5)) continue;
|
|
4029
|
+
if ((dx || dy) && nx >= 0 && nx <= 8 && ny >= 0 && ny <= 12 && valid(at) && !s.edges.includes(footballEdge(s.ball, at)))
|
|
4030
|
+
moves.push(at);
|
|
4031
|
+
}
|
|
4032
|
+
return moves;
|
|
4033
|
+
}
|
|
4034
|
+
function playFootball(s, at) {
|
|
4035
|
+
if (!footballMoves(s).includes(at))
|
|
4036
|
+
throw Error("Choose an adjacent point along an unused line.");
|
|
4037
|
+
const bounced = boundary(at) || s.edges.some((edge) => edge.split(":").map(Number).includes(at));
|
|
4038
|
+
const y = Math.floor(at / 9);
|
|
4039
|
+
const next3 = {
|
|
4040
|
+
...s,
|
|
4041
|
+
ball: at,
|
|
4042
|
+
edges: [...s.edges, footballEdge(s.ball, at)],
|
|
4043
|
+
turn: bounced ? s.turn : s.turn === 0 ? 1 : 0,
|
|
4044
|
+
winner: y === 0 ? 0 : y === 12 ? 1 : null,
|
|
4045
|
+
last: [s.ball, at]
|
|
4046
|
+
};
|
|
4047
|
+
if (next3.winner === null && !footballMoves(next3).length)
|
|
4048
|
+
next3.winner = 1 - next3.turn;
|
|
4049
|
+
return next3;
|
|
4050
|
+
}
|
|
4051
|
+
var compass = { "0,-1": "up", "1,-1": "up-right", "1,0": "right", "1,1": "down-right", "0,1": "down", "-1,1": "down-left", "-1,0": "left", "-1,-1": "up-left" };
|
|
4052
|
+
function footballLabeled(s) {
|
|
4053
|
+
const [x, y] = footballPoint(s.ball);
|
|
4054
|
+
return [
|
|
4055
|
+
`ball: point ${s.ball} (row ${y}, column ${x}); points are row*9+column on a 9-column, 13-row grid, rows 0 and 12 are the goals`,
|
|
4056
|
+
"seat 0 scores in the top goal (row 0: points 3, 4, 5); seat 1 in the bottom goal (row 12: points 111, 112, 113)",
|
|
4057
|
+
...footballMoves(s).map((at) => {
|
|
4058
|
+
const [nx, ny] = footballPoint(at);
|
|
4059
|
+
const bounce = boundary(at) || s.edges.some((edge) => edge.split(":").map(Number).includes(at));
|
|
4060
|
+
return `move to ${at}: ${compass[`${nx - x},${ny - y}`]}${ny === 0 || ny === 12 ? ", goal" : bounce ? ", bounce: you move again" : ""}`;
|
|
4061
|
+
})
|
|
4062
|
+
];
|
|
4063
|
+
}
|
|
4064
|
+
|
|
3799
4065
|
// lib/multiplayer/mills.ts
|
|
3800
4066
|
var millLines = [
|
|
3801
4067
|
[0, 1, 2],
|
|
@@ -4409,7 +4675,7 @@ var multiplayerGames = [
|
|
|
4409
4675
|
symbol: "1000",
|
|
4410
4676
|
players: "2\u20134 players",
|
|
4411
4677
|
description: "Bid your hand. Marry a new trump.",
|
|
4412
|
-
rules: "A 24-card deck, nine through ace; strength is nine, jack, queen, king, ten, ace. Three active players receive seven cards and a three-card kitty. With four seats the dealer sits out and scores revealed kitty aces (50 each) and marriages. Two players receive ten cards each and choose one of two two-card kitties. The first active player opens at 100; bid in tens, no higher than 120 plus held marriages. Winner takes a kitty, gives each other active player a card (or discards two with two seats), then declares at least the auction bid. Follow suit and beat the winner when possible; if void, trump when held. Leading a held queen-king pair with the marriage declaration earns clubs 60, diamonds 80, hearts 100 or spades 40 and changes trump. Card points: ace 11, ten 10, king 4, queen 3, jack 2. Cards left from the kitties, including the declarer\u2019s two discards in a two-player game, score for the last trick winner. Declarer makes or loses their contract; others round their points to tens. At 800, only declarer earns further points. First to 1000 wins, with declarer winning a simultaneous finish. The first bomb cancels freely; subsequent bombs award opponents 60 (40 with four seats). Four nines can request a redeal."
|
|
4678
|
+
rules: "A 24-card deck, nine through ace; strength from lowest to highest is nine, jack, queen, king, ten, ace. Three active players receive seven cards and a three-card kitty. With four seats the dealer sits out and scores revealed kitty aces (50 each) and marriages. Two players receive ten cards each and choose one of two two-card kitties. The first active player opens at 100; bid in tens, no higher than 120 plus held marriages. Winner takes a kitty, gives each other active player a card (or discards two with two seats), then declares at least the auction bid. Follow suit and beat the winner when possible; if void, trump when held. Leading a held queen-king pair with the marriage declaration earns clubs 60, diamonds 80, hearts 100 or spades 40 and changes trump. Card points: ace 11, ten 10, king 4, queen 3, jack 2. Cards left from the kitties, including the declarer\u2019s two discards in a two-player game, score for the last trick winner. Declarer makes or loses their contract; others round their points to tens. At 800, only declarer earns further points. First to 1000 wins, with declarer winning a simultaneous finish. The first bomb cancels freely; subsequent bombs award opponents 60 (40 with four seats). Four nines can request a redeal."
|
|
4413
4679
|
},
|
|
4414
4680
|
{
|
|
4415
4681
|
id: "binokel",
|
|
@@ -4529,7 +4795,7 @@ var multiplayerGames = [
|
|
|
4529
4795
|
symbol: "\u2654",
|
|
4530
4796
|
players: "2 players",
|
|
4531
4797
|
description: "Claim sixty-six. Close the stock with care.",
|
|
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."
|
|
4798
|
+
rules: "Six cards each from a 24-card deck. Rank order from lowest to highest: 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."
|
|
4533
4799
|
},
|
|
4534
4800
|
{
|
|
4535
4801
|
id: "spades",
|
|
@@ -5004,107 +5270,6 @@ var action_catalog_default = {
|
|
|
5004
5270
|
]
|
|
5005
5271
|
};
|
|
5006
5272
|
|
|
5007
|
-
// lib/multiplayer/dice.ts
|
|
5008
|
-
var diceCategories = [
|
|
5009
|
-
"Ones",
|
|
5010
|
-
"Twos",
|
|
5011
|
-
"Threes",
|
|
5012
|
-
"Fours",
|
|
5013
|
-
"Fives",
|
|
5014
|
-
"Sixes",
|
|
5015
|
-
"Three of a kind",
|
|
5016
|
-
"Four of a kind",
|
|
5017
|
-
"Full house",
|
|
5018
|
-
"Small straight",
|
|
5019
|
-
"Large straight",
|
|
5020
|
-
"Five of a kind",
|
|
5021
|
-
"Chance"
|
|
5022
|
-
];
|
|
5023
|
-
function createDice(count) {
|
|
5024
|
-
if (!index(count - 2, 3)) throw Error("Choose two to four players.");
|
|
5025
|
-
return {
|
|
5026
|
-
dice: [0, 0, 0, 0, 0],
|
|
5027
|
-
held: Array(5).fill(false),
|
|
5028
|
-
rolls: 0,
|
|
5029
|
-
sheets: Array.from({ length: count }, () => Array(13).fill(null)),
|
|
5030
|
-
bonuses: Array(count).fill(0),
|
|
5031
|
-
turn: 0,
|
|
5032
|
-
finished: false,
|
|
5033
|
-
winner: null
|
|
5034
|
-
};
|
|
5035
|
-
}
|
|
5036
|
-
function diceScore(dice, category, joker = false) {
|
|
5037
|
-
const counts = Array.from(
|
|
5038
|
-
{ length: 6 },
|
|
5039
|
-
(_, i) => dice.filter((v) => v === i + 1).length
|
|
5040
|
-
), sum = dice.reduce((a, b) => a + b, 0);
|
|
5041
|
-
if (category < 6) return counts[category] * (category + 1);
|
|
5042
|
-
switch (category) {
|
|
5043
|
-
case 6:
|
|
5044
|
-
return Math.max(...counts) >= 3 ? sum : 0;
|
|
5045
|
-
case 7:
|
|
5046
|
-
return Math.max(...counts) >= 4 ? sum : 0;
|
|
5047
|
-
case 8:
|
|
5048
|
-
return joker || counts.includes(3) && counts.includes(2) ? 25 : 0;
|
|
5049
|
-
case 9:
|
|
5050
|
-
return joker || [0, 1, 2].some((start) => counts.slice(start, start + 4).every(Boolean)) ? 30 : 0;
|
|
5051
|
-
case 10:
|
|
5052
|
-
return joker || [0, 1].some((start) => counts.slice(start, start + 5).every(Boolean)) ? 40 : 0;
|
|
5053
|
-
case 11:
|
|
5054
|
-
return counts.includes(5) ? 50 : 0;
|
|
5055
|
-
case 12:
|
|
5056
|
-
return sum;
|
|
5057
|
-
default:
|
|
5058
|
-
throw Error("Choose a score category.");
|
|
5059
|
-
}
|
|
5060
|
-
}
|
|
5061
|
-
function diceTotal(s, seat) {
|
|
5062
|
-
const sheet = s.sheets[seat], upper = sheet.slice(0, 6).reduce((sum, n) => sum + (n ?? 0), 0);
|
|
5063
|
-
return sheet.reduce((sum, n) => sum + (n ?? 0), 0) + (upper >= 63 ? 35 : 0) + s.bonuses[seat];
|
|
5064
|
-
}
|
|
5065
|
-
function playDice(s, type, at, entropy2) {
|
|
5066
|
-
if (s.finished) throw Error("This match is finished.");
|
|
5067
|
-
if (type === "roll") {
|
|
5068
|
-
if (s.rolls >= 3) throw Error("Record a score after three rolls.");
|
|
5069
|
-
const random = draws(entropy2, 5);
|
|
5070
|
-
return {
|
|
5071
|
-
...s,
|
|
5072
|
-
rolls: s.rolls + 1,
|
|
5073
|
-
dice: s.dice.map(
|
|
5074
|
-
(v, i) => s.rolls && s.held[i] ? v : 1 + Math.floor(random[i] * 6)
|
|
5075
|
-
)
|
|
5076
|
-
};
|
|
5077
|
-
}
|
|
5078
|
-
if (at === void 0 || !index(at, type === "hold" ? 5 : 13) || !s.rolls)
|
|
5079
|
-
throw Error("Roll first, then choose a die or score category.");
|
|
5080
|
-
if (type === "hold")
|
|
5081
|
-
return { ...s, held: s.held.map((v, i) => i === at ? !v : v) };
|
|
5082
|
-
if (type !== "score" || s.sheets[s.turn][at] !== null)
|
|
5083
|
-
throw Error("Choose an unused score category.");
|
|
5084
|
-
const sheets = s.sheets.map((row) => [...row]), bonuses = [...s.bonuses];
|
|
5085
|
-
const five = s.dice.every((v) => v === s.dice[0]), upper = s.dice[0] - 1;
|
|
5086
|
-
const joker = five && sheets[s.turn][11] !== null && sheets[s.turn][upper] !== null;
|
|
5087
|
-
if (joker && sheets[s.turn].slice(6).some((n) => n === null) && at < 6)
|
|
5088
|
-
throw Error(
|
|
5089
|
-
"Use the five-of-a-kind joker in an unused lower category first."
|
|
5090
|
-
);
|
|
5091
|
-
sheets[s.turn][at] = diceScore(s.dice, at, joker);
|
|
5092
|
-
if (joker && sheets[s.turn][11] === 50) bonuses[s.turn] += 100;
|
|
5093
|
-
const next3 = {
|
|
5094
|
-
...s,
|
|
5095
|
-
sheets,
|
|
5096
|
-
bonuses,
|
|
5097
|
-
turn: (s.turn + 1) % s.sheets.length,
|
|
5098
|
-
rolls: 0,
|
|
5099
|
-
held: Array(5).fill(false),
|
|
5100
|
-
dice: [0, 0, 0, 0, 0]
|
|
5101
|
-
};
|
|
5102
|
-
next3.finished = sheets.every((row) => row.every((n) => n !== null));
|
|
5103
|
-
if (next3.finished)
|
|
5104
|
-
next3.winner = best(sheets.map((_, i) => diceTotal(next3, i)));
|
|
5105
|
-
return next3;
|
|
5106
|
-
}
|
|
5107
|
-
|
|
5108
5273
|
// lib/agents/action-help.ts
|
|
5109
5274
|
var help = {
|
|
5110
5275
|
amazons: "move: numeric from/to 0\u201399 and amount for the arrow square. Select an exact legal_wire_actions entry.",
|
|
@@ -5113,49 +5278,49 @@ var help = {
|
|
|
5113
5278
|
euchre: "Use visible_state.legalActions. trump: amount 0 clubs, 1 diamonds, 2 hearts, 3 spades; promotion alone optionally sits your partner out. pass declines trump. discard/play: at is a held card ID, not a hand index. IDs are suit*6+rank, ranks 0\u20135 mean 9,10,J,Q,K,A. The left bower belongs to trump for following suit. next deals after a hand. Partners are opposite seats; first team to 10 wins.",
|
|
5114
5279
|
cribbage: "Use visible_state.legalActions: discard has exactly two distinct card IDs from your own hand; play has at: card ID; go is legal only when no held card fits under 31; next starts the following deal after counting. Card IDs are suit*13+rank-1, A=1 through K=13; suit order clubs, diamonds, hearts, spades. Crib and opponents' unplayed cards remain hidden.",
|
|
5115
5280
|
fanorona: "Use visible_state.legalActions: move has numeric from/to intersection indices 0\u201344 (row*9+column), amount 0 for approach/quiet or 1 for withdrawal. stop ends an optional capture continuation. Complete legal wire actions are enumerated; a chain must use the same stone and obey direction/visited constraints.",
|
|
5116
|
-
categories: `begin: host starts a timed round; promotion:'untimed' disables the timer. submit/stop: text is a string holding a JSON array of six English answers in category order, for example text set to the string '["Apple","Ant",...]' (not a raw array); stop needs a complete sheet. expire reveals a timed-out round. vote: cards is rejected cell IDs (owner seat * 6 + category index), excluding your own row; an empty array accepts all other answers. next: host advances. Answers and ballots lock once submitted.
|
|
5281
|
+
categories: `begin: host starts a timed round; promotion:'untimed' disables the timer. submit/stop: text is a string holding a JSON array of six English answers in category order, for example text set to the string '["Apple","Ant",...]' (not a raw array); stop needs a complete sheet. expire reveals a timed-out round. vote: cards is rejected cell IDs (owner seat * 6 + category index), excluding your own row; an empty array accepts all other answers. next: host advances. Answers and ballots lock once submitted. The CLI fills at (the visible zero-based round) and scoreKey (the visible roundKey) on submit/stop/vote/expire when you leave them out, and accepts text as a plain array: {"type":"submit","text":["Spain","Seville","Seal","Soup","Singer","Spoon"]}.`,
|
|
5117
5282
|
cheat: "play: cards is one to four card IDs from your hand, claiming the displayed rank. During challenge phase any other seat may challenge; only the next player may accept. Final-card claims must resolve before a win.",
|
|
5118
5283
|
president: "play: cards is one to four equal-rank card IDs. Match pile size and beat its rank; 2 is high. Opening group includes 3 of clubs. pass sits out until the pile clears. Finished seats leave play; order gives final places.",
|
|
5119
5284
|
rummy: "stock/upcard: draw first. meld: cards is a set or run of held card IDs. layoff: at is a public meld index, cards is held card IDs to add. discard: at is a held card ID. Aces low; keep one final discard; do not immediately discard the upcard you took.",
|
|
5120
|
-
"connect-four": "
|
|
5285
|
+
"connect-four": 'legal_actions list {"type":"drop","column":1\u20137}, one-based; pass one unchanged or use --column. The CLI converts it to the wire action place with zero-based at = column-1.',
|
|
5121
5286
|
"tic-tac-toe": "place: at is a zero-based row-major square (0\u20138). legal_actions use one-based row/column instead; pass either form.",
|
|
5122
5287
|
chess: "move: from/to are algebraic squares; promotion is q/r/b/n.",
|
|
5123
5288
|
"atomic-chess": "move: from/to are algebraic squares; promotion is q/r/b/n. Explosions follow the game rules.",
|
|
5124
5289
|
"three-check": "move: from/to are algebraic squares; promotion is q/r/b/n. Check counts are in visible_state.",
|
|
5125
|
-
checkers: "
|
|
5290
|
+
checkers: 'legal_actions use algebraic squares (from "c3" to "d4"); pass one unchanged or use --from/--to. The wire move uses zero-based row-major squares (0\u201363). Continue a mandatory capture chain. Men move and capture forward only; kings move both ways. Convenience flags use algebraic squares.',
|
|
5126
5291
|
"brazilian-checkers": "move: from/to are zero-based squares (0\u201363); maximum captures apply.",
|
|
5127
5292
|
"international-draughts": "move: from/to are zero-based squares (0\u201399). sequence: cards is a complete legal path of square indexes, including the origin.",
|
|
5128
|
-
reversi: "
|
|
5129
|
-
gomoku: "
|
|
5130
|
-
hex: "place
|
|
5293
|
+
reversi: "legal_actions use one-based row/column (1\u20138); pass one unchanged or use --row/--column. The wire place uses zero-based row-major at (0\u201363). Unavailable turns pass automatically.",
|
|
5294
|
+
gomoku: "legal_actions use one-based row/column (1\u201315); pass one unchanged or use --row/--column. The wire place uses zero-based row-major at (0\u2013224). Exactly five wins.",
|
|
5295
|
+
hex: "legal_actions use one-based row/column; pass one unchanged or use --row/--column. The wire place uses a zero-based row-major hexagon. swap: no payload, only the second seat's first turn. Convenience: --row/--column or --swap.",
|
|
5131
5296
|
"dots-boxes": 'Send a legal_actions entry ({"type":"edge","edge":N}, one-based) unchanged or use --edge N; the CLI converts it to the wire action place with zero-based at = N-1 (visible_state.edges[N-1]). board_labeled lists each box with its edge numbers. Horizontal edges precede vertical edges in row-major order. Edges and box owners are in visible_state. Convenience: --edge is one-based.',
|
|
5132
|
-
oware: "sow:
|
|
5297
|
+
oware: 'legal_actions list {"type":"sow","pit":N}, one-based: seat 0 owns pits 1\u20136, seat 1 pits 7\u201312; pass one unchanged or use --pit. The wire sow uses zero-based at = pit-1 (0\u20135 for seat 0, 6\u201311 for seat 1).',
|
|
5133
5298
|
go: "place/dead: at is a zero-based row-major intersection (board_labeled shows row start indexes). pass/resume: no payload. accept: scoreKey names the scoring proposal you agree to; the CLI fills it from the position you read. Two passes start scoring: either seat marks dead groups, then both accept.",
|
|
5134
5299
|
battleships: "auto/clear/ready: no payload. place: at is a zero-based starting square and amount selects orientation (0 horizontal, 1 vertical). fire: at is a zero-based target square. Inspect your fleet/setup phase; enemy ships remain hidden.",
|
|
5135
5300
|
mills: "place/capture: at is a zero-based point index. move: from is the origin and at the destination. Completing a mill requires a subsequent capture action.",
|
|
5136
5301
|
dice: `roll: no payload. hold: at is a die index (0\u20134) to toggle its hold after rolling. score: at is a category index: ${diceCategories.map((name, i) => `${i} ${name}`).join(", ")}. Inspect the roll phase and score sheet.`,
|
|
5137
5302
|
ludo: "roll: no payload. move: at is the index (0\u20133) of your token. Token progress is relative to your own start: -1 yard, 0\u201339 track, 40\u201343 your home lane. Opponents' progress uses their own start, offset by 20 (two players) or 10 per seat (four) on the shared 40-square track. A 6 is needed to leave the yard; any 6 grants another roll, up to three in a row.",
|
|
5138
5303
|
domino: "tile: at is your zero-based hand position (not a pip value) and amount selects the chain end: 0 left, 1 right. draw: draw until you can play, or pass when the stock is empty. next: start the following round. Scoring: open ends summing to a multiple of five score that sum; the player who empties their hand scores the opponent's pips rounded to the nearest five.",
|
|
5139
|
-
football: "place: at is a
|
|
5304
|
+
football: "place: at is the destination point, row*9+column on a 9-column, 13-row grid (0\u2013116; rows 0 and 12 hold only the goal points 3\u20135 and 111\u2013113). board_labeled names each legal step with its direction and whether it bounces (another move). Seat 0 attacks the top goal, seat 1 the bottom.",
|
|
5140
5305
|
backgammon: "roll: no payload. move: numeric from/to and amount (die value), one die per action. Points are 0\u201323. Positive point counts are seat 0, negative seat 1. Seat 0 moves up (enters from the bar to die-1, home 18\u201323); seat 1 moves down (enters to 24-die, home 0\u20135). from -1 means the bar; to 24 bears off for both seats. Compulsory dice usage is enforced.",
|
|
5141
5306
|
shogi: "move: from is a zero-based square or a hand-piece type for a drop; to is a zero-based square; promotion:'yes' promotes. Inspect your hands and piece owners.",
|
|
5142
5307
|
xiangqi: "move: numeric zero-based row-major from/to on the 9\xD710 board. promotion is unused.",
|
|
5143
5308
|
poker: "fold/check/call/next: no payload. raise: amount is the desired bet total. Follow phase, pot, current bet, your cards and stack. next is a between-hand host action.",
|
|
5144
|
-
durak: "play: at is your card ID from the visible hand; take
|
|
5309
|
+
durak: "play: at is your card ID from the visible hand (attack or add a card of a rank already on the table; defend by beating the open attack card). take: the defender picks up the table after the attackers finish adding. pass: an attacker stops adding cards; when every attacker has passed, beaten cards are discarded (or the defender takes them after take). Follow attack/defence roles and visible trump/table.",
|
|
5145
5310
|
makao: "play: cards is 1\u20134 card IDs from your visible hand, all of the same rank. amount chooses an ace suit (0 clubs, 1 diamonds, 2 hearts, 3 spades) or a jack demand (rank 5\u201310 or 13 for king; 0 for no demand); visible_state.suit and demand use the same numbers; promotion:'makao' announces the last card. draw/pass/catch act according to phase. Inspect drawn-card and demand state.",
|
|
5146
5311
|
pan: "play: cards is an array of card IDs from the visible hand; take/next: no payload. Follow the table rank and legal group rules.",
|
|
5147
5312
|
"gin-rummy": "stock/upcard/pass/next: no payload. discard/knock: at is a card ID from the visible hand. Inspect draw/discard phase and deadwood rules.",
|
|
5148
5313
|
"sixty-six": "play: at is a card ID from the visible hand; promotion:'marriage' or 'marriage-claim' declares. exchange/close/claim/next are phase-specific commands.",
|
|
5149
5314
|
hearts: "select: at is a card ID from the visible hand for passing (selecting a chosen card again unselects it; visible_state.selected shows your choice), then pass. play: at is a card ID from the visible hand. moon: amount selects a scoring choice. Follow phase, lead suit and legal cards.",
|
|
5150
|
-
spades: "bid: amount is the bid; blind/look for blind nil where offered. play: at is a card ID from the visible hand. Follow partner, phase and lead suit.",
|
|
5315
|
+
spades: "bid: amount is the bid; blind/look for blind nil where offered. play: at is a card ID from the visible hand. Partners are opposite: seats 0 and 2 against seats 1 and 3 (the rules text calls them Players 1 & 3 and 2 & 4). Follow partner, phase and lead suit.",
|
|
5151
5316
|
"oh-hell": "bid: amount is the bid. play: at is a card ID from the visible hand. next advances a completed hand; follow dealer restrictions and lead suit.",
|
|
5152
|
-
"three-five-eight": "exchange: at is one card ID from your visible hand. discard: cards is exactly four distinct card IDs from your visible hand. play: at is one card ID from your visible hand. contract: amount 0
|
|
5317
|
+
"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; the listed sets are samples, any four of your cards are legal. play: at is one card ID from your visible hand. contract: amount 0 clubs, 1 diamonds, 2 hearts, 3 spades as trump, 4 no trumps, 5 no tricks (misere); each once per player. Cards rank 2 (low) to ace (high). next advances the hand.",
|
|
5153
5318
|
bridge: "bid: amount 1\u201335 encodes level floor((amount-1)/5)+1 and strain (amount-1)%5; pass/double/redouble have no payload. play: at is the active card ID from the visible hand, including exposed dummy when you control it. Follow auction, strain and lead-suit rules.",
|
|
5154
5319
|
"polish-hearts": "trump: amount is a suit index; pass: no payload, only when no layout card fits; play: at is a card ID from the visible hand. Follow the current penalty/trump/layout phase.",
|
|
5155
5320
|
thousand: "bid/declare: amount is points. kitty: at selects a kitty. exchange: cards is card IDs from the visible hand. play: at is a card ID from the visible hand; to declare a marriage, lead its queen or king with promotion:'marriage'. pass/bomb/redeal/next depend on phase. Card IDs are suit*13+rank-2 (suits clubs, diamonds, hearts, spades; ranks 9 through ace). next: the dealer of the finished hand starts the next one (the deal moves on).",
|
|
5156
|
-
binokel: "bid: amount is points; trump: amount is a suit; play: at is a card ID from the visible hand. pass/next have no payload. Meld values: aces around 10
|
|
5321
|
+
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 (one set, two sets): aces around 10, 100; kings around 8, 80; queens around 6, 60; jacks around 4, 40; trump run 15, 150; royal marriage 4, ordinary marriage 2 per pair. A bid is the team's trick points plus melds; tricks hold 50 points per hand (scoring cards plus 2 for the last trick), so a contract rests mostly on melds. The declaring team needs 20 meld points.",
|
|
5157
5322
|
skat: "bid: amount is an auction value; hold/pass/hand/pickup act in auction/pickup phases (after a hold the bidder must bid higher or pass). discard: cards is exactly two card IDs from your visible hand. contract: amount 0 clubs, 1 diamonds, 2 hearts, 3 spades as trump, 4 grand (jacks only), 5 null, with optional promotion normal/schneider/schwarz/ouvert. play: at is a card ID from the visible hand. Jacks rank clubs, spades, hearts, diamonds. Card IDs are suit*13+rank-2 (suits clubs, diamonds, hearts, spades). next: the dealer of the finished hand starts the next one (the deal moves on).",
|
|
5158
|
-
baska: "announce: no payload in the honor phase. contract: amount is one of 1,3,4,5,6 and must exceed the current contract. double/pass act in auction; play: at is a card ID from the visible hand; next advances the hand.",
|
|
5323
|
+
baska: "announce: no payload in the honor phase. contract: amount is one of 1 Wedding, 3 Grand, 4 Zolo, 5 Grand Du, 6 Zolo Du and must exceed the current contract (the other numbers name contracts not bid this way: 0 Ordinary, 2 Zolo Bieda, 7 Master, 8 Baska). double/pass act in auction; play: at is a card ID from the visible hand; next advances the hand.",
|
|
5159
5324
|
canasta: "stock/next: no payload. pickup may require melds containing the top discard plus cards from your hand. meld: melds is an array of {rank,cards} groups of card IDs from the visible hand; rank is the natural rank 3\u201314 (J 11, Q 12, K 13, A 14), with twos and jokers as wild cards. cards_labeled names each card. discard: at is a card ID from the visible hand. Follow opening meld and canasta requirements. Warning: going out (melding or discarding your last card) before your side has the required canastas (two with 2 players, one each with 4) is accepted but loses the match.",
|
|
5160
5325
|
"letter-tiles": "place: placements is an array of {at,rack,letter?}; at is a board index and rack a rack index, letter assigns a blank. exchange: cards is rack indexes. accept/challenge/pass/next depend on phase.",
|
|
5161
5326
|
"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.",
|
|
@@ -5239,20 +5404,23 @@ function canastaCount(melds) {
|
|
|
5239
5404
|
return melds.filter((m) => m.cards.length >= 7).length;
|
|
5240
5405
|
}
|
|
5241
5406
|
function finish2(t, out2, concealed = false) {
|
|
5407
|
+
const parts = [];
|
|
5242
5408
|
for (let i = 0; i < 2; i++) {
|
|
5243
|
-
const meld2 = t.melds[i], red = t.red[i].length,
|
|
5409
|
+
const meld2 = t.melds[i], red = t.red[i].length, canastas = meld2.reduce(
|
|
5244
5410
|
(n, m) => n + (m.cards.length >= 7 ? m.cards.some(canastaWild) ? 300 : 500 : 0),
|
|
5245
5411
|
0
|
|
5246
|
-
)
|
|
5412
|
+
), threes = (red * 100 + (red === 4 ? 400 : 0)) * (meld2.length ? 1 : -1), bonus = canastas + threes, cards = meld2.flatMap((m) => m.cards).reduce((n, c) => n + canastaValue(c), 0), held = t.hands.reduce(
|
|
5247
5413
|
(n, h, seat) => n + (team(t, seat) === i ? h.reduce((n2, c) => n2 + canastaValue(c), 0) : 0),
|
|
5248
5414
|
0
|
|
5249
5415
|
);
|
|
5250
|
-
|
|
5416
|
+
const going = out2 !== null && team(t, out2) === i ? concealed ? 200 : 100 : 0;
|
|
5417
|
+
t.scores[i] += bonus + cards - held + going;
|
|
5418
|
+
parts.push(`Team ${i + 1}: melded cards ${cards}, canastas ${canastas}, red threes ${threes}${red && !meld2.length ? " (no meld, so negative)" : ""}, going out ${going}, cards left in hand -${held}`);
|
|
5251
5419
|
}
|
|
5252
5420
|
t.phase = t.scores.some((n) => n >= 5e3) ? "finished" : "round";
|
|
5253
5421
|
t.winner = t.phase === "finished" ? best(t.scores) : null;
|
|
5254
5422
|
t.turn = t.dealer;
|
|
5255
|
-
t.result = `Hand complete. Scores ${t.scores.join(" / ")}.`;
|
|
5423
|
+
t.result = `Hand complete${out2 === null ? " (stock exhausted)" : ""}. ${parts.join("; ")}. Scores ${t.scores.join(" / ")}.`;
|
|
5256
5424
|
}
|
|
5257
5425
|
function out(t, seat) {
|
|
5258
5426
|
if (t.hands[seat].length) return;
|
|
@@ -5739,7 +5907,7 @@ function award(s) {
|
|
|
5739
5907
|
}
|
|
5740
5908
|
return s;
|
|
5741
5909
|
}
|
|
5742
|
-
function
|
|
5910
|
+
function advance2(s) {
|
|
5743
5911
|
const live = s.players.filter((p) => !p.folded && !p.out);
|
|
5744
5912
|
if (live.length <= 1) return award(s);
|
|
5745
5913
|
if (s.pending.length) {
|
|
@@ -5765,7 +5933,7 @@ function advance(s) {
|
|
|
5765
5933
|
s.pending = s.players.flatMap((p, i) => canAct(p) ? [i] : []);
|
|
5766
5934
|
if (s.pending.length <= 1) {
|
|
5767
5935
|
s.pending = [];
|
|
5768
|
-
return
|
|
5936
|
+
return advance2(s);
|
|
5769
5937
|
}
|
|
5770
5938
|
s.turn = nextSeat(s, s.button, canAct);
|
|
5771
5939
|
return s;
|
|
@@ -5814,7 +5982,7 @@ function begin(s, seed) {
|
|
|
5814
5982
|
s.turn = nextSeat(s, big, canAct);
|
|
5815
5983
|
if (!s.pending.length || s.pending.length === 1 && s.players[s.pending[0]].street >= Math.max(...s.players.map((p) => p.street))) {
|
|
5816
5984
|
s.pending = [];
|
|
5817
|
-
return
|
|
5985
|
+
return advance2(s);
|
|
5818
5986
|
}
|
|
5819
5987
|
return s;
|
|
5820
5988
|
}
|
|
@@ -5909,7 +6077,7 @@ function pokerAction(previous, seat, action, seed) {
|
|
|
5909
6077
|
s.actedAt ??= s.players.map((player) => player.street);
|
|
5910
6078
|
s.actedAt[seat] = s.bet;
|
|
5911
6079
|
s.log = [...s.log, `Player ${seat + 1} ${label}`].slice(-80);
|
|
5912
|
-
return
|
|
6080
|
+
return advance2(s);
|
|
5913
6081
|
}
|
|
5914
6082
|
function leavePoker(previous, seat) {
|
|
5915
6083
|
const s = structuredClone(previous);
|
|
@@ -5927,7 +6095,7 @@ function leavePoker(previous, seat) {
|
|
|
5927
6095
|
return s;
|
|
5928
6096
|
}
|
|
5929
6097
|
if (s.turn === seat || s.players.filter((p) => !p.folded && !p.out).length <= 1)
|
|
5930
|
-
return
|
|
6098
|
+
return advance2(s);
|
|
5931
6099
|
return s;
|
|
5932
6100
|
}
|
|
5933
6101
|
function pokerView(s, seat) {
|
|
@@ -7716,7 +7884,8 @@ function moveVariant(state, from, to, promotion = "q") {
|
|
|
7716
7884
|
if (!parseUci(from + to)) throw Error("Choose a legal move.");
|
|
7717
7885
|
const move2 = variantMoves(state).find((m) => m.from === from && m.to === to && (!m.promotion || m.promotion === promotion));
|
|
7718
7886
|
if (!move2) throw Error("Choose a legal move.");
|
|
7719
|
-
const pos = variantPosition(state),
|
|
7887
|
+
const pos = variantPosition(state), san = makeSanAndPlay(pos, move2.internal), setup = pos.toSetup();
|
|
7888
|
+
const last = san.endsWith("#") && !pos.isCheckmate() ? san.slice(0, -1) + (pos.isCheck() ? "+" : "") : san;
|
|
7720
7889
|
const fen = makeFen(setup), history = [...state.history, repetitionKey(fen)];
|
|
7721
7890
|
const outcome = pos.outcome();
|
|
7722
7891
|
const repeated = history.filter((key2) => key2 === repetitionKey(fen)).length >= 3;
|
|
@@ -8312,13 +8481,14 @@ function playBackgammon(s, type, from, to, die, entropy2) {
|
|
|
8312
8481
|
opening: false,
|
|
8313
8482
|
rolled,
|
|
8314
8483
|
dice: rolled[0] === rolled[1] ? Array(4).fill(rolled[0]) : rolled,
|
|
8315
|
-
result: `
|
|
8484
|
+
result: `Player ${turn + 1} rolled ${rolled.join(" and ")}.`,
|
|
8485
|
+
last: { by_seat: turn, rolled, moves: [] }
|
|
8316
8486
|
};
|
|
8317
8487
|
return backgammonMoves(next4).length ? next4 : {
|
|
8318
8488
|
...next4,
|
|
8319
8489
|
dice: [],
|
|
8320
8490
|
turn: turn === 0 ? 1 : 0,
|
|
8321
|
-
result: "
|
|
8491
|
+
result: `Player ${turn + 1} rolled ${rolled.join(" and ")}: all moves are blocked. Turn passes.`
|
|
8322
8492
|
};
|
|
8323
8493
|
}
|
|
8324
8494
|
const move2 = backgammonMoves(s).find(
|
|
@@ -8329,6 +8499,7 @@ function playBackgammon(s, type, from, to, die, entropy2) {
|
|
|
8329
8499
|
"Use the maximum number of dice; if only one can be used, use the higher die."
|
|
8330
8500
|
);
|
|
8331
8501
|
const next3 = step(s, move2);
|
|
8502
|
+
next3.last = { by_seat: s.turn, rolled: s.last?.rolled ?? s.rolled, moves: [...s.last?.by_seat === s.turn ? s.last.moves : [], { ...move2, ...move2.to !== 24 && s.points[move2.to] === (s.turn === 0 ? -1 : 1) ? { hit: true } : {} }] };
|
|
8332
8503
|
if (next3.off[s.turn] === 15) {
|
|
8333
8504
|
const opponent = 1 - s.turn, backgammon = next3.bar[opponent] > 0 || next3.points.some(
|
|
8334
8505
|
(n, i) => (s.turn === 0 ? i >= 18 : i < 6) && n * (s.turn === 0 ? -1 : 1) > 0
|
|
@@ -8336,147 +8507,13 @@ function playBackgammon(s, type, from, to, die, entropy2) {
|
|
|
8336
8507
|
next3.winner = s.turn;
|
|
8337
8508
|
next3.result = next3.off[opponent] ? "Single game." : backgammon ? "Backgammon \xB7 3 points." : "Gammon \xB7 2 points.";
|
|
8338
8509
|
} else if (!backgammonMoves(next3).length) {
|
|
8510
|
+
if (next3.dice.length) next3.result = `Player ${s.turn + 1} cannot use the remaining ${next3.dice.join(" and ")}. Turn passes.`;
|
|
8339
8511
|
next3.dice = [];
|
|
8340
8512
|
next3.turn = s.turn === 0 ? 1 : 0;
|
|
8341
8513
|
}
|
|
8342
8514
|
return next3;
|
|
8343
8515
|
}
|
|
8344
8516
|
|
|
8345
|
-
// lib/multiplayer/football.ts
|
|
8346
|
-
var footballPoint = (at) => [at % 9, Math.floor(at / 9)];
|
|
8347
|
-
var footballEdge = (a, b) => a < b ? `${a}:${b}` : `${b}:${a}`;
|
|
8348
|
-
var valid = (at) => index(at, 117) && (Math.floor(at / 9) > 0 && Math.floor(at / 9) < 12 || at % 9 >= 3 && at % 9 <= 5);
|
|
8349
|
-
var boundary = (at) => {
|
|
8350
|
-
const [x, y] = footballPoint(at);
|
|
8351
|
-
return x === 0 || x === 8 || y === 0 || y === 12 || (y === 1 || y === 11) && x !== 4;
|
|
8352
|
-
};
|
|
8353
|
-
function createFootball() {
|
|
8354
|
-
const edges = [];
|
|
8355
|
-
for (let y = 1; y <= 11; y++)
|
|
8356
|
-
for (const x of [0, 8])
|
|
8357
|
-
if (y < 11) edges.push(footballEdge(y * 9 + x, (y + 1) * 9 + x));
|
|
8358
|
-
for (const y of [1, 11])
|
|
8359
|
-
for (let x = 0; x < 8; x++)
|
|
8360
|
-
if (x < 3 || x >= 5) edges.push(footballEdge(y * 9 + x, y * 9 + x + 1));
|
|
8361
|
-
for (const y of [0, 12]) {
|
|
8362
|
-
edges.push(
|
|
8363
|
-
footballEdge(y * 9 + 3, y * 9 + 4),
|
|
8364
|
-
footballEdge(y * 9 + 4, y * 9 + 5)
|
|
8365
|
-
);
|
|
8366
|
-
for (const x of [3, 5])
|
|
8367
|
-
edges.push(footballEdge(y * 9 + x, (y === 0 ? 1 : 11) * 9 + x));
|
|
8368
|
-
}
|
|
8369
|
-
return { ball: 58, edges, turn: 0, winner: null, last: null };
|
|
8370
|
-
}
|
|
8371
|
-
function footballMoves(s) {
|
|
8372
|
-
if (s.winner !== null) return [];
|
|
8373
|
-
const [x, y] = footballPoint(s.ball), moves = [];
|
|
8374
|
-
for (let dy = -1; dy <= 1; dy++)
|
|
8375
|
-
for (let dx = -1; dx <= 1; dx++) {
|
|
8376
|
-
const nx = x + dx, ny = y + dy, at = ny * 9 + nx;
|
|
8377
|
-
if ((ny === 0 || ny === 12) && (x < 3 || x > 5)) continue;
|
|
8378
|
-
if ((dx || dy) && nx >= 0 && nx <= 8 && ny >= 0 && ny <= 12 && valid(at) && !s.edges.includes(footballEdge(s.ball, at)))
|
|
8379
|
-
moves.push(at);
|
|
8380
|
-
}
|
|
8381
|
-
return moves;
|
|
8382
|
-
}
|
|
8383
|
-
function playFootball(s, at) {
|
|
8384
|
-
if (!footballMoves(s).includes(at))
|
|
8385
|
-
throw Error("Choose an adjacent point along an unused line.");
|
|
8386
|
-
const bounced = boundary(at) || s.edges.some((edge) => edge.split(":").map(Number).includes(at));
|
|
8387
|
-
const y = Math.floor(at / 9);
|
|
8388
|
-
const next3 = {
|
|
8389
|
-
...s,
|
|
8390
|
-
ball: at,
|
|
8391
|
-
edges: [...s.edges, footballEdge(s.ball, at)],
|
|
8392
|
-
turn: bounced ? s.turn : s.turn === 0 ? 1 : 0,
|
|
8393
|
-
winner: y === 0 ? 0 : y === 12 ? 1 : null,
|
|
8394
|
-
last: [s.ball, at]
|
|
8395
|
-
};
|
|
8396
|
-
if (next3.winner === null && !footballMoves(next3).length)
|
|
8397
|
-
next3.winner = 1 - next3.turn;
|
|
8398
|
-
return next3;
|
|
8399
|
-
}
|
|
8400
|
-
|
|
8401
|
-
// lib/multiplayer/ludo.ts
|
|
8402
|
-
function createLudo(count) {
|
|
8403
|
-
if (!index(count - 2, 3)) throw Error("Choose two to four players.");
|
|
8404
|
-
return {
|
|
8405
|
-
pieces: Array.from({ length: count }, () => [-1, -1, -1, -1]),
|
|
8406
|
-
turn: 0,
|
|
8407
|
-
die: 0,
|
|
8408
|
-
streak: 0,
|
|
8409
|
-
attempts: 0,
|
|
8410
|
-
winner: null,
|
|
8411
|
-
last: "Roll a six to leave your yard."
|
|
8412
|
-
};
|
|
8413
|
-
}
|
|
8414
|
-
var ludoTrack = (seat, progress, count = 4) => (seat * (count === 2 ? 20 : 10) + progress) % 40;
|
|
8415
|
-
function ludoMoves(s) {
|
|
8416
|
-
if (!s.die || s.winner !== null) return [];
|
|
8417
|
-
return s.pieces[s.turn].flatMap((p, i) => {
|
|
8418
|
-
const to = p < 0 ? s.die === 6 ? 0 : -1 : p + s.die;
|
|
8419
|
-
return to >= 0 && to <= 43 && !s.pieces[s.turn].some((other, j) => j !== i && other === to) ? [i] : [];
|
|
8420
|
-
});
|
|
8421
|
-
}
|
|
8422
|
-
function advance2(s) {
|
|
8423
|
-
return {
|
|
8424
|
-
...s,
|
|
8425
|
-
turn: (s.turn + 1) % s.pieces.length,
|
|
8426
|
-
die: 0,
|
|
8427
|
-
streak: 0,
|
|
8428
|
-
attempts: 0
|
|
8429
|
-
};
|
|
8430
|
-
}
|
|
8431
|
-
function playLudo(s, type, at, entropy2) {
|
|
8432
|
-
if (s.winner !== null) throw Error("This match is finished.");
|
|
8433
|
-
if (type === "roll") {
|
|
8434
|
-
if (s.die) throw Error("Move a piece before rolling again.");
|
|
8435
|
-
const die = 1 + Math.floor(draws(entropy2, 1)[0] * 6), streak = die === 6 ? s.streak + 1 : 0;
|
|
8436
|
-
let next4 = {
|
|
8437
|
-
...s,
|
|
8438
|
-
die,
|
|
8439
|
-
streak,
|
|
8440
|
-
attempts: s.attempts + 1,
|
|
8441
|
-
last: `Player ${s.turn + 1} rolled ${die}.`
|
|
8442
|
-
};
|
|
8443
|
-
if (streak > 3)
|
|
8444
|
-
return advance2({
|
|
8445
|
-
...next4,
|
|
8446
|
-
last: "Three extra moves is the limit. Turn passes."
|
|
8447
|
-
});
|
|
8448
|
-
if (!ludoMoves(next4).length) {
|
|
8449
|
-
const allHome = s.pieces[s.turn].every((p) => p === -1);
|
|
8450
|
-
next4 = allHome && next4.attempts < 3 || die === 6 ? { ...next4, die: 0 } : advance2(next4);
|
|
8451
|
-
}
|
|
8452
|
-
return next4;
|
|
8453
|
-
}
|
|
8454
|
-
if (type !== "move" || at === void 0 || !index(at, 4) || !ludoMoves(s).includes(at))
|
|
8455
|
-
throw Error(
|
|
8456
|
-
"Choose a highlighted piece. You need an exact roll to reach home."
|
|
8457
|
-
);
|
|
8458
|
-
const pieces = s.pieces.map((row) => [...row]), old = pieces[s.turn][at], to = old < 0 ? 0 : old + s.die;
|
|
8459
|
-
pieces[s.turn][at] = to;
|
|
8460
|
-
if (to < 40) {
|
|
8461
|
-
for (let seat = 0; seat < pieces.length; seat++)
|
|
8462
|
-
if (seat !== s.turn) {
|
|
8463
|
-
pieces[seat] = pieces[seat].map(
|
|
8464
|
-
(p) => p >= 0 && p < 40 && ludoTrack(seat, p, pieces.length) === ludoTrack(s.turn, to, pieces.length) ? -1 : p
|
|
8465
|
-
);
|
|
8466
|
-
}
|
|
8467
|
-
}
|
|
8468
|
-
const won = pieces[s.turn].every((p) => p >= 40);
|
|
8469
|
-
const next3 = {
|
|
8470
|
-
...s,
|
|
8471
|
-
pieces,
|
|
8472
|
-
die: 0,
|
|
8473
|
-
attempts: 0,
|
|
8474
|
-
winner: won ? s.turn : null,
|
|
8475
|
-
last: `Player ${s.turn + 1} moved piece ${at + 1}.`
|
|
8476
|
-
};
|
|
8477
|
-
return s.die === 6 && s.streak < 3 || won ? next3 : advance2(next3);
|
|
8478
|
-
}
|
|
8479
|
-
|
|
8480
8517
|
// lib/multiplayer/go.ts
|
|
8481
8518
|
function createGo(size = 9) {
|
|
8482
8519
|
const board = Array(size * size).fill(0);
|
|
@@ -11208,7 +11245,7 @@ function playProperties(s, a, entropy2) {
|
|
|
11208
11245
|
}
|
|
11209
11246
|
if (s.phase === "tax") {
|
|
11210
11247
|
if (a.type !== "tax" || ![0, 1].includes(a.amount ?? -1))
|
|
11211
|
-
throw Error("Choose
|
|
11248
|
+
throw Error("Choose the tax: amount 0 pays the flat 200, amount 1 pays ten percent of your estate value.");
|
|
11212
11249
|
const estate = p.cash + s.assets.reduce(
|
|
11213
11250
|
(n, a2, i) => n + (a2.owner === seat ? propertyFields[i].price + a2.houses * propertyFields[i].build : 0),
|
|
11214
11251
|
0
|
|
@@ -11283,7 +11320,8 @@ function playProperties(s, a, entropy2) {
|
|
|
11283
11320
|
t.dice = [];
|
|
11284
11321
|
return t;
|
|
11285
11322
|
}
|
|
11286
|
-
throw Error("
|
|
11323
|
+
if (a.type === "release") throw Error(p.jail < 0 ? "You are not in jail." : "Leave jail at the start of your turn, in the roll phase: release with amount 0 pays 50, amount 1 uses a release card.");
|
|
11324
|
+
throw Error(`That action is not available in the ${s.phase} phase; choose one from the legal list.`);
|
|
11287
11325
|
}
|
|
11288
11326
|
function propertyView(s) {
|
|
11289
11327
|
const { decks, ...publicState } = s;
|
|
@@ -11768,9 +11806,9 @@ function dominoMoves(s) {
|
|
|
11768
11806
|
].filter((m) => m !== null);
|
|
11769
11807
|
});
|
|
11770
11808
|
}
|
|
11771
|
-
function endRound(s, winner2, bonus) {
|
|
11772
|
-
const scores2 = [...s.scores];
|
|
11773
|
-
if (winner2 !== null) scores2[winner2] +=
|
|
11809
|
+
function endRound(s, winner2, bonus, out2 = false) {
|
|
11810
|
+
const scores2 = [...s.scores], points = Math.round(bonus / 5) * 5;
|
|
11811
|
+
if (winner2 !== null) scores2[winner2] += points;
|
|
11774
11812
|
const finished = scores2.some((n) => n >= 100);
|
|
11775
11813
|
return {
|
|
11776
11814
|
...s,
|
|
@@ -11778,7 +11816,7 @@ function endRound(s, winner2, bonus) {
|
|
|
11778
11816
|
winner: finished ? best(scores2) : null,
|
|
11779
11817
|
phase: finished ? "finished" : "round",
|
|
11780
11818
|
turn: winner2 ?? s.turn,
|
|
11781
|
-
result: winner2 === null ? "Blocked round: equal pip totals." : `Player ${winner2 + 1}
|
|
11819
|
+
result: winner2 === null ? "Blocked round: equal pip totals, nobody scores." : out2 ? `Player ${winner2 + 1} goes out and scores ${points} for the opponent's ${bonus} pips (rounded to five).` : `Blocked round: Player ${winner2 + 1} holds fewer pips and scores ${points} for the ${bonus}-pip difference (rounded to five).`
|
|
11782
11820
|
};
|
|
11783
11821
|
}
|
|
11784
11822
|
var pipSum = (hand2) => hand2.reduce((n, [a, b]) => n + a + b, 0);
|
|
@@ -11797,8 +11835,9 @@ function playDomino(s, type, at, end, entropy2) {
|
|
|
11797
11835
|
throw Error("Play a matching tile before drawing.");
|
|
11798
11836
|
while (stock.length && !dominoMoves({ ...s, hands }).length)
|
|
11799
11837
|
hands[s.turn].push(stock.pop());
|
|
11800
|
-
|
|
11801
|
-
|
|
11838
|
+
const drawn = hands[s.turn].length - s.hands[s.turn].length;
|
|
11839
|
+
if (dominoMoves({ ...s, hands }).length) return { ...s, hands, stock, result: `Player ${s.turn + 1} drew ${drawn} tile${drawn === 1 ? "" : "s"}.` };
|
|
11840
|
+
const next4 = { ...s, hands, stock, turn: 1 - s.turn, passes: s.passes + 1, result: `Player ${s.turn + 1} ${drawn ? `drew ${drawn} tile${drawn === 1 ? "" : "s"}, ` : ""}cannot play and passes.` };
|
|
11802
11841
|
if (next4.passes < 2) return next4;
|
|
11803
11842
|
const a2 = pipSum(hands[0]), b2 = pipSum(hands[1]);
|
|
11804
11843
|
return endRound(next4, a2 === b2 ? null : a2 < b2 ? 0 : 1, Math.abs(a2 - b2));
|
|
@@ -11816,7 +11855,8 @@ function playDomino(s, type, at, end, entropy2) {
|
|
|
11816
11855
|
}
|
|
11817
11856
|
const left = chain[0], right = chain.at(-1);
|
|
11818
11857
|
const total = chain.length === 1 ? left[0] + left[1] : (left[0] === left[1] ? left[0] * 2 : left[0]) + (right[0] === right[1] ? right[1] * 2 : right[1]);
|
|
11819
|
-
|
|
11858
|
+
const scored = total % 5 === 0 && total > 0;
|
|
11859
|
+
if (scored) scores2[s.turn] += total;
|
|
11820
11860
|
const next3 = {
|
|
11821
11861
|
...s,
|
|
11822
11862
|
hands,
|
|
@@ -11824,10 +11864,11 @@ function playDomino(s, type, at, end, entropy2) {
|
|
|
11824
11864
|
chain,
|
|
11825
11865
|
scores: scores2,
|
|
11826
11866
|
passes: 0,
|
|
11827
|
-
turn: 1 - s.turn
|
|
11867
|
+
turn: 1 - s.turn,
|
|
11868
|
+
result: `Player ${s.turn + 1} played ${a}-${b}${scored ? ` and scores ${total}: the open ends total ${total}` : ""}.`
|
|
11828
11869
|
};
|
|
11829
11870
|
if (!hands[s.turn].length)
|
|
11830
|
-
return endRound(next3, s.turn, pipSum(hands[1 - s.turn]));
|
|
11871
|
+
return endRound(next3, s.turn, pipSum(hands[1 - s.turn]), true);
|
|
11831
11872
|
if (scores2.some((n) => n >= 100))
|
|
11832
11873
|
return { ...next3, phase: "finished", winner: best(scores2) };
|
|
11833
11874
|
return next3;
|
|
@@ -12139,7 +12180,7 @@ function agentListComplete(game, state, legal) {
|
|
|
12139
12180
|
if (game === "poker") return !legal.some((action) => action.type === "raise");
|
|
12140
12181
|
return false;
|
|
12141
12182
|
}
|
|
12142
|
-
var amounts = [.../* @__PURE__ */ new Set([...Array.from({ length: 301 }, (_, i) => i), ...Array.from({ length:
|
|
12183
|
+
var amounts = [.../* @__PURE__ */ new Set([...Array.from({ length: 301 }, (_, i) => i), ...Array.from({ length: 141 }, (_, i) => 300 + i * 5)])];
|
|
12143
12184
|
var promotions = ["marriage", "marriage-claim", "makao", "normal", "schneider", "schwarz", "ouvert"];
|
|
12144
12185
|
function hand(state, seat) {
|
|
12145
12186
|
for (const key2 of ["hands", "hand"]) {
|
|
@@ -12166,6 +12207,15 @@ function cardGroups(cards) {
|
|
|
12166
12207
|
else for (let i = 0; i + 4 <= cards.length; i += 4) add(cards.slice(i, i + 4));
|
|
12167
12208
|
return [...groups.values()].map(([group]) => group);
|
|
12168
12209
|
}
|
|
12210
|
+
function runGroups(cards) {
|
|
12211
|
+
const runs = [];
|
|
12212
|
+
for (const card2 of cards) for (let length = 3; ; length++) {
|
|
12213
|
+
const run = Array.from({ length }, (_, i) => card2 + i);
|
|
12214
|
+
if (card2 % 13 + length > 13 || !run.every((c) => cards.includes(c))) break;
|
|
12215
|
+
runs.push(run);
|
|
12216
|
+
}
|
|
12217
|
+
return runs;
|
|
12218
|
+
}
|
|
12169
12219
|
var atTypes = /* @__PURE__ */ new Set(["fire", "sow", "hold", "score", "capture", "knock", "dead", "mortgage", "redeem", "build", "sell", "choose", "kitty", "place", "tile", "move"]);
|
|
12170
12220
|
var amountTypes = /* @__PURE__ */ new Set(["bid", "raise", "declare", "tax", "release", "moon", "trump", "contract", "kitty", "announce", "choose", "tile", "place", "move"]);
|
|
12171
12221
|
var cardTypes = /* @__PURE__ */ new Set(["play", "discard", "exchange", "select", "meld", "layoff"]);
|
|
@@ -12220,6 +12270,10 @@ function specific(game, state, seat) {
|
|
|
12220
12270
|
{ type: "play", cards, promotion: "makao" },
|
|
12221
12271
|
...[0, 1, 2, 3, 5, 6, 7, 8, 9, 10, 13].flatMap((amount) => [{ type: "play", cards, amount }, { type: "play", cards, amount, promotion: "makao" }])
|
|
12222
12272
|
])];
|
|
12273
|
+
if (game === "rummy") {
|
|
12274
|
+
const runs = runGroups(hand(state, seat));
|
|
12275
|
+
return [...generic(adapters.rummy, state, seat), ...runs.map((cards) => ({ type: "meld", cards })), ...runs.flatMap((cards) => Array.from({ length: 20 }, (_, at) => ({ type: "layoff", at, cards })))];
|
|
12276
|
+
}
|
|
12223
12277
|
if (game === "go") {
|
|
12224
12278
|
const s = state;
|
|
12225
12279
|
if (s.phase === "scoring" && s.accepted.includes(seat)) return [];
|
|
@@ -12429,12 +12483,33 @@ function agentPosition(room) {
|
|
|
12429
12483
|
const active = room.phase === "playing" && !!match && !room.players[room.seat].left && !ended && !room.players.some((p) => p.paused);
|
|
12430
12484
|
const grid = !complete && match ? indexedBoard(room.game, match.state) : null;
|
|
12431
12485
|
const wire = !complete && match && enumeratedAgentGames.includes(room.game) ? active ? agentLegalActions(room.game, match.state, room.seat, room.capacity) ?? [] : [] : null;
|
|
12432
|
-
const checkerLast = match?.kind === "checkers"
|
|
12486
|
+
const checkerLast = match?.kind === "checkers" ? checkerLastMove(match.state.history) : null;
|
|
12487
|
+
const snapshotLast = (() => {
|
|
12488
|
+
if (!match) return null;
|
|
12489
|
+
const draughts2 = (key2) => key2?.split(":")[0].split(",").map(Number);
|
|
12490
|
+
const pieceOwner = (p) => p ? Math.abs(p) - 1 : -1, seatOwner = (p) => p;
|
|
12491
|
+
if (match.kind === "checkers" || match.kind === "brazilian-checkers" || match.kind === "international-draughts") {
|
|
12492
|
+
const [a, b] = match.state.history.slice(-2);
|
|
12493
|
+
return snapshotTurn(draughts2(a), draughts2(b), pieceOwner, [1 - Number(b?.split(":")[1])]);
|
|
12494
|
+
}
|
|
12495
|
+
if (match.kind === "fanorona") {
|
|
12496
|
+
const [a, b] = match.state.history.slice(-2).map((k) => k.split(":")[0].match(/-1|0|1/g)?.map(Number));
|
|
12497
|
+
return snapshotTurn(a, b, seatOwner, [1 - match.state.turn, match.state.turn]);
|
|
12498
|
+
}
|
|
12499
|
+
if (match.kind === "go") {
|
|
12500
|
+
if (match.state.passes > 0) return { by_seat: 1 - match.state.turn, type: "pass" };
|
|
12501
|
+
const [a, b] = match.state.history.slice(-2).map((k) => [...k].map(Number));
|
|
12502
|
+
return snapshotTurn(a, b, (p) => p - 1, [1 - match.state.turn]);
|
|
12503
|
+
}
|
|
12504
|
+
if ((match.kind === "amazons" || match.kind === "dots-boxes" || match.kind === "lines-of-action" || match.kind === "backgammon") && match.state.last) return match.state.last;
|
|
12505
|
+
return null;
|
|
12506
|
+
})();
|
|
12433
12507
|
const dotsLabeled = match?.kind === "dots-boxes" ? match.state.boxes.map((owner4, box) => {
|
|
12434
12508
|
const { size, edges } = match.state, sides2 = boxEdges(size, box), drawn = sides2.filter((e) => edges[e] >= 0).length;
|
|
12435
12509
|
const named = ["top", "bottom", "left", "right"].map((side2, i) => `${side2} ${sides2[i] + 1}${edges[sides2[i]] >= 0 ? "*" : ""}`).join(", ");
|
|
12436
12510
|
return `box row ${Math.floor(box / size) + 1} col ${box % size + 1} [${owner4 < 0 ? "." : "XO"[owner4]}]: ${named}${owner4 < 0 && drawn === 3 ? " (three sides: takeable)" : ""}`;
|
|
12437
12511
|
}) : null;
|
|
12512
|
+
const ludoLabeled = match?.kind === "ludo" ? match.state.pieces.map((row, seat) => `seat ${seat} (${room.players[seat]?.name ?? "empty"}): ${row.map((p, i) => `piece ${i + 1} ${p < 0 ? "yard" : p >= 40 ? `home lane ${p - 39}/4` : `progress ${p}, track square ${ludoTrack(seat, p, match.state.pieces.length)}`}`).join("; ")}`) : null;
|
|
12438
12513
|
const ginHand = match?.kind === "gin-rummy" && match.state.phase !== "finished" ? match.state.hands[room.seat]?.filter((c) => c !== null) ?? [] : [];
|
|
12439
12514
|
const ginHint = ginHand.length ? (() => {
|
|
12440
12515
|
const layout = ginLayout(ginHand), names = (cards) => cards.map((c) => `${cardLabel(c)} (id ${c})`);
|
|
@@ -12447,7 +12522,10 @@ function agentPosition(room) {
|
|
|
12447
12522
|
knock_limit: 10
|
|
12448
12523
|
};
|
|
12449
12524
|
})() : 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.`] : []) :
|
|
12525
|
+
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.`] : []) : match?.kind === "dice" ? match.state.sheets.map((sheet, seat) => {
|
|
12526
|
+
const upper = sheet.slice(0, 6).reduce((n, v) => n + (v ?? 0), 0);
|
|
12527
|
+
return `Seat ${seat} (${room.players[seat]?.name ?? "empty"}): upper section ${upper}/63${upper >= 63 ? ", 35 bonus earned" : ""}; total ${diceTotal(match.state, seat)}.`;
|
|
12528
|
+
}) : match?.kind === "polish-hearts" && match.state.phase !== "finished" ? [polishHandNote(match.state.round, match.state.hands.length)] : match?.kind === "three-check" ? [`Checks given: seat 0 ${match.state.checks[0]}, seat 1 ${match.state.checks[1]}; the third check wins.`] : [];
|
|
12451
12529
|
const sixtyUnseen = match?.kind === "sixty-six" && match.state.phase === "play" ? (() => {
|
|
12452
12530
|
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
12531
|
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})`);
|
|
@@ -12471,10 +12549,10 @@ function agentPosition(room) {
|
|
|
12471
12549
|
players: room.players.map((p, seat) => ({ seat, name: p.name, left: p.left, ...p.agent ? { agent: true } : {}, ...p.paused ? { paused: true } : {} })),
|
|
12472
12550
|
board_size: complete ? { rows, columns } : grid ? { rows: grid.rows, columns: grid.columns } : { rows: 0, columns: 0 },
|
|
12473
12551
|
board_top_to_bottom: board,
|
|
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,
|
|
12552
|
+
board_labeled: dotsLabeled ?? ludoLabeled ?? (match?.kind === "football" ? footballLabeled(match.state) : null) ?? (match?.kind === "oware" ? [...labeledBoard(room.game, board) ?? [], `captured: seat 0 ${match.state.scores[0]}, seat 1 ${match.state.scores[1]}`] : null) ?? labeledBoard(room.game, board) ?? grid?.labeled ?? null,
|
|
12475
12553
|
...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." } : {},
|
|
12476
12554
|
// Chess and its variants record the last move in SAN.
|
|
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 },
|
|
12555
|
+
last_move: (match?.kind === "chess" || match?.kind === "atomic-chess" || match?.kind === "three-check") && match.state.last ? { san: match.state.last } : checkerLast ? { by_seat: snapshotLast?.by_seat, from: square(checkerLast.from), to: square(checkerLast.to), ...snapshotLast && "captured" in snapshotLast && Array.isArray(snapshotLast.captured) ? { captured: snapshotLast.captured.map(square) } : {} } : snapshotLast ? snapshotLast : 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 },
|
|
12478
12556
|
...room.game === "chess" ? { fen } : {},
|
|
12479
12557
|
// The side to move is in check (chess family, xiangqi, shogi).
|
|
12480
12558
|
...match && room.phase === "playing" && (match.kind === "chess" ? chessPosition(match.state).inCheck() : "check" in match.state && match.state.check === true) ? { in_check: true } : {},
|
|
@@ -12510,13 +12588,29 @@ function labeledBoard(game, board) {
|
|
|
12510
12588
|
...board.map((row, r) => `${String(algebraic ? board.length - r : r + 1).padStart(rowWidth)} ${game === "hex" ? " ".repeat(Math.round(r * (cell + 1) / 2)) : ""}${[...row].map((p) => p.padStart(cell)).join(" ")}`)
|
|
12511
12589
|
];
|
|
12512
12590
|
}
|
|
12591
|
+
function moveRejection(position, input) {
|
|
12592
|
+
if (!position.turn?.yours) return "It is not your turn. ";
|
|
12593
|
+
if (input.from === void 0) return "";
|
|
12594
|
+
const wire = "legal_wire_actions" in position ? position.legal_wire_actions ?? [] : [];
|
|
12595
|
+
const moves = [...position.legal_actions, ...wire].filter((a) => a.type === "move" && String(a.from) === input.from);
|
|
12596
|
+
if (!moves.length) return `No legal move starts on ${input.from}. `;
|
|
12597
|
+
const exact = moves.filter((a) => String(a.to) === input.to);
|
|
12598
|
+
if (exact.length) return `${input.from} to ${input.to} is listed with promotion ${[...new Set(exact.map((a) => String(a.promotion ?? "none")))].join(" or ")}; pass --promotion exactly as listed. `;
|
|
12599
|
+
return `${input.from} to ${input.to} is not legal now; from ${input.from} the legal destinations are ${[...new Set(moves.map((a) => String(a.to)))].join(", ")}. `;
|
|
12600
|
+
}
|
|
12601
|
+
function polishHandNote(round, count) {
|
|
12602
|
+
const three = count === 3, total = 8 + count;
|
|
12603
|
+
const hands = [`no tricks: ${three ? 15 : 20} off per trick taken`, "no hearts: 20 off per heart", "no queens: 60 off per queen", "no jacks or kings: 30 off each", `no king of hearts: ${three ? 140 : 150} off`, `no seventh or last trick: ${three ? 70 : 75} off each`, "robber: every penalty of hands 1\u20136 at once"];
|
|
12604
|
+
const name = round < 7 ? hands[round] : round < 7 + count ? `trump contract chosen by the dealer: each trick taken scores +${three ? 20 : 25}` : "layout: build the four suit rows outward from the base rank (scoring as in the rules)";
|
|
12605
|
+
return `Hand ${round + 1} of ${total}: ${name}.`;
|
|
12606
|
+
}
|
|
12513
12607
|
function agentMove(position, input) {
|
|
12514
12608
|
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);
|
|
12515
12609
|
if (!action && input.from !== void 0 && input.to !== void 0 && "legal_wire_actions" in position) {
|
|
12516
12610
|
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));
|
|
12517
12611
|
if (wire) return wire;
|
|
12518
12612
|
}
|
|
12519
|
-
if (!action) throw Error(
|
|
12613
|
+
if (!action) throw Error(`${moveRejection(position, input)}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.`);
|
|
12520
12614
|
if (action.type === "swap") return { type: "swap" };
|
|
12521
12615
|
if (action.type === "edge") return { type: "place", at: action.edge - 1 };
|
|
12522
12616
|
if (action.type === "sow") return { type: "sow", at: action.pit - 1 };
|
|
@@ -12558,6 +12652,10 @@ function agentWireMove(position, json) {
|
|
|
12558
12652
|
if (!position.action_types.includes(action.type)) throw Error(`Choose a wire action type (${position.action_types.join(", ")}) from game rules, or pass an exact legal_actions entry.`);
|
|
12559
12653
|
if (position.phase !== "playing" || position.you.left || position.paused) throw Error("This match is not active for your seat.");
|
|
12560
12654
|
if (position.game === "go" && action.type === "accept" && action.scoreKey === void 0 && position.visible_state) action = { ...action, scoreKey: goProposalKey(position.visible_state) };
|
|
12655
|
+
if (position.game === "categories" && ["submit", "stop", "vote", "expire"].includes(action.type) && position.visible_state) {
|
|
12656
|
+
const state = position.visible_state;
|
|
12657
|
+
action = { ...action, at: action.at ?? state.round, scoreKey: action.scoreKey ?? state.roundKey ?? void 0, ...Array.isArray(action.text) ? { text: JSON.stringify(action.text) } : {} };
|
|
12658
|
+
}
|
|
12561
12659
|
return action;
|
|
12562
12660
|
}
|
|
12563
12661
|
function sameFields(a, b) {
|
|
@@ -12811,7 +12909,9 @@ function nextStep(position, event, brief = false, server) {
|
|
|
12811
12909
|
const room = `--room ${position.room}${server && server !== defaultServer ? ` --server ${server}` : ""}`, revision = position.revision, flag = brief ? " --brief" : "";
|
|
12812
12910
|
const wait = `leafarcade game wait ${room} --after-revision ${revision} --timeout ${defaultWaitSeconds}${flag}`;
|
|
12813
12911
|
if (position.you.left) return "You left this room. Create or join another room only if the human asks.";
|
|
12814
|
-
|
|
12912
|
+
const won = position.outcome?.winner;
|
|
12913
|
+
const result = won === void 0 || won === null ? position.phase === "finished" ? " Nobody won." : "" : won === position.you.seat ? " You won." : ` Seat ${won} (${position.players[won]?.name ?? "empty"}) won.`;
|
|
12914
|
+
if (position.phase === "finished") return `Match finished${position.outcome?.label ? ` (${position.outcome.label})` : ""}.${result} Tell the human the result. Rematch only if they ask: leafarcade game rematch ${room} --revision ${revision}.`;
|
|
12815
12915
|
if (position.paused) return `Paused. Stop and tell the human. Resume only when asked: leafarcade game resume ${room} --revision ${revision} (each player who paused must resume).`;
|
|
12816
12916
|
if (position.phase === "waiting") {
|
|
12817
12917
|
const host = position.you.seat === 0, seated = position.players.filter((p) => !p.left).length;
|
|
@@ -12935,7 +13035,7 @@ async function installSkill(agent = "codex", update = false) {
|
|
|
12935
13035
|
// packages/cli/package.json
|
|
12936
13036
|
var package_default = {
|
|
12937
13037
|
name: "@leafarcade/cli",
|
|
12938
|
-
version: "0.9.
|
|
13038
|
+
version: "0.9.1",
|
|
12939
13039
|
private: false,
|
|
12940
13040
|
description: "Create private browser board games and play with your AI agent. Includes 53 games and an installable Codex/Claude skill.",
|
|
12941
13041
|
type: "module",
|
|
@@ -13043,13 +13143,13 @@ var examples = {
|
|
|
13043
13143
|
amazons: `--action '{"type":"move","from":30,"to":33,"amount":53}'`,
|
|
13044
13144
|
"lines-of-action": `--action '{"type":"move","from":1,"to":17}'`,
|
|
13045
13145
|
"last-greenhouse": `--action '{"type":"share"}'`,
|
|
13046
|
-
euchre: "
|
|
13146
|
+
euchre: `--action '{"type":"trump","amount":2}'`,
|
|
13047
13147
|
cribbage: `--action '{"type":"discard","cards":[0,1]}'`,
|
|
13048
13148
|
fanorona: `--action '{"type":"move","from":13,"to":22,"amount":0}'`,
|
|
13049
|
-
categories: '{"type":"begin","promotion":"untimed"}'
|
|
13050
|
-
cheat: '{"type":"play","cards":[0]}'
|
|
13051
|
-
president: '{"type":"play","cards":[1]}'
|
|
13052
|
-
rummy: '{"type":"stock"}'
|
|
13149
|
+
categories: `--action '{"type":"begin","promotion":"untimed"}'`,
|
|
13150
|
+
cheat: `--action '{"type":"play","cards":[0]}'`,
|
|
13151
|
+
president: `--action '{"type":"play","cards":[1]}'`,
|
|
13152
|
+
rummy: `--action '{"type":"stock"}'`,
|
|
13053
13153
|
"connect-four": "--column 4",
|
|
13054
13154
|
chess: "--from e2 --to e4",
|
|
13055
13155
|
checkers: "--from b6 --to a5",
|
|
@@ -13712,6 +13812,27 @@ var scores = (position) => {
|
|
|
13712
13812
|
return Array.isArray(value) && value.length && value.every((n) => typeof n === "number") ? value : null;
|
|
13713
13813
|
};
|
|
13714
13814
|
var withoutLegal = (state) => Object.fromEntries(Object.entries(state).filter(([key2]) => key2 !== "legalActions"));
|
|
13815
|
+
function groupedLegal(list) {
|
|
13816
|
+
if (list.length <= 80 || !list.every((a) => a && typeof a === "object")) return null;
|
|
13817
|
+
const groups = /* @__PURE__ */ new Map();
|
|
13818
|
+
for (const action of list) {
|
|
13819
|
+
const keys = Object.keys(action), last = keys.at(-1);
|
|
13820
|
+
if (keys.length < 2 || last === "type") return null;
|
|
13821
|
+
const head = Object.fromEntries(keys.slice(0, -1).map((k) => [k, action[k]])), id = JSON.stringify(head) + last;
|
|
13822
|
+
const group = groups.get(id) ?? { ...head, [`${last}_choices`]: [] };
|
|
13823
|
+
group[`${last}_choices`].push(action[last]);
|
|
13824
|
+
groups.set(id, group);
|
|
13825
|
+
}
|
|
13826
|
+
return [...groups.values()].map((group) => {
|
|
13827
|
+
const [name, choices] = Object.entries(group).find(([k]) => k.endsWith("_choices"));
|
|
13828
|
+
const numbers = choices.every((n) => Number.isInteger(n)) ? choices.toSorted((a, b) => a - b) : null;
|
|
13829
|
+
if (!numbers || numbers.length < 20 || numbers.some((n, i) => i && n !== numbers[i - 1] + 1)) return group;
|
|
13830
|
+
const { [name]: _list, ...rest } = group;
|
|
13831
|
+
void _list;
|
|
13832
|
+
return { ...rest, [name.replace(/_choices$/, "_range")]: [numbers[0], numbers.at(-1)] };
|
|
13833
|
+
});
|
|
13834
|
+
}
|
|
13835
|
+
var groupNote = "Grouped list: each entry gives the choices for its last field (*_choices, or *_range [min, max] inclusive). Send one action with a single value, e.g. column 4, not the list.";
|
|
13715
13836
|
function briefPosition(position) {
|
|
13716
13837
|
const turn = position.turn ? position.turn.yours ? "yours" : seatName(position, position.turn.seat) : null;
|
|
13717
13838
|
const wire = "legal_wire_actions" in position ? position.legal_wire_actions ?? [] : null;
|
|
@@ -13734,7 +13855,8 @@ function briefPosition(position) {
|
|
|
13734
13855
|
board_labeled: position.board_labeled,
|
|
13735
13856
|
last_move: position.last_move,
|
|
13736
13857
|
..."changed_since_last_read" in position ? { changed_since_last_read: position.changed_since_last_read } : {},
|
|
13737
|
-
...wire ? { legal_wire_actions: wire, legal_wire_actions_complete: !!position.legal_wire_actions_complete } : { legal_actions: position.legal_actions, legal_actions_complete: position.legal_actions_complete },
|
|
13858
|
+
...wire ? { legal_wire_actions: groupedLegal(wire) ?? wire, legal_wire_actions_complete: !!position.legal_wire_actions_complete } : { legal_actions: groupedLegal(position.legal_actions) ?? position.legal_actions, legal_actions_complete: position.legal_actions_complete },
|
|
13859
|
+
...groupedLegal(wire ?? position.legal_actions) ? { legal_actions_grouped: groupNote } : {},
|
|
13738
13860
|
// Card, tile and other games without a labeled board still need their seat-visible state.
|
|
13739
13861
|
..."deadline_seconds_left" in position ? { deadline_seconds_left: position.deadline_seconds_left } : {},
|
|
13740
13862
|
..."board_legend" in position ? { board_legend: position.board_legend } : {},
|
|
@@ -13843,8 +13965,10 @@ try {
|
|
|
13843
13965
|
const goSize = integer(values["go-size"] ?? "9", "go-size", 9, 19);
|
|
13844
13966
|
if (goSize !== 9 && goSize !== 13 && goSize !== 19) throw Error("go-size must be 9, 13 or 19.");
|
|
13845
13967
|
result = await createRoom({ server, site: values.site ?? "https://leafarcade.com", game: values.game ?? "connect-four", name: values.name, capacity: values.capacity === void 0 ? void 0 : integer(values.capacity, "capacity", 2, 8), goSize, open: values.open, access: values.access, allowSpectators: !values["no-spectators"], brief: values.brief }, controller.signal);
|
|
13846
|
-
} else if (command === "rules")
|
|
13847
|
-
|
|
13968
|
+
} else if (command === "rules") {
|
|
13969
|
+
if (!values.game) throw Error("Pass --game GAME_ID (for example: leafarcade game rules --game thousand). leafarcade --help lists the game IDs.");
|
|
13970
|
+
result = agentRules(values.game);
|
|
13971
|
+
} else {
|
|
13848
13972
|
const server = serviceUrl(values.server ?? defaultServer);
|
|
13849
13973
|
if (command === "join" ? !positionals[1] : !values.room) throw Error(command === "join" ? 'Missing ROOMCODE. Usage: leafarcade join ROOMCODE --name "My agent"' : `Missing --room ROOMCODE. Usage: leafarcade game ${command} --room ROOMCODE. Saved rooms: leafarcade rooms.`);
|
|
13850
13974
|
const room = roomCode(command === "join" ? positionals[1] : values.room);
|
|
@@ -13918,6 +14042,8 @@ try {
|
|
|
13918
14042
|
const row = values.row === void 0 ? void 0 : integer(values.row, "row", 1, position.board_size.rows);
|
|
13919
14043
|
if (revision !== position.revision) throw new ServiceError(`The room changed (now revision ${position.revision}). Read game status before playing.`, "STALE_REVISION");
|
|
13920
14044
|
if (values.action && [values.column, values.row, values.from, values.to, values.promotion, values.edge, values.pit, values.swap].some((v) => v !== void 0)) throw Error("Use --action or convenience flags, not both.");
|
|
14045
|
+
const grouped = values.action?.match(/"(\w+)_(?:choices|range)"/);
|
|
14046
|
+
if (grouped) throw Error(`That entry is from a grouped list: replace the list with "${grouped[1]}" and one value from it.`);
|
|
13921
14047
|
const action = values.action ? agentWireMove(position, values.action) : agentMove(position, { column, row, from: values.from, to: values.to, promotion: values.promotion, swap: values.swap, edge: values.edge === void 0 ? void 0 : integer(values.edge, "edge", 1, 1e3), pit: values.pit === void 0 ? void 0 : integer(values.pit, "pit", 1, 12) });
|
|
13922
14048
|
const waitSeconds = integer(values.timeout ?? String(defaultWaitSeconds), "timeout", 1, 3600);
|
|
13923
14049
|
await rpc(session, "play", { revision, action }, false, controller.signal);
|
package/package.json
CHANGED
|
@@ -24,7 +24,7 @@ leafarcade game create --game connect-four --name "My agent" # add --open on
|
|
|
24
24
|
|
|
25
25
|
Every JSON response has a `next` field with the exact command to run next. Follow it.
|
|
26
26
|
|
|
27
|
-
Add `--brief` to `status`, `wait`, `start` and `play`. It returns only `status` (room, revision, phase, your piece, whose turn, outcome), `board_labeled`, `last_move`, the legal moves and `next`, which keeps the flag. Games without a labeled board (cards, tiles) also get `visible_state`. Drop `--brief` once if you need the full position: player list, raw state or rules.
|
|
27
|
+
Add `--brief` to `status`, `wait`, `start` and `play`. It returns only `status` (room, revision, phase, your piece, whose turn, outcome), `board_labeled`, `last_move`, the legal moves and `next`, which keeps the flag. Games without a labeled board (cards, tiles) also get `visible_state`. Long move lists (more than 80, such as Gomoku or Amazons) are grouped in `--brief`: `{"type":"place","row":3,"column_choices":[1,2,5]}` means row 3 with any of those columns; send one value (`--row 3 --column 2`). Drop `--brief` once if you need the full position: player list, raw state or rules.
|
|
28
28
|
|
|
29
29
|
`leafarcade help games` lists game IDs. `--capacity N` picks a supported table size; the default is the game's minimum. Each additional agent needs its own `LEAFARCADE_CONFIG_DIR` and seat.
|
|
30
30
|
|
|
@@ -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
|
|
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 latest move in board games with `by_seat` and any `captured` squares (null in most card games; read `changed_since_last_read` there). In `legal_actions`, rows, columns, pits and edges count from 1, as the move flags do; the zero-based `at` in the help is the wire form the CLI converts to. Some games add hints: `rule_notes` (rules in force now, such as Thousand's 800 rule, three-check counts or the Dice upper bonus), `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.
|