@leafarcade/cli 0.8.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 +488 -313
- package/package.json +1 -1
- package/skills/leafarcade/SKILL.md +2 -2
package/dist/cli.mjs
CHANGED
|
@@ -3754,6 +3754,21 @@ 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
|
+
}
|
|
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
|
+
}
|
|
3757
3772
|
|
|
3758
3773
|
// lib/random.ts
|
|
3759
3774
|
function randomStep(seed) {
|
|
@@ -3792,6 +3807,261 @@ function best(scores2) {
|
|
|
3792
3807
|
return winners.length === 1 ? winners[0] : null;
|
|
3793
3808
|
}
|
|
3794
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
|
+
|
|
3795
4065
|
// lib/multiplayer/mills.ts
|
|
3796
4066
|
var millLines = [
|
|
3797
4067
|
[0, 1, 2],
|
|
@@ -4271,13 +4541,22 @@ function createGin(entropy2) {
|
|
|
4271
4541
|
}
|
|
4272
4542
|
function playGin(s, type, at, entropy2) {
|
|
4273
4543
|
if (type === "next" && s.phase === "round")
|
|
4274
|
-
return
|
|
4275
|
-
|
|
4276
|
-
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4544
|
+
return {
|
|
4545
|
+
...deal2(
|
|
4546
|
+
s.result.startsWith("Stock") ? s.dealer : 1 - s.dealer,
|
|
4547
|
+
s.scores,
|
|
4548
|
+
s.wins,
|
|
4549
|
+
s.round + 1,
|
|
4550
|
+
entropy2
|
|
4551
|
+
),
|
|
4552
|
+
previous: [
|
|
4553
|
+
s.result,
|
|
4554
|
+
...s.hands.map((h, i) => {
|
|
4555
|
+
const layout = ginLayout(h);
|
|
4556
|
+
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}).`;
|
|
4557
|
+
})
|
|
4558
|
+
].join(" ")
|
|
4559
|
+
};
|
|
4281
4560
|
if (["finished", "round"].includes(s.phase))
|
|
4282
4561
|
throw Error("This round is finished.");
|
|
4283
4562
|
const t = structuredClone(s), seat = s.turn;
|
|
@@ -4396,7 +4675,7 @@ var multiplayerGames = [
|
|
|
4396
4675
|
symbol: "1000",
|
|
4397
4676
|
players: "2\u20134 players",
|
|
4398
4677
|
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.
|
|
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."
|
|
4400
4679
|
},
|
|
4401
4680
|
{
|
|
4402
4681
|
id: "binokel",
|
|
@@ -4516,7 +4795,7 @@ var multiplayerGames = [
|
|
|
4516
4795
|
symbol: "\u2654",
|
|
4517
4796
|
players: "2 players",
|
|
4518
4797
|
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."
|
|
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."
|
|
4520
4799
|
},
|
|
4521
4800
|
{
|
|
4522
4801
|
id: "spades",
|
|
@@ -4628,7 +4907,7 @@ var multiplayerGames = [
|
|
|
4628
4907
|
symbol: "\u25CC",
|
|
4629
4908
|
players: "2 players",
|
|
4630
4909
|
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."
|
|
4910
|
+
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
4911
|
},
|
|
4633
4912
|
{
|
|
4634
4913
|
id: "mills",
|
|
@@ -4991,107 +5270,6 @@ var action_catalog_default = {
|
|
|
4991
5270
|
]
|
|
4992
5271
|
};
|
|
4993
5272
|
|
|
4994
|
-
// lib/multiplayer/dice.ts
|
|
4995
|
-
var diceCategories = [
|
|
4996
|
-
"Ones",
|
|
4997
|
-
"Twos",
|
|
4998
|
-
"Threes",
|
|
4999
|
-
"Fours",
|
|
5000
|
-
"Fives",
|
|
5001
|
-
"Sixes",
|
|
5002
|
-
"Three of a kind",
|
|
5003
|
-
"Four of a kind",
|
|
5004
|
-
"Full house",
|
|
5005
|
-
"Small straight",
|
|
5006
|
-
"Large straight",
|
|
5007
|
-
"Five of a kind",
|
|
5008
|
-
"Chance"
|
|
5009
|
-
];
|
|
5010
|
-
function createDice(count) {
|
|
5011
|
-
if (!index(count - 2, 3)) throw Error("Choose two to four players.");
|
|
5012
|
-
return {
|
|
5013
|
-
dice: [0, 0, 0, 0, 0],
|
|
5014
|
-
held: Array(5).fill(false),
|
|
5015
|
-
rolls: 0,
|
|
5016
|
-
sheets: Array.from({ length: count }, () => Array(13).fill(null)),
|
|
5017
|
-
bonuses: Array(count).fill(0),
|
|
5018
|
-
turn: 0,
|
|
5019
|
-
finished: false,
|
|
5020
|
-
winner: null
|
|
5021
|
-
};
|
|
5022
|
-
}
|
|
5023
|
-
function diceScore(dice, category, joker = false) {
|
|
5024
|
-
const counts = Array.from(
|
|
5025
|
-
{ length: 6 },
|
|
5026
|
-
(_, i) => dice.filter((v) => v === i + 1).length
|
|
5027
|
-
), sum = dice.reduce((a, b) => a + b, 0);
|
|
5028
|
-
if (category < 6) return counts[category] * (category + 1);
|
|
5029
|
-
switch (category) {
|
|
5030
|
-
case 6:
|
|
5031
|
-
return Math.max(...counts) >= 3 ? sum : 0;
|
|
5032
|
-
case 7:
|
|
5033
|
-
return Math.max(...counts) >= 4 ? sum : 0;
|
|
5034
|
-
case 8:
|
|
5035
|
-
return joker || counts.includes(3) && counts.includes(2) ? 25 : 0;
|
|
5036
|
-
case 9:
|
|
5037
|
-
return joker || [0, 1, 2].some((start) => counts.slice(start, start + 4).every(Boolean)) ? 30 : 0;
|
|
5038
|
-
case 10:
|
|
5039
|
-
return joker || [0, 1].some((start) => counts.slice(start, start + 5).every(Boolean)) ? 40 : 0;
|
|
5040
|
-
case 11:
|
|
5041
|
-
return counts.includes(5) ? 50 : 0;
|
|
5042
|
-
case 12:
|
|
5043
|
-
return sum;
|
|
5044
|
-
default:
|
|
5045
|
-
throw Error("Choose a score category.");
|
|
5046
|
-
}
|
|
5047
|
-
}
|
|
5048
|
-
function diceTotal(s, seat) {
|
|
5049
|
-
const sheet = s.sheets[seat], upper = sheet.slice(0, 6).reduce((sum, n) => sum + (n ?? 0), 0);
|
|
5050
|
-
return sheet.reduce((sum, n) => sum + (n ?? 0), 0) + (upper >= 63 ? 35 : 0) + s.bonuses[seat];
|
|
5051
|
-
}
|
|
5052
|
-
function playDice(s, type, at, entropy2) {
|
|
5053
|
-
if (s.finished) throw Error("This match is finished.");
|
|
5054
|
-
if (type === "roll") {
|
|
5055
|
-
if (s.rolls >= 3) throw Error("Record a score after three rolls.");
|
|
5056
|
-
const random = draws(entropy2, 5);
|
|
5057
|
-
return {
|
|
5058
|
-
...s,
|
|
5059
|
-
rolls: s.rolls + 1,
|
|
5060
|
-
dice: s.dice.map(
|
|
5061
|
-
(v, i) => s.rolls && s.held[i] ? v : 1 + Math.floor(random[i] * 6)
|
|
5062
|
-
)
|
|
5063
|
-
};
|
|
5064
|
-
}
|
|
5065
|
-
if (at === void 0 || !index(at, type === "hold" ? 5 : 13) || !s.rolls)
|
|
5066
|
-
throw Error("Roll first, then choose a die or score category.");
|
|
5067
|
-
if (type === "hold")
|
|
5068
|
-
return { ...s, held: s.held.map((v, i) => i === at ? !v : v) };
|
|
5069
|
-
if (type !== "score" || s.sheets[s.turn][at] !== null)
|
|
5070
|
-
throw Error("Choose an unused score category.");
|
|
5071
|
-
const sheets = s.sheets.map((row) => [...row]), bonuses = [...s.bonuses];
|
|
5072
|
-
const five = s.dice.every((v) => v === s.dice[0]), upper = s.dice[0] - 1;
|
|
5073
|
-
const joker = five && sheets[s.turn][11] !== null && sheets[s.turn][upper] !== null;
|
|
5074
|
-
if (joker && sheets[s.turn].slice(6).some((n) => n === null) && at < 6)
|
|
5075
|
-
throw Error(
|
|
5076
|
-
"Use the five-of-a-kind joker in an unused lower category first."
|
|
5077
|
-
);
|
|
5078
|
-
sheets[s.turn][at] = diceScore(s.dice, at, joker);
|
|
5079
|
-
if (joker && sheets[s.turn][11] === 50) bonuses[s.turn] += 100;
|
|
5080
|
-
const next3 = {
|
|
5081
|
-
...s,
|
|
5082
|
-
sheets,
|
|
5083
|
-
bonuses,
|
|
5084
|
-
turn: (s.turn + 1) % s.sheets.length,
|
|
5085
|
-
rolls: 0,
|
|
5086
|
-
held: Array(5).fill(false),
|
|
5087
|
-
dice: [0, 0, 0, 0, 0]
|
|
5088
|
-
};
|
|
5089
|
-
next3.finished = sheets.every((row) => row.every((n) => n !== null));
|
|
5090
|
-
if (next3.finished)
|
|
5091
|
-
next3.winner = best(sheets.map((_, i) => diceTotal(next3, i)));
|
|
5092
|
-
return next3;
|
|
5093
|
-
}
|
|
5094
|
-
|
|
5095
5273
|
// lib/agents/action-help.ts
|
|
5096
5274
|
var help = {
|
|
5097
5275
|
amazons: "move: numeric from/to 0\u201399 and amount for the arrow square. Select an exact legal_wire_actions entry.",
|
|
@@ -5100,49 +5278,49 @@ var help = {
|
|
|
5100
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.",
|
|
5101
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.",
|
|
5102
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.",
|
|
5103
|
-
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"]}.`,
|
|
5104
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.",
|
|
5105
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.",
|
|
5106
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.",
|
|
5107
|
-
"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.',
|
|
5108
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.",
|
|
5109
5287
|
chess: "move: from/to are algebraic squares; promotion is q/r/b/n.",
|
|
5110
5288
|
"atomic-chess": "move: from/to are algebraic squares; promotion is q/r/b/n. Explosions follow the game rules.",
|
|
5111
5289
|
"three-check": "move: from/to are algebraic squares; promotion is q/r/b/n. Check counts are in visible_state.",
|
|
5112
|
-
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.',
|
|
5113
5291
|
"brazilian-checkers": "move: from/to are zero-based squares (0\u201363); maximum captures apply.",
|
|
5114
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.",
|
|
5115
|
-
reversi: "
|
|
5116
|
-
gomoku: "
|
|
5117
|
-
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.",
|
|
5118
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.',
|
|
5119
|
-
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).',
|
|
5120
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.",
|
|
5121
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.",
|
|
5122
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.",
|
|
5123
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.`,
|
|
5124
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.",
|
|
5125
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.",
|
|
5126
|
-
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.",
|
|
5127
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.",
|
|
5128
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.",
|
|
5129
5307
|
xiangqi: "move: numeric zero-based row-major from/to on the 9\xD710 board. promotion is unused.",
|
|
5130
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.",
|
|
5131
|
-
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.",
|
|
5132
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.",
|
|
5133
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.",
|
|
5134
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.",
|
|
5135
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.",
|
|
5136
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.",
|
|
5137
|
-
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.",
|
|
5138
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.",
|
|
5139
|
-
"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.",
|
|
5140
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.",
|
|
5141
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.",
|
|
5142
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).",
|
|
5143
|
-
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.",
|
|
5144
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).",
|
|
5145
|
-
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.",
|
|
5146
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.",
|
|
5147
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.",
|
|
5148
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.",
|
|
@@ -5226,20 +5404,23 @@ function canastaCount(melds) {
|
|
|
5226
5404
|
return melds.filter((m) => m.cards.length >= 7).length;
|
|
5227
5405
|
}
|
|
5228
5406
|
function finish2(t, out2, concealed = false) {
|
|
5407
|
+
const parts = [];
|
|
5229
5408
|
for (let i = 0; i < 2; i++) {
|
|
5230
|
-
const meld2 = t.melds[i], red = t.red[i].length,
|
|
5409
|
+
const meld2 = t.melds[i], red = t.red[i].length, canastas = meld2.reduce(
|
|
5231
5410
|
(n, m) => n + (m.cards.length >= 7 ? m.cards.some(canastaWild) ? 300 : 500 : 0),
|
|
5232
5411
|
0
|
|
5233
|
-
)
|
|
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(
|
|
5234
5413
|
(n, h, seat) => n + (team(t, seat) === i ? h.reduce((n2, c) => n2 + canastaValue(c), 0) : 0),
|
|
5235
5414
|
0
|
|
5236
5415
|
);
|
|
5237
|
-
|
|
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}`);
|
|
5238
5419
|
}
|
|
5239
5420
|
t.phase = t.scores.some((n) => n >= 5e3) ? "finished" : "round";
|
|
5240
5421
|
t.winner = t.phase === "finished" ? best(t.scores) : null;
|
|
5241
5422
|
t.turn = t.dealer;
|
|
5242
|
-
t.result = `Hand complete. Scores ${t.scores.join(" / ")}.`;
|
|
5423
|
+
t.result = `Hand complete${out2 === null ? " (stock exhausted)" : ""}. ${parts.join("; ")}. Scores ${t.scores.join(" / ")}.`;
|
|
5243
5424
|
}
|
|
5244
5425
|
function out(t, seat) {
|
|
5245
5426
|
if (t.hands[seat].length) return;
|
|
@@ -5726,7 +5907,7 @@ function award(s) {
|
|
|
5726
5907
|
}
|
|
5727
5908
|
return s;
|
|
5728
5909
|
}
|
|
5729
|
-
function
|
|
5910
|
+
function advance2(s) {
|
|
5730
5911
|
const live = s.players.filter((p) => !p.folded && !p.out);
|
|
5731
5912
|
if (live.length <= 1) return award(s);
|
|
5732
5913
|
if (s.pending.length) {
|
|
@@ -5752,7 +5933,7 @@ function advance(s) {
|
|
|
5752
5933
|
s.pending = s.players.flatMap((p, i) => canAct(p) ? [i] : []);
|
|
5753
5934
|
if (s.pending.length <= 1) {
|
|
5754
5935
|
s.pending = [];
|
|
5755
|
-
return
|
|
5936
|
+
return advance2(s);
|
|
5756
5937
|
}
|
|
5757
5938
|
s.turn = nextSeat(s, s.button, canAct);
|
|
5758
5939
|
return s;
|
|
@@ -5801,7 +5982,7 @@ function begin(s, seed) {
|
|
|
5801
5982
|
s.turn = nextSeat(s, big, canAct);
|
|
5802
5983
|
if (!s.pending.length || s.pending.length === 1 && s.players[s.pending[0]].street >= Math.max(...s.players.map((p) => p.street))) {
|
|
5803
5984
|
s.pending = [];
|
|
5804
|
-
return
|
|
5985
|
+
return advance2(s);
|
|
5805
5986
|
}
|
|
5806
5987
|
return s;
|
|
5807
5988
|
}
|
|
@@ -5896,7 +6077,7 @@ function pokerAction(previous, seat, action, seed) {
|
|
|
5896
6077
|
s.actedAt ??= s.players.map((player) => player.street);
|
|
5897
6078
|
s.actedAt[seat] = s.bet;
|
|
5898
6079
|
s.log = [...s.log, `Player ${seat + 1} ${label}`].slice(-80);
|
|
5899
|
-
return
|
|
6080
|
+
return advance2(s);
|
|
5900
6081
|
}
|
|
5901
6082
|
function leavePoker(previous, seat) {
|
|
5902
6083
|
const s = structuredClone(previous);
|
|
@@ -5914,7 +6095,7 @@ function leavePoker(previous, seat) {
|
|
|
5914
6095
|
return s;
|
|
5915
6096
|
}
|
|
5916
6097
|
if (s.turn === seat || s.players.filter((p) => !p.folded && !p.out).length <= 1)
|
|
5917
|
-
return
|
|
6098
|
+
return advance2(s);
|
|
5918
6099
|
return s;
|
|
5919
6100
|
}
|
|
5920
6101
|
function pokerView(s, seat) {
|
|
@@ -7703,7 +7884,8 @@ function moveVariant(state, from, to, promotion = "q") {
|
|
|
7703
7884
|
if (!parseUci(from + to)) throw Error("Choose a legal move.");
|
|
7704
7885
|
const move2 = variantMoves(state).find((m) => m.from === from && m.to === to && (!m.promotion || m.promotion === promotion));
|
|
7705
7886
|
if (!move2) throw Error("Choose a legal move.");
|
|
7706
|
-
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;
|
|
7707
7889
|
const fen = makeFen(setup), history = [...state.history, repetitionKey(fen)];
|
|
7708
7890
|
const outcome = pos.outcome();
|
|
7709
7891
|
const repeated = history.filter((key2) => key2 === repetitionKey(fen)).length >= 3;
|
|
@@ -8299,13 +8481,14 @@ function playBackgammon(s, type, from, to, die, entropy2) {
|
|
|
8299
8481
|
opening: false,
|
|
8300
8482
|
rolled,
|
|
8301
8483
|
dice: rolled[0] === rolled[1] ? Array(4).fill(rolled[0]) : rolled,
|
|
8302
|
-
result: `
|
|
8484
|
+
result: `Player ${turn + 1} rolled ${rolled.join(" and ")}.`,
|
|
8485
|
+
last: { by_seat: turn, rolled, moves: [] }
|
|
8303
8486
|
};
|
|
8304
8487
|
return backgammonMoves(next4).length ? next4 : {
|
|
8305
8488
|
...next4,
|
|
8306
8489
|
dice: [],
|
|
8307
8490
|
turn: turn === 0 ? 1 : 0,
|
|
8308
|
-
result: "
|
|
8491
|
+
result: `Player ${turn + 1} rolled ${rolled.join(" and ")}: all moves are blocked. Turn passes.`
|
|
8309
8492
|
};
|
|
8310
8493
|
}
|
|
8311
8494
|
const move2 = backgammonMoves(s).find(
|
|
@@ -8316,6 +8499,7 @@ function playBackgammon(s, type, from, to, die, entropy2) {
|
|
|
8316
8499
|
"Use the maximum number of dice; if only one can be used, use the higher die."
|
|
8317
8500
|
);
|
|
8318
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 } : {} }] };
|
|
8319
8503
|
if (next3.off[s.turn] === 15) {
|
|
8320
8504
|
const opponent = 1 - s.turn, backgammon = next3.bar[opponent] > 0 || next3.points.some(
|
|
8321
8505
|
(n, i) => (s.turn === 0 ? i >= 18 : i < 6) && n * (s.turn === 0 ? -1 : 1) > 0
|
|
@@ -8323,147 +8507,13 @@ function playBackgammon(s, type, from, to, die, entropy2) {
|
|
|
8323
8507
|
next3.winner = s.turn;
|
|
8324
8508
|
next3.result = next3.off[opponent] ? "Single game." : backgammon ? "Backgammon \xB7 3 points." : "Gammon \xB7 2 points.";
|
|
8325
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.`;
|
|
8326
8511
|
next3.dice = [];
|
|
8327
8512
|
next3.turn = s.turn === 0 ? 1 : 0;
|
|
8328
8513
|
}
|
|
8329
8514
|
return next3;
|
|
8330
8515
|
}
|
|
8331
8516
|
|
|
8332
|
-
// lib/multiplayer/football.ts
|
|
8333
|
-
var footballPoint = (at) => [at % 9, Math.floor(at / 9)];
|
|
8334
|
-
var footballEdge = (a, b) => a < b ? `${a}:${b}` : `${b}:${a}`;
|
|
8335
|
-
var valid = (at) => index(at, 117) && (Math.floor(at / 9) > 0 && Math.floor(at / 9) < 12 || at % 9 >= 3 && at % 9 <= 5);
|
|
8336
|
-
var boundary = (at) => {
|
|
8337
|
-
const [x, y] = footballPoint(at);
|
|
8338
|
-
return x === 0 || x === 8 || y === 0 || y === 12 || (y === 1 || y === 11) && x !== 4;
|
|
8339
|
-
};
|
|
8340
|
-
function createFootball() {
|
|
8341
|
-
const edges = [];
|
|
8342
|
-
for (let y = 1; y <= 11; y++)
|
|
8343
|
-
for (const x of [0, 8])
|
|
8344
|
-
if (y < 11) edges.push(footballEdge(y * 9 + x, (y + 1) * 9 + x));
|
|
8345
|
-
for (const y of [1, 11])
|
|
8346
|
-
for (let x = 0; x < 8; x++)
|
|
8347
|
-
if (x < 3 || x >= 5) edges.push(footballEdge(y * 9 + x, y * 9 + x + 1));
|
|
8348
|
-
for (const y of [0, 12]) {
|
|
8349
|
-
edges.push(
|
|
8350
|
-
footballEdge(y * 9 + 3, y * 9 + 4),
|
|
8351
|
-
footballEdge(y * 9 + 4, y * 9 + 5)
|
|
8352
|
-
);
|
|
8353
|
-
for (const x of [3, 5])
|
|
8354
|
-
edges.push(footballEdge(y * 9 + x, (y === 0 ? 1 : 11) * 9 + x));
|
|
8355
|
-
}
|
|
8356
|
-
return { ball: 58, edges, turn: 0, winner: null, last: null };
|
|
8357
|
-
}
|
|
8358
|
-
function footballMoves(s) {
|
|
8359
|
-
if (s.winner !== null) return [];
|
|
8360
|
-
const [x, y] = footballPoint(s.ball), moves = [];
|
|
8361
|
-
for (let dy = -1; dy <= 1; dy++)
|
|
8362
|
-
for (let dx = -1; dx <= 1; dx++) {
|
|
8363
|
-
const nx = x + dx, ny = y + dy, at = ny * 9 + nx;
|
|
8364
|
-
if ((ny === 0 || ny === 12) && (x < 3 || x > 5)) continue;
|
|
8365
|
-
if ((dx || dy) && nx >= 0 && nx <= 8 && ny >= 0 && ny <= 12 && valid(at) && !s.edges.includes(footballEdge(s.ball, at)))
|
|
8366
|
-
moves.push(at);
|
|
8367
|
-
}
|
|
8368
|
-
return moves;
|
|
8369
|
-
}
|
|
8370
|
-
function playFootball(s, at) {
|
|
8371
|
-
if (!footballMoves(s).includes(at))
|
|
8372
|
-
throw Error("Choose an adjacent point along an unused line.");
|
|
8373
|
-
const bounced = boundary(at) || s.edges.some((edge) => edge.split(":").map(Number).includes(at));
|
|
8374
|
-
const y = Math.floor(at / 9);
|
|
8375
|
-
const next3 = {
|
|
8376
|
-
...s,
|
|
8377
|
-
ball: at,
|
|
8378
|
-
edges: [...s.edges, footballEdge(s.ball, at)],
|
|
8379
|
-
turn: bounced ? s.turn : s.turn === 0 ? 1 : 0,
|
|
8380
|
-
winner: y === 0 ? 0 : y === 12 ? 1 : null,
|
|
8381
|
-
last: [s.ball, at]
|
|
8382
|
-
};
|
|
8383
|
-
if (next3.winner === null && !footballMoves(next3).length)
|
|
8384
|
-
next3.winner = 1 - next3.turn;
|
|
8385
|
-
return next3;
|
|
8386
|
-
}
|
|
8387
|
-
|
|
8388
|
-
// lib/multiplayer/ludo.ts
|
|
8389
|
-
function createLudo(count) {
|
|
8390
|
-
if (!index(count - 2, 3)) throw Error("Choose two to four players.");
|
|
8391
|
-
return {
|
|
8392
|
-
pieces: Array.from({ length: count }, () => [-1, -1, -1, -1]),
|
|
8393
|
-
turn: 0,
|
|
8394
|
-
die: 0,
|
|
8395
|
-
streak: 0,
|
|
8396
|
-
attempts: 0,
|
|
8397
|
-
winner: null,
|
|
8398
|
-
last: "Roll a six to leave your yard."
|
|
8399
|
-
};
|
|
8400
|
-
}
|
|
8401
|
-
var ludoTrack = (seat, progress, count = 4) => (seat * (count === 2 ? 20 : 10) + progress) % 40;
|
|
8402
|
-
function ludoMoves(s) {
|
|
8403
|
-
if (!s.die || s.winner !== null) return [];
|
|
8404
|
-
return s.pieces[s.turn].flatMap((p, i) => {
|
|
8405
|
-
const to = p < 0 ? s.die === 6 ? 0 : -1 : p + s.die;
|
|
8406
|
-
return to >= 0 && to <= 43 && !s.pieces[s.turn].some((other, j) => j !== i && other === to) ? [i] : [];
|
|
8407
|
-
});
|
|
8408
|
-
}
|
|
8409
|
-
function advance2(s) {
|
|
8410
|
-
return {
|
|
8411
|
-
...s,
|
|
8412
|
-
turn: (s.turn + 1) % s.pieces.length,
|
|
8413
|
-
die: 0,
|
|
8414
|
-
streak: 0,
|
|
8415
|
-
attempts: 0
|
|
8416
|
-
};
|
|
8417
|
-
}
|
|
8418
|
-
function playLudo(s, type, at, entropy2) {
|
|
8419
|
-
if (s.winner !== null) throw Error("This match is finished.");
|
|
8420
|
-
if (type === "roll") {
|
|
8421
|
-
if (s.die) throw Error("Move a piece before rolling again.");
|
|
8422
|
-
const die = 1 + Math.floor(draws(entropy2, 1)[0] * 6), streak = die === 6 ? s.streak + 1 : 0;
|
|
8423
|
-
let next4 = {
|
|
8424
|
-
...s,
|
|
8425
|
-
die,
|
|
8426
|
-
streak,
|
|
8427
|
-
attempts: s.attempts + 1,
|
|
8428
|
-
last: `Player ${s.turn + 1} rolled ${die}.`
|
|
8429
|
-
};
|
|
8430
|
-
if (streak > 3)
|
|
8431
|
-
return advance2({
|
|
8432
|
-
...next4,
|
|
8433
|
-
last: "Three extra moves is the limit. Turn passes."
|
|
8434
|
-
});
|
|
8435
|
-
if (!ludoMoves(next4).length) {
|
|
8436
|
-
const allHome = s.pieces[s.turn].every((p) => p === -1);
|
|
8437
|
-
next4 = allHome && next4.attempts < 3 || die === 6 ? { ...next4, die: 0 } : advance2(next4);
|
|
8438
|
-
}
|
|
8439
|
-
return next4;
|
|
8440
|
-
}
|
|
8441
|
-
if (type !== "move" || at === void 0 || !index(at, 4) || !ludoMoves(s).includes(at))
|
|
8442
|
-
throw Error(
|
|
8443
|
-
"Choose a highlighted piece. You need an exact roll to reach home."
|
|
8444
|
-
);
|
|
8445
|
-
const pieces = s.pieces.map((row) => [...row]), old = pieces[s.turn][at], to = old < 0 ? 0 : old + s.die;
|
|
8446
|
-
pieces[s.turn][at] = to;
|
|
8447
|
-
if (to < 40) {
|
|
8448
|
-
for (let seat = 0; seat < pieces.length; seat++)
|
|
8449
|
-
if (seat !== s.turn) {
|
|
8450
|
-
pieces[seat] = pieces[seat].map(
|
|
8451
|
-
(p) => p >= 0 && p < 40 && ludoTrack(seat, p, pieces.length) === ludoTrack(s.turn, to, pieces.length) ? -1 : p
|
|
8452
|
-
);
|
|
8453
|
-
}
|
|
8454
|
-
}
|
|
8455
|
-
const won = pieces[s.turn].every((p) => p >= 40);
|
|
8456
|
-
const next3 = {
|
|
8457
|
-
...s,
|
|
8458
|
-
pieces,
|
|
8459
|
-
die: 0,
|
|
8460
|
-
attempts: 0,
|
|
8461
|
-
winner: won ? s.turn : null,
|
|
8462
|
-
last: `Player ${s.turn + 1} moved piece ${at + 1}.`
|
|
8463
|
-
};
|
|
8464
|
-
return s.die === 6 && s.streak < 3 || won ? next3 : advance2(next3);
|
|
8465
|
-
}
|
|
8466
|
-
|
|
8467
8517
|
// lib/multiplayer/go.ts
|
|
8468
8518
|
function createGo(size = 9) {
|
|
8469
8519
|
const board = Array(size * size).fill(0);
|
|
@@ -8708,7 +8758,7 @@ var adapter5 = defineMatchAdapter({
|
|
|
8708
8758
|
return {
|
|
8709
8759
|
finished: state.winner !== null || state.draw,
|
|
8710
8760
|
winner: state.winner,
|
|
8711
|
-
label: state.draw ?
|
|
8761
|
+
label: state.draw ? drawReason(state.history) : state.winner !== null ? `Player ${state.winner + 1} wins \xB7 the opponent has no legal move.` : ""
|
|
8712
8762
|
};
|
|
8713
8763
|
},
|
|
8714
8764
|
apply(state, _seat, action, _entropy, _now) {
|
|
@@ -8726,7 +8776,7 @@ var adapter6 = defineMatchAdapter({
|
|
|
8726
8776
|
actions: ["move", "sequence"],
|
|
8727
8777
|
create: () => createInternational(),
|
|
8728
8778
|
turn: (s) => s.turn,
|
|
8729
|
-
outcome: (s) => ({ finished: s.winner !== null || s.draw, winner: s.winner, label: s.draw ?
|
|
8779
|
+
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
8780
|
apply(s, _seat, a) {
|
|
8731
8781
|
if (a.type === "move" && typeof a.from === "number" && typeof a.to === "number") return moveInternational(s, a.from, a.to);
|
|
8732
8782
|
if (a.type === "sequence" && Array.isArray(a.cards) && a.cards.length >= 2 && a.cards.length <= 21) {
|
|
@@ -9061,6 +9111,7 @@ function playThousand(s, a, entropy2) {
|
|
|
9061
9111
|
if (a.type === "next" && s.phase === "round") {
|
|
9062
9112
|
t.round++;
|
|
9063
9113
|
t.dealer = (s.dealer + 1) % s.hands.length;
|
|
9114
|
+
t.previous = s.result;
|
|
9064
9115
|
return deal6(t, entropy2);
|
|
9065
9116
|
}
|
|
9066
9117
|
if (a.type === "redeal" && s.phase === "auction" && s.hands[seat].filter((c) => cardRank(c) === 9).length === 4)
|
|
@@ -9107,9 +9158,9 @@ function playThousand(s, a, entropy2) {
|
|
|
9107
9158
|
return t;
|
|
9108
9159
|
}
|
|
9109
9160
|
if (a.type === "bomb" && ["kitty", "exchange", "declare"].includes(s.phase)) {
|
|
9110
|
-
|
|
9111
|
-
|
|
9112
|
-
t.result =
|
|
9161
|
+
const penalty2 = t.bombs[seat]++ > 0 ? t.hands.length === 4 ? 40 : 60 : 0;
|
|
9162
|
+
t.players.filter((i) => i !== seat && t.scores[i] < 800).forEach((i) => t.scores[i] += penalty2);
|
|
9163
|
+
t.result = `Player ${seat + 1} cancels the hand with a bomb${penalty2 ? `; the other players score ${penalty2}` : " (the first bomb is free)"}.`;
|
|
9113
9164
|
finish4(t);
|
|
9114
9165
|
return t;
|
|
9115
9166
|
}
|
|
@@ -9154,10 +9205,12 @@ function playThousand(s, a, entropy2) {
|
|
|
9154
9205
|
(n, c) => n + pointValue(cardRank(c)),
|
|
9155
9206
|
0
|
|
9156
9207
|
);
|
|
9208
|
+
const before = t.scores;
|
|
9157
9209
|
t.scores = t.scores.map(
|
|
9158
9210
|
(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
9211
|
);
|
|
9160
|
-
t.
|
|
9212
|
+
const made = t.points[t.bidder] >= t.contract, change = (i) => `${t.scores[i] - before[i] >= 0 ? "+" : ""}${t.scores[i] - before[i]}`;
|
|
9213
|
+
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
9214
|
finish4(t);
|
|
9162
9215
|
}
|
|
9163
9216
|
return t;
|
|
@@ -10413,6 +10466,7 @@ function playMakao(s, a, entropy2) {
|
|
|
10413
10466
|
const t = structuredClone(s), seat = s.turn;
|
|
10414
10467
|
if (a.type === "catch" && s.unannounced !== null && s.unannounced !== seat) {
|
|
10415
10468
|
draw(t, s.unannounced, 5, entropy2);
|
|
10469
|
+
t.last = { seat: s.unannounced, type: "caught", count: 5 };
|
|
10416
10470
|
t.unannounced = null;
|
|
10417
10471
|
t.result = "Missed Makao: draw five.";
|
|
10418
10472
|
return t;
|
|
@@ -10421,12 +10475,13 @@ function playMakao(s, a, entropy2) {
|
|
|
10421
10475
|
t.unannounced = null;
|
|
10422
10476
|
if (s.wait) {
|
|
10423
10477
|
t.blocked[seat] = s.wait - 1;
|
|
10478
|
+
t.last = { seat, type: "wait", count: s.wait };
|
|
10424
10479
|
t.wait = 0;
|
|
10425
10480
|
advance3(t);
|
|
10426
10481
|
return t;
|
|
10427
10482
|
}
|
|
10428
10483
|
if (s.penalty) {
|
|
10429
|
-
draw(t, seat, s.penalty, entropy2);
|
|
10484
|
+
t.last = { seat, type: "penalty", count: draw(t, seat, s.penalty, entropy2).length };
|
|
10430
10485
|
t.penalty = 0;
|
|
10431
10486
|
t.suit = null;
|
|
10432
10487
|
advance3(t);
|
|
@@ -10434,11 +10489,13 @@ function playMakao(s, a, entropy2) {
|
|
|
10434
10489
|
}
|
|
10435
10490
|
const received = draw(t, seat, 1, entropy2);
|
|
10436
10491
|
t.drawn = received[0] ?? null;
|
|
10492
|
+
t.last = { seat, type: "draw", count: received.length };
|
|
10437
10493
|
t.phase = "drawn";
|
|
10438
10494
|
if (t.drawn === null) advance3(t);
|
|
10439
10495
|
return t;
|
|
10440
10496
|
}
|
|
10441
10497
|
if (a.type === "pass" && s.phase === "drawn") {
|
|
10498
|
+
t.last = { seat, type: "keep", count: 1 };
|
|
10442
10499
|
t.unannounced = null;
|
|
10443
10500
|
t.suit = null;
|
|
10444
10501
|
advance3(t);
|
|
@@ -10453,6 +10510,7 @@ function playMakao(s, a, entropy2) {
|
|
|
10453
10510
|
throw Error("Request an ordinary rank, or choose no demand.");
|
|
10454
10511
|
t.unannounced = null;
|
|
10455
10512
|
t.hands[seat] = t.hands[seat].filter((c) => !cards.includes(c));
|
|
10513
|
+
t.last = { seat, type: "play", cards: [...cards], ...t.hands[seat].length === 1 && a.promotion === "makao" ? { announced: true } : {} };
|
|
10456
10514
|
t.pile.push(...cards);
|
|
10457
10515
|
t.suit = null;
|
|
10458
10516
|
if (rank2 === 14) t.suit = a.amount;
|
|
@@ -11187,7 +11245,7 @@ function playProperties(s, a, entropy2) {
|
|
|
11187
11245
|
}
|
|
11188
11246
|
if (s.phase === "tax") {
|
|
11189
11247
|
if (a.type !== "tax" || ![0, 1].includes(a.amount ?? -1))
|
|
11190
|
-
throw Error("Choose
|
|
11248
|
+
throw Error("Choose the tax: amount 0 pays the flat 200, amount 1 pays ten percent of your estate value.");
|
|
11191
11249
|
const estate = p.cash + s.assets.reduce(
|
|
11192
11250
|
(n, a2, i) => n + (a2.owner === seat ? propertyFields[i].price + a2.houses * propertyFields[i].build : 0),
|
|
11193
11251
|
0
|
|
@@ -11262,7 +11320,8 @@ function playProperties(s, a, entropy2) {
|
|
|
11262
11320
|
t.dice = [];
|
|
11263
11321
|
return t;
|
|
11264
11322
|
}
|
|
11265
|
-
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.`);
|
|
11266
11325
|
}
|
|
11267
11326
|
function propertyView(s) {
|
|
11268
11327
|
const { decks, ...publicState } = s;
|
|
@@ -11406,7 +11465,7 @@ function panLegal(s, cards) {
|
|
|
11406
11465
|
}
|
|
11407
11466
|
function playPan(s, type, cards, entropy2) {
|
|
11408
11467
|
if (type === "next" && s.phase === "round")
|
|
11409
|
-
return deal12(s.hands.length, entropy2, s.letters, s.round + 1);
|
|
11468
|
+
return { ...deal12(s.hands.length, entropy2, s.letters, s.round + 1), previous: s.result };
|
|
11410
11469
|
if (s.phase !== "play") throw Error("This round is finished.");
|
|
11411
11470
|
const t = structuredClone(s), seat = s.turn;
|
|
11412
11471
|
if (type === "play" && panLegal(s, cards)) {
|
|
@@ -11560,7 +11619,7 @@ function award2(t, winner2, failure = false) {
|
|
|
11560
11619
|
}
|
|
11561
11620
|
function playSixtySix(s, type, at, marriage = false, claimMarriage = false, entropy2) {
|
|
11562
11621
|
if (type === "next" && s.phase === "round")
|
|
11563
|
-
return deal13(1 - s.dealer, s.scores, s.round + 1, entropy2);
|
|
11622
|
+
return { ...deal13(1 - s.dealer, s.scores, s.round + 1, entropy2), previous: s.result };
|
|
11564
11623
|
if (s.phase !== "play") throw Error("This round is finished.");
|
|
11565
11624
|
const t = structuredClone(s), seat = s.turn;
|
|
11566
11625
|
if (type === "close" && !s.trick.length && s.closed === null && s.upcard !== null) {
|
|
@@ -11605,6 +11664,7 @@ function playSixtySix(s, type, at, marriage = false, claimMarriage = false, entr
|
|
|
11605
11664
|
t.tricks[winner2]++;
|
|
11606
11665
|
t.turn = winner2;
|
|
11607
11666
|
t.lastTrick = t.trick;
|
|
11667
|
+
t.played = [...s.played ?? [], ...t.trick.map((c) => c.card)];
|
|
11608
11668
|
t.trick = [];
|
|
11609
11669
|
if (t.closed === null && t.upcard !== null)
|
|
11610
11670
|
for (const i of [winner2, 1 - winner2]) {
|
|
@@ -11746,9 +11806,9 @@ function dominoMoves(s) {
|
|
|
11746
11806
|
].filter((m) => m !== null);
|
|
11747
11807
|
});
|
|
11748
11808
|
}
|
|
11749
|
-
function endRound(s, winner2, bonus) {
|
|
11750
|
-
const scores2 = [...s.scores];
|
|
11751
|
-
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;
|
|
11752
11812
|
const finished = scores2.some((n) => n >= 100);
|
|
11753
11813
|
return {
|
|
11754
11814
|
...s,
|
|
@@ -11756,7 +11816,7 @@ function endRound(s, winner2, bonus) {
|
|
|
11756
11816
|
winner: finished ? best(scores2) : null,
|
|
11757
11817
|
phase: finished ? "finished" : "round",
|
|
11758
11818
|
turn: winner2 ?? s.turn,
|
|
11759
|
-
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).`
|
|
11760
11820
|
};
|
|
11761
11821
|
}
|
|
11762
11822
|
var pipSum = (hand2) => hand2.reduce((n, [a, b]) => n + a + b, 0);
|
|
@@ -11775,8 +11835,9 @@ function playDomino(s, type, at, end, entropy2) {
|
|
|
11775
11835
|
throw Error("Play a matching tile before drawing.");
|
|
11776
11836
|
while (stock.length && !dominoMoves({ ...s, hands }).length)
|
|
11777
11837
|
hands[s.turn].push(stock.pop());
|
|
11778
|
-
|
|
11779
|
-
|
|
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.` };
|
|
11780
11841
|
if (next4.passes < 2) return next4;
|
|
11781
11842
|
const a2 = pipSum(hands[0]), b2 = pipSum(hands[1]);
|
|
11782
11843
|
return endRound(next4, a2 === b2 ? null : a2 < b2 ? 0 : 1, Math.abs(a2 - b2));
|
|
@@ -11794,7 +11855,8 @@ function playDomino(s, type, at, end, entropy2) {
|
|
|
11794
11855
|
}
|
|
11795
11856
|
const left = chain[0], right = chain.at(-1);
|
|
11796
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]);
|
|
11797
|
-
|
|
11858
|
+
const scored = total % 5 === 0 && total > 0;
|
|
11859
|
+
if (scored) scores2[s.turn] += total;
|
|
11798
11860
|
const next3 = {
|
|
11799
11861
|
...s,
|
|
11800
11862
|
hands,
|
|
@@ -11802,10 +11864,11 @@ function playDomino(s, type, at, end, entropy2) {
|
|
|
11802
11864
|
chain,
|
|
11803
11865
|
scores: scores2,
|
|
11804
11866
|
passes: 0,
|
|
11805
|
-
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}` : ""}.`
|
|
11806
11869
|
};
|
|
11807
11870
|
if (!hands[s.turn].length)
|
|
11808
|
-
return endRound(next3, s.turn, pipSum(hands[1 - s.turn]));
|
|
11871
|
+
return endRound(next3, s.turn, pipSum(hands[1 - s.turn]), true);
|
|
11809
11872
|
if (scores2.some((n) => n >= 100))
|
|
11810
11873
|
return { ...next3, phase: "finished", winner: best(scores2) };
|
|
11811
11874
|
return next3;
|
|
@@ -12117,7 +12180,7 @@ function agentListComplete(game, state, legal) {
|
|
|
12117
12180
|
if (game === "poker") return !legal.some((action) => action.type === "raise");
|
|
12118
12181
|
return false;
|
|
12119
12182
|
}
|
|
12120
|
-
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)])];
|
|
12121
12184
|
var promotions = ["marriage", "marriage-claim", "makao", "normal", "schneider", "schwarz", "ouvert"];
|
|
12122
12185
|
function hand(state, seat) {
|
|
12123
12186
|
for (const key2 of ["hands", "hand"]) {
|
|
@@ -12144,6 +12207,15 @@ function cardGroups(cards) {
|
|
|
12144
12207
|
else for (let i = 0; i + 4 <= cards.length; i += 4) add(cards.slice(i, i + 4));
|
|
12145
12208
|
return [...groups.values()].map(([group]) => group);
|
|
12146
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
|
+
}
|
|
12147
12219
|
var atTypes = /* @__PURE__ */ new Set(["fire", "sow", "hold", "score", "capture", "knock", "dead", "mortgage", "redeem", "build", "sell", "choose", "kitty", "place", "tile", "move"]);
|
|
12148
12220
|
var amountTypes = /* @__PURE__ */ new Set(["bid", "raise", "declare", "tax", "release", "moon", "trump", "contract", "kitty", "announce", "choose", "tile", "place", "move"]);
|
|
12149
12221
|
var cardTypes = /* @__PURE__ */ new Set(["play", "discard", "exchange", "select", "meld", "layoff"]);
|
|
@@ -12198,6 +12270,10 @@ function specific(game, state, seat) {
|
|
|
12198
12270
|
{ type: "play", cards, promotion: "makao" },
|
|
12199
12271
|
...[0, 1, 2, 3, 5, 6, 7, 8, 9, 10, 13].flatMap((amount) => [{ type: "play", cards, amount }, { type: "play", cards, amount, promotion: "makao" }])
|
|
12200
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
|
+
}
|
|
12201
12277
|
if (game === "go") {
|
|
12202
12278
|
const s = state;
|
|
12203
12279
|
if (s.phase === "scoring" && s.accepted.includes(seat)) return [];
|
|
@@ -12407,12 +12483,33 @@ function agentPosition(room) {
|
|
|
12407
12483
|
const active = room.phase === "playing" && !!match && !room.players[room.seat].left && !ended && !room.players.some((p) => p.paused);
|
|
12408
12484
|
const grid = !complete && match ? indexedBoard(room.game, match.state) : null;
|
|
12409
12485
|
const wire = !complete && match && enumeratedAgentGames.includes(room.game) ? active ? agentLegalActions(room.game, match.state, room.seat, room.capacity) ?? [] : [] : null;
|
|
12410
|
-
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
|
+
})();
|
|
12411
12507
|
const dotsLabeled = match?.kind === "dots-boxes" ? match.state.boxes.map((owner4, box) => {
|
|
12412
12508
|
const { size, edges } = match.state, sides2 = boxEdges(size, box), drawn = sides2.filter((e) => edges[e] >= 0).length;
|
|
12413
12509
|
const named = ["top", "bottom", "left", "right"].map((side2, i) => `${side2} ${sides2[i] + 1}${edges[sides2[i]] >= 0 ? "*" : ""}`).join(", ");
|
|
12414
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)" : ""}`;
|
|
12415
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;
|
|
12416
12513
|
const ginHand = match?.kind === "gin-rummy" && match.state.phase !== "finished" ? match.state.hands[room.seat]?.filter((c) => c !== null) ?? [] : [];
|
|
12417
12514
|
const ginHint = ginHand.length ? (() => {
|
|
12418
12515
|
const layout = ginLayout(ginHand), names = (cards) => cards.map((c) => `${cardLabel(c)} (id ${c})`);
|
|
@@ -12425,11 +12522,19 @@ function agentPosition(room) {
|
|
|
12425
12522
|
knock_limit: 10
|
|
12426
12523
|
};
|
|
12427
12524
|
})() : null;
|
|
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.`] : [];
|
|
12529
|
+
const sixtyUnseen = match?.kind === "sixty-six" && match.state.phase === "play" ? (() => {
|
|
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]]);
|
|
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})`);
|
|
12532
|
+
})() : null;
|
|
12428
12533
|
const asianMove = match?.kind === "shogi" || match?.kind === "xiangqi" ? asianLastMove(match.state.history) : null;
|
|
12429
12534
|
const asianLast = asianMove && (match?.kind === "shogi" || match?.kind === "xiangqi") ? { ...asianMove, by: match.state.history.at(-1).mover } : null;
|
|
12430
12535
|
const millLast = match?.kind === "mills" ? millLastTurn(match.state) : null;
|
|
12431
|
-
const
|
|
12432
|
-
const cribbageLast =
|
|
12536
|
+
const peg = match?.kind === "cribbage" && match.state.phase === "peg" ? match.state.lastPeg : void 0;
|
|
12537
|
+
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
12538
|
return {
|
|
12434
12539
|
protocol_version: 1,
|
|
12435
12540
|
room: room.code,
|
|
@@ -12444,16 +12549,18 @@ function agentPosition(room) {
|
|
|
12444
12549
|
players: room.players.map((p, seat) => ({ seat, name: p.name, left: p.left, ...p.agent ? { agent: true } : {}, ...p.paused ? { paused: true } : {} })),
|
|
12445
12550
|
board_size: complete ? { rows, columns } : grid ? { rows: grid.rows, columns: grid.columns } : { rows: 0, columns: 0 },
|
|
12446
12551
|
board_top_to_bottom: board,
|
|
12447
|
-
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,
|
|
12448
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." } : {},
|
|
12449
12554
|
// 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 },
|
|
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 },
|
|
12451
12556
|
...room.game === "chess" ? { fen } : {},
|
|
12452
12557
|
// The side to move is in check (chess family, xiangqi, shogi).
|
|
12453
12558
|
...match && room.phase === "playing" && (match.kind === "chess" ? chessPosition(match.state).inCheck() : "check" in match.state && match.state.check === true) ? { in_check: true } : {},
|
|
12454
12559
|
visible_state: match?.state ?? null,
|
|
12455
12560
|
cards_labeled: match ? cardsLabeled(room.game, match.state) : null,
|
|
12456
12561
|
...ginHint ? { gin_deadwood: ginHint } : {},
|
|
12562
|
+
...sixtyUnseen ? { sixty_six_unseen: sixtyUnseen } : {},
|
|
12563
|
+
...ruleNotes.length ? { rule_notes: ruleNotes } : {},
|
|
12457
12564
|
// Timed rounds (drawing, categories): agents cannot measure time, so give the remaining seconds.
|
|
12458
12565
|
...typeof match?.state?.deadline === "number" && room.serverNow ? { deadline_seconds_left: Math.max(0, Math.ceil((match.state.deadline - room.serverNow) / 1e3)) } : {},
|
|
12459
12566
|
action_types: action_catalog_default[room.game],
|
|
@@ -12481,13 +12588,29 @@ function labeledBoard(game, board) {
|
|
|
12481
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(" ")}`)
|
|
12482
12589
|
];
|
|
12483
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
|
+
}
|
|
12484
12607
|
function agentMove(position, input) {
|
|
12485
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);
|
|
12486
12609
|
if (!action && input.from !== void 0 && input.to !== void 0 && "legal_wire_actions" in position) {
|
|
12487
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));
|
|
12488
12611
|
if (wire) return wire;
|
|
12489
12612
|
}
|
|
12490
|
-
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.`);
|
|
12491
12614
|
if (action.type === "swap") return { type: "swap" };
|
|
12492
12615
|
if (action.type === "edge") return { type: "place", at: action.edge - 1 };
|
|
12493
12616
|
if (action.type === "sow") return { type: "sow", at: action.pit - 1 };
|
|
@@ -12529,6 +12652,10 @@ function agentWireMove(position, json) {
|
|
|
12529
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.`);
|
|
12530
12653
|
if (position.phase !== "playing" || position.you.left || position.paused) throw Error("This match is not active for your seat.");
|
|
12531
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
|
+
}
|
|
12532
12659
|
return action;
|
|
12533
12660
|
}
|
|
12534
12661
|
function sameFields(a, b) {
|
|
@@ -12782,7 +12909,9 @@ function nextStep(position, event, brief = false, server) {
|
|
|
12782
12909
|
const room = `--room ${position.room}${server && server !== defaultServer ? ` --server ${server}` : ""}`, revision = position.revision, flag = brief ? " --brief" : "";
|
|
12783
12910
|
const wait = `leafarcade game wait ${room} --after-revision ${revision} --timeout ${defaultWaitSeconds}${flag}`;
|
|
12784
12911
|
if (position.you.left) return "You left this room. Create or join another room only if the human asks.";
|
|
12785
|
-
|
|
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}.`;
|
|
12786
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).`;
|
|
12787
12916
|
if (position.phase === "waiting") {
|
|
12788
12917
|
const host = position.you.seat === 0, seated = position.players.filter((p) => !p.left).length;
|
|
@@ -12795,7 +12924,7 @@ function nextStep(position, event, brief = false, server) {
|
|
|
12795
12924
|
const play = (move2) => `leafarcade game play ${room} --revision ${revision} --action ${move2}`;
|
|
12796
12925
|
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
12926
|
if (position.turn?.yours) {
|
|
12798
|
-
if (wire?.length === 1 && wire[0].type === "next" && Object.keys(wire[0]).length === 1) return `The hand is over
|
|
12927
|
+
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
12928
|
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
12929
|
const move2 = wire ? "'<one legal_wire_actions entry>'" : position.legal_actions_complete ? "'<one legal_actions entry>'" : "'<wire action JSON from game rules>'";
|
|
12801
12930
|
const check = lineGames.includes(position.game) ? "Check your immediate win and your opponent's immediate threats (every line, including diagonals), then: " : "";
|
|
@@ -12803,12 +12932,19 @@ function nextStep(position, event, brief = false, server) {
|
|
|
12803
12932
|
state?.penalty ? `A ${state.penalty}-card penalty is active: answer with a matching attack card, or draw takes all ${state.penalty}. ` : "",
|
|
12804
12933
|
state?.wait ? `A ${state.wait}-turn wait is active: answer with a 4, or draw serves the wait (no cards drawn). ` : "",
|
|
12805
12934
|
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]}. ` : ""
|
|
12935
|
+
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)` : ""}. ` : "",
|
|
12936
|
+
state?.suit !== null && state?.suit !== void 0 ? `Ace suit in force: ${["clubs", "diamonds", "hearts", "spades"][state.suit]}. ` : "",
|
|
12937
|
+
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. ` : "",
|
|
12938
|
+
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
12939
|
].join(""))(position.visible_state) : "";
|
|
12940
|
+
const chain = position.visible_state?.chain;
|
|
12941
|
+
const jumper = (position.legal_actions?.[0] ?? wire?.[0])?.from;
|
|
12942
|
+
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
12943
|
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}`;
|
|
12944
|
+
return `Your move. ${capture}${mill}${makao}${partial}${check}${play(move2)} --wait${flag}`;
|
|
12811
12945
|
}
|
|
12946
|
+
const dealing = ["round", "show"].includes(String(position.visible_state?.phase));
|
|
12947
|
+
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
12948
|
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
12949
|
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
12950
|
const mover = position.turn ? ` (seat ${position.turn.seat}, ${position.players[position.turn.seat]?.name ?? "empty"}, to act)` : "";
|
|
@@ -12899,7 +13035,7 @@ async function installSkill(agent = "codex", update = false) {
|
|
|
12899
13035
|
// packages/cli/package.json
|
|
12900
13036
|
var package_default = {
|
|
12901
13037
|
name: "@leafarcade/cli",
|
|
12902
|
-
version: "0.
|
|
13038
|
+
version: "0.9.1",
|
|
12903
13039
|
private: false,
|
|
12904
13040
|
description: "Create private browser board games and play with your AI agent. Includes 53 games and an installable Codex/Claude skill.",
|
|
12905
13041
|
type: "module",
|
|
@@ -13007,13 +13143,13 @@ var examples = {
|
|
|
13007
13143
|
amazons: `--action '{"type":"move","from":30,"to":33,"amount":53}'`,
|
|
13008
13144
|
"lines-of-action": `--action '{"type":"move","from":1,"to":17}'`,
|
|
13009
13145
|
"last-greenhouse": `--action '{"type":"share"}'`,
|
|
13010
|
-
euchre: "
|
|
13146
|
+
euchre: `--action '{"type":"trump","amount":2}'`,
|
|
13011
13147
|
cribbage: `--action '{"type":"discard","cards":[0,1]}'`,
|
|
13012
13148
|
fanorona: `--action '{"type":"move","from":13,"to":22,"amount":0}'`,
|
|
13013
|
-
categories: '{"type":"begin","promotion":"untimed"}'
|
|
13014
|
-
cheat: '{"type":"play","cards":[0]}'
|
|
13015
|
-
president: '{"type":"play","cards":[1]}'
|
|
13016
|
-
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"}'`,
|
|
13017
13153
|
"connect-four": "--column 4",
|
|
13018
13154
|
chess: "--from e2 --to e4",
|
|
13019
13155
|
checkers: "--from b6 --to a5",
|
|
@@ -13195,6 +13331,15 @@ function stateChanges(game, before, after, limit = 30) {
|
|
|
13195
13331
|
const here = path.join(".");
|
|
13196
13332
|
const primitive = (v) => v === null || typeof v !== "object";
|
|
13197
13333
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
13334
|
+
if (a.length && b.length && [...a, ...b].every((v) => typeof v === "string")) {
|
|
13335
|
+
for (let k = 0; k < a.length; k++) {
|
|
13336
|
+
const kept = a.slice(k);
|
|
13337
|
+
if (kept.every((v, i) => v === b[i]) && b.length > kept.length) {
|
|
13338
|
+
out2.push(`${here}: + ${b.slice(kept.length).map((v) => show(v)).join(", ")}`);
|
|
13339
|
+
return;
|
|
13340
|
+
}
|
|
13341
|
+
}
|
|
13342
|
+
}
|
|
13198
13343
|
if (a.length === b.length && a.length > 4 && a.every(primitive) && b.every(primitive)) {
|
|
13199
13344
|
a.forEach((v, i) => {
|
|
13200
13345
|
if (v !== b[i] && out2.length <= limit) out2.push(`${here}[${i}]: ${show(v)} \u2192 ${show(b[i])}`);
|
|
@@ -13666,6 +13811,28 @@ var scores = (position) => {
|
|
|
13666
13811
|
const value = position.visible_state?.scores;
|
|
13667
13812
|
return Array.isArray(value) && value.length && value.every((n) => typeof n === "number") ? value : null;
|
|
13668
13813
|
};
|
|
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.";
|
|
13669
13836
|
function briefPosition(position) {
|
|
13670
13837
|
const turn = position.turn ? position.turn.yours ? "yours" : seatName(position, position.turn.seat) : null;
|
|
13671
13838
|
const wire = "legal_wire_actions" in position ? position.legal_wire_actions ?? [] : null;
|
|
@@ -13688,14 +13855,18 @@ function briefPosition(position) {
|
|
|
13688
13855
|
board_labeled: position.board_labeled,
|
|
13689
13856
|
last_move: position.last_move,
|
|
13690
13857
|
..."changed_since_last_read" in position ? { changed_since_last_read: position.changed_since_last_read } : {},
|
|
13691
|
-
...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 } : {},
|
|
13692
13860
|
// Card, tile and other games without a labeled board still need their seat-visible state.
|
|
13693
13861
|
..."deadline_seconds_left" in position ? { deadline_seconds_left: position.deadline_seconds_left } : {},
|
|
13694
13862
|
..."board_legend" in position ? { board_legend: position.board_legend } : {},
|
|
13695
13863
|
// Only the classic convenience games are fully described by their labeled board.
|
|
13696
|
-
|
|
13864
|
+
// The legal moves are listed once at the top level, not again inside the state.
|
|
13865
|
+
...position.legal_actions_complete && position.board_labeled || !position.visible_state ? {} : { visible_state: withoutLegal(position.visible_state) },
|
|
13697
13866
|
...position.cards_labeled ? { cards_labeled: position.cards_labeled } : {},
|
|
13698
13867
|
..."gin_deadwood" in position ? { gin_deadwood: position.gin_deadwood } : {},
|
|
13868
|
+
..."sixty_six_unseen" in position ? { sixty_six_unseen: position.sixty_six_unseen } : {},
|
|
13869
|
+
..."rule_notes" in position ? { rule_notes: position.rule_notes } : {},
|
|
13699
13870
|
...position.access_requests?.length ? { access_requests: position.access_requests } : {}
|
|
13700
13871
|
};
|
|
13701
13872
|
}
|
|
@@ -13794,8 +13965,10 @@ try {
|
|
|
13794
13965
|
const goSize = integer(values["go-size"] ?? "9", "go-size", 9, 19);
|
|
13795
13966
|
if (goSize !== 9 && goSize !== 13 && goSize !== 19) throw Error("go-size must be 9, 13 or 19.");
|
|
13796
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);
|
|
13797
|
-
} else if (command === "rules")
|
|
13798
|
-
|
|
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 {
|
|
13799
13972
|
const server = serviceUrl(values.server ?? defaultServer);
|
|
13800
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.`);
|
|
13801
13974
|
const room = roomCode(command === "join" ? positionals[1] : values.room);
|
|
@@ -13869,6 +14042,8 @@ try {
|
|
|
13869
14042
|
const row = values.row === void 0 ? void 0 : integer(values.row, "row", 1, position.board_size.rows);
|
|
13870
14043
|
if (revision !== position.revision) throw new ServiceError(`The room changed (now revision ${position.revision}). Read game status before playing.`, "STALE_REVISION");
|
|
13871
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.`);
|
|
13872
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) });
|
|
13873
14048
|
const waitSeconds = integer(values.timeout ?? String(defaultWaitSeconds), "timeout", 1, 3600);
|
|
13874
14049
|
await rpc(session, "play", { revision, action }, false, controller.signal);
|