Signed-off-by: Matt Bruce <matt.bruce1@toyota.com>
This commit is contained in:
parent
55fd30d77e
commit
24a9cfb5b7
@ -105,11 +105,29 @@ final class GameState {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var playerHandValue: Int {
|
var playerHandValue: Int {
|
||||||
engine.playerHand.value
|
// Only calculate value from visible face-up cards
|
||||||
|
guard visiblePlayerCards.count == playerCardsFaceUp.count else { return 0 }
|
||||||
|
guard playerCardsFaceUp.allSatisfy({ $0 }) else {
|
||||||
|
// If not all cards are face up, calculate value from only the face-up cards
|
||||||
|
let faceUpCards = zip(visiblePlayerCards, playerCardsFaceUp)
|
||||||
|
.filter { $1 }
|
||||||
|
.map { $0.0 }
|
||||||
|
return faceUpCards.reduce(0) { $0 + $1.baccaratValue } % 10
|
||||||
|
}
|
||||||
|
return engine.playerHand.value
|
||||||
}
|
}
|
||||||
|
|
||||||
var bankerHandValue: Int {
|
var bankerHandValue: Int {
|
||||||
engine.bankerHand.value
|
// Only calculate value from visible face-up cards
|
||||||
|
guard visibleBankerCards.count == bankerCardsFaceUp.count else { return 0 }
|
||||||
|
guard bankerCardsFaceUp.allSatisfy({ $0 }) else {
|
||||||
|
// If not all cards are face up, calculate value from only the face-up cards
|
||||||
|
let faceUpCards = zip(visibleBankerCards, bankerCardsFaceUp)
|
||||||
|
.filter { $1 }
|
||||||
|
.map { $0.0 }
|
||||||
|
return faceUpCards.reduce(0) { $0 + $1.baccaratValue } % 10
|
||||||
|
}
|
||||||
|
return engine.bankerHand.value
|
||||||
}
|
}
|
||||||
|
|
||||||
// Recent results for the road display (last 20)
|
// Recent results for the road display (last 20)
|
||||||
@ -684,9 +702,11 @@ final class GameState {
|
|||||||
sound.playCardDeal()
|
sound.playCardDeal()
|
||||||
visiblePlayerCards.append(playerThird)
|
visiblePlayerCards.append(playerThird)
|
||||||
playerCardsFaceUp.append(false)
|
playerCardsFaceUp.append(false)
|
||||||
|
CasinoDesign.debugLog("🃏 Player 3rd card dealt face-down: cards=\(visiblePlayerCards.count), faceUp=\(playerCardsFaceUp)")
|
||||||
try? await Task.sleep(for: shortDelay)
|
try? await Task.sleep(for: shortDelay)
|
||||||
sound.playCardFlip()
|
sound.playCardFlip()
|
||||||
playerCardsFaceUp[2] = true
|
playerCardsFaceUp[2] = true
|
||||||
|
CasinoDesign.debugLog("🃏 Player 3rd card flipped: cards=\(visiblePlayerCards.count), faceUp=\(playerCardsFaceUp)")
|
||||||
try? await Task.sleep(for: flipDelay)
|
try? await Task.sleep(for: flipDelay)
|
||||||
} else {
|
} else {
|
||||||
visiblePlayerCards.append(playerThird)
|
visiblePlayerCards.append(playerThird)
|
||||||
@ -702,9 +722,11 @@ final class GameState {
|
|||||||
sound.playCardDeal()
|
sound.playCardDeal()
|
||||||
visibleBankerCards.append(bankerThird)
|
visibleBankerCards.append(bankerThird)
|
||||||
bankerCardsFaceUp.append(false)
|
bankerCardsFaceUp.append(false)
|
||||||
|
CasinoDesign.debugLog("🃏 Banker 3rd card dealt face-down: cards=\(visibleBankerCards.count), faceUp=\(bankerCardsFaceUp)")
|
||||||
try? await Task.sleep(for: shortDelay)
|
try? await Task.sleep(for: shortDelay)
|
||||||
sound.playCardFlip()
|
sound.playCardFlip()
|
||||||
bankerCardsFaceUp[2] = true
|
bankerCardsFaceUp[2] = true
|
||||||
|
CasinoDesign.debugLog("🃏 Banker 3rd card flipped: cards=\(visibleBankerCards.count), faceUp=\(bankerCardsFaceUp)")
|
||||||
try? await Task.sleep(for: dealDelay)
|
try? await Task.sleep(for: dealDelay)
|
||||||
} else {
|
} else {
|
||||||
visibleBankerCards.append(bankerThird)
|
visibleBankerCards.append(bankerThird)
|
||||||
|
|||||||
@ -166,6 +166,10 @@ struct GameTableView: View {
|
|||||||
onStartPlaying: {
|
onStartPlaying: {
|
||||||
showWelcome = false
|
showWelcome = false
|
||||||
state.onboarding.completeWelcome()
|
state.onboarding.completeWelcome()
|
||||||
|
// Mark all hints as shown so they don't appear
|
||||||
|
state.onboarding.markHintShown("bettingZone")
|
||||||
|
state.onboarding.markHintShown("dealButton")
|
||||||
|
state.onboarding.markHintShown("firstResult")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -206,14 +206,28 @@ struct CardsDisplayArea: View {
|
|||||||
// MARK: - Private Views
|
// MARK: - Private Views
|
||||||
|
|
||||||
private func playerHandSection(width: CGFloat) -> some View {
|
private func playerHandSection(width: CGFloat) -> some View {
|
||||||
VStack(spacing: Design.Spacing.small) {
|
// Calculate value from face-up cards
|
||||||
|
let visibleValue: Int = {
|
||||||
|
guard !playerCards.isEmpty else { return 0 }
|
||||||
|
let faceUpCards = zip(playerCards, playerCardsFaceUp)
|
||||||
|
.filter { $1 }
|
||||||
|
.map { $0.0 }
|
||||||
|
return faceUpCards.reduce(0) { $0 + $1.baccaratValue } % 10
|
||||||
|
}()
|
||||||
|
|
||||||
|
let allCardsFaceUp = !playerCards.isEmpty && playerCardsFaceUp.count == playerCards.count && playerCardsFaceUp.allSatisfy({ $0 })
|
||||||
|
let _ = CasinoDesign.debugLog("🎯 Player: cards=\(playerCards.count), faceUp=\(playerCardsFaceUp.count), allFaceUp=\(allCardsFaceUp), visibleValue=\(visibleValue)")
|
||||||
|
|
||||||
|
return VStack(spacing: Design.Spacing.small) {
|
||||||
HStack(spacing: Design.Spacing.small) {
|
HStack(spacing: Design.Spacing.small) {
|
||||||
Text("PLAYER")
|
Text("PLAYER")
|
||||||
.font(.system(size: labelFontSize, weight: .bold, design: .rounded))
|
.font(.system(size: labelFontSize, weight: .bold, design: .rounded))
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
|
|
||||||
if !playerCards.isEmpty && playerCardsFaceUp.contains(true) {
|
// Always show value when there are cards - it updates as cards become visible
|
||||||
HandValueBadge(value: playerValue, color: .blue)
|
if !playerCards.isEmpty {
|
||||||
|
HandValueBadge(value: visibleValue, color: .blue)
|
||||||
|
.animation(nil, value: visibleValue) // No animation when value changes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(minHeight: labelRowMinHeight)
|
.frame(minHeight: labelRowMinHeight)
|
||||||
@ -234,14 +248,28 @@ struct CardsDisplayArea: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private func bankerHandSection(width: CGFloat) -> some View {
|
private func bankerHandSection(width: CGFloat) -> some View {
|
||||||
VStack(spacing: Design.Spacing.small) {
|
// Calculate value from face-up cards
|
||||||
|
let visibleValue: Int = {
|
||||||
|
guard !bankerCards.isEmpty else { return 0 }
|
||||||
|
let faceUpCards = zip(bankerCards, bankerCardsFaceUp)
|
||||||
|
.filter { $1 }
|
||||||
|
.map { $0.0 }
|
||||||
|
return faceUpCards.reduce(0) { $0 + $1.baccaratValue } % 10
|
||||||
|
}()
|
||||||
|
|
||||||
|
let allCardsFaceUp = !bankerCards.isEmpty && bankerCardsFaceUp.count == bankerCards.count && bankerCardsFaceUp.allSatisfy({ $0 })
|
||||||
|
let _ = CasinoDesign.debugLog("🎯 Banker: cards=\(bankerCards.count), faceUp=\(bankerCardsFaceUp.count), allFaceUp=\(allCardsFaceUp), visibleValue=\(visibleValue)")
|
||||||
|
|
||||||
|
return VStack(spacing: Design.Spacing.small) {
|
||||||
HStack(spacing: Design.Spacing.small) {
|
HStack(spacing: Design.Spacing.small) {
|
||||||
Text("BANKER")
|
Text("BANKER")
|
||||||
.font(.system(size: labelFontSize, weight: .bold, design: .rounded))
|
.font(.system(size: labelFontSize, weight: .bold, design: .rounded))
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
|
|
||||||
if !bankerCards.isEmpty && bankerCardsFaceUp.contains(true) {
|
// Always show value when there are cards - it updates as cards become visible
|
||||||
HandValueBadge(value: bankerValue, color: .red)
|
if !bankerCards.isEmpty {
|
||||||
|
HandValueBadge(value: visibleValue, color: .red)
|
||||||
|
.animation(nil, value: visibleValue) // No animation when value changes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(minHeight: labelRowMinHeight)
|
.frame(minHeight: labelRowMinHeight)
|
||||||
|
|||||||
@ -8,28 +8,245 @@
|
|||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
final class BaccaratUITests: XCTestCase {
|
final class BaccaratUITests: XCTestCase {
|
||||||
|
|
||||||
|
var app: XCUIApplication!
|
||||||
|
|
||||||
override func setUpWithError() throws {
|
override func setUpWithError() throws {
|
||||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||||
|
|
||||||
// In UI tests it is usually best to stop immediately when a failure occurs.
|
// In UI tests it is usually best to stop immediately when a failure occurs.
|
||||||
continueAfterFailure = false
|
continueAfterFailure = false
|
||||||
|
|
||||||
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
|
app = XCUIApplication()
|
||||||
|
app.launchArguments = ["UI-TESTING"]
|
||||||
|
|
||||||
|
// In UI tests it's important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tearDownWithError() throws {
|
override func tearDownWithError() throws {
|
||||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||||
|
app = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func testExample() throws {
|
func testExample() throws {
|
||||||
// UI tests must launch the application that they test.
|
// UI tests must launch the application that they test.
|
||||||
let app = XCUIApplication()
|
|
||||||
app.launch()
|
app.launch()
|
||||||
|
|
||||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - UI Navigation Tests
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func testAppLaunches() throws {
|
||||||
|
app.launch()
|
||||||
|
XCTAssertTrue(app.exists, "App should launch successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func testTopBarElements() throws {
|
||||||
|
app.launch()
|
||||||
|
|
||||||
|
// Wait for the app to load
|
||||||
|
let balanceText = app.staticTexts.matching(identifier: "balance").firstMatch
|
||||||
|
XCTAssertTrue(balanceText.waitForExistence(timeout: 2), "Balance should be visible")
|
||||||
|
|
||||||
|
// Check settings button exists
|
||||||
|
let settingsButton = app.buttons.matching(identifier: "settings").firstMatch
|
||||||
|
XCTAssertTrue(settingsButton.exists, "Settings button should exist")
|
||||||
|
|
||||||
|
// Check help button exists
|
||||||
|
let helpButton = app.buttons.matching(identifier: "help").firstMatch
|
||||||
|
XCTAssertTrue(helpButton.exists, "Help button should exist")
|
||||||
|
|
||||||
|
// Check stats button exists
|
||||||
|
let statsButton = app.buttons.matching(identifier: "stats").firstMatch
|
||||||
|
XCTAssertTrue(statsButton.exists, "Stats button should exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func testSettingsSheet() throws {
|
||||||
|
app.launch()
|
||||||
|
|
||||||
|
// Find and tap settings button
|
||||||
|
let settingsButton = app.buttons.matching(identifier: "settings").firstMatch
|
||||||
|
XCTAssertTrue(settingsButton.waitForExistence(timeout: 2), "Settings button should exist")
|
||||||
|
settingsButton.tap()
|
||||||
|
|
||||||
|
// Wait for settings sheet to appear
|
||||||
|
let settingsSheet = app.scrollViews.firstMatch
|
||||||
|
XCTAssertTrue(settingsSheet.waitForExistence(timeout: 2), "Settings sheet should appear")
|
||||||
|
|
||||||
|
// Check for some expected elements in settings
|
||||||
|
XCTAssertTrue(app.staticTexts["Table Limits"].waitForExistence(timeout: 1) ||
|
||||||
|
app.staticTexts["Game Settings"].waitForExistence(timeout: 1),
|
||||||
|
"Settings content should be visible")
|
||||||
|
|
||||||
|
// Close settings (tap outside or dismiss button)
|
||||||
|
if app.buttons["Done"].exists {
|
||||||
|
app.buttons["Done"].tap()
|
||||||
|
} else {
|
||||||
|
// Tap outside to dismiss
|
||||||
|
app.coordinate(withNormalizedOffset: CGVector(dx: 0.1, dy: 0.1)).tap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func testHelpSheet() throws {
|
||||||
|
app.launch()
|
||||||
|
|
||||||
|
// Find and tap help button
|
||||||
|
let helpButton = app.buttons.matching(identifier: "help").firstMatch
|
||||||
|
XCTAssertTrue(helpButton.waitForExistence(timeout: 2), "Help button should exist")
|
||||||
|
helpButton.tap()
|
||||||
|
|
||||||
|
// Wait for help sheet to appear
|
||||||
|
let helpSheet = app.scrollViews.firstMatch
|
||||||
|
XCTAssertTrue(helpSheet.waitForExistence(timeout: 2), "Help sheet should appear")
|
||||||
|
|
||||||
|
// Check for rules content
|
||||||
|
XCTAssertTrue(app.staticTexts["Rules"].exists ||
|
||||||
|
app.staticTexts["How to Play"].exists,
|
||||||
|
"Rules content should be visible")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Betting Tests
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func testChipSelector() throws {
|
||||||
|
app.launch()
|
||||||
|
|
||||||
|
// Wait for chip selector to appear
|
||||||
|
let chipSelector = app.otherElements["chip-selector"]
|
||||||
|
XCTAssertTrue(chipSelector.waitForExistence(timeout: 2) ||
|
||||||
|
app.buttons.matching(NSPredicate(format: "label CONTAINS '$'")).count > 0,
|
||||||
|
"Chip selector should be visible")
|
||||||
|
|
||||||
|
// Find a chip button (they should have $ in the label)
|
||||||
|
let chipButtons = app.buttons.matching(NSPredicate(format: "label CONTAINS '$'"))
|
||||||
|
XCTAssertGreaterThan(chipButtons.count, 0, "Should have chip buttons")
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func testBettingZones() throws {
|
||||||
|
app.launch()
|
||||||
|
|
||||||
|
// Wait for betting table
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
// Check for Player, Banker, and Tie betting zones
|
||||||
|
let playerZone = app.staticTexts["PLAYER"]
|
||||||
|
let bankerZone = app.staticTexts["BANKER"]
|
||||||
|
let tieZone = app.staticTexts["TIE"]
|
||||||
|
|
||||||
|
XCTAssertTrue(playerZone.exists, "Player betting zone should exist")
|
||||||
|
XCTAssertTrue(bankerZone.exists, "Banker betting zone should exist")
|
||||||
|
XCTAssertTrue(tieZone.exists, "Tie betting zone should exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func testPlaceBet() throws {
|
||||||
|
app.launch()
|
||||||
|
|
||||||
|
// Wait for betting area
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
// Find deal button (should be disabled initially)
|
||||||
|
let dealButton = app.buttons["Deal"]
|
||||||
|
XCTAssertTrue(dealButton.waitForExistence(timeout: 2), "Deal button should exist")
|
||||||
|
|
||||||
|
// Initially deal button should be disabled
|
||||||
|
XCTAssertFalse(dealButton.isEnabled, "Deal button should be disabled before bet")
|
||||||
|
|
||||||
|
// Tap on Player betting zone
|
||||||
|
let playerZone = app.staticTexts["PLAYER"]
|
||||||
|
if playerZone.exists {
|
||||||
|
// Tap multiple times to ensure we meet minimum bet
|
||||||
|
for _ in 1...5 {
|
||||||
|
playerZone.tap()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deal button should now be enabled after placing sufficient bet
|
||||||
|
sleep(1)
|
||||||
|
// Note: Button might still be disabled if bet is below minimum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func testClearButton() throws {
|
||||||
|
app.launch()
|
||||||
|
|
||||||
|
// Wait and place a bet
|
||||||
|
sleep(1)
|
||||||
|
let playerZone = app.staticTexts["PLAYER"]
|
||||||
|
if playerZone.exists {
|
||||||
|
playerZone.tap()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find clear button
|
||||||
|
let clearButton = app.buttons["Clear"]
|
||||||
|
if clearButton.exists {
|
||||||
|
clearButton.tap()
|
||||||
|
|
||||||
|
// After clearing, deal button should be disabled again
|
||||||
|
let dealButton = app.buttons["Deal"]
|
||||||
|
XCTAssertFalse(dealButton.isEnabled, "Deal button should be disabled after clear")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Gameplay Tests
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func testDealCards() throws {
|
||||||
|
app.launch()
|
||||||
|
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
// Place a bet on Player by tapping multiple times
|
||||||
|
let playerZone = app.staticTexts["PLAYER"]
|
||||||
|
if playerZone.exists {
|
||||||
|
for _ in 1...5 {
|
||||||
|
playerZone.tap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find and tap deal button
|
||||||
|
let dealButton = app.buttons["Deal"]
|
||||||
|
if dealButton.waitForExistence(timeout: 2) && dealButton.isEnabled {
|
||||||
|
dealButton.tap()
|
||||||
|
|
||||||
|
// Wait for cards to be dealt
|
||||||
|
sleep(3)
|
||||||
|
|
||||||
|
// After dealing, we should see hand value badges or a result
|
||||||
|
// Check for the PLAYER and BANKER labels which should still be visible
|
||||||
|
let playerLabel = app.staticTexts["PLAYER"]
|
||||||
|
let bankerLabel = app.staticTexts["BANKER"]
|
||||||
|
|
||||||
|
XCTAssertTrue(playerLabel.exists, "Player label should be visible after deal")
|
||||||
|
XCTAssertTrue(bankerLabel.exists, "Banker label should be visible after deal")
|
||||||
|
|
||||||
|
// Eventually a New Round button should appear
|
||||||
|
let newRoundButton = app.buttons["New Round"]
|
||||||
|
XCTAssertTrue(newRoundButton.waitForExistence(timeout: 5), "New Round button should appear")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func testRoadMapExists() throws {
|
||||||
|
app.launch()
|
||||||
|
|
||||||
|
// Road map might not be visible on small screens or in portrait mode
|
||||||
|
// This is an optional test that checks if enabled in settings
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
// Look for history text or road map elements
|
||||||
|
let historyText = app.staticTexts["HISTORY"]
|
||||||
|
// Road map is conditional, so this is not a required assertion
|
||||||
|
// Just checking it doesn't crash
|
||||||
|
_ = historyText.exists
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func testLaunchPerformance() throws {
|
func testLaunchPerformance() throws {
|
||||||
|
|||||||
@ -74,6 +74,12 @@ final class GameState {
|
|||||||
/// Index of the hand currently being played.
|
/// Index of the hand currently being played.
|
||||||
private(set) var activeHandIndex: Int = 0
|
private(set) var activeHandIndex: Int = 0
|
||||||
|
|
||||||
|
/// Number of cards that have completed their deal animation for each player hand.
|
||||||
|
private(set) var playerHandsVisibleCardCount: [Int] = []
|
||||||
|
|
||||||
|
/// Number of cards that have completed their deal animation for dealer hand.
|
||||||
|
private(set) var dealerVisibleCardCount: Int = 0
|
||||||
|
|
||||||
/// Whether an action is currently being processed (prevents double-tap issues).
|
/// Whether an action is currently being processed (prevents double-tap issues).
|
||||||
private(set) var isProcessingAction: Bool = false
|
private(set) var isProcessingAction: Bool = false
|
||||||
|
|
||||||
@ -487,6 +493,10 @@ final class GameState {
|
|||||||
perfectPairsResult = nil
|
perfectPairsResult = nil
|
||||||
twentyOnePlusThreeResult = nil
|
twentyOnePlusThreeResult = nil
|
||||||
|
|
||||||
|
// Reset visible card counts for animations
|
||||||
|
playerHandsVisibleCardCount = [0]
|
||||||
|
dealerVisibleCardCount = 0
|
||||||
|
|
||||||
let delay = settings.showAnimations ? 0.3 * settings.dealingSpeed : 0
|
let delay = settings.showAnimations ? 0.3 * settings.dealingSpeed : 0
|
||||||
|
|
||||||
// European no-hole-card: deal 3 cards (player, dealer, player)
|
// European no-hole-card: deal 3 cards (player, dealer, player)
|
||||||
@ -504,6 +514,13 @@ final class GameState {
|
|||||||
if delay > 0 {
|
if delay > 0 {
|
||||||
try? await Task.sleep(for: .seconds(delay))
|
try? await Task.sleep(for: .seconds(delay))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mark card as visible after animation delay
|
||||||
|
if i % 2 == 0 {
|
||||||
|
playerHandsVisibleCardCount[0] += 1
|
||||||
|
} else {
|
||||||
|
dealerVisibleCardCount += 1
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -622,6 +639,15 @@ final class GameState {
|
|||||||
playerHands[activeHandIndex].cards.append(card)
|
playerHands[activeHandIndex].cards.append(card)
|
||||||
sound.play(.cardDeal)
|
sound.play(.cardDeal)
|
||||||
|
|
||||||
|
// Wait for animation if enabled
|
||||||
|
let delay = settings.showAnimations ? 0.3 * settings.dealingSpeed : 0
|
||||||
|
if delay > 0 {
|
||||||
|
try? await Task.sleep(for: .seconds(delay))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark card as visible after animation
|
||||||
|
playerHandsVisibleCardCount[activeHandIndex] += 1
|
||||||
|
|
||||||
// Check for bust or 21
|
// Check for bust or 21
|
||||||
if playerHands[activeHandIndex].isBusted {
|
if playerHands[activeHandIndex].isBusted {
|
||||||
playerHands[activeHandIndex].result = .bust
|
playerHands[activeHandIndex].result = .bust
|
||||||
@ -669,6 +695,15 @@ final class GameState {
|
|||||||
if let card = engine.dealCard() {
|
if let card = engine.dealCard() {
|
||||||
playerHands[activeHandIndex].cards.append(card)
|
playerHands[activeHandIndex].cards.append(card)
|
||||||
sound.play(.cardDeal)
|
sound.play(.cardDeal)
|
||||||
|
|
||||||
|
// Wait for animation if enabled
|
||||||
|
let delay = settings.showAnimations ? 0.3 * settings.dealingSpeed : 0
|
||||||
|
if delay > 0 {
|
||||||
|
try? await Task.sleep(for: .seconds(delay))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mark card as visible after animation
|
||||||
|
playerHandsVisibleCardCount[activeHandIndex] += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
if playerHands[activeHandIndex].isBusted {
|
if playerHands[activeHandIndex].isBusted {
|
||||||
@ -707,14 +742,22 @@ final class GameState {
|
|||||||
sound.play(.chipPlace)
|
sound.play(.chipPlace)
|
||||||
|
|
||||||
// Deal one card to each hand
|
// Deal one card to each hand
|
||||||
|
let delay = settings.showAnimations ? 0.3 * settings.dealingSpeed : 0
|
||||||
|
|
||||||
if let card1 = engine.dealCard() {
|
if let card1 = engine.dealCard() {
|
||||||
hand1.cards.append(card1)
|
hand1.cards.append(card1)
|
||||||
sound.play(.cardDeal)
|
sound.play(.cardDeal)
|
||||||
|
if delay > 0 {
|
||||||
|
try? await Task.sleep(for: .seconds(delay))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if let card2 = engine.dealCard() {
|
if let card2 = engine.dealCard() {
|
||||||
hand2.cards.append(card2)
|
hand2.cards.append(card2)
|
||||||
sound.play(.cardDeal)
|
sound.play(.cardDeal)
|
||||||
|
if delay > 0 {
|
||||||
|
try? await Task.sleep(for: .seconds(delay))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Replace original with split hands
|
// Replace original with split hands
|
||||||
@ -722,6 +765,11 @@ final class GameState {
|
|||||||
playerHands.insert(hand1, at: activeHandIndex)
|
playerHands.insert(hand1, at: activeHandIndex)
|
||||||
playerHands.insert(hand2, at: activeHandIndex + 1)
|
playerHands.insert(hand2, at: activeHandIndex + 1)
|
||||||
|
|
||||||
|
// Update visible card counts - each split hand starts with 2 visible cards
|
||||||
|
playerHandsVisibleCardCount.remove(at: activeHandIndex)
|
||||||
|
playerHandsVisibleCardCount.insert(2, at: activeHandIndex)
|
||||||
|
playerHandsVisibleCardCount.insert(2, at: activeHandIndex + 1)
|
||||||
|
|
||||||
// If split aces, typically only one card each and stand
|
// If split aces, typically only one card each and stand
|
||||||
if originalHand.cards[0].rank == .ace && !settings.resplitAces {
|
if originalHand.cards[0].rank == .ace && !settings.resplitAces {
|
||||||
playerHands[activeHandIndex].isStanding = true
|
playerHands[activeHandIndex].isStanding = true
|
||||||
@ -790,6 +838,9 @@ final class GameState {
|
|||||||
if delay > 0 {
|
if delay > 0 {
|
||||||
try? await Task.sleep(for: .seconds(delay))
|
try? await Task.sleep(for: .seconds(delay))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mark card as visible after animation
|
||||||
|
dealerVisibleCardCount += 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for dealer blackjack in European mode
|
// Check for dealer blackjack in European mode
|
||||||
@ -809,12 +860,17 @@ final class GameState {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// American style: reveal hole card
|
// American style: reveal hole card (card is already in hand, just mark as visible)
|
||||||
sound.play(.cardFlip)
|
sound.play(.cardFlip)
|
||||||
|
|
||||||
if delay > 0 {
|
if delay > 0 {
|
||||||
try? await Task.sleep(for: .seconds(delay))
|
try? await Task.sleep(for: .seconds(delay))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mark hole card as visible (if not already)
|
||||||
|
if dealerVisibleCardCount < dealerHand.cards.count {
|
||||||
|
dealerVisibleCardCount = dealerHand.cards.count
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Dealer draws
|
// Dealer draws
|
||||||
@ -826,6 +882,9 @@ final class GameState {
|
|||||||
if delay > 0 {
|
if delay > 0 {
|
||||||
try? await Task.sleep(for: .seconds(delay))
|
try? await Task.sleep(for: .seconds(delay))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Mark card as visible after animation
|
||||||
|
dealerVisibleCardCount += 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1057,6 +1116,10 @@ final class GameState {
|
|||||||
dealerHand = BlackjackHand()
|
dealerHand = BlackjackHand()
|
||||||
activeHandIndex = 0
|
activeHandIndex = 0
|
||||||
|
|
||||||
|
// Reset visible card counts
|
||||||
|
playerHandsVisibleCardCount = []
|
||||||
|
dealerVisibleCardCount = 0
|
||||||
|
|
||||||
// Reset bets
|
// Reset bets
|
||||||
currentBet = 0
|
currentBet = 0
|
||||||
insuranceBet = 0
|
insuranceBet = 0
|
||||||
|
|||||||
@ -114,6 +114,10 @@ struct GameTableView: View {
|
|||||||
onStartPlaying: {
|
onStartPlaying: {
|
||||||
showWelcome = false
|
showWelcome = false
|
||||||
state.onboarding.completeWelcome()
|
state.onboarding.completeWelcome()
|
||||||
|
// Mark all hints as shown so they don't appear
|
||||||
|
state.onboarding.markHintShown("bettingZone")
|
||||||
|
state.onboarding.markHintShown("dealButton")
|
||||||
|
state.onboarding.markHintShown("playerActions")
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -94,7 +94,8 @@ struct BlackjackTableView: View {
|
|||||||
showAnimations: state.settings.showAnimations,
|
showAnimations: state.settings.showAnimations,
|
||||||
dealingSpeed: state.settings.dealingSpeed,
|
dealingSpeed: state.settings.dealingSpeed,
|
||||||
cardWidth: cardWidth,
|
cardWidth: cardWidth,
|
||||||
cardSpacing: cardSpacing
|
cardSpacing: cardSpacing,
|
||||||
|
visibleCardCount: state.dealerVisibleCardCount
|
||||||
)
|
)
|
||||||
.debugBorder(showDebugBorders, color: .red, label: "Dealer")
|
.debugBorder(showDebugBorders, color: .red, label: "Dealer")
|
||||||
|
|
||||||
@ -127,6 +128,7 @@ struct BlackjackTableView: View {
|
|||||||
dealingSpeed: state.settings.dealingSpeed,
|
dealingSpeed: state.settings.dealingSpeed,
|
||||||
cardWidth: cardWidth,
|
cardWidth: cardWidth,
|
||||||
cardSpacing: cardSpacing,
|
cardSpacing: cardSpacing,
|
||||||
|
visibleCardCounts: state.playerHandsVisibleCardCount,
|
||||||
currentHint: state.currentHint,
|
currentHint: state.currentHint,
|
||||||
showHintToast: state.showHintToast
|
showHintToast: state.showHintToast
|
||||||
)
|
)
|
||||||
|
|||||||
@ -17,6 +17,9 @@ struct DealerHandView: View {
|
|||||||
let cardWidth: CGFloat
|
let cardWidth: CGFloat
|
||||||
let cardSpacing: CGFloat
|
let cardSpacing: CGFloat
|
||||||
|
|
||||||
|
/// Number of cards that have completed their animation
|
||||||
|
let visibleCardCount: Int
|
||||||
|
|
||||||
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.Size.handLabelFontSize
|
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.Size.handLabelFontSize
|
||||||
@ScaledMetric(relativeTo: .headline) private var badgeHeight: CGFloat = CasinoDesign.Size.valueBadge
|
@ScaledMetric(relativeTo: .headline) private var badgeHeight: CGFloat = CasinoDesign.Size.valueBadge
|
||||||
|
|
||||||
@ -34,21 +37,28 @@ struct DealerHandView: View {
|
|||||||
.font(.system(size: labelFontSize, weight: .bold, design: .rounded))
|
.font(.system(size: labelFontSize, weight: .bold, design: .rounded))
|
||||||
.foregroundStyle(.white)
|
.foregroundStyle(.white)
|
||||||
|
|
||||||
// Badge animates in when cards are dealt
|
// Calculate value from visible cards only
|
||||||
if !hand.cards.isEmpty {
|
if !hand.cards.isEmpty && visibleCardCount > 0 {
|
||||||
if showHoleCard {
|
if showHoleCard && visibleCardCount >= hand.cards.count {
|
||||||
// All cards visible - show total hand value
|
// All cards visible - calculate total hand value from visible cards
|
||||||
ValueBadge(value: hand.value, color: Color.Hand.dealer)
|
let visibleCards = Array(hand.cards.prefix(visibleCardCount))
|
||||||
.transition(.scale.combined(with: .opacity))
|
let visibleValue = visibleCards.reduce(0) { $0 + $1.blackjackValue }
|
||||||
} else {
|
let visibleHasSoftAce = visibleCards.contains { $0.rank == .ace } && visibleValue + 10 <= 21
|
||||||
// Hole card hidden - show only the first (face-up) card's value
|
let displayValue = visibleHasSoftAce && visibleValue + 10 <= 21 ? visibleValue + 10 : visibleValue
|
||||||
|
|
||||||
|
ValueBadge(value: displayValue, color: Color.Hand.dealer)
|
||||||
|
.animation(nil, value: displayValue) // No animation when value changes
|
||||||
|
} else if visibleCardCount >= 1 {
|
||||||
|
// Hole card hidden or not all cards visible - show only the first (face-up) card's value
|
||||||
ValueBadge(value: hand.cards[0].blackjackValue, color: Color.Hand.dealer)
|
ValueBadge(value: hand.cards[0].blackjackValue, color: Color.Hand.dealer)
|
||||||
.transition(.scale.combined(with: .opacity))
|
.animation(nil, value: hand.cards[0].blackjackValue) // No animation when value changes
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(minHeight: badgeHeight) // Reserve consistent height
|
.frame(minHeight: badgeHeight) // Reserve consistent height
|
||||||
.animation(.spring(duration: Design.Animation.springDuration), value: hand.cards.isEmpty)
|
// Remove animations on badge appearance/value changes
|
||||||
|
.animation(nil, value: visibleCardCount)
|
||||||
|
.animation(nil, value: showHoleCard)
|
||||||
// Cards with result badge overlay (overlay prevents height change)
|
// Cards with result badge overlay (overlay prevents height change)
|
||||||
HStack(spacing: hand.cards.isEmpty ? Design.Spacing.small : cardSpacing) {
|
HStack(spacing: hand.cards.isEmpty ? Design.Spacing.small : cardSpacing) {
|
||||||
if hand.cards.isEmpty {
|
if hand.cards.isEmpty {
|
||||||
@ -152,7 +162,8 @@ struct DealerHandView: View {
|
|||||||
showAnimations: true,
|
showAnimations: true,
|
||||||
dealingSpeed: 1.0,
|
dealingSpeed: 1.0,
|
||||||
cardWidth: 60,
|
cardWidth: 60,
|
||||||
cardSpacing: -20
|
cardSpacing: -20,
|
||||||
|
visibleCardCount: 0
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -170,7 +181,8 @@ struct DealerHandView: View {
|
|||||||
showAnimations: true,
|
showAnimations: true,
|
||||||
dealingSpeed: 1.0,
|
dealingSpeed: 1.0,
|
||||||
cardWidth: 60,
|
cardWidth: 60,
|
||||||
cardSpacing: -20
|
cardSpacing: -20,
|
||||||
|
visibleCardCount: 2
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -188,7 +200,8 @@ struct DealerHandView: View {
|
|||||||
showAnimations: true,
|
showAnimations: true,
|
||||||
dealingSpeed: 1.0,
|
dealingSpeed: 1.0,
|
||||||
cardWidth: 60,
|
cardWidth: 60,
|
||||||
cardSpacing: -20
|
cardSpacing: -20,
|
||||||
|
visibleCardCount: 2
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,6 +21,9 @@ struct PlayerHandsView: View {
|
|||||||
let cardWidth: CGFloat
|
let cardWidth: CGFloat
|
||||||
let cardSpacing: CGFloat
|
let cardSpacing: CGFloat
|
||||||
|
|
||||||
|
/// Number of visible cards for each hand (completed animations)
|
||||||
|
let visibleCardCounts: [Int]
|
||||||
|
|
||||||
/// Current hint to display (shown on active hand only).
|
/// Current hint to display (shown on active hand only).
|
||||||
let currentHint: String?
|
let currentHint: String?
|
||||||
|
|
||||||
@ -41,6 +44,7 @@ struct PlayerHandsView: View {
|
|||||||
// Play order: Hand 1 played first (rightmost), then Hand 2, etc.
|
// Play order: Hand 1 played first (rightmost), then Hand 2, etc.
|
||||||
ForEach(Array(hands.enumerated()).reversed(), id: \.element.id) { index, hand in
|
ForEach(Array(hands.enumerated()).reversed(), id: \.element.id) { index, hand in
|
||||||
let isActiveHand = index == activeHandIndex && isPlayerTurn
|
let isActiveHand = index == activeHandIndex && isPlayerTurn
|
||||||
|
let visibleCount = index < visibleCardCounts.count ? visibleCardCounts[index] : 0
|
||||||
PlayerHandView(
|
PlayerHandView(
|
||||||
hand: hand,
|
hand: hand,
|
||||||
isActive: isActiveHand,
|
isActive: isActiveHand,
|
||||||
@ -51,6 +55,7 @@ struct PlayerHandsView: View {
|
|||||||
handNumber: hands.count > 1 ? index + 1 : nil,
|
handNumber: hands.count > 1 ? index + 1 : nil,
|
||||||
cardWidth: cardWidth,
|
cardWidth: cardWidth,
|
||||||
cardSpacing: cardSpacing,
|
cardSpacing: cardSpacing,
|
||||||
|
visibleCardCount: visibleCount,
|
||||||
// Only show hint on the active hand
|
// Only show hint on the active hand
|
||||||
currentHint: isActiveHand ? currentHint : nil,
|
currentHint: isActiveHand ? currentHint : nil,
|
||||||
showHintToast: isActiveHand && showHintToast
|
showHintToast: isActiveHand && showHintToast
|
||||||
@ -105,6 +110,9 @@ struct PlayerHandView: View {
|
|||||||
let cardWidth: CGFloat
|
let cardWidth: CGFloat
|
||||||
let cardSpacing: CGFloat
|
let cardSpacing: CGFloat
|
||||||
|
|
||||||
|
/// Number of cards that have completed their animation
|
||||||
|
let visibleCardCount: Int
|
||||||
|
|
||||||
/// Current hint to display on this hand.
|
/// Current hint to display on this hand.
|
||||||
let currentHint: String?
|
let currentHint: String?
|
||||||
|
|
||||||
@ -206,10 +214,35 @@ struct PlayerHandView: View {
|
|||||||
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
.foregroundStyle(.white.opacity(Design.Opacity.medium))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Calculate value from visible (animation-completed) cards
|
||||||
|
// Always show the value - it updates as cards become visible
|
||||||
if !hand.cards.isEmpty {
|
if !hand.cards.isEmpty {
|
||||||
Text(hand.valueDisplay)
|
// Use only the cards that have completed their animation
|
||||||
|
let visibleCards = Array(hand.cards.prefix(visibleCardCount))
|
||||||
|
let visibleValue = visibleCards.reduce(0) { $0 + $1.blackjackValue }
|
||||||
|
let visibleHasSoftAce = visibleCards.contains { $0.rank == .ace } && visibleValue + 10 <= 21
|
||||||
|
let displayValue = visibleHasSoftAce && visibleValue + 10 <= 21 ? visibleValue + 10 : visibleValue
|
||||||
|
|
||||||
|
// Determine color based on visible cards
|
||||||
|
let isVisibleBlackjack = visibleCards.count == 2 && displayValue == 21
|
||||||
|
let isVisibleBusted = visibleValue > 21
|
||||||
|
let isVisible21 = displayValue == 21 && !isVisibleBlackjack
|
||||||
|
|
||||||
|
let displayColor: Color = {
|
||||||
|
if isVisibleBlackjack { return .yellow }
|
||||||
|
if isVisibleBusted { return .red }
|
||||||
|
if isVisible21 { return .green }
|
||||||
|
return .white
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Show value like hand.valueDisplay does
|
||||||
|
let valueText = visibleHasSoftAce ? "\(visibleValue)/\(displayValue)" : "\(displayValue)"
|
||||||
|
|
||||||
|
Text(valueText)
|
||||||
.font(.system(size: labelFontSize, weight: .bold, design: .rounded))
|
.font(.system(size: labelFontSize, weight: .bold, design: .rounded))
|
||||||
.foregroundStyle(valueColor)
|
.foregroundStyle(displayColor)
|
||||||
|
.animation(nil, value: valueText) // No animation when text changes
|
||||||
|
.animation(nil, value: displayColor) // No animation when color changes
|
||||||
}
|
}
|
||||||
|
|
||||||
if hand.isDoubledDown {
|
if hand.isDoubledDown {
|
||||||
@ -268,6 +301,7 @@ struct PlayerHandView: View {
|
|||||||
dealingSpeed: 1.0,
|
dealingSpeed: 1.0,
|
||||||
cardWidth: 60,
|
cardWidth: 60,
|
||||||
cardSpacing: -20,
|
cardSpacing: -20,
|
||||||
|
visibleCardCounts: [0],
|
||||||
currentHint: nil,
|
currentHint: nil,
|
||||||
showHintToast: false
|
showHintToast: false
|
||||||
)
|
)
|
||||||
@ -289,6 +323,7 @@ struct PlayerHandView: View {
|
|||||||
dealingSpeed: 1.0,
|
dealingSpeed: 1.0,
|
||||||
cardWidth: 60,
|
cardWidth: 60,
|
||||||
cardSpacing: -20,
|
cardSpacing: -20,
|
||||||
|
visibleCardCounts: [2],
|
||||||
currentHint: "Hit",
|
currentHint: "Hit",
|
||||||
showHintToast: true
|
showHintToast: true
|
||||||
)
|
)
|
||||||
@ -324,6 +359,7 @@ struct PlayerHandView: View {
|
|||||||
dealingSpeed: 1.0,
|
dealingSpeed: 1.0,
|
||||||
cardWidth: 60,
|
cardWidth: 60,
|
||||||
cardSpacing: -20,
|
cardSpacing: -20,
|
||||||
|
visibleCardCounts: [2, 2, 2, 2],
|
||||||
currentHint: "Stand",
|
currentHint: "Stand",
|
||||||
showHintToast: true
|
showHintToast: true
|
||||||
)
|
)
|
||||||
|
|||||||
@ -7,11 +7,281 @@
|
|||||||
|
|
||||||
import Testing
|
import Testing
|
||||||
@testable import Blackjack
|
@testable import Blackjack
|
||||||
|
import CasinoKit
|
||||||
|
|
||||||
struct BlackjackTests {
|
struct BlackjackTests {
|
||||||
|
|
||||||
@Test func example() async throws {
|
@Test func example() async throws {
|
||||||
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
|
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - Hand Value Tests
|
||||||
|
|
||||||
|
@Test @MainActor func handValueCalculation() async throws {
|
||||||
|
var hand = BlackjackHand()
|
||||||
|
|
||||||
|
// Test empty hand
|
||||||
|
#expect(hand.value == 0, "Empty hand should have value 0")
|
||||||
|
|
||||||
|
// Test simple values
|
||||||
|
hand = BlackjackHand(cards: [
|
||||||
|
Card(suit: .spades, rank: .five),
|
||||||
|
Card(suit: .hearts, rank: .seven)
|
||||||
|
])
|
||||||
|
#expect(hand.value == 12, "5 + 7 should equal 12")
|
||||||
|
#expect(!hand.isSoft, "Hand without ace should not be soft")
|
||||||
|
|
||||||
|
// Test face cards
|
||||||
|
hand = BlackjackHand(cards: [
|
||||||
|
Card(suit: .clubs, rank: .king),
|
||||||
|
Card(suit: .diamonds, rank: .queen)
|
||||||
|
])
|
||||||
|
#expect(hand.value == 20, "King (10) + Queen (10) should equal 20")
|
||||||
|
|
||||||
|
// Test blackjack
|
||||||
|
hand = BlackjackHand(cards: [
|
||||||
|
Card(suit: .spades, rank: .ace),
|
||||||
|
Card(suit: .hearts, rank: .king)
|
||||||
|
])
|
||||||
|
#expect(hand.value == 21, "Ace + King should equal 21")
|
||||||
|
#expect(hand.isBlackjack, "Ace + King should be blackjack")
|
||||||
|
#expect(hand.isSoft, "Blackjack hand should be soft")
|
||||||
|
|
||||||
|
// Test soft hand (ace as 11)
|
||||||
|
hand = BlackjackHand(cards: [
|
||||||
|
Card(suit: .diamonds, rank: .ace),
|
||||||
|
Card(suit: .clubs, rank: .six)
|
||||||
|
])
|
||||||
|
#expect(hand.value == 17, "Ace + 6 should equal soft 17")
|
||||||
|
#expect(hand.isSoft, "Ace + 6 should be soft")
|
||||||
|
|
||||||
|
// Test hard hand (ace as 1)
|
||||||
|
hand = BlackjackHand(cards: [
|
||||||
|
Card(suit: .spades, rank: .ace),
|
||||||
|
Card(suit: .hearts, rank: .five),
|
||||||
|
Card(suit: .clubs, rank: .nine)
|
||||||
|
])
|
||||||
|
#expect(hand.value == 15, "Ace + 5 + 9 = 15 (ace counts as 1)")
|
||||||
|
#expect(!hand.isSoft, "Hand over 21 if ace=11 should be hard")
|
||||||
|
|
||||||
|
// Test bust
|
||||||
|
hand = BlackjackHand(cards: [
|
||||||
|
Card(suit: .diamonds, rank: .king),
|
||||||
|
Card(suit: .clubs, rank: .queen),
|
||||||
|
Card(suit: .hearts, rank: .five)
|
||||||
|
])
|
||||||
|
#expect(hand.value == 25, "10 + 10 + 5 = 25")
|
||||||
|
#expect(hand.isBusted, "Hand over 21 should be busted")
|
||||||
|
|
||||||
|
// Test multiple aces
|
||||||
|
hand = BlackjackHand(cards: [
|
||||||
|
Card(suit: .spades, rank: .ace),
|
||||||
|
Card(suit: .hearts, rank: .ace)
|
||||||
|
])
|
||||||
|
#expect(hand.value == 12, "Ace + Ace = 12 (one as 11, one as 1)")
|
||||||
|
#expect(hand.isSoft, "Two aces should be soft")
|
||||||
|
|
||||||
|
// Test three aces
|
||||||
|
hand = BlackjackHand(cards: [
|
||||||
|
Card(suit: .spades, rank: .ace),
|
||||||
|
Card(suit: .hearts, rank: .ace),
|
||||||
|
Card(suit: .clubs, rank: .ace)
|
||||||
|
])
|
||||||
|
#expect(hand.value == 13, "Three aces = 13 (one as 11, two as 1)")
|
||||||
|
#expect(hand.isSoft, "Three aces should be soft")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test @MainActor func handBlackjackDetection() async throws {
|
||||||
|
// Natural blackjack
|
||||||
|
var hand = BlackjackHand(cards: [
|
||||||
|
Card(suit: .spades, rank: .ace),
|
||||||
|
Card(suit: .hearts, rank: .ten)
|
||||||
|
])
|
||||||
|
#expect(hand.isBlackjack, "Ace + 10 should be blackjack")
|
||||||
|
|
||||||
|
// 21 with three cards is not blackjack
|
||||||
|
hand = BlackjackHand(cards: [
|
||||||
|
Card(suit: .spades, rank: .seven),
|
||||||
|
Card(suit: .hearts, rank: .seven),
|
||||||
|
Card(suit: .clubs, rank: .seven)
|
||||||
|
])
|
||||||
|
#expect(hand.value == 21, "Should have value 21")
|
||||||
|
#expect(!hand.isBlackjack, "21 with 3 cards is not blackjack")
|
||||||
|
|
||||||
|
// Split hand 21 is not blackjack
|
||||||
|
hand = BlackjackHand(cards: [
|
||||||
|
Card(suit: .spades, rank: .ace),
|
||||||
|
Card(suit: .hearts, rank: .king)
|
||||||
|
], bet: 100)
|
||||||
|
hand.isSplit = true
|
||||||
|
#expect(hand.value == 21, "Should have value 21")
|
||||||
|
#expect(!hand.isBlackjack, "21 after split is not blackjack")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test @MainActor func handSplitDetection() async throws {
|
||||||
|
// Splittable pair
|
||||||
|
var hand = BlackjackHand(cards: [
|
||||||
|
Card(suit: .spades, rank: .eight),
|
||||||
|
Card(suit: .hearts, rank: .eight)
|
||||||
|
])
|
||||||
|
#expect(hand.canSplit, "Pair of 8s should be splittable")
|
||||||
|
|
||||||
|
// Face cards of same rank can split
|
||||||
|
hand = BlackjackHand(cards: [
|
||||||
|
Card(suit: .spades, rank: .king),
|
||||||
|
Card(suit: .hearts, rank: .king)
|
||||||
|
])
|
||||||
|
#expect(hand.canSplit, "Pair of Kings should be splittable")
|
||||||
|
|
||||||
|
// Face cards of different ranks cannot split
|
||||||
|
hand = BlackjackHand(cards: [
|
||||||
|
Card(suit: .spades, rank: .king),
|
||||||
|
Card(suit: .hearts, rank: .queen)
|
||||||
|
])
|
||||||
|
#expect(!hand.canSplit, "King + Queen should not be splittable (different ranks)")
|
||||||
|
|
||||||
|
// Not splittable - different ranks
|
||||||
|
hand = BlackjackHand(cards: [
|
||||||
|
Card(suit: .spades, rank: .nine),
|
||||||
|
Card(suit: .hearts, rank: .ten)
|
||||||
|
])
|
||||||
|
#expect(!hand.canSplit, "9 + 10 should not be splittable")
|
||||||
|
|
||||||
|
// Not splittable - more than 2 cards
|
||||||
|
hand = BlackjackHand(cards: [
|
||||||
|
Card(suit: .spades, rank: .five),
|
||||||
|
Card(suit: .hearts, rank: .five),
|
||||||
|
Card(suit: .clubs, rank: .ace)
|
||||||
|
])
|
||||||
|
#expect(!hand.canSplit, "Hand with 3 cards should not be splittable")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test @MainActor func handDoubleDownDetection() async throws {
|
||||||
|
// Can double down with 2 cards
|
||||||
|
var hand = BlackjackHand(cards: [
|
||||||
|
Card(suit: .spades, rank: .five),
|
||||||
|
Card(suit: .hearts, rank: .six)
|
||||||
|
])
|
||||||
|
#expect(hand.canDoubleDown, "Should be able to double down with 2 cards")
|
||||||
|
|
||||||
|
// Cannot double down after already doubled
|
||||||
|
hand.isDoubledDown = true
|
||||||
|
#expect(!hand.canDoubleDown, "Should not be able to double down twice")
|
||||||
|
|
||||||
|
// Cannot double down with 3+ cards
|
||||||
|
hand = BlackjackHand(cards: [
|
||||||
|
Card(suit: .spades, rank: .five),
|
||||||
|
Card(suit: .hearts, rank: .six),
|
||||||
|
Card(suit: .clubs, rank: .two)
|
||||||
|
])
|
||||||
|
#expect(!hand.canDoubleDown, "Should not be able to double down with 3 cards")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test @MainActor func handHitDetection() async throws {
|
||||||
|
// Can hit with normal hand
|
||||||
|
var hand = BlackjackHand(cards: [
|
||||||
|
Card(suit: .spades, rank: .five),
|
||||||
|
Card(suit: .hearts, rank: .six)
|
||||||
|
])
|
||||||
|
#expect(hand.canHit, "Should be able to hit with 11")
|
||||||
|
|
||||||
|
// Cannot hit after standing
|
||||||
|
hand.isStanding = true
|
||||||
|
#expect(!hand.canHit, "Should not be able to hit after standing")
|
||||||
|
|
||||||
|
// Cannot hit when busted
|
||||||
|
hand = BlackjackHand(cards: [
|
||||||
|
Card(suit: .spades, rank: .king),
|
||||||
|
Card(suit: .hearts, rank: .queen),
|
||||||
|
Card(suit: .clubs, rank: .five)
|
||||||
|
])
|
||||||
|
#expect(!hand.canHit, "Should not be able to hit when busted")
|
||||||
|
|
||||||
|
// Cannot hit with blackjack
|
||||||
|
hand = BlackjackHand(cards: [
|
||||||
|
Card(suit: .spades, rank: .ace),
|
||||||
|
Card(suit: .hearts, rank: .king)
|
||||||
|
])
|
||||||
|
#expect(!hand.canHit, "Should not be able to hit with blackjack")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Card Value Tests
|
||||||
|
|
||||||
|
@Test func cardHiLoValues() async throws {
|
||||||
|
// Low cards (2-6) = +1
|
||||||
|
#expect(Card(suit: .spades, rank: .two).hiLoValue == 1)
|
||||||
|
#expect(Card(suit: .hearts, rank: .three).hiLoValue == 1)
|
||||||
|
#expect(Card(suit: .clubs, rank: .four).hiLoValue == 1)
|
||||||
|
#expect(Card(suit: .diamonds, rank: .five).hiLoValue == 1)
|
||||||
|
#expect(Card(suit: .spades, rank: .six).hiLoValue == 1)
|
||||||
|
|
||||||
|
// Neutral cards (7-9) = 0
|
||||||
|
#expect(Card(suit: .hearts, rank: .seven).hiLoValue == 0)
|
||||||
|
#expect(Card(suit: .clubs, rank: .eight).hiLoValue == 0)
|
||||||
|
#expect(Card(suit: .diamonds, rank: .nine).hiLoValue == 0)
|
||||||
|
|
||||||
|
// High cards (10, J, Q, K, A) = -1
|
||||||
|
#expect(Card(suit: .spades, rank: .ten).hiLoValue == -1)
|
||||||
|
#expect(Card(suit: .hearts, rank: .jack).hiLoValue == -1)
|
||||||
|
#expect(Card(suit: .clubs, rank: .queen).hiLoValue == -1)
|
||||||
|
#expect(Card(suit: .diamonds, rank: .king).hiLoValue == -1)
|
||||||
|
#expect(Card(suit: .spades, rank: .ace).hiLoValue == -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Game State Tests
|
||||||
|
|
||||||
|
@Test @MainActor func gameStateInitialization() async throws {
|
||||||
|
let state = GameState(settings: GameSettings())
|
||||||
|
|
||||||
|
// Initial state
|
||||||
|
#expect(state.balance == 10000, "Should start with $10,000")
|
||||||
|
#expect(state.currentBet == 0, "Should start with no bet")
|
||||||
|
#expect(state.playerHands.isEmpty, "Should start with no hands")
|
||||||
|
#expect(state.dealerHand.cards.isEmpty, "Should start with empty dealer hand")
|
||||||
|
|
||||||
|
// Check phase enum
|
||||||
|
switch state.currentPhase {
|
||||||
|
case .betting:
|
||||||
|
break // Expected
|
||||||
|
default:
|
||||||
|
Issue.record("Should start in betting phase")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test @MainActor func gameStateBetting() async throws {
|
||||||
|
let state = GameState(settings: GameSettings())
|
||||||
|
|
||||||
|
// Place a bet
|
||||||
|
state.placeBet(amount: 100)
|
||||||
|
#expect(state.currentBet == 100, "Bet should be placed")
|
||||||
|
#expect(state.balance == 9900, "Balance should decrease")
|
||||||
|
|
||||||
|
// Add to bet
|
||||||
|
state.placeBet(amount: 50)
|
||||||
|
#expect(state.currentBet == 150, "Bet should increase")
|
||||||
|
#expect(state.balance == 9850, "Balance should decrease more")
|
||||||
|
|
||||||
|
// Clear bet
|
||||||
|
state.clearBet()
|
||||||
|
#expect(state.currentBet == 0, "Bet should be cleared")
|
||||||
|
#expect(state.balance == 10000, "Balance should be restored")
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test @MainActor func gameStateDealing() async throws {
|
||||||
|
let state = GameState(settings: GameSettings())
|
||||||
|
|
||||||
|
// Place bet and deal
|
||||||
|
state.placeBet(amount: 100)
|
||||||
|
await state.deal()
|
||||||
|
|
||||||
|
// Verify hands were dealt
|
||||||
|
#expect(state.playerHands.count == 1, "Should have 1 player hand")
|
||||||
|
#expect(state.playerHands[0].cards.count == 2, "Player should have 2 cards")
|
||||||
|
#expect(state.dealerHand.cards.count == 2, "Dealer should have 2 cards")
|
||||||
|
|
||||||
|
// Verify values are valid
|
||||||
|
#expect(state.playerHands[0].value >= 2, "Player hand should have valid value")
|
||||||
|
#expect(state.playerHands[0].value <= 21, "Player hand should not exceed 21 initially")
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -8,28 +8,205 @@
|
|||||||
import XCTest
|
import XCTest
|
||||||
|
|
||||||
final class BlackjackUITests: XCTestCase {
|
final class BlackjackUITests: XCTestCase {
|
||||||
|
|
||||||
|
var app: XCUIApplication!
|
||||||
|
|
||||||
override func setUpWithError() throws {
|
override func setUpWithError() throws {
|
||||||
// Put setup code here. This method is called before the invocation of each test method in the class.
|
// Put setup code here. This method is called before the invocation of each test method in the class.
|
||||||
|
|
||||||
// In UI tests it is usually best to stop immediately when a failure occurs.
|
// In UI tests it is usually best to stop immediately when a failure occurs.
|
||||||
continueAfterFailure = false
|
continueAfterFailure = false
|
||||||
|
|
||||||
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
|
app = XCUIApplication()
|
||||||
|
app.launchArguments = ["UI-TESTING"]
|
||||||
|
|
||||||
|
// In UI tests it's important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
|
||||||
}
|
}
|
||||||
|
|
||||||
override func tearDownWithError() throws {
|
override func tearDownWithError() throws {
|
||||||
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
// Put teardown code here. This method is called after the invocation of each test method in the class.
|
||||||
|
app = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func testExample() throws {
|
func testExample() throws {
|
||||||
// UI tests must launch the application that they test.
|
// UI tests must launch the application that they test.
|
||||||
let app = XCUIApplication()
|
|
||||||
app.launch()
|
app.launch()
|
||||||
|
|
||||||
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
// Use XCTAssert and related functions to verify your tests produce the correct results.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MARK: - UI Navigation Tests
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func testAppLaunches() throws {
|
||||||
|
app.launch()
|
||||||
|
XCTAssertTrue(app.exists, "App should launch successfully")
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func testTopBarElements() throws {
|
||||||
|
app.launch()
|
||||||
|
|
||||||
|
// Wait for the app to load
|
||||||
|
let balanceText = app.staticTexts.matching(identifier: "balance").firstMatch
|
||||||
|
XCTAssertTrue(balanceText.waitForExistence(timeout: 2), "Balance should be visible")
|
||||||
|
|
||||||
|
// Check settings button exists
|
||||||
|
let settingsButton = app.buttons.matching(identifier: "settings").firstMatch
|
||||||
|
XCTAssertTrue(settingsButton.exists, "Settings button should exist")
|
||||||
|
|
||||||
|
// Check help button exists
|
||||||
|
let helpButton = app.buttons.matching(identifier: "help").firstMatch
|
||||||
|
XCTAssertTrue(helpButton.exists, "Help button should exist")
|
||||||
|
|
||||||
|
// Check stats button exists
|
||||||
|
let statsButton = app.buttons.matching(identifier: "stats").firstMatch
|
||||||
|
XCTAssertTrue(statsButton.exists, "Stats button should exist")
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func testSettingsSheet() throws {
|
||||||
|
app.launch()
|
||||||
|
|
||||||
|
// Find and tap settings button
|
||||||
|
let settingsButton = app.buttons.matching(identifier: "settings").firstMatch
|
||||||
|
XCTAssertTrue(settingsButton.waitForExistence(timeout: 2), "Settings button should exist")
|
||||||
|
settingsButton.tap()
|
||||||
|
|
||||||
|
// Wait for settings sheet to appear
|
||||||
|
let settingsSheet = app.scrollViews.firstMatch
|
||||||
|
XCTAssertTrue(settingsSheet.waitForExistence(timeout: 2), "Settings sheet should appear")
|
||||||
|
|
||||||
|
// Check for some expected elements in settings
|
||||||
|
XCTAssertTrue(app.staticTexts["Table Limits"].waitForExistence(timeout: 1) ||
|
||||||
|
app.staticTexts["Game Settings"].waitForExistence(timeout: 1),
|
||||||
|
"Settings content should be visible")
|
||||||
|
|
||||||
|
// Close settings (tap outside or dismiss button)
|
||||||
|
if app.buttons["Done"].exists {
|
||||||
|
app.buttons["Done"].tap()
|
||||||
|
} else {
|
||||||
|
// Tap outside to dismiss
|
||||||
|
app.coordinate(withNormalizedOffset: CGVector(dx: 0.1, dy: 0.1)).tap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func testHelpSheet() throws {
|
||||||
|
app.launch()
|
||||||
|
|
||||||
|
// Find and tap help button
|
||||||
|
let helpButton = app.buttons.matching(identifier: "help").firstMatch
|
||||||
|
XCTAssertTrue(helpButton.waitForExistence(timeout: 2), "Help button should exist")
|
||||||
|
helpButton.tap()
|
||||||
|
|
||||||
|
// Wait for help sheet to appear
|
||||||
|
let helpSheet = app.scrollViews.firstMatch
|
||||||
|
XCTAssertTrue(helpSheet.waitForExistence(timeout: 2), "Help sheet should appear")
|
||||||
|
|
||||||
|
// Check for rules content
|
||||||
|
XCTAssertTrue(app.staticTexts["Rules"].exists ||
|
||||||
|
app.staticTexts["How to Play"].exists,
|
||||||
|
"Rules content should be visible")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Betting Tests
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func testChipSelector() throws {
|
||||||
|
app.launch()
|
||||||
|
|
||||||
|
// Wait for chip selector to appear
|
||||||
|
let chipSelector = app.otherElements["chip-selector"]
|
||||||
|
XCTAssertTrue(chipSelector.waitForExistence(timeout: 2) ||
|
||||||
|
app.buttons.matching(NSPredicate(format: "label CONTAINS '$'")).count > 0,
|
||||||
|
"Chip selector should be visible")
|
||||||
|
|
||||||
|
// Find a chip button (they should have $ in the label)
|
||||||
|
let chipButtons = app.buttons.matching(NSPredicate(format: "label CONTAINS '$'"))
|
||||||
|
XCTAssertGreaterThan(chipButtons.count, 0, "Should have chip buttons")
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func testPlaceBet() throws {
|
||||||
|
app.launch()
|
||||||
|
|
||||||
|
// Wait for betting area
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
// Find deal button (should be disabled initially)
|
||||||
|
let dealButton = app.buttons["Deal"]
|
||||||
|
XCTAssertTrue(dealButton.waitForExistence(timeout: 2), "Deal button should exist")
|
||||||
|
|
||||||
|
// Initially deal button should be disabled
|
||||||
|
XCTAssertFalse(dealButton.isEnabled, "Deal button should be disabled before bet")
|
||||||
|
|
||||||
|
// Tap on betting zone
|
||||||
|
let bettingZone = app.otherElements["betting-zone"]
|
||||||
|
if bettingZone.exists {
|
||||||
|
bettingZone.tap()
|
||||||
|
|
||||||
|
// Deal button should now be enabled (or still disabled if bet is below minimum)
|
||||||
|
// We can't reliably test this without knowing the bet amount vs minimum
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func testClearButton() throws {
|
||||||
|
app.launch()
|
||||||
|
|
||||||
|
// Wait and place a bet by tapping betting zone
|
||||||
|
sleep(1)
|
||||||
|
let bettingZone = app.otherElements["betting-zone"]
|
||||||
|
if bettingZone.exists {
|
||||||
|
bettingZone.tap()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find clear button
|
||||||
|
let clearButton = app.buttons["Clear"]
|
||||||
|
if clearButton.exists {
|
||||||
|
clearButton.tap()
|
||||||
|
|
||||||
|
// After clearing, deal button should be disabled again
|
||||||
|
let dealButton = app.buttons["Deal"]
|
||||||
|
XCTAssertFalse(dealButton.isEnabled, "Deal button should be disabled after clear")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Gameplay Tests
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
func testDealCards() throws {
|
||||||
|
app.launch()
|
||||||
|
|
||||||
|
sleep(1)
|
||||||
|
|
||||||
|
// Place a bet by tapping betting zone multiple times to ensure we meet minimum
|
||||||
|
let bettingZone = app.otherElements["betting-zone"]
|
||||||
|
if bettingZone.exists {
|
||||||
|
for _ in 1...5 {
|
||||||
|
bettingZone.tap()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find and tap deal button
|
||||||
|
let dealButton = app.buttons["Deal"]
|
||||||
|
if dealButton.waitForExistence(timeout: 2) && dealButton.isEnabled {
|
||||||
|
dealButton.tap()
|
||||||
|
|
||||||
|
// Wait for cards to be dealt
|
||||||
|
sleep(2)
|
||||||
|
|
||||||
|
// After dealing, we should see hit/stand buttons or a result
|
||||||
|
let hitButton = app.buttons["Hit"]
|
||||||
|
let standButton = app.buttons["Stand"]
|
||||||
|
let newRoundButton = app.buttons["New Round"]
|
||||||
|
|
||||||
|
XCTAssertTrue(hitButton.exists || standButton.exists || newRoundButton.exists,
|
||||||
|
"Should see action buttons after dealing")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
func testLaunchPerformance() throws {
|
func testLaunchPerformance() throws {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user