280 lines
8.7 KiB
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()
|
|
}
|
|
}
|