diff --git a/src/basic-strategy-checker.ts b/src/basic-strategy-checker.ts index 5fc8454..461dd0a 100644 --- a/src/basic-strategy-checker.ts +++ b/src/basic-strategy-checker.ts @@ -31,7 +31,7 @@ export default class BasicStrategyChecker { return 'N'; } - const allowSplit = hand.player.hands.length < game.settings.maxHandsAllowed; + const allowSplit = hand.player.handsCount < game.settings.maxHandsAllowed; const { chart: chartGroup } = selectCharts(game.settings); const chartType = this._chartType(hand, allowSplit); diff --git a/src/cli-renderer.ts b/src/cli-renderer.ts index d551e08..0d9afc4 100644 --- a/src/cli-renderer.ts +++ b/src/cli-renderer.ts @@ -69,7 +69,7 @@ export default class CLIRenderer implements Renderer { if ( this.game.focusedHand.hasPairs && - this.game.player.hands.length < this.game.settings.maxHandsAllowed + this.game.player.handsCount < this.game.settings.maxHandsAllowed ) { choices.push('P (split)'); } diff --git a/src/game.ts b/src/game.ts index eb8c9f8..e51b1b8 100644 --- a/src/game.ts +++ b/src/game.ts @@ -98,7 +98,7 @@ export default class Game extends EventEmitter { } get focusedHand(): Hand { - return this.player.hands[this.state.focusedHandIndex]; + return this.player.getHand(this.state.focusedHandIndex); } updateSettings(settings: GameSettings): void { @@ -113,6 +113,7 @@ export default class Game extends EventEmitter { this.shoe = this.chainEmitChange(new Shoe({ settings: this.settings })); this.dealer = this.chainEmitChange( new Dealer({ + handsMax: this.settings.maxHandsAllowed, debug: this.settings.debug, strategy: PlayerStrategy.DEALER, }) @@ -123,6 +124,7 @@ export default class Game extends EventEmitter { (_item, index) => this.chainEmitChange( new Player({ + handsMax: this.settings.maxHandsAllowed, balance: this.settings.playerBankroll, blackjackPayout: this.settings.blackjackPayout, debug: this.settings.debug, @@ -151,7 +153,7 @@ export default class Game extends EventEmitter { this.emit('create-record', 'hand-result', { createdAt: Date.now(), gameId: this.gameId, - dealerHand: this.dealer.hands[0].serialize({ showHidden: true }), + dealerHand: this.dealer.firstHand.serialize({ showHidden: true }), playerHand: hand.serialize(), winner, }); @@ -226,7 +228,7 @@ export default class Game extends EventEmitter { this.emit('create-record', 'move', { createdAt: Date.now(), gameId: this.gameId, - dealerHand: this.dealer.hands[0].serialize({ showHidden: true }), + dealerHand: this.dealer.firstHand.serialize({ showHidden: true }), playerHand: this.focusedHand.serialize(), move: input, correction: typeof checkerResult === 'object' ? checkerResult.code : null, @@ -374,9 +376,9 @@ export default class Game extends EventEmitter { // Draw card for each player face up again (upcard). for (const player of this.players) { - for (const hand of player.hands) { + player.eachHand((hand) => { player.takeCard(this.shoe.drawCard(), { hand }); - } + }); } // Draw card for dealer face down (hole card). @@ -387,12 +389,12 @@ export default class Game extends EventEmitter { // Dealer peeks at the hole card if the upcard is 10 to check blackjack. if (this.dealer.upcard.value === 10 && this.dealer.holeCard.value === 11) { this.dealer.cards[0].flip(); - this.dealer.hands[0].incrementTotalsForCard(this.dealer.cards[0]); + this.dealer.firstHand.incrementTotalsForCard(this.dealer.cards[0]); for (const player of this.players) { - for (const hand of player.hands) { + player.eachHand((hand) => { player.setHandWinner({ winner: 'dealer', hand }); - } + }); } return 'waiting-for-new-game-input'; @@ -415,7 +417,7 @@ export default class Game extends EventEmitter { ...players: Player[] ): void { for (const player of players) { - for (const hand of player.hands) { + player.eachHand((hand) => { const input = player.isUser && userInput ? userInput @@ -429,7 +431,7 @@ export default class Game extends EventEmitter { if (input === 'ask-insurance') { player.useChips(amount / 2, { hand }); } - } + }); } } @@ -439,7 +441,7 @@ export default class Game extends EventEmitter { } for (const player of this.players) { - for (const hand of player.hands) { + player.eachHand((hand) => { player.setHandWinner({ winner: 'dealer', hand }); // TODO: Store this in state so we don't have to check it again. @@ -454,7 +456,7 @@ export default class Game extends EventEmitter { player === this.player ? this.betAmount : this.settings.minimumBet ); } - } + }); } } @@ -503,7 +505,7 @@ export default class Game extends EventEmitter { if ( input === 'split' && - player.hands.length < this.settings.maxHandsAllowed + player.handsCount < this.settings.maxHandsAllowed ) { const newHandCard = hand.removeCard(); @@ -556,7 +558,7 @@ export default class Game extends EventEmitter { playNPCHands(...players: Player[]): void { for (const player of players) { - for (const hand of player.hands) { + player.eachHand((hand) => { let handFinished = false; while (!handFinished) { @@ -567,7 +569,7 @@ export default class Game extends EventEmitter { player.getNPCInput(this, hand) ); } - } + }); } } @@ -580,7 +582,7 @@ export default class Game extends EventEmitter { ); if (handFinished) { - if (this.state.focusedHandIndex < this.player.hands.length - 1) { + if (this.state.focusedHandIndex < this.player.handsCount - 1) { this.state.focusedHandIndex += 1; } else { return 'play-hands-left'; @@ -592,7 +594,7 @@ export default class Game extends EventEmitter { playDealer(): void { this.dealer.cards[0].flip(); - this.dealer.hands[0].incrementTotalsForCard(this.dealer.cards[0]); + this.dealer.firstHand.incrementTotalsForCard(this.dealer.cards[0]); // Dealer draws cards until they reach 17. However, if all player hands have // busted, this step is skipped. @@ -611,9 +613,9 @@ export default class Game extends EventEmitter { } for (const player of this.players) { - for (const hand of player.hands) { + player.eachHand((hand) => { if (player.handWinner.get(hand.id)) { - continue; + return; } if (this.dealer.busted) { @@ -625,7 +627,7 @@ export default class Game extends EventEmitter { } else { player.setHandWinner({ winner: 'push', hand }); } - } + }); } } diff --git a/src/hand.ts b/src/hand.ts index 200298e..89a41ac 100644 --- a/src/hand.ts +++ b/src/hand.ts @@ -15,32 +15,37 @@ export type HandAttributes = { export default class Hand extends GameObject { static entity = 'hand'; + acesCount!: number; + betAmount!: number; + cardHighTotal!: number; + cardLowTotal!: number; + cards!: Card[]; + fromSplit!: boolean; id: string; player: Player; - fromSplit: boolean; - betAmount: number; - cardHighTotal: number; - cardLowTotal: number; - acesCount: number; - cards: Card[]; constructor(player: Player, cards: Card[] = []) { super(); + this.reset(); + this.id = Utils.randomId(); this.player = player; - this.fromSplit = false; - this.betAmount = 0; - this.cardHighTotal = 0; - this.cardLowTotal = 0; - this.acesCount = 0; - this.cards = []; for (const card of cards) { this.takeCard(card); } } + reset(): void { + this.acesCount = 0; + this.betAmount = 0; + this.cardHighTotal = 0; + this.cardLowTotal = 0; + this.cards = []; + this.fromSplit = false; + } + takeCard(card: Card, { prepend = false } = {}): void { card.on('change', () => this.emitChange()); @@ -85,12 +90,9 @@ export default class Hand extends GameObject { // TODO: Remove change handler when removing cards. removeCards(): Card[] { - const cards = this.cards.slice(); - this.cards = []; - this.cardHighTotal = 0; - this.cardLowTotal = 0; - this.acesCount = 0; + const cards = this.cards; + this.reset(); this.emitChange(); return cards; diff --git a/src/hi-lo-deviation-checker.ts b/src/hi-lo-deviation-checker.ts index 61510bd..7500a90 100644 --- a/src/hi-lo-deviation-checker.ts +++ b/src/hi-lo-deviation-checker.ts @@ -93,7 +93,7 @@ export default class HiLoDeviationChecker { return { correctMove: false }; } - const allowSplit = hand.player.hands.length < game.settings.maxHandsAllowed; + const allowSplit = hand.player.handsCount < game.settings.maxHandsAllowed; if (correctMove === 'P' && !allowSplit) { return { correctMove: false }; } diff --git a/src/player.ts b/src/player.ts index 0ed80e8..7f26306 100644 --- a/src/player.ts +++ b/src/player.ts @@ -33,20 +33,25 @@ export default class Player extends GameObject { balance: number; blackjackPayout: blackjackPayouts; debug: boolean; - hands: Hand[]; + handsCount: number; + handsMax: number; handWinner: Map; id: string; strategy: PlayerStrategy; + _hands: Hand[]; + constructor({ balance = 10000 * 100, blackjackPayout = '3:2', debug = false, + handsMax, strategy, }: { balance?: number; blackjackPayout?: blackjackPayouts; debug?: boolean; + handsMax: number; strategy: PlayerStrategy; }) { super(); @@ -54,10 +59,28 @@ export default class Player extends GameObject { this.balance = balance; this.blackjackPayout = blackjackPayout; this.debug = debug; - this.hands = []; + this.handsCount = 0; + this.handsMax = handsMax; this.handWinner = new Map(); this.id = Utils.randomId(); this.strategy = strategy; + + this._hands = Array.from({ length: handsMax }, () => { + // TODO: Use `chainEmitChange`. + const hand = new Hand(this); + hand.on('change', () => this.emitChange()); + return hand; + }); + } + + getHand(index: number): Hand { + return this._hands[index]; + } + + eachHand(callback: (hand: Hand) => void): void { + for (let i = 0; i < this.handsCount; i += 1) { + callback(this._hands[i]); + } } getNPCInput(game: Game, hand: Hand): actions { @@ -76,7 +99,7 @@ export default class Player extends GameObject { this.strategy, this.id, 'dealer', - game.dealer.hands[0].serialize(), + game.dealer.firstHand.serialize(), 'player', hand.serialize(), `(${hand.cardTotal})`, @@ -88,10 +111,10 @@ export default class Player extends GameObject { } addHand(betAmount = 0, cards: Card[] = []): Hand { - const hand = new Hand(this, cards); - hand.on('change', () => this.emitChange()); + this.handsCount += 1; - this.hands.push(hand); + const hand = this._hands[this.handsCount - 1]; + hand.cards = cards; if (betAmount !== 0) { this.useChips(betAmount, { hand }); @@ -110,7 +133,7 @@ export default class Player extends GameObject { return; } - const targetHand = hand ?? this.hands[0] ?? this.addHand(); + const targetHand = hand ?? this._hands[0]; targetHand.takeCard(card, { prepend }); if (this.debug) { @@ -126,17 +149,12 @@ export default class Player extends GameObject { this.emitChange(); } - removeCards({ hand }: { hand?: Hand } = {}): Card[] { - if (hand) { - return hand.removeCards(); - } else { - const cards = Utils.arrayFlatten( - this.hands.map((hand) => hand.removeCards()) - ); - this.hands = []; - this.emitChange(); - return cards; + removeCards(): void { + for (let i = 0; i < this.handsCount; i += 1) { + this._hands[i].removeCards(); } + + this.handsCount = 0; } attributes(): PlayerAttributes { @@ -160,7 +178,7 @@ export default class Player extends GameObject { useChips(betAmount: number, { hand }: { hand?: Hand } = {}): void { if (!hand) { - hand = this.hands[0]; + hand = this._hands[0]; } if (!hand) { @@ -198,7 +216,7 @@ export default class Player extends GameObject { } setHandWinner({ - hand = this.hands[0], + hand = this._hands[0], winner, surrender = false, }: { @@ -239,6 +257,14 @@ export default class Player extends GameObject { this.emit('hand-winner', hand, winner); } + get hands(): Hand[] { + return this._hands.slice(0, this.handsCount); + } + + get firstHand(): Hand { + return this._hands[0]; + } + get isUser(): boolean { return this.strategy === PlayerStrategy.USER_INPUT; } @@ -249,36 +275,36 @@ export default class Player extends GameObject { // TODO: Consider using `Proxy`. get cards(): Card[] { - return this.hands[0].cards; + return this._hands[0].cards; } // TODO: Consider using `Proxy`. get busted(): boolean { - return this.hands[0].busted; + return this._hands[0].busted; } // TODO: Consider using `Proxy`. get blackjack(): boolean { - return this.hands[0].blackjack; + return this._hands[0].blackjack; } // TODO: Consider using `Proxy`. get cardTotal(): number { - return this.hands[0].cardTotal; + return this._hands[0].cardTotal; } // TODO: Consider using `Proxy`. get hasPairs(): boolean { - return this.hands[0].hasPairs; + return this._hands[0].hasPairs; } // TODO: Consider using `Proxy`. get isSoft(): boolean { - return this.hands[0].isSoft; + return this._hands[0].isSoft; } // TODO: Consider using `Proxy`. get isHard(): boolean { - return this.hands[0].isHard; + return this._hands[0].isHard; } }