'use strict'; const crypto = require('crypto'); const config = require('./config'); const Blinds = require('../public/assets/blinds'); // 進行フェーズ。プレイ処理は未実装なので、ホストが手動で進めるスタブ。 const HAND_PHASES = ['PREFLOP', 'FLOP', 'TURN', 'RIVER', 'SHOWDOWN']; class Table { constructor(opts) { this.id = opts.id; this.name = opts.name; this.startingStack = opts.startingStack; this.baseSmallBlind = opts.smallBlind; this.baseBigBlind = opts.bigBlind; this.timerEnabled = opts.blindTimerEnabled; this.intervalMin = opts.blindIntervalMin; this.hostToken = opts.hostToken || null; this.maxSeats = config.MAX_SEATS; this.seats = new Array(this.maxSeats).fill(null); // { token, name, stack } this.phase = 'WAITING'; this.started = false; this.paused = false; this.level = 1; this.handNumber = 0; this.pot = 0; this.buttonSeat = null; this.levelEndsAt = null; this.levelRemainingMs = this.intervalMs; this.createdAt = Date.now(); this.emptySince = null; } get intervalMs() { return this.intervalMin * 60 * 1000; } get blinds() { return Blinds.blindsAt(this.baseSmallBlind, this.baseBigBlind, this.level); } get playerCount() { return this.seats.filter(Boolean).length; } seatOf(token) { return this.seats.findIndex((s) => s && s.token === token); } isHost(token) { return Boolean(token) && token === this.hostToken; } // 着席した席番号を返す。満席なら -1(観戦) join(token, name) { const existing = this.seatOf(token); if (existing >= 0) { this.seats[existing].name = name; return existing; } const free = this.seats.findIndex((s) => !s); if (free < 0) return -1; this.seats[free] = { token, name, stack: this.startingStack }; // ホスト(通常は作成者)が着席していなければ、この人がホストになる if (this.seatOf(this.hostToken) < 0) this.hostToken = token; return free; } leave(token) { const i = this.seatOf(token); if (i < 0) return false; this.seats[i] = null; if (this.hostToken === token) { const next = this.seats.find(Boolean); this.hostToken = next ? next.token : null; } if (this.playerCount < 2 && this.phase !== 'WAITING') { this.phase = 'WAITING'; } return true; } // 次に埋まっている席(時計回り = 席番号の昇順) nextOccupied(from) { for (let k = 1; k <= this.maxSeats; k++) { const i = (from + k) % this.maxSeats; if (this.seats[i]) return i; } return null; } blindSeats() { if (this.buttonSeat == null || this.phase === 'WAITING' || this.playerCount < 2) { return { sbSeat: null, bbSeat: null }; } let sbSeat; if (this.playerCount === 2 && this.seats[this.buttonSeat]) { sbSeat = this.buttonSeat; // ヘッズアップはボタンがSB } else { sbSeat = this.nextOccupied(this.buttonSeat); } return { sbSeat, bbSeat: this.nextOccupied(sbSeat) }; } startNewHand() { const occupied = this.seats.map((s, i) => (s ? i : -1)).filter((i) => i >= 0); this.buttonSeat = this.buttonSeat == null ? occupied[Math.floor(Math.random() * occupied.length)] : this.nextOccupied(this.buttonSeat); this.handNumber++; this.pot = 0; this.phase = 'PREFLOP'; } // ---- ホスト操作(エラー時はメッセージを返す) ---- start(token) { if (!this.isHost(token)) return 'ホストのみ開始できます'; if (this.phase !== 'WAITING') return 'すでに進行中です'; if (this.playerCount < 2) return '2人以上必要です'; if (!this.started) { this.started = true; if (this.timerEnabled) this.levelEndsAt = Date.now() + this.intervalMs; } this.startNewHand(); return null; } // スタブ: フェーズを1つ進める(SHOWDOWN の次は新しいハンド) nextPhase(token) { if (!this.isHost(token)) return 'ホストのみ操作できます'; if (this.phase === 'WAITING') return 'まだ開始していません'; if (this.playerCount < 2) return '2人以上必要です'; const i = HAND_PHASES.indexOf(this.phase); if (i === HAND_PHASES.length - 1) this.startNewHand(); else this.phase = HAND_PHASES[i + 1]; return null; } togglePause(token) { if (!this.isHost(token)) return 'ホストのみ操作できます'; if (!this.started) return 'まだ開始していません'; const now = Date.now(); if (this.paused) { this.paused = false; if (this.timerEnabled) this.levelEndsAt = now + this.levelRemainingMs; } else { this.paused = true; if (this.timerEnabled) { this.levelRemainingMs = Math.max(0, this.levelEndsAt - now); this.levelEndsAt = null; } } return null; } // ブラインドタイマー。レベルが上がったら true tick(now) { let changed = false; while (this.levelEndsAt != null && now >= this.levelEndsAt) { this.level++; this.levelEndsAt += this.intervalMs; changed = true; } return changed; } summary() { const { sb, bb } = this.blinds; return { id: this.id, name: this.name, playerCount: this.playerCount, maxSeats: this.maxSeats, startingStack: this.startingStack, smallBlind: sb, bigBlind: bb, level: this.level, phase: this.phase, paused: this.paused, timerEnabled: this.timerEnabled, intervalMin: this.intervalMin, createdAt: this.createdAt, }; } // token = 閲覧者のトークン(他人のトークンは送らない) publicState(token, connectedTokens) { const { sb, bb } = this.blinds; const next = Blinds.blindsAt(this.baseSmallBlind, this.baseBigBlind, this.level + 1); const host = this.seatOf(this.hostToken); return { id: this.id, name: this.name, startingStack: this.startingStack, maxSeats: this.maxSeats, level: this.level, smallBlind: sb, bigBlind: bb, nextBlinds: { smallBlind: next.sb, bigBlind: next.bb }, timer: { enabled: this.timerEnabled, intervalMin: this.intervalMin, endsAt: this.levelEndsAt, remainingMs: this.levelEndsAt != null ? this.levelEndsAt - Date.now() : this.levelRemainingMs, running: this.levelEndsAt != null, }, phase: this.phase, started: this.started, paused: this.paused, pot: this.pot, handNumber: this.handNumber, buttonSeat: this.phase === 'WAITING' ? null : this.buttonSeat, ...this.blindSeats(), hostSeat: host >= 0 ? host : null, seats: this.seats.map((s, i) => s ? { seat: i, name: s.name, stack: s.stack, connected: connectedTokens.has(s.token) } : null ), mySeat: this.seatOf(token), isHost: this.isHost(token), serverNow: Date.now(), }; } } class TableManager { constructor() { this.tables = new Map(); } get(id) { return this.tables.get(id); } list() { return [...this.tables.values()] .sort((a, b) => b.createdAt - a.createdAt) .map((t) => t.summary()); } nameTaken(name) { const n = name.toLowerCase(); return [...this.tables.values()].some((t) => t.name.toLowerCase() === n); } suggestName() { for (;;) { const name = `Table #${1000 + crypto.randomInt(9000)}`; if (!this.nameTaken(name)) return name; } } create(opts) { let id; do id = crypto.randomBytes(4).toString('hex'); while (this.tables.has(id)); const table = new Table({ ...opts, id }); this.tables.set(id, table); return table; } delete(id) { this.tables.delete(id); } } module.exports = { TableManager };