A new table type, "review" (レビュー付きCPU戦), can only be created by admins. Only the creator and CPUs sit there: friends cannot join, and the table is hidden from non-admin lobbies. After every hand a review opens, and the next hand waits until the creator presses "次のハンドへ". - Preflop: shows which color of the registered hand range the hole cards are and whether the action followed it. With no entrant before you, open with the color for the number of players still to act; facing an entrant, estimate their range from their position, call one color above it and raise two above. Purple ranks just below white (SB only). - Flop onward: equity vs ranges estimated from those colors (narrowed by later bets/calls) gives a rough EV per option. The score is the EV lost against the best option, relative to the pot. It is a guide, not GTO. - Also: pot-odds call check, all-in equity and luck-adjusted P&L, VPIP/PFR/3bet/Cbet/WTSD/W$SD/AF, per-color and per-position compliance, habits to watch, worst-5 hands, a P&L chart and a step-by-step replay on the admin-only /review page. Hand now logs every action for the review. The range data moves to admin/range-data.js, shared by the range page and the server. Reviews are saved under data/reviews/ (newest 50 sessions). Admin scripts and CSS are no longer cached for a day, so updates show up immediately. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
84 lines
3.2 KiB
JavaScript
84 lines
3.2 KiB
JavaScript
(function () {
|
|
'use strict';
|
|
|
|
const { Store, esc, chips } = App;
|
|
const nameInput = document.getElementById('name');
|
|
const nameError = document.getElementById('name-error');
|
|
const listEl = document.getElementById('list');
|
|
const countEl = document.getElementById('count');
|
|
|
|
nameInput.value = Store.name;
|
|
nameInput.addEventListener('input', () => {
|
|
Store.name = nameInput.value.trim();
|
|
nameInput.classList.remove('invalid');
|
|
nameError.textContent = '';
|
|
});
|
|
|
|
function requireName() {
|
|
if (Store.name) return true;
|
|
nameInput.classList.add('invalid');
|
|
nameError.textContent = '先に名前を入力してください。';
|
|
nameInput.focus();
|
|
return false;
|
|
}
|
|
|
|
// 管理者だけにハンドレンジ学習・CPU戦のレビューへのリンクを出す(ページ自体もサーバ側で管理者以外は 404)
|
|
App.api('GET', '/api/me')
|
|
.then((me) => {
|
|
document.getElementById('range-link').hidden = !me.admin;
|
|
document.getElementById('review-link').hidden = !me.admin;
|
|
})
|
|
.catch(() => {});
|
|
|
|
document.getElementById('create').addEventListener('click', () => {
|
|
if (requireName()) location.href = '/create';
|
|
});
|
|
|
|
listEl.addEventListener('click', (e) => {
|
|
const btn = e.target.closest('[data-join]');
|
|
if (btn && requireName()) location.href = '/table/' + encodeURIComponent(btn.dataset.join);
|
|
});
|
|
|
|
const MODE_LABEL = { chips: '💰 キャッシュオンリー', friends: '🃏 友人戦', cpu: '🤖 CPU戦', review: '📝 レビュー付きCPU戦' };
|
|
|
|
function phaseBadge(t) {
|
|
if (t.paused) return '<span class="badge paused">PAUSED</span>';
|
|
if (t.phase === 'WAITING') return '<span class="badge">WAITING</span>';
|
|
return '<span class="badge live">PLAYING</span>';
|
|
}
|
|
|
|
function render(tables) {
|
|
countEl.textContent = tables.length ? `${tables.length} テーブル` : '';
|
|
if (!tables.length) {
|
|
listEl.innerHTML = '<div class="empty">開催中のテーブルはありません。<br>テーブルを作成して友達を招待しよう。</div>';
|
|
return;
|
|
}
|
|
listEl.innerHTML = tables
|
|
.map((t) => {
|
|
const full = t.playerCount >= t.maxSeats;
|
|
const timer = t.timerEnabled ? `⏱ ${t.intervalMin}分` : '⏱ なし';
|
|
return `
|
|
<div class="table-item">
|
|
<div class="info">
|
|
<div class="name">${esc(t.name)}${phaseBadge(t)}</div>
|
|
<div class="meta">
|
|
<span>👤 <b>${t.playerCount}/${t.maxSeats}</b></span>
|
|
<span>開始 <b>${chips(t.startingStack)}</b></span>
|
|
<span class="gold">Lv.${t.level} <b class="gold">${chips(t.smallBlind)}/${chips(t.bigBlind)}</b></span>
|
|
<span>${timer}</span>
|
|
<span>${MODE_LABEL[t.mode] || ''}</span>
|
|
</div>
|
|
</div>
|
|
<button class="btn btn-blue btn-sm" data-join="${esc(t.id)}" ${full ? 'title="満席(観戦のみ)"' : ''}>
|
|
${full ? '観戦' : '参加'}
|
|
</button>
|
|
</div>`;
|
|
})
|
|
.join('');
|
|
}
|
|
|
|
const socket = App.connectSocket();
|
|
socket.on('connect', () => socket.emit('lobby:subscribe'));
|
|
socket.on('lobby:tables', render);
|
|
})();
|