Skip to content

Instantly share code, notes, and snippets.

@alekseypetrenko
Created January 25, 2024 19:44
Show Gist options
  • Save alekseypetrenko/f75052ed48f67f4ef436e6c93ac43f52 to your computer and use it in GitHub Desktop.
Save alekseypetrenko/f75052ed48f67f4ef436e6c93ac43f52 to your computer and use it in GitHub Desktop.
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();
}
}
}
@xanf
Copy link

xanf commented Jan 27, 2024

13/37

Спасибо, на удивление хорошая основа кода.
Я бы даже сказал по структуре текущего оформления - одно из лучших (с поправкой конечно на отсутствующие куски и заготовки)

Уверен, что при необходимости это можно разивать дальше

Отдельное спасибо, за то что используете игровые термины (по-русски: термины доменной области) а-ля флоп и т.д. - это правильно и хорошо

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment