Initial commit: private Texas Hold'em ring game site (lobby + table stub)
- 6-digit access key gate with long-lived HttpOnly cookie, brute-force limits - Lobby with saved player name and live table list - Table creation (name, starting stack, blinds, blind timer) - Table play screen stub (seats, button/SB/BB, phase, blinds, timer) - Security hardening: strict CSP, origin checks, host pinning, input sanitizing Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
commit
b2c91062f1
22 files changed
+3450
No files matched your search
@@ -0,0 +1,143 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const { Store, api, chips } = App;
|
||||
if (!Store.name) {
|
||||
location.replace('/');
|
||||
return;
|
||||
}
|
||||
|
||||
const $ = (id) => document.getElementById(id);
|
||||
const form = $('form');
|
||||
const nameInput = $('table-name');
|
||||
const stackInput = $('stack');
|
||||
const sbInput = $('sb');
|
||||
const bbInput = $('bb');
|
||||
const timerInput = $('timer');
|
||||
const intervalInput = $('interval');
|
||||
const scheduleEl = $('schedule');
|
||||
const blindHint = $('blind-hint');
|
||||
const errorEl = $('error');
|
||||
const submitBtn = $('submit');
|
||||
|
||||
// ブラインドを手で変えるまではスタックに追従させる
|
||||
let blindsTouched = false;
|
||||
|
||||
async function suggestName() {
|
||||
try {
|
||||
const { name } = await api('GET', '/api/tables/suggest-name');
|
||||
nameInput.value = name;
|
||||
} catch { /* 失敗時は手入力 */ }
|
||||
}
|
||||
|
||||
const int = (el) => {
|
||||
const n = Number(el.value);
|
||||
return Number.isInteger(n) ? n : NaN;
|
||||
};
|
||||
|
||||
function applyDefaultBlinds() {
|
||||
const stack = int(stackInput);
|
||||
if (!(stack >= 10)) return;
|
||||
const { sb, bb } = Blinds.defaultBlinds(stack);
|
||||
sbInput.value = sb;
|
||||
bbInput.value = bb;
|
||||
}
|
||||
|
||||
function updateHint() {
|
||||
const stack = int(stackInput);
|
||||
const bb = int(bbInput);
|
||||
if (blindsTouched && stack > 0 && bb > 0) {
|
||||
blindHint.innerHTML = `開始スタック = ${Math.floor(stack / bb)} BB ・ <a href="#" id="reset-blinds" class="link-gold">おすすめに戻す</a>`;
|
||||
} else {
|
||||
blindHint.textContent = 'スタックに合わせて自動設定(BB ≒ スタックの1/100)';
|
||||
}
|
||||
}
|
||||
|
||||
function updateSchedule() {
|
||||
const sb = int(sbInput);
|
||||
const bb = int(bbInput);
|
||||
const on = timerInput.checked;
|
||||
intervalInput.disabled = !on;
|
||||
scheduleEl.hidden = !on || !(sb > 0 && bb >= sb);
|
||||
if (scheduleEl.hidden) return;
|
||||
const mins = int(intervalInput) > 0 ? int(intervalInput) : 0;
|
||||
scheduleEl.innerHTML = Blinds.schedule(sb, bb, 8)
|
||||
.map((l, i) => `<span>Lv.${l.level} <b>${chips(l.sb)}/${chips(l.bb)}</b>${mins ? ` · ${i * mins}分〜` : ''}</span>`)
|
||||
.join('');
|
||||
}
|
||||
|
||||
function refresh() {
|
||||
updateHint();
|
||||
updateSchedule();
|
||||
}
|
||||
|
||||
stackInput.addEventListener('input', () => {
|
||||
if (!blindsTouched) applyDefaultBlinds();
|
||||
refresh();
|
||||
});
|
||||
[sbInput, bbInput].forEach((el) =>
|
||||
el.addEventListener('input', () => {
|
||||
blindsTouched = true;
|
||||
if (el === sbInput && int(sbInput) > 0) bbInput.value = int(sbInput) * 2;
|
||||
refresh();
|
||||
})
|
||||
);
|
||||
blindHint.addEventListener('click', (e) => {
|
||||
if (e.target.id !== 'reset-blinds') return;
|
||||
e.preventDefault();
|
||||
blindsTouched = false;
|
||||
applyDefaultBlinds();
|
||||
refresh();
|
||||
});
|
||||
timerInput.addEventListener('change', refresh);
|
||||
intervalInput.addEventListener('input', refresh);
|
||||
$('reroll').addEventListener('click', suggestName);
|
||||
|
||||
function validate() {
|
||||
const name = nameInput.value.trim();
|
||||
const stack = int(stackInput);
|
||||
const sb = int(sbInput);
|
||||
const bb = int(bbInput);
|
||||
const interval = int(intervalInput);
|
||||
if (!name) return [nameInput, 'テーブル名を入力してください'];
|
||||
if (!(stack >= 10)) return [stackInput, '開始スタックは10以上の整数にしてください'];
|
||||
if (!(sb >= 1)) return [sbInput, 'SBは1以上の整数にしてください'];
|
||||
if (!(bb >= sb)) return [bbInput, 'BBはSB以上にしてください'];
|
||||
if (bb > stack) return [bbInput, 'BBが開始スタックより大きいです'];
|
||||
if (timerInput.checked && !(interval >= 1 && interval <= 240)) return [intervalInput, 'インターバルは1〜240分にしてください'];
|
||||
return null;
|
||||
}
|
||||
|
||||
form.addEventListener('submit', async (e) => {
|
||||
e.preventDefault();
|
||||
form.querySelectorAll('.invalid').forEach((el) => el.classList.remove('invalid'));
|
||||
const problem = validate();
|
||||
if (problem) {
|
||||
problem[0].classList.add('invalid');
|
||||
problem[0].focus();
|
||||
errorEl.textContent = problem[1];
|
||||
return;
|
||||
}
|
||||
errorEl.textContent = '';
|
||||
submitBtn.disabled = true;
|
||||
try {
|
||||
const { id } = await api('POST', '/api/tables', {
|
||||
name: nameInput.value.trim(),
|
||||
startingStack: int(stackInput),
|
||||
smallBlind: int(sbInput),
|
||||
bigBlind: int(bbInput),
|
||||
blindTimerEnabled: timerInput.checked,
|
||||
blindIntervalMin: int(intervalInput),
|
||||
token: Store.token,
|
||||
});
|
||||
location.href = '/table/' + encodeURIComponent(id);
|
||||
} catch (err) {
|
||||
errorEl.textContent = err.message;
|
||||
submitBtn.disabled = false;
|
||||
}
|
||||
});
|
||||
|
||||
applyDefaultBlinds();
|
||||
refresh();
|
||||
suggestName();
|
||||
})();
|
||||
Reference in new issue
Block a user