Skip to content

Instantly share code, notes, and snippets.

@ivan-khuda
Created January 26, 2024 18:28
Show Gist options
  • Save ivan-khuda/6241cd776a3d400f479d4b39353eca5b to your computer and use it in GitHub Desktop.
Save ivan-khuda/6241cd776a3d400f479d4b39353eca5b to your computer and use it in GitHub Desktop.
Hand
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;
}
}
@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