Add optional card mode: server deals cards and settles showdowns automatically

- New "カードを配る" switch on the create-table screen (default off); the lobby
  shows "カードあり" for tables that use it
- Card mode (server/cards.js restored): crypto-shuffled deck kept server-side,
  hole cards sent only to their owner until showdown/all-in reveal, board dealt
  per street with burns, all-in runout on a timer (frozen while paused)
- Showdown is evaluated and every pot (incl. side pots, split pots) is paid out
  automatically; host winner selection is only used when card mode is off
- Play UI: board, hole cards, own cards in the action bar, hand names, winning
  cards highlighted; layout tuned for 7+ seats and short screens
- Playing-card styles use .pcard so they don't clash with the existing .card panel
- Tests for hand evaluation, card privacy, runout, side pots, ties and random play

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
agomizi1cmandClaude Opus 5.5 committed 2026-10-17 16:44:22 +09:00
1 parent 6d33440048
commit 66907345ff
13 files changed
+653 -42

No files matched your search

+32 -6
View File
@@ -14,6 +14,8 @@ class Table {
this.baseBigBlind = opts.bigBlind;
this.timerEnabled = opts.blindTimerEnabled;
this.intervalMin = opts.blindIntervalMin;
// カード機能: サーバがカードを配り、ショーダウンの勝敗も自動で判定する(オフなら実物のカードで遊ぶ)
this.cardsEnabled = opts.cardsEnabled === true;
this.hostToken = opts.hostToken || null;
this.maxSeats = config.MAX_SEATS;
@@ -28,6 +30,7 @@ class Table {
this.handNumber = 0;
this.buttonSeat = null;
this.nextHandAt = null;
this.runoutAt = null; // オールイン後、次の場札をめくる時刻
this.levelEndsAt = null;
this.levelRemainingMs = this.intervalMs;
@@ -139,7 +142,16 @@ class Table {
this.handNumber++;
const { sb, bb } = this.blinds;
this.nextHandAt = null;
this.hand = new Hand({ number: this.handNumber, seats: this.seats, order, button: this.buttonSeat, sb, bb });
this.runoutAt = null;
this.hand = new Hand({
number: this.handNumber,
seats: this.seats,
order,
button: this.buttonSeat,
sb,
bb,
cards: this.cardsEnabled,
});
}
// ---- プレイヤー操作(エラー時はメッセージを返す) ----
@@ -202,8 +214,9 @@ class Table {
if (!this.isHost(token)) return 'ホストのみ操作できます';
if (!this.started) return 'まだ開始していません';
if (this.paused) {
// 止めていた時間だけ次のハンドの開始を後ろにずらす
// 止めていた時間だけ次のハンドの開始・場札をめくる時刻を後ろにずらす
if (this.nextHandAt != null) this.nextHandAt += now - this.pausedAt;
if (this.runoutAt != null) this.runoutAt += now - this.pausedAt;
if (this.timerEnabled) this.levelEndsAt = now + this.levelRemainingMs;
this.paused = false;
this.pausedAt = null;
@@ -218,7 +231,7 @@ class Table {
return null;
}
// 時間で進む処理(ブラインドタイマー・次のハンドの開始)
// 時間で進む処理(ブラインドタイマー・オールイン後の場札・次のハンドの開始)
// 状態が変わったら true
tick(now) {
let changed = false;
@@ -229,9 +242,20 @@ class Table {
}
if (this.paused) return changed;
const h = this.hand;
if (h && h.runout) {
if (this.runoutAt == null) this.runoutAt = now + config.RUNOUT_STEP_MS;
else if (now >= this.runoutAt) {
h.runoutStep();
this.runoutAt = h.runout ? now + config.RUNOUT_STEP_MS : null;
changed = true;
}
}
if (this.hand && this.hand.finished) {
if (this.nextHandAt == null) {
this.nextHandAt = now + (this.hand.showdown ? config.SHOWDOWN_MS : config.FOLD_WIN_MS);
const showdownMs = this.cardsEnabled ? config.CARD_SHOWDOWN_MS : config.SHOWDOWN_MS;
this.nextHandAt = now + (this.hand.showdown ? showdownMs : config.FOLD_WIN_MS);
} else if (now >= this.nextHandAt) {
if (this.canDeal()) this.startNewHand();
else {
@@ -267,18 +291,19 @@ class Table {
paused: this.paused,
timerEnabled: this.timerEnabled,
intervalMin: this.intervalMin,
cardsEnabled: this.cardsEnabled,
createdAt: this.createdAt,
};
}
// token = 閲覧者のトークン(他人のトークンは送らない)
// token = 閲覧者のトークン(他人のトークンや手札は送らない)
publicState(token, connectedTokens) {
const now = Date.now();
const { sb, bb } = this.blinds;
const next = Blinds.blindsAt(this.baseSmallBlind, this.baseBigBlind, this.level + 1);
const mySeat = this.seatOf(token);
const h = this.hand;
const hv = h ? h.view() : null;
const hv = h ? h.view(mySeat, token) : null;
const me = mySeat >= 0 ? this.seats[mySeat] : null;
const inLiveHand = Boolean(this.handActive && h.players.has(mySeat) && !h.players.get(mySeat).folded);
@@ -287,6 +312,7 @@ class Table {
name: this.name,
startingStack: this.startingStack,
maxSeats: this.maxSeats,
cardsEnabled: this.cardsEnabled,
level: this.level,
smallBlind: sb,
bigBlind: bb,