Wait for the host to press "next hand" on every table
Previously only the review CPU table paused after each hand; cash-only, friends and CPU tables moved on automatically after a short delay. Now every table keeps the result on screen until the host presses 次のハンドへ, and the next hand is dealt right away. If the host has left, the new host presses it; with no human seated the table still advances on its own. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
22ee28ba03
commit
f64c502086
8 files changed
+103
-31
No files matched your search
@@ -16,7 +16,7 @@
|
||||
| レビュー付きCPU戦 | 管理者だけが作れる。作成者と CPU だけで遊び(友達は入れない・ロビーにも出ない)、ハンドごとにレビューが出る(下の「管理者」を参照) |
|
||||
|
||||
- ポーカーのルール(ブラインド・手番・最低レイズ額・再レイズ権・返金・サイドポット・ショーダウンの判定と分配)と役の判定は、公開ライブラリ [@pokertools/engine](https://www.npmjs.com/package/@pokertools/engine) / [@pokertools/evaluator](https://www.npmjs.com/package/@pokertools/evaluator)(MIT、バージョン固定)に任せている。
|
||||
- ノーリミット・テキサスホールデムのリングゲーム(最大9人)。ホストが START を押すと、以降は2人以上いる限り自動でハンドが続く。
|
||||
- ノーリミット・テキサスホールデムのリングゲーム(最大9人)。ホストが START を押すとハンドが始まる。ハンドが終わると結果を表示したまま止まり、ホストが「次のハンドへ」を押すと次のハンドが配られる(ホストが退席したら次のホストが押す。人間が誰も着席していなければ待たずに進む)。
|
||||
- 画面にディーラーボタン(D)・SB・BB と、いまのストリート(PREFLOP / FLOP / TURN / RIVER)が出るので、それに合わせて実物のカードを配る。
|
||||
- ブラインドは自動で支払われ、ハンド開始時のレベルで固定。BB は SB より大きくする。ヘッズアップはボタンが SB。
|
||||
- 各自が自分のスマホで Fold / Check / Call / Bet / Raise を行う。最低レイズ額は直前のレイズ幅以上。足りないオールインのレイズは、すでにアクションした人に再レイズ権を与えない。
|
||||
|
||||
+16
-8
@@ -464,9 +464,12 @@
|
||||
let status = '';
|
||||
if (!s.started) status = s.isHost ? '2人以上揃ったら START を押してください' : 'ホストの開始を待っています';
|
||||
else if (s.paused) status = '一時停止中';
|
||||
else if (s.review && s.review.waiting) status = 'レビューを確認したら「次のハンドへ」を押してください';
|
||||
else if (s.awaitingNext && s.isHost) {
|
||||
status = s.review ? 'レビューを確認したら「次のハンドへ」を押してください' : '「次のハンドへ」を押すと次のハンドを始めます';
|
||||
}
|
||||
else if (m.sittingOut) status = '離席中です。復帰すると次のハンドから参加します';
|
||||
else if (m.canRebuy) status = 'チップがなくなりました';
|
||||
else if (s.awaitingNext) status = 'ホストが次のハンドを始めるのを待っています';
|
||||
else if (o) status = o.canCheck ? 'あなたの番です' : `あなたの番です(コール ${chips(o.toCall)})`;
|
||||
else if (awaiting) status = m.canAward ? 'カードを見せ合って、勝った人を選んでください' : 'ホストが勝者を選んでいます';
|
||||
else if (h && !h.finished && s.toAct != null) {
|
||||
@@ -488,15 +491,19 @@
|
||||
if (awarding) renderAwardPanel(h.pots);
|
||||
|
||||
// ボタン
|
||||
const reviewWaiting = Boolean(s.review && s.review.waiting);
|
||||
const alt = (m.sittingOut || m.canRebuy) && !awarding && !reviewWaiting;
|
||||
// ハンドが終わったら、ホストが「次のハンドへ」を押すまで待つ(レビュー付きCPU戦はレビューも開ける)
|
||||
const nexting = Boolean(s.awaitingNext && s.isHost && !s.paused);
|
||||
const alt = (m.sittingOut || m.canRebuy) && !awarding;
|
||||
if (!o) raising = false;
|
||||
$('review-actions').hidden = !reviewWaiting;
|
||||
$('next-actions').hidden = !nexting;
|
||||
$('next-actions').classList.toggle('raising', Boolean(s.review));
|
||||
$('next-actions').classList.toggle('single', !s.review);
|
||||
$('review-show').hidden = !s.review;
|
||||
$('alt-actions').hidden = !alt;
|
||||
$('sit-in').hidden = !m.sittingOut;
|
||||
$('rebuy').hidden = !m.canRebuy;
|
||||
$('rebuy').textContent = `リバイ(${chips(s.startingStack)})`;
|
||||
$('actions').hidden = alt || awarding || reviewWaiting;
|
||||
$('actions').hidden = alt || awarding || nexting;
|
||||
if (raising && o && !alt) {
|
||||
renderRaisePanel(o);
|
||||
betSheet.open();
|
||||
@@ -593,7 +600,8 @@
|
||||
// 同じタブで開き、レビューの画面の「テーブル」で戻ってこられるようにテーブルIDを渡す
|
||||
const back = `t=${encodeURIComponent(tableId)}`;
|
||||
$('review-link').href = r.sessionId ? `/review?s=${encodeURIComponent(r.sessionId)}&${back}` : `/review?${back}`;
|
||||
$('review-modal-next').hidden = !r.waiting;
|
||||
const waiting = Boolean(s.awaitingNext && s.isHost);
|
||||
$('review-modal-next').hidden = !waiting;
|
||||
const key = JSON.stringify(r);
|
||||
if (key !== reviewKey) {
|
||||
reviewKey = key;
|
||||
@@ -602,7 +610,7 @@
|
||||
// 新しいハンドのレビューが届いたら自動で開く
|
||||
if (r.last.number !== seenReview) {
|
||||
seenReview = r.last.number;
|
||||
if (r.waiting) reviewSheet.open();
|
||||
if (waiting) reviewSheet.open();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -626,7 +634,7 @@
|
||||
emit('table:next');
|
||||
};
|
||||
$('review-copy').addEventListener('click', () => copyHandPrompt(state && state.review));
|
||||
$('review-next').addEventListener('click', nextHand);
|
||||
$('next-hand').addEventListener('click', nextHand);
|
||||
$('review-modal-next').addEventListener('click', nextHand);
|
||||
|
||||
// ---------- ランキング(スタック順・収支順を切り替える) ----------
|
||||
|
||||
+3
-3
@@ -96,9 +96,9 @@
|
||||
<button class="btn act-call" id="act-call" disabled>Check</button>
|
||||
<button class="btn act-raise" id="act-raise" disabled>Raise</button>
|
||||
</div>
|
||||
<div class="actions raising" id="review-actions" hidden>
|
||||
<button class="btn" id="review-show">レビュー</button>
|
||||
<button class="btn btn-tint" id="review-next">次のハンドへ</button>
|
||||
<div class="actions single" id="next-actions" hidden>
|
||||
<button class="btn" id="review-show" hidden>レビュー</button>
|
||||
<button class="btn btn-tint" id="next-hand">次のハンドへ</button>
|
||||
</div>
|
||||
<div class="actions single" id="alt-actions" hidden>
|
||||
<button class="btn btn-tint" id="sit-in">復帰する</button>
|
||||
|
||||
+1
-1
@@ -349,7 +349,7 @@ io.on('connection', (socket) => {
|
||||
'table:sitOut': (t, token) => t.setSittingOut(token, true),
|
||||
'table:sitIn': (t, token) => t.setSittingOut(token, false),
|
||||
'table:rebuy': (t, token) => t.rebuy(token),
|
||||
'table:next': (t, token) => t.reviewNext(token),
|
||||
'table:next': (t, token) => t.nextHand(token),
|
||||
'table:seating': (t, token, p) => {
|
||||
// order: 時計回りに並べた席番号
|
||||
const order = p && typeof p === 'object' ? p.order : null;
|
||||
|
||||
+23
-14
@@ -42,8 +42,8 @@ class Table {
|
||||
this.pendingSeating = null; // ホストが決めた席順(時計回りのトークン)。ハンド中は次のハンドの始めに並べ替える
|
||||
this.withBots = this.mode === 'cpu' || this.mode === 'review';
|
||||
if (this.withBots) this.seatBots(opts.cpuCount);
|
||||
// レビュー付きCPU戦: session = 保存する対局、last = 直前のハンドのレビュー、waiting = 次のハンドへ進むのを待っている
|
||||
this.review = this.mode === 'review' ? { session: null, last: null, waiting: false } : null;
|
||||
// レビュー付きCPU戦: session = 保存する対局、last = 直前のハンドのレビュー
|
||||
this.review = this.mode === 'review' ? { session: null, last: null } : null;
|
||||
this.hand = null;
|
||||
this.started = false;
|
||||
this.paused = false;
|
||||
@@ -119,6 +119,24 @@ class Table {
|
||||
.filter((i) => i >= 0);
|
||||
}
|
||||
|
||||
// ハンドが終わったら、ホストが「次のハンドへ」を押すまで次のハンドを始めない
|
||||
// (ホストが着席していなければ待たずに進める)
|
||||
get awaitingNext() {
|
||||
const h = this.hand;
|
||||
if (!h || !h.finished || h.nextApproved) return false;
|
||||
const host = this.seatOf(this.hostToken);
|
||||
return host >= 0 && !this.seats[host].left;
|
||||
}
|
||||
|
||||
nextHand(token, now = Date.now()) {
|
||||
if (!this.isHost(token)) return 'ホストのみ操作できます';
|
||||
if (this.paused) return '一時停止中です';
|
||||
if (!this.awaitingNext) return 'ハンドが終わっていません';
|
||||
this.hand.nextApproved = true;
|
||||
this.nextHandAt = now; // 待たずに次の tick で始める
|
||||
return null;
|
||||
}
|
||||
|
||||
// CPU だけでハンドを続けても意味がないので、人間が1人は参加している必要がある
|
||||
canDeal() {
|
||||
const eligible = this.eligibleSeats();
|
||||
@@ -201,7 +219,6 @@ class Table {
|
||||
order.push(i);
|
||||
}
|
||||
this.handNumber++;
|
||||
if (this.review) this.review.waiting = false;
|
||||
const { sb, bb } = this.blinds;
|
||||
this.nextHandAt = null;
|
||||
this.runoutAt = null;
|
||||
@@ -388,7 +405,7 @@ class Table {
|
||||
this.reviewHand();
|
||||
changed = true;
|
||||
}
|
||||
if (this.hand && this.hand.finished && this.review && this.review.waiting) return changed;
|
||||
if (this.awaitingNext) return changed;
|
||||
|
||||
if (this.hand && this.hand.finished) {
|
||||
if (this.nextHandAt == null) {
|
||||
@@ -436,7 +453,7 @@ class Table {
|
||||
|
||||
// ---- レビュー付きCPU戦 ----
|
||||
|
||||
// 終わったハンドのレビューを作って保存し、作成者が見終わるまで次のハンドを待つ
|
||||
// 終わったハンドのレビューを作って保存する(作成者が見終わって「次のハンドへ」を押すまで次のハンドは始まらない)
|
||||
reviewHand() {
|
||||
const h = this.hand;
|
||||
h.reviewed = true;
|
||||
@@ -458,14 +475,6 @@ class Table {
|
||||
}
|
||||
reviews.addHand(this.review.session, record);
|
||||
this.review.last = record;
|
||||
this.review.waiting = !this.seats[hero].left;
|
||||
}
|
||||
|
||||
reviewNext(token) {
|
||||
if (!this.review) return 'レビュー付きのテーブルではありません';
|
||||
if (!this.isHost(token)) return '操作できません';
|
||||
this.review.waiting = false;
|
||||
return null;
|
||||
}
|
||||
|
||||
// 作成者に送るレビュー(直前のハンドの要約)
|
||||
@@ -476,7 +485,6 @@ class Table {
|
||||
const scores = hands.map((x) => x.handScore).filter((x) => x != null);
|
||||
return {
|
||||
sessionId: r.session ? r.session.id : null,
|
||||
waiting: r.waiting,
|
||||
hands: hands.length,
|
||||
average: scores.length ? Math.round(scores.reduce((a, b) => a + b, 0) / scores.length) : null,
|
||||
last: last && {
|
||||
@@ -565,6 +573,7 @@ class Table {
|
||||
phase: this.phase,
|
||||
started: this.started,
|
||||
paused: this.paused,
|
||||
awaitingNext: this.awaitingNext,
|
||||
handNumber: this.handNumber,
|
||||
hand: hv,
|
||||
toAct: this.handActive ? h.toAct : null,
|
||||
|
||||
+54
-1
@@ -232,6 +232,7 @@ test('離席中の人の手番は自動でチェック/フォールド、次の
|
||||
assert.notEqual(h.toAct, first);
|
||||
t.act(tk(t, h.toAct), 'fold', 0); // 残り1人で終了
|
||||
assert.ok(h.finished);
|
||||
assert.equal(t.nextHand(T(0), 0), null);
|
||||
t.tick(10 ** 6);
|
||||
t.tick(10 ** 7);
|
||||
assert.equal(t.hand.number, 2);
|
||||
@@ -253,15 +254,60 @@ test('一時停止中はアクションも勝者の決定も次のハンドも
|
||||
t.togglePause(T(0), 4000);
|
||||
assert.equal(t.award(T(0), [[h.order[0]]]), null);
|
||||
t.togglePause(T(0), 5000);
|
||||
assert.equal(t.nextHand(T(0), 5000), '一時停止中です');
|
||||
t.tick(10 ** 7);
|
||||
t.tick(10 ** 8);
|
||||
assert.equal(t.hand.number, 1); // 止まっている
|
||||
t.togglePause(T(0), 10 ** 8);
|
||||
assert.equal(t.nextHand(T(0), 10 ** 8), null);
|
||||
t.tick(10 ** 8 + 10 ** 5);
|
||||
t.tick(10 ** 8 + 10 ** 6);
|
||||
assert.equal(t.hand.number, 2);
|
||||
});
|
||||
|
||||
test('次のハンドはホストが「次のハンドへ」を押すまで始まらない', () => {
|
||||
for (const mode of ['chips', 'friends']) {
|
||||
const t = makeTable([1000, 1000, 1000], { mode });
|
||||
t.start(T(0), 0);
|
||||
assert.equal(t.nextHand(T(0), 0), 'ハンドが終わっていません');
|
||||
const h = t.hand;
|
||||
for (let i = 0; i < 10 && !h.finished; i++) t.act(tk(t, h.toAct), 'fold', 0);
|
||||
assert.ok(h.finished);
|
||||
assert.equal(t.publicState(T(1), new Set()).awaitingNext, true);
|
||||
for (let k = 1; k <= 5; k++) t.tick(k * 10 ** 6);
|
||||
assert.equal(t.hand, h); // 待っている
|
||||
assert.equal(t.nextHand(T(1), 10 ** 7), 'ホストのみ操作できます');
|
||||
assert.equal(t.nextHand(T(0), 10 ** 7), null);
|
||||
assert.equal(t.awaitingNext, false);
|
||||
t.tick(10 ** 7);
|
||||
assert.equal(t.hand.number, 2);
|
||||
}
|
||||
});
|
||||
|
||||
test('ホストが退席したら、次のホストが押す。人間がいなければ待たずに進む', () => {
|
||||
const t = makeTable([1000, 1000, 1000], { mode: 'friends' });
|
||||
t.start(T(0), 0);
|
||||
const h = t.hand;
|
||||
for (let i = 0; i < 10 && !h.finished; i++) t.act(tk(t, h.toAct), 'fold', 0);
|
||||
assert.ok(t.leave(T(0)));
|
||||
assert.ok(t.awaitingNext);
|
||||
assert.equal(t.nextHand(T(0), 0), 'ホストのみ操作できます');
|
||||
assert.ok(t.isHost(T(1)));
|
||||
assert.equal(t.nextHand(T(1), 0), null);
|
||||
t.tick(1);
|
||||
assert.equal(t.hand.number, 2);
|
||||
|
||||
// CPU戦で人間が退席すると、ボタンを待たずに片付けへ進む
|
||||
const c = makeTable([1000], { mode: 'cpu', cpuCount: 2 });
|
||||
c.start(T(0), 0);
|
||||
const ch = c.hand;
|
||||
c.leave(T(0));
|
||||
let now = 0;
|
||||
for (let k = 0; k < 400 && !ch.finished; k++) c.tick((now += 5000));
|
||||
assert.ok(ch.finished);
|
||||
assert.equal(c.awaitingNext, false);
|
||||
});
|
||||
|
||||
test('ハンド中の退席: フォールド扱いで席はハンド終了まで予約', () => {
|
||||
const t = makeTable([1000, 1000, 1000]);
|
||||
t.start(T(0), 0);
|
||||
@@ -380,6 +426,7 @@ test('ランダムに遊んでもチップの合計が変わらず、必ず終
|
||||
});
|
||||
assert.equal(t.award(T(0), winners), null);
|
||||
} else {
|
||||
if (t.awaitingNext) assert.equal(t.nextHand(T(0), now), null);
|
||||
t.tick((now += 10 ** 5));
|
||||
}
|
||||
for (const s of t.seats) if (s) assert.ok(s.stack >= 0);
|
||||
@@ -623,6 +670,7 @@ test('カードあり: ランダムに遊んでもチップの合計が変わら
|
||||
} else {
|
||||
assert.equal(h.awaitingWinners, false);
|
||||
if (h.runout && t.runoutAt == null) runouts++;
|
||||
if (t.awaitingNext) assert.equal(t.nextHand(T(0), now), null);
|
||||
t.tick((now += 10 ** 5));
|
||||
}
|
||||
for (const s of t.seats) if (s) assert.ok(s.stack >= 0);
|
||||
@@ -703,6 +751,7 @@ test('CPU: どんな状況でも正しいアクションを返す', () => {
|
||||
assert.equal(h.act(h.toAct, d.type, d.amount), null, JSON.stringify(d));
|
||||
actions++;
|
||||
} else {
|
||||
if (t.awaitingNext) assert.equal(t.nextHand(T(0), now), null);
|
||||
t.tick((now += 10 ** 5));
|
||||
}
|
||||
const inPot = t.hand && !t.hand.finished ? t.hand.potTotal : 0;
|
||||
@@ -736,6 +785,7 @@ test('CPU: どの思考(ライブラリ)も自分で正しいアクション
|
||||
assert.equal(h.act(h.toAct, d.type, d.amount), null, `${brain} ${JSON.stringify(d)}`);
|
||||
decisions++;
|
||||
} else {
|
||||
if (t.awaitingNext) assert.equal(t.nextHand(T(0), now), null);
|
||||
t.tick((now += 10 ** 5));
|
||||
}
|
||||
}
|
||||
@@ -818,7 +868,10 @@ test('CPU戦: CPU が席に着き、ホストは人間のまま、CPU は自動
|
||||
|
||||
// 人間が離席すると CPU だけで次のハンドは始まらない
|
||||
t.setSittingOut(T(0), true);
|
||||
for (let k = 0; k < 400 && t.hand; k++) t.tick((now += 5000));
|
||||
for (let k = 0; k < 400 && t.hand; k++) {
|
||||
if (t.awaitingNext) assert.equal(t.nextHand(T(0), now), null);
|
||||
t.tick((now += 5000));
|
||||
}
|
||||
assert.equal(t.hand, null);
|
||||
t.setSittingOut(T(0), false);
|
||||
|
||||
|
||||
+3
-3
@@ -342,7 +342,7 @@ test('レビュー付きCPU戦: 作成者だけが座れ、ハンドごとにレ
|
||||
assert.ok(t.hand.finished);
|
||||
t.tick((now += 5000));
|
||||
const v = t.publicState(T(0), new Set([T(0)]));
|
||||
assert.ok(v.review && v.review.last && v.review.waiting);
|
||||
assert.ok(v.review && v.review.last && v.awaitingNext);
|
||||
assert.equal(v.review.hands, 1);
|
||||
assert.equal(t.publicState(T(5), new Set()).review, null); // 作成者以外には送らない
|
||||
|
||||
@@ -350,8 +350,8 @@ test('レビュー付きCPU戦: 作成者だけが座れ、ハンドごとにレ
|
||||
const number = t.hand.number;
|
||||
for (let k = 0; k < 10; k++) t.tick((now += 60000));
|
||||
assert.equal(t.hand.number, number);
|
||||
assert.equal(t.reviewNext(T(5)), '操作できません');
|
||||
assert.equal(t.reviewNext(T(0)), null);
|
||||
assert.equal(t.nextHand(T(5), now), 'ホストのみ操作できます');
|
||||
assert.equal(t.nextHand(T(0), now), null);
|
||||
for (let k = 0; k < 10 && t.hand.number === number; k++) t.tick((now += 5000));
|
||||
assert.equal(t.hand.number, number + 1);
|
||||
|
||||
|
||||
@@ -61,6 +61,7 @@ test('席順: ハンド中は次のハンドの始めに並べ替え、ボタン
|
||||
// 全員降りてハンドを終わらせ、次のハンドへ
|
||||
for (let i = 0; i < 10 && !h.finished; i++) t.act(t.seats[h.toAct].token, 'fold', 0);
|
||||
assert.ok(h.finished);
|
||||
assert.equal(t.nextHand(T(0), 0), null);
|
||||
t.tick(10 ** 6);
|
||||
t.tick(10 ** 7);
|
||||
assert.equal(t.hand.number, 2);
|
||||
@@ -84,6 +85,7 @@ test('席順: 決めた後に座った人は最後に加え、いなくなった
|
||||
t.join(T(5), 'p5'); // ハンド中に新しく座る
|
||||
t.leave(T(1)); // ハンド中に退席
|
||||
for (let i = 0; i < 10 && !h.finished; i++) t.act(t.seats[h.toAct].token, 'fold', 0);
|
||||
assert.equal(t.nextHand(T(0), 0), null);
|
||||
t.tick(10 ** 6);
|
||||
t.tick(10 ** 7);
|
||||
assert.deepEqual(names(t), ['p2', 'p0', 'p5']);
|
||||
|
||||
Reference in new issue
Block a user