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:
1 parent
6d33440048
commit
66907345ff
13 files changed
+653
-42
No files matched your search
+32
-6
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user