Files
Poker/public/assets/common.js
T
agomizi1cmandClaude Opus 5.5 8de55f7ca5 Implement Texas Hold'em play: dealing, betting, side pots, showdown
- Hand engine (server/hand.js): crypto-shuffled deck kept server-side,
  blinds (heads-up button = SB), min-raise rules incl. incomplete all-in,
  uncalled bet refunds, side pots, split pots, all-in runout
- Hand evaluator (server/cards.js) and node:test suite (npm test)
- Table flow: hands auto-continue after host START, 30s action clock
  (timeout = check/fold + sit out), sit out / sit in, rebuy, mid-hand leave
  folds and keeps the seat reserved until the hand ends, pause freezes all clocks
- Only each player's own hole cards are sent until a showdown/runout reveal;
  all actions validated server-side
- Play UI: board, hole cards, bets, action tags, turn timer, raise panel with
  presets, showdown results with winning cards highlighted, compact 7+ layout
- Remove the host crown and the temporary "next phase" control

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-15 17:34:25 +09:00

100 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) => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;' }[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',
FINISHING: 'FINISHING',
};
window.App = { Store, esc, chips, toast, api, connectSocket, safeNext, PHASE_LABELS };
})();