- 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>
101 lines
3.1 KiB
JavaScript
101 lines
3.1 KiB
JavaScript
// 各ページ共通のユーティリティ
|
|
(function () {
|
|
'use strict';
|
|
|
|
function lsGet(key) {
|
|
try { return localStorage.getItem(key); } catch { return null; }
|
|
}
|
|
function lsSet(key, value) {
|
|
try { localStorage.setItem(key, value); } catch { /* プライベートモード等 */ }
|
|
}
|
|
|
|
// 非HTTPS環境でも動くよう crypto.randomUUID ではなく getRandomValues を使う
|
|
function randomHex(bytes) {
|
|
const a = new Uint8Array(bytes);
|
|
crypto.getRandomValues(a);
|
|
return Array.from(a, (b) => b.toString(16).padStart(2, '0')).join('');
|
|
}
|
|
|
|
const Store = {
|
|
get name() { return lsGet('poker.name') || ''; },
|
|
set name(v) { lsSet('poker.name', v); },
|
|
// 席の再接続用。ブラウザごとに固定
|
|
get token() {
|
|
let t = lsGet('poker.token');
|
|
if (!t || !/^[0-9a-f]{32}$/.test(t)) {
|
|
t = randomHex(16);
|
|
lsSet('poker.token', t);
|
|
}
|
|
return t;
|
|
},
|
|
};
|
|
|
|
function esc(s) {
|
|
return String(s).replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
}
|
|
|
|
function chips(n) {
|
|
return '$' + Number(n).toLocaleString('en-US');
|
|
}
|
|
|
|
let toastTimer;
|
|
function toast(msg) {
|
|
let el = document.querySelector('.toast');
|
|
if (!el) {
|
|
el = document.createElement('div');
|
|
el.className = 'toast';
|
|
document.body.appendChild(el);
|
|
}
|
|
el.textContent = msg;
|
|
el.hidden = false;
|
|
clearTimeout(toastTimer);
|
|
toastTimer = setTimeout(() => { el.hidden = true; }, 2500);
|
|
}
|
|
|
|
async function api(method, url, body) {
|
|
const res = await fetch(url, {
|
|
method,
|
|
headers: body ? { 'Content-Type': 'application/json' } : undefined,
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
credentials: 'same-origin',
|
|
});
|
|
if (res.status === 401 && url !== '/api/auth') {
|
|
location.href = '/key?next=' + encodeURIComponent(location.pathname);
|
|
throw new Error('unauthorized');
|
|
}
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok) throw new Error(data.error || 'エラーが発生しました');
|
|
return data;
|
|
}
|
|
|
|
// サーバは WebSocket のみ受け付ける。接続を拒否されたら鍵が無効になっていないか確認する
|
|
function connectSocket() {
|
|
const socket = io({ transports: ['websocket'] });
|
|
socket.on('connect_error', () => {
|
|
fetch('/api/tables', { credentials: 'same-origin' })
|
|
.then((res) => {
|
|
if (res.status === 401) location.href = '/key?next=' + encodeURIComponent(location.pathname);
|
|
})
|
|
.catch(() => {});
|
|
});
|
|
return socket;
|
|
}
|
|
|
|
// 自サイト内のパスだけ許可("//x" や "/\x" は外部サイト扱いになるので拒否)
|
|
function safeNext(next) {
|
|
return typeof next === 'string' && /^\/(?![/\\])[^\\\s]*$/.test(next) && next.length <= 200 ? next : '/';
|
|
}
|
|
|
|
const PHASE_LABELS = {
|
|
WAITING: 'WAITING',
|
|
PREFLOP: 'PREFLOP',
|
|
FLOP: 'FLOP',
|
|
TURN: 'TURN',
|
|
RIVER: 'RIVER',
|
|
SHOWDOWN: 'SHOWDOWN',
|
|
FINISHING: 'FINISHING',
|
|
};
|
|
|
|
window.App = { Store, esc, chips, toast, api, connectSocket, safeNext, PHASE_LABELS };
|
|
})();
|