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>
This commit is contained in:
agomizi1cmandClaude Opus 5.5 committed 2026-10-17 16:55:25 +09:00
1 parent 66907345ff
commit 0d3a054f0d
12 files changed
+550 -46

No files matched your search

+9 -3
View File
@@ -6,7 +6,7 @@ const express = require('express');
const { Server } = require('socket.io');
const config = require('./config');
const auth = require('./auth');
const { TableManager } = require('./tables');
const { TableManager, MODES } = require('./tables');
const PUBLIC = path.join(__dirname, '..', 'public');
// 管理者専用のファイル(public の外に置き、管理者の Cookie があるときだけ返す)
@@ -114,7 +114,8 @@ app.post('/api/tables', (req, res) => {
const bigBlind = b.bigBlind;
const blindTimerEnabled = b.blindTimerEnabled === true;
const blindIntervalMin = b.blindIntervalMin;
const cardsEnabled = b.cardsEnabled === true;
const mode = b.mode;
const cpuCount = b.cpuCount;
const hostToken = cleanToken(b.token);
if (tables.tables.size >= config.MAX_TABLES) return res.status(429).json({ error: 'テーブル数が上限に達しています' });
@@ -124,6 +125,10 @@ app.post('/api/tables', (req, res) => {
if (!intIn(smallBlind, 1, startingStack)) return res.status(400).json({ error: 'SBが不正です' });
if (!intIn(bigBlind, smallBlind, startingStack)) return res.status(400).json({ error: 'BBはSB以上・開始スタック以下にしてください' });
if (blindTimerEnabled && !intIn(blindIntervalMin, 1, 240)) return res.status(400).json({ error: 'インターバルは1〜240分で入力してください' });
if (!MODES.includes(mode)) return res.status(400).json({ error: 'テーブルの種類が不正です' });
if (mode === 'cpu' && !intIn(cpuCount, 1, config.MAX_SEATS - 1)) {
return res.status(400).json({ error: `CPU の人数は1〜${config.MAX_SEATS - 1}人にしてください` });
}
if (!hostToken) return res.status(400).json({ error: 'プレイヤーIDがありません' });
const table = tables.create({
@@ -133,7 +138,8 @@ app.post('/api/tables', (req, res) => {
bigBlind,
blindTimerEnabled,
blindIntervalMin: blindTimerEnabled ? blindIntervalMin : 10,
cardsEnabled,
mode,
cpuCount: mode === 'cpu' ? cpuCount : 0,
hostToken,
});
broadcastLobby();