Only the table host splits the pot; host buttons beside the table name
- Showdown winners can now be chosen only by the table host (others see "ホストが勝者を選んでいます"); if the host leaves, the next host does it - START / pause moved into the header to the right of the table name (pause is an icon button), freeing the row they used to occupy Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e89d867f85
commit
6d33440048
6 files changed
+50
-34
No files matched your search
@@ -13,7 +13,7 @@
|
|||||||
- ブラインドは自動で支払われ、ハンド開始時のレベルで固定。ヘッズアップはボタンが SB。
|
- ブラインドは自動で支払われ、ハンド開始時のレベルで固定。ヘッズアップはボタンが SB。
|
||||||
- 各自が自分のスマホで Fold / Check / Call / Bet / Raise を行う。最低レイズ額は直前のレイズ幅以上。足りないオールインのレイズは、すでにアクションした人に再レイズ権を与えない。
|
- 各自が自分のスマホで Fold / Check / Call / Bet / Raise を行う。最低レイズ額は直前のレイズ幅以上。足りないオールインのレイズは、すでにアクションした人に再レイズ権を与えない。
|
||||||
- ベッティングが終わると次のストリートへ。全員オールインならその時点でショーダウンになる(残りのカードは実物でめくる)。
|
- ベッティングが終わると次のストリートへ。全員オールインならその時点でショーダウンになる(残りのカードは実物でめくる)。
|
||||||
- **ショーダウン**: カードを見せ合い、勝った人を画面で選んで確定する(選べるのはショーダウンに残った人とホスト)。サイドポットがあるときはポットごとに選ぶ。複数選ぶと山分け(端数はボタンの左から)。
|
- **ショーダウン**: カードを見せ合い、ホストが勝った人を画面で選んで確定する(ポットの分配はテーブルのホストだけが行える)。サイドポットがあるときはポットごとに選ぶ。複数選ぶと山分け(端数はボタンの左から)。
|
||||||
- 全員フォールドしたら残った人が自動でポットを獲得。誰もコールしなかった分は自動で返る。
|
- 全員フォールドしたら残った人が自動でポットを獲得。誰もコールしなかった分は自動で返る。
|
||||||
- 「離席」にすると次のハンドから配られない(手番が来たら自動でチェック/フォールド)。「復帰する」で戻る。
|
- 「離席」にすると次のハンドから配られない(手番が来たら自動でチェック/フォールド)。「復帰する」で戻る。
|
||||||
- チップがなくなったら開始スタックでリバイできる。画面下に収支(現在のチップ − バイイン合計)を表示。
|
- チップがなくなったら開始スタックでリバイできる。画面下に収支(現在のチップ − バイイン合計)を表示。
|
||||||
@@ -80,7 +80,7 @@ http://localhost:4671 を開く。
|
|||||||
|
|
||||||
- テーブルはメモリ上にのみ保持(サーバ再起動で消える)。誰も接続していない状態が30分続くと自動削除。
|
- テーブルはメモリ上にのみ保持(サーバ再起動で消える)。誰も接続していない状態が30分続くと自動削除。
|
||||||
- 名前とプレイヤーID はブラウザの localStorage に保存。リロードしても同じ席に戻れる。
|
- 名前とプレイヤーID はブラウザの localStorage に保存。リロードしても同じ席に戻れる。
|
||||||
- 作成者がホスト(START・一時停止ができる)。ホストが退席すると次の着席者に移る。
|
- 作成者がホスト(START・一時停止・ショーダウン後のポットの分配ができる。ボタンはテーブル名の右に出る)。ホストが退席すると次の着席者に移る。
|
||||||
|
|
||||||
## VPS へのデプロイ例(Ubuntu + nginx + systemd + HTTPS)
|
## VPS へのデプロイ例(Ubuntu + nginx + systemd + HTTPS)
|
||||||
|
|
||||||
|
|||||||
@@ -304,10 +304,10 @@ body.table-page {
|
|||||||
.table-page input { -webkit-user-select: text; user-select: text; }
|
.table-page input { -webkit-user-select: text; user-select: text; }
|
||||||
.table-page .wrap { padding-top: 12px; padding-bottom: 0; flex: 1; min-height: 0; display: flex; flex-direction: column; width: 100%; }
|
.table-page .wrap { padding-top: 12px; padding-bottom: 0; flex: 1; min-height: 0; display: flex; flex-direction: column; width: 100%; }
|
||||||
|
|
||||||
.table-header { display: flex; align-items: center; gap: 12px; }
|
.table-header { display: flex; align-items: center; gap: 10px; margin-bottom: 12px; }
|
||||||
.table-header h1 {
|
.table-header h1 {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
text-align: center;
|
min-width: 0;
|
||||||
font-size: 22px;
|
font-size: 22px;
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
@@ -316,8 +316,10 @@ body.table-page {
|
|||||||
}
|
}
|
||||||
.icon-btn.leave { color: var(--red); border-color: rgba(255, 90, 90, 0.35); }
|
.icon-btn.leave { color: var(--red); border-color: rgba(255, 90, 90, 0.35); }
|
||||||
|
|
||||||
.host-controls { display: flex; justify-content: center; gap: 8px; margin: 16px 0 12px; flex-wrap: wrap; }
|
/* ホスト操作(テーブル名の右) */
|
||||||
.host-controls .btn { border-radius: 999px; padding: 10px 16px; font-size: 14px; }
|
.host-controls { display: flex; align-items: center; gap: 8px; flex: none; }
|
||||||
|
.host-controls .btn { border-radius: 999px; padding: 9px 14px; font-size: 13px; }
|
||||||
|
.host-controls .icon-btn.paused { color: var(--green); border-color: rgba(46, 204, 113, 0.5); }
|
||||||
|
|
||||||
.level-bar {
|
.level-bar {
|
||||||
background: var(--panel);
|
background: var(--panel);
|
||||||
@@ -643,9 +645,8 @@ body.table-page {
|
|||||||
.table-page .wrap { padding-top: 8px; }
|
.table-page .wrap { padding-top: 8px; }
|
||||||
.table-header .icon-btn { width: 38px; height: 38px; font-size: 16px; }
|
.table-header .icon-btn { width: 38px; height: 38px; font-size: 16px; }
|
||||||
.table-header h1 { font-size: 18px; }
|
.table-header h1 { font-size: 18px; }
|
||||||
.host-controls { margin: 8px 0 6px; }
|
.table-header { margin-bottom: 8px; }
|
||||||
.host-controls .btn { padding: 7px 14px; }
|
.host-controls .btn { padding: 7px 12px; }
|
||||||
.host-spacer { height: 6px; }
|
|
||||||
.level-bar { padding: 8px 14px; }
|
.level-bar { padding: 8px 14px; }
|
||||||
.level-sub { display: none; }
|
.level-sub { display: none; }
|
||||||
.felt-area { margin: 40px 0 34px; }
|
.felt-area { margin: 40px 0 34px; }
|
||||||
@@ -677,4 +678,3 @@ body.table-page {
|
|||||||
.slash { flex: none !important; color: var(--muted); font-weight: 800; }
|
.slash { flex: none !important; color: var(--muted); font-weight: 800; }
|
||||||
label.switch { margin: 0; display: inline-block; }
|
label.switch { margin: 0; display: inline-block; }
|
||||||
.link-gold { color: var(--gold); }
|
.link-gold { color: var(--gold); }
|
||||||
.host-spacer { height: 12px; }
|
|
||||||
@@ -121,13 +121,15 @@
|
|||||||
|
|
||||||
// ホスト操作
|
// ホスト操作
|
||||||
$('host-controls').hidden = !s.isHost;
|
$('host-controls').hidden = !s.isHost;
|
||||||
$('host-spacer').hidden = s.isHost;
|
|
||||||
const eligible = s.seats.filter((p) => p && !p.sittingOut && p.stack > 0).length;
|
const eligible = s.seats.filter((p) => p && !p.sittingOut && p.stack > 0).length;
|
||||||
$('start').hidden = s.started;
|
$('start').hidden = s.started;
|
||||||
$('start').disabled = eligible < 2;
|
$('start').disabled = eligible < 2;
|
||||||
$('start').title = eligible < 2 ? '2人以上で開始できます' : '';
|
$('start').title = eligible < 2 ? '2人以上で開始できます' : '';
|
||||||
$('pause').hidden = !s.started;
|
$('pause').hidden = !s.started;
|
||||||
$('pause').textContent = s.paused ? '再開' : '一時停止';
|
$('pause').textContent = s.paused ? '▶' : '⏸';
|
||||||
|
$('pause').title = s.paused ? '再開' : '一時停止';
|
||||||
|
$('pause').setAttribute('aria-label', $('pause').title);
|
||||||
|
$('pause').classList.toggle('paused', s.paused);
|
||||||
|
|
||||||
// 自分
|
// 自分
|
||||||
$('action-bar').hidden = !me;
|
$('action-bar').hidden = !me;
|
||||||
@@ -250,7 +252,7 @@
|
|||||||
else if (m.sittingOut) status = '離席中です。復帰すると次のハンドから参加します';
|
else if (m.sittingOut) status = '離席中です。復帰すると次のハンドから参加します';
|
||||||
else if (m.canRebuy) status = 'チップがなくなりました';
|
else if (m.canRebuy) status = 'チップがなくなりました';
|
||||||
else if (o) status = o.canCheck ? 'あなたの番です' : `あなたの番です(コール ${chips(o.toCall)})`;
|
else if (o) status = o.canCheck ? 'あなたの番です' : `あなたの番です(コール ${chips(o.toCall)})`;
|
||||||
else if (awaiting) status = m.canAward ? 'カードを見せ合って、勝った人を選んでください' : '勝者の選択を待っています';
|
else if (awaiting) status = m.canAward ? 'カードを見せ合って、勝った人を選んでください' : 'ホストが勝者を選んでいます';
|
||||||
else if (h && !h.finished && s.toAct != null) status = `${nameOf(s.toAct)} の番です`;
|
else if (h && !h.finished && s.toAct != null) status = `${nameOf(s.toAct)} の番です`;
|
||||||
else if (h && !h.finished && !seat.inHand) status = '次のハンドから参加します';
|
else if (h && !h.finished && !seat.inHand) status = '次のハンドから参加します';
|
||||||
else if (h && h.finished) status = '次のハンドを準備中…';
|
else if (h && h.finished) status = '次のハンドを準備中…';
|
||||||
|
|||||||
+5
-6
@@ -16,15 +16,14 @@
|
|||||||
<div class="table-header">
|
<div class="table-header">
|
||||||
<button class="icon-btn leave" id="leave" aria-label="退席する" title="退席する">⏏</button>
|
<button class="icon-btn leave" id="leave" aria-label="退席する" title="退席する">⏏</button>
|
||||||
<h1 id="table-name"></h1>
|
<h1 id="table-name"></h1>
|
||||||
|
<!-- ホストだけに表示 -->
|
||||||
|
<span class="host-controls" id="host-controls" hidden>
|
||||||
|
<button class="btn btn-green btn-sm" id="start">START</button>
|
||||||
|
<button class="icon-btn" id="pause" aria-label="一時停止" title="一時停止">⏸</button>
|
||||||
|
</span>
|
||||||
<button class="icon-btn" id="share" aria-label="サイトのリンクをコピー" title="サイトのリンクをコピー">🔗</button>
|
<button class="icon-btn" id="share" aria-label="サイトのリンクをコピー" title="サイトのリンクをコピー">🔗</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="host-controls" id="host-controls" hidden>
|
|
||||||
<button class="btn btn-green" id="start">START ▶</button>
|
|
||||||
<button class="btn btn-dark" id="pause">一時停止</button>
|
|
||||||
</div>
|
|
||||||
<div class="host-spacer" id="host-spacer"></div>
|
|
||||||
|
|
||||||
<div class="level-bar">
|
<div class="level-bar">
|
||||||
<span class="lv" id="level"></span>
|
<span class="lv" id="level"></span>
|
||||||
<span class="blinds" id="blinds"></span>
|
<span class="blinds" id="blinds"></span>
|
||||||
|
|||||||
+3
-6
@@ -152,17 +152,14 @@ class Table {
|
|||||||
return this.hand.act(i, type, amount);
|
return this.hand.act(i, type, amount);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ショーダウンの勝者を選べるのは、ショーダウンに残った人とホスト
|
// ショーダウンの勝者(ポットの分配)を決められるのはテーブルのホストだけ
|
||||||
canAward(token) {
|
canAward(token) {
|
||||||
if (!this.handActive || !this.hand.awaitingWinners) return false;
|
return this.handActive && this.hand.awaitingWinners && this.isHost(token);
|
||||||
const i = this.seatOf(token);
|
|
||||||
const p = i >= 0 ? this.hand.players.get(i) : null;
|
|
||||||
return this.isHost(token) || Boolean(p && !p.folded && !this.seats[i].left);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
award(token, winners) {
|
award(token, winners) {
|
||||||
if (!this.handActive || !this.hand.awaitingWinners) return 'ショーダウンではありません';
|
if (!this.handActive || !this.hand.awaitingWinners) return 'ショーダウンではありません';
|
||||||
if (!this.canAward(token)) return 'ショーダウンに残った人かホストが選んでください';
|
if (!this.canAward(token)) return 'ポットの分配はホストのみ行えます';
|
||||||
if (this.paused) return '一時停止中です';
|
if (this.paused) return '一時停止中です';
|
||||||
return this.hand.award(winners);
|
return this.hand.award(winners);
|
||||||
}
|
}
|
||||||
|
|||||||
+27
-9
@@ -131,22 +131,40 @@ test('勝者の指定: 山分け(端数はボタンの左から)と不正な
|
|||||||
assert.equal(t.act(tk(t, bb), 'check', 0), null);
|
assert.equal(t.act(tk(t, bb), 'check', 0), null);
|
||||||
checkDown(t);
|
checkDown(t);
|
||||||
assert.equal(h.street, 'SHOWDOWN');
|
assert.equal(h.street, 'SHOWDOWN');
|
||||||
t.hostToken = T(7); // ホストの特例を除いて確認する
|
const host = T(0);
|
||||||
|
|
||||||
assert.equal(t.award(tk(t, sb), [[bb]]), 'ショーダウンに残った人かホストが選んでください');
|
// ホスト以外は、ショーダウンに残った人でも分配できない
|
||||||
assert.ok(t.award(tk(t, bb), []));
|
for (const s of [sb, bb, btn]) {
|
||||||
assert.ok(t.award(tk(t, bb), [[]]));
|
if (tk(t, s) !== host) assert.equal(t.award(tk(t, s), [[bb]]), 'ポットの分配はホストのみ行えます');
|
||||||
assert.ok(t.award(tk(t, bb), [[sb]])); // フォールドした人は勝てない
|
}
|
||||||
assert.ok(t.award(tk(t, bb), [[bb, bb]]));
|
|
||||||
assert.ok(t.award(tk(t, bb), [[bb], [btn]])); // ポットは1つ
|
|
||||||
assert.ok(t.award(T(8), [[bb]])); // 着席していない
|
assert.ok(t.award(T(8), [[bb]])); // 着席していない
|
||||||
|
assert.ok(t.award(host, []));
|
||||||
|
assert.ok(t.award(host, [[]]));
|
||||||
|
assert.ok(t.award(host, [[sb]])); // フォールドした人は勝てない
|
||||||
|
assert.ok(t.award(host, [[bb, bb]]));
|
||||||
|
assert.ok(t.award(host, [[bb], [btn]])); // ポットは1つ
|
||||||
assert.ok(!h.finished);
|
assert.ok(!h.finished);
|
||||||
|
|
||||||
assert.equal(t.award(tk(t, bb), [[bb, btn]]), null);
|
assert.equal(t.award(host, [[bb, btn]]), null);
|
||||||
// 50 を2人で → 25 ずつ
|
// 50 を2人で → 25 ずつ
|
||||||
assert.equal(t.seats[bb].stack, 1005);
|
assert.equal(t.seats[bb].stack, 1005);
|
||||||
assert.equal(t.seats[btn].stack, 1005);
|
assert.equal(t.seats[btn].stack, 1005);
|
||||||
assert.equal(t.award(tk(t, bb), [[bb]]), 'ショーダウンではありません');
|
assert.equal(t.award(host, [[bb]]), 'ショーダウンではありません');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('ホストが退席したら次のホストが分配する', () => {
|
||||||
|
const t = makeTable([1000, 1000, 1000]);
|
||||||
|
t.start(T(0), 0);
|
||||||
|
const h = t.hand;
|
||||||
|
// ホスト(席0)以外の2人と一緒にショーダウンまで進め、ホストが退席する
|
||||||
|
checkDown(t);
|
||||||
|
assert.equal(h.street, 'SHOWDOWN');
|
||||||
|
t.leave(T(0));
|
||||||
|
assert.notEqual(t.hostToken, T(0));
|
||||||
|
const live = h.live().map((p) => p.seat);
|
||||||
|
assert.equal(t.award(T(0), [[live[0]]]), 'ポットの分配はホストのみ行えます');
|
||||||
|
assert.equal(t.award(t.hostToken, [[live[0]]]), null);
|
||||||
|
assert.ok(h.finished);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('端数はボタンの左から配る', () => {
|
test('端数はボタンの左から配る', () => {
|
||||||
|
|||||||
Reference in new issue
Block a user