Texas Holdem MCP Server

by freshlife001
Verified
import { Card } from './Card'; import { Deck } from './Deck'; import { Player, PlayerAction } from './Player'; import { HandEvaluator, HandResult } from '../services/HandEvaluator'; import { broadcastTableUpdate } from '../index'; export enum GameStage { WAITING = "waiting", PRE_FLOP = "pre-flop", FLOP = "flop", TURN = "turn", RIVER = "river", SHOWDOWN = "showdown" } export class Table { id: string; name: string; players: Player[] = []; deck: Deck = new Deck(); communityCards: Card[] = []; pot: number = 0; currentBet: number = 0; smallBlind: number; bigBlind: number; dealerPosition: number = -1; currentPlayerIndex: number = -1; stage: GameStage = GameStage.WAITING; maxPlayers: number; minPlayers: number = 2; // Add timer properties private actionTimer: NodeJS.Timeout | null = null; private actionTimeoutSeconds: number = 120; constructor(id: string, name: string, smallBlind: number = 5, bigBlind: number = 10, maxPlayers: number = 9) { this.id = id; this.name = name; this.smallBlind = smallBlind; this.bigBlind = bigBlind; this.maxPlayers = maxPlayers; } addPlayer(player: Player): boolean { if (this.players.length >= this.maxPlayers) { return false; } this.players.push(player); if (this.players.length >= this.minPlayers && this.stage === GameStage.WAITING) { this.startGame(); } broadcastTableUpdate(this.id); return true; } removePlayer(playerId: string): boolean { const index = this.players.findIndex(p => p.id === playerId); if (index === -1) { return false; } this.players.splice(index, 1); if (this.players.length < this.minPlayers) { this.stage = GameStage.WAITING; } broadcastTableUpdate(this.id); return true; } startGame(): void { console.log("Starting game"); if (this.players.length < this.minPlayers) { return; } // Reset game state this.deck = new Deck(); this.communityCards = []; this.pot = 0; this.currentBet = 0; // Reset player hands and action flags this.players.forEach(player => { player.resetHand(); player.hasActedThisStage = false; }); // Move dealer button this.dealerPosition = (this.dealerPosition + 1) % this.players.length; // Set blinds const smallBlindPos = (this.dealerPosition + 1) % this.players.length; const bigBlindPos = (this.dealerPosition + 2) % this.players.length; this.players[this.dealerPosition].isDealer = true; this.players[smallBlindPos].isSmallBlind = true; this.players[bigBlindPos].isBigBlind = true; // Post blinds this.pot += this.players[smallBlindPos].placeBet(this.smallBlind); this.pot += this.players[bigBlindPos].placeBet(this.bigBlind); this.currentBet = this.bigBlind; // Deal cards for (let i = 0; i < 2; i++) { for (let player of this.players) { const card = this.deck.dealCard(); if (card) { player.addCard(card); } } } // Set first player to act (after big blind) this.currentPlayerIndex = (bigBlindPos + 1) % this.players.length; this.players[this.currentPlayerIndex].isActive = true; this.stage = GameStage.PRE_FLOP; this.setActionTimer(); } handlePlayerAction(playerId: string, action: PlayerAction, amount: number = 0): boolean { // Find the player const playerIndex = this.players.findIndex(p => p.id === playerId); if (playerIndex === -1 || playerIndex !== this.currentPlayerIndex) { return false; } const player = this.players[playerIndex]; // Process the action switch (action) { case PlayerAction.FOLD: player.fold(); break; case PlayerAction.CHECK: // Player can only check if they've matched the current bet if (player.bet < this.currentBet) { return false; } // Mark this player as having checked player.isChecked = true; // Check if all active players have checked if (this.haveAllPlayersChecked()) { // Move to the next stage directly player.isActive = false; this.moveToNextStage(); broadcastTableUpdate(this.id); return true; } break; case PlayerAction.CALL: const callAmount = this.currentBet - player.bet; if (callAmount <= 0) { // If nothing to call, treat as a check break; } // Check if player has enough chips if (player.chips < callAmount) { // All-in case this.pot += player.chips; player.bet += player.chips; player.chips = 0; player.isAllIn = true; } else { // Regular call this.pot += callAmount; player.chips -= callAmount; player.bet += callAmount; } break; case PlayerAction.BET: if (this.currentBet > 0 || amount < this.bigBlind) { return false; } this.pot += player.placeBet(amount); this.currentBet = amount; // Reset all players' isChecked flags when someone bets this.players.forEach(p => p.isChecked = false); break; case PlayerAction.RAISE: if (amount <= this.currentBet || amount < this.currentBet * 2) { return false; } this.pot += player.placeBet(amount - player.bet); this.currentBet = amount; // Reset all players' isChecked flags when someone raises this.players.forEach(p => p.isChecked = false); break; case PlayerAction.ALL_IN: const allInAmount = player.chips; this.pot += player.placeBet(allInAmount); if (player.bet > this.currentBet) { this.currentBet = player.bet; } break; default: return false; } // Mark that this player has acted in this stage player.hasActedThisStage = true; player.isActive = false; // Move to next player or next stage this.moveToNextPlayer(); // Broadcast the table update after the action broadcastTableUpdate(this.id); return true; } moveToNextPlayer(): void { // Clear previous timer this.clearActionTimer(); // Check if only one player remains (not folded) const activePlayers = this.players.filter(p => !p.folded); if (activePlayers.length === 1) { console.log("Only one player remains, they win automatically"); // Award pot to the last remaining player activePlayers[0].chips += this.pot; this.pot = 0; // Move to showdown to end the round this.stage = GameStage.SHOWDOWN; // Broadcast the update so players can see the winner broadcastTableUpdate(this.id); // Start a new game after a delay console.log("Starting new game in 5 seconds"); setTimeout(() => { this.startGame(); broadcastTableUpdate(this.id); }, 5000); return; } // Check if round is complete if (this.isRoundComplete()) { console.log("Round is complete, move to next state"); this.moveToNextStage(); return; } // Find next active player let nextPlayerIndex = (this.currentPlayerIndex + 1) % this.players.length; while ( this.players[nextPlayerIndex].folded || this.players[nextPlayerIndex].isAllIn || (this.currentBet > 0 && this.players[nextPlayerIndex].bet === this.currentBet && this.stage !== GameStage.PRE_FLOP) ) { nextPlayerIndex = (nextPlayerIndex + 1) % this.players.length; // If we've gone full circle, end the round if (nextPlayerIndex === this.currentPlayerIndex) { console.log("Got full circle, move to next state"); this.moveToNextStage(); return; } } this.currentPlayerIndex = nextPlayerIndex; this.players[this.currentPlayerIndex].isActive = true; // Set action timeout for current player this.setActionTimer(); // Broadcast update to let clients know whose turn it is broadcastTableUpdate(this.id); } // Add method to set the timer // Add a property to track when the timer started private actionTimerStartTime: number = 0; // Modify setActionTimer method to record start time private setActionTimer(): void { console.log('setActionTimer') // Make sure to clear previous timer first this.clearActionTimer(); // Record the start time this.actionTimerStartTime = Date.now(); // Set new timer this.actionTimer = setTimeout(() => { // If timer triggers, perform automatic action if (this.currentPlayerIndex >= 0 && this.currentPlayerIndex < this.players.length) { const player = this.players[this.currentPlayerIndex]; console.log(`Player ${player.name} (${player.id}) timeout, performing auto action`); // Try to Check first, if not possible then Fold const canCheck = player.bet >= this.currentBet; if (canCheck) { console.log(`Auto checking for player ${player.name}`); this.handlePlayerAction(player.id, PlayerAction.CHECK); } else { console.log(`Auto folding for player ${player.name}`); this.handlePlayerAction(player.id, PlayerAction.FOLD); } } }, this.actionTimeoutSeconds * 1000); } // Add method to clear the timer private clearActionTimer(): void { if (this.actionTimer) { clearTimeout(this.actionTimer); this.actionTimer = null; } } isRoundComplete(): boolean { // Check if all players have folded except one const activePlayers = this.players.filter(p => !p.folded); if (activePlayers.length === 1) { return true; } // Make sure all non-folded players have had a chance to act in this stage const nonFoldedPlayers = this.players.filter(p => !p.folded); const allPlayersHaveActed = nonFoldedPlayers.every(p => p.hasActedThisStage || p.isAllIn); if (!allPlayersHaveActed) { return false; } // Check if all remaining players have bet the same amount or are all-in return this.players.every(p => p.folded || p.isAllIn || p.isChecked || this.currentBet > 0 && p.bet === this.currentBet ); } // New method to check if all active players have checked private haveAllPlayersChecked(): boolean { // Get active players who haven't folded or gone all-in const activePlayers = this.players.filter(p => !p.folded && !p.isAllIn); // If there are no active players or just one, return true if (activePlayers.length <= 1) { return true; } // Check if all active players have checked return activePlayers.every(p => p.isChecked); } moveToNextStage(): void { // Reset player bets for the next round this.players.forEach(p => { p.isActive = false; p.isChecked = false; // Reset isChecked when moving to next stage p.bet = 0; // Reset player bets when moving to next stage p.hasActedThisStage = false; // Reset hasActedThisStage flag for the new stage }); // Reset the current bet for the new betting round this.currentBet = 0; switch (this.stage) { case GameStage.PRE_FLOP: // Deal flop for (let i = 0; i < 3; i++) { const card = this.deck.dealCard(); if (card) { this.communityCards.push(card); } } this.stage = GameStage.FLOP; break; case GameStage.FLOP: // Deal turn const turnCard = this.deck.dealCard(); if (turnCard) { this.communityCards.push(turnCard); } this.stage = GameStage.TURN; break; case GameStage.TURN: // Deal river const riverCard = this.deck.dealCard(); if (riverCard) { this.communityCards.push(riverCard); } this.stage = GameStage.RIVER; break; case GameStage.RIVER: // Move to showdown this.stage = GameStage.SHOWDOWN; this.determineWinner(); // Broadcast the update so players can see the winner broadcastTableUpdate(this.id); console.log("Starting new game in 5 seconds"); setTimeout(() => { this.startGame(); broadcastTableUpdate(this.id); }, 5000); return; case GameStage.SHOWDOWN: // Start new game return; } // Set first player to act (after dealer) this.currentPlayerIndex = (this.dealerPosition + 1) % this.players.length; // Skip folded and all-in players while ( this.players[this.currentPlayerIndex].folded || this.players[this.currentPlayerIndex].isAllIn ) { this.currentPlayerIndex = (this.currentPlayerIndex + 1) % this.players.length; // If all players are folded or all-in, move to next stage if (this.currentPlayerIndex === (this.dealerPosition + 1) % this.players.length) { this.moveToNextStage(); return; } } this.players[this.currentPlayerIndex].isActive = true; // Set action timeout for the current player after moving to next stage this.setActionTimer(); // Broadcast the update so clients know whose turn it is broadcastTableUpdate(this.id); } determineWinner(): void { // Get all active (non-folded) players const activePlayers = this.players.filter(p => !p.folded); if (activePlayers.length === 1) { // Only one player left, they win activePlayers[0].chips += this.pot; console.log(`Player ${activePlayers[0].name} wins ${this.pot} chips as the only remaining player`); } else { // Evaluate hands for all active players const playerHands: { player: Player; handResult: HandResult }[] = []; for (const player of activePlayers) { const handResult = HandEvaluator.evaluateHand(player.hand, this.communityCards); playerHands.push({ player, handResult }); console.log(`${player.name}'s hand: ${handResult.description}`); } // Sort by hand strength (highest first) playerHands.sort((a, b) => HandEvaluator.compareHands(b.handResult, a.handResult) ); // Check for ties const winners: { player: Player; handResult: HandResult }[] = [playerHands[0]]; for (let i = 1; i < playerHands.length; i++) { if (HandEvaluator.compareHands(playerHands[0].handResult, playerHands[i].handResult) === 0) { winners.push(playerHands[i]); } else { break; // No more ties } } // Split pot among winners const winAmount = Math.floor(this.pot / winners.length); const remainder = this.pot % winners.length; for (const winner of winners) { winner.player.chips += winAmount; } // Add remainder to first winner (can't split odd chips evenly) if (remainder > 0) { winners[0].player.chips += remainder; } // Log the winners const winnerNames = winners.map(w => `${w.player.name} (${w.handResult.description})`).join(', '); console.log(`Winners: ${winnerNames} each win ${winAmount} chips`); } this.pot = 0; // Check for players with zero chips and remove them this.removePlayersWithNoChips(); } // New method to remove players with no chips private removePlayersWithNoChips(): void { const playersToRemove = this.players.filter(player => player.chips <= 0); if (playersToRemove.length > 0) { console.log(`Removing ${playersToRemove.length} players with no chips`); // Remove each player playersToRemove.forEach(player => { console.log(`Player ${player.name} has no chips left and is being removed`); const index = this.players.findIndex(p => p.id === player.id); if (index !== -1) { this.players.splice(index, 1); } }); // If not enough players left, set stage to waiting if (this.players.length < this.minPlayers) { console.log("Not enough players left, setting stage to WAITING"); this.stage = GameStage.WAITING; } } } // Add method to get remaining time private getRemainingActionTime(): number { if (!this.actionTimer) return 0; const elapsedTime = (Date.now() - this.actionTimerStartTime) / 1000; return Math.max(0, this.actionTimeoutSeconds - elapsedTime); } // Update toJSON to include the timer information toJSON() { return { id: this.id, name: this.name, players: this.players.map(p => p.toJSON()), communityCards: this.communityCards.filter(card => card !== undefined && card !== null).map(card => card.toString()), pot: this.pot, currentBet: this.currentBet, smallBlind: this.smallBlind, bigBlind: this.bigBlind, dealerPosition: this.dealerPosition, currentPlayerIndex: this.currentPlayerIndex, stage: this.stage, maxPlayers: this.maxPlayers, // Add remaining action time remainingActionTime: this.getRemainingActionTime() }; } }
ID: zu05b54ubz