CasinoGames/Baccarat/Engine/BaccaratEngine.swift

280 lines
8.7 KiB
Swift

//
// BaccaratEngine.swift
// Baccarat
//
// Core game engine implementing all baccarat rules including third card logic.
//
import Foundation
import CasinoKit
/// The baccarat game engine implementing Punto Banco rules.
struct BaccaratEngine {
private(set) var shoe: Shoe
private(set) var playerHand: Hand
private(set) var bankerHand: Hand
/// Creates a new engine with a fresh shoe.
init(deckCount: Int = 8) {
shoe = Shoe(deckCount: deckCount)
playerHand = Hand()
bankerHand = Hand()
// Burn first card according to casino rules
shoe.burn(1)
}
/// Clears hands and checks if shoe needs reshuffling.
mutating func prepareNewRound() {
playerHand.clear()
bankerHand.clear()
if shoe.needsReshuffle {
shoe.shuffle()
shoe.burn(1)
}
}
/// Deals the initial two cards to both Player and Banker.
/// Returns the cards in order they were dealt: P1, B1, P2, B2
mutating func dealInitialCards() -> [Card] {
var dealtCards: [Card] = []
// Deal alternating: Player, Banker, Player, Banker
if let p1 = shoe.deal() {
playerHand.addCard(p1)
dealtCards.append(p1)
}
if let b1 = shoe.deal() {
bankerHand.addCard(b1)
dealtCards.append(b1)
}
if let p2 = shoe.deal() {
playerHand.addCard(p2)
dealtCards.append(p2)
}
if let b2 = shoe.deal() {
bankerHand.addCard(b2)
dealtCards.append(b2)
}
return dealtCards
}
/// Determines if the Player should draw a third card.
/// Player draws on 0-5, stands on 6-7. Natural (8-9) prevents drawing.
func shouldPlayerDraw() -> Bool {
// Check for naturals first
if playerHand.isNatural || bankerHand.isNatural {
return false
}
return playerHand.value <= 5
}
/// Draws a third card for the Player if rules allow.
/// - Returns: The drawn card, or nil if Player stands.
mutating func drawPlayerThirdCard() -> Card? {
guard shouldPlayerDraw() else { return nil }
if let card = shoe.deal() {
playerHand.addCard(card)
return card
}
return nil
}
/// Determines if the Banker should draw a third card.
/// This follows the complex Punto Banco third card rules.
func shouldBankerDraw() -> Bool {
// Check for naturals first
if playerHand.isNatural || bankerHand.isNatural {
return false
}
let bankerValue = bankerHand.value
// If Player didn't draw (stood on 6-7), Banker uses simple rules
if playerHand.cardCount == 2 {
return bankerValue <= 5
}
// Player drew a third card - apply complex Banker rules
guard let playerThirdCard = playerHand.thirdCard else {
return bankerValue <= 5
}
let p3Value = playerThirdCard.baccaratValue
// Banker third card rules based on Banker's total and Player's third card
switch bankerValue {
case 0, 1, 2:
// Banker always draws on 0-2
return true
case 3:
// Banker draws unless Player's third card was 8
return p3Value != 8
case 4:
// Banker draws if Player's third card was 2-7
return (2...7).contains(p3Value)
case 5:
// Banker draws if Player's third card was 4-7
return (4...7).contains(p3Value)
case 6:
// Banker draws if Player's third card was 6-7
return (6...7).contains(p3Value)
case 7:
// Banker stands on 7
return false
default:
// 8-9 are naturals, shouldn't reach here
return false
}
}
/// Draws a third card for the Banker if rules allow.
/// - Returns: The drawn card, or nil if Banker stands.
mutating func drawBankerThirdCard() -> Card? {
guard shouldBankerDraw() else { return nil }
if let card = shoe.deal() {
bankerHand.addCard(card)
return card
}
return nil
}
/// Determines the winner of the current round.
func determineResult() -> GameResult {
let playerValue = playerHand.value
let bankerValue = bankerHand.value
if playerValue > bankerValue {
return .playerWins
} else if bankerValue > playerValue {
return .bankerWins
} else {
return .tie
}
}
// MARK: - Side Bet Checks
/// Whether the Player hand has a pair (first two cards same rank).
var playerHasPair: Bool {
guard playerHand.cardCount >= 2,
let first = playerHand.cards.first,
let second = playerHand.cards.dropFirst().first else {
return false
}
return first.rank == second.rank
}
/// Whether the Banker hand has a pair (first two cards same rank).
var bankerHasPair: Bool {
guard bankerHand.cardCount >= 2,
let first = bankerHand.cards.first,
let second = bankerHand.cards.dropFirst().first else {
return false
}
return first.rank == second.rank
}
/// The margin of victory for Player (positive) or Banker (negative).
/// Zero means tie.
var victoryMargin: Int {
playerHand.value - bankerHand.value
}
/// Whether the winning hand had a natural (8 or 9).
var winnerHadNatural: Bool {
let result = determineResult()
switch result {
case .playerWins: return playerHand.isNatural
case .bankerWins: return bankerHand.isNatural
case .tie: return false
}
}
// MARK: - Payout Calculations
/// Calculates the payout for a bet given the result.
/// - Parameters:
/// - bet: The bet that was placed.
/// - result: The result of the round.
/// - Returns: The net winnings (positive), net loss (negative), or 0 for push.
func calculatePayout(bet: Bet, result: GameResult) -> Int {
switch bet.type {
case .player, .banker, .tie:
return calculateMainBetPayout(bet: bet, result: result)
case .playerPair:
return playerHasPair ? Int(Double(bet.amount) * bet.type.payoutMultiplier) : -bet.amount
case .bankerPair:
return bankerHasPair ? Int(Double(bet.amount) * bet.type.payoutMultiplier) : -bet.amount
case .dragonBonusPlayer:
return calculateDragonBonusPayout(bet: bet, forPlayer: true, result: result)
case .dragonBonusBanker:
return calculateDragonBonusPayout(bet: bet, forPlayer: false, result: result)
}
}
/// Calculates payout for main bets (Player, Banker, Tie).
private func calculateMainBetPayout(bet: Bet, result: GameResult) -> Int {
if result.isPush(for: bet.type) {
// Push - bet is returned
return 0
}
if result.isWinningBet(bet.type) {
// Win - return winnings based on payout multiplier
return Int(Double(bet.amount) * bet.type.payoutMultiplier)
} else {
// Loss - lose the bet amount
return -bet.amount
}
}
/// Calculates Dragon Bonus payout.
private func calculateDragonBonusPayout(bet: Bet, forPlayer: Bool, result: GameResult) -> Int {
// Determine if the side we bet on won
let ourSideWon = forPlayer ? (result == .playerWins) : (result == .bankerWins)
if !ourSideWon {
// Dragon Bonus loses if our side didn't win (including ties)
return -bet.amount
}
// Calculate margin
let margin = abs(victoryMargin)
let isNatural = forPlayer ? playerHand.isNatural : bankerHand.isNatural
// Get the multiplier
if let multiplier = DragonBonusPayout.multiplier(for: margin, isNatural: isNatural) {
return bet.amount * multiplier
} else {
// Win by less than 4 - loses
return -bet.amount
}
}
/// Plays a complete round automatically and returns the result.
/// Used for simulation/testing purposes.
mutating func playRound() -> GameResult {
prepareNewRound()
_ = dealInitialCards()
_ = drawPlayerThirdCard()
_ = drawBankerThirdCard()
return determineResult()
}
}