@leafarcade/cli 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.mjs +1201 -1034
- package/package.json +1 -1
- package/skills/leafarcade/SKILL.md +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -3735,6 +3735,26 @@ function checkerMoves(s) {
|
|
|
3735
3735
|
return moves.some((m) => m.capture !== null) ? moves.filter((m) => m.capture !== null) : moves;
|
|
3736
3736
|
}
|
|
3737
3737
|
|
|
3738
|
+
// lib/multiplayer/room-last-move.ts
|
|
3739
|
+
function checkerLastMove(history) {
|
|
3740
|
+
const [before, after] = history.slice(-2).map((key2) => key2.split(":")[0].split(",").map(Number));
|
|
3741
|
+
if (!before || !after || before.length !== after.length) return null;
|
|
3742
|
+
const to = after.findIndex((p, i) => p !== 0 && before[i] === 0);
|
|
3743
|
+
if (to < 0) return null;
|
|
3744
|
+
const from = before.findIndex((p, i) => p !== 0 && after[i] === 0 && Math.abs(p) === Math.abs(after[to]));
|
|
3745
|
+
return from < 0 ? null : { from, to };
|
|
3746
|
+
}
|
|
3747
|
+
function asianLastMove(history) {
|
|
3748
|
+
const [before, after] = history.slice(-2);
|
|
3749
|
+
if (!before || !after || after.mover < 0) return null;
|
|
3750
|
+
const cells = (key2) => key2.split(":")[1]?.split(",") ?? [];
|
|
3751
|
+
const a = cells(before.key), b = cells(after.key), mine = String(after.mover);
|
|
3752
|
+
const to = b.findIndex((cell, i) => cell.startsWith(mine) && cell !== a[i]);
|
|
3753
|
+
if (to < 0) return null;
|
|
3754
|
+
const from = a.findIndex((cell, i) => cell.startsWith(mine) && !b[i].startsWith(mine));
|
|
3755
|
+
return { from: from < 0 ? null : from, to };
|
|
3756
|
+
}
|
|
3757
|
+
|
|
3738
3758
|
// lib/random.ts
|
|
3739
3759
|
function randomStep(seed) {
|
|
3740
3760
|
const next3 = seed + 1831565813 >>> 0;
|
|
@@ -3767,180 +3787,736 @@ function shuffled(items, entropy2) {
|
|
|
3767
3787
|
function index(value, length) {
|
|
3768
3788
|
return Number.isInteger(value) && value >= 0 && value < length;
|
|
3769
3789
|
}
|
|
3770
|
-
function best(
|
|
3771
|
-
const high = Math.max(...
|
|
3790
|
+
function best(scores2) {
|
|
3791
|
+
const high = Math.max(...scores2), winners = scores2.flatMap((s, i) => s === high ? [i] : []);
|
|
3772
3792
|
return winners.length === 1 ? winners[0] : null;
|
|
3773
3793
|
}
|
|
3774
3794
|
|
|
3775
|
-
// lib/multiplayer/
|
|
3776
|
-
var
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3795
|
+
// lib/multiplayer/mills.ts
|
|
3796
|
+
var millLines = [
|
|
3797
|
+
[0, 1, 2],
|
|
3798
|
+
[2, 3, 4],
|
|
3799
|
+
[4, 5, 6],
|
|
3800
|
+
[6, 7, 0],
|
|
3801
|
+
[8, 9, 10],
|
|
3802
|
+
[10, 11, 12],
|
|
3803
|
+
[12, 13, 14],
|
|
3804
|
+
[14, 15, 8],
|
|
3805
|
+
[16, 17, 18],
|
|
3806
|
+
[18, 19, 20],
|
|
3807
|
+
[20, 21, 22],
|
|
3808
|
+
[22, 23, 16],
|
|
3809
|
+
[1, 9, 17],
|
|
3810
|
+
[3, 11, 19],
|
|
3811
|
+
[5, 13, 21],
|
|
3812
|
+
[7, 15, 23]
|
|
3813
|
+
];
|
|
3814
|
+
var millEdges = millLines.flatMap(([a, b, c]) => [
|
|
3815
|
+
[a, b],
|
|
3816
|
+
[b, c]
|
|
3817
|
+
]);
|
|
3818
|
+
function createMills() {
|
|
3819
|
+
return {
|
|
3820
|
+
board: Array(24).fill(0),
|
|
3821
|
+
reserves: [9, 9],
|
|
3822
|
+
turn: 0,
|
|
3823
|
+
captures: 0,
|
|
3824
|
+
winner: null,
|
|
3825
|
+
draw: false,
|
|
3826
|
+
history: []
|
|
3827
|
+
};
|
|
3789
3828
|
}
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3829
|
+
var linesAt = (board, at) => millLines.filter(
|
|
3830
|
+
(line) => line.includes(at) && line.every((i) => board[i] === board[at])
|
|
3831
|
+
);
|
|
3832
|
+
function millCaptures(s) {
|
|
3833
|
+
const opponents = s.board.flatMap((p, i) => p === 2 - s.turn ? [i] : []), loose = opponents.filter((at) => !linesAt(s.board, at).length);
|
|
3834
|
+
return loose.length ? loose : opponents;
|
|
3835
|
+
}
|
|
3836
|
+
function millMoves(s) {
|
|
3837
|
+
if (s.winner !== null || s.draw || s.captures) return [];
|
|
3838
|
+
const empty = s.board.flatMap((p, i) => !p ? [i] : []);
|
|
3839
|
+
if (s.reserves[s.turn]) return empty.map((to) => ({ from: null, to }));
|
|
3840
|
+
const own = s.board.flatMap((p, i) => p === s.turn + 1 ? [i] : []);
|
|
3841
|
+
return own.flatMap(
|
|
3842
|
+
(from) => empty.filter(
|
|
3843
|
+
(to) => own.length === 3 || millEdges.some(
|
|
3844
|
+
([a, b]) => a === from && b === to || b === from && a === to
|
|
3845
|
+
)
|
|
3846
|
+
).map((to) => ({ from, to }))
|
|
3847
|
+
);
|
|
3848
|
+
}
|
|
3849
|
+
function finishTurn(s) {
|
|
3850
|
+
const turn = s.turn === 0 ? 1 : 0, key2 = s.board.join(",") + ":" + turn + ":" + s.reserves.join(",");
|
|
3851
|
+
const next3 = {
|
|
3852
|
+
...s,
|
|
3853
|
+
turn,
|
|
3854
|
+
captures: 0,
|
|
3855
|
+
history: [...s.history, key2]
|
|
3856
|
+
};
|
|
3857
|
+
const count = next3.board.filter((p) => p === turn + 1).length;
|
|
3858
|
+
if (!next3.reserves[turn] && (count < 3 || !millMoves(next3).length))
|
|
3859
|
+
next3.winner = s.turn;
|
|
3860
|
+
else next3.draw = next3.history.filter((k) => k === key2).length >= 3;
|
|
3861
|
+
return next3;
|
|
3862
|
+
}
|
|
3863
|
+
function playMills(s, type, to, from) {
|
|
3864
|
+
if (!index(to, 24) || s.winner !== null || s.draw)
|
|
3865
|
+
throw Error("Choose a legal point.");
|
|
3866
|
+
const board = [...s.board], reserves = [...s.reserves];
|
|
3867
|
+
if (type === "capture") {
|
|
3868
|
+
if (!s.captures || !millCaptures(s).includes(to))
|
|
3869
|
+
throw Error(
|
|
3870
|
+
"Capture a piece outside a mill, unless all pieces are in mills."
|
|
3871
|
+
);
|
|
3872
|
+
board[to] = 0;
|
|
3873
|
+
const next4 = { ...s, board, captures: s.captures - 1 };
|
|
3874
|
+
return next4.captures && millCaptures(next4).length ? next4 : finishTurn(next4);
|
|
3875
|
+
}
|
|
3876
|
+
if (!["place", "move"].includes(type) || !millMoves(s).some(
|
|
3877
|
+
(m) => m.to === to && m.from === (type === "place" ? null : from)
|
|
3878
|
+
))
|
|
3879
|
+
throw Error(
|
|
3880
|
+
"Place from your reserve or move to a connected point. With three pieces you may fly."
|
|
3881
|
+
);
|
|
3882
|
+
if (type === "place") reserves[s.turn]--;
|
|
3883
|
+
else board[from] = 0;
|
|
3884
|
+
board[to] = s.turn + 1;
|
|
3885
|
+
const captures = Math.min(
|
|
3886
|
+
linesAt(board, to).length,
|
|
3887
|
+
board.filter((p) => p === 2 - s.turn).length
|
|
3795
3888
|
);
|
|
3889
|
+
const next3 = { ...s, board, reserves, captures };
|
|
3890
|
+
return captures ? next3 : finishTurn(next3);
|
|
3891
|
+
}
|
|
3892
|
+
function millLastTurn(s) {
|
|
3893
|
+
const boards = s.history.slice(-2).map((key2) => key2.split(":")[0].split(",").map(Number));
|
|
3894
|
+
const after = boards.at(-1), before = boards.length === 2 ? boards[0] : Array(24).fill(0);
|
|
3895
|
+
if (!after) return null;
|
|
3896
|
+
const seat = Number(s.history.at(-1).split(":")[1]) === 0 ? 1 : 0;
|
|
3897
|
+
const to = after.findIndex((p, i) => p === seat + 1 && before[i] !== seat + 1);
|
|
3898
|
+
if (to < 0) return null;
|
|
3899
|
+
const from = before.findIndex((p, i) => p === seat + 1 && after[i] !== seat + 1);
|
|
3900
|
+
const captured = before.flatMap((p, i) => p === 2 - seat && after[i] !== p ? [i] : []);
|
|
3901
|
+
return { seat, from: from < 0 ? null : from, to, captured };
|
|
3796
3902
|
}
|
|
3797
3903
|
|
|
3798
|
-
// lib/multiplayer/
|
|
3799
|
-
var
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
3875
|
-
|
|
3876
|
-
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
|
|
3880
|
-
|
|
3881
|
-
|
|
3882
|
-
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
|
|
3936
|
-
}
|
|
3937
|
-
|
|
3904
|
+
// lib/multiplayer/cribbage.ts
|
|
3905
|
+
var cribbageLabel = (c) => ["A", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K"][c % 13] + ["\u2663", "\u2666", "\u2665", "\u2660"][Math.floor(c / 13)];
|
|
3906
|
+
|
|
3907
|
+
// lib/multiplayer/dots-boxes.ts
|
|
3908
|
+
function boxEdges(size, box) {
|
|
3909
|
+
const x = box % size, y = Math.floor(box / size), offset = size * (size + 1);
|
|
3910
|
+
return [
|
|
3911
|
+
y * size + x,
|
|
3912
|
+
(y + 1) * size + x,
|
|
3913
|
+
offset + y * (size + 1) + x,
|
|
3914
|
+
offset + y * (size + 1) + x + 1
|
|
3915
|
+
];
|
|
3916
|
+
}
|
|
3917
|
+
|
|
3918
|
+
// lib/multiplayer/trick-cards.ts
|
|
3919
|
+
var cardSuit = (card2) => Math.floor(card2 / 13);
|
|
3920
|
+
var cardRank = (card2) => card2 % 13 + 2;
|
|
3921
|
+
var cardLabel = (card2) => `${["", "", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"][cardRank(card2)]}${["\u2663", "\u2666", "\u2665", "\u2660"][cardSuit(card2)]}`;
|
|
3922
|
+
var penalty = (card2) => cardSuit(card2) === 2 ? 1 : card2 === 49 ? 13 : 0;
|
|
3923
|
+
function first(s) {
|
|
3924
|
+
return s.variant === "spades" ? s.dealer : (s.dealer + 1) % s.hands.length;
|
|
3925
|
+
}
|
|
3926
|
+
function deal(s, entropy2) {
|
|
3927
|
+
const count = s.hands.length, size = s.variant === "oh-hell" ? Math.max(1, (count === 4 ? 13 : 17) - s.round) : 13, deck = shuffled(
|
|
3928
|
+
Array.from({ length: 52 }, (_, i) => i),
|
|
3929
|
+
entropy2
|
|
3930
|
+
);
|
|
3931
|
+
const hands = Array.from(
|
|
3932
|
+
{ length: count },
|
|
3933
|
+
(_, i) => deck.slice(i * size, (i + 1) * size).sort((a, b) => a - b)
|
|
3934
|
+
);
|
|
3935
|
+
const t = {
|
|
3936
|
+
...s,
|
|
3937
|
+
size,
|
|
3938
|
+
hands,
|
|
3939
|
+
stock: deck.slice(count * size),
|
|
3940
|
+
trick: [],
|
|
3941
|
+
lastTrick: [],
|
|
3942
|
+
tricks: Array(count).fill(0),
|
|
3943
|
+
penalties: Array(count).fill(0),
|
|
3944
|
+
bids: Array(count).fill(null),
|
|
3945
|
+
blind: Array(count).fill(false),
|
|
3946
|
+
blindAsked: Array(count).fill(false),
|
|
3947
|
+
selected: Array.from({ length: count }, () => []),
|
|
3948
|
+
passed: Array(count).fill(false),
|
|
3949
|
+
broken: false,
|
|
3950
|
+
winner: null,
|
|
3951
|
+
result: ""
|
|
3952
|
+
};
|
|
3953
|
+
t.trump = s.variant === "spades" ? 3 : s.variant === "oh-hell" && s.round > 0 ? cardSuit(t.stock[0]) : null;
|
|
3954
|
+
t.phase = s.variant === "hearts" ? s.round % 4 === 3 ? "play" : "pass" : s.variant === "spades" && Math.abs(s.scores[0] - s.scores[1]) >= 200 ? "blind" : "bid";
|
|
3955
|
+
t.turn = s.variant === "hearts" ? t.phase === "play" ? hands.findIndex((h) => h.includes(0)) : 0 : first(t);
|
|
3956
|
+
return t;
|
|
3957
|
+
}
|
|
3958
|
+
function createTrickCards(variant, count, entropy2) {
|
|
3959
|
+
if (!index(count - 2, 3) || variant !== "oh-hell" && count !== 4)
|
|
3960
|
+
throw Error("Choose the required player count.");
|
|
3961
|
+
return deal(
|
|
3962
|
+
{
|
|
3963
|
+
variant,
|
|
3964
|
+
turn: 0,
|
|
3965
|
+
dealer: Math.floor(draws(entropy2, 1)[0] * count),
|
|
3966
|
+
round: 0,
|
|
3967
|
+
size: 13,
|
|
3968
|
+
phase: "bid",
|
|
3969
|
+
hands: Array.from({ length: count }, () => []),
|
|
3970
|
+
stock: [],
|
|
3971
|
+
trick: [],
|
|
3972
|
+
lastTrick: [],
|
|
3973
|
+
tricks: [],
|
|
3974
|
+
penalties: [],
|
|
3975
|
+
bids: [],
|
|
3976
|
+
blind: [],
|
|
3977
|
+
blindAsked: [],
|
|
3978
|
+
selected: [],
|
|
3979
|
+
passed: [],
|
|
3980
|
+
scores: Array(variant === "spades" ? 2 : count).fill(0),
|
|
3981
|
+
bags: [0, 0],
|
|
3982
|
+
trump: null,
|
|
3983
|
+
broken: false,
|
|
3984
|
+
winner: null,
|
|
3985
|
+
result: ""
|
|
3986
|
+
},
|
|
3987
|
+
entropy2
|
|
3988
|
+
);
|
|
3989
|
+
}
|
|
3990
|
+
function trickLegalCards(s) {
|
|
3991
|
+
if (s.phase !== "play") return [];
|
|
3992
|
+
const hand2 = s.hands[s.turn].filter((c) => c !== null), lead = s.trick[0];
|
|
3993
|
+
if (lead) {
|
|
3994
|
+
const suited = hand2.filter((c) => cardSuit(c) === cardSuit(lead.card));
|
|
3995
|
+
if (suited.length) return suited;
|
|
3996
|
+
if (s.variant === "hearts" && s.tricks.every((n) => n === 0)) {
|
|
3997
|
+
const clean = hand2.filter((c) => !penalty(c));
|
|
3998
|
+
if (clean.length) return clean;
|
|
3999
|
+
}
|
|
4000
|
+
return hand2;
|
|
4001
|
+
}
|
|
4002
|
+
if (s.variant === "hearts") {
|
|
4003
|
+
if (s.tricks.every((n) => n === 0)) return hand2.includes(0) ? [0] : [];
|
|
4004
|
+
if (!s.broken) {
|
|
4005
|
+
const clean = hand2.filter((c) => cardSuit(c) !== 2);
|
|
4006
|
+
if (clean.length) return clean;
|
|
4007
|
+
}
|
|
4008
|
+
}
|
|
4009
|
+
if (s.variant === "spades" && !s.broken) {
|
|
4010
|
+
const clean = hand2.filter((c) => cardSuit(c) !== 3);
|
|
4011
|
+
if (clean.length) return clean;
|
|
4012
|
+
}
|
|
4013
|
+
return hand2;
|
|
4014
|
+
}
|
|
4015
|
+
function finish(s) {
|
|
4016
|
+
if (s.variant === "hearts") {
|
|
4017
|
+
s.scores = s.scores.map((n, i) => n + s.penalties[i]);
|
|
4018
|
+
s.result = `Round penalties: ${s.penalties.join(" \xB7 ")}`;
|
|
4019
|
+
if (s.scores.some((n) => n >= 100)) {
|
|
4020
|
+
s.phase = "finished";
|
|
4021
|
+
s.winner = best(s.scores.map((n) => -n));
|
|
4022
|
+
} else s.phase = "round";
|
|
4023
|
+
} else if (s.variant === "spades") {
|
|
4024
|
+
const gains = [0, 0];
|
|
4025
|
+
for (const team2 of [0, 1]) {
|
|
4026
|
+
const seats2 = [team2, team2 + 2], bid = seats2.reduce((n, i) => n + s.bids[i], 0), taken = seats2.filter((i) => s.bids[i] !== 0).reduce((n, i) => n + s.tricks[i], 0);
|
|
4027
|
+
gains[team2] = taken >= bid ? bid * 10 + Math.max(0, taken - bid) : -bid * 10;
|
|
4028
|
+
const bags = Math.max(0, taken - bid) + seats2.filter((i) => s.bids[i] === 0).reduce((n, i) => n + s.tricks[i], 0);
|
|
4029
|
+
s.bags[team2] += bags;
|
|
4030
|
+
gains[team2] -= 100 * Math.floor(s.bags[team2] / 10);
|
|
4031
|
+
s.bags[team2] %= 10;
|
|
4032
|
+
for (const i of seats2)
|
|
4033
|
+
if (s.bids[i] === 0)
|
|
4034
|
+
gains[team2] += (s.tricks[i] === 0 ? 1 : -1) * (s.blind[i] ? 200 : 100);
|
|
4035
|
+
}
|
|
4036
|
+
s.scores = s.scores.map((n, i) => n + gains[i]);
|
|
4037
|
+
s.result = `Team 1 & 3: ${gains[0]} \xB7 Team 2 & 4: ${gains[1]}`;
|
|
4038
|
+
if (s.scores.some((n) => n >= 500 || n <= -200)) {
|
|
4039
|
+
s.phase = "finished";
|
|
4040
|
+
s.winner = best(s.scores);
|
|
4041
|
+
} else s.phase = "round";
|
|
4042
|
+
} else {
|
|
4043
|
+
s.scores = s.scores.map(
|
|
4044
|
+
(n, i) => n + (s.bids[i] === s.tricks[i] ? 10 + s.tricks[i] : 0)
|
|
4045
|
+
);
|
|
4046
|
+
s.result = `Tricks taken: ${s.tricks.join(" \xB7 ")}`;
|
|
4047
|
+
const rounds = (s.hands.length === 4 ? 13 : 17) + s.hands.length - 1;
|
|
4048
|
+
if (s.round + 1 >= rounds) {
|
|
4049
|
+
s.phase = "finished";
|
|
4050
|
+
s.winner = best(s.scores);
|
|
4051
|
+
} else s.phase = "round";
|
|
4052
|
+
}
|
|
4053
|
+
s.turn = first(s);
|
|
4054
|
+
}
|
|
4055
|
+
function playTrickCards(s, type, at, amount, entropy2) {
|
|
4056
|
+
if (s.phase === "finished") throw Error("This match is finished.");
|
|
4057
|
+
if (type === "next" && s.phase === "round")
|
|
4058
|
+
return deal(
|
|
4059
|
+
{ ...s, round: s.round + 1, dealer: (s.dealer + 1) % s.hands.length },
|
|
4060
|
+
entropy2
|
|
4061
|
+
);
|
|
4062
|
+
const t = structuredClone(s), seat = s.turn, count = s.hands.length;
|
|
4063
|
+
if (s.phase === "pass") {
|
|
4064
|
+
if (type === "select" && at !== void 0 && s.hands[seat].includes(at)) {
|
|
4065
|
+
const selected = t.selected[seat];
|
|
4066
|
+
if (selected.includes(at))
|
|
4067
|
+
t.selected[seat] = selected.filter((c) => c !== at);
|
|
4068
|
+
else if (selected.length < 3) selected.push(at);
|
|
4069
|
+
else throw Error("Select three cards.");
|
|
4070
|
+
return t;
|
|
4071
|
+
}
|
|
4072
|
+
if (type !== "pass" || t.selected[seat].length !== 3)
|
|
4073
|
+
throw Error("Choose three cards to pass.");
|
|
4074
|
+
t.passed[seat] = true;
|
|
4075
|
+
if (t.passed.every(Boolean)) {
|
|
4076
|
+
const offset = [1, 3, 2][s.round % 4];
|
|
4077
|
+
t.hands = t.hands.map(
|
|
4078
|
+
(h, i) => h.filter((c) => !t.selected[i].includes(c))
|
|
4079
|
+
);
|
|
4080
|
+
t.selected.forEach(
|
|
4081
|
+
(cards, i) => t.hands[(i + offset) % 4].push(...cards)
|
|
4082
|
+
);
|
|
4083
|
+
t.hands.forEach((h) => h.sort((a, b) => a - b));
|
|
4084
|
+
t.selected = t.selected.map(() => []);
|
|
4085
|
+
t.phase = "play";
|
|
4086
|
+
t.turn = t.hands.findIndex((h) => h.includes(0));
|
|
4087
|
+
} else t.turn = t.passed.findIndex((n) => !n);
|
|
4088
|
+
return t;
|
|
4089
|
+
}
|
|
4090
|
+
if (s.phase === "blind") {
|
|
4091
|
+
const eligible = s.scores[1 - seat % 2] - s.scores[seat % 2] >= 200;
|
|
4092
|
+
if (type === "blind" && eligible) {
|
|
4093
|
+
t.bids[seat] = 0;
|
|
4094
|
+
t.blind[seat] = true;
|
|
4095
|
+
} else if (type !== "look")
|
|
4096
|
+
throw Error("Choose blind nil or look at your cards.");
|
|
4097
|
+
t.blindAsked[seat] = true;
|
|
4098
|
+
if (t.blindAsked.every(Boolean)) {
|
|
4099
|
+
t.phase = "bid";
|
|
4100
|
+
t.turn = first(t);
|
|
4101
|
+
while (t.bids[t.turn] !== null) t.turn = (t.turn + 1) % count;
|
|
4102
|
+
} else t.turn = (seat + 1) % count;
|
|
4103
|
+
return t;
|
|
4104
|
+
}
|
|
4105
|
+
if (s.phase === "bid") {
|
|
4106
|
+
if (type !== "bid" || amount === void 0 || !index(amount, s.size + 1))
|
|
4107
|
+
throw Error("Choose a legal bid.");
|
|
4108
|
+
if (s.variant === "oh-hell" && s.bids.filter((b) => b !== null).length === count - 1 && s.bids.reduce((n, b) => n + (b ?? 0), 0) + amount === s.size)
|
|
4109
|
+
throw Error("The last bid cannot make the total equal the hand size.");
|
|
4110
|
+
t.bids[seat] = amount;
|
|
4111
|
+
if (t.bids.every((b) => b !== null)) {
|
|
4112
|
+
t.phase = "play";
|
|
4113
|
+
t.turn = first(t);
|
|
4114
|
+
} else {
|
|
4115
|
+
t.turn = (seat + 1) % count;
|
|
4116
|
+
while (t.bids[t.turn] !== null) t.turn = (t.turn + 1) % count;
|
|
4117
|
+
}
|
|
4118
|
+
return t;
|
|
4119
|
+
}
|
|
4120
|
+
if (s.phase === "moon") {
|
|
4121
|
+
if (type !== "moon" || ![0, 1].includes(amount ?? -1))
|
|
4122
|
+
throw Error("Choose how to score the moon.");
|
|
4123
|
+
t.penalties = t.penalties.map(
|
|
4124
|
+
(_, i) => amount === 0 ? i === seat ? -26 : 0 : i === seat ? 0 : 26
|
|
4125
|
+
);
|
|
4126
|
+
finish(t);
|
|
4127
|
+
return t;
|
|
4128
|
+
}
|
|
4129
|
+
if (type !== "play" || at === void 0 || !trickLegalCards(s).includes(at))
|
|
4130
|
+
throw Error("Choose a legal card.");
|
|
4131
|
+
t.hands[seat] = t.hands[seat].filter((c) => c !== at);
|
|
4132
|
+
t.trick.push({ seat, card: at });
|
|
4133
|
+
if (s.variant === "spades" && cardSuit(at) === 3 && s.trick.length > 0 && cardSuit(s.trick[0].card) !== 3 || s.variant === "hearts" && (cardSuit(at) === 2 || at === 49))
|
|
4134
|
+
t.broken = true;
|
|
4135
|
+
t.turn = (seat + 1) % count;
|
|
4136
|
+
if (t.trick.length === count) {
|
|
4137
|
+
const lead = cardSuit(t.trick[0].card), strength = (c) => (cardSuit(c) === s.trump ? 100 : cardSuit(c) === lead ? 50 : 0) + cardRank(c);
|
|
4138
|
+
const winner2 = t.trick.reduce(
|
|
4139
|
+
(a, b) => strength(b.card) > strength(a.card) ? b : a
|
|
4140
|
+
).seat;
|
|
4141
|
+
t.tricks[winner2]++;
|
|
4142
|
+
t.penalties[winner2] += t.trick.reduce((n, c) => n + penalty(c.card), 0);
|
|
4143
|
+
t.lastTrick = t.trick;
|
|
4144
|
+
t.trick = [];
|
|
4145
|
+
t.turn = winner2;
|
|
4146
|
+
if (t.hands.every((h) => !h.length)) {
|
|
4147
|
+
const moon = s.variant === "hearts" ? t.penalties.findIndex((n) => n === 26) : -1;
|
|
4148
|
+
if (moon >= 0) {
|
|
4149
|
+
t.phase = "moon";
|
|
4150
|
+
t.turn = moon;
|
|
4151
|
+
} else finish(t);
|
|
4152
|
+
}
|
|
4153
|
+
}
|
|
4154
|
+
return t;
|
|
4155
|
+
}
|
|
4156
|
+
function trickView(s, seat) {
|
|
4157
|
+
const { stock, selected, ...publicState } = s;
|
|
4158
|
+
return {
|
|
4159
|
+
...publicState,
|
|
4160
|
+
stockCount: stock.length,
|
|
4161
|
+
selected: selected.map((cards, i) => i === seat ? cards : []),
|
|
4162
|
+
hands: s.hands.map(
|
|
4163
|
+
(h, i) => i === seat && !(s.phase === "blind" && !s.blindAsked[i]) ? h : h.map(() => null)
|
|
4164
|
+
)
|
|
4165
|
+
};
|
|
4166
|
+
}
|
|
4167
|
+
|
|
4168
|
+
// lib/multiplayer/gin-rummy.ts
|
|
4169
|
+
var ginRank = (c) => cardRank(c) === 14 ? 1 : cardRank(c);
|
|
4170
|
+
var ginValue = (c) => Math.min(10, ginRank(c));
|
|
4171
|
+
function ginMelds(hand2) {
|
|
4172
|
+
const melds = [];
|
|
4173
|
+
for (let rank2 = 1; rank2 <= 13; rank2++) {
|
|
4174
|
+
const cards = hand2.filter((c) => ginRank(c) === rank2);
|
|
4175
|
+
if (cards.length >= 3) {
|
|
4176
|
+
if (cards.length === 4) {
|
|
4177
|
+
melds.push(cards);
|
|
4178
|
+
for (let i = 0; i < 4; i++) melds.push(cards.filter((_, j) => j !== i));
|
|
4179
|
+
} else melds.push(cards);
|
|
4180
|
+
}
|
|
4181
|
+
}
|
|
4182
|
+
for (let suit = 0; suit < 4; suit++) {
|
|
4183
|
+
const cards = hand2.filter((c) => cardSuit(c) === suit).sort((a, b) => ginRank(a) - ginRank(b));
|
|
4184
|
+
for (let start = 0; start < cards.length; start++)
|
|
4185
|
+
for (let end = start + 2; end < cards.length; end++) {
|
|
4186
|
+
const slice = cards.slice(start, end + 1);
|
|
4187
|
+
if (slice.every(
|
|
4188
|
+
(c, i) => i === 0 || ginRank(c) === ginRank(slice[i - 1]) + 1
|
|
4189
|
+
))
|
|
4190
|
+
melds.push(slice);
|
|
4191
|
+
else break;
|
|
4192
|
+
}
|
|
4193
|
+
}
|
|
4194
|
+
return melds;
|
|
4195
|
+
}
|
|
4196
|
+
function partitions(hand2) {
|
|
4197
|
+
const melds = ginMelds(hand2), out2 = [];
|
|
4198
|
+
function visit(start, used, chosen) {
|
|
4199
|
+
const deadwood2 = hand2.filter((c) => !used.has(c));
|
|
4200
|
+
out2.push({
|
|
4201
|
+
melds: chosen,
|
|
4202
|
+
deadwood: deadwood2,
|
|
4203
|
+
value: deadwood2.reduce((n, c) => n + ginValue(c), 0)
|
|
4204
|
+
});
|
|
4205
|
+
for (let i = start; i < melds.length; i++)
|
|
4206
|
+
if (melds[i].every((c) => !used.has(c)))
|
|
4207
|
+
visit(i + 1, /* @__PURE__ */ new Set([...used, ...melds[i]]), [...chosen, melds[i]]);
|
|
4208
|
+
}
|
|
4209
|
+
visit(0, /* @__PURE__ */ new Set(), []);
|
|
4210
|
+
return out2;
|
|
4211
|
+
}
|
|
4212
|
+
function ginLayout(hand2) {
|
|
4213
|
+
return partitions(hand2).reduce((a, b) => b.value < a.value ? b : a);
|
|
4214
|
+
}
|
|
4215
|
+
function ginLayoffValue(hand2, onto) {
|
|
4216
|
+
const cache = /* @__PURE__ */ new Map();
|
|
4217
|
+
function attach(cards, melds) {
|
|
4218
|
+
const key2 = cards.join(",") + "|" + melds.map((m) => m.join(",")).join(";"), cached = cache.get(key2);
|
|
4219
|
+
if (cached !== void 0) return cached;
|
|
4220
|
+
let best2 = cards.reduce((n, c) => n + ginValue(c), 0);
|
|
4221
|
+
cards.forEach(
|
|
4222
|
+
(c, i) => melds.forEach((m, j) => {
|
|
4223
|
+
const set = m.every((x) => ginRank(x) === ginRank(m[0])), ranks = m.map(ginRank), fits = set ? m.length === 3 && ginRank(c) === ranks[0] : cardSuit(c) === cardSuit(m[0]) && (ginRank(c) === Math.min(...ranks) - 1 || ginRank(c) === Math.max(...ranks) + 1);
|
|
4224
|
+
if (fits) {
|
|
4225
|
+
const next3 = melds.map(
|
|
4226
|
+
(m2, k) => k === j ? [...m2, c].sort((a, b) => ginRank(a) - ginRank(b)) : m2
|
|
4227
|
+
);
|
|
4228
|
+
best2 = Math.min(
|
|
4229
|
+
best2,
|
|
4230
|
+
attach(
|
|
4231
|
+
cards.filter((_, k) => k !== i),
|
|
4232
|
+
next3
|
|
4233
|
+
)
|
|
4234
|
+
);
|
|
4235
|
+
}
|
|
4236
|
+
})
|
|
4237
|
+
);
|
|
4238
|
+
cache.set(key2, best2);
|
|
4239
|
+
return best2;
|
|
4240
|
+
}
|
|
4241
|
+
return Math.min(...partitions(hand2).map((p) => attach(p.deadwood, onto)));
|
|
4242
|
+
}
|
|
4243
|
+
function deal2(dealer, scores2, wins, round, entropy2) {
|
|
4244
|
+
const deck = shuffled(
|
|
4245
|
+
Array.from({ length: 52 }, (_, i) => i),
|
|
4246
|
+
entropy2
|
|
4247
|
+
);
|
|
4248
|
+
return {
|
|
4249
|
+
hands: [
|
|
4250
|
+
deck.slice(0, 10).sort((a, b) => a - b),
|
|
4251
|
+
deck.slice(10, 20).sort((a, b) => a - b)
|
|
4252
|
+
],
|
|
4253
|
+
stock: deck.slice(21),
|
|
4254
|
+
discard: [deck[20]],
|
|
4255
|
+
turn: 1 - dealer,
|
|
4256
|
+
dealer,
|
|
4257
|
+
phase: "offer",
|
|
4258
|
+
offers: 0,
|
|
4259
|
+
drawnDiscard: null,
|
|
4260
|
+
scores: scores2,
|
|
4261
|
+
wins,
|
|
4262
|
+
winner: null,
|
|
4263
|
+
round,
|
|
4264
|
+
result: "",
|
|
4265
|
+
revealed: [],
|
|
4266
|
+
margin: null
|
|
4267
|
+
};
|
|
4268
|
+
}
|
|
4269
|
+
function createGin(entropy2) {
|
|
4270
|
+
return deal2(Math.floor(draws(entropy2, 1)[0] * 2), [0, 0], [0, 0], 0, entropy2);
|
|
4271
|
+
}
|
|
4272
|
+
function playGin(s, type, at, entropy2) {
|
|
4273
|
+
if (type === "next" && s.phase === "round")
|
|
4274
|
+
return deal2(
|
|
4275
|
+
s.result.startsWith("Stock") ? s.dealer : 1 - s.dealer,
|
|
4276
|
+
s.scores,
|
|
4277
|
+
s.wins,
|
|
4278
|
+
s.round + 1,
|
|
4279
|
+
entropy2
|
|
4280
|
+
);
|
|
4281
|
+
if (["finished", "round"].includes(s.phase))
|
|
4282
|
+
throw Error("This round is finished.");
|
|
4283
|
+
const t = structuredClone(s), seat = s.turn;
|
|
4284
|
+
if (type === "pass" && s.phase === "offer") {
|
|
4285
|
+
t.offers++;
|
|
4286
|
+
if (t.offers === 2) {
|
|
4287
|
+
t.phase = "draw";
|
|
4288
|
+
t.turn = 1 - s.dealer;
|
|
4289
|
+
} else t.turn = 1 - seat;
|
|
4290
|
+
return t;
|
|
4291
|
+
}
|
|
4292
|
+
if (["draw", "offer"].includes(s.phase) && ["stock", "upcard"].includes(type)) {
|
|
4293
|
+
if (s.phase === "offer" && type === "stock")
|
|
4294
|
+
throw Error("Take the upcard or pass the opening offer.");
|
|
4295
|
+
if (s.phase === "draw" && s.offers === 2 && s.round === t.round && type === "upcard")
|
|
4296
|
+
throw Error("Both players passed: draw from the stock.");
|
|
4297
|
+
if (s.stock.length <= 2) {
|
|
4298
|
+
t.phase = "round";
|
|
4299
|
+
t.result = "Stock exhausted: round cancelled, same dealer.";
|
|
4300
|
+
t.turn = s.dealer;
|
|
4301
|
+
return t;
|
|
4302
|
+
}
|
|
4303
|
+
const card2 = type === "upcard" ? t.discard.pop() : t.stock.shift();
|
|
4304
|
+
t.drawnDiscard = type === "upcard" ? card2 : null;
|
|
4305
|
+
t.hands[seat].push(card2);
|
|
4306
|
+
t.hands[seat].sort((a, b) => a - b);
|
|
4307
|
+
t.phase = "discard";
|
|
4308
|
+
t.offers = 0;
|
|
4309
|
+
return t;
|
|
4310
|
+
}
|
|
4311
|
+
if (s.phase === "discard" && ["discard", "knock"].includes(type) && at !== void 0 && s.hands[seat].includes(at) && at !== s.drawnDiscard) {
|
|
4312
|
+
t.hands[seat] = t.hands[seat].filter((c) => c !== at);
|
|
4313
|
+
t.discard.push(at);
|
|
4314
|
+
t.drawnDiscard = null;
|
|
4315
|
+
if (type === "knock") {
|
|
4316
|
+
const layout = ginLayout(t.hands[seat]);
|
|
4317
|
+
if (layout.value > 10)
|
|
4318
|
+
throw Error("Knock only with ten or fewer deadwood points.");
|
|
4319
|
+
const other = 1 - seat, opp = layout.value === 0 ? ginLayout(t.hands[other]).value : ginLayoffValue(t.hands[other], layout.melds), winner2 = layout.value === 0 || layout.value < opp ? seat : other;
|
|
4320
|
+
const points = layout.value === 0 ? opp + 25 : winner2 === seat ? opp - layout.value : layout.value - opp + 25;
|
|
4321
|
+
t.scores[winner2] += points;
|
|
4322
|
+
t.wins[winner2]++;
|
|
4323
|
+
t.result = `${layout.value === 0 ? "Gin" : winner2 === seat ? "Knock" : "Undercut"}: Player ${winner2 + 1} scores ${points}. Deadwood ${layout.value} vs ${opp}.`;
|
|
4324
|
+
t.revealed = t.hands.map((h) => ginLayout(h).melds);
|
|
4325
|
+
t.phase = t.scores[winner2] >= 100 ? "finished" : "round";
|
|
4326
|
+
t.turn = winner2;
|
|
4327
|
+
if (t.phase === "finished") {
|
|
4328
|
+
t.winner = winner2;
|
|
4329
|
+
t.margin = t.scores[winner2] + 100 + (t.scores[other] === 0 ? 100 : 0) + 25 * t.wins[winner2] - t.scores[other] - 25 * t.wins[other];
|
|
4330
|
+
t.result += ` Final margin including game and hand bonuses: ${t.margin}.`;
|
|
4331
|
+
}
|
|
4332
|
+
} else {
|
|
4333
|
+
t.phase = "draw";
|
|
4334
|
+
t.turn = 1 - seat;
|
|
4335
|
+
}
|
|
4336
|
+
return t;
|
|
4337
|
+
}
|
|
4338
|
+
throw Error("Choose a legal draw, discard or knock.");
|
|
4339
|
+
}
|
|
4340
|
+
function ginView(s, seat) {
|
|
4341
|
+
const { stock, ...rest } = s;
|
|
4342
|
+
return {
|
|
4343
|
+
...rest,
|
|
4344
|
+
stockCount: stock.length,
|
|
4345
|
+
hands: s.hands.map(
|
|
4346
|
+
(h, i) => i === seat || ["round", "finished"].includes(s.phase) ? h : h.map(() => null)
|
|
4347
|
+
)
|
|
4348
|
+
};
|
|
4349
|
+
}
|
|
4350
|
+
|
|
4351
|
+
// lib/multiplayer/oware.ts
|
|
4352
|
+
var side = (at) => at < 6 ? 0 : 1;
|
|
4353
|
+
function sow(pits, at) {
|
|
4354
|
+
const board = [...pits];
|
|
4355
|
+
let seeds = board[at], last = at;
|
|
4356
|
+
board[at] = 0;
|
|
4357
|
+
while (seeds > 0) {
|
|
4358
|
+
last = (last + 1) % 12;
|
|
4359
|
+
if (last !== at) {
|
|
4360
|
+
board[last]++;
|
|
4361
|
+
seeds--;
|
|
4362
|
+
}
|
|
4363
|
+
}
|
|
4364
|
+
return { board, last };
|
|
4365
|
+
}
|
|
4366
|
+
function owareMoves(s) {
|
|
4367
|
+
if (s.finished) return [];
|
|
4368
|
+
const opponentEmpty = s.pits.every((n, i) => side(i) === s.turn || n === 0);
|
|
4369
|
+
return s.pits.flatMap(
|
|
4370
|
+
(n, at) => n > 0 && side(at) === s.turn && (!opponentEmpty || sow(s.pits, at).board.some((n2, i) => side(i) !== s.turn && n2 > 0)) ? [at] : []
|
|
4371
|
+
);
|
|
4372
|
+
}
|
|
4373
|
+
|
|
4374
|
+
// lib/multiplayer/catalog.ts
|
|
4375
|
+
var multiplayerGames = [
|
|
4376
|
+
{ "id": "amazons", "name": "Amazons", "symbol": "\u265B", "players": "2 players", "description": "Claim territory with queens and arrows. Every move reshapes the board.", "rules": "Four queens per player on a 10 by 10 board. Player 1 starts at A4,D1,G1,J4; Player 2 at A7,D10,G10,J7. Move a queen any unobstructed distance horizontally, vertically or diagonally, then shoot an arrow from its destination along another unobstructed queen line. The arrow permanently blocks its square; it can land on the square just vacated. Complete both steps as one turn. A player with no queen move loses. JSON move uses numeric zero-based row-major from/to and amount for the arrow, 0\u201399. Exact legal wire actions are supplied to the active player." },
|
|
4377
|
+
{ "id": "lines-of-action", "name": "Lines of Action", "symbol": "\u25CF", "players": "2 players", "description": "Bring your scattered stones together in a tactical race for connection.", "rules": "Claude Soucie\u2019s 8 by 8 connection game. Player 1 has six stones on each horizontal edge, Player 2 six on each vertical edge, with corners empty. Move any stone in one straight line exactly as many squares as there are stones of either color on that entire line. Jump over friendly stones but never enemies. Capture an enemy at the destination; never land on a friendly stone. Connect all your remaining stones into one group using orthogonal or diagonal adjacency to win; a single stone is connected. This edition awards simultaneous connection to the mover. An immobile player passes automatically; both immobile or three repetitions with the same player to move draw. This bounded-table edition also draws after 512 moves without connection. JSON move uses numeric row-major from/to, 0\u201363. Exact legal actions are listed." },
|
|
4378
|
+
{ "id": "last-greenhouse", "name": "The Last Greenhouse", "symbol": "\u2667", "players": "2 players \xB7 cooperative", "description": "Two private journals. Five locked rooms. Save the last seed bank together.", "rules": "An original cooperative escape-room story for two keepers. Read your private journal and compare it with your partner\u2019s observation to solve each room. Share publishes your current observation to both players; Pass hands the controls to your partner; Choose selects setting at 0\u20133 from the visible room options. Every action hands over the controls. Wrong answers are safe to retry and are included in the legal-action list. Complete the atrium, prism gallery, seed archive, pump room and dawn canopy together. Only your own private journal and shared notes are projected; spectators see no private journal. Local handoff conceals journals between turns. No competitive rating or Quick Play." },
|
|
4379
|
+
{ id: "euchre", name: "Euchre", symbol: "J", players: "4 players \xB7 2 teams", description: "Call trump, trust your partner, or take all five tricks alone.", rules: "Four players in opposite-seat partnerships. A 24-card deck uses 9 through ace. Each player receives five cards and one card is turned up. Starting left of dealer, order the turned suit or pass. If ordered, the dealer picks up the upcard and discards one card face down. After four passes, call any other suit; four more passes produce a fresh deal with the next dealer. A caller may go alone: their partner sits out. Left of dealer leads, skipping a sitting partner. Follow suit when possible. The trump jack is highest, followed by the other jack of the same color (the left bower, which belongs to trump), then A,K,Q,10,9 of trump. Other suits rank A,K,Q,J,10,9. Trick winner leads. Makers score 1 for three or four tricks, 2 for all five together, or 4 for all five alone. Defenders score 2 if makers take fewer than three. First team to 10 wins. No stick-the-dealer, joker or defensive lone hands. Hands and kitty remain private. Legal actions are enumerated for the active seat." },
|
|
4380
|
+
{ id: "cribbage", name: "Cribbage", symbol: "121", players: "2 players", description: "Choose your crib. Peg the count. Race to 121.", rules: "Two-player six-card cribbage with a standard deck. Aces are low. Each player discards two cards face down to the dealer's crib; the starter is revealed after both discard. A starter jack gives the dealer two points. Non-dealer leads pegging: play one card without exceeding 31, or say Go only when no card fits. Fifteen and thirty-one score two; consecutive pairs, triples and quads score two, six and twelve; the longest trailing run scores its length. The last card before a reset scores one except at 31. After a Go/reset the opponent of the last player leads if holding cards. Count non-dealer's hand, dealer's hand, then crib: fifteens, pairs, longest runs with multiplicities, four/five-card hand flushes, five-card-only crib flush, and one for a jack matching the starter's suit. Counting is automatic; no muggins. Dealer alternates. First to reach 121 at any scoring event wins immediately. Hidden cards and crib stay private until counting; spectators see only public cards. Exact legal JSON actions are supplied in visible_state.legalActions for the active player." },
|
|
4381
|
+
{ id: "fanorona", name: "Fanorona", symbol: "\u2194", players: "2 players", description: "Approach or withdraw. One move can capture a whole line.", rules: "Two players on the traditional 9 by 5 connected-point board, twenty-two stones each. Player 1 starts. Move one stone along a drawn line to an adjacent empty point. Captures are compulsory when available: approach captures the consecutive enemy line beyond the destination; withdrawal captures the consecutive enemy line behind the starting point. Choose one when both apply. After a capture you may continue with the same stone or Stop. A capture sequence cannot revisit a point or repeat the immediately preceding direction. A non-capturing move is allowed only when no capture exists. Capture every opposing stone or leave the opponent without a move to win. This implementation draws at three repetitions of the same position with the same player to move, or eighty completed turns without a capture. Intersections are zero-based row-major: top-left 0, bottom-right 44. JSON move uses from/to plus amount 0 for approach or a quiet move, 1 for withdrawal; stop ends an optional continuation. Exact legal actions are supplied in visible_state.legalActions." },
|
|
4382
|
+
{ "id": "categories", "name": "Categories", "symbol": "A\u2013Z", "players": "2\u20138 players", "description": "One letter. Six categories. Race your friends to a unique answer.", "rules": "Five English-language rounds with Country, City, Animal, Food, Occupation and Object. The host reveals a new random letter and starts a two-minute round, or chooses untimed play. Submit one locked answer sheet; blank answers are allowed. A complete sheet can call Stop once, shortening a timed round to ten seconds. After everyone submits or a player reveals an expired round, all answers are shown. Unsubmitted answers are blank. Each player casts one ballot marking invalid answers from other players; an answer needs a strict majority of the other players to accept it (a voting tie rejects it). Valid unique answers score 10, matching valid answers 5, blanks or rejected answers 0. Highest total after five rounds wins; equal totals draw. Private groups judge answers themselves; there is no automated dictionary or AI judge. Local play uses untimed private handoffs." },
|
|
4383
|
+
{ "id": "cheat", "name": "Cheat", "symbol": "?!", "players": "3\u20136 players", "description": "Play face down. Tell a convincing story. Call the bluff.", "rules": "Deal all 52 cards clockwise. Player 1 claims aces, then claims cycle through 2 to king and back to ace. On your turn place one to four cards face down, claiming the required rank; bluffing is allowed. Before the next play, any other player may challenge, or the next player may accept. A challenge reveals only the latest cards: if any is the wrong rank the claimant takes the whole pile; otherwise the challenger takes it. Accepted cards stay hidden. The next clockwise player then takes their turn with the next required rank. An empty hand wins only after its final claim is accepted or survives a challenge. This table uses an explicit challenge window without a reaction timer." },
|
|
4384
|
+
{ "id": "president", "name": "President", "symbol": "3\u21922", "players": "3\u20136 players", "description": "Beat the group. Pass at the right moment. Finish first.", "rules": "A single deal with a standard 52-card deck, all cards dealt clockwise. Ranks rise from 3 through ace, with 2 highest. The holder of 3 of clubs opens with a group containing it. Lead one to four equal-rank cards; following plays must contain the same number of cards at a strictly higher rank. You may pass, but cannot re-enter until that pile clears. When all other active players pass, the last player to play leads again; if they have finished, the next active player leads. Finishers leave play and receive places in order. Play continues until one player remains; the first finisher is President. No jokers, suit tie-breaks, special twos, revolution or between-deal tribute." },
|
|
4385
|
+
{ "id": "rummy", "name": "Classic Rummy", "symbol": "777", "players": "2\u20134 players", "description": "Draw a card. Build sets and runs. Empty your hand.", "rules": "One deal, no jokers. Two players receive ten cards each; three or four receive seven. Aces are low only. Draw one card from the stock or take the top discard. Then optionally meld sets of three or four equal ranks, or runs of three or more consecutive cards of one suit. You may add held cards to either end of any public run or complete a public set. Keep at least one card and end your turn by discarding; a taken upcard cannot be discarded immediately. First to discard their last card wins and scores the total value of cards held by opponents: ace 1, faces 10, other cards face value. Recycle discards beneath the top card when stock runs out. If stock and recyclable discards are both empty and the next player chooses stock, the deal ends; lowest remaining card value wins, with ties drawn. No initial meld threshold, knock or Rummy 500 scoring." },
|
|
4386
|
+
{ id: "tic-tac-toe", name: "Tic-tac-toe", symbol: "XO", players: "2 players", description: "Nine squares. Three in a row. Your next quick match.", rules: "X starts on a 3 by 3 board. Take turns placing your mark on an empty square. Three marks in a horizontal, vertical or diagonal line wins. A full board without a winner draws." },
|
|
4387
|
+
{ id: "international-draughts", name: "International Draughts", symbol: "\u265B", players: "2 players", description: "Twenty pieces. Flying kings. Capture the longest chain.", rules: "White starts on a 10\xD710 board with twenty pieces per side. Men move one diagonal square forward and capture in either direction. Kings slide diagonally and land on any empty square beyond a captured piece. Captures are compulsory: choose a sequence taking the greatest number of pieces. Captured pieces remain as blockers until the entire sequence ends. A man becomes a king only if its final square is on the promotion row. No pieces or no legal move loses. Threefold repetition, twenty-five successive king moves per player without captures or man moves, and the official sixteen/five-move king endgame limits draw automatically." },
|
|
4388
|
+
{ id: "atomic-chess", name: "Atomic Chess", symbol: "\u265C", players: "2 players", description: "Every capture explodes. Protect your king from the blast.", rules: "Standard chess setup and moves, with explosive captures: the capturing piece, captured piece and adjacent non-pawns disappear. Adjacent pawns survive. En passant explodes at the capturing pawn\u2019s destination. A move that explodes your own king is illegal. Kings cannot capture, but may stand next to one another. Explode the opposing king or deliver checkmate to win. Stalemate, insufficient material, threefold repetition or fifty moves without a pawn move or capture draws. Castling and all four promotions are supported." },
|
|
4389
|
+
{ id: "three-check", name: "Three-check", symbol: "\u2654", players: "2 players", description: "Give three checks to win. Every threat counts.", rules: "Standard chess rules, plus a second way to win: give your opponent check three times. A double check counts as one checking move. Checkmate also wins. Each player\u2019s checks are shown beside the board. Standard castling, en passant and all four promotions apply. Stalemate, insufficient material, threefold repetition or fifty moves without a pawn move or capture draws." },
|
|
4390
|
+
{ id: "connect-four", name: "Connect Four", symbol: "\u25CF", players: "2 players", description: "Drop a piece. Connect four. Watch the next column.", rules: "Two players alternate dropping pieces into one of seven columns on a six-row board. Each piece falls to the lowest empty space. Four or more pieces of your color in a horizontal, vertical or diagonal line wins. A full board without a winner draws." },
|
|
4391
|
+
{ id: "dots-boxes", name: "Dots & Boxes", symbol: "\u25A6", players: "2 players", description: "Complete a square. Claim another turn.", rules: "Players alternate drawing one unused edge on a 3 \xD7 3 box board. Completing the fourth side of a box claims it and gives another turn. An edge can complete two boxes. When all nine boxes are owned, most boxes wins. All information is public." },
|
|
4392
|
+
{ id: "battleships", name: "Battleships", symbol: "\u2693", players: "2 players", description: "Hide your fleet. Read the hits. Find the last ship.", rules: "Each player secretly places five straight ships of lengths 5, 4, 3, 3 and 2 on a 10\xD710 grid. Ships may touch but cannot overlap or leave the grid. Tap a starting square to place the next ship; switch horizontal/vertical or arrange automatically, then lock the fleet. Player 1 sets up first, then Player 2. Player 1 fires first. Take exactly one shot per turn, including after a hit. The target grid marks hits and misses, and reveals a ship when every square is hit. Sink all five enemy ships to win. On one device, hide your fleet before passing it. Online, only your own ships and already-known shot results are sent to your browser." },
|
|
4393
|
+
{
|
|
4394
|
+
id: "thousand",
|
|
4395
|
+
name: "Thousand",
|
|
4396
|
+
symbol: "1000",
|
|
4397
|
+
players: "2\u20134 players",
|
|
4398
|
+
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. Remaining kitty cards 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
|
+
},
|
|
4401
|
+
{
|
|
4402
|
+
id: "binokel",
|
|
4403
|
+
name: "Binokel",
|
|
4404
|
+
symbol: "AA",
|
|
4405
|
+
players: "4 players",
|
|
4406
|
+
description: "Four copies. Two partners. One contract.",
|
|
4407
|
+
rules: "Partners sit opposite. Four copies of jack, queen, king, ten and ace in every suit form an 80-card deck; twenty cards each. Auction starts at 50, rises by one below 60 and by five thereafter. Passed players leave the auction. If the first three pass, dealer plays 50. Declarer chooses trump only from a held queen-king pair; no pair loses the contract. The declaring partnership needs at least 20 meld points. Melds score automatically and their cards are revealed: aces/kings/queens/jacks around; ordinary and royal marriages; spade queen with diamond jack; and a trump A-10-K-Q-J run. Duplicate melds use the full single/double/triple/quadruple table. A royal marriage already in a run is not counted twice. Follow suit and beat the current winner when possible; when void, trump and overtrump when possible. Equal copies favor the first played. Each ace, ten or king taken earns one; last trick earns two. At least 20 trick points are required to score a hand, and at least 20 meld points to add melds. Declarers reaching their bid score their total, otherwise lose the bid. Opponents score qualifying totals. First team to 500 wins; if both reach 500, the declaring team wins."
|
|
4408
|
+
},
|
|
4409
|
+
{
|
|
4410
|
+
id: "skat",
|
|
4411
|
+
name: "Skat",
|
|
4412
|
+
symbol: "J",
|
|
4413
|
+
players: "3 players",
|
|
4414
|
+
description: "Win the bidding duel. Play alone against two.",
|
|
4415
|
+
rules: "Thirty-two cards, seven through ace. Middlehand speaks values to forehand, who holds or passes; rearhand then speaks to the survivor. If both speakers pass without a bid, forehand plays 18 or passes out. Declarer picks up the two-card skat and discards two, or plays from hand. Choose a suit, Grand (only jacks trump) or Null (lose every trick). Jacks rank clubs, spades, hearts, diamonds above suit trumps. Other suit/Grand cards rank A-10-K-Q-9-8-7; Null ranks A-K-Q-J-10-9-8-7. Follow the effective suit; jacks belong to trump in suit/Grand games. Forehand always leads. Suit/Grand need 61 card points, including skat; Null ends in loss on declarer\u2019s first trick. Value multiplies suit base (diamonds 9, hearts 10, spades 11, clubs 12, Grand 24) by matadors, game, hand and Schneider/Schwarz/open levels. Hand games can announce 90 points or all tricks. Null values are 23, hand 35, open 46, open hand 59. Lost or overbid games subtract twice their value, rounding overbids up to a sufficient base multiple. Highest score after eighteen hands wins. No Ramsch, Kontra or tournament bonus variant."
|
|
4416
|
+
},
|
|
4417
|
+
{
|
|
4418
|
+
id: "baska",
|
|
4419
|
+
name: "Baska",
|
|
4420
|
+
symbol: "DQ",
|
|
4421
|
+
players: "4 players",
|
|
4422
|
+
description: "Find your hidden partner in sixteen cards.",
|
|
4423
|
+
rules: "The red (heart-ace) variant, sixteen cards: ace, ten, queen and jack in each suit. Trump strength: heart ace, heart ten, queens clubs-spades-hearts-diamonds, jacks in that order, diamond ace, diamond ten. Other cards keep their printed suits. Old partners hold club and spade queens; holding both creates a silent solo. Wedding pairs that holder with the first available club, spade or heart jack holder. Auction offers Grand, Zolo and their all-trick Du contracts, with alternating counters up to redouble. Grand has only jack trumps. Four tens, aces or queens automatically claim Zolo Bieda, Master or Baska. Follow suit and beat the winner when possible. Card points are A11, 10=10, Q3, J2. Old side needs 53; with 52\u201352, the side that last countered loses. Ordinary/wedding stakes rise for failure to reach 26 (young) or 27 (old), or failure to take a trick; old losses cost twice. Silent solo stakes four, Grand/Zolo five, Du/honor ten, all doubled by counters. Paired winners gain the stake each; a solo winner gains it from each opponent; highest total after sixteen hands wins. Black and three-tens variants are outside this table."
|
|
4424
|
+
},
|
|
4425
|
+
{
|
|
4426
|
+
id: "canasta",
|
|
4427
|
+
name: "Canasta",
|
|
4428
|
+
symbol: "77",
|
|
4429
|
+
players: "2 or 4 players",
|
|
4430
|
+
description: "Build seven-card melds. Break a frozen pile.",
|
|
4431
|
+
rules: "Two 54-card decks, including four jokers. Two players get fifteen cards and draw two; four players get eleven and draw one, sharing melds with the opposite partner. Meld matching natural ranks with at least two natural cards and at least three total; at most three wild twos/jokers, with no more wild than natural. Seven or more cards form a canasta. Opening melds total 15 below zero score, 50 below 1500, 90 below 3000, then 120. Take the entire discard pile only by immediately melding its top; a frozen pile requires two matching natural cards held. Wild cards block pickup and freeze the pile; black threes block pickup except a final meld. Red threes are set aside and replaced when drawn from stock. Black threes meld only when going out. End a turn by discarding one. Going out requires two canastas with two players, one with partners; premature going out loses the match. Natural canastas earn 500, mixed 300; going out 100, concealed opening 200. Red threes earn 100 each plus 400 for all four, negative without a meld. Melded cards score positive, held cards negative. Card values: joker50, A/2=20, 8\u2013K=10, others5. Highest score reaching 5000 wins. Optional partner permission is not required."
|
|
4432
|
+
},
|
|
4433
|
+
{
|
|
4434
|
+
id: "bridge",
|
|
4435
|
+
name: "Bridge",
|
|
4436
|
+
symbol: "NT",
|
|
4437
|
+
players: "4 players",
|
|
4438
|
+
description: "Bid a contract. Plan two hands together.",
|
|
4439
|
+
rules: "Partners sit opposite. A sixteen-board match uses standard duplicate contract scores and the rotating vulnerability cycle. Each player has thirteen cards. Bids increase by level and strain: clubs, diamonds, hearts, spades, no trump. Three passes after a bid end the auction; four opening passes pass out the board. Opponents may double; the declaring side may redouble. Declarer is the first player in the winning partnership to bid the final strain. The player to declarer\u2019s left leads. Dummy is exposed after that lead and declarer controls its plays. Follow suit when possible; highest trump or lead-suit card wins. Contract, game, slam, overtrick and undertrick scores account for doubling and vulnerability. Highest net team score after sixteen boards wins. This table uses raw duplicate scores; Kurnik\u2019s HCP-adjusted IMP table, conventions, claims and tournament director rulings are outside this variant."
|
|
4440
|
+
},
|
|
4441
|
+
{
|
|
4442
|
+
id: "polish-hearts",
|
|
4443
|
+
name: "Polish Hearts",
|
|
4444
|
+
symbol: "\u2665",
|
|
4445
|
+
players: "3\u20134 players",
|
|
4446
|
+
description: "Avoid seven penalties, then win points back.",
|
|
4447
|
+
rules: "Seven penalty hands: each trick costs 20; each heart 20; queen 60; king or jack 30; heart king 150; seventh and last tricks 75 each; the seventh hand combines all penalties. Follow suit, with no obligation to beat the current winner. Hearts cannot lead while another suit is held in the heart, heart-king and combined hands. Next, each player deals a trump hand and chooses trump from the first five cards, before the rest are dealt; each trick earns 25. Finally, build four suit columns around the dealer\u2019s opening rank; extend a column by one rank, or start another at that opening rank. Play whenever possible. First out earns 800, second 500. Three-player games omit the club two, use six cards for the trump choice, cost 15 per trick, 140 for the heart king and 70 per special trick, earn 20 per trump trick and award 790/485 for the final layout. Highest total wins."
|
|
4448
|
+
},
|
|
4449
|
+
{
|
|
4450
|
+
id: "durak",
|
|
4451
|
+
name: "Durak",
|
|
4452
|
+
symbol: "\u2660",
|
|
4453
|
+
players: "2\u20134 players",
|
|
4454
|
+
description: "Beat the attack. Empty your hand.",
|
|
4455
|
+
rules: "Individual throw-in Durak with 36 cards, six through ace. Deal six each; the bottom stock card shows trump and is drawn last. The lowest dealt trump holder opens. Attack with one card, then add ranks already on the table, including defended ranks. The defender beats a card with a higher card of the same suit or any trump over a nontrump. Attacks are capped at six cards or the defender\u2019s starting hand size. The main attacker has first priority; finishing offers other attackers a clockwise turn. Taking opens a final throw-in window, then the defender collects all table cards and skips their next attack. Successful defense discards the table and the defender attacks next. Refill the main attacker first, other players clockwise and defender last. Empty hands leave once the stock runs out. The last player holding cards loses; simultaneous empty hands draw. This table has no transfer attacks, teams or optional trump-six exchange."
|
|
4456
|
+
},
|
|
4457
|
+
{
|
|
4458
|
+
id: "makao",
|
|
4459
|
+
name: "Makao",
|
|
4460
|
+
symbol: "Q",
|
|
4461
|
+
players: "2\u20134 players",
|
|
4462
|
+
description: "Match suits and ranks. Counter the demand.",
|
|
4463
|
+
rules: "Five cards each from a 52-card deck. Match the pile\u2019s suit or rank, or play a queen on an ordinary card; any ordinary card fits a queen. Groups of the same rank can be played together, with the first card matching. Twos and threes add draw penalties; the heart king adds five to the next player, the spade king to the previous player. Penalties can be stacked with any fighting card. Fours add waiting turns, countered with fours. Jacks request an ordinary rank for one complete cycle, or cancel a request; aces request a suit for the next player. Queens do not override demands. A normal draw offers only that new card for immediate play. Say Makao with the checkbox when one card remains; the next player can catch a missed declaration for five cards. When both stock and reusable discards run out, every player returns a random half of their hand, rounded down. The first empty hand wins. No jokers or sequence-chain variant."
|
|
4464
|
+
},
|
|
4465
|
+
{
|
|
4466
|
+
id: "three-five-eight",
|
|
4467
|
+
name: "3-5-8",
|
|
4468
|
+
symbol: "358",
|
|
4469
|
+
players: "3 players",
|
|
4470
|
+
description: "Meet your quota. Exchange for stronger cards.",
|
|
4471
|
+
rules: "Eighteen hands with a 52-card deck. Sixteen cards each and a four-card kitty. Dealer targets eight tricks, the next player five, the third three. Each player chooses each of the six contracts once on their own deals: four trump suits, no trump and no tricks. Discard four before taking the kitty. Follow suit; if void, play trump when held. Highest trump or highest lead suit wins. Ordinary hands score tricks minus quota; no-tricks reverses that score. Before subsequent hands, players above their previous quota exchange one unwanted card per surplus trick with players below quota; the receiver automatically returns their highest card in its suit. Higher current quotas exchange first. Highest total after eighteen hands wins. This uses the eighteen-hand contract variant rather than the twelve-trick sudden-win variant."
|
|
4472
|
+
},
|
|
4473
|
+
{
|
|
4474
|
+
id: "property-trading",
|
|
4475
|
+
name: "Property Trading",
|
|
4476
|
+
symbol: "\u2302",
|
|
4477
|
+
players: "2\u20134 players",
|
|
4478
|
+
description: "Build a garden estate. Bargain for a complete street.",
|
|
4479
|
+
rules: "An original forty-space estate board with classic property trading rules. Each player starts with 1,500 points; passing Start earns 200. Roll two dice, buy unowned properties or auction them to all players. Doubles earn another roll; three consecutive doubles send you to jail. Collect rents, complete color groups and build evenly using the finite bank of 32 houses and 12 hotels. Mortgage unbuilt properties for half their price; redeem for 60%. Trade unbuilt properties and cash by an explicit offer and acceptance; receiving a mortgaged property costs 10% of its price. Jail ends with doubles, a release card or a payment of 50; the third failed roll forces payment. Income tax is 200 or 10% of estate value; luxury tax is 75. Free Park pays nothing. Resolve debts by selling buildings or mortgaging assets; bankruptcy transfers an estate to the creditor, or auctions it for bank debts. The last solvent player wins. Free points only."
|
|
4480
|
+
},
|
|
4481
|
+
{
|
|
4482
|
+
id: "draw-guess",
|
|
4483
|
+
name: "Draw & Guess",
|
|
4484
|
+
symbol: "\u270E",
|
|
4485
|
+
players: "2\u201320 players",
|
|
4486
|
+
description: "A private word. A shared sketch.",
|
|
4487
|
+
rules: "Take turns drawing one of three private English prompts. You have two minutes. Other players guess while the drawing updates. A correct guess earns 1\u201345 points based on remaining time, and the drawer earns up to five more, capped at 45, for that guess. Each player can score once per round. If everyone guesses, the round ends early. No correct guesses costs the drawer ten. First to 100 ends the match; highest score wins. Draw pictures rather than writing the word. Local play offers a player switch and hides the prompt from guessers."
|
|
4488
|
+
},
|
|
4489
|
+
{
|
|
4490
|
+
id: "letter-tiles",
|
|
4491
|
+
name: "Letter Tiles",
|
|
4492
|
+
symbol: "A",
|
|
4493
|
+
players: "2\u20134 players",
|
|
4494
|
+
description: "Build a crossword. Make every letter count.",
|
|
4495
|
+
rules: "American English Scrabble-style home scoring on the standard 15 \xD7 15 premium-square layout, with 100 tiles and seven per rack. First word: at least two letters covering the center double-word square. Place connected tiles in one row or column without gaps; every new or crossed word scores, including existing letters. English values: A E I L N O R S T U 1; D G 2; B C M P 3; F H V W Y 4; K 5; J X 8; Q Z 10. Blanks stay zero and keep their chosen letter. DL/TL multiply each newly placed letter; DW/TW multiply its whole word after letter bonuses. New premiums apply to every word using that tile, once on its placement turn; multiple word multipliers multiply together. Using seven new tiles adds 50 after all multipliers. Next player accepts or challenges against our ENABLE English word-game list with QI/ZA additions; accepted words differ from official NWL/Collins lists. Invalid words return the tiles and lose the proposer\u2019s turn; a failed challenge loses the challenger\u2019s next turn. Exchange any number of rack tiles as often as needed, using a turn, when at least seven remain in the bag. Everyone passing twice ends play, as does emptying a rack with an empty bag. Deduct unplayed tiles; the player going out also receives opponents\u2019 deductions. Highest final score wins; a final tie favors the higher pre-adjustment score. Earlier rounds retain their original rules until restarted."
|
|
4496
|
+
},
|
|
4497
|
+
{
|
|
4498
|
+
id: "pan",
|
|
4499
|
+
name: "Pan",
|
|
4500
|
+
symbol: "\u2667",
|
|
4501
|
+
players: "2\u20134 players",
|
|
4502
|
+
description: "Shed your cards before the pile turns.",
|
|
4503
|
+
rules: "A 24-card deck, nine through ace. The holder of the heart nine opens with that card or all four nines. Play one card or all four of a rank, no lower than the pile top. With only the heart nine on the pile, the other three nines can be played together. Instead of playing, take up to three top cards, leaving the heart nine. Empty hands leave the round. The last player receives a P, then A, then N; three letters loses the match. A last player who can immediately shed one or four aces draws the round. This table uses the ordinary clockwise variant."
|
|
4504
|
+
},
|
|
4505
|
+
{
|
|
4506
|
+
id: "gin-rummy",
|
|
4507
|
+
name: "Gin Rummy",
|
|
4508
|
+
symbol: "\u2666",
|
|
4509
|
+
players: "2 players",
|
|
4510
|
+
description: "Build runs and sets. Know when to knock.",
|
|
4511
|
+
rules: "Ten cards each. The dealer\u2019s opponent may take the opening upcard; if passed, the dealer may take it. If both pass, draw from stock. Draw one, then discard another; you cannot return the upcard just drawn. Meld sets of three/four equal ranks or same-suit runs of at least three, ace low. Knock when the remaining deadwood is at most ten: ace one, face cards ten. Melds and opponent layoffs are optimized automatically. Gin earns deadwood difference plus 25; an equal or lower opponent deadwood undercuts for 25 plus difference. At 100, the game ends with game and hand bonuses. Two cards left in stock cancels the round with the same dealer."
|
|
4512
|
+
},
|
|
4513
|
+
{
|
|
3938
4514
|
id: "sixty-six",
|
|
3939
4515
|
name: "Sixty-Six",
|
|
3940
4516
|
symbol: "\u2654",
|
|
3941
4517
|
players: "2 players",
|
|
3942
4518
|
description: "Claim sixty-six. Close the stock with care.",
|
|
3943
|
-
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,
|
|
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."
|
|
3944
4520
|
},
|
|
3945
4521
|
{
|
|
3946
4522
|
id: "spades",
|
|
@@ -4395,338 +4971,186 @@ var action_catalog_default = {
|
|
|
4395
4971
|
"move",
|
|
4396
4972
|
"stop"
|
|
4397
4973
|
],
|
|
4398
|
-
euchre: [
|
|
4399
|
-
"pass",
|
|
4400
|
-
"trump",
|
|
4401
|
-
"discard",
|
|
4402
|
-
"play",
|
|
4403
|
-
"next"
|
|
4404
|
-
],
|
|
4405
|
-
amazons: [
|
|
4406
|
-
"move"
|
|
4407
|
-
],
|
|
4408
|
-
"lines-of-action": [
|
|
4409
|
-
"move"
|
|
4410
|
-
],
|
|
4411
|
-
"last-greenhouse": [
|
|
4412
|
-
"share",
|
|
4413
|
-
"pass",
|
|
4414
|
-
"choose"
|
|
4415
|
-
]
|
|
4416
|
-
};
|
|
4417
|
-
|
|
4418
|
-
// lib/agents/action-help.ts
|
|
4419
|
-
var help = {
|
|
4420
|
-
amazons: "move: numeric from/to 0\u201399 and amount for the arrow square. Select an exact legal_wire_actions entry.",
|
|
4421
|
-
"lines-of-action": "move: numeric from/to 0\u201363. Move the count of all stones along the full line; enemy stones block jumping.",
|
|
4422
|
-
"last-greenhouse": "share: publish your current private journal observation. pass: hand controls to partner. choose: at 0\u20133 selects a room setting. All choices are listed, including incorrect answers. Cooperate using both journals.",
|
|
4423
|
-
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.",
|
|
4424
|
-
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.",
|
|
4425
|
-
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.",
|
|
4426
|
-
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. Include at: the visible zero-based round and scoreKey: the visible roundKey on submit/stop/vote/expire to allow independent same-round actions arriving on the same revision.`,
|
|
4427
|
-
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.",
|
|
4428
|
-
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.",
|
|
4429
|
-
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.",
|
|
4430
|
-
"connect-four": "place: at is a zero-based column (0\u20136). Convenience: --column 1\u20137.",
|
|
4431
|
-
"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.",
|
|
4432
|
-
chess: "move: from/to are algebraic squares; promotion is q/r/b/n.",
|
|
4433
|
-
"atomic-chess": "move: from/to are algebraic squares; promotion is q/r/b/n. Explosions follow the game rules.",
|
|
4434
|
-
"three-check": "move: from/to are algebraic squares; promotion is q/r/b/n. Check counts are in visible_state.",
|
|
4435
|
-
checkers: "move: from/to are zero-based row-major squares (0\u201363); continue a mandatory capture chain. Convenience flags use algebraic squares.",
|
|
4436
|
-
"brazilian-checkers": "move: from/to are zero-based squares (0\u201363); maximum captures apply.",
|
|
4437
|
-
"international-draughts": "move: from/to are zero-based squares (0\u201399). sequence: cards is a complete legal path of square indexes, including the origin.",
|
|
4438
|
-
reversi: "place: at is a zero-based row-major square (0\u201363). Unavailable turns pass automatically.",
|
|
4439
|
-
gomoku: "place: at is a zero-based row-major intersection (0\u2013224). Exactly five wins.",
|
|
4440
|
-
hex: "place: at is a zero-based row-major hexagon. swap: no payload, only the second seat's first turn. Convenience: --row/--column or --swap.",
|
|
4441
|
-
"dots-boxes": "place: at is a zero-based edge index; legal_actions and --edge use the same edge one-based (edge N is visible_state.edges[N-1]). Horizontal edges precede vertical edges in row-major order. Edges and box owners are in visible_state. Convenience: --edge is one-based.",
|
|
4442
|
-
oware: "sow: at is a zero-based pit index, 0\u20135 for seat 0 and 6\u201311 for seat 1. Convenience: --pit is one-based.",
|
|
4443
|
-
go: "place/dead: at is a zero-based row-major intersection (board_labeled shows row start indexes). pass/resume: no payload. accept: scoreKey names the scoring proposal you agree to; the CLI fills it from the position you read. Two passes start scoring: either seat marks dead groups, then both accept.",
|
|
4444
|
-
battleships: "auto/clear/ready: no payload. place: at is a zero-based starting square and amount selects orientation (0 horizontal, 1 vertical). fire: at is a zero-based target square. Inspect your fleet/setup phase; enemy ships remain hidden.",
|
|
4445
|
-
mills: "place/capture: at is a zero-based point index. move: from is the origin and at the destination. Completing a mill requires a subsequent capture action.",
|
|
4446
|
-
dice: "roll: no payload. hold: at is a die index (0\u20134) to toggle its hold after rolling. score: at is a category index. Inspect the roll phase and score sheet.",
|
|
4447
|
-
ludo: "roll: no payload. move: at is the index (0\u20133) of your token. Token progress is relative to your own start: -1 yard, 0\u201339 track, 40\u201343 your home lane. Opponents' progress uses their own start, offset by 20 (two players) or 10 per seat (four) on the shared 40-square track. A 6 leaves the yard and keeps the turn.",
|
|
4448
|
-
domino: "tile: at is your zero-based hand position (not a pip value) and amount selects the chain end: 0 left, 1 right. draw: draw until you can play, or pass when the stock is empty. next: start the following round. Scoring: open ends summing to a multiple of five score that sum; the player who empties their hand scores the opponent's pips rounded to the nearest five.",
|
|
4449
|
-
football: "place: at is a zero-based destination point. The state records ball, used edges and continuing rebounds.",
|
|
4450
|
-
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.",
|
|
4451
|
-
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.",
|
|
4452
|
-
xiangqi: "move: numeric zero-based row-major from/to on the 9\xD710 board. promotion is unused.",
|
|
4453
|
-
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.",
|
|
4454
|
-
durak: "play: at is your card ID from the visible hand; take/pass: no payload. Follow attack/defence roles and visible trump/table.",
|
|
4455
|
-
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.",
|
|
4456
|
-
pan: "play: cards is an array of card IDs from the visible hand; take/next: no payload. Follow the table rank and legal group rules.",
|
|
4457
|
-
"gin-rummy": "stock/upcard/pass/next: no payload. discard/knock: at is a card ID from the visible hand. Inspect draw/discard phase and deadwood rules.",
|
|
4458
|
-
"sixty-six": "play: at is a card ID from the visible hand; promotion:'marriage' or 'marriage-claim' declares. exchange/close/claim/next are phase-specific commands.",
|
|
4459
|
-
hearts: "select: at is a card ID from the visible hand for passing, then pass. play: at is a card ID from the visible hand. moon: amount selects a scoring choice. Follow phase, lead suit and legal cards.",
|
|
4460
|
-
spades: "bid: amount is the bid; blind/look for blind nil where offered. play: at is a card ID from the visible hand. Follow partner, phase and lead suit.",
|
|
4461
|
-
"oh-hell": "bid: amount is the bid. play: at is a card ID from the visible hand. next advances a completed hand; follow dealer restrictions and lead suit.",
|
|
4462
|
-
"three-five-eight": "exchange: at is one card ID from your visible hand. discard: cards is exactly four distinct card IDs from your visible hand. play: at is one card ID from your visible hand. contract: amount 0\u20133 chooses a suit, 4 no trumps, 5 misere. next advances the hand.",
|
|
4463
|
-
bridge: "bid: amount 1\u201335 encodes level floor((amount-1)/5)+1 and strain (amount-1)%5; pass/double/redouble have no payload. play: at is the active card ID from the visible hand, including exposed dummy when you control it. Follow auction, strain and lead-suit rules.",
|
|
4464
|
-
"polish-hearts": "trump: amount is a suit index; pass: no payload, only when no layout card fits; play: at is a card ID from the visible hand. Follow the current penalty/trump/layout phase.",
|
|
4465
|
-
thousand: "bid/declare: amount is points. kitty: at selects a kitty. exchange: cards is card IDs from the visible hand. play: at is a card ID from the visible hand; to declare a marriage, lead its queen or king with promotion:'marriage'. pass/bomb/redeal/next depend on phase. Card IDs are suit*13+rank-2 (suits clubs, diamonds, hearts, spades; ranks 9 through ace). next: the declarer deals the next hand.",
|
|
4466
|
-
binokel: "bid: amount is points; trump: amount is a suit; play: at is a card ID from the visible hand. pass/next have no payload. Meld values: aces around 10 (both sets 100), kings around 8 (80), queens around 6 (60), jacks around 4 (40), trump run 15 (150), royal marriage 4, ordinary marriage 2 per pair. A bid is the team's trick points plus melds; tricks hold about 240 points per hand, so bids far above that rarely make. The declaring team needs 20 meld points.",
|
|
4467
|
-
skat: "bid: amount is an auction value; hold/pass/hand/pickup act in auction/pickup phases (after a hold the bidder must bid higher or pass). discard: cards is exactly two card IDs from your visible hand. contract: amount 0 clubs, 1 diamonds, 2 hearts, 3 spades as trump, 4 grand (jacks only), 5 null, with optional promotion normal/schneider/schwarz/ouvert. play: at is a card ID from the visible hand. Jacks rank clubs, spades, hearts, diamonds. Card IDs are suit*13+rank-2 (suits clubs, diamonds, hearts, spades). next: the previous declarer deals the next hand.",
|
|
4468
|
-
baska: "announce: no payload in the honor phase. contract: amount is one of 1,3,4,5,6 and must exceed the current contract. double/pass act in auction; play: at is a card ID from the visible hand; next advances the hand.",
|
|
4469
|
-
canasta: "stock/next: no payload. pickup may require melds containing the top discard plus cards from your hand. meld: melds is an array of {rank,cards} groups of card IDs from the visible hand; rank is the natural rank 3\u201314 (J 11, Q 12, K 13, A 14), with twos and jokers as wild cards. cards_labeled names each card. discard: at is a card ID from the visible hand. Follow opening meld and canasta requirements.",
|
|
4470
|
-
"letter-tiles": "place: placements is an array of {at,rack,letter?}; at is a board index and rack a rack index, letter assigns a blank. exchange: cards is rack indexes. accept/challenge/pass/next depend on phase.",
|
|
4471
|
-
"draw-guess": "choose: at selects one of your offered prompts. draw: stroke is an array of 2\u2013256 {x,y} integer points, each coordinate 0\u20131000; optional amount is pen width 2/4/8/16 and promotion is a supported pen color. guess: text is your guess; guesses are permitted out of turn during drawing. clear/expire/next depend on phase. Hidden prompts stay hidden from guessers.",
|
|
4472
|
-
"property-trading": "roll/end/buy/auction/pay/bankrupt/pass/reject have phase-specific effects. bid: amount is the bid. mortgage/redeem/build/sell: at selects a property. tax: amount is 0 or 1. release: amount 0 pays fifty, 1 uses a release card. offer: tradeOffer contains seat,give,take,giveCash,takeCash; accept confirms the current trade."
|
|
4974
|
+
euchre: [
|
|
4975
|
+
"pass",
|
|
4976
|
+
"trump",
|
|
4977
|
+
"discard",
|
|
4978
|
+
"play",
|
|
4979
|
+
"next"
|
|
4980
|
+
],
|
|
4981
|
+
amazons: [
|
|
4982
|
+
"move"
|
|
4983
|
+
],
|
|
4984
|
+
"lines-of-action": [
|
|
4985
|
+
"move"
|
|
4986
|
+
],
|
|
4987
|
+
"last-greenhouse": [
|
|
4988
|
+
"share",
|
|
4989
|
+
"pass",
|
|
4990
|
+
"choose"
|
|
4991
|
+
]
|
|
4473
4992
|
};
|
|
4474
|
-
function actionHelp(game) {
|
|
4475
|
-
return { format: `game play --room CODE --revision R --action '{"type":"...",...}'`, indexing: "Board and hand-position indexes are zero-based. Card IDs are the actual values in visible_state, not offsets into a hand array. Convenience --row/--column/--edge/--pit flags are one-based. Inspect the exact player-visible state; do not guess hidden cards or board indexes.", game: help[game], validation: "action_types lists permitted command families, not currently legal moves. The server enforces phases, turns, ownership and legality. After a rejected or uncertain action read status again; never retry blindly." };
|
|
4476
|
-
}
|
|
4477
4993
|
|
|
4478
|
-
// lib/multiplayer/
|
|
4479
|
-
var
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
4489
|
-
|
|
4490
|
-
|
|
4491
|
-
|
|
4492
|
-
|
|
4493
|
-
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
|
|
4506
|
-
|
|
4507
|
-
|
|
4508
|
-
|
|
4509
|
-
|
|
4510
|
-
|
|
4511
|
-
|
|
4512
|
-
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
|
|
4517
|
-
|
|
4518
|
-
|
|
4519
|
-
|
|
4520
|
-
|
|
4521
|
-
|
|
4522
|
-
|
|
4523
|
-
|
|
4524
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
4527
|
-
|
|
4528
|
-
|
|
4529
|
-
|
|
4530
|
-
stock: [],
|
|
4531
|
-
trick: [],
|
|
4532
|
-
lastTrick: [],
|
|
4533
|
-
tricks: [],
|
|
4534
|
-
penalties: [],
|
|
4535
|
-
bids: [],
|
|
4536
|
-
blind: [],
|
|
4537
|
-
blindAsked: [],
|
|
4538
|
-
selected: [],
|
|
4539
|
-
passed: [],
|
|
4540
|
-
scores: Array(variant === "spades" ? 2 : count).fill(0),
|
|
4541
|
-
bags: [0, 0],
|
|
4542
|
-
trump: null,
|
|
4543
|
-
broken: false,
|
|
4544
|
-
winner: null,
|
|
4545
|
-
result: ""
|
|
4546
|
-
},
|
|
4547
|
-
entropy2
|
|
4548
|
-
);
|
|
4549
|
-
}
|
|
4550
|
-
function trickLegalCards(s) {
|
|
4551
|
-
if (s.phase !== "play") return [];
|
|
4552
|
-
const hand2 = s.hands[s.turn].filter((c) => c !== null), lead = s.trick[0];
|
|
4553
|
-
if (lead) {
|
|
4554
|
-
const suited = hand2.filter((c) => cardSuit(c) === cardSuit(lead.card));
|
|
4555
|
-
if (suited.length) return suited;
|
|
4556
|
-
if (s.variant === "hearts" && s.tricks.every((n) => n === 0)) {
|
|
4557
|
-
const clean = hand2.filter((c) => !penalty(c));
|
|
4558
|
-
if (clean.length) return clean;
|
|
4559
|
-
}
|
|
4560
|
-
return hand2;
|
|
4561
|
-
}
|
|
4562
|
-
if (s.variant === "hearts") {
|
|
4563
|
-
if (s.tricks.every((n) => n === 0)) return hand2.includes(0) ? [0] : [];
|
|
4564
|
-
if (!s.broken) {
|
|
4565
|
-
const clean = hand2.filter((c) => cardSuit(c) !== 2);
|
|
4566
|
-
if (clean.length) return clean;
|
|
4567
|
-
}
|
|
4568
|
-
}
|
|
4569
|
-
if (s.variant === "spades" && !s.broken) {
|
|
4570
|
-
const clean = hand2.filter((c) => cardSuit(c) !== 3);
|
|
4571
|
-
if (clean.length) return clean;
|
|
4572
|
-
}
|
|
4573
|
-
return hand2;
|
|
4574
|
-
}
|
|
4575
|
-
function finish(s) {
|
|
4576
|
-
if (s.variant === "hearts") {
|
|
4577
|
-
s.scores = s.scores.map((n, i) => n + s.penalties[i]);
|
|
4578
|
-
s.result = `Round penalties: ${s.penalties.join(" \xB7 ")}`;
|
|
4579
|
-
if (s.scores.some((n) => n >= 100)) {
|
|
4580
|
-
s.phase = "finished";
|
|
4581
|
-
s.winner = best(s.scores.map((n) => -n));
|
|
4582
|
-
} else s.phase = "round";
|
|
4583
|
-
} else if (s.variant === "spades") {
|
|
4584
|
-
const gains = [0, 0];
|
|
4585
|
-
for (const team2 of [0, 1]) {
|
|
4586
|
-
const seats2 = [team2, team2 + 2], bid = seats2.reduce((n, i) => n + s.bids[i], 0), taken = seats2.filter((i) => s.bids[i] !== 0).reduce((n, i) => n + s.tricks[i], 0);
|
|
4587
|
-
gains[team2] = taken >= bid ? bid * 10 + Math.max(0, taken - bid) : -bid * 10;
|
|
4588
|
-
const bags = Math.max(0, taken - bid) + seats2.filter((i) => s.bids[i] === 0).reduce((n, i) => n + s.tricks[i], 0);
|
|
4589
|
-
s.bags[team2] += bags;
|
|
4590
|
-
gains[team2] -= 100 * Math.floor(s.bags[team2] / 10);
|
|
4591
|
-
s.bags[team2] %= 10;
|
|
4592
|
-
for (const i of seats2)
|
|
4593
|
-
if (s.bids[i] === 0)
|
|
4594
|
-
gains[team2] += (s.tricks[i] === 0 ? 1 : -1) * (s.blind[i] ? 200 : 100);
|
|
4595
|
-
}
|
|
4596
|
-
s.scores = s.scores.map((n, i) => n + gains[i]);
|
|
4597
|
-
s.result = `Team 1 & 3: ${gains[0]} \xB7 Team 2 & 4: ${gains[1]}`;
|
|
4598
|
-
if (s.scores.some((n) => n >= 500 || n <= -200)) {
|
|
4599
|
-
s.phase = "finished";
|
|
4600
|
-
s.winner = best(s.scores);
|
|
4601
|
-
} else s.phase = "round";
|
|
4602
|
-
} else {
|
|
4603
|
-
s.scores = s.scores.map(
|
|
4604
|
-
(n, i) => n + (s.bids[i] === s.tricks[i] ? 10 + s.tricks[i] : 0)
|
|
4605
|
-
);
|
|
4606
|
-
s.result = `Tricks taken: ${s.tricks.join(" \xB7 ")}`;
|
|
4607
|
-
const rounds = (s.hands.length === 4 ? 13 : 17) + s.hands.length - 1;
|
|
4608
|
-
if (s.round + 1 >= rounds) {
|
|
4609
|
-
s.phase = "finished";
|
|
4610
|
-
s.winner = best(s.scores);
|
|
4611
|
-
} else s.phase = "round";
|
|
4612
|
-
}
|
|
4613
|
-
s.turn = first(s);
|
|
4614
|
-
}
|
|
4615
|
-
function playTrickCards(s, type, at, amount, entropy2) {
|
|
4616
|
-
if (s.phase === "finished") throw Error("This match is finished.");
|
|
4617
|
-
if (type === "next" && s.phase === "round")
|
|
4618
|
-
return deal(
|
|
4619
|
-
{ ...s, round: s.round + 1, dealer: (s.dealer + 1) % s.hands.length },
|
|
4620
|
-
entropy2
|
|
4621
|
-
);
|
|
4622
|
-
const t = structuredClone(s), seat = s.turn, count = s.hands.length;
|
|
4623
|
-
if (s.phase === "pass") {
|
|
4624
|
-
if (type === "select" && at !== void 0 && s.hands[seat].includes(at)) {
|
|
4625
|
-
const selected = t.selected[seat];
|
|
4626
|
-
if (selected.includes(at))
|
|
4627
|
-
t.selected[seat] = selected.filter((c) => c !== at);
|
|
4628
|
-
else if (selected.length < 3) selected.push(at);
|
|
4629
|
-
else throw Error("Select three cards.");
|
|
4630
|
-
return t;
|
|
4631
|
-
}
|
|
4632
|
-
if (type !== "pass" || t.selected[seat].length !== 3)
|
|
4633
|
-
throw Error("Choose three cards to pass.");
|
|
4634
|
-
t.passed[seat] = true;
|
|
4635
|
-
if (t.passed.every(Boolean)) {
|
|
4636
|
-
const offset = [1, 3, 2][s.round % 4];
|
|
4637
|
-
t.hands = t.hands.map(
|
|
4638
|
-
(h, i) => h.filter((c) => !t.selected[i].includes(c))
|
|
4639
|
-
);
|
|
4640
|
-
t.selected.forEach(
|
|
4641
|
-
(cards, i) => t.hands[(i + offset) % 4].push(...cards)
|
|
4642
|
-
);
|
|
4643
|
-
t.hands.forEach((h) => h.sort((a, b) => a - b));
|
|
4644
|
-
t.selected = t.selected.map(() => []);
|
|
4645
|
-
t.phase = "play";
|
|
4646
|
-
t.turn = t.hands.findIndex((h) => h.includes(0));
|
|
4647
|
-
} else t.turn = t.passed.findIndex((n) => !n);
|
|
4648
|
-
return t;
|
|
4649
|
-
}
|
|
4650
|
-
if (s.phase === "blind") {
|
|
4651
|
-
const eligible = s.scores[1 - seat % 2] - s.scores[seat % 2] >= 200;
|
|
4652
|
-
if (type === "blind" && eligible) {
|
|
4653
|
-
t.bids[seat] = 0;
|
|
4654
|
-
t.blind[seat] = true;
|
|
4655
|
-
} else if (type !== "look")
|
|
4656
|
-
throw Error("Choose blind nil or look at your cards.");
|
|
4657
|
-
t.blindAsked[seat] = true;
|
|
4658
|
-
if (t.blindAsked.every(Boolean)) {
|
|
4659
|
-
t.phase = "bid";
|
|
4660
|
-
t.turn = first(t);
|
|
4661
|
-
while (t.bids[t.turn] !== null) t.turn = (t.turn + 1) % count;
|
|
4662
|
-
} else t.turn = (seat + 1) % count;
|
|
4663
|
-
return t;
|
|
4664
|
-
}
|
|
4665
|
-
if (s.phase === "bid") {
|
|
4666
|
-
if (type !== "bid" || amount === void 0 || !index(amount, s.size + 1))
|
|
4667
|
-
throw Error("Choose a legal bid.");
|
|
4668
|
-
if (s.variant === "oh-hell" && s.bids.filter((b) => b !== null).length === count - 1 && s.bids.reduce((n, b) => n + (b ?? 0), 0) + amount === s.size)
|
|
4669
|
-
throw Error("The last bid cannot make the total equal the hand size.");
|
|
4670
|
-
t.bids[seat] = amount;
|
|
4671
|
-
if (t.bids.every((b) => b !== null)) {
|
|
4672
|
-
t.phase = "play";
|
|
4673
|
-
t.turn = first(t);
|
|
4674
|
-
} else {
|
|
4675
|
-
t.turn = (seat + 1) % count;
|
|
4676
|
-
while (t.bids[t.turn] !== null) t.turn = (t.turn + 1) % count;
|
|
4677
|
-
}
|
|
4678
|
-
return t;
|
|
4679
|
-
}
|
|
4680
|
-
if (s.phase === "moon") {
|
|
4681
|
-
if (type !== "moon" || ![0, 1].includes(amount ?? -1))
|
|
4682
|
-
throw Error("Choose how to score the moon.");
|
|
4683
|
-
t.penalties = t.penalties.map(
|
|
4684
|
-
(_, i) => amount === 0 ? i === seat ? -26 : 0 : i === seat ? 0 : 26
|
|
4685
|
-
);
|
|
4686
|
-
finish(t);
|
|
4687
|
-
return t;
|
|
4688
|
-
}
|
|
4689
|
-
if (type !== "play" || at === void 0 || !trickLegalCards(s).includes(at))
|
|
4690
|
-
throw Error("Choose a legal card.");
|
|
4691
|
-
t.hands[seat] = t.hands[seat].filter((c) => c !== at);
|
|
4692
|
-
t.trick.push({ seat, card: at });
|
|
4693
|
-
if (s.variant === "spades" && cardSuit(at) === 3 && s.trick.length > 0 && cardSuit(s.trick[0].card) !== 3 || s.variant === "hearts" && (cardSuit(at) === 2 || at === 49))
|
|
4694
|
-
t.broken = true;
|
|
4695
|
-
t.turn = (seat + 1) % count;
|
|
4696
|
-
if (t.trick.length === count) {
|
|
4697
|
-
const lead = cardSuit(t.trick[0].card), strength = (c) => (cardSuit(c) === s.trump ? 100 : cardSuit(c) === lead ? 50 : 0) + cardRank(c);
|
|
4698
|
-
const winner2 = t.trick.reduce(
|
|
4699
|
-
(a, b) => strength(b.card) > strength(a.card) ? b : a
|
|
4700
|
-
).seat;
|
|
4701
|
-
t.tricks[winner2]++;
|
|
4702
|
-
t.penalties[winner2] += t.trick.reduce((n, c) => n + penalty(c.card), 0);
|
|
4703
|
-
t.lastTrick = t.trick;
|
|
4704
|
-
t.trick = [];
|
|
4705
|
-
t.turn = winner2;
|
|
4706
|
-
if (t.hands.every((h) => !h.length)) {
|
|
4707
|
-
const moon = s.variant === "hearts" ? t.penalties.findIndex((n) => n === 26) : -1;
|
|
4708
|
-
if (moon >= 0) {
|
|
4709
|
-
t.phase = "moon";
|
|
4710
|
-
t.turn = moon;
|
|
4711
|
-
} else finish(t);
|
|
4712
|
-
}
|
|
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.");
|
|
4713
5046
|
}
|
|
4714
|
-
return t;
|
|
4715
5047
|
}
|
|
4716
|
-
function
|
|
4717
|
-
const
|
|
4718
|
-
return
|
|
4719
|
-
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
|
|
4723
|
-
|
|
4724
|
-
)
|
|
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]
|
|
4725
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;
|
|
4726
5093
|
}
|
|
4727
5094
|
|
|
4728
|
-
// lib/
|
|
4729
|
-
var
|
|
5095
|
+
// lib/agents/action-help.ts
|
|
5096
|
+
var help = {
|
|
5097
|
+
amazons: "move: numeric from/to 0\u201399 and amount for the arrow square. Select an exact legal_wire_actions entry.",
|
|
5098
|
+
"lines-of-action": "move: numeric from/to 0\u201363. Move the count of all stones along the full line; enemy stones block jumping.",
|
|
5099
|
+
"last-greenhouse": "share: publish your current private journal observation. pass: hand controls to partner. choose: at 0\u20133 selects a room setting. All choices are listed, including incorrect answers. Cooperate using both journals.",
|
|
5100
|
+
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
|
+
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
|
+
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. Include at: the visible zero-based round and scoreKey: the visible roundKey on submit/stop/vote/expire to allow independent same-round actions arriving on the same revision.`,
|
|
5104
|
+
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
|
+
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
|
+
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": "place: at is a zero-based column (0\u20136). Convenience: --column 1\u20137.",
|
|
5108
|
+
"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
|
+
chess: "move: from/to are algebraic squares; promotion is q/r/b/n.",
|
|
5110
|
+
"atomic-chess": "move: from/to are algebraic squares; promotion is q/r/b/n. Explosions follow the game rules.",
|
|
5111
|
+
"three-check": "move: from/to are algebraic squares; promotion is q/r/b/n. Check counts are in visible_state.",
|
|
5112
|
+
checkers: "move: from/to are 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
|
+
"brazilian-checkers": "move: from/to are zero-based squares (0\u201363); maximum captures apply.",
|
|
5114
|
+
"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: "place: at is a zero-based row-major square (0\u201363). Unavailable turns pass automatically.",
|
|
5116
|
+
gomoku: "place: at is a zero-based row-major intersection (0\u2013224). Exactly five wins.",
|
|
5117
|
+
hex: "place: at is a zero-based row-major hexagon. swap: no payload, only the second seat's first turn. Convenience: --row/--column or --swap.",
|
|
5118
|
+
"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: at is a zero-based pit index, 0\u20135 for seat 0 and 6\u201311 for seat 1. Convenience: --pit is one-based.",
|
|
5120
|
+
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
|
+
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
|
+
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
|
+
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
|
+
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
|
+
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 zero-based destination point. The state records ball, used edges and continuing rebounds.",
|
|
5127
|
+
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
|
+
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
|
+
xiangqi: "move: numeric zero-based row-major from/to on the 9\xD710 board. promotion is unused.",
|
|
5130
|
+
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/pass: no payload. Follow attack/defence roles and visible trump/table.",
|
|
5132
|
+
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
|
+
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
|
+
"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
|
+
"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
|
+
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.",
|
|
5138
|
+
"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\u20133 chooses a suit, 4 no trumps, 5 misere. next advances the hand.",
|
|
5140
|
+
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
|
+
"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
|
+
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 (both sets 100), kings around 8 (80), queens around 6 (60), jacks around 4 (40), trump run 15 (150), royal marriage 4, ordinary marriage 2 per pair. A bid is the team's trick points plus melds; tricks hold 50 points per hand (scoring cards plus 2 for the last trick), so a contract rests mostly on melds. The declaring team needs 20 meld points.",
|
|
5144
|
+
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.",
|
|
5146
|
+
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
|
+
"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
|
+
"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.",
|
|
5149
|
+
"property-trading": "roll/end/buy/auction/pay/bankrupt/pass/reject have phase-specific effects. bid: amount is the bid. mortgage/redeem/build/sell: at selects a property. tax: amount is 0 or 1. release: amount 0 pays fifty, 1 uses a release card. offer: tradeOffer contains seat,give,take,giveCash,takeCash; accept confirms the current trade."
|
|
5150
|
+
};
|
|
5151
|
+
function actionHelp(game) {
|
|
5152
|
+
return { format: `game play --room CODE --revision R --action '{"type":"...",...}'`, indexing: "Board and hand-position indexes are zero-based. Card IDs are the actual values in visible_state, not offsets into a hand array. Convenience --row/--column/--edge/--pit flags are one-based. Inspect the exact player-visible state; do not guess hidden cards or board indexes.", game: help[game], validation: "action_types lists permitted command families, not currently legal moves. The server enforces phases, turns, ownership and legality. After a rejected or uncertain action read status again; never retry blindly." };
|
|
5153
|
+
}
|
|
4730
5154
|
|
|
4731
5155
|
// lib/multiplayer/euchre.ts
|
|
4732
5156
|
var euchreSuit = (card2) => Math.floor(card2 / 6);
|
|
@@ -4755,7 +5179,7 @@ function redFromHand(t, seat, replace) {
|
|
|
4755
5179
|
}
|
|
4756
5180
|
return true;
|
|
4757
5181
|
}
|
|
4758
|
-
function
|
|
5182
|
+
function deal3(s, entropy2) {
|
|
4759
5183
|
const t = structuredClone(s), deck = shuffled(
|
|
4760
5184
|
Array.from({ length: 108 }, (_, i) => i),
|
|
4761
5185
|
entropy2
|
|
@@ -4778,7 +5202,7 @@ function deal2(s, entropy2) {
|
|
|
4778
5202
|
}
|
|
4779
5203
|
function createCanasta(count, entropy2) {
|
|
4780
5204
|
if (count !== 2 && count !== 4) throw Error("Choose two or four players.");
|
|
4781
|
-
return
|
|
5205
|
+
return deal3(
|
|
4782
5206
|
{
|
|
4783
5207
|
hands: Array.from({ length: count }, () => []),
|
|
4784
5208
|
stock: [],
|
|
@@ -4857,7 +5281,7 @@ function playCanasta(s, a, entropy2) {
|
|
|
4857
5281
|
if (a.type === "next" && s.phase === "round") {
|
|
4858
5282
|
t.round++;
|
|
4859
5283
|
t.dealer = (t.dealer + 1) % s.hands.length;
|
|
4860
|
-
return
|
|
5284
|
+
return deal3(t, entropy2);
|
|
4861
5285
|
}
|
|
4862
5286
|
if (a.type === "stock" && s.phase === "draw") {
|
|
4863
5287
|
t.openingTurn = !s.opened[i];
|
|
@@ -5001,7 +5425,7 @@ function binokelMeld(hand2, trump) {
|
|
|
5001
5425
|
add([16, 5], [0, 4, 30, 60, 90], "binokel");
|
|
5002
5426
|
return { score, labels: labels2, shown: [...shown] };
|
|
5003
5427
|
}
|
|
5004
|
-
function
|
|
5428
|
+
function deal4(s, entropy2) {
|
|
5005
5429
|
const t = structuredClone(s), deck = shuffled(
|
|
5006
5430
|
Array.from({ length: 80 }, (_, i) => i),
|
|
5007
5431
|
entropy2
|
|
@@ -5024,7 +5448,7 @@ function deal3(s, entropy2) {
|
|
|
5024
5448
|
return t;
|
|
5025
5449
|
}
|
|
5026
5450
|
function createBinokel(entropy2) {
|
|
5027
|
-
return
|
|
5451
|
+
return deal4(
|
|
5028
5452
|
{
|
|
5029
5453
|
hands: [[], [], [], []],
|
|
5030
5454
|
turn: 0,
|
|
@@ -5072,7 +5496,7 @@ function playBinokel(s, a, entropy2) {
|
|
|
5072
5496
|
if (a.type === "next" && s.phase === "round") {
|
|
5073
5497
|
t.round++;
|
|
5074
5498
|
t.dealer = (t.dealer + 1) % 4;
|
|
5075
|
-
return
|
|
5499
|
+
return deal4(t, entropy2);
|
|
5076
5500
|
}
|
|
5077
5501
|
if (s.phase === "auction") {
|
|
5078
5502
|
if (a.type === "bid" && a.amount !== void 0 && Number.isInteger(a.amount) && a.amount > s.bid && (a.amount < 60 || a.amount % 5 === 0) && a.amount <= 1e3) {
|
|
@@ -5246,7 +5670,7 @@ function nextSeat(s, from, eligible) {
|
|
|
5246
5670
|
return -1;
|
|
5247
5671
|
}
|
|
5248
5672
|
var canAct = (p) => !p.folded && !p.out && p.stack > 0;
|
|
5249
|
-
function
|
|
5673
|
+
function deal5(s, n) {
|
|
5250
5674
|
s.deck.pop();
|
|
5251
5675
|
for (let i = 0; i < n; i++) s.community.push(s.deck.pop());
|
|
5252
5676
|
}
|
|
@@ -5311,13 +5735,13 @@ function advance(s) {
|
|
|
5311
5735
|
}
|
|
5312
5736
|
if (s.phase === "river") return award(s);
|
|
5313
5737
|
if (s.phase === "preflop") {
|
|
5314
|
-
|
|
5738
|
+
deal5(s, 3);
|
|
5315
5739
|
s.phase = "flop";
|
|
5316
5740
|
} else if (s.phase === "flop") {
|
|
5317
|
-
|
|
5741
|
+
deal5(s, 1);
|
|
5318
5742
|
s.phase = "turn";
|
|
5319
5743
|
} else {
|
|
5320
|
-
|
|
5744
|
+
deal5(s, 1);
|
|
5321
5745
|
s.phase = "river";
|
|
5322
5746
|
}
|
|
5323
5747
|
s.bet = 0;
|
|
@@ -5538,6 +5962,7 @@ var labels = {
|
|
|
5538
5962
|
var singleCard = /^(upcard|starter|top|trumpCard|card|attack|defen[cs]e|turned)$/;
|
|
5539
5963
|
var suits = ["\u2663 clubs", "\u2666 diamonds", "\u2665 hearts", "\u2660 spades"];
|
|
5540
5964
|
var cardField = /^(hands?|hole|trick|lastTrick|tricks?Won|pile|table|attack|defen[cs]e|discards?|kitty|kitties|shownKitty|skat|crib|starter|upcard|top|community|board|melds?|layout|red|revealed|played|dummy|selected|cards?|exchange|stock|widow|talon|turned|trumpCard|last)$/;
|
|
5965
|
+
var hasCardLabels = (game) => game in labels;
|
|
5541
5966
|
function cardsLabeled(game, state) {
|
|
5542
5967
|
const label = labels[game];
|
|
5543
5968
|
if (!label || !state || typeof state !== "object") return null;
|
|
@@ -7881,125 +8306,27 @@ function playBackgammon(s, type, from, to, die, entropy2) {
|
|
|
7881
8306
|
dice: [],
|
|
7882
8307
|
turn: turn === 0 ? 1 : 0,
|
|
7883
8308
|
result: "All moves are blocked. Turn passes."
|
|
7884
|
-
};
|
|
7885
|
-
}
|
|
7886
|
-
const move2 = backgammonMoves(s).find(
|
|
7887
|
-
(m) => m.from === from && m.to === to && m.die === die
|
|
7888
|
-
);
|
|
7889
|
-
if (type !== "move" || !move2)
|
|
7890
|
-
throw Error(
|
|
7891
|
-
"Use the maximum number of dice; if only one can be used, use the higher die."
|
|
7892
|
-
);
|
|
7893
|
-
const next3 = step(s, move2);
|
|
7894
|
-
if (next3.off[s.turn] === 15) {
|
|
7895
|
-
const opponent = 1 - s.turn, backgammon = next3.bar[opponent] > 0 || next3.points.some(
|
|
7896
|
-
(n, i) => (s.turn === 0 ? i >= 18 : i < 6) && n * (s.turn === 0 ? -1 : 1) > 0
|
|
7897
|
-
);
|
|
7898
|
-
next3.winner = s.turn;
|
|
7899
|
-
next3.result = next3.off[opponent] ? "Single game." : backgammon ? "Backgammon \xB7 3 points." : "Gammon \xB7 2 points.";
|
|
7900
|
-
} else if (!backgammonMoves(next3).length) {
|
|
7901
|
-
next3.dice = [];
|
|
7902
|
-
next3.turn = s.turn === 0 ? 1 : 0;
|
|
7903
|
-
}
|
|
7904
|
-
return next3;
|
|
7905
|
-
}
|
|
7906
|
-
|
|
7907
|
-
// lib/multiplayer/mills.ts
|
|
7908
|
-
var millLines = [
|
|
7909
|
-
[0, 1, 2],
|
|
7910
|
-
[2, 3, 4],
|
|
7911
|
-
[4, 5, 6],
|
|
7912
|
-
[6, 7, 0],
|
|
7913
|
-
[8, 9, 10],
|
|
7914
|
-
[10, 11, 12],
|
|
7915
|
-
[12, 13, 14],
|
|
7916
|
-
[14, 15, 8],
|
|
7917
|
-
[16, 17, 18],
|
|
7918
|
-
[18, 19, 20],
|
|
7919
|
-
[20, 21, 22],
|
|
7920
|
-
[22, 23, 16],
|
|
7921
|
-
[1, 9, 17],
|
|
7922
|
-
[3, 11, 19],
|
|
7923
|
-
[5, 13, 21],
|
|
7924
|
-
[7, 15, 23]
|
|
7925
|
-
];
|
|
7926
|
-
var millEdges = millLines.flatMap(([a, b, c]) => [
|
|
7927
|
-
[a, b],
|
|
7928
|
-
[b, c]
|
|
7929
|
-
]);
|
|
7930
|
-
function createMills() {
|
|
7931
|
-
return {
|
|
7932
|
-
board: Array(24).fill(0),
|
|
7933
|
-
reserves: [9, 9],
|
|
7934
|
-
turn: 0,
|
|
7935
|
-
captures: 0,
|
|
7936
|
-
winner: null,
|
|
7937
|
-
draw: false,
|
|
7938
|
-
history: []
|
|
7939
|
-
};
|
|
7940
|
-
}
|
|
7941
|
-
var linesAt = (board, at) => millLines.filter(
|
|
7942
|
-
(line) => line.includes(at) && line.every((i) => board[i] === board[at])
|
|
7943
|
-
);
|
|
7944
|
-
function millCaptures(s) {
|
|
7945
|
-
const opponents = s.board.flatMap((p, i) => p === 2 - s.turn ? [i] : []), loose = opponents.filter((at) => !linesAt(s.board, at).length);
|
|
7946
|
-
return loose.length ? loose : opponents;
|
|
7947
|
-
}
|
|
7948
|
-
function millMoves(s) {
|
|
7949
|
-
if (s.winner !== null || s.draw || s.captures) return [];
|
|
7950
|
-
const empty = s.board.flatMap((p, i) => !p ? [i] : []);
|
|
7951
|
-
if (s.reserves[s.turn]) return empty.map((to) => ({ from: null, to }));
|
|
7952
|
-
const own = s.board.flatMap((p, i) => p === s.turn + 1 ? [i] : []);
|
|
7953
|
-
return own.flatMap(
|
|
7954
|
-
(from) => empty.filter(
|
|
7955
|
-
(to) => own.length === 3 || millEdges.some(
|
|
7956
|
-
([a, b]) => a === from && b === to || b === from && a === to
|
|
7957
|
-
)
|
|
7958
|
-
).map((to) => ({ from, to }))
|
|
7959
|
-
);
|
|
7960
|
-
}
|
|
7961
|
-
function finishTurn(s) {
|
|
7962
|
-
const turn = s.turn === 0 ? 1 : 0, key2 = s.board.join(",") + ":" + turn + ":" + s.reserves.join(",");
|
|
7963
|
-
const next3 = {
|
|
7964
|
-
...s,
|
|
7965
|
-
turn,
|
|
7966
|
-
captures: 0,
|
|
7967
|
-
history: [...s.history, key2]
|
|
7968
|
-
};
|
|
7969
|
-
const count = next3.board.filter((p) => p === turn + 1).length;
|
|
7970
|
-
if (!next3.reserves[turn] && (count < 3 || !millMoves(next3).length))
|
|
7971
|
-
next3.winner = s.turn;
|
|
7972
|
-
else next3.draw = next3.history.filter((k) => k === key2).length >= 3;
|
|
7973
|
-
return next3;
|
|
7974
|
-
}
|
|
7975
|
-
function playMills(s, type, to, from) {
|
|
7976
|
-
if (!index(to, 24) || s.winner !== null || s.draw)
|
|
7977
|
-
throw Error("Choose a legal point.");
|
|
7978
|
-
const board = [...s.board], reserves = [...s.reserves];
|
|
7979
|
-
if (type === "capture") {
|
|
7980
|
-
if (!s.captures || !millCaptures(s).includes(to))
|
|
7981
|
-
throw Error(
|
|
7982
|
-
"Capture a piece outside a mill, unless all pieces are in mills."
|
|
7983
|
-
);
|
|
7984
|
-
board[to] = 0;
|
|
7985
|
-
const next4 = { ...s, board, captures: s.captures - 1 };
|
|
7986
|
-
return next4.captures && millCaptures(next4).length ? next4 : finishTurn(next4);
|
|
8309
|
+
};
|
|
7987
8310
|
}
|
|
7988
|
-
|
|
7989
|
-
(m) => m.
|
|
7990
|
-
)
|
|
8311
|
+
const move2 = backgammonMoves(s).find(
|
|
8312
|
+
(m) => m.from === from && m.to === to && m.die === die
|
|
8313
|
+
);
|
|
8314
|
+
if (type !== "move" || !move2)
|
|
7991
8315
|
throw Error(
|
|
7992
|
-
"
|
|
8316
|
+
"Use the maximum number of dice; if only one can be used, use the higher die."
|
|
7993
8317
|
);
|
|
7994
|
-
|
|
7995
|
-
|
|
7996
|
-
|
|
7997
|
-
|
|
7998
|
-
|
|
7999
|
-
|
|
8000
|
-
|
|
8001
|
-
|
|
8002
|
-
|
|
8318
|
+
const next3 = step(s, move2);
|
|
8319
|
+
if (next3.off[s.turn] === 15) {
|
|
8320
|
+
const opponent = 1 - s.turn, backgammon = next3.bar[opponent] > 0 || next3.points.some(
|
|
8321
|
+
(n, i) => (s.turn === 0 ? i >= 18 : i < 6) && n * (s.turn === 0 ? -1 : 1) > 0
|
|
8322
|
+
);
|
|
8323
|
+
next3.winner = s.turn;
|
|
8324
|
+
next3.result = next3.off[opponent] ? "Single game." : backgammon ? "Backgammon \xB7 3 points." : "Gammon \xB7 2 points.";
|
|
8325
|
+
} else if (!backgammonMoves(next3).length) {
|
|
8326
|
+
next3.dice = [];
|
|
8327
|
+
next3.turn = s.turn === 0 ? 1 : 0;
|
|
8328
|
+
}
|
|
8329
|
+
return next3;
|
|
8003
8330
|
}
|
|
8004
8331
|
|
|
8005
8332
|
// lib/multiplayer/football.ts
|
|
@@ -8270,13 +8597,13 @@ function goAction(s, seat, action, at) {
|
|
|
8270
8597
|
}
|
|
8271
8598
|
const accepted = [.../* @__PURE__ */ new Set([...s.accepted, seat])];
|
|
8272
8599
|
if (accepted.length < 2) return { ...s, accepted };
|
|
8273
|
-
const
|
|
8600
|
+
const scores2 = scoreGo(s);
|
|
8274
8601
|
return {
|
|
8275
8602
|
...s,
|
|
8276
8603
|
accepted,
|
|
8277
|
-
scores,
|
|
8604
|
+
scores: scores2,
|
|
8278
8605
|
phase: "finished",
|
|
8279
|
-
winner:
|
|
8606
|
+
winner: scores2[0] > scores2[1] ? 0 : 1
|
|
8280
8607
|
};
|
|
8281
8608
|
}
|
|
8282
8609
|
function goProposalKey(s) {
|
|
@@ -8333,7 +8660,7 @@ var adapter3 = defineMatchAdapter({
|
|
|
8333
8660
|
return {
|
|
8334
8661
|
finished: state.winner !== null || state.draw,
|
|
8335
8662
|
winner: state.winner,
|
|
8336
|
-
label: state.draw ? "
|
|
8663
|
+
label: state.draw ? "Draw \xB7 repetition, insufficient material or the move limit" : state.winner !== null ? `${state.check ? "Checkmate" : "No legal move"} \xB7 Player ${state.winner + 1} wins.` : state.check ? "Check" : ""
|
|
8337
8664
|
};
|
|
8338
8665
|
},
|
|
8339
8666
|
apply(state, _seat, action, _entropy, _now) {
|
|
@@ -8357,7 +8684,7 @@ var adapter4 = defineMatchAdapter({
|
|
|
8357
8684
|
return {
|
|
8358
8685
|
finished: state.winner !== null || state.draw,
|
|
8359
8686
|
winner: state.winner,
|
|
8360
|
-
label: state.draw ? "
|
|
8687
|
+
label: state.draw ? "Draw \xB7 repetition, insufficient material or the move limit" : state.winner !== null ? `${state.check ? "Checkmate" : "No legal move"} \xB7 Player ${state.winner + 1} wins.` : state.check ? "Check" : ""
|
|
8361
8688
|
};
|
|
8362
8689
|
},
|
|
8363
8690
|
apply(state, _seat, action, _entropy, _now) {
|
|
@@ -8444,7 +8771,7 @@ var adapter8 = defineMatchAdapter({
|
|
|
8444
8771
|
return {
|
|
8445
8772
|
finished: state.winner !== null || state.draw,
|
|
8446
8773
|
winner: state.winner,
|
|
8447
|
-
label: state.draw ? "Draw" : state.winner !== null ? `Player ${state.winner + 1} wins \xB7 the opponent is down to two pieces
|
|
8774
|
+
label: state.draw ? "Draw" : state.winner !== null ? `Player ${state.winner + 1} wins \xB7 ${state.board.filter((p) => p === 2 - state.winner).length < 3 ? "the opponent is down to two pieces" : "the opponent has no legal move"}.` : ""
|
|
8448
8775
|
};
|
|
8449
8776
|
},
|
|
8450
8777
|
apply(state, _seat, action, _entropy, _now) {
|
|
@@ -8492,7 +8819,7 @@ var adapter10 = defineMatchAdapter({
|
|
|
8492
8819
|
return {
|
|
8493
8820
|
finished: state.winner !== null,
|
|
8494
8821
|
winner: state.winner,
|
|
8495
|
-
label: state.last
|
|
8822
|
+
label: state.winner !== null ? `Player ${state.winner + 1} brings all four tokens home and wins.` : state.last
|
|
8496
8823
|
};
|
|
8497
8824
|
},
|
|
8498
8825
|
apply(state, _seat, action, entropy2, _now) {
|
|
@@ -8631,9 +8958,9 @@ function loserResult(count, loser) {
|
|
|
8631
8958
|
validSeat(count, loser);
|
|
8632
8959
|
return { sides: Array.from({ length: count }, (_, seat) => ({ seats: [seat], place: seat === loser ? 1 : 0 })) };
|
|
8633
8960
|
}
|
|
8634
|
-
function scoreResult(
|
|
8635
|
-
validSeat(
|
|
8636
|
-
const values =
|
|
8961
|
+
function scoreResult(scores2, winner2, lowerWins = false) {
|
|
8962
|
+
validSeat(scores2.length, winner2);
|
|
8963
|
+
const values = scores2.map((n) => lowerWins ? -n : n);
|
|
8637
8964
|
return { sides: values.map((value, seat) => ({ seats: [seat], place: seat === winner2 ? 0 : Number(winner2 !== null) + values.filter((n, i) => i !== winner2 && n > value).length })) };
|
|
8638
8965
|
}
|
|
8639
8966
|
function partnershipResult(count, winner2) {
|
|
@@ -8650,7 +8977,7 @@ function next(s, seat) {
|
|
|
8650
8977
|
const i = s.players.indexOf(seat);
|
|
8651
8978
|
return s.players[(i + 1) % s.players.length];
|
|
8652
8979
|
}
|
|
8653
|
-
function
|
|
8980
|
+
function deal6(s, entropy2) {
|
|
8654
8981
|
const t = structuredClone(s), count = s.hands.length, deck = shuffled(
|
|
8655
8982
|
Array.from({ length: 52 }, (_, i) => i).filter((c) => cardRank(c) >= 9),
|
|
8656
8983
|
entropy2
|
|
@@ -8682,7 +9009,7 @@ function deal5(s, entropy2) {
|
|
|
8682
9009
|
}
|
|
8683
9010
|
function createThousand(count, entropy2) {
|
|
8684
9011
|
if (!index(count - 2, 3)) throw Error("Choose two to four players.");
|
|
8685
|
-
return
|
|
9012
|
+
return deal6(
|
|
8686
9013
|
{
|
|
8687
9014
|
hands: Array.from({ length: count }, () => []),
|
|
8688
9015
|
kitties: [],
|
|
@@ -8734,10 +9061,10 @@ function playThousand(s, a, entropy2) {
|
|
|
8734
9061
|
if (a.type === "next" && s.phase === "round") {
|
|
8735
9062
|
t.round++;
|
|
8736
9063
|
t.dealer = (s.dealer + 1) % s.hands.length;
|
|
8737
|
-
return
|
|
9064
|
+
return deal6(t, entropy2);
|
|
8738
9065
|
}
|
|
8739
9066
|
if (a.type === "redeal" && s.phase === "auction" && s.hands[seat].filter((c) => cardRank(c) === 9).length === 4)
|
|
8740
|
-
return
|
|
9067
|
+
return deal6(t, entropy2);
|
|
8741
9068
|
if (s.phase === "auction") {
|
|
8742
9069
|
if (a.type === "bid" && a.amount !== void 0 && Number.isInteger(a.amount) && a.amount % 10 === 0 && a.amount > s.bid && a.amount <= thousandMaxBid(s.hands[seat])) {
|
|
8743
9070
|
t.bid = a.amount;
|
|
@@ -8907,7 +9234,7 @@ var skatBids = [
|
|
|
8907
9234
|
)
|
|
8908
9235
|
])
|
|
8909
9236
|
].filter((n) => n <= 264).sort((a, b) => a - b);
|
|
8910
|
-
function
|
|
9237
|
+
function deal7(s, entropy2) {
|
|
8911
9238
|
const t = structuredClone(s), deck = shuffled(
|
|
8912
9239
|
Array.from({ length: 52 }, (_, i) => i).filter((c) => cardRank(c) >= 7),
|
|
8913
9240
|
entropy2
|
|
@@ -8941,7 +9268,7 @@ function deal6(s, entropy2) {
|
|
|
8941
9268
|
return t;
|
|
8942
9269
|
}
|
|
8943
9270
|
function createSkat(entropy2) {
|
|
8944
|
-
return
|
|
9271
|
+
return deal7(
|
|
8945
9272
|
{
|
|
8946
9273
|
hands: [[], [], []],
|
|
8947
9274
|
skat: [],
|
|
@@ -9042,7 +9369,7 @@ function playSkat(s, a, entropy2) {
|
|
|
9042
9369
|
if (a.type === "next" && s.phase === "round") {
|
|
9043
9370
|
t.round++;
|
|
9044
9371
|
t.dealer = (s.dealer + 1) % 3;
|
|
9045
|
-
return
|
|
9372
|
+
return deal7(t, entropy2);
|
|
9046
9373
|
}
|
|
9047
9374
|
if (s.phase === "auction") {
|
|
9048
9375
|
if (s.offer === null) {
|
|
@@ -9203,7 +9530,7 @@ function sides(t) {
|
|
|
9203
9530
|
t.old = t.hands.map((_, i) => i === t.declarer);
|
|
9204
9531
|
else t.old = t.hands.map((h) => queens.some((c) => h.includes(c)));
|
|
9205
9532
|
}
|
|
9206
|
-
function
|
|
9533
|
+
function deal8(s, entropy2) {
|
|
9207
9534
|
const t = structuredClone(s), deck = shuffled(
|
|
9208
9535
|
Array.from({ length: 52 }, (_, i) => i).filter(
|
|
9209
9536
|
(c) => [10, 11, 12, 14].includes(cardRank(c))
|
|
@@ -9244,7 +9571,7 @@ function deal7(s, entropy2) {
|
|
|
9244
9571
|
return t;
|
|
9245
9572
|
}
|
|
9246
9573
|
function createBaska(entropy2) {
|
|
9247
|
-
return
|
|
9574
|
+
return deal8(
|
|
9248
9575
|
{
|
|
9249
9576
|
hands: [[], [], [], []],
|
|
9250
9577
|
turn: 0,
|
|
@@ -9313,7 +9640,7 @@ function playBaska(s, a, entropy2) {
|
|
|
9313
9640
|
if (a.type === "next" && s.phase === "round") {
|
|
9314
9641
|
t.round++;
|
|
9315
9642
|
t.dealer = (t.dealer + 1) % 4;
|
|
9316
|
-
return
|
|
9643
|
+
return deal8(t, entropy2);
|
|
9317
9644
|
}
|
|
9318
9645
|
if (a.type === "announce" && s.phase === "honor") {
|
|
9319
9646
|
finish5(t, true);
|
|
@@ -9433,7 +9760,7 @@ var adapter17 = defineMatchAdapter({
|
|
|
9433
9760
|
|
|
9434
9761
|
// lib/multiplayer/bridge.ts
|
|
9435
9762
|
var vulnerability = [0, 1, 2, 3, 1, 2, 3, 0, 2, 3, 0, 1, 3, 0, 1, 2];
|
|
9436
|
-
function
|
|
9763
|
+
function deal9(s, entropy2) {
|
|
9437
9764
|
const t = structuredClone(s), deck = shuffled(
|
|
9438
9765
|
Array.from({ length: 52 }, (_, i) => i),
|
|
9439
9766
|
entropy2
|
|
@@ -9461,7 +9788,7 @@ function deal8(s, entropy2) {
|
|
|
9461
9788
|
return t;
|
|
9462
9789
|
}
|
|
9463
9790
|
function createBridge(entropy2) {
|
|
9464
|
-
return
|
|
9791
|
+
return deal9(
|
|
9465
9792
|
{
|
|
9466
9793
|
hands: [[], [], [], []],
|
|
9467
9794
|
turn: 0,
|
|
@@ -9514,7 +9841,7 @@ function playBridge(s, a, entropy2) {
|
|
|
9514
9841
|
if (a.type === "next" && s.phase === "round") {
|
|
9515
9842
|
t.board++;
|
|
9516
9843
|
t.dealer = (t.dealer + 1) % 4;
|
|
9517
|
-
return
|
|
9844
|
+
return deal9(t, entropy2);
|
|
9518
9845
|
}
|
|
9519
9846
|
if (s.phase === "auction") {
|
|
9520
9847
|
if (a.type === "bid" && index((a.amount ?? 0) - 1, 35)) {
|
|
@@ -9620,7 +9947,7 @@ var adapter18 = defineMatchAdapter({
|
|
|
9620
9947
|
});
|
|
9621
9948
|
|
|
9622
9949
|
// lib/multiplayer/polish-hearts.ts
|
|
9623
|
-
function
|
|
9950
|
+
function deal10(s, entropy2) {
|
|
9624
9951
|
const t = structuredClone(s), count = s.hands.length, deck = shuffled(
|
|
9625
9952
|
Array.from({ length: 52 }, (_, i) => i).filter(
|
|
9626
9953
|
(c) => count !== 3 || c !== 0
|
|
@@ -9647,7 +9974,7 @@ function deal9(s, entropy2) {
|
|
|
9647
9974
|
}
|
|
9648
9975
|
function createPolish(count, entropy2) {
|
|
9649
9976
|
if (count !== 3 && count !== 4) throw Error("Choose three or four players.");
|
|
9650
|
-
return
|
|
9977
|
+
return deal10(
|
|
9651
9978
|
{
|
|
9652
9979
|
hands: Array.from({ length: count }, () => []),
|
|
9653
9980
|
rest: [],
|
|
@@ -9711,7 +10038,7 @@ function playPolish(s, a, entropy2) {
|
|
|
9711
10038
|
if (a.type === "next" && s.phase === "round") {
|
|
9712
10039
|
t.round++;
|
|
9713
10040
|
t.dealer = (t.dealer + 1) % count;
|
|
9714
|
-
return
|
|
10041
|
+
return deal10(t, entropy2);
|
|
9715
10042
|
}
|
|
9716
10043
|
if (a.type === "trump" && s.phase === "trump" && index(a.amount ?? -1, 4)) {
|
|
9717
10044
|
t.trump = a.amount;
|
|
@@ -10184,7 +10511,7 @@ var adapter21 = defineMatchAdapter({
|
|
|
10184
10511
|
});
|
|
10185
10512
|
|
|
10186
10513
|
// lib/multiplayer/three-five-eight.ts
|
|
10187
|
-
function
|
|
10514
|
+
function deal11(s, entropy2) {
|
|
10188
10515
|
const t = structuredClone(s), deck = shuffled(
|
|
10189
10516
|
Array.from({ length: 52 }, (_, i) => i),
|
|
10190
10517
|
entropy2
|
|
@@ -10221,7 +10548,7 @@ function deal10(s, entropy2) {
|
|
|
10221
10548
|
return t;
|
|
10222
10549
|
}
|
|
10223
10550
|
function createThree(entropy2) {
|
|
10224
|
-
return
|
|
10551
|
+
return deal11(
|
|
10225
10552
|
{
|
|
10226
10553
|
hands: [[], [], []],
|
|
10227
10554
|
kitty: [],
|
|
@@ -10260,7 +10587,7 @@ function playThree(s, a, entropy2) {
|
|
|
10260
10587
|
if (a.type === "next" && s.phase === "round") {
|
|
10261
10588
|
t.round++;
|
|
10262
10589
|
t.dealer = (s.dealer + 1) % 3;
|
|
10263
|
-
return
|
|
10590
|
+
return deal11(t, entropy2);
|
|
10264
10591
|
}
|
|
10265
10592
|
if (s.phase === "exchange" && a.type === "exchange" && a.at !== void 0 && s.hands[s.turn].includes(a.at)) {
|
|
10266
10593
|
const q = t.exchange[0], suit = cardSuit(a.at), other = t.hands[q.to].filter((c) => cardSuit(c) === suit).sort((a2, b) => cardRank(b) - cardRank(a2)), returned = other[0];
|
|
@@ -10652,6 +10979,7 @@ function settle(s, entropy2) {
|
|
|
10652
10979
|
s.bills.shift();
|
|
10653
10980
|
}
|
|
10654
10981
|
if (["buy", "tax", "trade", "auction"].includes(s.phase)) return;
|
|
10982
|
+
if (s.players[s.active].bankrupt) s.pendingMove = null;
|
|
10655
10983
|
if (s.pendingMove !== null) {
|
|
10656
10984
|
const distance = s.pendingMove;
|
|
10657
10985
|
s.pendingMove = null;
|
|
@@ -11040,297 +11368,114 @@ var adapter26 = defineMatchAdapter({
|
|
|
11040
11368
|
|
|
11041
11369
|
// lib/multiplayer/pan.ts
|
|
11042
11370
|
var nineHearts = 33;
|
|
11043
|
-
function
|
|
11371
|
+
function deal12(count, entropy2, letters, round) {
|
|
11044
11372
|
const cards = shuffled(
|
|
11045
11373
|
Array.from({ length: 52 }, (_, i) => i).filter((c) => cardRank(c) >= 9),
|
|
11046
11374
|
entropy2
|
|
11047
|
-
), hands = Array.from(
|
|
11048
|
-
{ length: count },
|
|
11049
|
-
(_, i) => cards.slice(i * 24 / count, (i + 1) * 24 / count).sort((a, b) => a - b)
|
|
11050
|
-
);
|
|
11051
|
-
return {
|
|
11052
|
-
hands,
|
|
11053
|
-
pile: [],
|
|
11054
|
-
turn: hands.findIndex((h) => h.includes(nineHearts)),
|
|
11055
|
-
letters,
|
|
11056
|
-
phase: "play",
|
|
11057
|
-
winner: null,
|
|
11058
|
-
loser: null,
|
|
11059
|
-
round,
|
|
11060
|
-
result: ""
|
|
11061
|
-
};
|
|
11062
|
-
}
|
|
11063
|
-
function createPan(count, entropy2) {
|
|
11064
|
-
if (!index(count - 2, 3)) throw Error("Choose two to four players.");
|
|
11065
|
-
return deal11(count, entropy2, Array(count).fill(0), 0);
|
|
11066
|
-
}
|
|
11067
|
-
function panLegal(s, cards) {
|
|
11068
|
-
const hand2 = s.hands[s.turn];
|
|
11069
|
-
if (s.phase !== "play" || !cards.length || new Set(cards).size !== cards.length || cards.some((c) => !hand2.includes(c)))
|
|
11070
|
-
return false;
|
|
11071
|
-
const rank2 = cardRank(cards[0]);
|
|
11072
|
-
if (cards.some((c) => cardRank(c) !== rank2)) return false;
|
|
11073
|
-
if (!s.pile.length)
|
|
11074
|
-
return cards.includes(nineHearts) && (cards.length === 1 || cards.length === 4);
|
|
11075
|
-
if (![1, 4].includes(cards.length) && !(cards.length === 3 && rank2 === 9 && s.pile.length === 1))
|
|
11076
|
-
return false;
|
|
11077
|
-
return rank2 >= cardRank(s.pile.at(-1));
|
|
11078
|
-
}
|
|
11079
|
-
function playPan(s, type, cards, entropy2) {
|
|
11080
|
-
if (type === "next" && s.phase === "round")
|
|
11081
|
-
return deal11(s.hands.length, entropy2, s.letters, s.round + 1);
|
|
11082
|
-
if (s.phase !== "play") throw Error("This round is finished.");
|
|
11083
|
-
const t = structuredClone(s), seat = s.turn;
|
|
11084
|
-
if (type === "play" && panLegal(s, cards)) {
|
|
11085
|
-
const ordered = [...cards].sort(
|
|
11086
|
-
(a, b) => a === nineHearts ? -1 : b === nineHearts ? 1 : a - b
|
|
11087
|
-
);
|
|
11088
|
-
t.pile.push(...ordered);
|
|
11089
|
-
t.hands[seat] = t.hands[seat].filter((c) => !cards.includes(c));
|
|
11090
|
-
} else if (type === "take" && s.pile.length > 1) {
|
|
11091
|
-
t.hands[seat].push(...t.pile.splice(Math.max(1, t.pile.length - 3)));
|
|
11092
|
-
t.hands[seat].sort((a, b) => a - b);
|
|
11093
|
-
} else
|
|
11094
|
-
throw Error(
|
|
11095
|
-
"Play one card, four of a rank, or take up to three from the pile."
|
|
11096
|
-
);
|
|
11097
|
-
const active = t.hands.flatMap((h, i) => h.length ? [i] : []);
|
|
11098
|
-
if (active.length <= 1) {
|
|
11099
|
-
const last = active[0];
|
|
11100
|
-
if (last !== void 0 && panLegal({ ...t, turn: last }, t.hands[last]) && t.hands[last].every((c) => cardRank(c) === 14)) {
|
|
11101
|
-
t.turn = last;
|
|
11102
|
-
return t;
|
|
11103
|
-
}
|
|
11104
|
-
if (last !== void 0) {
|
|
11105
|
-
t.letters[last]++;
|
|
11106
|
-
t.loser = last;
|
|
11107
|
-
t.result = `Player ${last + 1} receives ${"PAN".slice(0, t.letters[last])}.`;
|
|
11108
|
-
} else t.result = "Round drawn: everyone shed their cards.";
|
|
11109
|
-
t.phase = last !== void 0 && t.letters[last] >= 3 ? "finished" : "round";
|
|
11110
|
-
t.winner = t.phase === "finished" && t.hands.length === 2 ? 1 - last : null;
|
|
11111
|
-
t.turn = last ?? seat;
|
|
11112
|
-
return t;
|
|
11113
|
-
}
|
|
11114
|
-
do {
|
|
11115
|
-
t.turn = (t.turn + 1) % t.hands.length;
|
|
11116
|
-
} while (!t.hands[t.turn].length);
|
|
11117
|
-
return t;
|
|
11118
|
-
}
|
|
11119
|
-
function panView(s, seat) {
|
|
11120
|
-
return {
|
|
11121
|
-
...s,
|
|
11122
|
-
hands: s.hands.map(
|
|
11123
|
-
(h, i) => i === seat || s.phase !== "play" ? h : h.map(() => null)
|
|
11124
|
-
)
|
|
11125
|
-
};
|
|
11126
|
-
}
|
|
11127
|
-
|
|
11128
|
-
// lib/multiplayer/adapters/pan.ts
|
|
11129
|
-
var adapter27 = defineMatchAdapter({
|
|
11130
|
-
ranking: (state, count) => loserResult(count, state.loser),
|
|
11131
|
-
actions: ["next", "play", "take"],
|
|
11132
|
-
create(count, entropy2, _goSize) {
|
|
11133
|
-
return createPan(count, entropy2);
|
|
11134
|
-
},
|
|
11135
|
-
turn: (state) => state.turn,
|
|
11136
|
-
outcome(state) {
|
|
11137
|
-
return {
|
|
11138
|
-
finished: state.phase === "finished",
|
|
11139
|
-
winner: state.winner,
|
|
11140
|
-
label: state.result
|
|
11141
|
-
};
|
|
11142
|
-
},
|
|
11143
|
-
apply(state, _seat, action, entropy2, _now) {
|
|
11144
|
-
return playPan(state, action.type, action.cards ?? [], entropy2);
|
|
11145
|
-
},
|
|
11146
|
-
view(state, seat) {
|
|
11147
|
-
return panView(state, seat);
|
|
11148
|
-
}
|
|
11149
|
-
});
|
|
11150
|
-
|
|
11151
|
-
// lib/multiplayer/gin-rummy.ts
|
|
11152
|
-
var ginRank = (c) => cardRank(c) === 14 ? 1 : cardRank(c);
|
|
11153
|
-
var ginValue = (c) => Math.min(10, ginRank(c));
|
|
11154
|
-
function ginMelds(hand2) {
|
|
11155
|
-
const melds = [];
|
|
11156
|
-
for (let rank2 = 1; rank2 <= 13; rank2++) {
|
|
11157
|
-
const cards = hand2.filter((c) => ginRank(c) === rank2);
|
|
11158
|
-
if (cards.length >= 3) {
|
|
11159
|
-
if (cards.length === 4) {
|
|
11160
|
-
melds.push(cards);
|
|
11161
|
-
for (let i = 0; i < 4; i++) melds.push(cards.filter((_, j) => j !== i));
|
|
11162
|
-
} else melds.push(cards);
|
|
11163
|
-
}
|
|
11164
|
-
}
|
|
11165
|
-
for (let suit = 0; suit < 4; suit++) {
|
|
11166
|
-
const cards = hand2.filter((c) => cardSuit(c) === suit).sort((a, b) => ginRank(a) - ginRank(b));
|
|
11167
|
-
for (let start = 0; start < cards.length; start++)
|
|
11168
|
-
for (let end = start + 2; end < cards.length; end++) {
|
|
11169
|
-
const slice = cards.slice(start, end + 1);
|
|
11170
|
-
if (slice.every(
|
|
11171
|
-
(c, i) => i === 0 || ginRank(c) === ginRank(slice[i - 1]) + 1
|
|
11172
|
-
))
|
|
11173
|
-
melds.push(slice);
|
|
11174
|
-
else break;
|
|
11175
|
-
}
|
|
11176
|
-
}
|
|
11177
|
-
return melds;
|
|
11178
|
-
}
|
|
11179
|
-
function partitions(hand2) {
|
|
11180
|
-
const melds = ginMelds(hand2), out2 = [];
|
|
11181
|
-
function visit(start, used, chosen) {
|
|
11182
|
-
const deadwood2 = hand2.filter((c) => !used.has(c));
|
|
11183
|
-
out2.push({
|
|
11184
|
-
melds: chosen,
|
|
11185
|
-
deadwood: deadwood2,
|
|
11186
|
-
value: deadwood2.reduce((n, c) => n + ginValue(c), 0)
|
|
11187
|
-
});
|
|
11188
|
-
for (let i = start; i < melds.length; i++)
|
|
11189
|
-
if (melds[i].every((c) => !used.has(c)))
|
|
11190
|
-
visit(i + 1, /* @__PURE__ */ new Set([...used, ...melds[i]]), [...chosen, melds[i]]);
|
|
11191
|
-
}
|
|
11192
|
-
visit(0, /* @__PURE__ */ new Set(), []);
|
|
11193
|
-
return out2;
|
|
11194
|
-
}
|
|
11195
|
-
function ginLayout(hand2) {
|
|
11196
|
-
return partitions(hand2).reduce((a, b) => b.value < a.value ? b : a);
|
|
11197
|
-
}
|
|
11198
|
-
function ginLayoffValue(hand2, onto) {
|
|
11199
|
-
const cache = /* @__PURE__ */ new Map();
|
|
11200
|
-
function attach(cards, melds) {
|
|
11201
|
-
const key2 = cards.join(",") + "|" + melds.map((m) => m.join(",")).join(";"), cached = cache.get(key2);
|
|
11202
|
-
if (cached !== void 0) return cached;
|
|
11203
|
-
let best2 = cards.reduce((n, c) => n + ginValue(c), 0);
|
|
11204
|
-
cards.forEach(
|
|
11205
|
-
(c, i) => melds.forEach((m, j) => {
|
|
11206
|
-
const set = m.every((x) => ginRank(x) === ginRank(m[0])), ranks = m.map(ginRank), fits = set ? m.length === 3 && ginRank(c) === ranks[0] : cardSuit(c) === cardSuit(m[0]) && (ginRank(c) === Math.min(...ranks) - 1 || ginRank(c) === Math.max(...ranks) + 1);
|
|
11207
|
-
if (fits) {
|
|
11208
|
-
const next3 = melds.map(
|
|
11209
|
-
(m2, k) => k === j ? [...m2, c].sort((a, b) => ginRank(a) - ginRank(b)) : m2
|
|
11210
|
-
);
|
|
11211
|
-
best2 = Math.min(
|
|
11212
|
-
best2,
|
|
11213
|
-
attach(
|
|
11214
|
-
cards.filter((_, k) => k !== i),
|
|
11215
|
-
next3
|
|
11216
|
-
)
|
|
11217
|
-
);
|
|
11218
|
-
}
|
|
11219
|
-
})
|
|
11220
|
-
);
|
|
11221
|
-
cache.set(key2, best2);
|
|
11222
|
-
return best2;
|
|
11223
|
-
}
|
|
11224
|
-
return Math.min(...partitions(hand2).map((p) => attach(p.deadwood, onto)));
|
|
11225
|
-
}
|
|
11226
|
-
function deal12(dealer, scores, wins, round, entropy2) {
|
|
11227
|
-
const deck = shuffled(
|
|
11228
|
-
Array.from({ length: 52 }, (_, i) => i),
|
|
11229
|
-
entropy2
|
|
11230
|
-
);
|
|
11231
|
-
return {
|
|
11232
|
-
hands: [
|
|
11233
|
-
deck.slice(0, 10).sort((a, b) => a - b),
|
|
11234
|
-
deck.slice(10, 20).sort((a, b) => a - b)
|
|
11235
|
-
],
|
|
11236
|
-
stock: deck.slice(21),
|
|
11237
|
-
discard: [deck[20]],
|
|
11238
|
-
turn: 1 - dealer,
|
|
11239
|
-
dealer,
|
|
11240
|
-
phase: "offer",
|
|
11241
|
-
offers: 0,
|
|
11242
|
-
drawnDiscard: null,
|
|
11243
|
-
scores,
|
|
11244
|
-
wins,
|
|
11375
|
+
), hands = Array.from(
|
|
11376
|
+
{ length: count },
|
|
11377
|
+
(_, i) => cards.slice(i * 24 / count, (i + 1) * 24 / count).sort((a, b) => a - b)
|
|
11378
|
+
);
|
|
11379
|
+
return {
|
|
11380
|
+
hands,
|
|
11381
|
+
pile: [],
|
|
11382
|
+
turn: hands.findIndex((h) => h.includes(nineHearts)),
|
|
11383
|
+
letters,
|
|
11384
|
+
phase: "play",
|
|
11245
11385
|
winner: null,
|
|
11386
|
+
loser: null,
|
|
11246
11387
|
round,
|
|
11247
|
-
result: ""
|
|
11248
|
-
revealed: [],
|
|
11249
|
-
margin: null
|
|
11388
|
+
result: ""
|
|
11250
11389
|
};
|
|
11251
11390
|
}
|
|
11252
|
-
function
|
|
11253
|
-
|
|
11391
|
+
function createPan(count, entropy2) {
|
|
11392
|
+
if (!index(count - 2, 3)) throw Error("Choose two to four players.");
|
|
11393
|
+
return deal12(count, entropy2, Array(count).fill(0), 0);
|
|
11254
11394
|
}
|
|
11255
|
-
function
|
|
11395
|
+
function panLegal(s, cards) {
|
|
11396
|
+
const hand2 = s.hands[s.turn];
|
|
11397
|
+
if (s.phase !== "play" || !cards.length || new Set(cards).size !== cards.length || cards.some((c) => !hand2.includes(c)))
|
|
11398
|
+
return false;
|
|
11399
|
+
const rank2 = cardRank(cards[0]);
|
|
11400
|
+
if (cards.some((c) => cardRank(c) !== rank2)) return false;
|
|
11401
|
+
if (!s.pile.length)
|
|
11402
|
+
return cards.includes(nineHearts) && (cards.length === 1 || cards.length === 4);
|
|
11403
|
+
if (![1, 4].includes(cards.length) && !(cards.length === 3 && rank2 === 9 && s.pile.length === 1))
|
|
11404
|
+
return false;
|
|
11405
|
+
return rank2 >= cardRank(s.pile.at(-1));
|
|
11406
|
+
}
|
|
11407
|
+
function playPan(s, type, cards, entropy2) {
|
|
11256
11408
|
if (type === "next" && s.phase === "round")
|
|
11257
|
-
return deal12(
|
|
11258
|
-
|
|
11259
|
-
s.scores,
|
|
11260
|
-
s.wins,
|
|
11261
|
-
s.round + 1,
|
|
11262
|
-
entropy2
|
|
11263
|
-
);
|
|
11264
|
-
if (["finished", "round"].includes(s.phase))
|
|
11265
|
-
throw Error("This round is finished.");
|
|
11409
|
+
return deal12(s.hands.length, entropy2, s.letters, s.round + 1);
|
|
11410
|
+
if (s.phase !== "play") throw Error("This round is finished.");
|
|
11266
11411
|
const t = structuredClone(s), seat = s.turn;
|
|
11267
|
-
if (type === "
|
|
11268
|
-
|
|
11269
|
-
|
|
11270
|
-
|
|
11271
|
-
|
|
11272
|
-
|
|
11273
|
-
|
|
11274
|
-
|
|
11275
|
-
if (["draw", "offer"].includes(s.phase) && ["stock", "upcard"].includes(type)) {
|
|
11276
|
-
if (s.phase === "offer" && type === "stock")
|
|
11277
|
-
throw Error("Take the upcard or pass the opening offer.");
|
|
11278
|
-
if (s.phase === "draw" && s.offers === 2 && s.round === t.round && type === "upcard")
|
|
11279
|
-
throw Error("Both players passed: draw from the stock.");
|
|
11280
|
-
if (s.stock.length <= 2) {
|
|
11281
|
-
t.phase = "round";
|
|
11282
|
-
t.result = "Stock exhausted: round cancelled, same dealer.";
|
|
11283
|
-
t.turn = s.dealer;
|
|
11284
|
-
return t;
|
|
11285
|
-
}
|
|
11286
|
-
const card2 = type === "upcard" ? t.discard.pop() : t.stock.shift();
|
|
11287
|
-
t.drawnDiscard = type === "upcard" ? card2 : null;
|
|
11288
|
-
t.hands[seat].push(card2);
|
|
11412
|
+
if (type === "play" && panLegal(s, cards)) {
|
|
11413
|
+
const ordered = [...cards].sort(
|
|
11414
|
+
(a, b) => a === nineHearts ? -1 : b === nineHearts ? 1 : a - b
|
|
11415
|
+
);
|
|
11416
|
+
t.pile.push(...ordered);
|
|
11417
|
+
t.hands[seat] = t.hands[seat].filter((c) => !cards.includes(c));
|
|
11418
|
+
} else if (type === "take" && s.pile.length > 1) {
|
|
11419
|
+
t.hands[seat].push(...t.pile.splice(Math.max(1, t.pile.length - 3)));
|
|
11289
11420
|
t.hands[seat].sort((a, b) => a - b);
|
|
11290
|
-
|
|
11291
|
-
|
|
11292
|
-
|
|
11293
|
-
|
|
11294
|
-
|
|
11295
|
-
|
|
11296
|
-
|
|
11297
|
-
t.
|
|
11298
|
-
|
|
11299
|
-
|
|
11300
|
-
if (layout.value > 10)
|
|
11301
|
-
throw Error("Knock only with ten or fewer deadwood points.");
|
|
11302
|
-
const other = 1 - seat, opp = layout.value === 0 ? ginLayout(t.hands[other]).value : ginLayoffValue(t.hands[other], layout.melds), winner2 = layout.value === 0 || layout.value < opp ? seat : other;
|
|
11303
|
-
const points = layout.value === 0 ? opp + 25 : winner2 === seat ? opp - layout.value : layout.value - opp + 25;
|
|
11304
|
-
t.scores[winner2] += points;
|
|
11305
|
-
t.wins[winner2]++;
|
|
11306
|
-
t.result = `${layout.value === 0 ? "Gin" : winner2 === seat ? "Knock" : "Undercut"}: Player ${winner2 + 1} scores ${points}. Deadwood ${layout.value} vs ${opp}.`;
|
|
11307
|
-
t.revealed = t.hands.map((h) => ginLayout(h).melds);
|
|
11308
|
-
t.phase = t.scores[winner2] >= 100 ? "finished" : "round";
|
|
11309
|
-
t.turn = winner2;
|
|
11310
|
-
if (t.phase === "finished") {
|
|
11311
|
-
t.winner = winner2;
|
|
11312
|
-
t.margin = t.scores[winner2] + 100 + (t.scores[other] === 0 ? 100 : 0) + 25 * t.wins[winner2] - t.scores[other] - 25 * t.wins[other];
|
|
11313
|
-
t.result += ` Final margin including game and hand bonuses: ${t.margin}.`;
|
|
11314
|
-
}
|
|
11315
|
-
} else {
|
|
11316
|
-
t.phase = "draw";
|
|
11317
|
-
t.turn = 1 - seat;
|
|
11421
|
+
} else
|
|
11422
|
+
throw Error(
|
|
11423
|
+
"Play one card, four of a rank, or take up to three from the pile."
|
|
11424
|
+
);
|
|
11425
|
+
const active = t.hands.flatMap((h, i) => h.length ? [i] : []);
|
|
11426
|
+
if (active.length <= 1) {
|
|
11427
|
+
const last = active[0];
|
|
11428
|
+
if (last !== void 0 && panLegal({ ...t, turn: last }, t.hands[last]) && t.hands[last].every((c) => cardRank(c) === 14)) {
|
|
11429
|
+
t.turn = last;
|
|
11430
|
+
return t;
|
|
11318
11431
|
}
|
|
11432
|
+
if (last !== void 0) {
|
|
11433
|
+
t.letters[last]++;
|
|
11434
|
+
t.loser = last;
|
|
11435
|
+
t.result = `Player ${last + 1} receives ${"PAN".slice(0, t.letters[last])}.`;
|
|
11436
|
+
} else t.result = "Round drawn: everyone shed their cards.";
|
|
11437
|
+
t.phase = last !== void 0 && t.letters[last] >= 3 ? "finished" : "round";
|
|
11438
|
+
t.winner = t.phase === "finished" && t.hands.length === 2 ? 1 - last : null;
|
|
11439
|
+
t.turn = last ?? seat;
|
|
11319
11440
|
return t;
|
|
11320
11441
|
}
|
|
11321
|
-
|
|
11442
|
+
do {
|
|
11443
|
+
t.turn = (t.turn + 1) % t.hands.length;
|
|
11444
|
+
} while (!t.hands[t.turn].length);
|
|
11445
|
+
return t;
|
|
11322
11446
|
}
|
|
11323
|
-
function
|
|
11324
|
-
const { stock, ...rest } = s;
|
|
11447
|
+
function panView(s, seat) {
|
|
11325
11448
|
return {
|
|
11326
|
-
...
|
|
11327
|
-
stockCount: stock.length,
|
|
11449
|
+
...s,
|
|
11328
11450
|
hands: s.hands.map(
|
|
11329
|
-
(h, i) => i === seat ||
|
|
11451
|
+
(h, i) => i === seat || s.phase !== "play" ? h : h.map(() => null)
|
|
11330
11452
|
)
|
|
11331
11453
|
};
|
|
11332
11454
|
}
|
|
11333
11455
|
|
|
11456
|
+
// lib/multiplayer/adapters/pan.ts
|
|
11457
|
+
var adapter27 = defineMatchAdapter({
|
|
11458
|
+
ranking: (state, count) => loserResult(count, state.loser),
|
|
11459
|
+
actions: ["next", "play", "take"],
|
|
11460
|
+
create(count, entropy2, _goSize) {
|
|
11461
|
+
return createPan(count, entropy2);
|
|
11462
|
+
},
|
|
11463
|
+
turn: (state) => state.turn,
|
|
11464
|
+
outcome(state) {
|
|
11465
|
+
return {
|
|
11466
|
+
finished: state.phase === "finished",
|
|
11467
|
+
winner: state.winner,
|
|
11468
|
+
label: state.result
|
|
11469
|
+
};
|
|
11470
|
+
},
|
|
11471
|
+
apply(state, _seat, action, entropy2, _now) {
|
|
11472
|
+
return playPan(state, action.type, action.cards ?? [], entropy2);
|
|
11473
|
+
},
|
|
11474
|
+
view(state, seat) {
|
|
11475
|
+
return panView(state, seat);
|
|
11476
|
+
}
|
|
11477
|
+
});
|
|
11478
|
+
|
|
11334
11479
|
// lib/multiplayer/adapters/gin-rummy.ts
|
|
11335
11480
|
var adapter28 = defineMatchAdapter({
|
|
11336
11481
|
actions: ["next", "pass", "stock", "upcard", "discard", "knock"],
|
|
@@ -11356,7 +11501,7 @@ var adapter28 = defineMatchAdapter({
|
|
|
11356
11501
|
// lib/multiplayer/sixty-six.ts
|
|
11357
11502
|
var sixtyStrength = (c) => [9, 11, 12, 13, 10, 14].indexOf(cardRank(c));
|
|
11358
11503
|
var sixtyPoints = (c) => [0, 2, 3, 4, 10, 11][sixtyStrength(c)];
|
|
11359
|
-
function deal13(dealer,
|
|
11504
|
+
function deal13(dealer, scores2, round, entropy2) {
|
|
11360
11505
|
const deck = shuffled(
|
|
11361
11506
|
Array.from({ length: 52 }, (_, i) => i).filter((c) => cardRank(c) >= 9),
|
|
11362
11507
|
entropy2
|
|
@@ -11378,7 +11523,7 @@ function deal13(dealer, scores, round, entropy2) {
|
|
|
11378
11523
|
points: [0, 0],
|
|
11379
11524
|
pending: [0, 0],
|
|
11380
11525
|
tricks: [0, 0],
|
|
11381
|
-
scores,
|
|
11526
|
+
scores: scores2,
|
|
11382
11527
|
phase: "play",
|
|
11383
11528
|
winner: null,
|
|
11384
11529
|
result: "",
|
|
@@ -11548,92 +11693,6 @@ var adapter30 = defineMatchAdapter({
|
|
|
11548
11693
|
leave: leavePoker
|
|
11549
11694
|
});
|
|
11550
11695
|
|
|
11551
|
-
// lib/multiplayer/dice.ts
|
|
11552
|
-
function createDice(count) {
|
|
11553
|
-
if (!index(count - 2, 3)) throw Error("Choose two to four players.");
|
|
11554
|
-
return {
|
|
11555
|
-
dice: [0, 0, 0, 0, 0],
|
|
11556
|
-
held: Array(5).fill(false),
|
|
11557
|
-
rolls: 0,
|
|
11558
|
-
sheets: Array.from({ length: count }, () => Array(13).fill(null)),
|
|
11559
|
-
bonuses: Array(count).fill(0),
|
|
11560
|
-
turn: 0,
|
|
11561
|
-
finished: false,
|
|
11562
|
-
winner: null
|
|
11563
|
-
};
|
|
11564
|
-
}
|
|
11565
|
-
function diceScore(dice, category, joker = false) {
|
|
11566
|
-
const counts = Array.from(
|
|
11567
|
-
{ length: 6 },
|
|
11568
|
-
(_, i) => dice.filter((v) => v === i + 1).length
|
|
11569
|
-
), sum = dice.reduce((a, b) => a + b, 0);
|
|
11570
|
-
if (category < 6) return counts[category] * (category + 1);
|
|
11571
|
-
switch (category) {
|
|
11572
|
-
case 6:
|
|
11573
|
-
return Math.max(...counts) >= 3 ? sum : 0;
|
|
11574
|
-
case 7:
|
|
11575
|
-
return Math.max(...counts) >= 4 ? sum : 0;
|
|
11576
|
-
case 8:
|
|
11577
|
-
return joker || counts.includes(3) && counts.includes(2) ? 25 : 0;
|
|
11578
|
-
case 9:
|
|
11579
|
-
return joker || [0, 1, 2].some((start) => counts.slice(start, start + 4).every(Boolean)) ? 30 : 0;
|
|
11580
|
-
case 10:
|
|
11581
|
-
return joker || [0, 1].some((start) => counts.slice(start, start + 5).every(Boolean)) ? 40 : 0;
|
|
11582
|
-
case 11:
|
|
11583
|
-
return counts.includes(5) ? 50 : 0;
|
|
11584
|
-
case 12:
|
|
11585
|
-
return sum;
|
|
11586
|
-
default:
|
|
11587
|
-
throw Error("Choose a score category.");
|
|
11588
|
-
}
|
|
11589
|
-
}
|
|
11590
|
-
function diceTotal(s, seat) {
|
|
11591
|
-
const sheet = s.sheets[seat], upper = sheet.slice(0, 6).reduce((sum, n) => sum + (n ?? 0), 0);
|
|
11592
|
-
return sheet.reduce((sum, n) => sum + (n ?? 0), 0) + (upper >= 63 ? 35 : 0) + s.bonuses[seat];
|
|
11593
|
-
}
|
|
11594
|
-
function playDice(s, type, at, entropy2) {
|
|
11595
|
-
if (s.finished) throw Error("This match is finished.");
|
|
11596
|
-
if (type === "roll") {
|
|
11597
|
-
if (s.rolls >= 3) throw Error("Record a score after three rolls.");
|
|
11598
|
-
const random = draws(entropy2, 5);
|
|
11599
|
-
return {
|
|
11600
|
-
...s,
|
|
11601
|
-
rolls: s.rolls + 1,
|
|
11602
|
-
dice: s.dice.map(
|
|
11603
|
-
(v, i) => s.rolls && s.held[i] ? v : 1 + Math.floor(random[i] * 6)
|
|
11604
|
-
)
|
|
11605
|
-
};
|
|
11606
|
-
}
|
|
11607
|
-
if (at === void 0 || !index(at, type === "hold" ? 5 : 13) || !s.rolls)
|
|
11608
|
-
throw Error("Roll first, then choose a die or score category.");
|
|
11609
|
-
if (type === "hold")
|
|
11610
|
-
return { ...s, held: s.held.map((v, i) => i === at ? !v : v) };
|
|
11611
|
-
if (type !== "score" || s.sheets[s.turn][at] !== null)
|
|
11612
|
-
throw Error("Choose an unused score category.");
|
|
11613
|
-
const sheets = s.sheets.map((row) => [...row]), bonuses = [...s.bonuses];
|
|
11614
|
-
const five = s.dice.every((v) => v === s.dice[0]), upper = s.dice[0] - 1;
|
|
11615
|
-
const joker = five && sheets[s.turn][11] !== null && sheets[s.turn][upper] !== null;
|
|
11616
|
-
if (joker && sheets[s.turn].slice(6).some((n) => n === null) && at < 6)
|
|
11617
|
-
throw Error(
|
|
11618
|
-
"Use the five-of-a-kind joker in an unused lower category first."
|
|
11619
|
-
);
|
|
11620
|
-
sheets[s.turn][at] = diceScore(s.dice, at, joker);
|
|
11621
|
-
if (joker && sheets[s.turn][11] === 50) bonuses[s.turn] += 100;
|
|
11622
|
-
const next3 = {
|
|
11623
|
-
...s,
|
|
11624
|
-
sheets,
|
|
11625
|
-
bonuses,
|
|
11626
|
-
turn: (s.turn + 1) % s.sheets.length,
|
|
11627
|
-
rolls: 0,
|
|
11628
|
-
held: Array(5).fill(false),
|
|
11629
|
-
dice: [0, 0, 0, 0, 0]
|
|
11630
|
-
};
|
|
11631
|
-
next3.finished = sheets.every((row) => row.every((n) => n !== null));
|
|
11632
|
-
if (next3.finished)
|
|
11633
|
-
next3.winner = best(sheets.map((_, i) => diceTotal(next3, i)));
|
|
11634
|
-
return next3;
|
|
11635
|
-
}
|
|
11636
|
-
|
|
11637
11696
|
// lib/multiplayer/adapters/dice.ts
|
|
11638
11697
|
var adapter31 = defineMatchAdapter({
|
|
11639
11698
|
ranking: (state) => scoreResult(state.sheets.map((_, seat) => diceTotal(state, seat)), state.winner),
|
|
@@ -11688,13 +11747,13 @@ function dominoMoves(s) {
|
|
|
11688
11747
|
});
|
|
11689
11748
|
}
|
|
11690
11749
|
function endRound(s, winner2, bonus) {
|
|
11691
|
-
const
|
|
11692
|
-
if (winner2 !== null)
|
|
11693
|
-
const finished =
|
|
11750
|
+
const scores2 = [...s.scores];
|
|
11751
|
+
if (winner2 !== null) scores2[winner2] += Math.round(bonus / 5) * 5;
|
|
11752
|
+
const finished = scores2.some((n) => n >= 100);
|
|
11694
11753
|
return {
|
|
11695
11754
|
...s,
|
|
11696
|
-
scores,
|
|
11697
|
-
winner: finished ? best(
|
|
11755
|
+
scores: scores2,
|
|
11756
|
+
winner: finished ? best(scores2) : null,
|
|
11698
11757
|
phase: finished ? "finished" : "round",
|
|
11699
11758
|
turn: winner2 ?? s.turn,
|
|
11700
11759
|
result: winner2 === null ? "Blocked round: equal pip totals." : `Player ${winner2 + 1} wins the round.`
|
|
@@ -11710,7 +11769,7 @@ function playDomino(s, type, at, end, entropy2) {
|
|
|
11710
11769
|
turn: s.turn
|
|
11711
11770
|
};
|
|
11712
11771
|
if (s.phase !== "play") throw Error("This round is finished.");
|
|
11713
|
-
const hands = s.hands.map((h) => [...h]), stock = [...s.stock], chain = [...s.chain],
|
|
11772
|
+
const hands = s.hands.map((h) => [...h]), stock = [...s.stock], chain = [...s.chain], scores2 = [...s.scores];
|
|
11714
11773
|
if (type === "draw") {
|
|
11715
11774
|
if (dominoMoves(s).length)
|
|
11716
11775
|
throw Error("Play a matching tile before drawing.");
|
|
@@ -11735,20 +11794,20 @@ function playDomino(s, type, at, end, entropy2) {
|
|
|
11735
11794
|
}
|
|
11736
11795
|
const left = chain[0], right = chain.at(-1);
|
|
11737
11796
|
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]);
|
|
11738
|
-
if (total % 5 === 0)
|
|
11797
|
+
if (total % 5 === 0) scores2[s.turn] += total;
|
|
11739
11798
|
const next3 = {
|
|
11740
11799
|
...s,
|
|
11741
11800
|
hands,
|
|
11742
11801
|
stock,
|
|
11743
11802
|
chain,
|
|
11744
|
-
scores,
|
|
11803
|
+
scores: scores2,
|
|
11745
11804
|
passes: 0,
|
|
11746
11805
|
turn: 1 - s.turn
|
|
11747
11806
|
};
|
|
11748
11807
|
if (!hands[s.turn].length)
|
|
11749
11808
|
return endRound(next3, s.turn, pipSum(hands[1 - s.turn]));
|
|
11750
|
-
if (
|
|
11751
|
-
return { ...next3, phase: "finished", winner: best(
|
|
11809
|
+
if (scores2.some((n) => n >= 100))
|
|
11810
|
+
return { ...next3, phase: "finished", winner: best(scores2) };
|
|
11752
11811
|
return next3;
|
|
11753
11812
|
}
|
|
11754
11813
|
function dominoView(s, seat) {
|
|
@@ -11966,11 +12025,12 @@ function playRummy(s, seat, a, entropy2) {
|
|
|
11966
12025
|
return t;
|
|
11967
12026
|
}
|
|
11968
12027
|
if (s.phase !== "meld") throw Error("Draw from the stock or take the top discard first.");
|
|
11969
|
-
|
|
12028
|
+
const keepsDiscard = (cards) => s.hands[seat].some((c) => !cards.includes(c) && c !== s.taken);
|
|
12029
|
+
if (a.type === "meld" && ownedCards(s.hands[seat], a.cards) && keepsDiscard(a.cards) && rummyMeld(a.cards)) {
|
|
11970
12030
|
t.melds.push({ seat, cards: [...a.cards].sort((a2, b) => rummyRank(a2) - rummyRank(b)) });
|
|
11971
12031
|
t.hands[seat] = t.hands[seat].filter((c) => !a.cards.includes(c));
|
|
11972
12032
|
t.result = `Player ${seat + 1} melds ${a.cards.length} cards.`;
|
|
11973
|
-
} else if (a.type === "layoff" && index(a.at, s.melds.length) && ownedCards(s.hands[seat], a.cards) && a.cards
|
|
12033
|
+
} else if (a.type === "layoff" && index(a.at, s.melds.length) && ownedCards(s.hands[seat], a.cards) && keepsDiscard(a.cards) && rummyMeld([...s.melds[a.at].cards, ...a.cards])) {
|
|
11974
12034
|
t.melds[a.at].cards.push(...a.cards);
|
|
11975
12035
|
t.melds[a.at].cards.sort((a2, b) => rummyRank(a2) - rummyRank(b));
|
|
11976
12036
|
t.hands[seat] = t.hands[seat].filter((c) => !a.cards.includes(c));
|
|
@@ -12062,7 +12122,8 @@ var promotions = ["marriage", "marriage-claim", "makao", "normal", "schneider",
|
|
|
12062
12122
|
function hand(state, seat) {
|
|
12063
12123
|
for (const key2 of ["hands", "hand"]) {
|
|
12064
12124
|
const hands = state[key2];
|
|
12065
|
-
|
|
12125
|
+
const held = typeof state.playSeat === "number" && state.turn === seat ? state.playSeat : seat;
|
|
12126
|
+
if (Array.isArray(hands) && Array.isArray(hands[held])) return hands[held].filter((c) => typeof c === "number" && c >= 0);
|
|
12066
12127
|
}
|
|
12067
12128
|
return [];
|
|
12068
12129
|
}
|
|
@@ -12133,8 +12194,13 @@ function specific(game, state, seat) {
|
|
|
12133
12194
|
const s = state;
|
|
12134
12195
|
return s.die ? ludoMoves(s).map((at) => ({ type: "move", at })) : [{ type: "roll" }];
|
|
12135
12196
|
}
|
|
12197
|
+
if (game === "makao") return [...generic(adapters.makao, state, seat), ...cardGroups(hand(state, seat)).flatMap((cards) => [
|
|
12198
|
+
{ type: "play", cards, promotion: "makao" },
|
|
12199
|
+
...[0, 1, 2, 3, 5, 6, 7, 8, 9, 10, 13].flatMap((amount) => [{ type: "play", cards, amount }, { type: "play", cards, amount, promotion: "makao" }])
|
|
12200
|
+
])];
|
|
12136
12201
|
if (game === "go") {
|
|
12137
12202
|
const s = state;
|
|
12203
|
+
if (s.phase === "scoring" && s.accepted.includes(seat)) return [];
|
|
12138
12204
|
if (s.phase === "scoring") return [...s.board.flatMap((p, at) => p ? [{ type: "dead", at }] : []), { type: "accept", scoreKey: goProposalKey(s) }, { type: "resume" }];
|
|
12139
12205
|
return s.phase === "play" && s.turn === seat ? [...s.board.flatMap((p, at) => p ? [] : [{ type: "place", at }]), { type: "pass" }] : [];
|
|
12140
12206
|
}
|
|
@@ -12188,7 +12254,7 @@ function reconstruct(game, view, seat) {
|
|
|
12188
12254
|
const own = view.ownFleet, fired = view.fired, incoming = view.incoming;
|
|
12189
12255
|
return { ...view, fleets: seat === 0 ? [own, []] : [[], own], shots: seat === 0 ? [fired, incoming] : [incoming, fired] };
|
|
12190
12256
|
}
|
|
12191
|
-
var entropy = Array.from({ length:
|
|
12257
|
+
var entropy = Array.from({ length: 512 }, (_, i) => i * 7919 + 1);
|
|
12192
12258
|
function hydrate(adapter36, view, seats2) {
|
|
12193
12259
|
let template;
|
|
12194
12260
|
try {
|
|
@@ -12231,6 +12297,35 @@ function millsBoard(board) {
|
|
|
12231
12297
|
legend: "Each point shows its index then X (seat 0), O (seat 1) or . (empty). Lines connect neighbours along each ring and the midpoints 1-9-17, 3-11-19, 5-13-21, 7-15-23; mills are three in a line."
|
|
12232
12298
|
};
|
|
12233
12299
|
}
|
|
12300
|
+
function battleshipsBoard(v) {
|
|
12301
|
+
const ships = new Set(v.ownFleet.flat()), sunk = new Set(v.sunk.flat());
|
|
12302
|
+
const target = (at) => sunk.has(at) ? "#" : v.hits.includes(at) ? "x" : v.fired.includes(at) ? "o" : ".";
|
|
12303
|
+
const fleet = (at) => ships.has(at) ? v.incoming.includes(at) ? "X" : "S" : v.incoming.includes(at) ? "o" : ".";
|
|
12304
|
+
const row = (r, cell) => Array.from({ length: 10 }, (_, c) => cell(r * 10 + c)).join(" ");
|
|
12305
|
+
return {
|
|
12306
|
+
rows: 10,
|
|
12307
|
+
columns: 10,
|
|
12308
|
+
labeled: [
|
|
12309
|
+
` target grid your fleet`,
|
|
12310
|
+
` +0 1 2 3 4 5 6 7 8 9 +0 1 2 3 4 5 6 7 8 9`,
|
|
12311
|
+
...Array.from({ length: 10 }, (_, r) => `${String(r * 10).padStart(2)} ${row(r, target)} ${row(r, fleet)}`)
|
|
12312
|
+
],
|
|
12313
|
+
legend: "Target grid: x hit, # sunk ship, o miss, . not fired. Your fleet: S ship, X hit ship, o enemy miss. Index = row label + column offset."
|
|
12314
|
+
};
|
|
12315
|
+
}
|
|
12316
|
+
function backgammonBoard(v) {
|
|
12317
|
+
const cell = (at) => `${String(at).padStart(2)}:${v.points[at] ? `${Math.abs(v.points[at])}${v.points[at] > 0 ? "X" : "O"}` : "."}`.padEnd(6);
|
|
12318
|
+
return {
|
|
12319
|
+
rows: 2,
|
|
12320
|
+
columns: 12,
|
|
12321
|
+
labeled: [
|
|
12322
|
+
Array.from({ length: 12 }, (_, i) => cell(12 + i)).join(" ").trimEnd(),
|
|
12323
|
+
Array.from({ length: 12 }, (_, i) => cell(11 - i)).join(" ").trimEnd(),
|
|
12324
|
+
`bar: X ${v.bar[0]}, O ${v.bar[1]} \xB7 off: X ${v.off[0]}/15, O ${v.off[1]}/15`
|
|
12325
|
+
],
|
|
12326
|
+
legend: "index:count then X (seat 0, moves 0\u219223, bears off past 23 to 24) or O (seat 1, moves 23\u21920, bears off to 24). from -1 enters from the bar. Index + 1 is the point number the browser shows."
|
|
12327
|
+
};
|
|
12328
|
+
}
|
|
12234
12329
|
function fenBoard(fen) {
|
|
12235
12330
|
const ranks = fen.split(" ")[0].split("/").map((rank2) => rank2.replace(/\d/g, (n) => ".".repeat(Number(n))));
|
|
12236
12331
|
return {
|
|
@@ -12243,6 +12338,8 @@ function fenBoard(fen) {
|
|
|
12243
12338
|
function indexedBoard(game, state) {
|
|
12244
12339
|
const grid = grids[game], board = state?.board;
|
|
12245
12340
|
if (game === "mills" && Array.isArray(board) && board.length === 24) return millsBoard(board);
|
|
12341
|
+
if (game === "backgammon" && state && typeof state === "object" && "points" in state) return backgammonBoard(state);
|
|
12342
|
+
if (game === "battleships" && state && typeof state === "object" && "ownFleet" in state) return battleshipsBoard(state);
|
|
12246
12343
|
const fen = state?.fen;
|
|
12247
12344
|
if ((game === "atomic-chess" || game === "three-check") && typeof fen === "string") return fenBoard(fen);
|
|
12248
12345
|
if (!grid || !Array.isArray(board)) return null;
|
|
@@ -12304,11 +12401,35 @@ function agentPosition(room) {
|
|
|
12304
12401
|
if (match.kind === "hex" && match.state.moves === 1 && match.state.turn === 1) legal.push({ type: "swap" });
|
|
12305
12402
|
}
|
|
12306
12403
|
}
|
|
12307
|
-
const last = match?.kind === "connect-four" || match?.kind === "tic-tac-toe" || match?.kind === "gomoku" ? match.state.last : null;
|
|
12308
|
-
const
|
|
12404
|
+
const last = match?.kind === "connect-four" || match?.kind === "tic-tac-toe" || match?.kind === "gomoku" ? match.state.last : match?.kind === "reversi" || match?.kind === "hex" ? match.state.last ?? null : null;
|
|
12405
|
+
const agreed = match?.kind === "go" && match.state.phase === "scoring" && match.state.accepted.includes(room.seat);
|
|
12406
|
+
const yours = room.phase === "playing" && !!match && turn === room.seat && !agreed && !room.players[room.seat].left && !ended && !room.players.some((p) => p.paused);
|
|
12309
12407
|
const active = room.phase === "playing" && !!match && !room.players[room.seat].left && !ended && !room.players.some((p) => p.paused);
|
|
12310
12408
|
const grid = !complete && match ? indexedBoard(room.game, match.state) : null;
|
|
12311
12409
|
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" || match?.kind === "brazilian-checkers" ? checkerLastMove(match.state.history) : null;
|
|
12411
|
+
const dotsLabeled = match?.kind === "dots-boxes" ? match.state.boxes.map((owner4, box) => {
|
|
12412
|
+
const { size, edges } = match.state, sides2 = boxEdges(size, box), drawn = sides2.filter((e) => edges[e] >= 0).length;
|
|
12413
|
+
const named = ["top", "bottom", "left", "right"].map((side2, i) => `${side2} ${sides2[i] + 1}${edges[sides2[i]] >= 0 ? "*" : ""}`).join(", ");
|
|
12414
|
+
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
|
+
}) : null;
|
|
12416
|
+
const ginHand = match?.kind === "gin-rummy" && match.state.phase !== "finished" ? match.state.hands[room.seat]?.filter((c) => c !== null) ?? [] : [];
|
|
12417
|
+
const ginHint = ginHand.length ? (() => {
|
|
12418
|
+
const layout = ginLayout(ginHand), names = (cards) => cards.map((c) => `${cardLabel(c)} (id ${c})`);
|
|
12419
|
+
const discards = ginHand.length > 10 ? ginHand.map((c) => ({ discard: c, deadwood: ginLayout(ginHand.filter((x) => x !== c)).value })).sort((a, b) => a.deadwood - b.deadwood) : [];
|
|
12420
|
+
return {
|
|
12421
|
+
deadwood: layout.value,
|
|
12422
|
+
melds: layout.melds.map(names),
|
|
12423
|
+
deadwood_cards: names(layout.deadwood),
|
|
12424
|
+
...discards.length ? { best_discard: `${cardLabel(discards[0].discard)} (id ${discards[0].discard})`, deadwood_after_best_discard: discards[0].deadwood } : {},
|
|
12425
|
+
knock_limit: 10
|
|
12426
|
+
};
|
|
12427
|
+
})() : null;
|
|
12428
|
+
const asianMove = match?.kind === "shogi" || match?.kind === "xiangqi" ? asianLastMove(match.state.history) : null;
|
|
12429
|
+
const asianLast = asianMove && (match?.kind === "shogi" || match?.kind === "xiangqi") ? { ...asianMove, by: match.state.history.at(-1).mover } : null;
|
|
12430
|
+
const millLast = match?.kind === "mills" ? millLastTurn(match.state) : null;
|
|
12431
|
+
const cribbageCard = match?.kind === "cribbage" && match.state.phase === "peg" ? match.state.sequence.at(-1) : void 0;
|
|
12432
|
+
const cribbageLast = match?.kind === "cribbage" && cribbageCard !== void 0 ? { by_seat: match.state.played.findIndex((cards) => cards.includes(cribbageCard)), played: `${cribbageLabel(cribbageCard)} (id ${cribbageCard})`, count: match.state.total } : null;
|
|
12312
12433
|
return {
|
|
12313
12434
|
protocol_version: 1,
|
|
12314
12435
|
room: room.code,
|
|
@@ -12323,15 +12444,16 @@ function agentPosition(room) {
|
|
|
12323
12444
|
players: room.players.map((p, seat) => ({ seat, name: p.name, left: p.left, ...p.agent ? { agent: true } : {}, ...p.paused ? { paused: true } : {} })),
|
|
12324
12445
|
board_size: complete ? { rows, columns } : grid ? { rows: grid.rows, columns: grid.columns } : { rows: 0, columns: 0 },
|
|
12325
12446
|
board_top_to_bottom: board,
|
|
12326
|
-
board_labeled: labeledBoard(room.game, board) ?? grid?.labeled ?? null,
|
|
12327
|
-
...grid ? { board_legend: grid.legend } : {},
|
|
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,
|
|
12448
|
+
...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." } : {},
|
|
12328
12449
|
// Chess and its variants record the last move in SAN.
|
|
12329
|
-
last_move: (match?.kind === "chess" || match?.kind === "atomic-chess" || match?.kind === "three-check") && match.state.last ? { san: match.state.last } : 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 },
|
|
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 },
|
|
12330
12451
|
...room.game === "chess" ? { fen } : {},
|
|
12331
12452
|
// The side to move is in check (chess family, xiangqi, shogi).
|
|
12332
12453
|
...match && room.phase === "playing" && (match.kind === "chess" ? chessPosition(match.state).inCheck() : "check" in match.state && match.state.check === true) ? { in_check: true } : {},
|
|
12333
12454
|
visible_state: match?.state ?? null,
|
|
12334
12455
|
cards_labeled: match ? cardsLabeled(room.game, match.state) : null,
|
|
12456
|
+
...ginHint ? { gin_deadwood: ginHint } : {},
|
|
12335
12457
|
// Timed rounds (drawing, categories): agents cannot measure time, so give the remaining seconds.
|
|
12336
12458
|
...typeof match?.state?.deadline === "number" && room.serverNow ? { deadline_seconds_left: Math.max(0, Math.ceil((match.state.deadline - room.serverNow) / 1e3)) } : {},
|
|
12337
12459
|
action_types: action_catalog_default[room.game],
|
|
@@ -12339,7 +12461,8 @@ function agentPosition(room) {
|
|
|
12339
12461
|
legal_actions_complete: complete,
|
|
12340
12462
|
legal_actions: yours ? legal : [],
|
|
12341
12463
|
...match?.kind === "amazons" || match?.kind === "lines-of-action" || match?.kind === "last-greenhouse" || match?.kind === "euchre" || match?.kind === "cribbage" || match?.kind === "fanorona" ? { legal_wire_actions_complete: true, legal_wire_actions: yours ? match.state.legalActions : [] } : wire ? { legal_wire_actions_complete: agentListComplete(room.game, match?.state, wire), legal_wire_actions: wire } : {},
|
|
12342
|
-
|
|
12464
|
+
// Engine labels count players from 1; name the zero-based seat as everywhere else in the CLI.
|
|
12465
|
+
outcome: room.outcome ? { winner: room.outcome.winner, label: room.outcome.label.replace(/Player (\d+)/g, (whole, n) => room.players[Number(n) - 1] ? `seat ${Number(n) - 1} (${room.players[Number(n) - 1].name})` : whole) } : null,
|
|
12343
12466
|
instructions_for_resume: "Resume your agent chat with this room code and read status. A timeout is not resignation. To rematch, use game rematch; never start one without the human asking.",
|
|
12344
12467
|
terminal_reason: room.phase === "finished" ? room.players.some((p) => p.left) ? "resignation_or_leave" : "game_result" : null,
|
|
12345
12468
|
rematch_ready: room.rematch.includes(room.seat),
|
|
@@ -12347,17 +12470,23 @@ function agentPosition(room) {
|
|
|
12347
12470
|
};
|
|
12348
12471
|
}
|
|
12349
12472
|
function labeledBoard(game, board) {
|
|
12350
|
-
if (!board?.length || game === "
|
|
12473
|
+
if (!board?.length || game === "dots-boxes") return null;
|
|
12474
|
+
if (game === "oware") return [`seat 0 pits 1-6: ${board[0]}`, `seat 1 pits 7-12: ${board[1]}`, "Sowing runs 1\u219212 and wraps to 1, skipping the origin pit after a full lap."];
|
|
12351
12475
|
const algebraic = game === "chess" || game === "checkers";
|
|
12352
12476
|
const rowWidth = String(board.length).length, cell = algebraic ? 1 : String(board[0].length).length;
|
|
12353
12477
|
const files2 = algebraic ? [..."abcdefgh"] : [...board[0]].map((_, c) => String(c + 1));
|
|
12354
12478
|
return [
|
|
12355
12479
|
`${" ".repeat(rowWidth)} ${files2.map((f) => f.padStart(cell)).join(" ")}`,
|
|
12356
|
-
|
|
12480
|
+
// Hex rows shift right one step per row, so the six neighbours sit around each cell.
|
|
12481
|
+
...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(" ")}`)
|
|
12357
12482
|
];
|
|
12358
12483
|
}
|
|
12359
12484
|
function agentMove(position, input) {
|
|
12360
12485
|
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
|
+
if (!action && input.from !== void 0 && input.to !== void 0 && "legal_wire_actions" in position) {
|
|
12487
|
+
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
|
+
if (wire) return wire;
|
|
12489
|
+
}
|
|
12361
12490
|
if (!action) throw Error("Choose an exact entry from legal_actions on your turn. Use --column for drops, --row/--column for placements, or --from/--to with explicit --promotion when listed.");
|
|
12362
12491
|
if (action.type === "swap") return { type: "swap" };
|
|
12363
12492
|
if (action.type === "edge") return { type: "place", at: action.edge - 1 };
|
|
@@ -12371,7 +12500,7 @@ function agentRules(game = "connect-four") {
|
|
|
12371
12500
|
const rules = {
|
|
12372
12501
|
"connect-four": "Drop into an unfilled column. Gravity places the piece in the lowest empty cell. Four or more matching pieces horizontally, vertically or diagonally wins; a full board without a winner draws.",
|
|
12373
12502
|
chess: "Standard chess, including castling, en passant, check, checkmate, stalemate, repetition and fifty-move draws. Seat 0 is White. Pick a listed from/to move. Promotions explicitly list q, r, b and n; supply --promotion. FEN is included for complete castling and en passant state.",
|
|
12374
|
-
checkers: "English draughts on 8 by 8. Seat 0
|
|
12503
|
+
checkers: "English draughts on 8 by 8. Seat 0 (x) starts on ranks 1\u20133 at the bottom and moves up towards rank 8; seat 1 (o) moves down. Men move and capture forward only, as in English draughts. Captures are compulsory; kings move one diagonal square in either direction. A multiple jump is submitted one step at a time: read status and continue with the same piece while it remains your turn. Crowning ends a turn. No pieces or no legal moves loses; repetition and 80 quiet half-moves draw.",
|
|
12375
12504
|
"tic-tac-toe": "X (seat 0) starts on a 3 by 3 board. Place on an empty square. Three matching marks horizontally, vertically or diagonally wins; a full board without a winner draws.",
|
|
12376
12505
|
reversi: "Bracket opposing discs in a straight line; all bracketed discs flip. Seat 0 is Black and starts. The engine automatically skips players with no legal move; the same seat may move twice. No pass command is needed. When neither can move, the most discs wins; equal counts draw.",
|
|
12377
12506
|
hex: "Connect opposite edges on the hexagonal board: seat 0 top to bottom, seat 1 left to right. On seat 1's first turn it may swap; this transposes the first stone and changes its owner. No draws.",
|
|
@@ -12379,7 +12508,7 @@ function agentRules(game = "connect-four") {
|
|
|
12379
12508
|
oware: "Sow the seeds from a nonempty pit on your side, skipping the origin after a full lap. Feed an empty opponent if possible. Capture opposing pits with two or three seeds, except when doing so would starve the opponent. Scores, feeding and end conditions follow the supplied rules and state.",
|
|
12380
12509
|
gomoku: "Place on an empty intersection of the 15 by 15 board. Exactly five contiguous stones horizontally, vertically or diagonally wins. A line longer than five does not win in this variant. A full board without a winner draws."
|
|
12381
12510
|
};
|
|
12382
|
-
const coordinates = !completeAgentGames.includes(game) || game === "dots-boxes" || game === "oware" ? actionHelp(game).indexing : game === "hex" ? "One-based rows and columns on the size in visible_state. Use --swap only when listed. Seat 0 connects top/bottom; seat 1 left/right." : game === "chess" ? "Use --from e2 --to e4. Board rows are ranks 8 to 1, columns a to h. Uppercase pieces are White, lowercase Black; dot is empty. FEN describes the full position." : game === "checkers" ? "Use --from b3 --to a4. Board rows are ranks 8 to 1, columns a to h. x/X are seat 0 men/kings; o/O are seat 1 men/kings; dot is empty." : game === "connect-four" ? "Columns are 1\u20137, left to right; use --column. X is seat 0, O is seat 1, dot is empty." : `Rows and columns are 1\u2013${game === "reversi" ? 8 : game === "gomoku" ? 15 : 3}, top to bottom and left to right; use --row and --column. X is seat 0, O is seat 1, dot is empty.`;
|
|
12511
|
+
const coordinates = !completeAgentGames.includes(game) || game === "dots-boxes" || game === "oware" ? actionHelp(game).indexing : game === "hex" ? "One-based rows and columns on the size in visible_state. Use --swap only when listed. Seat 0 (X, Black) connects top/bottom; seat 1 (O, White) left/right. board_labeled indents each row one step, so (r,c) touches the six cells (r,c-1), (r,c+1), (r-1,c), (r-1,c+1), (r+1,c-1) and (r+1,c)." : game === "chess" ? "Use --from e2 --to e4. Board rows are ranks 8 to 1, columns a to h. Uppercase pieces are White, lowercase Black; dot is empty. FEN describes the full position." : game === "checkers" ? "Use --from b3 --to a4. Board rows are ranks 8 to 1, columns a to h. x/X are seat 0 men/kings; o/O are seat 1 men/kings; dot is empty." : game === "connect-four" ? "Columns are 1\u20137, left to right; use --column. X is seat 0, O is seat 1, dot is empty." : `Rows and columns are 1\u2013${game === "reversi" ? 8 : game === "gomoku" ? 15 : 3}, top to bottom and left to right; use --row and --column. X is seat 0, O is seat 1, dot is empty.`;
|
|
12383
12512
|
return { protocol_version: 1, game, rules: multiplayerGames.find((g) => g.id === game).rules, guidance: rules[game] ?? "Use the player-visible state and documented wire actions. The server validates legality; an empty partial action list does not mean there are no legal moves.", action_types: action_catalog_default[game], action_help: actionHelp(game), coordinates, instructions };
|
|
12384
12513
|
}
|
|
12385
12514
|
var connectFourAgentRules = agentRules();
|
|
@@ -12558,6 +12687,7 @@ function networkCause(error) {
|
|
|
12558
12687
|
var readRoom = (session, signal) => rpc(session, "read", {}, true, signal);
|
|
12559
12688
|
var continuousGames = ["draw-guess", "categories"];
|
|
12560
12689
|
function waitForRoom(session, after, seconds, signal, untilAction = false) {
|
|
12690
|
+
const sawRevision = after !== void 0;
|
|
12561
12691
|
return new Promise((resolve, reject) => {
|
|
12562
12692
|
let socket, closed = false, reconnects = 0, received = false, opened = false;
|
|
12563
12693
|
let ping, retry;
|
|
@@ -12599,7 +12729,7 @@ function waitForRoom(session, after, seconds, signal, untilAction = false) {
|
|
|
12599
12729
|
received = true;
|
|
12600
12730
|
after ??= latest.revision;
|
|
12601
12731
|
if (latest.revision < after) throw new ServiceError("Your revision is ahead of this room. Read game status again.", "STALE_REVISION");
|
|
12602
|
-
const canAct2 = !continuousGames.includes(latest.game) && (latest.turn?.yours || !!(latest.legal_wire_actions_complete && latest.legal_wire_actions?.length));
|
|
12732
|
+
const canAct2 = !continuousGames.includes(latest.game) && (latest.turn?.yours || !sawRevision && !!(latest.legal_wire_actions_complete && latest.legal_wire_actions?.length));
|
|
12603
12733
|
if (untilAction && latest.revision > after && !canAct2 && latest.phase !== "finished" && !latest.you.left && !latest.paused) after = latest.revision;
|
|
12604
12734
|
if (latest.revision > after || canAct2 || latest.phase === "finished" || latest.you.left || latest.paused) finish6({ event: latest.phase === "finished" ? "finished" : latest.you.left ? "left" : latest.paused ? "paused" : latest.turn?.yours ? "your_turn" : canAct2 ? "can_act" : "changed", position: latest });
|
|
12605
12735
|
} catch (error) {
|
|
@@ -12665,11 +12795,19 @@ function nextStep(position, event, brief = false, server) {
|
|
|
12665
12795
|
const play = (move2) => `leafarcade game play ${room} --revision ${revision} --action ${move2}`;
|
|
12666
12796
|
if (wire?.length && !position.turn?.yours) return `You may act now (phase action outside the normal turn): ${play("'<one legal_wire_actions entry>'")}${flag}. Otherwise wait: ${wait}`;
|
|
12667
12797
|
if (position.turn?.yours) {
|
|
12668
|
-
if (wire?.length === 1 && wire[0].type === "next" && Object.keys(wire[0]).length === 1) return `The hand is over;
|
|
12798
|
+
if (wire?.length === 1 && wire[0].type === "next" && Object.keys(wire[0]).length === 1) return `The hand is over; start the next one (the deal moves on): ${play(`'{"type":"next"}'`)} --wait${flag}`;
|
|
12669
12799
|
const partial = !wire && position.legal_wire_actions?.length ? "Some legal actions are listed in legal_wire_actions, but the list is not exhaustive (see game rules for melds, offers, amounts or card groups). " : "";
|
|
12670
12800
|
const move2 = wire ? "'<one legal_wire_actions entry>'" : position.legal_actions_complete ? "'<one legal_actions entry>'" : "'<wire action JSON from game rules>'";
|
|
12671
12801
|
const check = lineGames.includes(position.game) ? "Check your immediate win and your opponent's immediate threats (every line, including diagonals), then: " : "";
|
|
12672
|
-
|
|
12802
|
+
const makao = position.game === "makao" ? ((state) => [
|
|
12803
|
+
state?.penalty ? `A ${state.penalty}-card penalty is active: answer with a matching attack card, or draw takes all ${state.penalty}. ` : "",
|
|
12804
|
+
state?.wait ? `A ${state.wait}-turn wait is active: answer with a 4, or draw serves the wait (no cards drawn). ` : "",
|
|
12805
|
+
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]}. ` : ""
|
|
12808
|
+
].join(""))(position.visible_state) : "";
|
|
12809
|
+
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}`;
|
|
12673
12811
|
}
|
|
12674
12812
|
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.`;
|
|
12675
12813
|
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.";
|
|
@@ -12761,7 +12899,7 @@ async function installSkill(agent = "codex", update = false) {
|
|
|
12761
12899
|
// packages/cli/package.json
|
|
12762
12900
|
var package_default = {
|
|
12763
12901
|
name: "@leafarcade/cli",
|
|
12764
|
-
version: "0.
|
|
12902
|
+
version: "0.8.0",
|
|
12765
12903
|
private: false,
|
|
12766
12904
|
description: "Create private browser board games and play with your AI agent. Includes 53 games and an installable Codex/Claude skill.",
|
|
12767
12905
|
type: "module",
|
|
@@ -12822,7 +12960,7 @@ var commands = {
|
|
|
12822
12960
|
status: { usage: "game status --room ROOMCODE", description: "Read your seat's visible state, phase, turn, revision, outcome and action completeness. Hidden information belonging to other players stays hidden. Includes a labeled board, last_move and a concrete next command.", options: ["room", "brief", "server"] },
|
|
12823
12961
|
"legal-actions": { usage: "game legal-actions --room ROOMCODE", description: "Read actions and legal_actions_complete. An empty partial list is not proof that no move exists. Use game rules and visible_state for JSON actions.", options: ["room", "server"] },
|
|
12824
12962
|
play: { usage: "game play --room ROOMCODE --revision R MOVE_FLAGS [--wait]", description: "Submit one move at the exact latest revision. Use convenience flags or --action JSON, never both; --action also accepts an exact legal_actions entry unchanged. --wait then waits for your next turn or the end, like game wait. The service validates legality. On stale or uncertain results, read status before acting again.", options: ["room", "revision", "action", "column", "row", "from", "to", "promotion", "swap", "edge", "pit", "wait", "timeout", "brief", "server"] },
|
|
12825
|
-
wait: { usage: "game wait --room ROOMCODE [--after-revision R] [--timeout 90]", description: "Wait for changes through WebSocket (1\u20133600 seconds, default 90). Without --after-revision it waits for the next change after the current one. Returns at once when you can
|
|
12963
|
+
wait: { usage: "game wait --room ROOMCODE [--after-revision R] [--timeout 90]", description: "Wait for changes through WebSocket (1\u20133600 seconds, default 90). Without --after-revision it waits for the next change after the current one. Returns at once when it is already your turn; optional phase actions outside your turn (such as a challenge window) return at once only without --after-revision, so you can wait through them. Returns your_turn, can_act (a phase action outside your turn), changed, paused, finished, left or waiting. Use it after every move instead of asking the human to report theirs. A timeout is normal, not resignation. This command cannot restart a stopped agent.", options: ["room", "after-revision", "timeout", "brief", "server"] },
|
|
12826
12964
|
pause: { usage: "game pause --room ROOMCODE --revision R", description: "Save your explicit pause in an active, unclocked private match. Any paused player blocks moves. Room expiry is not extended.", options: ["room", "revision", "brief", "server"] },
|
|
12827
12965
|
resume: { usage: "game resume --room ROOMCODE --revision R", description: "Clear your own pause. Each player who paused must resume before play continues.", options: ["room", "revision", "brief", "server"] },
|
|
12828
12966
|
rematch: { usage: "game rematch --room ROOMCODE --revision R", description: "Request another round after a normal finish. All players must agree and remain at the table. After resignation or leaving, create a fresh room. Never request a rematch without the human asking.", options: ["room", "revision", "brief", "server"] },
|
|
@@ -12958,7 +13096,7 @@ Coordinates
|
|
|
12958
13096
|
Actions
|
|
12959
13097
|
${rules.action_types.join(", ")}
|
|
12960
13098
|
${rules.action_help.game}
|
|
12961
|
-
${complete ? "Complete legal-action list: choose an exact legal_actions entry on your turn (pass it to --action unchanged, or use the flags)." : wireListed.includes(game) ?
|
|
13099
|
+
${complete ? "Complete legal-action list: choose an exact legal_actions entry on your turn (pass it to --action unchanged, or use the flags)." : wireListed.includes(game) ? `Complete legal_wire_actions list in status, including phase actions outside your turn: pass one entry to --action unchanged.${hasCardLabels(game) ? " Card names are in cards_labeled." : ""}` : "Partial legal-action list: inspect visible_state and game phase; use --action JSON. An empty list does not mean there are no moves."}
|
|
12962
13100
|
|
|
12963
13101
|
Move syntax example (illustrative; read status and verify legality first)
|
|
12964
13102
|
leafarcade game play --room ROOMCODE --revision R ${examples[game]} --wait
|
|
@@ -13042,6 +13180,11 @@ var show = (value) => JSON.stringify(value)?.slice(0, 120) ?? "undefined";
|
|
|
13042
13180
|
function stateChanges(game, before, after, limit = 30) {
|
|
13043
13181
|
const out2 = [];
|
|
13044
13182
|
const labels2 = [cardsLabeled(game, before) ?? {}, cardsLabeled(game, after) ?? {}];
|
|
13183
|
+
const names = /* @__PURE__ */ new Map();
|
|
13184
|
+
for (const side2 of labels2) for (const value of Object.values(side2)) for (const label of [value].flat()) {
|
|
13185
|
+
const id = /\(id (\d+)\)$/.exec(label)?.[1];
|
|
13186
|
+
if (id !== void 0) names.set(Number(id), label);
|
|
13187
|
+
}
|
|
13045
13188
|
const named = (path, value, side2) => {
|
|
13046
13189
|
const label = labels2[side2][path];
|
|
13047
13190
|
return label === void 0 ? show(value) : Array.isArray(label) ? label.join(", ") || "[]" : label;
|
|
@@ -13062,6 +13205,14 @@ function stateChanges(game, before, after, limit = 30) {
|
|
|
13062
13205
|
a.forEach((v, i) => walk(v, b[i], [...path, String(i)]));
|
|
13063
13206
|
return;
|
|
13064
13207
|
}
|
|
13208
|
+
const cardName = (card2) => names.get(card2) ?? show(card2);
|
|
13209
|
+
if (labels2[0][here] !== void 0 || labels2[1][here] !== void 0) {
|
|
13210
|
+
const added = b.filter((card2) => !a.includes(card2)), removed = a.filter((card2) => !b.includes(card2));
|
|
13211
|
+
if (added.length + removed.length < Math.max(a.length, b.length)) {
|
|
13212
|
+
out2.push(`${here}: ${[...added.map((c) => `+${cardName(c)}`), ...removed.map((c) => `-${cardName(c)}`)].join(", ")}`);
|
|
13213
|
+
return;
|
|
13214
|
+
}
|
|
13215
|
+
}
|
|
13065
13216
|
out2.push(`${here}: ${named(here, a, 0)} \u2192 ${named(here, b, 1)}`);
|
|
13066
13217
|
return;
|
|
13067
13218
|
}
|
|
@@ -13097,7 +13248,16 @@ async function withChanges(result, path) {
|
|
|
13097
13248
|
if (!changes.length) return result;
|
|
13098
13249
|
const boards = [before.state, position.visible_state].map((state) => state?.board);
|
|
13099
13250
|
const cells = Array.isArray(boards[0]) && Array.isArray(boards[1]) && boards[0].length === boards[1].length ? boards[1].flatMap((value, at) => JSON.stringify(value) === JSON.stringify(boards[0][at]) ? [] : [{ at, before: boards[0][at], after: value }]) : [];
|
|
13100
|
-
const
|
|
13251
|
+
const square2 = (at) => "abcdefgh"[at % 8] + (8 - Math.floor(at / 8));
|
|
13252
|
+
const derived = position.game === "checkers" ? cells.map((cell) => ({ ...cell, at: square2(cell.at) })) : "board_legend" in position ? cells : [];
|
|
13253
|
+
const piles = [before.state, position.visible_state].map((state) => state?.discard);
|
|
13254
|
+
const top = position.game === "gin-rummy" && Array.isArray(piles[0]) && Array.isArray(piles[1]) ? piles[0].at(-1) : void 0;
|
|
13255
|
+
const taken = typeof top === "number" && !piles[1].includes(top) ? `${cardLabel(top)} (id ${top})` : null;
|
|
13256
|
+
const annotated = {
|
|
13257
|
+
...position,
|
|
13258
|
+
...position.last_move === null && derived.length && derived.length <= 12 ? { last_move: { board_changes: derived } } : {},
|
|
13259
|
+
changed_since_last_read: { from_revision: before.revision, ...taken ? { taken_from_discard: taken } : {}, changes }
|
|
13260
|
+
};
|
|
13101
13261
|
return "position" in result ? { ...result, position: annotated } : annotated;
|
|
13102
13262
|
}
|
|
13103
13263
|
|
|
@@ -13502,6 +13662,10 @@ var winner = (position) => {
|
|
|
13502
13662
|
return seat === void 0 || seat === null ? null : seat === position.you.seat ? "you" : seatName(position, seat);
|
|
13503
13663
|
};
|
|
13504
13664
|
var seatName = (position, seat) => `seat ${seat} (${position.players[seat]?.name ?? "empty"})`;
|
|
13665
|
+
var scores = (position) => {
|
|
13666
|
+
const value = position.visible_state?.scores;
|
|
13667
|
+
return Array.isArray(value) && value.length && value.every((n) => typeof n === "number") ? value : null;
|
|
13668
|
+
};
|
|
13505
13669
|
function briefPosition(position) {
|
|
13506
13670
|
const turn = position.turn ? position.turn.yours ? "yours" : seatName(position, position.turn.seat) : null;
|
|
13507
13671
|
const wire = "legal_wire_actions" in position ? position.legal_wire_actions ?? [] : null;
|
|
@@ -13514,6 +13678,8 @@ function briefPosition(position) {
|
|
|
13514
13678
|
you: position.you.piece,
|
|
13515
13679
|
turn,
|
|
13516
13680
|
...position.paused ? { paused: true } : {},
|
|
13681
|
+
// Match or capture scores by zero-based seat, when the game keeps them.
|
|
13682
|
+
...scores(position) ? { scores: scores(position) } : {},
|
|
13517
13683
|
..."in_check" in position ? { in_check: true } : {},
|
|
13518
13684
|
...position.you.left ? { left: true } : {},
|
|
13519
13685
|
...position.phase === "waiting" ? { seated: position.players.filter((player) => !player.left).length, capacity: position.capacity } : {},
|
|
@@ -13527,8 +13693,9 @@ function briefPosition(position) {
|
|
|
13527
13693
|
..."deadline_seconds_left" in position ? { deadline_seconds_left: position.deadline_seconds_left } : {},
|
|
13528
13694
|
..."board_legend" in position ? { board_legend: position.board_legend } : {},
|
|
13529
13695
|
// Only the classic convenience games are fully described by their labeled board.
|
|
13530
|
-
...position.legal_actions_complete || !position.visible_state ? {} : { visible_state: position.visible_state },
|
|
13696
|
+
...position.legal_actions_complete && position.board_labeled || !position.visible_state ? {} : { visible_state: position.visible_state },
|
|
13531
13697
|
...position.cards_labeled ? { cards_labeled: position.cards_labeled } : {},
|
|
13698
|
+
..."gin_deadwood" in position ? { gin_deadwood: position.gin_deadwood } : {},
|
|
13532
13699
|
...position.access_requests?.length ? { access_requests: position.access_requests } : {}
|
|
13533
13700
|
};
|
|
13534
13701
|
}
|
|
@@ -13678,7 +13845,7 @@ try {
|
|
|
13678
13845
|
result = { event: "browser_requested", room, url, browser: await openBrowser(url) };
|
|
13679
13846
|
} else if (command === "start" || command === "leave" || command === "resign" || command === "rematch" || command === "pause" || command === "resume") {
|
|
13680
13847
|
const revision = integer(values.revision, "revision", 0, Number.MAX_SAFE_INTEGER);
|
|
13681
|
-
if (revision !== view.revision) throw new ServiceError(
|
|
13848
|
+
if (revision !== view.revision) throw new ServiceError(`The room changed (now revision ${view.revision}). Read game status before changing the room.`, "STALE_REVISION");
|
|
13682
13849
|
if (command === "start") {
|
|
13683
13850
|
await rpc(session, "start", { revision }, false, controller.signal);
|
|
13684
13851
|
result = { event: "started", position: agentPosition(await readRoom(session, controller.signal)) };
|
|
@@ -13700,7 +13867,7 @@ try {
|
|
|
13700
13867
|
const revision = integer(values.revision, "revision", 0, Number.MAX_SAFE_INTEGER);
|
|
13701
13868
|
const column = values.column === void 0 ? void 0 : integer(values.column, "column", 1, position.board_size.columns);
|
|
13702
13869
|
const row = values.row === void 0 ? void 0 : integer(values.row, "row", 1, position.board_size.rows);
|
|
13703
|
-
if (revision !== position.revision) throw new ServiceError(
|
|
13870
|
+
if (revision !== position.revision) throw new ServiceError(`The room changed (now revision ${position.revision}). Read game status before playing.`, "STALE_REVISION");
|
|
13704
13871
|
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.");
|
|
13705
13872
|
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) });
|
|
13706
13873
|
const waitSeconds = integer(values.timeout ?? String(defaultWaitSeconds), "timeout", 1, 3600);
|