Files
Poker/public/assets/create.js
T
agomizi1cmandClaude Opus 5.5 0d3a054f0d Add CPU opponents and a table type selector (cash only / friends / CPU)
- Create screen: choose キャッシュオンリー (real cards, chips only), 友人戦
  (server deals and settles) or CPU戦 (友人戦 plus 1–8 CPU players); replaces
  the card toggle. The lobby shows each table's type
- Poker AI written from scratch (server/ai.js, no libraries): fast 7-card
  evaluator (checked against bestHand), Monte Carlo equity, and decisions from
  hand rank, pot odds, raise count, opponents and a per-CPU style (aggression,
  bluff rate); continuation bets via the last aggressor. It never sees other
  players' hole cards
- CPU seats: think time of 0.7–1.8s (faster when no human is left in the hand),
  automatic rebuy, never the host, no new hand unless a human is playing,
  re-think after pause
- Play UI: robot avatars and a "考えています…" status for CPU turns
- Tests for the evaluator, equity, legality of AI actions and CPU tables

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

163 lines
5.4 KiB
JavaScript

(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 modeHint = $('mode-hint');
const cpuOptions = $('cpu-options');
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('');
}
const MODE_HINTS = {
chips: '実物のカードで遊び、サイトはチップだけを管理。ショーダウンの勝者はホストが選ぶ',
friends: '各自のスマホに手札が配られ、勝敗とポットの分配は自動',
cpu: 'CPU と対戦(友達も空いている席に参加できる)。カードの配布・勝敗の判定は自動',
};
const radioValue = (name) => (form.querySelector(`input[name="${name}"]:checked`) || {}).value;
function updateMode() {
const mode = radioValue('mode');
modeHint.textContent = MODE_HINTS[mode] || '';
cpuOptions.hidden = mode !== 'cpu';
}
function refresh() {
updateHint();
updateSchedule();
updateMode();
}
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);
form.querySelectorAll('input[type="radio"]').forEach((el) => el.addEventListener('change', 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),
mode: radioValue('mode'),
cpuCount: Number(radioValue('cpu-count')),
token: Store.token,
});
location.href = '/table/' + encodeURIComponent(id);
} catch (err) {
errorEl.textContent = err.message;
submitBtn.disabled = false;
}
});
applyDefaultBlinds();
refresh();
suggestName();
})();