@leafarcade/cli 0.7.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cli.mjs +1128 -952
- package/package.json +1 -1
- package/skills/leafarcade/SKILL.md +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -3735,6 +3735,30 @@ 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
|
+
function drawReason(history) {
|
|
3758
|
+
const last = history.at(-1);
|
|
3759
|
+
return history.filter((key2) => key2 === last).length >= 3 ? "Draw \xB7 the same position occurred three times." : "Draw \xB7 too many moves without progress (no capture or man move).";
|
|
3760
|
+
}
|
|
3761
|
+
|
|
3738
3762
|
// lib/random.ts
|
|
3739
3763
|
function randomStep(seed) {
|
|
3740
3764
|
const next3 = seed + 1831565813 >>> 0;
|
|
@@ -3767,180 +3791,745 @@ function shuffled(items, entropy2) {
|
|
|
3767
3791
|
function index(value, length) {
|
|
3768
3792
|
return Number.isInteger(value) && value >= 0 && value < length;
|
|
3769
3793
|
}
|
|
3770
|
-
function best(
|
|
3771
|
-
const high = Math.max(...
|
|
3794
|
+
function best(scores2) {
|
|
3795
|
+
const high = Math.max(...scores2), winners = scores2.flatMap((s, i) => s === high ? [i] : []);
|
|
3772
3796
|
return winners.length === 1 ? winners[0] : null;
|
|
3773
3797
|
}
|
|
3774
3798
|
|
|
3775
|
-
// lib/multiplayer/
|
|
3776
|
-
var
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3799
|
+
// lib/multiplayer/mills.ts
|
|
3800
|
+
var millLines = [
|
|
3801
|
+
[0, 1, 2],
|
|
3802
|
+
[2, 3, 4],
|
|
3803
|
+
[4, 5, 6],
|
|
3804
|
+
[6, 7, 0],
|
|
3805
|
+
[8, 9, 10],
|
|
3806
|
+
[10, 11, 12],
|
|
3807
|
+
[12, 13, 14],
|
|
3808
|
+
[14, 15, 8],
|
|
3809
|
+
[16, 17, 18],
|
|
3810
|
+
[18, 19, 20],
|
|
3811
|
+
[20, 21, 22],
|
|
3812
|
+
[22, 23, 16],
|
|
3813
|
+
[1, 9, 17],
|
|
3814
|
+
[3, 11, 19],
|
|
3815
|
+
[5, 13, 21],
|
|
3816
|
+
[7, 15, 23]
|
|
3817
|
+
];
|
|
3818
|
+
var millEdges = millLines.flatMap(([a, b, c]) => [
|
|
3819
|
+
[a, b],
|
|
3820
|
+
[b, c]
|
|
3821
|
+
]);
|
|
3822
|
+
function createMills() {
|
|
3823
|
+
return {
|
|
3824
|
+
board: Array(24).fill(0),
|
|
3825
|
+
reserves: [9, 9],
|
|
3826
|
+
turn: 0,
|
|
3827
|
+
captures: 0,
|
|
3828
|
+
winner: null,
|
|
3829
|
+
draw: false,
|
|
3830
|
+
history: []
|
|
3831
|
+
};
|
|
3789
3832
|
}
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3833
|
+
var linesAt = (board, at) => millLines.filter(
|
|
3834
|
+
(line) => line.includes(at) && line.every((i) => board[i] === board[at])
|
|
3835
|
+
);
|
|
3836
|
+
function millCaptures(s) {
|
|
3837
|
+
const opponents = s.board.flatMap((p, i) => p === 2 - s.turn ? [i] : []), loose = opponents.filter((at) => !linesAt(s.board, at).length);
|
|
3838
|
+
return loose.length ? loose : opponents;
|
|
3839
|
+
}
|
|
3840
|
+
function millMoves(s) {
|
|
3841
|
+
if (s.winner !== null || s.draw || s.captures) return [];
|
|
3842
|
+
const empty = s.board.flatMap((p, i) => !p ? [i] : []);
|
|
3843
|
+
if (s.reserves[s.turn]) return empty.map((to) => ({ from: null, to }));
|
|
3844
|
+
const own = s.board.flatMap((p, i) => p === s.turn + 1 ? [i] : []);
|
|
3845
|
+
return own.flatMap(
|
|
3846
|
+
(from) => empty.filter(
|
|
3847
|
+
(to) => own.length === 3 || millEdges.some(
|
|
3848
|
+
([a, b]) => a === from && b === to || b === from && a === to
|
|
3849
|
+
)
|
|
3850
|
+
).map((to) => ({ from, to }))
|
|
3851
|
+
);
|
|
3852
|
+
}
|
|
3853
|
+
function finishTurn(s) {
|
|
3854
|
+
const turn = s.turn === 0 ? 1 : 0, key2 = s.board.join(",") + ":" + turn + ":" + s.reserves.join(",");
|
|
3855
|
+
const next3 = {
|
|
3856
|
+
...s,
|
|
3857
|
+
turn,
|
|
3858
|
+
captures: 0,
|
|
3859
|
+
history: [...s.history, key2]
|
|
3860
|
+
};
|
|
3861
|
+
const count = next3.board.filter((p) => p === turn + 1).length;
|
|
3862
|
+
if (!next3.reserves[turn] && (count < 3 || !millMoves(next3).length))
|
|
3863
|
+
next3.winner = s.turn;
|
|
3864
|
+
else next3.draw = next3.history.filter((k) => k === key2).length >= 3;
|
|
3865
|
+
return next3;
|
|
3866
|
+
}
|
|
3867
|
+
function playMills(s, type, to, from) {
|
|
3868
|
+
if (!index(to, 24) || s.winner !== null || s.draw)
|
|
3869
|
+
throw Error("Choose a legal point.");
|
|
3870
|
+
const board = [...s.board], reserves = [...s.reserves];
|
|
3871
|
+
if (type === "capture") {
|
|
3872
|
+
if (!s.captures || !millCaptures(s).includes(to))
|
|
3873
|
+
throw Error(
|
|
3874
|
+
"Capture a piece outside a mill, unless all pieces are in mills."
|
|
3875
|
+
);
|
|
3876
|
+
board[to] = 0;
|
|
3877
|
+
const next4 = { ...s, board, captures: s.captures - 1 };
|
|
3878
|
+
return next4.captures && millCaptures(next4).length ? next4 : finishTurn(next4);
|
|
3879
|
+
}
|
|
3880
|
+
if (!["place", "move"].includes(type) || !millMoves(s).some(
|
|
3881
|
+
(m) => m.to === to && m.from === (type === "place" ? null : from)
|
|
3882
|
+
))
|
|
3883
|
+
throw Error(
|
|
3884
|
+
"Place from your reserve or move to a connected point. With three pieces you may fly."
|
|
3885
|
+
);
|
|
3886
|
+
if (type === "place") reserves[s.turn]--;
|
|
3887
|
+
else board[from] = 0;
|
|
3888
|
+
board[to] = s.turn + 1;
|
|
3889
|
+
const captures = Math.min(
|
|
3890
|
+
linesAt(board, to).length,
|
|
3891
|
+
board.filter((p) => p === 2 - s.turn).length
|
|
3795
3892
|
);
|
|
3893
|
+
const next3 = { ...s, board, reserves, captures };
|
|
3894
|
+
return captures ? next3 : finishTurn(next3);
|
|
3895
|
+
}
|
|
3896
|
+
function millLastTurn(s) {
|
|
3897
|
+
const boards = s.history.slice(-2).map((key2) => key2.split(":")[0].split(",").map(Number));
|
|
3898
|
+
const after = boards.at(-1), before = boards.length === 2 ? boards[0] : Array(24).fill(0);
|
|
3899
|
+
if (!after) return null;
|
|
3900
|
+
const seat = Number(s.history.at(-1).split(":")[1]) === 0 ? 1 : 0;
|
|
3901
|
+
const to = after.findIndex((p, i) => p === seat + 1 && before[i] !== seat + 1);
|
|
3902
|
+
if (to < 0) return null;
|
|
3903
|
+
const from = before.findIndex((p, i) => p === seat + 1 && after[i] !== seat + 1);
|
|
3904
|
+
const captured = before.flatMap((p, i) => p === 2 - seat && after[i] !== p ? [i] : []);
|
|
3905
|
+
return { seat, from: from < 0 ? null : from, to, captured };
|
|
3796
3906
|
}
|
|
3797
3907
|
|
|
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
|
-
|
|
3938
|
-
|
|
3908
|
+
// lib/multiplayer/cribbage.ts
|
|
3909
|
+
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)];
|
|
3910
|
+
|
|
3911
|
+
// lib/multiplayer/dots-boxes.ts
|
|
3912
|
+
function boxEdges(size, box) {
|
|
3913
|
+
const x = box % size, y = Math.floor(box / size), offset = size * (size + 1);
|
|
3914
|
+
return [
|
|
3915
|
+
y * size + x,
|
|
3916
|
+
(y + 1) * size + x,
|
|
3917
|
+
offset + y * (size + 1) + x,
|
|
3918
|
+
offset + y * (size + 1) + x + 1
|
|
3919
|
+
];
|
|
3920
|
+
}
|
|
3921
|
+
|
|
3922
|
+
// lib/multiplayer/trick-cards.ts
|
|
3923
|
+
var cardSuit = (card2) => Math.floor(card2 / 13);
|
|
3924
|
+
var cardRank = (card2) => card2 % 13 + 2;
|
|
3925
|
+
var cardLabel = (card2) => `${["", "", "2", "3", "4", "5", "6", "7", "8", "9", "10", "J", "Q", "K", "A"][cardRank(card2)]}${["\u2663", "\u2666", "\u2665", "\u2660"][cardSuit(card2)]}`;
|
|
3926
|
+
var penalty = (card2) => cardSuit(card2) === 2 ? 1 : card2 === 49 ? 13 : 0;
|
|
3927
|
+
function first(s) {
|
|
3928
|
+
return s.variant === "spades" ? s.dealer : (s.dealer + 1) % s.hands.length;
|
|
3929
|
+
}
|
|
3930
|
+
function deal(s, entropy2) {
|
|
3931
|
+
const count = s.hands.length, size = s.variant === "oh-hell" ? Math.max(1, (count === 4 ? 13 : 17) - s.round) : 13, deck = shuffled(
|
|
3932
|
+
Array.from({ length: 52 }, (_, i) => i),
|
|
3933
|
+
entropy2
|
|
3934
|
+
);
|
|
3935
|
+
const hands = Array.from(
|
|
3936
|
+
{ length: count },
|
|
3937
|
+
(_, i) => deck.slice(i * size, (i + 1) * size).sort((a, b) => a - b)
|
|
3938
|
+
);
|
|
3939
|
+
const t = {
|
|
3940
|
+
...s,
|
|
3941
|
+
size,
|
|
3942
|
+
hands,
|
|
3943
|
+
stock: deck.slice(count * size),
|
|
3944
|
+
trick: [],
|
|
3945
|
+
lastTrick: [],
|
|
3946
|
+
tricks: Array(count).fill(0),
|
|
3947
|
+
penalties: Array(count).fill(0),
|
|
3948
|
+
bids: Array(count).fill(null),
|
|
3949
|
+
blind: Array(count).fill(false),
|
|
3950
|
+
blindAsked: Array(count).fill(false),
|
|
3951
|
+
selected: Array.from({ length: count }, () => []),
|
|
3952
|
+
passed: Array(count).fill(false),
|
|
3953
|
+
broken: false,
|
|
3954
|
+
winner: null,
|
|
3955
|
+
result: ""
|
|
3956
|
+
};
|
|
3957
|
+
t.trump = s.variant === "spades" ? 3 : s.variant === "oh-hell" && s.round > 0 ? cardSuit(t.stock[0]) : null;
|
|
3958
|
+
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";
|
|
3959
|
+
t.turn = s.variant === "hearts" ? t.phase === "play" ? hands.findIndex((h) => h.includes(0)) : 0 : first(t);
|
|
3960
|
+
return t;
|
|
3961
|
+
}
|
|
3962
|
+
function createTrickCards(variant, count, entropy2) {
|
|
3963
|
+
if (!index(count - 2, 3) || variant !== "oh-hell" && count !== 4)
|
|
3964
|
+
throw Error("Choose the required player count.");
|
|
3965
|
+
return deal(
|
|
3966
|
+
{
|
|
3967
|
+
variant,
|
|
3968
|
+
turn: 0,
|
|
3969
|
+
dealer: Math.floor(draws(entropy2, 1)[0] * count),
|
|
3970
|
+
round: 0,
|
|
3971
|
+
size: 13,
|
|
3972
|
+
phase: "bid",
|
|
3973
|
+
hands: Array.from({ length: count }, () => []),
|
|
3974
|
+
stock: [],
|
|
3975
|
+
trick: [],
|
|
3976
|
+
lastTrick: [],
|
|
3977
|
+
tricks: [],
|
|
3978
|
+
penalties: [],
|
|
3979
|
+
bids: [],
|
|
3980
|
+
blind: [],
|
|
3981
|
+
blindAsked: [],
|
|
3982
|
+
selected: [],
|
|
3983
|
+
passed: [],
|
|
3984
|
+
scores: Array(variant === "spades" ? 2 : count).fill(0),
|
|
3985
|
+
bags: [0, 0],
|
|
3986
|
+
trump: null,
|
|
3987
|
+
broken: false,
|
|
3988
|
+
winner: null,
|
|
3989
|
+
result: ""
|
|
3990
|
+
},
|
|
3991
|
+
entropy2
|
|
3992
|
+
);
|
|
3993
|
+
}
|
|
3994
|
+
function trickLegalCards(s) {
|
|
3995
|
+
if (s.phase !== "play") return [];
|
|
3996
|
+
const hand2 = s.hands[s.turn].filter((c) => c !== null), lead = s.trick[0];
|
|
3997
|
+
if (lead) {
|
|
3998
|
+
const suited = hand2.filter((c) => cardSuit(c) === cardSuit(lead.card));
|
|
3999
|
+
if (suited.length) return suited;
|
|
4000
|
+
if (s.variant === "hearts" && s.tricks.every((n) => n === 0)) {
|
|
4001
|
+
const clean = hand2.filter((c) => !penalty(c));
|
|
4002
|
+
if (clean.length) return clean;
|
|
4003
|
+
}
|
|
4004
|
+
return hand2;
|
|
4005
|
+
}
|
|
4006
|
+
if (s.variant === "hearts") {
|
|
4007
|
+
if (s.tricks.every((n) => n === 0)) return hand2.includes(0) ? [0] : [];
|
|
4008
|
+
if (!s.broken) {
|
|
4009
|
+
const clean = hand2.filter((c) => cardSuit(c) !== 2);
|
|
4010
|
+
if (clean.length) return clean;
|
|
4011
|
+
}
|
|
4012
|
+
}
|
|
4013
|
+
if (s.variant === "spades" && !s.broken) {
|
|
4014
|
+
const clean = hand2.filter((c) => cardSuit(c) !== 3);
|
|
4015
|
+
if (clean.length) return clean;
|
|
4016
|
+
}
|
|
4017
|
+
return hand2;
|
|
4018
|
+
}
|
|
4019
|
+
function finish(s) {
|
|
4020
|
+
if (s.variant === "hearts") {
|
|
4021
|
+
s.scores = s.scores.map((n, i) => n + s.penalties[i]);
|
|
4022
|
+
s.result = `Round penalties: ${s.penalties.join(" \xB7 ")}`;
|
|
4023
|
+
if (s.scores.some((n) => n >= 100)) {
|
|
4024
|
+
s.phase = "finished";
|
|
4025
|
+
s.winner = best(s.scores.map((n) => -n));
|
|
4026
|
+
} else s.phase = "round";
|
|
4027
|
+
} else if (s.variant === "spades") {
|
|
4028
|
+
const gains = [0, 0];
|
|
4029
|
+
for (const team2 of [0, 1]) {
|
|
4030
|
+
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);
|
|
4031
|
+
gains[team2] = taken >= bid ? bid * 10 + Math.max(0, taken - bid) : -bid * 10;
|
|
4032
|
+
const bags = Math.max(0, taken - bid) + seats2.filter((i) => s.bids[i] === 0).reduce((n, i) => n + s.tricks[i], 0);
|
|
4033
|
+
s.bags[team2] += bags;
|
|
4034
|
+
gains[team2] -= 100 * Math.floor(s.bags[team2] / 10);
|
|
4035
|
+
s.bags[team2] %= 10;
|
|
4036
|
+
for (const i of seats2)
|
|
4037
|
+
if (s.bids[i] === 0)
|
|
4038
|
+
gains[team2] += (s.tricks[i] === 0 ? 1 : -1) * (s.blind[i] ? 200 : 100);
|
|
4039
|
+
}
|
|
4040
|
+
s.scores = s.scores.map((n, i) => n + gains[i]);
|
|
4041
|
+
s.result = `Team 1 & 3: ${gains[0]} \xB7 Team 2 & 4: ${gains[1]}`;
|
|
4042
|
+
if (s.scores.some((n) => n >= 500 || n <= -200)) {
|
|
4043
|
+
s.phase = "finished";
|
|
4044
|
+
s.winner = best(s.scores);
|
|
4045
|
+
} else s.phase = "round";
|
|
4046
|
+
} else {
|
|
4047
|
+
s.scores = s.scores.map(
|
|
4048
|
+
(n, i) => n + (s.bids[i] === s.tricks[i] ? 10 + s.tricks[i] : 0)
|
|
4049
|
+
);
|
|
4050
|
+
s.result = `Tricks taken: ${s.tricks.join(" \xB7 ")}`;
|
|
4051
|
+
const rounds = (s.hands.length === 4 ? 13 : 17) + s.hands.length - 1;
|
|
4052
|
+
if (s.round + 1 >= rounds) {
|
|
4053
|
+
s.phase = "finished";
|
|
4054
|
+
s.winner = best(s.scores);
|
|
4055
|
+
} else s.phase = "round";
|
|
4056
|
+
}
|
|
4057
|
+
s.turn = first(s);
|
|
4058
|
+
}
|
|
4059
|
+
function playTrickCards(s, type, at, amount, entropy2) {
|
|
4060
|
+
if (s.phase === "finished") throw Error("This match is finished.");
|
|
4061
|
+
if (type === "next" && s.phase === "round")
|
|
4062
|
+
return deal(
|
|
4063
|
+
{ ...s, round: s.round + 1, dealer: (s.dealer + 1) % s.hands.length },
|
|
4064
|
+
entropy2
|
|
4065
|
+
);
|
|
4066
|
+
const t = structuredClone(s), seat = s.turn, count = s.hands.length;
|
|
4067
|
+
if (s.phase === "pass") {
|
|
4068
|
+
if (type === "select" && at !== void 0 && s.hands[seat].includes(at)) {
|
|
4069
|
+
const selected = t.selected[seat];
|
|
4070
|
+
if (selected.includes(at))
|
|
4071
|
+
t.selected[seat] = selected.filter((c) => c !== at);
|
|
4072
|
+
else if (selected.length < 3) selected.push(at);
|
|
4073
|
+
else throw Error("Select three cards.");
|
|
4074
|
+
return t;
|
|
4075
|
+
}
|
|
4076
|
+
if (type !== "pass" || t.selected[seat].length !== 3)
|
|
4077
|
+
throw Error("Choose three cards to pass.");
|
|
4078
|
+
t.passed[seat] = true;
|
|
4079
|
+
if (t.passed.every(Boolean)) {
|
|
4080
|
+
const offset = [1, 3, 2][s.round % 4];
|
|
4081
|
+
t.hands = t.hands.map(
|
|
4082
|
+
(h, i) => h.filter((c) => !t.selected[i].includes(c))
|
|
4083
|
+
);
|
|
4084
|
+
t.selected.forEach(
|
|
4085
|
+
(cards, i) => t.hands[(i + offset) % 4].push(...cards)
|
|
4086
|
+
);
|
|
4087
|
+
t.hands.forEach((h) => h.sort((a, b) => a - b));
|
|
4088
|
+
t.selected = t.selected.map(() => []);
|
|
4089
|
+
t.phase = "play";
|
|
4090
|
+
t.turn = t.hands.findIndex((h) => h.includes(0));
|
|
4091
|
+
} else t.turn = t.passed.findIndex((n) => !n);
|
|
4092
|
+
return t;
|
|
4093
|
+
}
|
|
4094
|
+
if (s.phase === "blind") {
|
|
4095
|
+
const eligible = s.scores[1 - seat % 2] - s.scores[seat % 2] >= 200;
|
|
4096
|
+
if (type === "blind" && eligible) {
|
|
4097
|
+
t.bids[seat] = 0;
|
|
4098
|
+
t.blind[seat] = true;
|
|
4099
|
+
} else if (type !== "look")
|
|
4100
|
+
throw Error("Choose blind nil or look at your cards.");
|
|
4101
|
+
t.blindAsked[seat] = true;
|
|
4102
|
+
if (t.blindAsked.every(Boolean)) {
|
|
4103
|
+
t.phase = "bid";
|
|
4104
|
+
t.turn = first(t);
|
|
4105
|
+
while (t.bids[t.turn] !== null) t.turn = (t.turn + 1) % count;
|
|
4106
|
+
} else t.turn = (seat + 1) % count;
|
|
4107
|
+
return t;
|
|
4108
|
+
}
|
|
4109
|
+
if (s.phase === "bid") {
|
|
4110
|
+
if (type !== "bid" || amount === void 0 || !index(amount, s.size + 1))
|
|
4111
|
+
throw Error("Choose a legal bid.");
|
|
4112
|
+
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)
|
|
4113
|
+
throw Error("The last bid cannot make the total equal the hand size.");
|
|
4114
|
+
t.bids[seat] = amount;
|
|
4115
|
+
if (t.bids.every((b) => b !== null)) {
|
|
4116
|
+
t.phase = "play";
|
|
4117
|
+
t.turn = first(t);
|
|
4118
|
+
} else {
|
|
4119
|
+
t.turn = (seat + 1) % count;
|
|
4120
|
+
while (t.bids[t.turn] !== null) t.turn = (t.turn + 1) % count;
|
|
4121
|
+
}
|
|
4122
|
+
return t;
|
|
4123
|
+
}
|
|
4124
|
+
if (s.phase === "moon") {
|
|
4125
|
+
if (type !== "moon" || ![0, 1].includes(amount ?? -1))
|
|
4126
|
+
throw Error("Choose how to score the moon.");
|
|
4127
|
+
t.penalties = t.penalties.map(
|
|
4128
|
+
(_, i) => amount === 0 ? i === seat ? -26 : 0 : i === seat ? 0 : 26
|
|
4129
|
+
);
|
|
4130
|
+
finish(t);
|
|
4131
|
+
return t;
|
|
4132
|
+
}
|
|
4133
|
+
if (type !== "play" || at === void 0 || !trickLegalCards(s).includes(at))
|
|
4134
|
+
throw Error("Choose a legal card.");
|
|
4135
|
+
t.hands[seat] = t.hands[seat].filter((c) => c !== at);
|
|
4136
|
+
t.trick.push({ seat, card: at });
|
|
4137
|
+
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))
|
|
4138
|
+
t.broken = true;
|
|
4139
|
+
t.turn = (seat + 1) % count;
|
|
4140
|
+
if (t.trick.length === count) {
|
|
4141
|
+
const lead = cardSuit(t.trick[0].card), strength = (c) => (cardSuit(c) === s.trump ? 100 : cardSuit(c) === lead ? 50 : 0) + cardRank(c);
|
|
4142
|
+
const winner2 = t.trick.reduce(
|
|
4143
|
+
(a, b) => strength(b.card) > strength(a.card) ? b : a
|
|
4144
|
+
).seat;
|
|
4145
|
+
t.tricks[winner2]++;
|
|
4146
|
+
t.penalties[winner2] += t.trick.reduce((n, c) => n + penalty(c.card), 0);
|
|
4147
|
+
t.lastTrick = t.trick;
|
|
4148
|
+
t.trick = [];
|
|
4149
|
+
t.turn = winner2;
|
|
4150
|
+
if (t.hands.every((h) => !h.length)) {
|
|
4151
|
+
const moon = s.variant === "hearts" ? t.penalties.findIndex((n) => n === 26) : -1;
|
|
4152
|
+
if (moon >= 0) {
|
|
4153
|
+
t.phase = "moon";
|
|
4154
|
+
t.turn = moon;
|
|
4155
|
+
} else finish(t);
|
|
4156
|
+
}
|
|
4157
|
+
}
|
|
4158
|
+
return t;
|
|
4159
|
+
}
|
|
4160
|
+
function trickView(s, seat) {
|
|
4161
|
+
const { stock, selected, ...publicState } = s;
|
|
4162
|
+
return {
|
|
4163
|
+
...publicState,
|
|
4164
|
+
stockCount: stock.length,
|
|
4165
|
+
selected: selected.map((cards, i) => i === seat ? cards : []),
|
|
4166
|
+
hands: s.hands.map(
|
|
4167
|
+
(h, i) => i === seat && !(s.phase === "blind" && !s.blindAsked[i]) ? h : h.map(() => null)
|
|
4168
|
+
)
|
|
4169
|
+
};
|
|
4170
|
+
}
|
|
4171
|
+
|
|
4172
|
+
// lib/multiplayer/gin-rummy.ts
|
|
4173
|
+
var ginRank = (c) => cardRank(c) === 14 ? 1 : cardRank(c);
|
|
4174
|
+
var ginValue = (c) => Math.min(10, ginRank(c));
|
|
4175
|
+
function ginMelds(hand2) {
|
|
4176
|
+
const melds = [];
|
|
4177
|
+
for (let rank2 = 1; rank2 <= 13; rank2++) {
|
|
4178
|
+
const cards = hand2.filter((c) => ginRank(c) === rank2);
|
|
4179
|
+
if (cards.length >= 3) {
|
|
4180
|
+
if (cards.length === 4) {
|
|
4181
|
+
melds.push(cards);
|
|
4182
|
+
for (let i = 0; i < 4; i++) melds.push(cards.filter((_, j) => j !== i));
|
|
4183
|
+
} else melds.push(cards);
|
|
4184
|
+
}
|
|
4185
|
+
}
|
|
4186
|
+
for (let suit = 0; suit < 4; suit++) {
|
|
4187
|
+
const cards = hand2.filter((c) => cardSuit(c) === suit).sort((a, b) => ginRank(a) - ginRank(b));
|
|
4188
|
+
for (let start = 0; start < cards.length; start++)
|
|
4189
|
+
for (let end = start + 2; end < cards.length; end++) {
|
|
4190
|
+
const slice = cards.slice(start, end + 1);
|
|
4191
|
+
if (slice.every(
|
|
4192
|
+
(c, i) => i === 0 || ginRank(c) === ginRank(slice[i - 1]) + 1
|
|
4193
|
+
))
|
|
4194
|
+
melds.push(slice);
|
|
4195
|
+
else break;
|
|
4196
|
+
}
|
|
4197
|
+
}
|
|
4198
|
+
return melds;
|
|
4199
|
+
}
|
|
4200
|
+
function partitions(hand2) {
|
|
4201
|
+
const melds = ginMelds(hand2), out2 = [];
|
|
4202
|
+
function visit(start, used, chosen) {
|
|
4203
|
+
const deadwood2 = hand2.filter((c) => !used.has(c));
|
|
4204
|
+
out2.push({
|
|
4205
|
+
melds: chosen,
|
|
4206
|
+
deadwood: deadwood2,
|
|
4207
|
+
value: deadwood2.reduce((n, c) => n + ginValue(c), 0)
|
|
4208
|
+
});
|
|
4209
|
+
for (let i = start; i < melds.length; i++)
|
|
4210
|
+
if (melds[i].every((c) => !used.has(c)))
|
|
4211
|
+
visit(i + 1, /* @__PURE__ */ new Set([...used, ...melds[i]]), [...chosen, melds[i]]);
|
|
4212
|
+
}
|
|
4213
|
+
visit(0, /* @__PURE__ */ new Set(), []);
|
|
4214
|
+
return out2;
|
|
4215
|
+
}
|
|
4216
|
+
function ginLayout(hand2) {
|
|
4217
|
+
return partitions(hand2).reduce((a, b) => b.value < a.value ? b : a);
|
|
4218
|
+
}
|
|
4219
|
+
function ginLayoffValue(hand2, onto) {
|
|
4220
|
+
const cache = /* @__PURE__ */ new Map();
|
|
4221
|
+
function attach(cards, melds) {
|
|
4222
|
+
const key2 = cards.join(",") + "|" + melds.map((m) => m.join(",")).join(";"), cached = cache.get(key2);
|
|
4223
|
+
if (cached !== void 0) return cached;
|
|
4224
|
+
let best2 = cards.reduce((n, c) => n + ginValue(c), 0);
|
|
4225
|
+
cards.forEach(
|
|
4226
|
+
(c, i) => melds.forEach((m, j) => {
|
|
4227
|
+
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);
|
|
4228
|
+
if (fits) {
|
|
4229
|
+
const next3 = melds.map(
|
|
4230
|
+
(m2, k) => k === j ? [...m2, c].sort((a, b) => ginRank(a) - ginRank(b)) : m2
|
|
4231
|
+
);
|
|
4232
|
+
best2 = Math.min(
|
|
4233
|
+
best2,
|
|
4234
|
+
attach(
|
|
4235
|
+
cards.filter((_, k) => k !== i),
|
|
4236
|
+
next3
|
|
4237
|
+
)
|
|
4238
|
+
);
|
|
4239
|
+
}
|
|
4240
|
+
})
|
|
4241
|
+
);
|
|
4242
|
+
cache.set(key2, best2);
|
|
4243
|
+
return best2;
|
|
4244
|
+
}
|
|
4245
|
+
return Math.min(...partitions(hand2).map((p) => attach(p.deadwood, onto)));
|
|
4246
|
+
}
|
|
4247
|
+
function deal2(dealer, scores2, wins, round, entropy2) {
|
|
4248
|
+
const deck = shuffled(
|
|
4249
|
+
Array.from({ length: 52 }, (_, i) => i),
|
|
4250
|
+
entropy2
|
|
4251
|
+
);
|
|
4252
|
+
return {
|
|
4253
|
+
hands: [
|
|
4254
|
+
deck.slice(0, 10).sort((a, b) => a - b),
|
|
4255
|
+
deck.slice(10, 20).sort((a, b) => a - b)
|
|
4256
|
+
],
|
|
4257
|
+
stock: deck.slice(21),
|
|
4258
|
+
discard: [deck[20]],
|
|
4259
|
+
turn: 1 - dealer,
|
|
4260
|
+
dealer,
|
|
4261
|
+
phase: "offer",
|
|
4262
|
+
offers: 0,
|
|
4263
|
+
drawnDiscard: null,
|
|
4264
|
+
scores: scores2,
|
|
4265
|
+
wins,
|
|
4266
|
+
winner: null,
|
|
4267
|
+
round,
|
|
4268
|
+
result: "",
|
|
4269
|
+
revealed: [],
|
|
4270
|
+
margin: null
|
|
4271
|
+
};
|
|
4272
|
+
}
|
|
4273
|
+
function createGin(entropy2) {
|
|
4274
|
+
return deal2(Math.floor(draws(entropy2, 1)[0] * 2), [0, 0], [0, 0], 0, entropy2);
|
|
4275
|
+
}
|
|
4276
|
+
function playGin(s, type, at, entropy2) {
|
|
4277
|
+
if (type === "next" && s.phase === "round")
|
|
4278
|
+
return {
|
|
4279
|
+
...deal2(
|
|
4280
|
+
s.result.startsWith("Stock") ? s.dealer : 1 - s.dealer,
|
|
4281
|
+
s.scores,
|
|
4282
|
+
s.wins,
|
|
4283
|
+
s.round + 1,
|
|
4284
|
+
entropy2
|
|
4285
|
+
),
|
|
4286
|
+
previous: [
|
|
4287
|
+
s.result,
|
|
4288
|
+
...s.hands.map((h, i) => {
|
|
4289
|
+
const layout = ginLayout(h);
|
|
4290
|
+
return `Player ${i + 1}: melds ${layout.melds.map((m) => m.map(cardLabel).join(" ")).join(" \xB7 ") || "none"}; deadwood ${layout.deadwood.map(cardLabel).join(" ") || "none"} (${layout.value}).`;
|
|
4291
|
+
})
|
|
4292
|
+
].join(" ")
|
|
4293
|
+
};
|
|
4294
|
+
if (["finished", "round"].includes(s.phase))
|
|
4295
|
+
throw Error("This round is finished.");
|
|
4296
|
+
const t = structuredClone(s), seat = s.turn;
|
|
4297
|
+
if (type === "pass" && s.phase === "offer") {
|
|
4298
|
+
t.offers++;
|
|
4299
|
+
if (t.offers === 2) {
|
|
4300
|
+
t.phase = "draw";
|
|
4301
|
+
t.turn = 1 - s.dealer;
|
|
4302
|
+
} else t.turn = 1 - seat;
|
|
4303
|
+
return t;
|
|
4304
|
+
}
|
|
4305
|
+
if (["draw", "offer"].includes(s.phase) && ["stock", "upcard"].includes(type)) {
|
|
4306
|
+
if (s.phase === "offer" && type === "stock")
|
|
4307
|
+
throw Error("Take the upcard or pass the opening offer.");
|
|
4308
|
+
if (s.phase === "draw" && s.offers === 2 && s.round === t.round && type === "upcard")
|
|
4309
|
+
throw Error("Both players passed: draw from the stock.");
|
|
4310
|
+
if (s.stock.length <= 2) {
|
|
4311
|
+
t.phase = "round";
|
|
4312
|
+
t.result = "Stock exhausted: round cancelled, same dealer.";
|
|
4313
|
+
t.turn = s.dealer;
|
|
4314
|
+
return t;
|
|
4315
|
+
}
|
|
4316
|
+
const card2 = type === "upcard" ? t.discard.pop() : t.stock.shift();
|
|
4317
|
+
t.drawnDiscard = type === "upcard" ? card2 : null;
|
|
4318
|
+
t.hands[seat].push(card2);
|
|
4319
|
+
t.hands[seat].sort((a, b) => a - b);
|
|
4320
|
+
t.phase = "discard";
|
|
4321
|
+
t.offers = 0;
|
|
4322
|
+
return t;
|
|
4323
|
+
}
|
|
4324
|
+
if (s.phase === "discard" && ["discard", "knock"].includes(type) && at !== void 0 && s.hands[seat].includes(at) && at !== s.drawnDiscard) {
|
|
4325
|
+
t.hands[seat] = t.hands[seat].filter((c) => c !== at);
|
|
4326
|
+
t.discard.push(at);
|
|
4327
|
+
t.drawnDiscard = null;
|
|
4328
|
+
if (type === "knock") {
|
|
4329
|
+
const layout = ginLayout(t.hands[seat]);
|
|
4330
|
+
if (layout.value > 10)
|
|
4331
|
+
throw Error("Knock only with ten or fewer deadwood points.");
|
|
4332
|
+
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;
|
|
4333
|
+
const points = layout.value === 0 ? opp + 25 : winner2 === seat ? opp - layout.value : layout.value - opp + 25;
|
|
4334
|
+
t.scores[winner2] += points;
|
|
4335
|
+
t.wins[winner2]++;
|
|
4336
|
+
t.result = `${layout.value === 0 ? "Gin" : winner2 === seat ? "Knock" : "Undercut"}: Player ${winner2 + 1} scores ${points}. Deadwood ${layout.value} vs ${opp}.`;
|
|
4337
|
+
t.revealed = t.hands.map((h) => ginLayout(h).melds);
|
|
4338
|
+
t.phase = t.scores[winner2] >= 100 ? "finished" : "round";
|
|
4339
|
+
t.turn = winner2;
|
|
4340
|
+
if (t.phase === "finished") {
|
|
4341
|
+
t.winner = winner2;
|
|
4342
|
+
t.margin = t.scores[winner2] + 100 + (t.scores[other] === 0 ? 100 : 0) + 25 * t.wins[winner2] - t.scores[other] - 25 * t.wins[other];
|
|
4343
|
+
t.result += ` Final margin including game and hand bonuses: ${t.margin}.`;
|
|
4344
|
+
}
|
|
4345
|
+
} else {
|
|
4346
|
+
t.phase = "draw";
|
|
4347
|
+
t.turn = 1 - seat;
|
|
4348
|
+
}
|
|
4349
|
+
return t;
|
|
4350
|
+
}
|
|
4351
|
+
throw Error("Choose a legal draw, discard or knock.");
|
|
4352
|
+
}
|
|
4353
|
+
function ginView(s, seat) {
|
|
4354
|
+
const { stock, ...rest } = s;
|
|
4355
|
+
return {
|
|
4356
|
+
...rest,
|
|
4357
|
+
stockCount: stock.length,
|
|
4358
|
+
hands: s.hands.map(
|
|
4359
|
+
(h, i) => i === seat || ["round", "finished"].includes(s.phase) ? h : h.map(() => null)
|
|
4360
|
+
)
|
|
4361
|
+
};
|
|
4362
|
+
}
|
|
4363
|
+
|
|
4364
|
+
// lib/multiplayer/oware.ts
|
|
4365
|
+
var side = (at) => at < 6 ? 0 : 1;
|
|
4366
|
+
function sow(pits, at) {
|
|
4367
|
+
const board = [...pits];
|
|
4368
|
+
let seeds = board[at], last = at;
|
|
4369
|
+
board[at] = 0;
|
|
4370
|
+
while (seeds > 0) {
|
|
4371
|
+
last = (last + 1) % 12;
|
|
4372
|
+
if (last !== at) {
|
|
4373
|
+
board[last]++;
|
|
4374
|
+
seeds--;
|
|
4375
|
+
}
|
|
4376
|
+
}
|
|
4377
|
+
return { board, last };
|
|
4378
|
+
}
|
|
4379
|
+
function owareMoves(s) {
|
|
4380
|
+
if (s.finished) return [];
|
|
4381
|
+
const opponentEmpty = s.pits.every((n, i) => side(i) === s.turn || n === 0);
|
|
4382
|
+
return s.pits.flatMap(
|
|
4383
|
+
(n, at) => n > 0 && side(at) === s.turn && (!opponentEmpty || sow(s.pits, at).board.some((n2, i) => side(i) !== s.turn && n2 > 0)) ? [at] : []
|
|
4384
|
+
);
|
|
4385
|
+
}
|
|
4386
|
+
|
|
4387
|
+
// lib/multiplayer/catalog.ts
|
|
4388
|
+
var multiplayerGames = [
|
|
4389
|
+
{ "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." },
|
|
4390
|
+
{ "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." },
|
|
4391
|
+
{ "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." },
|
|
4392
|
+
{ 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." },
|
|
4393
|
+
{ 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." },
|
|
4394
|
+
{ 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." },
|
|
4395
|
+
{ "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." },
|
|
4396
|
+
{ "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." },
|
|
4397
|
+
{ "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." },
|
|
4398
|
+
{ "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." },
|
|
4399
|
+
{ 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." },
|
|
4400
|
+
{ 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." },
|
|
4401
|
+
{ 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." },
|
|
4402
|
+
{ 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." },
|
|
4403
|
+
{ 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." },
|
|
4404
|
+
{ 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." },
|
|
4405
|
+
{ 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." },
|
|
4406
|
+
{
|
|
4407
|
+
id: "thousand",
|
|
4408
|
+
name: "Thousand",
|
|
4409
|
+
symbol: "1000",
|
|
4410
|
+
players: "2\u20134 players",
|
|
4411
|
+
description: "Bid your hand. Marry a new trump.",
|
|
4412
|
+
rules: "A 24-card deck, nine through ace; strength is nine, jack, queen, king, ten, ace. Three active players receive seven cards and a three-card kitty. With four seats the dealer sits out and scores revealed kitty aces (50 each) and marriages. Two players receive ten cards each and choose one of two two-card kitties. The first active player opens at 100; bid in tens, no higher than 120 plus held marriages. Winner takes a kitty, gives each other active player a card (or discards two with two seats), then declares at least the auction bid. Follow suit and beat the winner when possible; if void, trump when held. Leading a held queen-king pair with the marriage declaration earns clubs 60, diamonds 80, hearts 100 or spades 40 and changes trump. Card points: ace 11, ten 10, king 4, queen 3, jack 2. Cards left from the kitties, including the declarer\u2019s two discards in a two-player game, score for the last trick winner. Declarer makes or loses their contract; others round their points to tens. At 800, only declarer earns further points. First to 1000 wins, with declarer winning a simultaneous finish. The first bomb cancels freely; subsequent bombs award opponents 60 (40 with four seats). Four nines can request a redeal."
|
|
4413
|
+
},
|
|
4414
|
+
{
|
|
4415
|
+
id: "binokel",
|
|
4416
|
+
name: "Binokel",
|
|
4417
|
+
symbol: "AA",
|
|
4418
|
+
players: "4 players",
|
|
4419
|
+
description: "Four copies. Two partners. One contract.",
|
|
4420
|
+
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."
|
|
4421
|
+
},
|
|
4422
|
+
{
|
|
4423
|
+
id: "skat",
|
|
4424
|
+
name: "Skat",
|
|
4425
|
+
symbol: "J",
|
|
4426
|
+
players: "3 players",
|
|
4427
|
+
description: "Win the bidding duel. Play alone against two.",
|
|
4428
|
+
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."
|
|
4429
|
+
},
|
|
4430
|
+
{
|
|
4431
|
+
id: "baska",
|
|
4432
|
+
name: "Baska",
|
|
4433
|
+
symbol: "DQ",
|
|
4434
|
+
players: "4 players",
|
|
4435
|
+
description: "Find your hidden partner in sixteen cards.",
|
|
4436
|
+
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."
|
|
4437
|
+
},
|
|
4438
|
+
{
|
|
4439
|
+
id: "canasta",
|
|
4440
|
+
name: "Canasta",
|
|
4441
|
+
symbol: "77",
|
|
4442
|
+
players: "2 or 4 players",
|
|
4443
|
+
description: "Build seven-card melds. Break a frozen pile.",
|
|
4444
|
+
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."
|
|
4445
|
+
},
|
|
4446
|
+
{
|
|
4447
|
+
id: "bridge",
|
|
4448
|
+
name: "Bridge",
|
|
4449
|
+
symbol: "NT",
|
|
4450
|
+
players: "4 players",
|
|
4451
|
+
description: "Bid a contract. Plan two hands together.",
|
|
4452
|
+
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."
|
|
4453
|
+
},
|
|
4454
|
+
{
|
|
4455
|
+
id: "polish-hearts",
|
|
4456
|
+
name: "Polish Hearts",
|
|
4457
|
+
symbol: "\u2665",
|
|
4458
|
+
players: "3\u20134 players",
|
|
4459
|
+
description: "Avoid seven penalties, then win points back.",
|
|
4460
|
+
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."
|
|
4461
|
+
},
|
|
4462
|
+
{
|
|
4463
|
+
id: "durak",
|
|
4464
|
+
name: "Durak",
|
|
4465
|
+
symbol: "\u2660",
|
|
4466
|
+
players: "2\u20134 players",
|
|
4467
|
+
description: "Beat the attack. Empty your hand.",
|
|
4468
|
+
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."
|
|
4469
|
+
},
|
|
4470
|
+
{
|
|
4471
|
+
id: "makao",
|
|
4472
|
+
name: "Makao",
|
|
4473
|
+
symbol: "Q",
|
|
4474
|
+
players: "2\u20134 players",
|
|
4475
|
+
description: "Match suits and ranks. Counter the demand.",
|
|
4476
|
+
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."
|
|
4477
|
+
},
|
|
4478
|
+
{
|
|
4479
|
+
id: "three-five-eight",
|
|
4480
|
+
name: "3-5-8",
|
|
4481
|
+
symbol: "358",
|
|
4482
|
+
players: "3 players",
|
|
4483
|
+
description: "Meet your quota. Exchange for stronger cards.",
|
|
4484
|
+
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."
|
|
4485
|
+
},
|
|
4486
|
+
{
|
|
4487
|
+
id: "property-trading",
|
|
4488
|
+
name: "Property Trading",
|
|
4489
|
+
symbol: "\u2302",
|
|
4490
|
+
players: "2\u20134 players",
|
|
4491
|
+
description: "Build a garden estate. Bargain for a complete street.",
|
|
4492
|
+
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."
|
|
4493
|
+
},
|
|
4494
|
+
{
|
|
4495
|
+
id: "draw-guess",
|
|
4496
|
+
name: "Draw & Guess",
|
|
4497
|
+
symbol: "\u270E",
|
|
4498
|
+
players: "2\u201320 players",
|
|
4499
|
+
description: "A private word. A shared sketch.",
|
|
4500
|
+
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."
|
|
4501
|
+
},
|
|
4502
|
+
{
|
|
4503
|
+
id: "letter-tiles",
|
|
4504
|
+
name: "Letter Tiles",
|
|
4505
|
+
symbol: "A",
|
|
4506
|
+
players: "2\u20134 players",
|
|
4507
|
+
description: "Build a crossword. Make every letter count.",
|
|
4508
|
+
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."
|
|
4509
|
+
},
|
|
4510
|
+
{
|
|
4511
|
+
id: "pan",
|
|
4512
|
+
name: "Pan",
|
|
4513
|
+
symbol: "\u2667",
|
|
4514
|
+
players: "2\u20134 players",
|
|
4515
|
+
description: "Shed your cards before the pile turns.",
|
|
4516
|
+
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."
|
|
4517
|
+
},
|
|
4518
|
+
{
|
|
4519
|
+
id: "gin-rummy",
|
|
4520
|
+
name: "Gin Rummy",
|
|
4521
|
+
symbol: "\u2666",
|
|
4522
|
+
players: "2 players",
|
|
4523
|
+
description: "Build runs and sets. Know when to knock.",
|
|
4524
|
+
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."
|
|
4525
|
+
},
|
|
4526
|
+
{
|
|
4527
|
+
id: "sixty-six",
|
|
3939
4528
|
name: "Sixty-Six",
|
|
3940
4529
|
symbol: "\u2654",
|
|
3941
4530
|
players: "2 players",
|
|
3942
4531
|
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,
|
|
4532
|
+
rules: "Six cards each from a 24-card deck. Rank order: nine, jack, queen, king, ten, ace; values 0, 2, 3, 4, 10, 11. While stock is open, play freely; trick winner draws first, and the trump upcard is the last card drawn. Exchange trump nine after winning a trick. Before leading, you may close the stock; then follow suit, overtake if possible, otherwise trump. A queen\u2013king marriage declared while leading with open stock earns 20, or 40 in trump, once you have won a trick (including the one it leads). Claim 66 before leading, or immediately with a marriage. False claims or failed closure award the opponent 2/3 match points. Otherwise round wins earn 1\u20133 based on opponent points. An unclosed last trick adds ten. First to seven match points wins."
|
|
3944
4533
|
},
|
|
3945
4534
|
{
|
|
3946
4535
|
id: "spades",
|
|
@@ -4052,7 +4641,7 @@ var multiplayerGames = [
|
|
|
4052
4641
|
symbol: "\u25CC",
|
|
4053
4642
|
players: "2 players",
|
|
4054
4643
|
description: "Sow seeds. Capture twos and threes.",
|
|
4055
|
-
rules: "Each of twelve pits starts with four seeds. Sow counterclockwise, skipping the starting pit on a full lap. Ending on an opposing pit with two or three seeds captures it and preceding opposing pits with two or three. Captures that empty the entire opposing row are cancelled. Feed an empty opponent if possible. More than 24 captured seeds wins. No legal feeding move or three repetitions ends play; remaining seeds go to their owners."
|
|
4644
|
+
rules: "Each of twelve pits starts with four seeds. Sow counterclockwise, skipping the starting pit on a full lap. Ending on an opposing pit with two or three seeds captures it and preceding opposing pits with two or three. Captures that would empty the entire opposing row (a grand slam) are cancelled. Feed an empty opponent if possible. More than 24 captured seeds wins. No legal feeding move or three repetitions ends play; remaining seeds go to their owners."
|
|
4056
4645
|
},
|
|
4057
4646
|
{
|
|
4058
4647
|
id: "mills",
|
|
@@ -4413,421 +5002,168 @@ var action_catalog_default = {
|
|
|
4413
5002
|
"pass",
|
|
4414
5003
|
"choose"
|
|
4415
5004
|
]
|
|
4416
|
-
};
|
|
4417
|
-
|
|
4418
|
-
// lib/multiplayer/dice.ts
|
|
4419
|
-
var diceCategories = [
|
|
4420
|
-
"Ones",
|
|
4421
|
-
"Twos",
|
|
4422
|
-
"Threes",
|
|
4423
|
-
"Fours",
|
|
4424
|
-
"Fives",
|
|
4425
|
-
"Sixes",
|
|
4426
|
-
"Three of a kind",
|
|
4427
|
-
"Four of a kind",
|
|
4428
|
-
"Full house",
|
|
4429
|
-
"Small straight",
|
|
4430
|
-
"Large straight",
|
|
4431
|
-
"Five of a kind",
|
|
4432
|
-
"Chance"
|
|
4433
|
-
];
|
|
4434
|
-
function createDice(count) {
|
|
4435
|
-
if (!index(count - 2, 3)) throw Error("Choose two to four players.");
|
|
4436
|
-
return {
|
|
4437
|
-
dice: [0, 0, 0, 0, 0],
|
|
4438
|
-
held: Array(5).fill(false),
|
|
4439
|
-
rolls: 0,
|
|
4440
|
-
sheets: Array.from({ length: count }, () => Array(13).fill(null)),
|
|
4441
|
-
bonuses: Array(count).fill(0),
|
|
4442
|
-
turn: 0,
|
|
4443
|
-
finished: false,
|
|
4444
|
-
winner: null
|
|
4445
|
-
};
|
|
4446
|
-
}
|
|
4447
|
-
function diceScore(dice, category, joker = false) {
|
|
4448
|
-
const counts = Array.from(
|
|
4449
|
-
{ length: 6 },
|
|
4450
|
-
(_, i) => dice.filter((v) => v === i + 1).length
|
|
4451
|
-
), sum = dice.reduce((a, b) => a + b, 0);
|
|
4452
|
-
if (category < 6) return counts[category] * (category + 1);
|
|
4453
|
-
switch (category) {
|
|
4454
|
-
case 6:
|
|
4455
|
-
return Math.max(...counts) >= 3 ? sum : 0;
|
|
4456
|
-
case 7:
|
|
4457
|
-
return Math.max(...counts) >= 4 ? sum : 0;
|
|
4458
|
-
case 8:
|
|
4459
|
-
return joker || counts.includes(3) && counts.includes(2) ? 25 : 0;
|
|
4460
|
-
case 9:
|
|
4461
|
-
return joker || [0, 1, 2].some((start) => counts.slice(start, start + 4).every(Boolean)) ? 30 : 0;
|
|
4462
|
-
case 10:
|
|
4463
|
-
return joker || [0, 1].some((start) => counts.slice(start, start + 5).every(Boolean)) ? 40 : 0;
|
|
4464
|
-
case 11:
|
|
4465
|
-
return counts.includes(5) ? 50 : 0;
|
|
4466
|
-
case 12:
|
|
4467
|
-
return sum;
|
|
4468
|
-
default:
|
|
4469
|
-
throw Error("Choose a score category.");
|
|
4470
|
-
}
|
|
4471
|
-
}
|
|
4472
|
-
function diceTotal(s, seat) {
|
|
4473
|
-
const sheet = s.sheets[seat], upper = sheet.slice(0, 6).reduce((sum, n) => sum + (n ?? 0), 0);
|
|
4474
|
-
return sheet.reduce((sum, n) => sum + (n ?? 0), 0) + (upper >= 63 ? 35 : 0) + s.bonuses[seat];
|
|
4475
|
-
}
|
|
4476
|
-
function playDice(s, type, at, entropy2) {
|
|
4477
|
-
if (s.finished) throw Error("This match is finished.");
|
|
4478
|
-
if (type === "roll") {
|
|
4479
|
-
if (s.rolls >= 3) throw Error("Record a score after three rolls.");
|
|
4480
|
-
const random = draws(entropy2, 5);
|
|
4481
|
-
return {
|
|
4482
|
-
...s,
|
|
4483
|
-
rolls: s.rolls + 1,
|
|
4484
|
-
dice: s.dice.map(
|
|
4485
|
-
(v, i) => s.rolls && s.held[i] ? v : 1 + Math.floor(random[i] * 6)
|
|
4486
|
-
)
|
|
4487
|
-
};
|
|
4488
|
-
}
|
|
4489
|
-
if (at === void 0 || !index(at, type === "hold" ? 5 : 13) || !s.rolls)
|
|
4490
|
-
throw Error("Roll first, then choose a die or score category.");
|
|
4491
|
-
if (type === "hold")
|
|
4492
|
-
return { ...s, held: s.held.map((v, i) => i === at ? !v : v) };
|
|
4493
|
-
if (type !== "score" || s.sheets[s.turn][at] !== null)
|
|
4494
|
-
throw Error("Choose an unused score category.");
|
|
4495
|
-
const sheets = s.sheets.map((row) => [...row]), bonuses = [...s.bonuses];
|
|
4496
|
-
const five = s.dice.every((v) => v === s.dice[0]), upper = s.dice[0] - 1;
|
|
4497
|
-
const joker = five && sheets[s.turn][11] !== null && sheets[s.turn][upper] !== null;
|
|
4498
|
-
if (joker && sheets[s.turn].slice(6).some((n) => n === null) && at < 6)
|
|
4499
|
-
throw Error(
|
|
4500
|
-
"Use the five-of-a-kind joker in an unused lower category first."
|
|
4501
|
-
);
|
|
4502
|
-
sheets[s.turn][at] = diceScore(s.dice, at, joker);
|
|
4503
|
-
if (joker && sheets[s.turn][11] === 50) bonuses[s.turn] += 100;
|
|
4504
|
-
const next3 = {
|
|
4505
|
-
...s,
|
|
4506
|
-
sheets,
|
|
4507
|
-
bonuses,
|
|
4508
|
-
turn: (s.turn + 1) % s.sheets.length,
|
|
4509
|
-
rolls: 0,
|
|
4510
|
-
held: Array(5).fill(false),
|
|
4511
|
-
dice: [0, 0, 0, 0, 0]
|
|
4512
|
-
};
|
|
4513
|
-
next3.finished = sheets.every((row) => row.every((n) => n !== null));
|
|
4514
|
-
if (next3.finished)
|
|
4515
|
-
next3.winner = best(sheets.map((_, i) => diceTotal(next3, i)));
|
|
4516
|
-
return next3;
|
|
4517
|
-
}
|
|
4518
|
-
|
|
4519
|
-
// lib/agents/action-help.ts
|
|
4520
|
-
var help = {
|
|
4521
|
-
amazons: "move: numeric from/to 0\u201399 and amount for the arrow square. Select an exact legal_wire_actions entry.",
|
|
4522
|
-
"lines-of-action": "move: numeric from/to 0\u201363. Move the count of all stones along the full line; enemy stones block jumping.",
|
|
4523
|
-
"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.",
|
|
4524
|
-
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.",
|
|
4525
|
-
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.",
|
|
4526
|
-
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.",
|
|
4527
|
-
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.`,
|
|
4528
|
-
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.",
|
|
4529
|
-
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.",
|
|
4530
|
-
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.",
|
|
4531
|
-
"connect-four": "place: at is a zero-based column (0\u20136). Convenience: --column 1\u20137.",
|
|
4532
|
-
"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.",
|
|
4533
|
-
chess: "move: from/to are algebraic squares; promotion is q/r/b/n.",
|
|
4534
|
-
"atomic-chess": "move: from/to are algebraic squares; promotion is q/r/b/n. Explosions follow the game rules.",
|
|
4535
|
-
"three-check": "move: from/to are algebraic squares; promotion is q/r/b/n. Check counts are in visible_state.",
|
|
4536
|
-
checkers: "move: from/to are zero-based row-major squares (0\u201363); continue a mandatory capture chain. Convenience flags use algebraic squares.",
|
|
4537
|
-
"brazilian-checkers": "move: from/to are zero-based squares (0\u201363); maximum captures apply.",
|
|
4538
|
-
"international-draughts": "move: from/to are zero-based squares (0\u201399). sequence: cards is a complete legal path of square indexes, including the origin.",
|
|
4539
|
-
reversi: "place: at is a zero-based row-major square (0\u201363). Unavailable turns pass automatically.",
|
|
4540
|
-
gomoku: "place: at is a zero-based row-major intersection (0\u2013224). Exactly five wins.",
|
|
4541
|
-
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.",
|
|
4542
|
-
"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.",
|
|
4543
|
-
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.",
|
|
4544
|
-
go: "place/dead: at is a zero-based row-major intersection (board_labeled shows row start indexes). pass/resume: no payload. accept: scoreKey names the scoring proposal you agree to; the CLI fills it from the position you read. Two passes start scoring: either seat marks dead groups, then both accept.",
|
|
4545
|
-
battleships: "auto/clear/ready: no payload. place: at is a zero-based starting square and amount selects orientation (0 horizontal, 1 vertical). fire: at is a zero-based target square. Inspect your fleet/setup phase; enemy ships remain hidden.",
|
|
4546
|
-
mills: "place/capture: at is a zero-based point index. move: from is the origin and at the destination. Completing a mill requires a subsequent capture action.",
|
|
4547
|
-
dice: `roll: no payload. hold: at is a die index (0\u20134) to toggle its hold after rolling. score: at is a category index: ${diceCategories.map((name, i) => `${i} ${name}`).join(", ")}. Inspect the roll phase and score sheet.`,
|
|
4548
|
-
ludo: "roll: no payload. move: at is the index (0\u20133) of your token. Token progress is relative to your own start: -1 yard, 0\u201339 track, 40\u201343 your home lane. Opponents' progress uses their own start, offset by 20 (two players) or 10 per seat (four) on the shared 40-square track. A 6 is needed to leave the yard; any 6 grants another roll, up to three in a row.",
|
|
4549
|
-
domino: "tile: at is your zero-based hand position (not a pip value) and amount selects the chain end: 0 left, 1 right. draw: draw until you can play, or pass when the stock is empty. next: start the following round. Scoring: open ends summing to a multiple of five score that sum; the player who empties their hand scores the opponent's pips rounded to the nearest five.",
|
|
4550
|
-
football: "place: at is a zero-based destination point. The state records ball, used edges and continuing rebounds.",
|
|
4551
|
-
backgammon: "roll: no payload. move: numeric from/to and amount (die value), one die per action. Points are 0\u201323. Positive point counts are seat 0, negative seat 1. Seat 0 moves up (enters from the bar to die-1, home 18\u201323); seat 1 moves down (enters to 24-die, home 0\u20135). from -1 means the bar; to 24 bears off for both seats. Compulsory dice usage is enforced.",
|
|
4552
|
-
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.",
|
|
4553
|
-
xiangqi: "move: numeric zero-based row-major from/to on the 9\xD710 board. promotion is unused.",
|
|
4554
|
-
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.",
|
|
4555
|
-
durak: "play: at is your card ID from the visible hand; take/pass: no payload. Follow attack/defence roles and visible trump/table.",
|
|
4556
|
-
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.",
|
|
4557
|
-
pan: "play: cards is an array of card IDs from the visible hand; take/next: no payload. Follow the table rank and legal group rules.",
|
|
4558
|
-
"gin-rummy": "stock/upcard/pass/next: no payload. discard/knock: at is a card ID from the visible hand. Inspect draw/discard phase and deadwood rules.",
|
|
4559
|
-
"sixty-six": "play: at is a card ID from the visible hand; promotion:'marriage' or 'marriage-claim' declares. exchange/close/claim/next are phase-specific commands.",
|
|
4560
|
-
hearts: "select: at is a card ID from the visible hand for passing (selecting a chosen card again unselects it; visible_state.selected shows your choice), then pass. play: at is a card ID from the visible hand. moon: amount selects a scoring choice. Follow phase, lead suit and legal cards.",
|
|
4561
|
-
spades: "bid: amount is the bid; blind/look for blind nil where offered. play: at is a card ID from the visible hand. Follow partner, phase and lead suit.",
|
|
4562
|
-
"oh-hell": "bid: amount is the bid. play: at is a card ID from the visible hand. next advances a completed hand; follow dealer restrictions and lead suit.",
|
|
4563
|
-
"three-five-eight": "exchange: at is one card ID from your visible hand. discard: cards is exactly four distinct card IDs from your visible hand. play: at is one card ID from your visible hand. contract: amount 0\u20133 chooses a suit, 4 no trumps, 5 misere. next advances the hand.",
|
|
4564
|
-
bridge: "bid: amount 1\u201335 encodes level floor((amount-1)/5)+1 and strain (amount-1)%5; pass/double/redouble have no payload. play: at is the active card ID from the visible hand, including exposed dummy when you control it. Follow auction, strain and lead-suit rules.",
|
|
4565
|
-
"polish-hearts": "trump: amount is a suit index; pass: no payload, only when no layout card fits; play: at is a card ID from the visible hand. Follow the current penalty/trump/layout phase.",
|
|
4566
|
-
thousand: "bid/declare: amount is points. kitty: at selects a kitty. exchange: cards is card IDs from the visible hand. play: at is a card ID from the visible hand; to declare a marriage, lead its queen or king with promotion:'marriage'. pass/bomb/redeal/next depend on phase. Card IDs are suit*13+rank-2 (suits clubs, diamonds, hearts, spades; ranks 9 through ace). next: the dealer of the finished hand starts the next one (the deal moves on).",
|
|
4567
|
-
binokel: "bid: amount is points; trump: amount is a suit; play: at is a card ID from the visible hand. pass/next have no payload. Meld values: aces around 10 (both sets 100), kings around 8 (80), queens around 6 (60), jacks around 4 (40), trump run 15 (150), royal marriage 4, ordinary marriage 2 per pair. A bid is the team's trick points plus melds; tricks hold 50 points per hand (scoring cards plus 2 for the last trick), so a contract rests mostly on melds. The declaring team needs 20 meld points.",
|
|
4568
|
-
skat: "bid: amount is an auction value; hold/pass/hand/pickup act in auction/pickup phases (after a hold the bidder must bid higher or pass). discard: cards is exactly two card IDs from your visible hand. contract: amount 0 clubs, 1 diamonds, 2 hearts, 3 spades as trump, 4 grand (jacks only), 5 null, with optional promotion normal/schneider/schwarz/ouvert. play: at is a card ID from the visible hand. Jacks rank clubs, spades, hearts, diamonds. Card IDs are suit*13+rank-2 (suits clubs, diamonds, hearts, spades). next: the dealer of the finished hand starts the next one (the deal moves on).",
|
|
4569
|
-
baska: "announce: no payload in the honor phase. contract: amount is one of 1,3,4,5,6 and must exceed the current contract. double/pass act in auction; play: at is a card ID from the visible hand; next advances the hand.",
|
|
4570
|
-
canasta: "stock/next: no payload. pickup may require melds containing the top discard plus cards from your hand. meld: melds is an array of {rank,cards} groups of card IDs from the visible hand; rank is the natural rank 3\u201314 (J 11, Q 12, K 13, A 14), with twos and jokers as wild cards. cards_labeled names each card. discard: at is a card ID from the visible hand. Follow opening meld and canasta requirements. Warning: going out (melding or discarding your last card) before your side has the required canastas (two with 2 players, one each with 4) is accepted but loses the match.",
|
|
4571
|
-
"letter-tiles": "place: placements is an array of {at,rack,letter?}; at is a board index and rack a rack index, letter assigns a blank. exchange: cards is rack indexes. accept/challenge/pass/next depend on phase.",
|
|
4572
|
-
"draw-guess": "choose: at selects one of your offered prompts. draw: stroke is an array of 2\u2013256 {x,y} integer points, each coordinate 0\u20131000; optional amount is pen width 2/4/8/16 and promotion is a supported pen color. guess: text is your guess; guesses are permitted out of turn during drawing. clear/expire/next depend on phase. Hidden prompts stay hidden from guessers.",
|
|
4573
|
-
"property-trading": "roll/end/buy/auction/pay/bankrupt/pass/reject have phase-specific effects. bid: amount is the bid. mortgage/redeem/build/sell: at selects a property. tax: amount is 0 or 1. release: amount 0 pays fifty, 1 uses a release card. offer: tradeOffer contains seat,give,take,giveCash,takeCash; accept confirms the current trade."
|
|
4574
|
-
};
|
|
4575
|
-
function actionHelp(game) {
|
|
4576
|
-
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." };
|
|
4577
|
-
}
|
|
5005
|
+
};
|
|
4578
5006
|
|
|
4579
|
-
// lib/multiplayer/
|
|
4580
|
-
var
|
|
4581
|
-
|
|
4582
|
-
|
|
4583
|
-
|
|
4584
|
-
|
|
4585
|
-
|
|
4586
|
-
|
|
4587
|
-
|
|
4588
|
-
|
|
4589
|
-
|
|
4590
|
-
|
|
4591
|
-
|
|
4592
|
-
|
|
4593
|
-
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
4598
|
-
|
|
4599
|
-
|
|
4600
|
-
|
|
4601
|
-
|
|
4602
|
-
|
|
4603
|
-
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
4628
|
-
|
|
4629
|
-
|
|
4630
|
-
|
|
4631
|
-
stock: [],
|
|
4632
|
-
trick: [],
|
|
4633
|
-
lastTrick: [],
|
|
4634
|
-
tricks: [],
|
|
4635
|
-
penalties: [],
|
|
4636
|
-
bids: [],
|
|
4637
|
-
blind: [],
|
|
4638
|
-
blindAsked: [],
|
|
4639
|
-
selected: [],
|
|
4640
|
-
passed: [],
|
|
4641
|
-
scores: Array(variant === "spades" ? 2 : count).fill(0),
|
|
4642
|
-
bags: [0, 0],
|
|
4643
|
-
trump: null,
|
|
4644
|
-
broken: false,
|
|
4645
|
-
winner: null,
|
|
4646
|
-
result: ""
|
|
4647
|
-
},
|
|
4648
|
-
entropy2
|
|
4649
|
-
);
|
|
4650
|
-
}
|
|
4651
|
-
function trickLegalCards(s) {
|
|
4652
|
-
if (s.phase !== "play") return [];
|
|
4653
|
-
const hand2 = s.hands[s.turn].filter((c) => c !== null), lead = s.trick[0];
|
|
4654
|
-
if (lead) {
|
|
4655
|
-
const suited = hand2.filter((c) => cardSuit(c) === cardSuit(lead.card));
|
|
4656
|
-
if (suited.length) return suited;
|
|
4657
|
-
if (s.variant === "hearts" && s.tricks.every((n) => n === 0)) {
|
|
4658
|
-
const clean = hand2.filter((c) => !penalty(c));
|
|
4659
|
-
if (clean.length) return clean;
|
|
4660
|
-
}
|
|
4661
|
-
return hand2;
|
|
4662
|
-
}
|
|
4663
|
-
if (s.variant === "hearts") {
|
|
4664
|
-
if (s.tricks.every((n) => n === 0)) return hand2.includes(0) ? [0] : [];
|
|
4665
|
-
if (!s.broken) {
|
|
4666
|
-
const clean = hand2.filter((c) => cardSuit(c) !== 2);
|
|
4667
|
-
if (clean.length) return clean;
|
|
4668
|
-
}
|
|
4669
|
-
}
|
|
4670
|
-
if (s.variant === "spades" && !s.broken) {
|
|
4671
|
-
const clean = hand2.filter((c) => cardSuit(c) !== 3);
|
|
4672
|
-
if (clean.length) return clean;
|
|
4673
|
-
}
|
|
4674
|
-
return hand2;
|
|
4675
|
-
}
|
|
4676
|
-
function finish(s) {
|
|
4677
|
-
if (s.variant === "hearts") {
|
|
4678
|
-
s.scores = s.scores.map((n, i) => n + s.penalties[i]);
|
|
4679
|
-
s.result = `Round penalties: ${s.penalties.join(" \xB7 ")}`;
|
|
4680
|
-
if (s.scores.some((n) => n >= 100)) {
|
|
4681
|
-
s.phase = "finished";
|
|
4682
|
-
s.winner = best(s.scores.map((n) => -n));
|
|
4683
|
-
} else s.phase = "round";
|
|
4684
|
-
} else if (s.variant === "spades") {
|
|
4685
|
-
const gains = [0, 0];
|
|
4686
|
-
for (const team2 of [0, 1]) {
|
|
4687
|
-
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);
|
|
4688
|
-
gains[team2] = taken >= bid ? bid * 10 + Math.max(0, taken - bid) : -bid * 10;
|
|
4689
|
-
const bags = Math.max(0, taken - bid) + seats2.filter((i) => s.bids[i] === 0).reduce((n, i) => n + s.tricks[i], 0);
|
|
4690
|
-
s.bags[team2] += bags;
|
|
4691
|
-
gains[team2] -= 100 * Math.floor(s.bags[team2] / 10);
|
|
4692
|
-
s.bags[team2] %= 10;
|
|
4693
|
-
for (const i of seats2)
|
|
4694
|
-
if (s.bids[i] === 0)
|
|
4695
|
-
gains[team2] += (s.tricks[i] === 0 ? 1 : -1) * (s.blind[i] ? 200 : 100);
|
|
4696
|
-
}
|
|
4697
|
-
s.scores = s.scores.map((n, i) => n + gains[i]);
|
|
4698
|
-
s.result = `Team 1 & 3: ${gains[0]} \xB7 Team 2 & 4: ${gains[1]}`;
|
|
4699
|
-
if (s.scores.some((n) => n >= 500 || n <= -200)) {
|
|
4700
|
-
s.phase = "finished";
|
|
4701
|
-
s.winner = best(s.scores);
|
|
4702
|
-
} else s.phase = "round";
|
|
4703
|
-
} else {
|
|
4704
|
-
s.scores = s.scores.map(
|
|
4705
|
-
(n, i) => n + (s.bids[i] === s.tricks[i] ? 10 + s.tricks[i] : 0)
|
|
4706
|
-
);
|
|
4707
|
-
s.result = `Tricks taken: ${s.tricks.join(" \xB7 ")}`;
|
|
4708
|
-
const rounds = (s.hands.length === 4 ? 13 : 17) + s.hands.length - 1;
|
|
4709
|
-
if (s.round + 1 >= rounds) {
|
|
4710
|
-
s.phase = "finished";
|
|
4711
|
-
s.winner = best(s.scores);
|
|
4712
|
-
} else s.phase = "round";
|
|
4713
|
-
}
|
|
4714
|
-
s.turn = first(s);
|
|
4715
|
-
}
|
|
4716
|
-
function playTrickCards(s, type, at, amount, entropy2) {
|
|
4717
|
-
if (s.phase === "finished") throw Error("This match is finished.");
|
|
4718
|
-
if (type === "next" && s.phase === "round")
|
|
4719
|
-
return deal(
|
|
4720
|
-
{ ...s, round: s.round + 1, dealer: (s.dealer + 1) % s.hands.length },
|
|
4721
|
-
entropy2
|
|
4722
|
-
);
|
|
4723
|
-
const t = structuredClone(s), seat = s.turn, count = s.hands.length;
|
|
4724
|
-
if (s.phase === "pass") {
|
|
4725
|
-
if (type === "select" && at !== void 0 && s.hands[seat].includes(at)) {
|
|
4726
|
-
const selected = t.selected[seat];
|
|
4727
|
-
if (selected.includes(at))
|
|
4728
|
-
t.selected[seat] = selected.filter((c) => c !== at);
|
|
4729
|
-
else if (selected.length < 3) selected.push(at);
|
|
4730
|
-
else throw Error("Select three cards.");
|
|
4731
|
-
return t;
|
|
4732
|
-
}
|
|
4733
|
-
if (type !== "pass" || t.selected[seat].length !== 3)
|
|
4734
|
-
throw Error("Choose three cards to pass.");
|
|
4735
|
-
t.passed[seat] = true;
|
|
4736
|
-
if (t.passed.every(Boolean)) {
|
|
4737
|
-
const offset = [1, 3, 2][s.round % 4];
|
|
4738
|
-
t.hands = t.hands.map(
|
|
4739
|
-
(h, i) => h.filter((c) => !t.selected[i].includes(c))
|
|
4740
|
-
);
|
|
4741
|
-
t.selected.forEach(
|
|
4742
|
-
(cards, i) => t.hands[(i + offset) % 4].push(...cards)
|
|
4743
|
-
);
|
|
4744
|
-
t.hands.forEach((h) => h.sort((a, b) => a - b));
|
|
4745
|
-
t.selected = t.selected.map(() => []);
|
|
4746
|
-
t.phase = "play";
|
|
4747
|
-
t.turn = t.hands.findIndex((h) => h.includes(0));
|
|
4748
|
-
} else t.turn = t.passed.findIndex((n) => !n);
|
|
4749
|
-
return t;
|
|
4750
|
-
}
|
|
4751
|
-
if (s.phase === "blind") {
|
|
4752
|
-
const eligible = s.scores[1 - seat % 2] - s.scores[seat % 2] >= 200;
|
|
4753
|
-
if (type === "blind" && eligible) {
|
|
4754
|
-
t.bids[seat] = 0;
|
|
4755
|
-
t.blind[seat] = true;
|
|
4756
|
-
} else if (type !== "look")
|
|
4757
|
-
throw Error("Choose blind nil or look at your cards.");
|
|
4758
|
-
t.blindAsked[seat] = true;
|
|
4759
|
-
if (t.blindAsked.every(Boolean)) {
|
|
4760
|
-
t.phase = "bid";
|
|
4761
|
-
t.turn = first(t);
|
|
4762
|
-
while (t.bids[t.turn] !== null) t.turn = (t.turn + 1) % count;
|
|
4763
|
-
} else t.turn = (seat + 1) % count;
|
|
4764
|
-
return t;
|
|
4765
|
-
}
|
|
4766
|
-
if (s.phase === "bid") {
|
|
4767
|
-
if (type !== "bid" || amount === void 0 || !index(amount, s.size + 1))
|
|
4768
|
-
throw Error("Choose a legal bid.");
|
|
4769
|
-
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)
|
|
4770
|
-
throw Error("The last bid cannot make the total equal the hand size.");
|
|
4771
|
-
t.bids[seat] = amount;
|
|
4772
|
-
if (t.bids.every((b) => b !== null)) {
|
|
4773
|
-
t.phase = "play";
|
|
4774
|
-
t.turn = first(t);
|
|
4775
|
-
} else {
|
|
4776
|
-
t.turn = (seat + 1) % count;
|
|
4777
|
-
while (t.bids[t.turn] !== null) t.turn = (t.turn + 1) % count;
|
|
4778
|
-
}
|
|
4779
|
-
return t;
|
|
4780
|
-
}
|
|
4781
|
-
if (s.phase === "moon") {
|
|
4782
|
-
if (type !== "moon" || ![0, 1].includes(amount ?? -1))
|
|
4783
|
-
throw Error("Choose how to score the moon.");
|
|
4784
|
-
t.penalties = t.penalties.map(
|
|
4785
|
-
(_, i) => amount === 0 ? i === seat ? -26 : 0 : i === seat ? 0 : 26
|
|
4786
|
-
);
|
|
4787
|
-
finish(t);
|
|
4788
|
-
return t;
|
|
4789
|
-
}
|
|
4790
|
-
if (type !== "play" || at === void 0 || !trickLegalCards(s).includes(at))
|
|
4791
|
-
throw Error("Choose a legal card.");
|
|
4792
|
-
t.hands[seat] = t.hands[seat].filter((c) => c !== at);
|
|
4793
|
-
t.trick.push({ seat, card: at });
|
|
4794
|
-
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))
|
|
4795
|
-
t.broken = true;
|
|
4796
|
-
t.turn = (seat + 1) % count;
|
|
4797
|
-
if (t.trick.length === count) {
|
|
4798
|
-
const lead = cardSuit(t.trick[0].card), strength = (c) => (cardSuit(c) === s.trump ? 100 : cardSuit(c) === lead ? 50 : 0) + cardRank(c);
|
|
4799
|
-
const winner2 = t.trick.reduce(
|
|
4800
|
-
(a, b) => strength(b.card) > strength(a.card) ? b : a
|
|
4801
|
-
).seat;
|
|
4802
|
-
t.tricks[winner2]++;
|
|
4803
|
-
t.penalties[winner2] += t.trick.reduce((n, c) => n + penalty(c.card), 0);
|
|
4804
|
-
t.lastTrick = t.trick;
|
|
4805
|
-
t.trick = [];
|
|
4806
|
-
t.turn = winner2;
|
|
4807
|
-
if (t.hands.every((h) => !h.length)) {
|
|
4808
|
-
const moon = s.variant === "hearts" ? t.penalties.findIndex((n) => n === 26) : -1;
|
|
4809
|
-
if (moon >= 0) {
|
|
4810
|
-
t.phase = "moon";
|
|
4811
|
-
t.turn = moon;
|
|
4812
|
-
} else finish(t);
|
|
4813
|
-
}
|
|
5007
|
+
// lib/multiplayer/dice.ts
|
|
5008
|
+
var diceCategories = [
|
|
5009
|
+
"Ones",
|
|
5010
|
+
"Twos",
|
|
5011
|
+
"Threes",
|
|
5012
|
+
"Fours",
|
|
5013
|
+
"Fives",
|
|
5014
|
+
"Sixes",
|
|
5015
|
+
"Three of a kind",
|
|
5016
|
+
"Four of a kind",
|
|
5017
|
+
"Full house",
|
|
5018
|
+
"Small straight",
|
|
5019
|
+
"Large straight",
|
|
5020
|
+
"Five of a kind",
|
|
5021
|
+
"Chance"
|
|
5022
|
+
];
|
|
5023
|
+
function createDice(count) {
|
|
5024
|
+
if (!index(count - 2, 3)) throw Error("Choose two to four players.");
|
|
5025
|
+
return {
|
|
5026
|
+
dice: [0, 0, 0, 0, 0],
|
|
5027
|
+
held: Array(5).fill(false),
|
|
5028
|
+
rolls: 0,
|
|
5029
|
+
sheets: Array.from({ length: count }, () => Array(13).fill(null)),
|
|
5030
|
+
bonuses: Array(count).fill(0),
|
|
5031
|
+
turn: 0,
|
|
5032
|
+
finished: false,
|
|
5033
|
+
winner: null
|
|
5034
|
+
};
|
|
5035
|
+
}
|
|
5036
|
+
function diceScore(dice, category, joker = false) {
|
|
5037
|
+
const counts = Array.from(
|
|
5038
|
+
{ length: 6 },
|
|
5039
|
+
(_, i) => dice.filter((v) => v === i + 1).length
|
|
5040
|
+
), sum = dice.reduce((a, b) => a + b, 0);
|
|
5041
|
+
if (category < 6) return counts[category] * (category + 1);
|
|
5042
|
+
switch (category) {
|
|
5043
|
+
case 6:
|
|
5044
|
+
return Math.max(...counts) >= 3 ? sum : 0;
|
|
5045
|
+
case 7:
|
|
5046
|
+
return Math.max(...counts) >= 4 ? sum : 0;
|
|
5047
|
+
case 8:
|
|
5048
|
+
return joker || counts.includes(3) && counts.includes(2) ? 25 : 0;
|
|
5049
|
+
case 9:
|
|
5050
|
+
return joker || [0, 1, 2].some((start) => counts.slice(start, start + 4).every(Boolean)) ? 30 : 0;
|
|
5051
|
+
case 10:
|
|
5052
|
+
return joker || [0, 1].some((start) => counts.slice(start, start + 5).every(Boolean)) ? 40 : 0;
|
|
5053
|
+
case 11:
|
|
5054
|
+
return counts.includes(5) ? 50 : 0;
|
|
5055
|
+
case 12:
|
|
5056
|
+
return sum;
|
|
5057
|
+
default:
|
|
5058
|
+
throw Error("Choose a score category.");
|
|
4814
5059
|
}
|
|
4815
|
-
return t;
|
|
4816
5060
|
}
|
|
4817
|
-
function
|
|
4818
|
-
const
|
|
4819
|
-
return
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
)
|
|
5061
|
+
function diceTotal(s, seat) {
|
|
5062
|
+
const sheet = s.sheets[seat], upper = sheet.slice(0, 6).reduce((sum, n) => sum + (n ?? 0), 0);
|
|
5063
|
+
return sheet.reduce((sum, n) => sum + (n ?? 0), 0) + (upper >= 63 ? 35 : 0) + s.bonuses[seat];
|
|
5064
|
+
}
|
|
5065
|
+
function playDice(s, type, at, entropy2) {
|
|
5066
|
+
if (s.finished) throw Error("This match is finished.");
|
|
5067
|
+
if (type === "roll") {
|
|
5068
|
+
if (s.rolls >= 3) throw Error("Record a score after three rolls.");
|
|
5069
|
+
const random = draws(entropy2, 5);
|
|
5070
|
+
return {
|
|
5071
|
+
...s,
|
|
5072
|
+
rolls: s.rolls + 1,
|
|
5073
|
+
dice: s.dice.map(
|
|
5074
|
+
(v, i) => s.rolls && s.held[i] ? v : 1 + Math.floor(random[i] * 6)
|
|
5075
|
+
)
|
|
5076
|
+
};
|
|
5077
|
+
}
|
|
5078
|
+
if (at === void 0 || !index(at, type === "hold" ? 5 : 13) || !s.rolls)
|
|
5079
|
+
throw Error("Roll first, then choose a die or score category.");
|
|
5080
|
+
if (type === "hold")
|
|
5081
|
+
return { ...s, held: s.held.map((v, i) => i === at ? !v : v) };
|
|
5082
|
+
if (type !== "score" || s.sheets[s.turn][at] !== null)
|
|
5083
|
+
throw Error("Choose an unused score category.");
|
|
5084
|
+
const sheets = s.sheets.map((row) => [...row]), bonuses = [...s.bonuses];
|
|
5085
|
+
const five = s.dice.every((v) => v === s.dice[0]), upper = s.dice[0] - 1;
|
|
5086
|
+
const joker = five && sheets[s.turn][11] !== null && sheets[s.turn][upper] !== null;
|
|
5087
|
+
if (joker && sheets[s.turn].slice(6).some((n) => n === null) && at < 6)
|
|
5088
|
+
throw Error(
|
|
5089
|
+
"Use the five-of-a-kind joker in an unused lower category first."
|
|
5090
|
+
);
|
|
5091
|
+
sheets[s.turn][at] = diceScore(s.dice, at, joker);
|
|
5092
|
+
if (joker && sheets[s.turn][11] === 50) bonuses[s.turn] += 100;
|
|
5093
|
+
const next3 = {
|
|
5094
|
+
...s,
|
|
5095
|
+
sheets,
|
|
5096
|
+
bonuses,
|
|
5097
|
+
turn: (s.turn + 1) % s.sheets.length,
|
|
5098
|
+
rolls: 0,
|
|
5099
|
+
held: Array(5).fill(false),
|
|
5100
|
+
dice: [0, 0, 0, 0, 0]
|
|
4826
5101
|
};
|
|
5102
|
+
next3.finished = sheets.every((row) => row.every((n) => n !== null));
|
|
5103
|
+
if (next3.finished)
|
|
5104
|
+
next3.winner = best(sheets.map((_, i) => diceTotal(next3, i)));
|
|
5105
|
+
return next3;
|
|
4827
5106
|
}
|
|
4828
5107
|
|
|
4829
|
-
// lib/
|
|
4830
|
-
var
|
|
5108
|
+
// lib/agents/action-help.ts
|
|
5109
|
+
var help = {
|
|
5110
|
+
amazons: "move: numeric from/to 0\u201399 and amount for the arrow square. Select an exact legal_wire_actions entry.",
|
|
5111
|
+
"lines-of-action": "move: numeric from/to 0\u201363. Move the count of all stones along the full line; enemy stones block jumping.",
|
|
5112
|
+
"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.",
|
|
5113
|
+
euchre: "Use visible_state.legalActions. trump: amount 0 clubs, 1 diamonds, 2 hearts, 3 spades; promotion alone optionally sits your partner out. pass declines trump. discard/play: at is a held card ID, not a hand index. IDs are suit*6+rank, ranks 0\u20135 mean 9,10,J,Q,K,A. The left bower belongs to trump for following suit. next deals after a hand. Partners are opposite seats; first team to 10 wins.",
|
|
5114
|
+
cribbage: "Use visible_state.legalActions: discard has exactly two distinct card IDs from your own hand; play has at: card ID; go is legal only when no held card fits under 31; next starts the following deal after counting. Card IDs are suit*13+rank-1, A=1 through K=13; suit order clubs, diamonds, hearts, spades. Crib and opponents' unplayed cards remain hidden.",
|
|
5115
|
+
fanorona: "Use visible_state.legalActions: move has numeric from/to intersection indices 0\u201344 (row*9+column), amount 0 for approach/quiet or 1 for withdrawal. stop ends an optional capture continuation. Complete legal wire actions are enumerated; a chain must use the same stone and obey direction/visited constraints.",
|
|
5116
|
+
categories: `begin: host starts a timed round; promotion:'untimed' disables the timer. submit/stop: text is a string holding a JSON array of six English answers in category order, for example text set to the string '["Apple","Ant",...]' (not a raw array); stop needs a complete sheet. expire reveals a timed-out round. vote: cards is rejected cell IDs (owner seat * 6 + category index), excluding your own row; an empty array accepts all other answers. next: host advances. Answers and ballots lock once submitted. 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.`,
|
|
5117
|
+
cheat: "play: cards is one to four card IDs from your hand, claiming the displayed rank. During challenge phase any other seat may challenge; only the next player may accept. Final-card claims must resolve before a win.",
|
|
5118
|
+
president: "play: cards is one to four equal-rank card IDs. Match pile size and beat its rank; 2 is high. Opening group includes 3 of clubs. pass sits out until the pile clears. Finished seats leave play; order gives final places.",
|
|
5119
|
+
rummy: "stock/upcard: draw first. meld: cards is a set or run of held card IDs. layoff: at is a public meld index, cards is held card IDs to add. discard: at is a held card ID. Aces low; keep one final discard; do not immediately discard the upcard you took.",
|
|
5120
|
+
"connect-four": "place: at is a zero-based column (0\u20136). Convenience: --column 1\u20137.",
|
|
5121
|
+
"tic-tac-toe": "place: at is a zero-based row-major square (0\u20138). legal_actions use one-based row/column instead; pass either form.",
|
|
5122
|
+
chess: "move: from/to are algebraic squares; promotion is q/r/b/n.",
|
|
5123
|
+
"atomic-chess": "move: from/to are algebraic squares; promotion is q/r/b/n. Explosions follow the game rules.",
|
|
5124
|
+
"three-check": "move: from/to are algebraic squares; promotion is q/r/b/n. Check counts are in visible_state.",
|
|
5125
|
+
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.",
|
|
5126
|
+
"brazilian-checkers": "move: from/to are zero-based squares (0\u201363); maximum captures apply.",
|
|
5127
|
+
"international-draughts": "move: from/to are zero-based squares (0\u201399). sequence: cards is a complete legal path of square indexes, including the origin.",
|
|
5128
|
+
reversi: "place: at is a zero-based row-major square (0\u201363). Unavailable turns pass automatically.",
|
|
5129
|
+
gomoku: "place: at is a zero-based row-major intersection (0\u2013224). Exactly five wins.",
|
|
5130
|
+
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.",
|
|
5131
|
+
"dots-boxes": 'Send a legal_actions entry ({"type":"edge","edge":N}, one-based) unchanged or use --edge N; the CLI converts it to the wire action place with zero-based at = N-1 (visible_state.edges[N-1]). board_labeled lists each box with its edge numbers. Horizontal edges precede vertical edges in row-major order. Edges and box owners are in visible_state. Convenience: --edge is one-based.',
|
|
5132
|
+
oware: "sow: at is a zero-based pit index, 0\u20135 for seat 0 and 6\u201311 for seat 1. Convenience: --pit is one-based.",
|
|
5133
|
+
go: "place/dead: at is a zero-based row-major intersection (board_labeled shows row start indexes). pass/resume: no payload. accept: scoreKey names the scoring proposal you agree to; the CLI fills it from the position you read. Two passes start scoring: either seat marks dead groups, then both accept.",
|
|
5134
|
+
battleships: "auto/clear/ready: no payload. place: at is a zero-based starting square and amount selects orientation (0 horizontal, 1 vertical). fire: at is a zero-based target square. Inspect your fleet/setup phase; enemy ships remain hidden.",
|
|
5135
|
+
mills: "place/capture: at is a zero-based point index. move: from is the origin and at the destination. Completing a mill requires a subsequent capture action.",
|
|
5136
|
+
dice: `roll: no payload. hold: at is a die index (0\u20134) to toggle its hold after rolling. score: at is a category index: ${diceCategories.map((name, i) => `${i} ${name}`).join(", ")}. Inspect the roll phase and score sheet.`,
|
|
5137
|
+
ludo: "roll: no payload. move: at is the index (0\u20133) of your token. Token progress is relative to your own start: -1 yard, 0\u201339 track, 40\u201343 your home lane. Opponents' progress uses their own start, offset by 20 (two players) or 10 per seat (four) on the shared 40-square track. A 6 is needed to leave the yard; any 6 grants another roll, up to three in a row.",
|
|
5138
|
+
domino: "tile: at is your zero-based hand position (not a pip value) and amount selects the chain end: 0 left, 1 right. draw: draw until you can play, or pass when the stock is empty. next: start the following round. Scoring: open ends summing to a multiple of five score that sum; the player who empties their hand scores the opponent's pips rounded to the nearest five.",
|
|
5139
|
+
football: "place: at is a zero-based destination point. The state records ball, used edges and continuing rebounds.",
|
|
5140
|
+
backgammon: "roll: no payload. move: numeric from/to and amount (die value), one die per action. Points are 0\u201323. Positive point counts are seat 0, negative seat 1. Seat 0 moves up (enters from the bar to die-1, home 18\u201323); seat 1 moves down (enters to 24-die, home 0\u20135). from -1 means the bar; to 24 bears off for both seats. Compulsory dice usage is enforced.",
|
|
5141
|
+
shogi: "move: from is a zero-based square or a hand-piece type for a drop; to is a zero-based square; promotion:'yes' promotes. Inspect your hands and piece owners.",
|
|
5142
|
+
xiangqi: "move: numeric zero-based row-major from/to on the 9\xD710 board. promotion is unused.",
|
|
5143
|
+
poker: "fold/check/call/next: no payload. raise: amount is the desired bet total. Follow phase, pot, current bet, your cards and stack. next is a between-hand host action.",
|
|
5144
|
+
durak: "play: at is your card ID from the visible hand; take/pass: no payload. Follow attack/defence roles and visible trump/table.",
|
|
5145
|
+
makao: "play: cards is 1\u20134 card IDs from your visible hand, all of the same rank. amount chooses an ace suit (0 clubs, 1 diamonds, 2 hearts, 3 spades) or a jack demand (rank 5\u201310 or 13 for king; 0 for no demand); visible_state.suit and demand use the same numbers; promotion:'makao' announces the last card. draw/pass/catch act according to phase. Inspect drawn-card and demand state.",
|
|
5146
|
+
pan: "play: cards is an array of card IDs from the visible hand; take/next: no payload. Follow the table rank and legal group rules.",
|
|
5147
|
+
"gin-rummy": "stock/upcard/pass/next: no payload. discard/knock: at is a card ID from the visible hand. Inspect draw/discard phase and deadwood rules.",
|
|
5148
|
+
"sixty-six": "play: at is a card ID from the visible hand; promotion:'marriage' or 'marriage-claim' declares. exchange/close/claim/next are phase-specific commands.",
|
|
5149
|
+
hearts: "select: at is a card ID from the visible hand for passing (selecting a chosen card again unselects it; visible_state.selected shows your choice), then pass. play: at is a card ID from the visible hand. moon: amount selects a scoring choice. Follow phase, lead suit and legal cards.",
|
|
5150
|
+
spades: "bid: amount is the bid; blind/look for blind nil where offered. play: at is a card ID from the visible hand. Follow partner, phase and lead suit.",
|
|
5151
|
+
"oh-hell": "bid: amount is the bid. play: at is a card ID from the visible hand. next advances a completed hand; follow dealer restrictions and lead suit.",
|
|
5152
|
+
"three-five-eight": "exchange: at is one card ID from your visible hand. discard: cards is exactly four distinct card IDs from your visible hand. play: at is one card ID from your visible hand. contract: amount 0\u20133 chooses a suit, 4 no trumps, 5 misere. next advances the hand.",
|
|
5153
|
+
bridge: "bid: amount 1\u201335 encodes level floor((amount-1)/5)+1 and strain (amount-1)%5; pass/double/redouble have no payload. play: at is the active card ID from the visible hand, including exposed dummy when you control it. Follow auction, strain and lead-suit rules.",
|
|
5154
|
+
"polish-hearts": "trump: amount is a suit index; pass: no payload, only when no layout card fits; play: at is a card ID from the visible hand. Follow the current penalty/trump/layout phase.",
|
|
5155
|
+
thousand: "bid/declare: amount is points. kitty: at selects a kitty. exchange: cards is card IDs from the visible hand. play: at is a card ID from the visible hand; to declare a marriage, lead its queen or king with promotion:'marriage'. pass/bomb/redeal/next depend on phase. Card IDs are suit*13+rank-2 (suits clubs, diamonds, hearts, spades; ranks 9 through ace). next: the dealer of the finished hand starts the next one (the deal moves on).",
|
|
5156
|
+
binokel: "bid: amount is points; trump: amount is a suit; play: at is a card ID from the visible hand. pass/next have no payload. Meld values: aces around 10 (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.",
|
|
5157
|
+
skat: "bid: amount is an auction value; hold/pass/hand/pickup act in auction/pickup phases (after a hold the bidder must bid higher or pass). discard: cards is exactly two card IDs from your visible hand. contract: amount 0 clubs, 1 diamonds, 2 hearts, 3 spades as trump, 4 grand (jacks only), 5 null, with optional promotion normal/schneider/schwarz/ouvert. play: at is a card ID from the visible hand. Jacks rank clubs, spades, hearts, diamonds. Card IDs are suit*13+rank-2 (suits clubs, diamonds, hearts, spades). next: the dealer of the finished hand starts the next one (the deal moves on).",
|
|
5158
|
+
baska: "announce: no payload in the honor phase. contract: amount is one of 1,3,4,5,6 and must exceed the current contract. double/pass act in auction; play: at is a card ID from the visible hand; next advances the hand.",
|
|
5159
|
+
canasta: "stock/next: no payload. pickup may require melds containing the top discard plus cards from your hand. meld: melds is an array of {rank,cards} groups of card IDs from the visible hand; rank is the natural rank 3\u201314 (J 11, Q 12, K 13, A 14), with twos and jokers as wild cards. cards_labeled names each card. discard: at is a card ID from the visible hand. Follow opening meld and canasta requirements. Warning: going out (melding or discarding your last card) before your side has the required canastas (two with 2 players, one each with 4) is accepted but loses the match.",
|
|
5160
|
+
"letter-tiles": "place: placements is an array of {at,rack,letter?}; at is a board index and rack a rack index, letter assigns a blank. exchange: cards is rack indexes. accept/challenge/pass/next depend on phase.",
|
|
5161
|
+
"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.",
|
|
5162
|
+
"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."
|
|
5163
|
+
};
|
|
5164
|
+
function actionHelp(game) {
|
|
5165
|
+
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." };
|
|
5166
|
+
}
|
|
4831
5167
|
|
|
4832
5168
|
// lib/multiplayer/euchre.ts
|
|
4833
5169
|
var euchreSuit = (card2) => Math.floor(card2 / 6);
|
|
@@ -4856,7 +5192,7 @@ function redFromHand(t, seat, replace) {
|
|
|
4856
5192
|
}
|
|
4857
5193
|
return true;
|
|
4858
5194
|
}
|
|
4859
|
-
function
|
|
5195
|
+
function deal3(s, entropy2) {
|
|
4860
5196
|
const t = structuredClone(s), deck = shuffled(
|
|
4861
5197
|
Array.from({ length: 108 }, (_, i) => i),
|
|
4862
5198
|
entropy2
|
|
@@ -4879,7 +5215,7 @@ function deal2(s, entropy2) {
|
|
|
4879
5215
|
}
|
|
4880
5216
|
function createCanasta(count, entropy2) {
|
|
4881
5217
|
if (count !== 2 && count !== 4) throw Error("Choose two or four players.");
|
|
4882
|
-
return
|
|
5218
|
+
return deal3(
|
|
4883
5219
|
{
|
|
4884
5220
|
hands: Array.from({ length: count }, () => []),
|
|
4885
5221
|
stock: [],
|
|
@@ -4958,7 +5294,7 @@ function playCanasta(s, a, entropy2) {
|
|
|
4958
5294
|
if (a.type === "next" && s.phase === "round") {
|
|
4959
5295
|
t.round++;
|
|
4960
5296
|
t.dealer = (t.dealer + 1) % s.hands.length;
|
|
4961
|
-
return
|
|
5297
|
+
return deal3(t, entropy2);
|
|
4962
5298
|
}
|
|
4963
5299
|
if (a.type === "stock" && s.phase === "draw") {
|
|
4964
5300
|
t.openingTurn = !s.opened[i];
|
|
@@ -5102,7 +5438,7 @@ function binokelMeld(hand2, trump) {
|
|
|
5102
5438
|
add([16, 5], [0, 4, 30, 60, 90], "binokel");
|
|
5103
5439
|
return { score, labels: labels2, shown: [...shown] };
|
|
5104
5440
|
}
|
|
5105
|
-
function
|
|
5441
|
+
function deal4(s, entropy2) {
|
|
5106
5442
|
const t = structuredClone(s), deck = shuffled(
|
|
5107
5443
|
Array.from({ length: 80 }, (_, i) => i),
|
|
5108
5444
|
entropy2
|
|
@@ -5125,7 +5461,7 @@ function deal3(s, entropy2) {
|
|
|
5125
5461
|
return t;
|
|
5126
5462
|
}
|
|
5127
5463
|
function createBinokel(entropy2) {
|
|
5128
|
-
return
|
|
5464
|
+
return deal4(
|
|
5129
5465
|
{
|
|
5130
5466
|
hands: [[], [], [], []],
|
|
5131
5467
|
turn: 0,
|
|
@@ -5173,7 +5509,7 @@ function playBinokel(s, a, entropy2) {
|
|
|
5173
5509
|
if (a.type === "next" && s.phase === "round") {
|
|
5174
5510
|
t.round++;
|
|
5175
5511
|
t.dealer = (t.dealer + 1) % 4;
|
|
5176
|
-
return
|
|
5512
|
+
return deal4(t, entropy2);
|
|
5177
5513
|
}
|
|
5178
5514
|
if (s.phase === "auction") {
|
|
5179
5515
|
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) {
|
|
@@ -5347,7 +5683,7 @@ function nextSeat(s, from, eligible) {
|
|
|
5347
5683
|
return -1;
|
|
5348
5684
|
}
|
|
5349
5685
|
var canAct = (p) => !p.folded && !p.out && p.stack > 0;
|
|
5350
|
-
function
|
|
5686
|
+
function deal5(s, n) {
|
|
5351
5687
|
s.deck.pop();
|
|
5352
5688
|
for (let i = 0; i < n; i++) s.community.push(s.deck.pop());
|
|
5353
5689
|
}
|
|
@@ -5412,13 +5748,13 @@ function advance(s) {
|
|
|
5412
5748
|
}
|
|
5413
5749
|
if (s.phase === "river") return award(s);
|
|
5414
5750
|
if (s.phase === "preflop") {
|
|
5415
|
-
|
|
5751
|
+
deal5(s, 3);
|
|
5416
5752
|
s.phase = "flop";
|
|
5417
5753
|
} else if (s.phase === "flop") {
|
|
5418
|
-
|
|
5754
|
+
deal5(s, 1);
|
|
5419
5755
|
s.phase = "turn";
|
|
5420
5756
|
} else {
|
|
5421
|
-
|
|
5757
|
+
deal5(s, 1);
|
|
5422
5758
|
s.phase = "river";
|
|
5423
5759
|
}
|
|
5424
5760
|
s.bet = 0;
|
|
@@ -7981,127 +8317,29 @@ function playBackgammon(s, type, from, to, die, entropy2) {
|
|
|
7981
8317
|
return backgammonMoves(next4).length ? next4 : {
|
|
7982
8318
|
...next4,
|
|
7983
8319
|
dice: [],
|
|
7984
|
-
turn: turn === 0 ? 1 : 0,
|
|
7985
|
-
result: "All moves are blocked. Turn passes."
|
|
7986
|
-
};
|
|
7987
|
-
}
|
|
7988
|
-
const move2 = backgammonMoves(s).find(
|
|
7989
|
-
(m) => m.from === from && m.to === to && m.die === die
|
|
7990
|
-
);
|
|
7991
|
-
if (type !== "move" || !move2)
|
|
7992
|
-
throw Error(
|
|
7993
|
-
"Use the maximum number of dice; if only one can be used, use the higher die."
|
|
7994
|
-
);
|
|
7995
|
-
const next3 = step(s, move2);
|
|
7996
|
-
if (next3.off[s.turn] === 15) {
|
|
7997
|
-
const opponent = 1 - s.turn, backgammon = next3.bar[opponent] > 0 || next3.points.some(
|
|
7998
|
-
(n, i) => (s.turn === 0 ? i >= 18 : i < 6) && n * (s.turn === 0 ? -1 : 1) > 0
|
|
7999
|
-
);
|
|
8000
|
-
next3.winner = s.turn;
|
|
8001
|
-
next3.result = next3.off[opponent] ? "Single game." : backgammon ? "Backgammon \xB7 3 points." : "Gammon \xB7 2 points.";
|
|
8002
|
-
} else if (!backgammonMoves(next3).length) {
|
|
8003
|
-
next3.dice = [];
|
|
8004
|
-
next3.turn = s.turn === 0 ? 1 : 0;
|
|
8005
|
-
}
|
|
8006
|
-
return next3;
|
|
8007
|
-
}
|
|
8008
|
-
|
|
8009
|
-
// lib/multiplayer/mills.ts
|
|
8010
|
-
var millLines = [
|
|
8011
|
-
[0, 1, 2],
|
|
8012
|
-
[2, 3, 4],
|
|
8013
|
-
[4, 5, 6],
|
|
8014
|
-
[6, 7, 0],
|
|
8015
|
-
[8, 9, 10],
|
|
8016
|
-
[10, 11, 12],
|
|
8017
|
-
[12, 13, 14],
|
|
8018
|
-
[14, 15, 8],
|
|
8019
|
-
[16, 17, 18],
|
|
8020
|
-
[18, 19, 20],
|
|
8021
|
-
[20, 21, 22],
|
|
8022
|
-
[22, 23, 16],
|
|
8023
|
-
[1, 9, 17],
|
|
8024
|
-
[3, 11, 19],
|
|
8025
|
-
[5, 13, 21],
|
|
8026
|
-
[7, 15, 23]
|
|
8027
|
-
];
|
|
8028
|
-
var millEdges = millLines.flatMap(([a, b, c]) => [
|
|
8029
|
-
[a, b],
|
|
8030
|
-
[b, c]
|
|
8031
|
-
]);
|
|
8032
|
-
function createMills() {
|
|
8033
|
-
return {
|
|
8034
|
-
board: Array(24).fill(0),
|
|
8035
|
-
reserves: [9, 9],
|
|
8036
|
-
turn: 0,
|
|
8037
|
-
captures: 0,
|
|
8038
|
-
winner: null,
|
|
8039
|
-
draw: false,
|
|
8040
|
-
history: []
|
|
8041
|
-
};
|
|
8042
|
-
}
|
|
8043
|
-
var linesAt = (board, at) => millLines.filter(
|
|
8044
|
-
(line) => line.includes(at) && line.every((i) => board[i] === board[at])
|
|
8045
|
-
);
|
|
8046
|
-
function millCaptures(s) {
|
|
8047
|
-
const opponents = s.board.flatMap((p, i) => p === 2 - s.turn ? [i] : []), loose = opponents.filter((at) => !linesAt(s.board, at).length);
|
|
8048
|
-
return loose.length ? loose : opponents;
|
|
8049
|
-
}
|
|
8050
|
-
function millMoves(s) {
|
|
8051
|
-
if (s.winner !== null || s.draw || s.captures) return [];
|
|
8052
|
-
const empty = s.board.flatMap((p, i) => !p ? [i] : []);
|
|
8053
|
-
if (s.reserves[s.turn]) return empty.map((to) => ({ from: null, to }));
|
|
8054
|
-
const own = s.board.flatMap((p, i) => p === s.turn + 1 ? [i] : []);
|
|
8055
|
-
return own.flatMap(
|
|
8056
|
-
(from) => empty.filter(
|
|
8057
|
-
(to) => own.length === 3 || millEdges.some(
|
|
8058
|
-
([a, b]) => a === from && b === to || b === from && a === to
|
|
8059
|
-
)
|
|
8060
|
-
).map((to) => ({ from, to }))
|
|
8061
|
-
);
|
|
8062
|
-
}
|
|
8063
|
-
function finishTurn(s) {
|
|
8064
|
-
const turn = s.turn === 0 ? 1 : 0, key2 = s.board.join(",") + ":" + turn + ":" + s.reserves.join(",");
|
|
8065
|
-
const next3 = {
|
|
8066
|
-
...s,
|
|
8067
|
-
turn,
|
|
8068
|
-
captures: 0,
|
|
8069
|
-
history: [...s.history, key2]
|
|
8070
|
-
};
|
|
8071
|
-
const count = next3.board.filter((p) => p === turn + 1).length;
|
|
8072
|
-
if (!next3.reserves[turn] && (count < 3 || !millMoves(next3).length))
|
|
8073
|
-
next3.winner = s.turn;
|
|
8074
|
-
else next3.draw = next3.history.filter((k) => k === key2).length >= 3;
|
|
8075
|
-
return next3;
|
|
8076
|
-
}
|
|
8077
|
-
function playMills(s, type, to, from) {
|
|
8078
|
-
if (!index(to, 24) || s.winner !== null || s.draw)
|
|
8079
|
-
throw Error("Choose a legal point.");
|
|
8080
|
-
const board = [...s.board], reserves = [...s.reserves];
|
|
8081
|
-
if (type === "capture") {
|
|
8082
|
-
if (!s.captures || !millCaptures(s).includes(to))
|
|
8083
|
-
throw Error(
|
|
8084
|
-
"Capture a piece outside a mill, unless all pieces are in mills."
|
|
8085
|
-
);
|
|
8086
|
-
board[to] = 0;
|
|
8087
|
-
const next4 = { ...s, board, captures: s.captures - 1 };
|
|
8088
|
-
return next4.captures && millCaptures(next4).length ? next4 : finishTurn(next4);
|
|
8320
|
+
turn: turn === 0 ? 1 : 0,
|
|
8321
|
+
result: "All moves are blocked. Turn passes."
|
|
8322
|
+
};
|
|
8089
8323
|
}
|
|
8090
|
-
|
|
8091
|
-
(m) => m.
|
|
8092
|
-
)
|
|
8324
|
+
const move2 = backgammonMoves(s).find(
|
|
8325
|
+
(m) => m.from === from && m.to === to && m.die === die
|
|
8326
|
+
);
|
|
8327
|
+
if (type !== "move" || !move2)
|
|
8093
8328
|
throw Error(
|
|
8094
|
-
"
|
|
8329
|
+
"Use the maximum number of dice; if only one can be used, use the higher die."
|
|
8095
8330
|
);
|
|
8096
|
-
|
|
8097
|
-
|
|
8098
|
-
|
|
8099
|
-
|
|
8100
|
-
|
|
8101
|
-
|
|
8102
|
-
|
|
8103
|
-
|
|
8104
|
-
|
|
8331
|
+
const next3 = step(s, move2);
|
|
8332
|
+
if (next3.off[s.turn] === 15) {
|
|
8333
|
+
const opponent = 1 - s.turn, backgammon = next3.bar[opponent] > 0 || next3.points.some(
|
|
8334
|
+
(n, i) => (s.turn === 0 ? i >= 18 : i < 6) && n * (s.turn === 0 ? -1 : 1) > 0
|
|
8335
|
+
);
|
|
8336
|
+
next3.winner = s.turn;
|
|
8337
|
+
next3.result = next3.off[opponent] ? "Single game." : backgammon ? "Backgammon \xB7 3 points." : "Gammon \xB7 2 points.";
|
|
8338
|
+
} else if (!backgammonMoves(next3).length) {
|
|
8339
|
+
next3.dice = [];
|
|
8340
|
+
next3.turn = s.turn === 0 ? 1 : 0;
|
|
8341
|
+
}
|
|
8342
|
+
return next3;
|
|
8105
8343
|
}
|
|
8106
8344
|
|
|
8107
8345
|
// lib/multiplayer/football.ts
|
|
@@ -8372,13 +8610,13 @@ function goAction(s, seat, action, at) {
|
|
|
8372
8610
|
}
|
|
8373
8611
|
const accepted = [.../* @__PURE__ */ new Set([...s.accepted, seat])];
|
|
8374
8612
|
if (accepted.length < 2) return { ...s, accepted };
|
|
8375
|
-
const
|
|
8613
|
+
const scores2 = scoreGo(s);
|
|
8376
8614
|
return {
|
|
8377
8615
|
...s,
|
|
8378
8616
|
accepted,
|
|
8379
|
-
scores,
|
|
8617
|
+
scores: scores2,
|
|
8380
8618
|
phase: "finished",
|
|
8381
|
-
winner:
|
|
8619
|
+
winner: scores2[0] > scores2[1] ? 0 : 1
|
|
8382
8620
|
};
|
|
8383
8621
|
}
|
|
8384
8622
|
function goProposalKey(s) {
|
|
@@ -8483,7 +8721,7 @@ var adapter5 = defineMatchAdapter({
|
|
|
8483
8721
|
return {
|
|
8484
8722
|
finished: state.winner !== null || state.draw,
|
|
8485
8723
|
winner: state.winner,
|
|
8486
|
-
label: state.draw ?
|
|
8724
|
+
label: state.draw ? drawReason(state.history) : state.winner !== null ? `Player ${state.winner + 1} wins \xB7 the opponent has no legal move.` : ""
|
|
8487
8725
|
};
|
|
8488
8726
|
},
|
|
8489
8727
|
apply(state, _seat, action, _entropy, _now) {
|
|
@@ -8501,7 +8739,7 @@ var adapter6 = defineMatchAdapter({
|
|
|
8501
8739
|
actions: ["move", "sequence"],
|
|
8502
8740
|
create: () => createInternational(),
|
|
8503
8741
|
turn: (s) => s.turn,
|
|
8504
|
-
outcome: (s) => ({ finished: s.winner !== null || s.draw, winner: s.winner, label: s.draw ?
|
|
8742
|
+
outcome: (s) => ({ finished: s.winner !== null || s.draw, winner: s.winner, label: s.draw ? drawReason(s.history) : s.winner !== null ? `Player ${s.winner + 1} wins \xB7 the opponent has no legal move.` : "" }),
|
|
8505
8743
|
apply(s, _seat, a) {
|
|
8506
8744
|
if (a.type === "move" && typeof a.from === "number" && typeof a.to === "number") return moveInternational(s, a.from, a.to);
|
|
8507
8745
|
if (a.type === "sequence" && Array.isArray(a.cards) && a.cards.length >= 2 && a.cards.length <= 21) {
|
|
@@ -8546,7 +8784,7 @@ var adapter8 = defineMatchAdapter({
|
|
|
8546
8784
|
return {
|
|
8547
8785
|
finished: state.winner !== null || state.draw,
|
|
8548
8786
|
winner: state.winner,
|
|
8549
|
-
label: state.draw ? "Draw" : state.winner !== null ? `Player ${state.winner + 1} wins \xB7 the opponent is down to two pieces
|
|
8787
|
+
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"}.` : ""
|
|
8550
8788
|
};
|
|
8551
8789
|
},
|
|
8552
8790
|
apply(state, _seat, action, _entropy, _now) {
|
|
@@ -8733,9 +8971,9 @@ function loserResult(count, loser) {
|
|
|
8733
8971
|
validSeat(count, loser);
|
|
8734
8972
|
return { sides: Array.from({ length: count }, (_, seat) => ({ seats: [seat], place: seat === loser ? 1 : 0 })) };
|
|
8735
8973
|
}
|
|
8736
|
-
function scoreResult(
|
|
8737
|
-
validSeat(
|
|
8738
|
-
const values =
|
|
8974
|
+
function scoreResult(scores2, winner2, lowerWins = false) {
|
|
8975
|
+
validSeat(scores2.length, winner2);
|
|
8976
|
+
const values = scores2.map((n) => lowerWins ? -n : n);
|
|
8739
8977
|
return { sides: values.map((value, seat) => ({ seats: [seat], place: seat === winner2 ? 0 : Number(winner2 !== null) + values.filter((n, i) => i !== winner2 && n > value).length })) };
|
|
8740
8978
|
}
|
|
8741
8979
|
function partnershipResult(count, winner2) {
|
|
@@ -8752,7 +8990,7 @@ function next(s, seat) {
|
|
|
8752
8990
|
const i = s.players.indexOf(seat);
|
|
8753
8991
|
return s.players[(i + 1) % s.players.length];
|
|
8754
8992
|
}
|
|
8755
|
-
function
|
|
8993
|
+
function deal6(s, entropy2) {
|
|
8756
8994
|
const t = structuredClone(s), count = s.hands.length, deck = shuffled(
|
|
8757
8995
|
Array.from({ length: 52 }, (_, i) => i).filter((c) => cardRank(c) >= 9),
|
|
8758
8996
|
entropy2
|
|
@@ -8784,7 +9022,7 @@ function deal5(s, entropy2) {
|
|
|
8784
9022
|
}
|
|
8785
9023
|
function createThousand(count, entropy2) {
|
|
8786
9024
|
if (!index(count - 2, 3)) throw Error("Choose two to four players.");
|
|
8787
|
-
return
|
|
9025
|
+
return deal6(
|
|
8788
9026
|
{
|
|
8789
9027
|
hands: Array.from({ length: count }, () => []),
|
|
8790
9028
|
kitties: [],
|
|
@@ -8836,10 +9074,11 @@ function playThousand(s, a, entropy2) {
|
|
|
8836
9074
|
if (a.type === "next" && s.phase === "round") {
|
|
8837
9075
|
t.round++;
|
|
8838
9076
|
t.dealer = (s.dealer + 1) % s.hands.length;
|
|
8839
|
-
|
|
9077
|
+
t.previous = s.result;
|
|
9078
|
+
return deal6(t, entropy2);
|
|
8840
9079
|
}
|
|
8841
9080
|
if (a.type === "redeal" && s.phase === "auction" && s.hands[seat].filter((c) => cardRank(c) === 9).length === 4)
|
|
8842
|
-
return
|
|
9081
|
+
return deal6(t, entropy2);
|
|
8843
9082
|
if (s.phase === "auction") {
|
|
8844
9083
|
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])) {
|
|
8845
9084
|
t.bid = a.amount;
|
|
@@ -8882,9 +9121,9 @@ function playThousand(s, a, entropy2) {
|
|
|
8882
9121
|
return t;
|
|
8883
9122
|
}
|
|
8884
9123
|
if (a.type === "bomb" && ["kitty", "exchange", "declare"].includes(s.phase)) {
|
|
8885
|
-
|
|
8886
|
-
|
|
8887
|
-
t.result =
|
|
9124
|
+
const penalty2 = t.bombs[seat]++ > 0 ? t.hands.length === 4 ? 40 : 60 : 0;
|
|
9125
|
+
t.players.filter((i) => i !== seat && t.scores[i] < 800).forEach((i) => t.scores[i] += penalty2);
|
|
9126
|
+
t.result = `Player ${seat + 1} cancels the hand with a bomb${penalty2 ? `; the other players score ${penalty2}` : " (the first bomb is free)"}.`;
|
|
8888
9127
|
finish4(t);
|
|
8889
9128
|
return t;
|
|
8890
9129
|
}
|
|
@@ -8929,10 +9168,12 @@ function playThousand(s, a, entropy2) {
|
|
|
8929
9168
|
(n, c) => n + pointValue(cardRank(c)),
|
|
8930
9169
|
0
|
|
8931
9170
|
);
|
|
9171
|
+
const before = t.scores;
|
|
8932
9172
|
t.scores = t.scores.map(
|
|
8933
9173
|
(n, i) => i === t.bidder ? n + (t.points[i] >= t.contract ? t.contract : -t.contract) : t.players.includes(i) && n < 800 ? n + Math.round(t.points[i] / 10) * 10 : n
|
|
8934
9174
|
);
|
|
8935
|
-
t.
|
|
9175
|
+
const made = t.points[t.bidder] >= t.contract, change = (i) => `${t.scores[i] - before[i] >= 0 ? "+" : ""}${t.scores[i] - before[i]}`;
|
|
9176
|
+
t.result = `Player ${t.bidder + 1} ${made ? "makes" : "fails"} ${t.contract} with ${t.points[t.bidder]} (${change(t.bidder)}). ${t.players.filter((i) => i !== t.bidder).map((i) => `Player ${i + 1} ${t.points[i]} points (${before[i] >= 800 ? "not scored: 800 rule" : change(i)})`).join(", ")}.`;
|
|
8936
9177
|
finish4(t);
|
|
8937
9178
|
}
|
|
8938
9179
|
return t;
|
|
@@ -9009,7 +9250,7 @@ var skatBids = [
|
|
|
9009
9250
|
)
|
|
9010
9251
|
])
|
|
9011
9252
|
].filter((n) => n <= 264).sort((a, b) => a - b);
|
|
9012
|
-
function
|
|
9253
|
+
function deal7(s, entropy2) {
|
|
9013
9254
|
const t = structuredClone(s), deck = shuffled(
|
|
9014
9255
|
Array.from({ length: 52 }, (_, i) => i).filter((c) => cardRank(c) >= 7),
|
|
9015
9256
|
entropy2
|
|
@@ -9043,7 +9284,7 @@ function deal6(s, entropy2) {
|
|
|
9043
9284
|
return t;
|
|
9044
9285
|
}
|
|
9045
9286
|
function createSkat(entropy2) {
|
|
9046
|
-
return
|
|
9287
|
+
return deal7(
|
|
9047
9288
|
{
|
|
9048
9289
|
hands: [[], [], []],
|
|
9049
9290
|
skat: [],
|
|
@@ -9144,7 +9385,7 @@ function playSkat(s, a, entropy2) {
|
|
|
9144
9385
|
if (a.type === "next" && s.phase === "round") {
|
|
9145
9386
|
t.round++;
|
|
9146
9387
|
t.dealer = (s.dealer + 1) % 3;
|
|
9147
|
-
return
|
|
9388
|
+
return deal7(t, entropy2);
|
|
9148
9389
|
}
|
|
9149
9390
|
if (s.phase === "auction") {
|
|
9150
9391
|
if (s.offer === null) {
|
|
@@ -9305,7 +9546,7 @@ function sides(t) {
|
|
|
9305
9546
|
t.old = t.hands.map((_, i) => i === t.declarer);
|
|
9306
9547
|
else t.old = t.hands.map((h) => queens.some((c) => h.includes(c)));
|
|
9307
9548
|
}
|
|
9308
|
-
function
|
|
9549
|
+
function deal8(s, entropy2) {
|
|
9309
9550
|
const t = structuredClone(s), deck = shuffled(
|
|
9310
9551
|
Array.from({ length: 52 }, (_, i) => i).filter(
|
|
9311
9552
|
(c) => [10, 11, 12, 14].includes(cardRank(c))
|
|
@@ -9346,7 +9587,7 @@ function deal7(s, entropy2) {
|
|
|
9346
9587
|
return t;
|
|
9347
9588
|
}
|
|
9348
9589
|
function createBaska(entropy2) {
|
|
9349
|
-
return
|
|
9590
|
+
return deal8(
|
|
9350
9591
|
{
|
|
9351
9592
|
hands: [[], [], [], []],
|
|
9352
9593
|
turn: 0,
|
|
@@ -9415,7 +9656,7 @@ function playBaska(s, a, entropy2) {
|
|
|
9415
9656
|
if (a.type === "next" && s.phase === "round") {
|
|
9416
9657
|
t.round++;
|
|
9417
9658
|
t.dealer = (t.dealer + 1) % 4;
|
|
9418
|
-
return
|
|
9659
|
+
return deal8(t, entropy2);
|
|
9419
9660
|
}
|
|
9420
9661
|
if (a.type === "announce" && s.phase === "honor") {
|
|
9421
9662
|
finish5(t, true);
|
|
@@ -9535,7 +9776,7 @@ var adapter17 = defineMatchAdapter({
|
|
|
9535
9776
|
|
|
9536
9777
|
// lib/multiplayer/bridge.ts
|
|
9537
9778
|
var vulnerability = [0, 1, 2, 3, 1, 2, 3, 0, 2, 3, 0, 1, 3, 0, 1, 2];
|
|
9538
|
-
function
|
|
9779
|
+
function deal9(s, entropy2) {
|
|
9539
9780
|
const t = structuredClone(s), deck = shuffled(
|
|
9540
9781
|
Array.from({ length: 52 }, (_, i) => i),
|
|
9541
9782
|
entropy2
|
|
@@ -9563,7 +9804,7 @@ function deal8(s, entropy2) {
|
|
|
9563
9804
|
return t;
|
|
9564
9805
|
}
|
|
9565
9806
|
function createBridge(entropy2) {
|
|
9566
|
-
return
|
|
9807
|
+
return deal9(
|
|
9567
9808
|
{
|
|
9568
9809
|
hands: [[], [], [], []],
|
|
9569
9810
|
turn: 0,
|
|
@@ -9616,7 +9857,7 @@ function playBridge(s, a, entropy2) {
|
|
|
9616
9857
|
if (a.type === "next" && s.phase === "round") {
|
|
9617
9858
|
t.board++;
|
|
9618
9859
|
t.dealer = (t.dealer + 1) % 4;
|
|
9619
|
-
return
|
|
9860
|
+
return deal9(t, entropy2);
|
|
9620
9861
|
}
|
|
9621
9862
|
if (s.phase === "auction") {
|
|
9622
9863
|
if (a.type === "bid" && index((a.amount ?? 0) - 1, 35)) {
|
|
@@ -9722,7 +9963,7 @@ var adapter18 = defineMatchAdapter({
|
|
|
9722
9963
|
});
|
|
9723
9964
|
|
|
9724
9965
|
// lib/multiplayer/polish-hearts.ts
|
|
9725
|
-
function
|
|
9966
|
+
function deal10(s, entropy2) {
|
|
9726
9967
|
const t = structuredClone(s), count = s.hands.length, deck = shuffled(
|
|
9727
9968
|
Array.from({ length: 52 }, (_, i) => i).filter(
|
|
9728
9969
|
(c) => count !== 3 || c !== 0
|
|
@@ -9749,7 +9990,7 @@ function deal9(s, entropy2) {
|
|
|
9749
9990
|
}
|
|
9750
9991
|
function createPolish(count, entropy2) {
|
|
9751
9992
|
if (count !== 3 && count !== 4) throw Error("Choose three or four players.");
|
|
9752
|
-
return
|
|
9993
|
+
return deal10(
|
|
9753
9994
|
{
|
|
9754
9995
|
hands: Array.from({ length: count }, () => []),
|
|
9755
9996
|
rest: [],
|
|
@@ -9813,7 +10054,7 @@ function playPolish(s, a, entropy2) {
|
|
|
9813
10054
|
if (a.type === "next" && s.phase === "round") {
|
|
9814
10055
|
t.round++;
|
|
9815
10056
|
t.dealer = (t.dealer + 1) % count;
|
|
9816
|
-
return
|
|
10057
|
+
return deal10(t, entropy2);
|
|
9817
10058
|
}
|
|
9818
10059
|
if (a.type === "trump" && s.phase === "trump" && index(a.amount ?? -1, 4)) {
|
|
9819
10060
|
t.trump = a.amount;
|
|
@@ -10188,6 +10429,7 @@ function playMakao(s, a, entropy2) {
|
|
|
10188
10429
|
const t = structuredClone(s), seat = s.turn;
|
|
10189
10430
|
if (a.type === "catch" && s.unannounced !== null && s.unannounced !== seat) {
|
|
10190
10431
|
draw(t, s.unannounced, 5, entropy2);
|
|
10432
|
+
t.last = { seat: s.unannounced, type: "caught", count: 5 };
|
|
10191
10433
|
t.unannounced = null;
|
|
10192
10434
|
t.result = "Missed Makao: draw five.";
|
|
10193
10435
|
return t;
|
|
@@ -10196,12 +10438,13 @@ function playMakao(s, a, entropy2) {
|
|
|
10196
10438
|
t.unannounced = null;
|
|
10197
10439
|
if (s.wait) {
|
|
10198
10440
|
t.blocked[seat] = s.wait - 1;
|
|
10441
|
+
t.last = { seat, type: "wait", count: s.wait };
|
|
10199
10442
|
t.wait = 0;
|
|
10200
10443
|
advance3(t);
|
|
10201
10444
|
return t;
|
|
10202
10445
|
}
|
|
10203
10446
|
if (s.penalty) {
|
|
10204
|
-
draw(t, seat, s.penalty, entropy2);
|
|
10447
|
+
t.last = { seat, type: "penalty", count: draw(t, seat, s.penalty, entropy2).length };
|
|
10205
10448
|
t.penalty = 0;
|
|
10206
10449
|
t.suit = null;
|
|
10207
10450
|
advance3(t);
|
|
@@ -10209,11 +10452,13 @@ function playMakao(s, a, entropy2) {
|
|
|
10209
10452
|
}
|
|
10210
10453
|
const received = draw(t, seat, 1, entropy2);
|
|
10211
10454
|
t.drawn = received[0] ?? null;
|
|
10455
|
+
t.last = { seat, type: "draw", count: received.length };
|
|
10212
10456
|
t.phase = "drawn";
|
|
10213
10457
|
if (t.drawn === null) advance3(t);
|
|
10214
10458
|
return t;
|
|
10215
10459
|
}
|
|
10216
10460
|
if (a.type === "pass" && s.phase === "drawn") {
|
|
10461
|
+
t.last = { seat, type: "keep", count: 1 };
|
|
10217
10462
|
t.unannounced = null;
|
|
10218
10463
|
t.suit = null;
|
|
10219
10464
|
advance3(t);
|
|
@@ -10228,6 +10473,7 @@ function playMakao(s, a, entropy2) {
|
|
|
10228
10473
|
throw Error("Request an ordinary rank, or choose no demand.");
|
|
10229
10474
|
t.unannounced = null;
|
|
10230
10475
|
t.hands[seat] = t.hands[seat].filter((c) => !cards.includes(c));
|
|
10476
|
+
t.last = { seat, type: "play", cards: [...cards], ...t.hands[seat].length === 1 && a.promotion === "makao" ? { announced: true } : {} };
|
|
10231
10477
|
t.pile.push(...cards);
|
|
10232
10478
|
t.suit = null;
|
|
10233
10479
|
if (rank2 === 14) t.suit = a.amount;
|
|
@@ -10286,7 +10532,7 @@ var adapter21 = defineMatchAdapter({
|
|
|
10286
10532
|
});
|
|
10287
10533
|
|
|
10288
10534
|
// lib/multiplayer/three-five-eight.ts
|
|
10289
|
-
function
|
|
10535
|
+
function deal11(s, entropy2) {
|
|
10290
10536
|
const t = structuredClone(s), deck = shuffled(
|
|
10291
10537
|
Array.from({ length: 52 }, (_, i) => i),
|
|
10292
10538
|
entropy2
|
|
@@ -10323,7 +10569,7 @@ function deal10(s, entropy2) {
|
|
|
10323
10569
|
return t;
|
|
10324
10570
|
}
|
|
10325
10571
|
function createThree(entropy2) {
|
|
10326
|
-
return
|
|
10572
|
+
return deal11(
|
|
10327
10573
|
{
|
|
10328
10574
|
hands: [[], [], []],
|
|
10329
10575
|
kitty: [],
|
|
@@ -10362,7 +10608,7 @@ function playThree(s, a, entropy2) {
|
|
|
10362
10608
|
if (a.type === "next" && s.phase === "round") {
|
|
10363
10609
|
t.round++;
|
|
10364
10610
|
t.dealer = (s.dealer + 1) % 3;
|
|
10365
|
-
return
|
|
10611
|
+
return deal11(t, entropy2);
|
|
10366
10612
|
}
|
|
10367
10613
|
if (s.phase === "exchange" && a.type === "exchange" && a.at !== void 0 && s.hands[s.turn].includes(a.at)) {
|
|
10368
10614
|
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];
|
|
@@ -11143,7 +11389,7 @@ var adapter26 = defineMatchAdapter({
|
|
|
11143
11389
|
|
|
11144
11390
|
// lib/multiplayer/pan.ts
|
|
11145
11391
|
var nineHearts = 33;
|
|
11146
|
-
function
|
|
11392
|
+
function deal12(count, entropy2, letters, round) {
|
|
11147
11393
|
const cards = shuffled(
|
|
11148
11394
|
Array.from({ length: 52 }, (_, i) => i).filter((c) => cardRank(c) >= 9),
|
|
11149
11395
|
entropy2
|
|
@@ -11165,7 +11411,7 @@ function deal11(count, entropy2, letters, round) {
|
|
|
11165
11411
|
}
|
|
11166
11412
|
function createPan(count, entropy2) {
|
|
11167
11413
|
if (!index(count - 2, 3)) throw Error("Choose two to four players.");
|
|
11168
|
-
return
|
|
11414
|
+
return deal12(count, entropy2, Array(count).fill(0), 0);
|
|
11169
11415
|
}
|
|
11170
11416
|
function panLegal(s, cards) {
|
|
11171
11417
|
const hand2 = s.hands[s.turn];
|
|
@@ -11181,7 +11427,7 @@ function panLegal(s, cards) {
|
|
|
11181
11427
|
}
|
|
11182
11428
|
function playPan(s, type, cards, entropy2) {
|
|
11183
11429
|
if (type === "next" && s.phase === "round")
|
|
11184
|
-
return
|
|
11430
|
+
return { ...deal12(s.hands.length, entropy2, s.letters, s.round + 1), previous: s.result };
|
|
11185
11431
|
if (s.phase !== "play") throw Error("This round is finished.");
|
|
11186
11432
|
const t = structuredClone(s), seat = s.turn;
|
|
11187
11433
|
if (type === "play" && panLegal(s, cards)) {
|
|
@@ -11251,189 +11497,6 @@ var adapter27 = defineMatchAdapter({
|
|
|
11251
11497
|
}
|
|
11252
11498
|
});
|
|
11253
11499
|
|
|
11254
|
-
// lib/multiplayer/gin-rummy.ts
|
|
11255
|
-
var ginRank = (c) => cardRank(c) === 14 ? 1 : cardRank(c);
|
|
11256
|
-
var ginValue = (c) => Math.min(10, ginRank(c));
|
|
11257
|
-
function ginMelds(hand2) {
|
|
11258
|
-
const melds = [];
|
|
11259
|
-
for (let rank2 = 1; rank2 <= 13; rank2++) {
|
|
11260
|
-
const cards = hand2.filter((c) => ginRank(c) === rank2);
|
|
11261
|
-
if (cards.length >= 3) {
|
|
11262
|
-
if (cards.length === 4) {
|
|
11263
|
-
melds.push(cards);
|
|
11264
|
-
for (let i = 0; i < 4; i++) melds.push(cards.filter((_, j) => j !== i));
|
|
11265
|
-
} else melds.push(cards);
|
|
11266
|
-
}
|
|
11267
|
-
}
|
|
11268
|
-
for (let suit = 0; suit < 4; suit++) {
|
|
11269
|
-
const cards = hand2.filter((c) => cardSuit(c) === suit).sort((a, b) => ginRank(a) - ginRank(b));
|
|
11270
|
-
for (let start = 0; start < cards.length; start++)
|
|
11271
|
-
for (let end = start + 2; end < cards.length; end++) {
|
|
11272
|
-
const slice = cards.slice(start, end + 1);
|
|
11273
|
-
if (slice.every(
|
|
11274
|
-
(c, i) => i === 0 || ginRank(c) === ginRank(slice[i - 1]) + 1
|
|
11275
|
-
))
|
|
11276
|
-
melds.push(slice);
|
|
11277
|
-
else break;
|
|
11278
|
-
}
|
|
11279
|
-
}
|
|
11280
|
-
return melds;
|
|
11281
|
-
}
|
|
11282
|
-
function partitions(hand2) {
|
|
11283
|
-
const melds = ginMelds(hand2), out2 = [];
|
|
11284
|
-
function visit(start, used, chosen) {
|
|
11285
|
-
const deadwood2 = hand2.filter((c) => !used.has(c));
|
|
11286
|
-
out2.push({
|
|
11287
|
-
melds: chosen,
|
|
11288
|
-
deadwood: deadwood2,
|
|
11289
|
-
value: deadwood2.reduce((n, c) => n + ginValue(c), 0)
|
|
11290
|
-
});
|
|
11291
|
-
for (let i = start; i < melds.length; i++)
|
|
11292
|
-
if (melds[i].every((c) => !used.has(c)))
|
|
11293
|
-
visit(i + 1, /* @__PURE__ */ new Set([...used, ...melds[i]]), [...chosen, melds[i]]);
|
|
11294
|
-
}
|
|
11295
|
-
visit(0, /* @__PURE__ */ new Set(), []);
|
|
11296
|
-
return out2;
|
|
11297
|
-
}
|
|
11298
|
-
function ginLayout(hand2) {
|
|
11299
|
-
return partitions(hand2).reduce((a, b) => b.value < a.value ? b : a);
|
|
11300
|
-
}
|
|
11301
|
-
function ginLayoffValue(hand2, onto) {
|
|
11302
|
-
const cache = /* @__PURE__ */ new Map();
|
|
11303
|
-
function attach(cards, melds) {
|
|
11304
|
-
const key2 = cards.join(",") + "|" + melds.map((m) => m.join(",")).join(";"), cached = cache.get(key2);
|
|
11305
|
-
if (cached !== void 0) return cached;
|
|
11306
|
-
let best2 = cards.reduce((n, c) => n + ginValue(c), 0);
|
|
11307
|
-
cards.forEach(
|
|
11308
|
-
(c, i) => melds.forEach((m, j) => {
|
|
11309
|
-
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);
|
|
11310
|
-
if (fits) {
|
|
11311
|
-
const next3 = melds.map(
|
|
11312
|
-
(m2, k) => k === j ? [...m2, c].sort((a, b) => ginRank(a) - ginRank(b)) : m2
|
|
11313
|
-
);
|
|
11314
|
-
best2 = Math.min(
|
|
11315
|
-
best2,
|
|
11316
|
-
attach(
|
|
11317
|
-
cards.filter((_, k) => k !== i),
|
|
11318
|
-
next3
|
|
11319
|
-
)
|
|
11320
|
-
);
|
|
11321
|
-
}
|
|
11322
|
-
})
|
|
11323
|
-
);
|
|
11324
|
-
cache.set(key2, best2);
|
|
11325
|
-
return best2;
|
|
11326
|
-
}
|
|
11327
|
-
return Math.min(...partitions(hand2).map((p) => attach(p.deadwood, onto)));
|
|
11328
|
-
}
|
|
11329
|
-
function deal12(dealer, scores, wins, round, entropy2) {
|
|
11330
|
-
const deck = shuffled(
|
|
11331
|
-
Array.from({ length: 52 }, (_, i) => i),
|
|
11332
|
-
entropy2
|
|
11333
|
-
);
|
|
11334
|
-
return {
|
|
11335
|
-
hands: [
|
|
11336
|
-
deck.slice(0, 10).sort((a, b) => a - b),
|
|
11337
|
-
deck.slice(10, 20).sort((a, b) => a - b)
|
|
11338
|
-
],
|
|
11339
|
-
stock: deck.slice(21),
|
|
11340
|
-
discard: [deck[20]],
|
|
11341
|
-
turn: 1 - dealer,
|
|
11342
|
-
dealer,
|
|
11343
|
-
phase: "offer",
|
|
11344
|
-
offers: 0,
|
|
11345
|
-
drawnDiscard: null,
|
|
11346
|
-
scores,
|
|
11347
|
-
wins,
|
|
11348
|
-
winner: null,
|
|
11349
|
-
round,
|
|
11350
|
-
result: "",
|
|
11351
|
-
revealed: [],
|
|
11352
|
-
margin: null
|
|
11353
|
-
};
|
|
11354
|
-
}
|
|
11355
|
-
function createGin(entropy2) {
|
|
11356
|
-
return deal12(Math.floor(draws(entropy2, 1)[0] * 2), [0, 0], [0, 0], 0, entropy2);
|
|
11357
|
-
}
|
|
11358
|
-
function playGin(s, type, at, entropy2) {
|
|
11359
|
-
if (type === "next" && s.phase === "round")
|
|
11360
|
-
return deal12(
|
|
11361
|
-
s.result.startsWith("Stock") ? s.dealer : 1 - s.dealer,
|
|
11362
|
-
s.scores,
|
|
11363
|
-
s.wins,
|
|
11364
|
-
s.round + 1,
|
|
11365
|
-
entropy2
|
|
11366
|
-
);
|
|
11367
|
-
if (["finished", "round"].includes(s.phase))
|
|
11368
|
-
throw Error("This round is finished.");
|
|
11369
|
-
const t = structuredClone(s), seat = s.turn;
|
|
11370
|
-
if (type === "pass" && s.phase === "offer") {
|
|
11371
|
-
t.offers++;
|
|
11372
|
-
if (t.offers === 2) {
|
|
11373
|
-
t.phase = "draw";
|
|
11374
|
-
t.turn = 1 - s.dealer;
|
|
11375
|
-
} else t.turn = 1 - seat;
|
|
11376
|
-
return t;
|
|
11377
|
-
}
|
|
11378
|
-
if (["draw", "offer"].includes(s.phase) && ["stock", "upcard"].includes(type)) {
|
|
11379
|
-
if (s.phase === "offer" && type === "stock")
|
|
11380
|
-
throw Error("Take the upcard or pass the opening offer.");
|
|
11381
|
-
if (s.phase === "draw" && s.offers === 2 && s.round === t.round && type === "upcard")
|
|
11382
|
-
throw Error("Both players passed: draw from the stock.");
|
|
11383
|
-
if (s.stock.length <= 2) {
|
|
11384
|
-
t.phase = "round";
|
|
11385
|
-
t.result = "Stock exhausted: round cancelled, same dealer.";
|
|
11386
|
-
t.turn = s.dealer;
|
|
11387
|
-
return t;
|
|
11388
|
-
}
|
|
11389
|
-
const card2 = type === "upcard" ? t.discard.pop() : t.stock.shift();
|
|
11390
|
-
t.drawnDiscard = type === "upcard" ? card2 : null;
|
|
11391
|
-
t.hands[seat].push(card2);
|
|
11392
|
-
t.hands[seat].sort((a, b) => a - b);
|
|
11393
|
-
t.phase = "discard";
|
|
11394
|
-
t.offers = 0;
|
|
11395
|
-
return t;
|
|
11396
|
-
}
|
|
11397
|
-
if (s.phase === "discard" && ["discard", "knock"].includes(type) && at !== void 0 && s.hands[seat].includes(at) && at !== s.drawnDiscard) {
|
|
11398
|
-
t.hands[seat] = t.hands[seat].filter((c) => c !== at);
|
|
11399
|
-
t.discard.push(at);
|
|
11400
|
-
t.drawnDiscard = null;
|
|
11401
|
-
if (type === "knock") {
|
|
11402
|
-
const layout = ginLayout(t.hands[seat]);
|
|
11403
|
-
if (layout.value > 10)
|
|
11404
|
-
throw Error("Knock only with ten or fewer deadwood points.");
|
|
11405
|
-
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;
|
|
11406
|
-
const points = layout.value === 0 ? opp + 25 : winner2 === seat ? opp - layout.value : layout.value - opp + 25;
|
|
11407
|
-
t.scores[winner2] += points;
|
|
11408
|
-
t.wins[winner2]++;
|
|
11409
|
-
t.result = `${layout.value === 0 ? "Gin" : winner2 === seat ? "Knock" : "Undercut"}: Player ${winner2 + 1} scores ${points}. Deadwood ${layout.value} vs ${opp}.`;
|
|
11410
|
-
t.revealed = t.hands.map((h) => ginLayout(h).melds);
|
|
11411
|
-
t.phase = t.scores[winner2] >= 100 ? "finished" : "round";
|
|
11412
|
-
t.turn = winner2;
|
|
11413
|
-
if (t.phase === "finished") {
|
|
11414
|
-
t.winner = winner2;
|
|
11415
|
-
t.margin = t.scores[winner2] + 100 + (t.scores[other] === 0 ? 100 : 0) + 25 * t.wins[winner2] - t.scores[other] - 25 * t.wins[other];
|
|
11416
|
-
t.result += ` Final margin including game and hand bonuses: ${t.margin}.`;
|
|
11417
|
-
}
|
|
11418
|
-
} else {
|
|
11419
|
-
t.phase = "draw";
|
|
11420
|
-
t.turn = 1 - seat;
|
|
11421
|
-
}
|
|
11422
|
-
return t;
|
|
11423
|
-
}
|
|
11424
|
-
throw Error("Choose a legal draw, discard or knock.");
|
|
11425
|
-
}
|
|
11426
|
-
function ginView(s, seat) {
|
|
11427
|
-
const { stock, ...rest } = s;
|
|
11428
|
-
return {
|
|
11429
|
-
...rest,
|
|
11430
|
-
stockCount: stock.length,
|
|
11431
|
-
hands: s.hands.map(
|
|
11432
|
-
(h, i) => i === seat || ["round", "finished"].includes(s.phase) ? h : h.map(() => null)
|
|
11433
|
-
)
|
|
11434
|
-
};
|
|
11435
|
-
}
|
|
11436
|
-
|
|
11437
11500
|
// lib/multiplayer/adapters/gin-rummy.ts
|
|
11438
11501
|
var adapter28 = defineMatchAdapter({
|
|
11439
11502
|
actions: ["next", "pass", "stock", "upcard", "discard", "knock"],
|
|
@@ -11459,7 +11522,7 @@ var adapter28 = defineMatchAdapter({
|
|
|
11459
11522
|
// lib/multiplayer/sixty-six.ts
|
|
11460
11523
|
var sixtyStrength = (c) => [9, 11, 12, 13, 10, 14].indexOf(cardRank(c));
|
|
11461
11524
|
var sixtyPoints = (c) => [0, 2, 3, 4, 10, 11][sixtyStrength(c)];
|
|
11462
|
-
function deal13(dealer,
|
|
11525
|
+
function deal13(dealer, scores2, round, entropy2) {
|
|
11463
11526
|
const deck = shuffled(
|
|
11464
11527
|
Array.from({ length: 52 }, (_, i) => i).filter((c) => cardRank(c) >= 9),
|
|
11465
11528
|
entropy2
|
|
@@ -11481,7 +11544,7 @@ function deal13(dealer, scores, round, entropy2) {
|
|
|
11481
11544
|
points: [0, 0],
|
|
11482
11545
|
pending: [0, 0],
|
|
11483
11546
|
tricks: [0, 0],
|
|
11484
|
-
scores,
|
|
11547
|
+
scores: scores2,
|
|
11485
11548
|
phase: "play",
|
|
11486
11549
|
winner: null,
|
|
11487
11550
|
result: "",
|
|
@@ -11518,7 +11581,7 @@ function award2(t, winner2, failure = false) {
|
|
|
11518
11581
|
}
|
|
11519
11582
|
function playSixtySix(s, type, at, marriage = false, claimMarriage = false, entropy2) {
|
|
11520
11583
|
if (type === "next" && s.phase === "round")
|
|
11521
|
-
return deal13(1 - s.dealer, s.scores, s.round + 1, entropy2);
|
|
11584
|
+
return { ...deal13(1 - s.dealer, s.scores, s.round + 1, entropy2), previous: s.result };
|
|
11522
11585
|
if (s.phase !== "play") throw Error("This round is finished.");
|
|
11523
11586
|
const t = structuredClone(s), seat = s.turn;
|
|
11524
11587
|
if (type === "close" && !s.trick.length && s.closed === null && s.upcard !== null) {
|
|
@@ -11563,6 +11626,7 @@ function playSixtySix(s, type, at, marriage = false, claimMarriage = false, entr
|
|
|
11563
11626
|
t.tricks[winner2]++;
|
|
11564
11627
|
t.turn = winner2;
|
|
11565
11628
|
t.lastTrick = t.trick;
|
|
11629
|
+
t.played = [...s.played ?? [], ...t.trick.map((c) => c.card)];
|
|
11566
11630
|
t.trick = [];
|
|
11567
11631
|
if (t.closed === null && t.upcard !== null)
|
|
11568
11632
|
for (const i of [winner2, 1 - winner2]) {
|
|
@@ -11705,13 +11769,13 @@ function dominoMoves(s) {
|
|
|
11705
11769
|
});
|
|
11706
11770
|
}
|
|
11707
11771
|
function endRound(s, winner2, bonus) {
|
|
11708
|
-
const
|
|
11709
|
-
if (winner2 !== null)
|
|
11710
|
-
const finished =
|
|
11772
|
+
const scores2 = [...s.scores];
|
|
11773
|
+
if (winner2 !== null) scores2[winner2] += Math.round(bonus / 5) * 5;
|
|
11774
|
+
const finished = scores2.some((n) => n >= 100);
|
|
11711
11775
|
return {
|
|
11712
11776
|
...s,
|
|
11713
|
-
scores,
|
|
11714
|
-
winner: finished ? best(
|
|
11777
|
+
scores: scores2,
|
|
11778
|
+
winner: finished ? best(scores2) : null,
|
|
11715
11779
|
phase: finished ? "finished" : "round",
|
|
11716
11780
|
turn: winner2 ?? s.turn,
|
|
11717
11781
|
result: winner2 === null ? "Blocked round: equal pip totals." : `Player ${winner2 + 1} wins the round.`
|
|
@@ -11727,7 +11791,7 @@ function playDomino(s, type, at, end, entropy2) {
|
|
|
11727
11791
|
turn: s.turn
|
|
11728
11792
|
};
|
|
11729
11793
|
if (s.phase !== "play") throw Error("This round is finished.");
|
|
11730
|
-
const hands = s.hands.map((h) => [...h]), stock = [...s.stock], chain = [...s.chain],
|
|
11794
|
+
const hands = s.hands.map((h) => [...h]), stock = [...s.stock], chain = [...s.chain], scores2 = [...s.scores];
|
|
11731
11795
|
if (type === "draw") {
|
|
11732
11796
|
if (dominoMoves(s).length)
|
|
11733
11797
|
throw Error("Play a matching tile before drawing.");
|
|
@@ -11752,20 +11816,20 @@ function playDomino(s, type, at, end, entropy2) {
|
|
|
11752
11816
|
}
|
|
11753
11817
|
const left = chain[0], right = chain.at(-1);
|
|
11754
11818
|
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]);
|
|
11755
|
-
if (total % 5 === 0)
|
|
11819
|
+
if (total % 5 === 0) scores2[s.turn] += total;
|
|
11756
11820
|
const next3 = {
|
|
11757
11821
|
...s,
|
|
11758
11822
|
hands,
|
|
11759
11823
|
stock,
|
|
11760
11824
|
chain,
|
|
11761
|
-
scores,
|
|
11825
|
+
scores: scores2,
|
|
11762
11826
|
passes: 0,
|
|
11763
11827
|
turn: 1 - s.turn
|
|
11764
11828
|
};
|
|
11765
11829
|
if (!hands[s.turn].length)
|
|
11766
11830
|
return endRound(next3, s.turn, pipSum(hands[1 - s.turn]));
|
|
11767
|
-
if (
|
|
11768
|
-
return { ...next3, phase: "finished", winner: best(
|
|
11831
|
+
if (scores2.some((n) => n >= 100))
|
|
11832
|
+
return { ...next3, phase: "finished", winner: best(scores2) };
|
|
11769
11833
|
return next3;
|
|
11770
11834
|
}
|
|
11771
11835
|
function dominoView(s, seat) {
|
|
@@ -11983,11 +12047,12 @@ function playRummy(s, seat, a, entropy2) {
|
|
|
11983
12047
|
return t;
|
|
11984
12048
|
}
|
|
11985
12049
|
if (s.phase !== "meld") throw Error("Draw from the stock or take the top discard first.");
|
|
11986
|
-
|
|
12050
|
+
const keepsDiscard = (cards) => s.hands[seat].some((c) => !cards.includes(c) && c !== s.taken);
|
|
12051
|
+
if (a.type === "meld" && ownedCards(s.hands[seat], a.cards) && keepsDiscard(a.cards) && rummyMeld(a.cards)) {
|
|
11987
12052
|
t.melds.push({ seat, cards: [...a.cards].sort((a2, b) => rummyRank(a2) - rummyRank(b)) });
|
|
11988
12053
|
t.hands[seat] = t.hands[seat].filter((c) => !a.cards.includes(c));
|
|
11989
12054
|
t.result = `Player ${seat + 1} melds ${a.cards.length} cards.`;
|
|
11990
|
-
} else if (a.type === "layoff" && index(a.at, s.melds.length) && ownedCards(s.hands[seat], a.cards) && a.cards
|
|
12055
|
+
} 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])) {
|
|
11991
12056
|
t.melds[a.at].cards.push(...a.cards);
|
|
11992
12057
|
t.melds[a.at].cards.sort((a2, b) => rummyRank(a2) - rummyRank(b));
|
|
11993
12058
|
t.hands[seat] = t.hands[seat].filter((c) => !a.cards.includes(c));
|
|
@@ -12079,7 +12144,8 @@ var promotions = ["marriage", "marriage-claim", "makao", "normal", "schneider",
|
|
|
12079
12144
|
function hand(state, seat) {
|
|
12080
12145
|
for (const key2 of ["hands", "hand"]) {
|
|
12081
12146
|
const hands = state[key2];
|
|
12082
|
-
|
|
12147
|
+
const held = typeof state.playSeat === "number" && state.turn === seat ? state.playSeat : seat;
|
|
12148
|
+
if (Array.isArray(hands) && Array.isArray(hands[held])) return hands[held].filter((c) => typeof c === "number" && c >= 0);
|
|
12083
12149
|
}
|
|
12084
12150
|
return [];
|
|
12085
12151
|
}
|
|
@@ -12150,6 +12216,10 @@ function specific(game, state, seat) {
|
|
|
12150
12216
|
const s = state;
|
|
12151
12217
|
return s.die ? ludoMoves(s).map((at) => ({ type: "move", at })) : [{ type: "roll" }];
|
|
12152
12218
|
}
|
|
12219
|
+
if (game === "makao") return [...generic(adapters.makao, state, seat), ...cardGroups(hand(state, seat)).flatMap((cards) => [
|
|
12220
|
+
{ type: "play", cards, promotion: "makao" },
|
|
12221
|
+
...[0, 1, 2, 3, 5, 6, 7, 8, 9, 10, 13].flatMap((amount) => [{ type: "play", cards, amount }, { type: "play", cards, amount, promotion: "makao" }])
|
|
12222
|
+
])];
|
|
12153
12223
|
if (game === "go") {
|
|
12154
12224
|
const s = state;
|
|
12155
12225
|
if (s.phase === "scoring" && s.accepted.includes(seat)) return [];
|
|
@@ -12249,6 +12319,35 @@ function millsBoard(board) {
|
|
|
12249
12319
|
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."
|
|
12250
12320
|
};
|
|
12251
12321
|
}
|
|
12322
|
+
function battleshipsBoard(v) {
|
|
12323
|
+
const ships = new Set(v.ownFleet.flat()), sunk = new Set(v.sunk.flat());
|
|
12324
|
+
const target = (at) => sunk.has(at) ? "#" : v.hits.includes(at) ? "x" : v.fired.includes(at) ? "o" : ".";
|
|
12325
|
+
const fleet = (at) => ships.has(at) ? v.incoming.includes(at) ? "X" : "S" : v.incoming.includes(at) ? "o" : ".";
|
|
12326
|
+
const row = (r, cell) => Array.from({ length: 10 }, (_, c) => cell(r * 10 + c)).join(" ");
|
|
12327
|
+
return {
|
|
12328
|
+
rows: 10,
|
|
12329
|
+
columns: 10,
|
|
12330
|
+
labeled: [
|
|
12331
|
+
` target grid your fleet`,
|
|
12332
|
+
` +0 1 2 3 4 5 6 7 8 9 +0 1 2 3 4 5 6 7 8 9`,
|
|
12333
|
+
...Array.from({ length: 10 }, (_, r) => `${String(r * 10).padStart(2)} ${row(r, target)} ${row(r, fleet)}`)
|
|
12334
|
+
],
|
|
12335
|
+
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."
|
|
12336
|
+
};
|
|
12337
|
+
}
|
|
12338
|
+
function backgammonBoard(v) {
|
|
12339
|
+
const cell = (at) => `${String(at).padStart(2)}:${v.points[at] ? `${Math.abs(v.points[at])}${v.points[at] > 0 ? "X" : "O"}` : "."}`.padEnd(6);
|
|
12340
|
+
return {
|
|
12341
|
+
rows: 2,
|
|
12342
|
+
columns: 12,
|
|
12343
|
+
labeled: [
|
|
12344
|
+
Array.from({ length: 12 }, (_, i) => cell(12 + i)).join(" ").trimEnd(),
|
|
12345
|
+
Array.from({ length: 12 }, (_, i) => cell(11 - i)).join(" ").trimEnd(),
|
|
12346
|
+
`bar: X ${v.bar[0]}, O ${v.bar[1]} \xB7 off: X ${v.off[0]}/15, O ${v.off[1]}/15`
|
|
12347
|
+
],
|
|
12348
|
+
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."
|
|
12349
|
+
};
|
|
12350
|
+
}
|
|
12252
12351
|
function fenBoard(fen) {
|
|
12253
12352
|
const ranks = fen.split(" ")[0].split("/").map((rank2) => rank2.replace(/\d/g, (n) => ".".repeat(Number(n))));
|
|
12254
12353
|
return {
|
|
@@ -12261,6 +12360,8 @@ function fenBoard(fen) {
|
|
|
12261
12360
|
function indexedBoard(game, state) {
|
|
12262
12361
|
const grid = grids[game], board = state?.board;
|
|
12263
12362
|
if (game === "mills" && Array.isArray(board) && board.length === 24) return millsBoard(board);
|
|
12363
|
+
if (game === "backgammon" && state && typeof state === "object" && "points" in state) return backgammonBoard(state);
|
|
12364
|
+
if (game === "battleships" && state && typeof state === "object" && "ownFleet" in state) return battleshipsBoard(state);
|
|
12264
12365
|
const fen = state?.fen;
|
|
12265
12366
|
if ((game === "atomic-chess" || game === "three-check") && typeof fen === "string") return fenBoard(fen);
|
|
12266
12367
|
if (!grid || !Array.isArray(board)) return null;
|
|
@@ -12322,12 +12423,40 @@ function agentPosition(room) {
|
|
|
12322
12423
|
if (match.kind === "hex" && match.state.moves === 1 && match.state.turn === 1) legal.push({ type: "swap" });
|
|
12323
12424
|
}
|
|
12324
12425
|
}
|
|
12325
|
-
const last = match?.kind === "connect-four" || match?.kind === "tic-tac-toe" || match?.kind === "gomoku" ? match.state.last : null;
|
|
12426
|
+
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;
|
|
12326
12427
|
const agreed = match?.kind === "go" && match.state.phase === "scoring" && match.state.accepted.includes(room.seat);
|
|
12327
12428
|
const yours = room.phase === "playing" && !!match && turn === room.seat && !agreed && !room.players[room.seat].left && !ended && !room.players.some((p) => p.paused);
|
|
12328
12429
|
const active = room.phase === "playing" && !!match && !room.players[room.seat].left && !ended && !room.players.some((p) => p.paused);
|
|
12329
12430
|
const grid = !complete && match ? indexedBoard(room.game, match.state) : null;
|
|
12330
12431
|
const wire = !complete && match && enumeratedAgentGames.includes(room.game) ? active ? agentLegalActions(room.game, match.state, room.seat, room.capacity) ?? [] : [] : null;
|
|
12432
|
+
const checkerLast = match?.kind === "checkers" || match?.kind === "brazilian-checkers" ? checkerLastMove(match.state.history) : null;
|
|
12433
|
+
const dotsLabeled = match?.kind === "dots-boxes" ? match.state.boxes.map((owner4, box) => {
|
|
12434
|
+
const { size, edges } = match.state, sides2 = boxEdges(size, box), drawn = sides2.filter((e) => edges[e] >= 0).length;
|
|
12435
|
+
const named = ["top", "bottom", "left", "right"].map((side2, i) => `${side2} ${sides2[i] + 1}${edges[sides2[i]] >= 0 ? "*" : ""}`).join(", ");
|
|
12436
|
+
return `box row ${Math.floor(box / size) + 1} col ${box % size + 1} [${owner4 < 0 ? "." : "XO"[owner4]}]: ${named}${owner4 < 0 && drawn === 3 ? " (three sides: takeable)" : ""}`;
|
|
12437
|
+
}) : null;
|
|
12438
|
+
const ginHand = match?.kind === "gin-rummy" && match.state.phase !== "finished" ? match.state.hands[room.seat]?.filter((c) => c !== null) ?? [] : [];
|
|
12439
|
+
const ginHint = ginHand.length ? (() => {
|
|
12440
|
+
const layout = ginLayout(ginHand), names = (cards) => cards.map((c) => `${cardLabel(c)} (id ${c})`);
|
|
12441
|
+
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) : [];
|
|
12442
|
+
return {
|
|
12443
|
+
deadwood: layout.value,
|
|
12444
|
+
melds: layout.melds.map(names),
|
|
12445
|
+
deadwood_cards: names(layout.deadwood),
|
|
12446
|
+
...discards.length ? { best_discard: `${cardLabel(discards[0].discard)} (id ${discards[0].discard})`, deadwood_after_best_discard: discards[0].deadwood } : {},
|
|
12447
|
+
knock_limit: 10
|
|
12448
|
+
};
|
|
12449
|
+
})() : null;
|
|
12450
|
+
const ruleNotes = match?.kind === "thousand" ? match.state.scores.flatMap((score, seat) => match.state.phase !== "finished" && score >= 800 ? [`Seat ${seat} (${room.players[seat]?.name ?? "empty"}) has ${score} points: from 800, defending points no longer score; only a made contract adds points.`] : []) : [];
|
|
12451
|
+
const sixtyUnseen = match?.kind === "sixty-six" && match.state.phase === "play" ? (() => {
|
|
12452
|
+
const s = match.state, seen = /* @__PURE__ */ new Set([...(s.hands[room.seat] ?? []).filter((c) => c !== null), ...s.played ?? [], ...s.trick.map((c) => c.card), ...s.upcard === null ? [] : [s.upcard]]);
|
|
12453
|
+
return Array.from({ length: 24 }, (_, i) => Math.floor(i / 6) * 13 + 7 + i % 6).filter((c) => !seen.has(c)).map((c) => `${cardLabel(c)} (id ${c})`);
|
|
12454
|
+
})() : null;
|
|
12455
|
+
const asianMove = match?.kind === "shogi" || match?.kind === "xiangqi" ? asianLastMove(match.state.history) : null;
|
|
12456
|
+
const asianLast = asianMove && (match?.kind === "shogi" || match?.kind === "xiangqi") ? { ...asianMove, by: match.state.history.at(-1).mover } : null;
|
|
12457
|
+
const millLast = match?.kind === "mills" ? millLastTurn(match.state) : null;
|
|
12458
|
+
const peg = match?.kind === "cribbage" && match.state.phase === "peg" ? match.state.lastPeg : void 0;
|
|
12459
|
+
const cribbageLast = peg ? { by_seat: peg.seat, ...peg.go ? { type: "go" } : { played: `${cribbageLabel(peg.card)} (id ${peg.card})` }, count: peg.count, ...peg.count === 31 || match?.kind === "cribbage" && match.state.total === 0 && peg.count > 0 ? { count_reset: true } : {} } : null;
|
|
12331
12460
|
return {
|
|
12332
12461
|
protocol_version: 1,
|
|
12333
12462
|
room: room.code,
|
|
@@ -12342,15 +12471,18 @@ function agentPosition(room) {
|
|
|
12342
12471
|
players: room.players.map((p, seat) => ({ seat, name: p.name, left: p.left, ...p.agent ? { agent: true } : {}, ...p.paused ? { paused: true } : {} })),
|
|
12343
12472
|
board_size: complete ? { rows, columns } : grid ? { rows: grid.rows, columns: grid.columns } : { rows: 0, columns: 0 },
|
|
12344
12473
|
board_top_to_bottom: board,
|
|
12345
|
-
board_labeled: labeledBoard(room.game, board) ?? grid?.labeled ?? null,
|
|
12346
|
-
...grid ? { board_legend: grid.legend } : {},
|
|
12474
|
+
board_labeled: dotsLabeled ?? (match?.kind === "oware" ? [...labeledBoard(room.game, board) ?? [], `captured: seat 0 ${match.state.scores[0]}, seat 1 ${match.state.scores[1]}`] : null) ?? labeledBoard(room.game, board) ?? grid?.labeled ?? null,
|
|
12475
|
+
...grid ? { board_legend: grid.legend } : dotsLabeled ? { board_legend: "One line per box: owner (X seat 0, O seat 1, . open), then its four edges as one-based edge numbers (--edge N); * marks a drawn edge. A box with three drawn edges can be taken now." } : {},
|
|
12347
12476
|
// Chess and its variants record the last move in SAN.
|
|
12348
|
-
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 },
|
|
12477
|
+
last_move: (match?.kind === "chess" || match?.kind === "atomic-chess" || match?.kind === "three-check") && match.state.last ? { san: match.state.last } : checkerLast ? { from: square(checkerLast.from), to: square(checkerLast.to) } : cribbageLast ? cribbageLast : match?.kind === "makao" && match.state.last ? { by_seat: match.state.last.seat, type: match.state.last.type, ...match.state.last.cards ? { cards: match.state.last.cards.map((c) => `${cardLabel(c)} (id ${c})`) } : {}, ...match.state.last.count !== void 0 && match.state.last.type !== "play" ? { count: match.state.last.count } : {}, ...match.state.last.announced ? { announced_makao: true } : {} } : asianLast ? { by_seat: asianLast.by, ...asianLast.from === null ? { type: "drop" } : { from: asianLast.from }, to: asianLast.to } : match?.kind === "oware" && match.state.last !== null ? { last_seed_in_pit: match.state.last + 1 } : millLast ? { by_seat: millLast.seat, ...millLast.from === null ? { type: "place" } : { type: "move", from: millLast.from }, at: millLast.to, ...millLast.captured.length ? { captured: millLast.captured } : {} } : last === null ? null : room.game === "connect-four" ? { column: last % columns + 1, row_from_top: Math.floor(last / columns) + 1 } : { row: Math.floor(last / columns) + 1, column: last % columns + 1 },
|
|
12349
12478
|
...room.game === "chess" ? { fen } : {},
|
|
12350
12479
|
// The side to move is in check (chess family, xiangqi, shogi).
|
|
12351
12480
|
...match && room.phase === "playing" && (match.kind === "chess" ? chessPosition(match.state).inCheck() : "check" in match.state && match.state.check === true) ? { in_check: true } : {},
|
|
12352
12481
|
visible_state: match?.state ?? null,
|
|
12353
12482
|
cards_labeled: match ? cardsLabeled(room.game, match.state) : null,
|
|
12483
|
+
...ginHint ? { gin_deadwood: ginHint } : {},
|
|
12484
|
+
...sixtyUnseen ? { sixty_six_unseen: sixtyUnseen } : {},
|
|
12485
|
+
...ruleNotes.length ? { rule_notes: ruleNotes } : {},
|
|
12354
12486
|
// Timed rounds (drawing, categories): agents cannot measure time, so give the remaining seconds.
|
|
12355
12487
|
...typeof match?.state?.deadline === "number" && room.serverNow ? { deadline_seconds_left: Math.max(0, Math.ceil((match.state.deadline - room.serverNow) / 1e3)) } : {},
|
|
12356
12488
|
action_types: action_catalog_default[room.game],
|
|
@@ -12358,7 +12490,8 @@ function agentPosition(room) {
|
|
|
12358
12490
|
legal_actions_complete: complete,
|
|
12359
12491
|
legal_actions: yours ? legal : [],
|
|
12360
12492
|
...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 } : {},
|
|
12361
|
-
|
|
12493
|
+
// Engine labels count players from 1; name the zero-based seat as everywhere else in the CLI.
|
|
12494
|
+
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,
|
|
12362
12495
|
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.",
|
|
12363
12496
|
terminal_reason: room.phase === "finished" ? room.players.some((p) => p.left) ? "resignation_or_leave" : "game_result" : null,
|
|
12364
12497
|
rematch_ready: room.rematch.includes(room.seat),
|
|
@@ -12367,13 +12500,14 @@ function agentPosition(room) {
|
|
|
12367
12500
|
}
|
|
12368
12501
|
function labeledBoard(game, board) {
|
|
12369
12502
|
if (!board?.length || game === "dots-boxes") return null;
|
|
12370
|
-
if (game === "oware") return [`seat 0 pits 1-6: ${board[0]}`, `seat 1 pits 7-12: ${board[1]}
|
|
12503
|
+
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."];
|
|
12371
12504
|
const algebraic = game === "chess" || game === "checkers";
|
|
12372
12505
|
const rowWidth = String(board.length).length, cell = algebraic ? 1 : String(board[0].length).length;
|
|
12373
12506
|
const files2 = algebraic ? [..."abcdefgh"] : [...board[0]].map((_, c) => String(c + 1));
|
|
12374
12507
|
return [
|
|
12375
12508
|
`${" ".repeat(rowWidth)} ${files2.map((f) => f.padStart(cell)).join(" ")}`,
|
|
12376
|
-
|
|
12509
|
+
// Hex rows shift right one step per row, so the six neighbours sit around each cell.
|
|
12510
|
+
...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(" ")}`)
|
|
12377
12511
|
];
|
|
12378
12512
|
}
|
|
12379
12513
|
function agentMove(position, input) {
|
|
@@ -12403,7 +12537,7 @@ function agentRules(game = "connect-four") {
|
|
|
12403
12537
|
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.",
|
|
12404
12538
|
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."
|
|
12405
12539
|
};
|
|
12406
|
-
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.`;
|
|
12540
|
+
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.`;
|
|
12407
12541
|
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 };
|
|
12408
12542
|
}
|
|
12409
12543
|
var connectFourAgentRules = agentRules();
|
|
@@ -12690,12 +12824,27 @@ function nextStep(position, event, brief = false, server) {
|
|
|
12690
12824
|
const play = (move2) => `leafarcade game play ${room} --revision ${revision} --action ${move2}`;
|
|
12691
12825
|
if (wire?.length && !position.turn?.yours) return `You may act now (phase action outside the normal turn): ${play("'<one legal_wire_actions entry>'")}${flag}. Otherwise wait: ${wait}`;
|
|
12692
12826
|
if (position.turn?.yours) {
|
|
12693
|
-
if (wire?.length === 1 && wire[0].type === "next" && Object.keys(wire[0]).length === 1) return `The hand is over
|
|
12827
|
+
if (wire?.length === 1 && wire[0].type === "next" && Object.keys(wire[0]).length === 1) return `The hand is over and you deal the next one: ${play(`'{"type":"next"}'`)} --wait${flag}`;
|
|
12694
12828
|
const partial = !wire && position.legal_wire_actions?.length ? "Some legal actions are listed in legal_wire_actions, but the list is not exhaustive (see game rules for melds, offers, amounts or card groups). " : "";
|
|
12695
12829
|
const move2 = wire ? "'<one legal_wire_actions entry>'" : position.legal_actions_complete ? "'<one legal_actions entry>'" : "'<wire action JSON from game rules>'";
|
|
12696
12830
|
const check = lineGames.includes(position.game) ? "Check your immediate win and your opponent's immediate threats (every line, including diagonals), then: " : "";
|
|
12697
|
-
|
|
12698
|
-
|
|
12831
|
+
const makao = position.game === "makao" ? ((state) => [
|
|
12832
|
+
state?.penalty ? `A ${state.penalty}-card penalty is active: answer with a matching attack card, or draw takes all ${state.penalty}. ` : "",
|
|
12833
|
+
state?.wait ? `A ${state.wait}-turn wait is active: answer with a 4, or draw serves the wait (no cards drawn). ` : "",
|
|
12834
|
+
state?.blocked?.[position.you.seat] ? `You are blocked for ${state.blocked[position.you.seat]} more turn(s). ` : "",
|
|
12835
|
+
state?.demand !== null && state?.demand !== void 0 ? `Jack demand in force: ${state.demand === 13 ? "king" : `rank ${state.demand}`}, or answer with a jack${state.demandTurns ? ` (${state.demandTurns} turn(s) left)` : ""}. ` : "",
|
|
12836
|
+
state?.suit !== null && state?.suit !== void 0 ? `Ace suit in force: ${["clubs", "diamonds", "hearts", "spades"][state.suit]}. ` : "",
|
|
12837
|
+
state?.phase === "drawn" && state.drawn !== null && state.drawn !== void 0 ? `You drew ${cardLabel(state.drawn)} (id ${state.drawn}): play it if it is listed, otherwise pass to keep it. ` : "",
|
|
12838
|
+
wire?.some((action) => action.type === "play" && Array.isArray(action.cards) && action.cards.length === (state?.hands?.[position.you.seat]?.length ?? 0) - 1) ? 'A play that leaves you one card must use its entry with "promotion":"makao", or the opponent can make you draw five. ' : ""
|
|
12839
|
+
].join(""))(position.visible_state) : "";
|
|
12840
|
+
const chain = position.visible_state?.chain;
|
|
12841
|
+
const jumper = (position.legal_actions?.[0] ?? wire?.[0])?.from;
|
|
12842
|
+
const capture = chain !== null && chain !== void 0 && jumper !== void 0 ? `Your capture continues: jump again with the piece on ${String(jumper)} (one jump per call). ` : "";
|
|
12843
|
+
const mill = position.game === "mills" && wire?.length && wire.every((action) => action.type === "capture") ? "You completed a mill: remove one opposing piece (only pieces outside mills, unless all are in mills). " : "";
|
|
12844
|
+
return `Your move. ${capture}${mill}${makao}${partial}${check}${play(move2)} --wait${flag}`;
|
|
12845
|
+
}
|
|
12846
|
+
const dealing = ["round", "show"].includes(String(position.visible_state?.phase));
|
|
12847
|
+
if (dealing && position.turn) return `The hand is over; seat ${position.turn.seat} (${position.players[position.turn.seat]?.name ?? "empty"}) deals the next one. Do not send next yourself. Wait: ${wait}`;
|
|
12699
12848
|
if (event === "waiting") return `No change yet. If the other players are still playing, wait again: ${wait}. After several silent waits, stop and tell the human how to resume.`;
|
|
12700
12849
|
const outOfTurn = position.legal_actions_complete || position.legal_wire_actions_complete ? "" : " Some phases allow out-of-turn actions; check game rules for this phase first.";
|
|
12701
12850
|
const mover = position.turn ? ` (seat ${position.turn.seat}, ${position.players[position.turn.seat]?.name ?? "empty"}, to act)` : "";
|
|
@@ -12786,7 +12935,7 @@ async function installSkill(agent = "codex", update = false) {
|
|
|
12786
12935
|
// packages/cli/package.json
|
|
12787
12936
|
var package_default = {
|
|
12788
12937
|
name: "@leafarcade/cli",
|
|
12789
|
-
version: "0.
|
|
12938
|
+
version: "0.9.0",
|
|
12790
12939
|
private: false,
|
|
12791
12940
|
description: "Create private browser board games and play with your AI agent. Includes 53 games and an installable Codex/Claude skill.",
|
|
12792
12941
|
type: "module",
|
|
@@ -13082,6 +13231,15 @@ function stateChanges(game, before, after, limit = 30) {
|
|
|
13082
13231
|
const here = path.join(".");
|
|
13083
13232
|
const primitive = (v) => v === null || typeof v !== "object";
|
|
13084
13233
|
if (Array.isArray(a) && Array.isArray(b)) {
|
|
13234
|
+
if (a.length && b.length && [...a, ...b].every((v) => typeof v === "string")) {
|
|
13235
|
+
for (let k = 0; k < a.length; k++) {
|
|
13236
|
+
const kept = a.slice(k);
|
|
13237
|
+
if (kept.every((v, i) => v === b[i]) && b.length > kept.length) {
|
|
13238
|
+
out2.push(`${here}: + ${b.slice(kept.length).map((v) => show(v)).join(", ")}`);
|
|
13239
|
+
return;
|
|
13240
|
+
}
|
|
13241
|
+
}
|
|
13242
|
+
}
|
|
13085
13243
|
if (a.length === b.length && a.length > 4 && a.every(primitive) && b.every(primitive)) {
|
|
13086
13244
|
a.forEach((v, i) => {
|
|
13087
13245
|
if (v !== b[i] && out2.length <= limit) out2.push(`${here}[${i}]: ${show(v)} \u2192 ${show(b[i])}`);
|
|
@@ -13137,7 +13295,14 @@ async function withChanges(result, path) {
|
|
|
13137
13295
|
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 }]) : [];
|
|
13138
13296
|
const square2 = (at) => "abcdefgh"[at % 8] + (8 - Math.floor(at / 8));
|
|
13139
13297
|
const derived = position.game === "checkers" ? cells.map((cell) => ({ ...cell, at: square2(cell.at) })) : "board_legend" in position ? cells : [];
|
|
13140
|
-
const
|
|
13298
|
+
const piles = [before.state, position.visible_state].map((state) => state?.discard);
|
|
13299
|
+
const top = position.game === "gin-rummy" && Array.isArray(piles[0]) && Array.isArray(piles[1]) ? piles[0].at(-1) : void 0;
|
|
13300
|
+
const taken = typeof top === "number" && !piles[1].includes(top) ? `${cardLabel(top)} (id ${top})` : null;
|
|
13301
|
+
const annotated = {
|
|
13302
|
+
...position,
|
|
13303
|
+
...position.last_move === null && derived.length && derived.length <= 12 ? { last_move: { board_changes: derived } } : {},
|
|
13304
|
+
changed_since_last_read: { from_revision: before.revision, ...taken ? { taken_from_discard: taken } : {}, changes }
|
|
13305
|
+
};
|
|
13141
13306
|
return "position" in result ? { ...result, position: annotated } : annotated;
|
|
13142
13307
|
}
|
|
13143
13308
|
|
|
@@ -13542,6 +13707,11 @@ var winner = (position) => {
|
|
|
13542
13707
|
return seat === void 0 || seat === null ? null : seat === position.you.seat ? "you" : seatName(position, seat);
|
|
13543
13708
|
};
|
|
13544
13709
|
var seatName = (position, seat) => `seat ${seat} (${position.players[seat]?.name ?? "empty"})`;
|
|
13710
|
+
var scores = (position) => {
|
|
13711
|
+
const value = position.visible_state?.scores;
|
|
13712
|
+
return Array.isArray(value) && value.length && value.every((n) => typeof n === "number") ? value : null;
|
|
13713
|
+
};
|
|
13714
|
+
var withoutLegal = (state) => Object.fromEntries(Object.entries(state).filter(([key2]) => key2 !== "legalActions"));
|
|
13545
13715
|
function briefPosition(position) {
|
|
13546
13716
|
const turn = position.turn ? position.turn.yours ? "yours" : seatName(position, position.turn.seat) : null;
|
|
13547
13717
|
const wire = "legal_wire_actions" in position ? position.legal_wire_actions ?? [] : null;
|
|
@@ -13554,6 +13724,8 @@ function briefPosition(position) {
|
|
|
13554
13724
|
you: position.you.piece,
|
|
13555
13725
|
turn,
|
|
13556
13726
|
...position.paused ? { paused: true } : {},
|
|
13727
|
+
// Match or capture scores by zero-based seat, when the game keeps them.
|
|
13728
|
+
...scores(position) ? { scores: scores(position) } : {},
|
|
13557
13729
|
..."in_check" in position ? { in_check: true } : {},
|
|
13558
13730
|
...position.you.left ? { left: true } : {},
|
|
13559
13731
|
...position.phase === "waiting" ? { seated: position.players.filter((player) => !player.left).length, capacity: position.capacity } : {},
|
|
@@ -13567,8 +13739,12 @@ function briefPosition(position) {
|
|
|
13567
13739
|
..."deadline_seconds_left" in position ? { deadline_seconds_left: position.deadline_seconds_left } : {},
|
|
13568
13740
|
..."board_legend" in position ? { board_legend: position.board_legend } : {},
|
|
13569
13741
|
// Only the classic convenience games are fully described by their labeled board.
|
|
13570
|
-
|
|
13742
|
+
// The legal moves are listed once at the top level, not again inside the state.
|
|
13743
|
+
...position.legal_actions_complete && position.board_labeled || !position.visible_state ? {} : { visible_state: withoutLegal(position.visible_state) },
|
|
13571
13744
|
...position.cards_labeled ? { cards_labeled: position.cards_labeled } : {},
|
|
13745
|
+
..."gin_deadwood" in position ? { gin_deadwood: position.gin_deadwood } : {},
|
|
13746
|
+
..."sixty_six_unseen" in position ? { sixty_six_unseen: position.sixty_six_unseen } : {},
|
|
13747
|
+
..."rule_notes" in position ? { rule_notes: position.rule_notes } : {},
|
|
13572
13748
|
...position.access_requests?.length ? { access_requests: position.access_requests } : {}
|
|
13573
13749
|
};
|
|
13574
13750
|
}
|
|
@@ -13718,7 +13894,7 @@ try {
|
|
|
13718
13894
|
result = { event: "browser_requested", room, url, browser: await openBrowser(url) };
|
|
13719
13895
|
} else if (command === "start" || command === "leave" || command === "resign" || command === "rematch" || command === "pause" || command === "resume") {
|
|
13720
13896
|
const revision = integer(values.revision, "revision", 0, Number.MAX_SAFE_INTEGER);
|
|
13721
|
-
if (revision !== view.revision) throw new ServiceError(
|
|
13897
|
+
if (revision !== view.revision) throw new ServiceError(`The room changed (now revision ${view.revision}). Read game status before changing the room.`, "STALE_REVISION");
|
|
13722
13898
|
if (command === "start") {
|
|
13723
13899
|
await rpc(session, "start", { revision }, false, controller.signal);
|
|
13724
13900
|
result = { event: "started", position: agentPosition(await readRoom(session, controller.signal)) };
|
|
@@ -13740,7 +13916,7 @@ try {
|
|
|
13740
13916
|
const revision = integer(values.revision, "revision", 0, Number.MAX_SAFE_INTEGER);
|
|
13741
13917
|
const column = values.column === void 0 ? void 0 : integer(values.column, "column", 1, position.board_size.columns);
|
|
13742
13918
|
const row = values.row === void 0 ? void 0 : integer(values.row, "row", 1, position.board_size.rows);
|
|
13743
|
-
if (revision !== position.revision) throw new ServiceError(
|
|
13919
|
+
if (revision !== position.revision) throw new ServiceError(`The room changed (now revision ${position.revision}). Read game status before playing.`, "STALE_REVISION");
|
|
13744
13920
|
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.");
|
|
13745
13921
|
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) });
|
|
13746
13922
|
const waitSeconds = integer(values.timeout ?? String(defaultWaitSeconds), "timeout", 1, 3600);
|