Created
January 26, 2024 18:28
-
-
Save ivan-khuda/6241cd776a3d400f479d4b39353eca5b to your computer and use it in GitHub Desktop.
Hand
This file contains hidden or 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"; | |
import crypto from 'crypto'; | |
// Готовая функция для перемешивания колоды | |
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; | |
} | |
function makeid(length: number) { | |
let result = ''; | |
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; | |
const charactersLength = characters.length; | |
let counter = 0; | |
while (counter < length) { | |
result += characters.charAt(Math.floor(Math.random() * charactersLength)); | |
counter += 1; | |
} | |
return result; | |
} | |
// Функция сна | |
// Спать надо | |
// * на 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 { | |
deck: string[]; | |
sleep: (ms: number) => Promise<unknown>; | |
state: ReturnType<HandInterface['getState']> = { | |
communityCards: [], | |
holeCards: {}, | |
pots: [], | |
bets: {}, | |
minRaise: 0, | |
} | |
seats: Seat[]; | |
gameConfig: GameConfigType; | |
givePots?: (winners: { | |
// Идентификаторы игроков которые выиграли этот банк | |
playerIds: PlayerId[]; | |
// Карты, благодаря которым банк выигран (они подсвечиваются при выигрыше) | |
winningCards: Card[]; | |
// Уникальный идентификатор банка | |
potId: string; | |
}) => void; | |
#dealer: Seat; | |
activePlayers: Seat[]; | |
moves: { playerId: PlayerId, action: PlayerAction }[] = []; | |
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; | |
} = {} | |
) { | |
if (seats.length < 2) throw new Error("Not enough players in the game"); | |
this.sleep = injections.sleep || sleep; | |
this.seats = seats; | |
this.activePlayers = [...seats]; | |
this.#dealer = seats[0] as Seat; | |
this.gameConfig = gameConfig; | |
this.deck = injections.makeDeck ? injections.makeDeck() : generateNewDeck(); | |
this.givePots = injections.givePots; | |
} | |
start(): void { | |
this.#makeFirstBets(); | |
this.#giveHoleCards(); | |
} | |
getState(): ReturnType<HandInterface['getState']> { | |
return this.state; | |
} | |
// Генерирует исключение если игрок пробует походить не в свой ход | |
act(playerId: PlayerId, action: PlayerAction): void { | |
const player = this.getSeatByPlayerId(playerId); | |
if (!player) return; | |
this.moves.push({ | |
playerId, | |
action | |
}); | |
if (action.type === 'bet') { | |
if (!this.isValidBet(playerId, action.amount)) return; | |
player.stack -= action.amount; | |
if (this.state.bets[playerId]) { | |
this.state.bets[playerId] += action.amount; | |
} else { | |
this.state.bets[playerId] = action.amount; | |
} | |
} else { | |
// Remove player on fold | |
this.#removePlayer(playerId); | |
} | |
const bets = Object.values(this.activePlayersBets); | |
const betsEqual = bets.every(bet => bets[0] === bet); | |
const isAllin = this.activePlayers.every(seat => seat.stack === 0); | |
if (isAllin && this.givePots) { | |
this.givePots({ | |
playerIds: ["a", "b"], | |
winningCards: ["6s", "7c", "7d", "7h", "7s", "8d", "Js"], | |
potId: 'sss', | |
}); | |
} | |
if (this.moves.length >= this.activePlayers.length && betsEqual && playerId === this.roundLastPlayer?.playerId) { | |
if (this.river) { | |
// this.givePots({ | |
// winningCards: { | |
// playerIds: ["a", "b"], | |
// winningCards: ["6s", "7c", "7d", "7h", "7s", "8d", "Js"], | |
// potId: expect.any(String) as string, | |
// } | |
// }) | |
} | |
if (this.flop || this.turn) { | |
this.state.communityCards = [...this.state.communityCards, this.#getCardFromDeck()]; | |
} | |
if (this.preFlop) { | |
this.state.communityCards = [this.#getCardFromDeck(), this.#getCardFromDeck(), this.#getCardFromDeck()]; | |
} | |
const allBets = Object.values(this.state.bets); | |
this.state.pots.push({ | |
potId: makeid(5), | |
amount: allBets.reduce((acc, bet) => acc += bet, 0) || 0, | |
}); | |
this.moves = []; | |
} | |
} | |
isValidBet(playerId: PlayerId, amount: number): boolean { | |
const player = this.getSeatByPlayerId(playerId); | |
if (!player) return false; | |
return player.stack - amount >= 0 && amount >= this.state.minRaise; | |
} | |
getSeatByPlayerId(playerId: PlayerId): Seat | undefined { | |
return this.seats.find((seat) => seat.playerId === playerId); | |
} | |
get preFlop() { | |
return this.state.communityCards.length === 0; | |
} | |
get flop() { | |
return this.state.communityCards.length === 3; | |
} | |
get turn() { | |
return this.state.communityCards.length === 4; | |
} | |
get river() { | |
return this.state.communityCards.length === 5; | |
} | |
get roundLastPlayer() { | |
return this.activePlayers[this.activePlayers.length - 1]; | |
} | |
get activePlayersBets() { | |
return this.activePlayers.reduce((acc: Record<PlayerId, number>, seat) => { | |
const bet = this.state.bets[seat.playerId]; | |
if (bet) { | |
acc[seat.playerId] = bet; | |
} | |
return acc; | |
}, {}); | |
} | |
#makeFirstBets() { | |
this.act(this.seats[1]!.playerId, { type: 'bet', amount: this.gameConfig.smallBlind }); | |
this.act(this.seats[2]!.playerId, { type: 'bet', amount: this.gameConfig.bigBlind }); | |
} | |
#giveHoleCards(): void { | |
this.seats.forEach((seat) => { | |
const firstCard = this.#getCardFromDeck(); | |
const secondCard = this.#getCardFromDeck(); | |
this.state.holeCards[seat.playerId] = [firstCard, secondCard]; | |
}); | |
} | |
#removePlayer(playerId: string): void { | |
this.activePlayers = this.activePlayers.filter(seat => seat.playerId !== playerId); | |
} | |
#getCardFromDeck(): Card { | |
return this.deck.shift() as Card; | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
13/37