Switch to chips-only play: real cards at the table, winners picked on screen

- Remove the deck, hole cards, board, hand evaluator and reveal logic
  (cards are dealt physically; the site only tracks bets, pots and stacks)
- Showdown now waits for a winner per pot (main/side), chosen by a player
  still in the hand or the host; multiple winners split the pot
- All-in with no further betting goes straight to showdown
- Remove the 30s action clock / auto sit-out (everyone is at the same table)
- UI: winner picker panel (main winner auto-fills side pots), compact table

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
agomizi1cmandClaude Opus 5.5 committed 2026-10-15 17:46:10 +09:00
1 parent 8de55f7ca5
commit b5d84e5f65
11 files changed
+408 -605

No files matched your search

+1
View File
@@ -92,6 +92,7 @@
FLOP: 'FLOP',
TURN: 'TURN',
RIVER: 'RIVER',
SHOWDOWN: 'SHOWDOWN',
FINISHING: 'FINISHING',
};
+17 -81
View File
@@ -319,8 +319,8 @@ input.invalid { border-color: var(--red); }
.felt-area {
position: relative;
flex: 1;
min-height: 400px;
margin: 30px 0 26px;
min-height: 340px;
margin: 20px 0 24px;
}
.felt {
position: absolute;
@@ -411,11 +411,11 @@ input.invalid { border-color: var(--red); }
justify-content: center;
border: 2px solid #222;
}
.chip-marker.dealer { background: #fff; color: #111; top: 22px; right: -16px; z-index: 2; }
.chip-marker.dealer { background: #fff; color: #111; top: -4px; right: -8px; z-index: 2; }
.blind-marker {
position: absolute;
top: 26px;
left: -20px;
top: -4px;
left: -10px;
z-index: 2;
font-size: 10px;
font-weight: 900;
@@ -509,57 +509,6 @@ input.invalid { border-color: var(--red); }
z-index: 30;
}
/* ---------- カード ---------- */
.card {
display: inline-flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: none;
width: 28px;
height: 40px;
background: #fff;
border-radius: 5px;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
font-weight: 900;
line-height: 1;
user-select: none;
transition: filter 0.2s;
}
.card b { font-size: 15px; letter-spacing: -0.05em; }
.card i { font-style: normal; font-size: 13px; margin-top: 1px; }
.suit-s { color: #1a1a1a; }
.suit-h { color: #d62839; }
.suit-d { color: #1f6fd1; }
.suit-c { color: #16884a; }
.card.back {
background: repeating-linear-gradient(45deg, #c0392b 0 3px, #a93226 3px 6px);
border: 2px solid #fff;
}
.card.slot { background: rgba(0, 0, 0, 0.12); box-shadow: none; border: 1px dashed rgba(255, 255, 255, 0.3); }
.card.deal { animation: deal 0.35s ease-out both; }
@keyframes deal {
from { opacity: 0; transform: translateY(-10px) scale(0.8); }
}
.card.dim { filter: brightness(0.5); }
.card.hl { box-shadow: 0 0 0 2px var(--gold), 0 0 10px rgba(246, 199, 68, 0.8); }
.board { display: flex; gap: 3px; margin-top: 8px; min-height: 40px; }
.hole {
position: absolute;
top: -18px;
left: 50%;
transform: translateX(-50%);
display: flex;
gap: 2px;
z-index: 1;
}
.hole .card { width: 25px; height: 35px; border-radius: 4px; }
.hole .card b { font-size: 13px; }
.hole .card i { font-size: 11px; }
.hole .card.back { border-width: 1.5px; }
/* ---------- 席の状態 ---------- */
.seat.folded .avatar, .seat.folded .nameplate { opacity: 0.45; }
.seat.away .avatar { opacity: 0.4; }
@@ -568,17 +517,7 @@ input.invalid { border-color: var(--red); }
box-shadow: 0 0 0 3px rgba(246, 199, 68, 0.35), 0 0 18px rgba(246, 199, 68, 0.7);
}
.seat.winner .nameplate { box-shadow: 0 0 0 2px var(--gold), 0 0 12px rgba(246, 199, 68, 0.6); }
.nameplate .phand {
font-size: 10px;
font-weight: 700;
color: var(--gold);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.seat-timer { height: 3px; background: rgba(255, 255, 255, 0.15); border-radius: 2px; margin-top: 3px; overflow: hidden; }
.seat-timer i, .turn-progress i { display: block; height: 100%; width: 100%; background: var(--green); transition: width 0.2s linear; }
.seat-timer i.low, .turn-progress i.low { background: var(--red); }
.seat.contender .avatar { border-color: var(--orange); }
.act-tag {
position: absolute;
@@ -625,12 +564,13 @@ input.invalid { border-color: var(--red); }
border: 2px dashed #fff;
box-sizing: border-box;
}
.bet.above { bottom: calc(100% + 20px); }
.bet.above { bottom: calc(100% + 4px); }
.bet.below { top: calc(100% + 4px); }
.bet.win { background: var(--gold); color: #111; padding: 2px 8px; }
.bet.win::before { display: none; }
.phase.paused { background: var(--red); }
.phase.showdown { background: var(--orange); color: #111; }
.result {
margin-top: 6px;
background: rgba(0, 0, 0, 0.4);
@@ -646,35 +586,31 @@ input.invalid { border-color: var(--red); }
.pot-tag { font-size: 9px; background: rgba(255, 255, 255, 0.2); padding: 0 4px; border-radius: 4px; margin-right: 4px; }
/* 7人以上 */
.felt-area.crowded { min-height: 470px; }
.felt-area.crowded { min-height: 420px; }
.crowded .seat { width: 80px; }
.crowded .avatar { width: 46px; height: 46px; }
.crowded .avatar svg { width: 22px; height: 22px; }
.crowded .nameplate { min-width: 72px; max-width: 80px; padding: 3px 6px; }
.crowded .nameplate .pname { font-size: 11px; }
.crowded .nameplate .pstack { font-size: 12px; }
.crowded .hole { top: -12px; }
.crowded .hole .card { width: 20px; height: 28px; }
.crowded .hole .card b { font-size: 11px; }
.crowded .hole .card i { font-size: 9px; }
.crowded .chip-marker.dealer { top: 14px; }
.crowded .blind-marker { top: 18px; }
/* ---------- 自分の操作エリア ---------- */
.turn-progress { position: absolute; top: 0; left: 0; right: 0; height: 4px; background: rgba(255, 255, 255, 0.08); }
.me-row { display: flex; align-items: center; gap: 12px; margin-bottom: 6px; }
.my-cards { display: flex; gap: 4px; flex: none; }
.my-cards .card { width: 42px; height: 58px; border-radius: 6px; }
.my-cards .card b { font-size: 22px; }
.my-cards .card i { font-size: 19px; }
.me-info { flex: 1; min-width: 0; }
.me-sub { font-size: 12px; color: var(--muted); display: flex; gap: 8px; margin-top: 3px; white-space: nowrap; }
.me-sub .my-hand { color: var(--gold); font-weight: 800; }
.me-sub .my-bet { color: var(--gold); font-weight: 700; }
.me-sub .plus { color: var(--green); }
.me-sub .minus { color: var(--red); }
.status-line { text-align: center; font-size: 12px; color: var(--muted); margin-bottom: 8px; min-height: 16px; }
.status-line.my-turn { color: var(--gold); font-weight: 800; }
.raise-panel { margin-bottom: 8px; }
.award-panel { margin-bottom: 4px; }
.award-pot { background: var(--panel-2); border-radius: 10px; padding: 8px 10px; margin-bottom: 8px; }
.award-title { font-size: 12px; color: var(--muted); margin-bottom: 6px; }
.award-title b { color: var(--gold); font-size: 14px; }
.picks { display: flex; flex-wrap: wrap; gap: 6px; }
.picks .pick { background: var(--input); color: var(--text); border: 2px solid transparent; }
.picks .pick.on { background: rgba(246, 199, 68, 0.18); border-color: var(--gold); color: var(--gold); }
.presets { display: grid; grid-template-columns: repeat(5, 1fr); gap: 6px; }
.presets .btn { padding: 8px 0; font-size: 13px; }
.presets .btn.active { background: var(--orange); color: #111; }
+100 -112
View File
@@ -1,4 +1,5 @@
// プレイ画面: サーバから受け取ったテーブル状態を描画し、アクションを送る
// カードは実物で配るので、ここで扱うのはチップのやり取りだけ
(function () {
'use strict';
@@ -8,19 +9,18 @@
const AVATAR_SVG =
'<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="8" r="4.5"/><path d="M3 21c0-4.4 4-7.5 9-7.5s9 3.1 9 7.5z"/></svg>';
// U+FE0E: iOS で絵文字として描画されないように
const SUIT_SYMBOL = { s: '♠︎', h: '♥︎', d: '♦︎', c: '♣︎' };
const ACTION_LABEL = { FOLD: 'Fold', CHECK: 'Check', CALL: 'Call', BET: 'Bet', RAISE: 'Raise', ALLIN: 'All-in' };
let socket = null;
let state = null;
let clockOffset = 0; // serverNow - clientNow
let pending = false; // アクション送信中(連打防止)
let pending = false; // 送信中(連打防止)
let raising = false;
let raiseValue = 0;
let seenBoard = ''; // 新しくめくられたカードだけアニメーションする
let seenHand = 0;
let wasMyTurn = false;
// ショーダウンの勝者選択(ポットごとの席番号の Set)
let awardKey = '';
let awardSel = [];
// ---------- 名前 ----------
function ensureName() {
@@ -65,7 +65,7 @@
});
}
function emit(event, payload, onDone) {
function emit(event, payload) {
if (!socket) return;
socket.emit(event, payload || null, (res) => {
if (res && !res.ok) {
@@ -73,7 +73,6 @@
toast(res.error === 'NOT_FOUND' ? 'テーブルが見つかりません' : res.error);
if (state) render();
}
if (onDone) onDone(res);
});
}
@@ -92,24 +91,14 @@
if (socket) socket.disconnect();
}
// ---------- カード ----------
function cardHtml(c, extra = '') {
if (typeof c !== 'string' || !/^[2-9TJQKA][shdc]$/.test(c)) return `<span class="card back ${extra}"></span>`;
const rank = c[0] === 'T' ? '10' : c[0];
return `<span class="card suit-${c[1]} ${extra}" data-card="${c}"><b>${rank}</b><i>${SUIT_SYMBOL[c[1]]}</i></span>`;
}
// ショーダウンで勝った5枚
function winningCards(h) {
return h && h.finished && h.showdown && h.results && h.results[0] ? new Set(h.results[0].bestCards) : null;
}
const nameOf = (seat) => (state.seats[seat] ? state.seats[seat].name : '退席者');
// ---------- 描画 ----------
function render() {
const s = state;
const h = s.hand;
const me = s.mySeat >= 0 ? s.seats[s.mySeat] : null;
const myTurn = Boolean(s.me && s.me.options);
const myTurn = Boolean(s.me && (s.me.options || (s.me.canAward && !s.paused)));
$('main').hidden = false;
document.title = `${myTurn ? '▶ ' : ''}${s.name} - Hold'em Night`;
@@ -126,8 +115,8 @@
$('pot').textContent = chips(h ? h.potTotal : 0);
$('phase').textContent = s.paused ? 'PAUSED' : App.PHASE_LABELS[s.phase] || s.phase;
$('phase').parentElement.classList.toggle('paused', s.paused);
$('phase').parentElement.classList.toggle('showdown', s.phase === 'SHOWDOWN' && !s.paused);
$('hand-no').textContent = s.handNumber ? `#${s.handNumber}` : '';
renderBoard(h);
renderResult(s);
// ホスト操作
@@ -152,36 +141,10 @@
if (me) renderActionBar(s, me);
renderTimer();
// 自分の番になったら知らせる
// 自分の番になったら知らせる(タップ前は振動できない)
const tapped = !navigator.userActivation || navigator.userActivation.hasBeenActive;
if (myTurn && !wasMyTurn && navigator.vibrate && tapped) navigator.vibrate(80);
wasMyTurn = myTurn;
if (h) {
seenBoard = `${h.number}:${h.board.length}`;
seenHand = h.number;
}
}
function renderBoard(h) {
const el = $('board');
if (!h) {
el.innerHTML = '';
return;
}
const [seenNo, seenLen] = seenBoard.split(':').map(Number);
const oldLen = seenNo === h.number ? seenLen : 0;
const best = winningCards(h);
let html = '';
for (let i = 0; i < 5; i++) {
const c = h.board[i];
if (!c) {
html += '<span class="card slot"></span>';
continue;
}
const cls = [i >= oldLen ? 'deal' : '', best ? (best.has(c) ? 'hl' : 'dim') : ''].join(' ');
html += cardHtml(c, cls);
}
el.innerHTML = html;
}
function renderResult(s) {
@@ -191,13 +154,11 @@
el.hidden = true;
return;
}
const nameOf = (seat) => (s.seats[seat] ? s.seats[seat].name : '退席者');
el.innerHTML = h.results
.map((r, i) => {
const label = h.results.length > 1 ? `<span class="pot-tag">${i === 0 ? 'メイン' : 'サイド'}</span>` : '';
const who = r.winners.map((w) => esc(nameOf(w))).join('・');
const hand = r.handName ? `<span class="rhand">${esc(r.handName)}</span>` : '';
return `<div>${label}<b>${who}</b> +${chips(r.amount)} ${hand}</div>`;
return `<div>${label}<b>${who}</b> +${chips(r.amount)}</div>`;
})
.join('');
el.hidden = false;
@@ -210,20 +171,21 @@
const startIdx = Math.max(0, occupied.findIndex((p) => p.seat === s.mySeat));
const ordered = occupied.slice(startIdx).concat(occupied.slice(0, startIdx));
const n = ordered.length;
const best = winningCards(h);
const angles = ordered.map((_, i) => Math.PI / 2 + (i * 2 * Math.PI) / Math.max(n, 1));
const showdown = s.phase === 'SHOWDOWN';
const finished = Boolean(h && h.finished);
const container = $('seats');
$('felt-area').classList.toggle('crowded', n >= 7); // 7人以上は小さめに表示
container.innerHTML = ordered
.map((p, idx) => {
const classes = ['seat'];
const isTurn = s.turn && s.turn.seat === p.seat;
if (p.seat === s.mySeat) classes.push('me');
if (!p.connected) classes.push('offline');
if (p.inHand && p.folded) classes.push('folded');
if (isTurn) classes.push('turn');
if (h && h.finished && p.won > 0) classes.push('winner');
if (s.toAct === p.seat) classes.push('turn');
if (showdown && p.inHand && !p.folded) classes.push('contender');
if (finished && p.won > 0) classes.push('winner');
if (p.sittingOut && !p.inHand) classes.push('away');
const markers = h
@@ -234,24 +196,14 @@
].join('')
: '';
// 手札(自分と公開された人は表、それ以外は裏)
let cards = '';
if (p.inHand && (!p.folded || p.cards)) {
const deal = h.number !== seenHand ? 'deal' : '';
const list = p.cards || [null, null];
cards = `<div class="hole">${list
.map((c) => cardHtml(c, [deal, best && c ? (best.has(c) ? 'hl' : 'dim') : ''].join(' ')))
.join('')}</div>`;
}
// ベット(中央寄り)・獲得額
const below = Math.sin(angles[idx]) <= 0.35;
let bet = '';
if (h && h.finished && p.won > 0) bet = `<div class="bet win ${below ? 'below' : 'above'}">+${chips(p.won)}</div>`;
if (finished && p.won > 0) bet = `<div class="bet win ${below ? 'below' : 'above'}">+${chips(p.won)}</div>`;
else if (p.bet > 0) bet = `<div class="bet ${below ? 'below' : 'above'}">${chips(p.bet)}</div>`;
let tag = '';
if (p.last && !(h && h.finished && p.won > 0)) {
if (p.last && !(finished && p.won > 0)) {
tag = `<div class="act-tag tag-${p.last.type.toLowerCase()}">${ACTION_LABEL[p.last.type] || ''}</div>`;
} else if (p.sittingOut && !p.inHand) {
tag = '<div class="act-tag tag-away">離席中</div>';
@@ -259,14 +211,12 @@
return `
<div class="${classes.join(' ')}">
<div class="avatar">${AVATAR_SVG}${markers}${cards}</div>
<div class="avatar">${AVATAR_SVG}${markers}</div>
<div class="nameplate">
${tag}
<div class="pname">${esc(p.name)}</div>
<div class="pstack">${p.allIn && !(h && h.finished) ? 'ALL-IN' : chips(p.stack)}</div>
${p.handName && p.seat !== s.mySeat ? `<div class="phand">${esc(p.handName)}</div>` : ''}
<div class="pstack">${p.allIn && !finished ? 'ALL-IN' : chips(p.stack)}</div>
${p.connected ? '' : '<div class="offline-tag">オフライン</div>'}
${isTurn ? '<div class="seat-timer"><i></i></div>' : ''}
</div>
${bet}
</div>`;
@@ -289,40 +239,38 @@
const net = m.net;
$('my-net').textContent = `収支 ${net > 0 ? '+' : net < 0 ? '−' : '±'}${chips(Math.abs(net))}`;
$('my-net').className = net > 0 ? 'plus' : net < 0 ? 'minus' : '';
$('my-hand').textContent = seat.handName || '';
const myCards = seat.inHand && seat.cards ? seat.cards : null;
const best = winningCards(h);
$('my-cards').innerHTML = myCards
? myCards.map((c) => cardHtml(c, [seat.folded ? 'dim' : '', best && best.has(c) ? 'hl' : ''].join(' '))).join('')
: '<span class="card slot"></span><span class="card slot"></span>';
$('my-bet').textContent = seat.inHand && seat.bet > 0 ? `ベット中 ${chips(seat.bet)}` : '';
$('sit-out').hidden = m.sittingOut;
// 状況
const awaiting = Boolean(h && h.pots && !h.finished);
let status = '';
if (!s.started) status = s.isHost ? '2人以上揃ったら START を押してください' : 'ホストの開始を待っています';
else if (s.paused) status = '一時停止中';
else if (m.sittingOut) status = '離席中です。復帰すると次のハンドから参加します';
else if (m.canRebuy) status = 'チップがなくなりました';
else if (o) status = o.canCheck ? 'あなたの番です' : `あなたの番です(コール ${chips(o.toCall)})`;
else if (h && !h.finished && s.turn) status = `${(s.seats[s.turn.seat] || {}).name || ''} の番です`;
else if (awaiting) status = m.canAward ? 'カードを見せ合って、勝った人を選んでください' : '勝者の選択を待っています';
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 && !s.turn) status = 'オールイン — 残りのカードをめくります';
else if (h && h.finished) status = '次のハンドを準備中…';
else if (!h) status = '参加者を待っています(2人以上で再開)';
$('status-line').textContent = status;
$('status-line').classList.toggle('my-turn', Boolean(o));
$('status-line').classList.toggle('my-turn', Boolean(o) || (awaiting && m.canAward));
// ショーダウンの勝者選択
const awarding = awaiting && m.canAward && !s.paused;
$('award-panel').hidden = !awarding;
if (awarding) renderAwardPanel(h.pots);
// ボタン
const alt = m.sittingOut || m.canRebuy;
const alt = (m.sittingOut || m.canRebuy) && !awarding;
if (!o) raising = false;
$('alt-actions').hidden = !alt;
$('sit-in').hidden = !m.sittingOut;
$('rebuy').hidden = !m.canRebuy;
$('rebuy').textContent = `リバイ(${chips(s.startingStack)})`;
if (!o) raising = false;
$('actions').hidden = alt || raising;
$('actions').hidden = alt || raising || awarding;
$('raise-actions').hidden = alt || !raising;
$('raise-panel').hidden = alt || !raising;
$('status-line').hidden = raising;
@@ -347,6 +295,63 @@
if (raising) renderRaisePanel(o);
}
// ---------- 勝者選択 ----------
function renderAwardPanel(pots) {
const key = `${state.handNumber}:${JSON.stringify(pots)}`;
if (key !== awardKey) {
awardKey = key;
// 参加者が1人だけのポットは最初から選んでおく
awardSel = pots.map((p) => new Set(p.eligible.length === 1 ? p.eligible : []));
}
$('award-pots').innerHTML = pots
.map((p, i) => {
const title = pots.length === 1 ? 'ポット' : i === 0 ? 'メインポット' : `サイドポット${pots.length > 2 ? i : ''}`;
const names = p.eligible
.map(
(seat) =>
`<button type="button" class="btn btn-sm pick${awardSel[i].has(seat) ? ' on' : ''}" data-pot="${i}" data-seat="${seat}">${esc(nameOf(seat))}</button>`
)
.join('');
return `<div class="award-pot"><div class="award-title">${title} <b>${chips(p.amount)}</b></div><div class="picks">${names}</div></div>`;
})
.join('');
const ready = awardSel.every((set) => set.size > 0);
const split = awardSel.some((set) => set.size > 1);
$('award-confirm').disabled = !ready || pending;
$('award-confirm').textContent = split ? '山分けで確定' : '確定';
}
$('award-pots').addEventListener('click', (e) => {
const b = e.target.closest('[data-pot]');
if (!b || !state || !state.hand || !state.hand.pots) return;
const i = Number(b.dataset.pot);
const seat = Number(b.dataset.seat);
const set = awardSel[i];
if (set.has(seat)) set.delete(seat);
else {
set.add(seat);
// メインで勝った人は、まだ決めていないサイドポットでも勝っているはず
state.hand.pots.forEach((p, j) => {
if (j > i && awardSel[j].size === 0 && p.eligible.includes(seat)) awardSel[j].add(seat);
});
}
renderAwardPanel(state.hand.pots);
});
$('award-confirm').addEventListener('click', () => {
if (pending || !awardSel.every((set) => set.size > 0)) return;
const winners = awardSel.map((set) => [...set]);
const names = winners.map((w) => w.map(nameOf).join('・'));
const msg =
names.length === 1
? `${names[0]} の勝ちで確定しますか?`
: names.map((n, i) => `${i === 0 ? 'メイン' : 'サイド'}: ${n}`).join('\n') + '\nで確定しますか?';
if (!confirm(msg)) return;
pending = true;
emit('table:award', { winners });
render();
});
// ---------- レイズ ----------
function presetValue(o, kind) {
const h = state.hand;
@@ -427,7 +432,7 @@
$('raise-range').addEventListener('input', (e) => {
const o = options();
if (!o) return;
// BB 単位に丸める(最大はオールイン額そのまま)
// SB 単位に丸める(最大はオールイン額そのまま)
const unit = Math.max(1, state.smallBlind);
let v = Number(e.target.value);
if (v < o.maxRaiseTo) v = o.minRaiseTo + Math.round((v - o.minRaiseTo) / unit) * unit;
@@ -447,48 +452,31 @@
if (o) $('raise-amount').value = clampRaise(o, raiseValue);
});
// ---------- タイマー ----------
// ---------- ブラインドタイマー ----------
function renderTimer() {
if (!state) return;
const el = $('timer');
const t = state.timer;
const now = Date.now() + clockOffset;
el.classList.toggle('paused', state.paused);
if (!t.enabled) {
el.textContent = state.paused ? '⏸' : '';
} else {
const remaining = t.running ? t.endsAt - now : t.remainingMs;
const sec = Math.max(0, Math.ceil(remaining / 1000));
const time = `${Math.floor(sec / 60)}:${String(sec % 60).padStart(2, '0')}`;
el.textContent = state.paused ? `⏸ ${time}` : time;
el.classList.toggle('warn', t.running && sec <= 60);
}
// 手番の残り時間
const turn = state.turn;
const ratio = turn ? Math.max(0, Math.min(1, (turn.endsAt != null ? turn.endsAt - now : turn.remainingMs) / turn.totalMs)) : 0;
const width = `${(ratio * 100).toFixed(1)}%`;
const bar = document.querySelector('.seat.turn .seat-timer i');
if (bar) {
bar.style.width = width;
bar.classList.toggle('low', ratio < 0.3);
}
const mine = Boolean(turn && turn.seat === state.mySeat);
$('turn-progress').hidden = !mine;
if (mine) {
$('turn-bar').style.width = width;
$('turn-bar').classList.toggle('low', ratio < 0.3);
return;
}
const remaining = t.running ? t.endsAt - (Date.now() + clockOffset) : t.remainingMs;
const sec = Math.max(0, Math.ceil(remaining / 1000));
const time = `${Math.floor(sec / 60)}:${String(sec % 60).padStart(2, '0')}`;
el.textContent = state.paused ? `⏸ ${time}` : time;
el.classList.toggle('warn', t.running && sec <= 60);
}
setInterval(renderTimer, 200);
setInterval(renderTimer, 250);
// ---------- その他の操作 ----------
$('start').addEventListener('click', () => emit('table:start'));
$('pause').addEventListener('click', () => emit('table:pause'));
$('sit-out').addEventListener('click', () => emit('table:sitOut'));
$('take-seat').addEventListener('click', () => join(true));
$('sit-in').addEventListener('click', () => emit('table:sitIn'));
$('rebuy').addEventListener('click', () => emit('table:rebuy'));
$('take-seat').addEventListener('click', () => join(true));
$('leave').addEventListener('click', () => {
const me = state && state.mySeat >= 0 ? state.seats[state.mySeat] : null;
+6 -4
View File
@@ -38,7 +38,6 @@
<div class="felt">
<div class="pot-label">POT</div>
<div class="pot" id="pot">$0</div>
<div class="board" id="board"></div>
<div class="phase"><span id="phase">WAITING</span><span class="hand-no" id="hand-no"></span></div>
<div class="result" id="result" hidden></div>
</div>
@@ -52,19 +51,22 @@
</main>
<div class="action-bar" id="action-bar" hidden>
<div class="turn-progress" id="turn-progress" hidden><i id="turn-bar"></i></div>
<div class="inner">
<div class="me-row">
<div class="my-cards" id="my-cards"></div>
<div class="me-info">
<div class="my-stack"><span class="wallet">👛</span><span id="my-stack">$0</span></div>
<div class="me-sub"><span class="my-hand" id="my-hand"></span><span id="my-net"></span></div>
<div class="me-sub"><span id="my-net"></span><span class="my-bet" id="my-bet"></span></div>
</div>
<button class="btn btn-ghost btn-sm" id="sit-out" title="次のハンドから参加しません">離席</button>
</div>
<div class="status-line" id="status-line"></div>
<div class="award-panel" id="award-panel" hidden>
<div id="award-pots"></div>
<button class="btn btn-orange btn-block" id="award-confirm" disabled>確定</button>
</div>
<div class="raise-panel" id="raise-panel" hidden>
<div class="presets" id="presets">
<button class="btn btn-dark btn-sm" data-preset="min">Min</button>