Created
January 25, 2024 19:44
-
-
Save alekseypetrenko/f75052ed48f67f4ef436e6c93ac43f52 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { | |
CardGroup, | |
OddsCalculator, | |
type Card as PokerToolsCard, | |
} from "poker-tools"; | |
// Готовая функция для перемешивания колоды | |
export function shuffle<T>(array: Array<T>) { | |
let currentIndex = array.length, | |
randomIndex; | |
while (currentIndex != 0) { | |
randomIndex = Math.floor(Math.random() * currentIndex); | |
currentIndex--; | |
// @ts-expect-error This is fine. | |
[array[currentIndex], array[randomIndex]] = [ | |
array[randomIndex], | |
array[currentIndex], | |
]; | |
} | |
return array; | |
} | |
// Функция сна | |
// Спать надо | |
// * на 1 секунду - после раздачи карт игрокам | |
// * на 1 секунду - после раздачи 3х карт на стол | |
// * на 1 секунду - после раздачи 4й карты на стол | |
// * на 1 секунду - после раздачи 5й карты на стол | |
// * на 1 секунду - после раздачи каждого выигрыша | |
const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); | |
type Card = string; | |
type PlayerAction = | |
| { | |
type: "fold"; | |
} | |
| { | |
type: "bet"; | |
amount: number; | |
}; | |
// Функция генерации новой колоды | |
// Возвращает массив из 52 карт | |
// Каждая карта - строка из 2х символов | |
// Первый символ - номер карты | |
// Второй символ - масть карты | |
function generateNewDeck() { | |
const suits = "hdcs"; | |
const numbers = "A23456789TJQK"; | |
const deck = [...suits] | |
.map((suit) => [...numbers].map((number) => `${number}${suit}`)) | |
.flat(); | |
return shuffle(deck); | |
} | |
type PlayerId = string; | |
type GameConfigType = { | |
smallBlind: number; | |
bigBlind: number; | |
antes: number; | |
timeLimit: number; | |
}; | |
type Pot = { | |
potId: string; | |
amount: number; | |
eligiblePlayers: Set<PlayerId>; | |
}; | |
type Seat = { | |
playerId: PlayerId; | |
stack: number; | |
}; | |
type CurrencyType = number; | |
export interface HandInterface { | |
getState(): { | |
// Карты на столе | |
communityCards: Card[]; | |
// Карты игроков | |
holeCards: Record<PlayerId, [Card, Card]>; | |
// Банки на столе. potId - произвольный уникальный идентификатор | |
pots: { potId: string; amount: number }[]; | |
// Ставки игроков в текущем раунде | |
bets: Record<PlayerId, number>; | |
// На сколько игроки должны поднять ставку, чтобы сделать минимальный рейз | |
minRaise: CurrencyType; | |
}; | |
start(): void; | |
// Генерирует исключение если игрок пробует походить не в свой ход | |
act(playerId: PlayerId, action: PlayerAction): void; | |
isValidBet(playerId: PlayerId, amount: number): boolean; | |
getSeatByPlayerId(playerId: PlayerId): Seat | undefined; | |
} | |
export class Hand implements HandInterface { | |
private makeDeck: () => Card[]; | |
private seats: Seat[]; | |
private holeCards: Record<PlayerId, [Card, Card]> = {}; | |
private communityCards: Card[]; | |
private gameConfig: GameConfigType; | |
private bets: Record<PlayerId, number> = {}; | |
private pots: { potId: string; amount: number }[]; | |
private smallBlindPosition: number; | |
private bigBlindPosition: number; | |
private deck: Card[]; | |
private currentPlayerIndex: number; | |
private minRaise: CurrencyType = 0; | |
private foldedPlayers: Set<PlayerId> = new Set(); | |
private highestBet: number = 0; | |
private totalAntePot: number = 0; | |
private allInPlayers: Set<PlayerId> = new Set(); | |
constructor( | |
// Игроки за столом. Первый игрок - дилер | |
// Можете считать что у всех игроков есть хотя бы 1 фишка | |
seats: Seat[], | |
gameConfig: GameConfigType, | |
injections: { | |
// Функция генерации колоды, значение по умолчанию - generateNewDeck | |
makeDeck?: () => string[]; | |
// Функция сна, значение по умолчанию - sleep | |
sleep?: (ms: number) => Promise<unknown>; | |
// Функция вызываемая когда надо выдать банк игрокам | |
givePots?: (winners: { | |
// Идентификаторы игроков которые выиграли этот банк | |
playerIds: PlayerId[]; | |
// Карты, благодаря которым банк выигран (они подсвечиваются при выигрыше) | |
winningCards: Card[]; | |
// Уникальный идентификатор банка | |
potId: string; | |
}) => void; | |
} = {} | |
) { | |
this.makeDeck = injections.makeDeck || generateNewDeck; | |
this.seats = seats; | |
this.communityCards = []; | |
this.gameConfig = gameConfig; | |
this.pots = []; | |
this.smallBlindPosition = -1; | |
this.bigBlindPosition = -1; | |
this.deck = []; | |
this.currentPlayerIndex = -1; | |
} | |
isPreFlop() { | |
return this.communityCards.length === 0; | |
} | |
isFlop() { | |
return this.communityCards.length === 3; | |
} | |
isTurn() { | |
return this.communityCards.length === 4; | |
} | |
isRiver() { | |
return this.communityCards.length === 5; | |
} | |
private dealFlop() { | |
if (this.deck.length < 3) { | |
throw new Error("Not enough cards in the deck to deal the flop"); | |
} | |
for (let i = 0; i < 3; i++) { | |
const card = this.deck.pop(); | |
if (card) { | |
this.communityCards.push(card); | |
} | |
} | |
// await this.sleep(1000); | |
} | |
private moveToTheNextPlayer(): void { | |
const totalSeats = this.seats.length; | |
let nextPlayerIndex = (this.currentPlayerIndex + 1) % totalSeats; | |
this.currentPlayerIndex = nextPlayerIndex; | |
} | |
private takeAntes() { | |
if (this.gameConfig.antes > 0) { | |
this.seats.forEach((seat) => { | |
const anteAmount = Math.min(this.gameConfig.antes, seat.stack); | |
seat.stack -= anteAmount; | |
this.totalAntePot += anteAmount; | |
if (anteAmount < this.gameConfig.antes) { | |
this.allInPlayers.add(seat.playerId); | |
this.bets[seat.playerId] = | |
(this.bets[seat.playerId] || 0) + anteAmount; | |
} | |
}); | |
} | |
} | |
private dealCards() { | |
this.seats.forEach((player) => { | |
const card1 = this.deck.pop(); | |
const card2 = this.deck.pop(); | |
if (card1 && card2) { | |
this.holeCards[player.playerId] = [card1, card2]; | |
} | |
}); | |
} | |
start() { | |
this.deck = this.makeDeck(); | |
this.takeAntes(); | |
this.dealCards(); | |
const { smallBlind, bigBlind, antes } = this.gameConfig; | |
this.smallBlindPosition = this.seats.length > 2 ? 1 : 0; | |
this.bigBlindPosition = this.seats.length > 2 ? 2 : 1; | |
const smallBlindSeat = this.seats[this.smallBlindPosition]; | |
const bigBlindSeat = this.seats[this.bigBlindPosition]; | |
// Bet small blind and big blind | |
if (smallBlindSeat) { | |
this.bets[smallBlindSeat.playerId] = | |
(this.bets[smallBlindSeat.playerId] || 0) + smallBlind; | |
smallBlindSeat.stack -= smallBlind; | |
} | |
if (bigBlindSeat) { | |
this.bets[bigBlindSeat.playerId] = | |
(this.bets[bigBlindSeat.playerId] || 0) + bigBlind; | |
this.highestBet = bigBlind; | |
bigBlindSeat.stack -= bigBlind; | |
} | |
this.currentPlayerIndex = (this.bigBlindPosition + 1) % this.seats.length; | |
this.minRaise = bigBlind * 2; | |
// Initialize pots TODO | |
this.pots = [ | |
{ | |
potId: "main", | |
amount: smallBlind + bigBlind + this.seats.length * antes, | |
}, | |
]; | |
} | |
getState() { | |
return { | |
communityCards: this.communityCards, | |
holeCards: this.holeCards, | |
pots: this.pots, | |
bets: this.bets, | |
minRaise: this.minRaise, | |
}; | |
} | |
isValidBet(playerId: string, amount: number): boolean { | |
const player = this.getSeatByPlayerId(playerId); | |
if (!player) { | |
return false; | |
} | |
if (amount < 0) { | |
console.log("amount < 0"); | |
return false; | |
} | |
if (amount > player.stack) { | |
console.log("amount > player.stack"); | |
return false; | |
} | |
const prevBet = this.bets[playerId] || 0; | |
if (prevBet + amount < this.highestBet) { | |
console.log("prevBet + amount < this.highestBet"); | |
return false; | |
} | |
return true; | |
} | |
getSeatByPlayerId(playerId: PlayerId): Seat | undefined { | |
return this.seats.find((seat) => seat.playerId === playerId); | |
} | |
shouldDealFlop() { | |
const activePlayers = this.seats.filter( | |
(seat) => | |
!this.foldedPlayers.has(seat.playerId) && | |
!this.allInPlayers.has(seat.playerId) | |
); | |
if (activePlayers.length === 0) { | |
return false; | |
} | |
const allMatchedBet = activePlayers.every((seat) => { | |
const playerBet = this.bets[seat.playerId] || 0; | |
return playerBet === this.highestBet; | |
}); | |
if (this.highestBet > this.gameConfig.bigBlind) { | |
return allMatchedBet; | |
} else { | |
return allMatchedBet && this.currentPlayerIndex === this.bigBlindPosition; | |
} | |
} | |
act(playerId: PlayerId, action: PlayerAction): void { | |
const currentPlayerSeat = this.seats[this.currentPlayerIndex]; | |
if (!currentPlayerSeat || playerId !== currentPlayerSeat.playerId) { | |
throw new Error("Not the player's turn"); | |
} | |
switch (action.type) { | |
case "bet": | |
if (!this.isValidBet(playerId, action.amount)) { | |
throw new Error("Invalid bet amount"); | |
} | |
currentPlayerSeat.stack -= action.amount; | |
this.bets[playerId] = (this.bets[playerId] || 0) + action.amount; | |
if (action.amount > 0 && (this.bets[playerId] || 0) > this.highestBet) { | |
this.highestBet = this.bets[playerId] || 0; | |
} | |
break; | |
case "fold": | |
this.foldedPlayers.add(playerId); | |
break; | |
} | |
if (this.isPreFlop() && this.shouldDealFlop()) { | |
this.dealFlop(); | |
} else { | |
this.moveToTheNextPlayer(); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
13/37
Спасибо, на удивление хорошая основа кода.
Я бы даже сказал по структуре текущего оформления - одно из лучших (с поправкой конечно на отсутствующие куски и заготовки)
Уверен, что при необходимости это можно разивать дальше
Отдельное спасибо, за то что используете игровые термины (по-русски: термины доменной области) а-ля флоп и т.д. - это правильно и хорошо