diff --git a/README.md b/README.md
index f8d6645..999fd17 100644
--- a/README.md
+++ b/README.md
@@ -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 を行う。最低レイズ額は直前のレイズ幅以上。足りないオールインのレイズは、すでにアクションした人に再レイズ権を与えない。
diff --git a/public/assets/table.js b/public/assets/table.js
index aca6ad0..f9ff01a 100644
--- a/public/assets/table.js
+++ b/public/assets/table.js
@@ -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);
// ---------- ランキング(スタック順・収支順を切り替える) ----------
diff --git a/public/table.html b/public/table.html
index c021c74..27fcfe9 100644
--- a/public/table.html
+++ b/public/table.html
@@ -96,9 +96,9 @@
-
-
-
+
+
+
diff --git a/server/index.js b/server/index.js
index f84cbc0..a30363d 100644
--- a/server/index.js
+++ b/server/index.js
@@ -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;
diff --git a/server/tables.js b/server/tables.js
index d8025a8..fab9142 100644
--- a/server/tables.js
+++ b/server/tables.js
@@ -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,
diff --git a/test/engine.test.js b/test/engine.test.js
index 82f7eef..b215c7f 100644
--- a/test/engine.test.js
+++ b/test/engine.test.js
@@ -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);
diff --git a/test/review.test.js b/test/review.test.js
index c4a9bbb..c978e47 100644
--- a/test/review.test.js
+++ b/test/review.test.js
@@ -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);
diff --git a/test/seating.test.js b/test/seating.test.js
index bf00a80..bff4f69 100644
--- a/test/seating.test.js
+++ b/test/seating.test.js
@@ -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']);