new projects

Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
Matt Bruce 2025-12-22 13:06:15 -06:00
parent fa5d9f4c75
commit 7759edfcd2
2384 changed files with 8 additions and 25018 deletions

View File

@ -7,25 +7,11 @@
objects = {
/* Begin PBXBuildFile section */
EA5AD2012EF34B660040CB90 /* CasinoKit in Frameworks */ = {isa = PBXBuildFile; productRef = EA5AD2002EF34B660040CB90 /* CasinoKit */; };
EA5ADB372EF9CD850040CB90 /* CasinoKit in Frameworks */ = {isa = PBXBuildFile; productRef = EA5ADB362EF9CD850040CB90 /* CasinoKit */; };
EAD891262EF25181006DBA80 /* CasinoKit in Frameworks */ = {isa = PBXBuildFile; productRef = EAD891252EF25181006DBA80 /* CasinoKit */; };
/* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */
EA5AD1BE2EF346C50040CB90 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = EAD890AF2EF1E9CE006DBA80 /* Project object */;
proxyType = 1;
remoteGlobalIDString = EA5AD1B02EF346C40040CB90;
remoteInfo = Blackjack;
};
EA5AD1C82EF346C50040CB90 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = EAD890AF2EF1E9CE006DBA80 /* Project object */;
proxyType = 1;
remoteGlobalIDString = EA5AD1B02EF346C40040CB90;
remoteInfo = Blackjack;
};
EAD890C52EF1E9CF006DBA80 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = EAD890AF2EF1E9CE006DBA80 /* Project object */;
@ -43,9 +29,6 @@
/* End PBXContainerItemProxy section */
/* Begin PBXFileReference section */
EA5AD1B12EF346C40040CB90 /* Blackjack.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Blackjack.app; sourceTree = BUILT_PRODUCTS_DIR; };
EA5AD1BD2EF346C50040CB90 /* BlackjackTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BlackjackTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
EA5AD1C72EF346C50040CB90 /* BlackjackUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BlackjackUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
EAD890B72EF1E9CE006DBA80 /* Baccarat.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Baccarat.app; sourceTree = BUILT_PRODUCTS_DIR; };
EAD890C42EF1E9CF006DBA80 /* BaccaratTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BaccaratTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
EAD890CE2EF1E9CF006DBA80 /* BaccaratUITests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = BaccaratUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
@ -62,21 +45,6 @@
/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */
/* Begin PBXFileSystemSynchronizedRootGroup section */
EA5AD1B22EF346C40040CB90 /* Blackjack */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = Blackjack;
sourceTree = "<group>";
};
EA5AD1C02EF346C50040CB90 /* BlackjackTests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = BlackjackTests;
sourceTree = "<group>";
};
EA5AD1CA2EF346C50040CB90 /* BlackjackUITests */ = {
isa = PBXFileSystemSynchronizedRootGroup;
path = BlackjackUITests;
sourceTree = "<group>";
};
EAD890B92EF1E9CE006DBA80 /* Baccarat */ = {
isa = PBXFileSystemSynchronizedRootGroup;
exceptions = (
@ -98,32 +66,11 @@
/* End PBXFileSystemSynchronizedRootGroup section */
/* Begin PBXFrameworksBuildPhase section */
EA5AD1AE2EF346C40040CB90 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
EA5AD2012EF34B660040CB90 /* CasinoKit in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
};
EA5AD1BA2EF346C50040CB90 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
EA5AD1C42EF346C50040CB90 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
EAD890B42EF1E9CE006DBA80 /* Frameworks */ = {
isa = PBXFrameworksBuildPhase;
buildActionMask = 2147483647;
files = (
EA5ADB372EF9CD850040CB90 /* CasinoKit in Frameworks */,
EAD891262EF25181006DBA80 /* CasinoKit in Frameworks */,
);
runOnlyForDeploymentPostprocessing = 0;
@ -158,9 +105,6 @@
EAD890B92EF1E9CE006DBA80 /* Baccarat */,
EAD890C72EF1E9CF006DBA80 /* BaccaratTests */,
EAD890D12EF1E9CF006DBA80 /* BaccaratUITests */,
EA5AD1B22EF346C40040CB90 /* Blackjack */,
EA5AD1C02EF346C50040CB90 /* BlackjackTests */,
EA5AD1CA2EF346C50040CB90 /* BlackjackUITests */,
EA5AD1FF2EF34B660040CB90 /* Frameworks */,
EAD890B82EF1E9CE006DBA80 /* Products */,
);
@ -172,9 +116,6 @@
EAD890B72EF1E9CE006DBA80 /* Baccarat.app */,
EAD890C42EF1E9CF006DBA80 /* BaccaratTests.xctest */,
EAD890CE2EF1E9CF006DBA80 /* BaccaratUITests.xctest */,
EA5AD1B12EF346C40040CB90 /* Blackjack.app */,
EA5AD1BD2EF346C50040CB90 /* BlackjackTests.xctest */,
EA5AD1C72EF346C50040CB90 /* BlackjackUITests.xctest */,
);
name = Products;
sourceTree = "<group>";
@ -182,75 +123,6 @@
/* End PBXGroup section */
/* Begin PBXNativeTarget section */
EA5AD1B02EF346C40040CB90 /* Blackjack */ = {
isa = PBXNativeTarget;
buildConfigurationList = EA5AD1D52EF346C50040CB90 /* Build configuration list for PBXNativeTarget "Blackjack" */;
buildPhases = (
EA5AD1AD2EF346C40040CB90 /* Sources */,
EA5AD1AE2EF346C40040CB90 /* Frameworks */,
EA5AD1AF2EF346C40040CB90 /* Resources */,
);
buildRules = (
);
dependencies = (
);
fileSystemSynchronizedGroups = (
EA5AD1B22EF346C40040CB90 /* Blackjack */,
);
name = Blackjack;
packageProductDependencies = (
EA5AD2002EF34B660040CB90 /* CasinoKit */,
);
productName = Blackjack;
productReference = EA5AD1B12EF346C40040CB90 /* Blackjack.app */;
productType = "com.apple.product-type.application";
};
EA5AD1BC2EF346C50040CB90 /* BlackjackTests */ = {
isa = PBXNativeTarget;
buildConfigurationList = EA5AD1D62EF346C50040CB90 /* Build configuration list for PBXNativeTarget "BlackjackTests" */;
buildPhases = (
EA5AD1B92EF346C50040CB90 /* Sources */,
EA5AD1BA2EF346C50040CB90 /* Frameworks */,
EA5AD1BB2EF346C50040CB90 /* Resources */,
);
buildRules = (
);
dependencies = (
EA5AD1BF2EF346C50040CB90 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
EA5AD1C02EF346C50040CB90 /* BlackjackTests */,
);
name = BlackjackTests;
packageProductDependencies = (
);
productName = BlackjackTests;
productReference = EA5AD1BD2EF346C50040CB90 /* BlackjackTests.xctest */;
productType = "com.apple.product-type.bundle.unit-test";
};
EA5AD1C62EF346C50040CB90 /* BlackjackUITests */ = {
isa = PBXNativeTarget;
buildConfigurationList = EA5AD1D72EF346C50040CB90 /* Build configuration list for PBXNativeTarget "BlackjackUITests" */;
buildPhases = (
EA5AD1C32EF346C50040CB90 /* Sources */,
EA5AD1C42EF346C50040CB90 /* Frameworks */,
EA5AD1C52EF346C50040CB90 /* Resources */,
);
buildRules = (
);
dependencies = (
EA5AD1C92EF346C50040CB90 /* PBXTargetDependency */,
);
fileSystemSynchronizedGroups = (
EA5AD1CA2EF346C50040CB90 /* BlackjackUITests */,
);
name = BlackjackUITests;
packageProductDependencies = (
);
productName = BlackjackUITests;
productReference = EA5AD1C72EF346C50040CB90 /* BlackjackUITests.xctest */;
productType = "com.apple.product-type.bundle.ui-testing";
};
EAD890B62EF1E9CE006DBA80 /* Baccarat */ = {
isa = PBXNativeTarget;
buildConfigurationList = EAD890D82EF1E9CF006DBA80 /* Build configuration list for PBXNativeTarget "Baccarat" */;
@ -269,6 +141,7 @@
name = Baccarat;
packageProductDependencies = (
EAD891252EF25181006DBA80 /* CasinoKit */,
EA5ADB362EF9CD850040CB90 /* CasinoKit */,
);
productName = Baccarat;
productReference = EAD890B72EF1E9CE006DBA80 /* Baccarat.app */;
@ -330,17 +203,6 @@
LastSwiftUpdateCheck = 2600;
LastUpgradeCheck = 2600;
TargetAttributes = {
EA5AD1B02EF346C40040CB90 = {
CreatedOnToolsVersion = 26.0;
};
EA5AD1BC2EF346C50040CB90 = {
CreatedOnToolsVersion = 26.0;
TestTargetID = EA5AD1B02EF346C40040CB90;
};
EA5AD1C62EF346C50040CB90 = {
CreatedOnToolsVersion = 26.0;
TestTargetID = EA5AD1B02EF346C40040CB90;
};
EAD890B62EF1E9CE006DBA80 = {
CreatedOnToolsVersion = 26.0;
};
@ -368,7 +230,7 @@
mainGroup = EAD890AE2EF1E9CE006DBA80;
minimizedProjectReferenceProxies = 1;
packageReferences = (
EAD891242EF25181006DBA80 /* XCLocalSwiftPackageReference "CasinoKit" */,
EA5ADB352EF9CD850040CB90 /* XCLocalSwiftPackageReference "../CasinoKit" */,
);
preferredProjectObjectVersion = 77;
productRefGroup = EAD890B82EF1E9CE006DBA80 /* Products */;
@ -378,35 +240,11 @@
EAD890B62EF1E9CE006DBA80 /* Baccarat */,
EAD890C32EF1E9CF006DBA80 /* BaccaratTests */,
EAD890CD2EF1E9CF006DBA80 /* BaccaratUITests */,
EA5AD1B02EF346C40040CB90 /* Blackjack */,
EA5AD1BC2EF346C50040CB90 /* BlackjackTests */,
EA5AD1C62EF346C50040CB90 /* BlackjackUITests */,
);
};
/* End PBXProject section */
/* Begin PBXResourcesBuildPhase section */
EA5AD1AF2EF346C40040CB90 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
EA5AD1BB2EF346C50040CB90 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
EA5AD1C52EF346C50040CB90 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
EAD890B52EF1E9CE006DBA80 /* Resources */ = {
isa = PBXResourcesBuildPhase;
buildActionMask = 2147483647;
@ -431,27 +269,6 @@
/* End PBXResourcesBuildPhase section */
/* Begin PBXSourcesBuildPhase section */
EA5AD1AD2EF346C40040CB90 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
EA5AD1B92EF346C50040CB90 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
EA5AD1C32EF346C50040CB90 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
);
runOnlyForDeploymentPostprocessing = 0;
};
EAD890B32EF1E9CE006DBA80 /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
@ -476,16 +293,6 @@
/* End PBXSourcesBuildPhase section */
/* Begin PBXTargetDependency section */
EA5AD1BF2EF346C50040CB90 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = EA5AD1B02EF346C40040CB90 /* Blackjack */;
targetProxy = EA5AD1BE2EF346C50040CB90 /* PBXContainerItemProxy */;
};
EA5AD1C92EF346C50040CB90 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = EA5AD1B02EF346C40040CB90 /* Blackjack */;
targetProxy = EA5AD1C82EF346C50040CB90 /* PBXContainerItemProxy */;
};
EAD890C62EF1E9CF006DBA80 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = EAD890B62EF1E9CE006DBA80 /* Baccarat */;
@ -499,158 +306,6 @@
/* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */
EA5AD1CF2EF346C50040CB90 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CODE_SIGN_ENTITLEMENTS = Blackjack/Blackjack.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.Blackjack;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Debug;
};
EA5AD1D02EF346C50040CB90 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
ASSETCATALOG_COMPILER_INCLUDE_ALL_APPICON_ASSETS = YES;
CODE_SIGN_ENTITLEMENTS = Blackjack/Blackjack.entitlements;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES;
INFOPLIST_KEY_UILaunchStoryboardName = LaunchScreen;
INFOPLIST_KEY_UISupportedInterfaceOrientations = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.Blackjack;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = YES;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_DEFAULT_ACTOR_ISOLATION = MainActor;
SWIFT_EMIT_LOC_STRINGS = YES;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
};
name = Release;
};
EA5AD1D12EF346C50040CB90 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.BlackjackTests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Blackjack.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Blackjack";
};
name = Debug;
};
EA5AD1D22EF346C50040CB90 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 26.0;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.BlackjackTests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Blackjack.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Blackjack";
};
name = Release;
};
EA5AD1D32EF346C50040CB90 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.BlackjackUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_TARGET_NAME = Blackjack;
};
name = Debug;
};
EA5AD1D42EF346C50040CB90 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 1;
DEVELOPMENT_TEAM = 6R7KLBPBLZ;
GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.mbrucedogs.BlackjackUITests;
PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO;
SWIFT_APPROACHABLE_CONCURRENCY = YES;
SWIFT_EMIT_LOC_STRINGS = NO;
SWIFT_UPCOMING_FEATURE_MEMBER_IMPORT_VISIBILITY = YES;
SWIFT_VERSION = 5.0;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_TARGET_NAME = Blackjack;
};
name = Release;
};
EAD890D62EF1E9CF006DBA80 /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
@ -935,33 +590,6 @@
/* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */
EA5AD1D52EF346C50040CB90 /* Build configuration list for PBXNativeTarget "Blackjack" */ = {
isa = XCConfigurationList;
buildConfigurations = (
EA5AD1CF2EF346C50040CB90 /* Debug */,
EA5AD1D02EF346C50040CB90 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
EA5AD1D62EF346C50040CB90 /* Build configuration list for PBXNativeTarget "BlackjackTests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
EA5AD1D12EF346C50040CB90 /* Debug */,
EA5AD1D22EF346C50040CB90 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
EA5AD1D72EF346C50040CB90 /* Build configuration list for PBXNativeTarget "BlackjackUITests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
EA5AD1D32EF346C50040CB90 /* Debug */,
EA5AD1D42EF346C50040CB90 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Release;
};
EAD890B22EF1E9CE006DBA80 /* Build configuration list for PBXProject "Baccarat" */ = {
isa = XCConfigurationList;
buildConfigurations = (
@ -1001,16 +629,15 @@
/* End XCConfigurationList section */
/* Begin XCLocalSwiftPackageReference section */
EAD891242EF25181006DBA80 /* XCLocalSwiftPackageReference "CasinoKit" */ = {
EA5ADB352EF9CD850040CB90 /* XCLocalSwiftPackageReference "../CasinoKit" */ = {
isa = XCLocalSwiftPackageReference;
relativePath = CasinoKit;
relativePath = ../CasinoKit;
};
/* End XCLocalSwiftPackageReference section */
/* Begin XCSwiftPackageProductDependency section */
EA5AD2002EF34B660040CB90 /* CasinoKit */ = {
EA5ADB362EF9CD850040CB90 /* CasinoKit */ = {
isa = XCSwiftPackageProductDependency;
package = EAD891242EF25181006DBA80 /* XCLocalSwiftPackageReference "CasinoKit" */;
productName = CasinoKit;
};
EAD891252EF25181006DBA80 /* CasinoKit */ = {

View File

@ -7,7 +7,7 @@
<key>Baccarat.xcscheme_^#shared#^_</key>
<dict>
<key>orderHint</key>
<integer>2</integer>
<integer>1</integer>
</dict>
<key>Blackjack.xcscheme_^#shared#^_</key>
<dict>

View File

@ -1,366 +0,0 @@
# Agent guide for Swift and SwiftUI
This repository contains an Xcode project written with Swift and SwiftUI. Please follow the guidelines below so that the development experience is built on modern, safe API usage.
## Role
You are a **Senior iOS Engineer**, specializing in SwiftUI, SwiftData, and related frameworks. Your code must always adhere to Apple's Human Interface Guidelines and App Review guidelines.
## Core instructions
- Target iOS 26.0 or later. (Yes, it definitely exists.)
- Swift 6.2 or later, using modern Swift concurrency.
- SwiftUI backed up by `@Observable` classes for shared data.
- Do not introduce third-party frameworks without asking first.
- Avoid UIKit unless requested.
## Swift instructions
- Always mark `@Observable` classes with `@MainActor`.
- Assume strict Swift concurrency rules are being applied.
- Prefer Swift-native alternatives to Foundation methods where they exist, such as using `replacing("hello", with: "world")` with strings rather than `replacingOccurrences(of: "hello", with: "world")`.
- Prefer modern Foundation API, for example `URL.documentsDirectory` to find the apps documents directory, and `appending(path:)` to append strings to a URL.
- Never use C-style number formatting such as `Text(String(format: "%.2f", abs(myNumber)))`; always use `Text(abs(change), format: .number.precision(.fractionLength(2)))` instead.
- Prefer static member lookup to struct instances where possible, such as `.circle` rather than `Circle()`, and `.borderedProminent` rather than `BorderedProminentButtonStyle()`.
- Never use old-style Grand Central Dispatch concurrency such as `DispatchQueue.main.async()`. If behavior like this is needed, always use modern Swift concurrency.
- Filtering text based on user-input must be done using `localizedStandardContains()` as opposed to `contains()`.
- Avoid force unwraps and force `try` unless it is unrecoverable.
## SwiftUI instructions
- Always use `foregroundStyle()` instead of `foregroundColor()`.
- Always use `clipShape(.rect(cornerRadius:))` instead of `cornerRadius()`.
- Always use the `Tab` API instead of `tabItem()`.
- Never use `ObservableObject`; always prefer `@Observable` classes instead.
- Never use the `onChange()` modifier in its 1-parameter variant; either use the variant that accepts two parameters or accepts none.
- Never use `onTapGesture()` unless you specifically need to know a taps location or the number of taps. All other usages should use `Button`.
- Never use `Task.sleep(nanoseconds:)`; always use `Task.sleep(for:)` instead.
- Never use `UIScreen.main.bounds` to read the size of the available space.
- Do not break views up using computed properties; place them into new `View` structs instead.
- Do not force specific font sizes; prefer using Dynamic Type instead.
- Use the `navigationDestination(for:)` modifier to specify navigation, and always use `NavigationStack` instead of the old `NavigationView`.
- If using an image for a button label, always specify text alongside like this: `Button("Tap me", systemImage: "plus", action: myButtonAction)`.
- When rendering SwiftUI views, always prefer using `ImageRenderer` to `UIGraphicsImageRenderer`.
- Dont apply the `fontWeight()` modifier unless there is good reason. If you want to make some text bold, always use `bold()` instead of `fontWeight(.bold)`.
- Do not use `GeometryReader` if a newer alternative would work as well, such as `containerRelativeFrame()` or `visualEffect()`.
- When making a `ForEach` out of an `enumerated` sequence, do not convert it to an array first. So, prefer `ForEach(x.enumerated(), id: \.element.id)` instead of `ForEach(Array(x.enumerated()), id: \.element.id)`.
- When hiding scroll view indicators, use the `.scrollIndicators(.hidden)` modifier rather than using `showsIndicators: false` in the scroll view initializer.
- Avoid `AnyView` unless it is absolutely required.
## View/State separation (MVVM-lite)
**Views should be "dumb" renderers.** All business logic belongs in `GameState` or dedicated view models.
### What belongs in the State/ViewModel:
- **Business logic**: Calculations, validations, game rules
- **Computed properties based on game data**: hints, recommendations, derived values
- **State checks**: `isPlayerTurn`, `canHit`, `isGameOver`, `isBetBelowMinimum`
- **Data transformations**: statistics calculations, filtering, aggregations
### What is acceptable in Views:
- **Pure UI layout logic**: `isIPad`, `maxContentWidth` based on size class
- **Visual styling**: color selection based on state (`valueColor`, `resultColor`)
- **@ViewBuilder sub-views**: breaking up complex layouts
- **Accessibility labels**: combining data into accessible descriptions
### Examples
**❌ BAD - Business logic in view:**
```swift
struct MyView: View {
@Bindable var state: GameState
private var isBetBelowMinimum: Bool {
state.currentBet > 0 && state.currentBet < state.settings.minBet
}
private var currentHint: String? {
guard let hand = state.activeHand else { return nil }
return state.engine.getHint(playerHand: hand, dealerUpCard: upCard)
}
}
```
**✅ GOOD - Logic in GameState, view just reads:**
```swift
// In GameState:
var isBetBelowMinimum: Bool {
currentBet > 0 && currentBet < settings.minBet
}
var currentHint: String? {
guard settings.showHints, isPlayerTurn else { return nil }
guard let hand = activeHand, let upCard = dealerUpCard else { return nil }
return engine.getHint(playerHand: hand, dealerUpCard: upCard)
}
// In View:
if state.isBetBelowMinimum { ... }
if let hint = state.currentHint { HintView(hint: hint) }
```
### Benefits:
- **Testable**: GameState logic can be unit tested without UI
- **Single source of truth**: No duplicated logic across views
- **Cleaner views**: Views focus purely on layout and presentation
- **Easier debugging**: Logic is centralized, not scattered
- **Never use raw numeric literals** for padding, spacing, opacity, font sizes, dimensions, corner radii, shadows, or animation durations—always use Design constants (see "No magic numbers" section).
- **Never use inline `Color(red:green:blue:)` or hex colors**—define all colors in the `Color` extension in `DesignConstants.swift` with semantic names.
- Avoid using UIKit colors in SwiftUI code.
## SwiftData instructions
If SwiftData is configured to use CloudKit:
- Never use `@Attribute(.unique)`.
- Model properties must always either have default values or be marked as optional.
- All relationships must be marked optional.
## Localization instructions
- Use **String Catalogs** (`.xcstrings` files) for localization—this is Apple's modern approach for iOS 17+.
- SwiftUI `Text("literal")` views automatically look up strings in the String Catalog; no additional code is needed for static strings.
- For strings outside of `Text` views or with dynamic content, use `String(localized:)` or create a helper extension:
```swift
extension String {
static func localized(_ key: String) -> String {
String(localized: String.LocalizationValue(key))
}
static func localized(_ key: String, _ arguments: CVarArg...) -> String {
let format = String(localized: String.LocalizationValue(key))
return String(format: format, arguments: arguments)
}
}
```
- For format strings with interpolation (e.g., "Balance: $%@"), define a key in the String Catalog and use `String.localized("key", value)`.
- Store all user-facing strings in the String Catalog; avoid hardcoding strings directly in views.
- Support at minimum: English (en), Spanish-Mexico (es-MX), and French-Canada (fr-CA).
- Never use `NSLocalizedString`; prefer the modern `String(localized:)` API.
## No magic numbers or hardcoded values
**Never use raw numeric literals or hardcoded colors directly in views.** All values must be extracted to named constants, enums, or variables. This applies to:
### Values that MUST be constants:
- **Spacing & Padding**: `.padding(Design.Spacing.medium)` not `.padding(12)`
- **Corner Radii**: `Design.CornerRadius.large` not `cornerRadius: 16`
- **Font Sizes**: `Design.BaseFontSize.body` not `size: 14`
- **Opacity Values**: `Design.Opacity.strong` not `.opacity(0.7)`
- **Colors**: `Color.Primary.accent` not `Color(red: 0.8, green: 0.6, blue: 0.2)`
- **Line Widths**: `Design.LineWidth.medium` not `lineWidth: 2`
- **Shadow Values**: `Design.Shadow.radiusLarge` not `radius: 10`
- **Animation Durations**: `Design.Animation.quick` not `duration: 0.3`
- **Component Sizes**: `Design.Size.chipBadge` not `frame(width: 32)`
### What to do when you see a magic number:
1. Check if an appropriate constant already exists in `DesignConstants.swift`
2. If not, add a new constant with a semantic name
3. Use the constant in place of the raw value
4. If it's truly view-specific and used only once, extract to a `private let` at the top of the view struct
### Examples of violations:
```swift
// ❌ BAD - Magic numbers everywhere
.padding(16)
.opacity(0.6)
.frame(width: 80, height: 52)
.shadow(radius: 10, y: 5)
Color(red: 0.25, green: 0.3, blue: 0.45)
// ✅ GOOD - Named constants
.padding(Design.Spacing.large)
.opacity(Design.Opacity.accent)
.frame(width: Design.Size.bonusZoneWidth, height: Design.Size.topBetRowHeight)
.shadow(radius: Design.Shadow.radiusLarge, y: Design.Shadow.offsetLarge)
Color.BettingZone.dragonBonusLight
```
## Design constants instructions
- Create a centralized design constants file (e.g., `DesignConstants.swift`) using enums for namespacing:
```swift
enum Design {
enum Spacing {
static let xxSmall: CGFloat = 2
static let xSmall: CGFloat = 4
static let small: CGFloat = 8
static let medium: CGFloat = 12
static let large: CGFloat = 16
static let xLarge: CGFloat = 20
}
enum CornerRadius {
static let small: CGFloat = 8
static let medium: CGFloat = 12
static let large: CGFloat = 16
}
enum BaseFontSize {
static let small: CGFloat = 10
static let body: CGFloat = 14
static let large: CGFloat = 18
static let title: CGFloat = 24
}
enum Opacity {
static let subtle: Double = 0.1
static let hint: Double = 0.2
static let light: Double = 0.3
static let medium: Double = 0.5
static let accent: Double = 0.6
static let strong: Double = 0.7
static let heavy: Double = 0.8
static let almostFull: Double = 0.9
}
enum LineWidth {
static let thin: CGFloat = 1
static let medium: CGFloat = 2
static let thick: CGFloat = 3
}
enum Shadow {
static let radiusSmall: CGFloat = 2
static let radiusMedium: CGFloat = 6
static let radiusLarge: CGFloat = 10
static let offsetSmall: CGFloat = 1
static let offsetMedium: CGFloat = 3
}
enum Animation {
static let quick: Double = 0.3
static let springDuration: Double = 0.4
static let staggerDelay1: Double = 0.1
static let staggerDelay2: Double = 0.25
}
}
```
- For colors used across the app, extend `Color` with semantic color definitions:
```swift
extension Color {
enum Primary {
static let background = Color(red: 0.1, green: 0.2, blue: 0.3)
static let accent = Color(red: 0.8, green: 0.6, blue: 0.2)
}
enum Button {
static let goldLight = Color(red: 1.0, green: 0.85, blue: 0.3)
static let goldDark = Color(red: 0.9, green: 0.7, blue: 0.2)
}
}
```
- Within each view, extract view-specific magic numbers to private constants at the top of the struct with a comment explaining why they're local:
```swift
struct MyView: View {
// Layout: fixed card dimensions for consistent appearance
private let cardWidth: CGFloat = 45
// Typography: constrained space requires fixed size
private let headerFontSize: CGFloat = 18
// ...
}
```
- Reference design constants in views: `Design.Spacing.medium`, `Design.CornerRadius.large`, `Color.Primary.accent`.
- Keep design constants organized by category: Spacing, CornerRadius, BaseFontSize, IconSize, Size, Animation, Opacity, LineWidth, Shadow.
- When adding new features, check existing constants first before creating new ones.
- Name constants semantically (what they represent) not literally (their value): `accent` not `pointSix`, `large` not `sixteen`.
## Dynamic Type instructions
- Always support Dynamic Type for accessibility; never use fixed font sizes without scaling.
- Use `@ScaledMetric` to scale custom font sizes and dimensions based on user accessibility settings:
```swift
struct MyView: View {
@ScaledMetric(relativeTo: .body) private var bodyFontSize: CGFloat = 14
@ScaledMetric(relativeTo: .title) private var titleFontSize: CGFloat = 24
@ScaledMetric(relativeTo: .caption) private var chipTextSize: CGFloat = 11
var body: some View {
Text("Hello")
.font(.system(size: bodyFontSize, weight: .medium))
}
}
```
- Choose the appropriate `relativeTo` text style based on the semantic purpose:
- `.largeTitle`, `.title`, `.title2`, `.title3` for headings
- `.headline`, `.subheadline` for emphasized content
- `.body` for main content
- `.callout`, `.footnote`, `.caption`, `.caption2` for smaller text
- For constrained UI elements (chips, cards, badges) where overflow would break the design, you may use fixed sizes but document the reason:
```swift
// Fixed size: chip face has strict space constraints
private let chipValueFontSize: CGFloat = 11
```
- Prefer system text styles when possible: `.font(.body)`, `.font(.title)`, `.font(.caption)`.
- Test with accessibility settings: Settings > Accessibility > Display & Text Size > Larger Text.
## VoiceOver accessibility instructions
- All interactive elements (buttons, betting zones, selectable items) must have meaningful `.accessibilityLabel()`.
- Use `.accessibilityValue()` to communicate dynamic state (e.g., current bet amount, selection state, hand value).
- Use `.accessibilityHint()` to describe what will happen when interacting with an element:
```swift
Button("Deal", action: deal)
.accessibilityHint("Deals cards and starts the round")
```
- Use `.accessibilityAddTraits()` to communicate element type:
- `.isButton` for tappable elements that aren't SwiftUI Buttons
- `.isHeader` for section headers
- `.isModal` for modal overlays
- `.updatesFrequently` for live-updating content
- Hide purely decorative elements from VoiceOver:
```swift
TableBackgroundView()
.accessibilityHidden(true) // Decorative element
```
- Group related elements to reduce VoiceOver navigation complexity:
```swift
VStack {
handLabel
cardStack
valueDisplay
}
.accessibilityElement(children: .ignore)
.accessibilityLabel("Player hand")
.accessibilityValue("Ace of Hearts, King of Spades. Value: 1")
```
- For complex elements, use `.accessibilityElement(children: .contain)` to allow navigation to children while adding context.
- Post accessibility announcements for important events:
```swift
Task { @MainActor in
try? await Task.sleep(for: .milliseconds(500))
UIAccessibility.post(notification: .announcement, argument: "Player wins!")
}
```
- Provide accessibility names for model types that appear in UI:
```swift
enum Suit {
var accessibilityName: String {
switch self {
case .hearts: return String(localized: "Hearts")
// ...
}
}
}
```
- Test with VoiceOver enabled: Settings > Accessibility > VoiceOver.
## Project structure
- Use a consistent project structure, with folder layout determined by app features.
- Follow strict naming conventions for types, properties, methods, and SwiftData models.
- Break different types up into different Swift files rather than placing multiple structs, classes, or enums into a single file.
- Write unit tests for core application logic.
- Only write UI tests if unit tests are not possible.
- Add code comments and documentation comments as needed.
- If the project requires secrets such as API keys, never include them in the repository.
## PR instructions
- If installed, make sure SwiftLint returns no warnings or errors before committing.

View File

@ -1,11 +0,0 @@
{
"colors" : [
{
"idiom" : "universal"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.3 MiB

View File

@ -1,36 +0,0 @@
{
"images" : [
{
"filename" : "AppIcon-1024.png",
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "dark"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
},
{
"appearances" : [
{
"appearance" : "luminosity",
"value" : "tinted"
}
],
"idiom" : "universal",
"platform" : "ios",
"size" : "1024x1024"
}
],
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1,6 +0,0 @@
{
"info" : {
"author" : "xcode",
"version" : 1
}
}

View File

@ -1,10 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>com.apple.developer.icloud-container-identifiers</key>
<array/>
<key>com.apple.developer.ubiquity-kvstore-identifier</key>
<string>$(TeamIdentifierPrefix)$(CFBundleIdentifier)</string>
</dict>
</plist>

View File

@ -1,29 +0,0 @@
//
// BlackjackApp.swift
// Blackjack
//
// Main application entry point.
//
import SwiftUI
import CasinoKit
@main
struct BlackjackApp: App {
init() {
// Configure sound manager defaults
SoundManager.shared.soundEnabled = true
SoundManager.shared.hapticsEnabled = true
}
var body: some Scene {
WindowGroup {
// #if DEBUG
// IconGeneratorView()
// #else
ContentView() // your real app root
// #endif
}
}
}

View File

@ -1,18 +0,0 @@
//
// ContentView.swift
// Blackjack
//
// Root view for the Blackjack app.
//
import SwiftUI
struct ContentView: View {
var body: some View {
GameTableView()
}
}
#Preview {
ContentView()
}

View File

@ -1,503 +0,0 @@
//
// BlackjackEngine.swift
// Blackjack
//
// Core game logic for Blackjack.
//
import Foundation
import CasinoKit
/// Manages the Blackjack game rules and shoe.
@Observable
@MainActor
final class BlackjackEngine {
// MARK: - Properties
/// The card shoe.
private(set) var shoe: Deck
/// Settings reference for rule variations.
private let settings: GameSettings
/// Running count for card counting (Hi-Lo system).
private(set) var runningCount: Int = 0
/// Number of decks in the shoe (reads from current settings).
var deckCount: Int {
settings.deckCount.rawValue
}
/// Cards remaining in shoe.
var cardsRemaining: Int {
shoe.cardsRemaining
}
/// True count (running count / decks remaining).
var trueCount: Double {
let decksRemaining = max(1.0, Double(cardsRemaining) / 52.0)
return Double(runningCount) / decksRemaining
}
/// Minimum cards needed to safely complete a hand.
/// Need ~10 cards worst case (player splits once, both hit twice, dealer hits twice).
private let minimumCardsForHand: Int = 10
/// The cut card position (cards to deal before reshuffling).
/// Set during reshuffle with slight random variation for realism.
private var cutCardPosition: Int = 0
/// Realistic default penetration based on deck count.
/// These match typical casino practices.
private var defaultPenetration: Double {
switch deckCount {
case 1: return 0.60 // 60% - common for single-deck (hand-held)
case 2: return 0.70 // 70% - typical double-deck pitch game
default: return 0.75 // 75% - standard for 4-8 deck shoe
}
}
/// Whether the shoe needs reshuffling (hit cut card position).
var needsReshuffle: Bool {
let cardsDealt = (52 * deckCount) - cardsRemaining
return cardsDealt >= cutCardPosition
}
/// Whether there are enough cards to start a new hand.
var canDealNewHand: Bool {
cardsRemaining >= minimumCardsForHand
}
/// Current penetration percentage (how much of the shoe has been dealt).
var penetrationPercentage: Double {
let totalCards = 52 * deckCount
return Double(totalCards - cardsRemaining) / Double(totalCards)
}
// MARK: - Initialization
init(settings: GameSettings) {
self.settings = settings
self.shoe = Deck(deckCount: settings.deckCount.rawValue)
shoe.shuffle()
// Set initial cut card position
let totalCards = 52 * settings.deckCount.rawValue
cutCardPosition = Int(Double(totalCards) * defaultPenetration)
}
// MARK: - Shoe Management
/// Reshuffles the shoe with the current deck count from settings.
func reshuffle() {
shoe = Deck(deckCount: settings.deckCount.rawValue)
shoe.shuffle()
runningCount = 0
// Calculate cut card position with slight random variation (±5%) for realism
let totalCards = 52 * deckCount
let basePenetration = defaultPenetration
let variation = Double.random(in: 0.95...1.05)
let cardsToDeal = Int(Double(totalCards) * basePenetration * variation)
// Clamp between 50% and 85% penetration
let minCards = Int(Double(totalCards) * 0.50)
let maxCards = Int(Double(totalCards) * 0.85)
cutCardPosition = max(min(cardsToDeal, maxCards), minCards)
}
/// Deals a single card from the shoe and updates the running count.
/// If the shoe is empty, automatically reshuffles and deals.
func dealCard() -> Card? {
// Emergency reshuffle if shoe is empty (should rarely happen)
if cardsRemaining == 0 {
reshuffle()
}
guard let card = shoe.draw() else { return nil }
runningCount += card.hiLoValue
return card
}
// MARK: - Card Counting
/// Updates the running count when a card is revealed (e.g., dealer hole card).
func updateCount(for card: Card) {
runningCount += card.hiLoValue
}
// MARK: - Hand Evaluation
/// Determines if dealer should hit based on rules.
func dealerShouldHit(hand: BlackjackHand) -> Bool {
let value = hand.value
if value < 17 {
return true
}
if value == 17 && hand.isSoft && settings.dealerHitsSoft17 {
return true
}
return false
}
/// Determines the result of a player hand against dealer.
func determineResult(playerHand: BlackjackHand, dealerHand: BlackjackHand) -> HandResult {
let playerValue = playerHand.value
let dealerValue = dealerHand.value
// Player busted
if playerHand.isBusted {
return .bust
}
// Player has blackjack
if playerHand.isBlackjack {
if dealerHand.isBlackjack {
return .push
}
return .blackjack
}
// Dealer busted
if dealerHand.isBusted {
return .win
}
// Dealer has blackjack (player doesn't)
if dealerHand.isBlackjack {
return .lose
}
// Compare values
if playerValue > dealerValue {
return .win
} else if playerValue < dealerValue {
return .lose
} else {
return .push
}
}
/// Calculates payout for a hand result.
func calculatePayout(bet: Int, result: HandResult, isDoubled: Bool) -> Int {
let effectiveBet = isDoubled ? bet * 2 : bet
switch result {
case .blackjack:
return Int(Double(bet) * settings.blackjackPayout) + bet
case .win:
return effectiveBet * 2
case .push:
return effectiveBet
case .lose, .bust:
return 0
case .surrender:
return bet / 2
case .insuranceWin:
return bet * 3 // 2:1 + original bet
case .insuranceLose:
return 0
}
}
// MARK: - Action Availability
/// Whether player can double down on this hand.
func canDoubleDown(hand: BlackjackHand, balance: Int) -> Bool {
guard hand.cards.count == 2 else { return false }
guard !hand.isDoubledDown else { return false }
guard balance >= hand.bet else { return false }
// After split, check DAS rule
if hand.isSplit && !settings.doubleAfterSplit {
return false
}
return true
}
/// Whether player can split this hand.
func canSplit(hand: BlackjackHand, balance: Int, currentSplitCount: Int) -> Bool {
guard hand.canSplit else { return false }
guard balance >= hand.bet else { return false }
guard currentSplitCount < 3 else { return false } // Max 4 hands
// Check resplit aces
if hand.isSplit && hand.cards.first?.rank == .ace && !settings.resplitAces {
return false
}
return true
}
/// Whether player can surrender.
func canSurrender(hand: BlackjackHand) -> Bool {
guard settings.lateSurrender else { return false }
guard hand.cards.count == 2 else { return false }
guard !hand.isSplit else { return false }
return true
}
/// Whether insurance should be offered.
func shouldOfferInsurance(dealerUpCard: Card) -> Bool {
settings.insuranceAllowed && dealerUpCard.rank == .ace
}
// MARK: - Basic Strategy Hint
/// Returns the basic strategy recommendation based on BJA chart.
/// Accounts for game settings (surrender, dealer hits soft 17, etc.)
func getHint(playerHand: BlackjackHand, dealerUpCard: Card) -> String {
let playerValue = playerHand.value
let dealerValue = dealerUpCard.blackjackValue
let isSoft = playerHand.isSoft
let canDouble = playerHand.cards.count == 2
let surrenderAvailable = settings.lateSurrender
let dealerHitsS17 = settings.dealerHitsSoft17
// SURRENDER (when available) - check first
if surrenderAvailable && playerHand.cards.count == 2 {
// 16 vs 9, 10, A - Surrender
if playerValue == 16 && !isSoft && (dealerValue >= 9 || dealerValue == 1) {
return String(localized: "Surrender")
}
// 15 vs 10 - Surrender
if playerValue == 15 && !isSoft && dealerValue == 10 {
return String(localized: "Surrender")
}
// 15 vs A - Surrender (if dealer hits soft 17)
if playerValue == 15 && !isSoft && dealerValue == 1 && dealerHitsS17 {
return String(localized: "Surrender")
}
}
// PAIRS
if playerHand.canSplit {
let pairRank = playerHand.cards[0].rank
switch pairRank {
case .ace:
return String(localized: "Split")
case .eight:
return String(localized: "Split")
case .ten, .jack, .queen, .king:
return String(localized: "Stand")
case .five:
// Never split 5s - treat as hard 10
return (canDouble && dealerValue <= 9) ? String(localized: "Double") : String(localized: "Hit")
case .four:
// Split 4s vs 5-6 (if DAS), otherwise hit
return (settings.doubleAfterSplit && (dealerValue == 5 || dealerValue == 6))
? String(localized: "Split") : String(localized: "Hit")
case .two, .three:
// Split 2s/3s vs 2-7
return dealerValue >= 2 && dealerValue <= 7 ? String(localized: "Split") : String(localized: "Hit")
case .six:
// Split 6s vs 2-6
return dealerValue >= 2 && dealerValue <= 6 ? String(localized: "Split") : String(localized: "Hit")
case .seven:
// Split 7s vs 2-7
return dealerValue >= 2 && dealerValue <= 7 ? String(localized: "Split") : String(localized: "Hit")
case .nine:
// Split 9s vs 2-6, 8-9. Stand vs 7, 10, A
if dealerValue == 7 || dealerValue == 10 || dealerValue == 1 {
return String(localized: "Stand")
}
return String(localized: "Split")
}
}
// SOFT HANDS (Ace counted as 11)
if isSoft {
switch playerValue {
case 20, 21: // A,9 or A,10
return String(localized: "Stand")
case 19: // A,8
// Double vs 6 if dealer hits S17, otherwise stand
if canDouble && dealerValue == 6 && dealerHitsS17 {
return String(localized: "Double")
}
return String(localized: "Stand")
case 18: // A,7
// Double vs 3-6, Stand vs 2/7/8, Hit vs 9/10/A
if canDouble && dealerValue >= 3 && dealerValue <= 6 {
return String(localized: "Double")
}
if dealerValue == 2 || dealerValue == 7 || dealerValue == 8 {
return String(localized: "Stand")
}
return String(localized: "Hit")
case 17: // A,6
// Double vs 3-6, otherwise hit
if canDouble && dealerValue >= 3 && dealerValue <= 6 {
return String(localized: "Double")
}
return String(localized: "Hit")
case 16, 15: // A,5 or A,4
// Double vs 4-6, otherwise hit
if canDouble && dealerValue >= 4 && dealerValue <= 6 {
return String(localized: "Double")
}
return String(localized: "Hit")
case 14, 13: // A,3 or A,2
// Double vs 5-6, otherwise hit
if canDouble && dealerValue >= 5 && dealerValue <= 6 {
return String(localized: "Double")
}
return String(localized: "Hit")
default:
return String(localized: "Hit")
}
}
// HARD HANDS
switch playerValue {
case 17...21:
return String(localized: "Stand")
case 16:
// Stand vs 2-6, Hit vs 7+
return dealerValue >= 2 && dealerValue <= 6 ? String(localized: "Stand") : String(localized: "Hit")
case 15:
// Stand vs 2-6, Hit vs 7+
return dealerValue >= 2 && dealerValue <= 6 ? String(localized: "Stand") : String(localized: "Hit")
case 14:
// Stand vs 2-6, Hit vs 7+
return dealerValue >= 2 && dealerValue <= 6 ? String(localized: "Stand") : String(localized: "Hit")
case 13:
// Stand vs 2-6, Hit vs 7+
return dealerValue >= 2 && dealerValue <= 6 ? String(localized: "Stand") : String(localized: "Hit")
case 12:
// Stand vs 4-6, Hit vs 2-3 and 7+
return dealerValue >= 4 && dealerValue <= 6 ? String(localized: "Stand") : String(localized: "Hit")
case 11:
// Always double (except vs A in some rules)
if canDouble {
return String(localized: "Double")
}
return String(localized: "Hit")
case 10:
// Double vs 2-9, Hit vs 10/A
if canDouble && dealerValue >= 2 && dealerValue <= 9 {
return String(localized: "Double")
}
return String(localized: "Hit")
case 9:
// Double vs 3-6, Hit otherwise
if canDouble && dealerValue >= 3 && dealerValue <= 6 {
return String(localized: "Double")
}
return String(localized: "Hit")
default: // 8 or less
return String(localized: "Hit")
}
}
/// Returns the count-adjusted strategy recommendation with deviation explanation.
/// Based on the "Illustrious 18" - the most valuable count-based deviations.
func getCountAdjustedHint(playerHand: BlackjackHand, dealerUpCard: Card) -> String {
let basicHint = getHint(playerHand: playerHand, dealerUpCard: dealerUpCard)
let tc = Int(trueCount.rounded())
let playerValue = playerHand.value
let dealerValue = dealerUpCard.blackjackValue
let isSoft = playerHand.isSoft
// Check for count-based deviations from basic strategy
// 16 vs 10: Stand at TC 0+ (basic says Hit)
if playerValue == 16 && !isSoft && dealerValue == 10 {
if tc >= 0 {
return String(localized: "Stand (Count: 16v10 at TC≥0)")
}
}
// 15 vs 10: Stand at TC +4+ (basic says Hit)
if playerValue == 15 && !isSoft && dealerValue == 10 {
if tc >= 4 {
return String(localized: "Stand (Count: 15v10 at TC≥+4)")
}
}
// 12 vs 2: Stand at TC +3+ (basic says Hit)
if playerValue == 12 && !isSoft && dealerValue == 2 {
if tc >= 3 {
return String(localized: "Stand (Count: 12v2 at TC≥+3)")
}
}
// 12 vs 3: Stand at TC +2+ (basic says Hit)
if playerValue == 12 && !isSoft && dealerValue == 3 {
if tc >= 2 {
return String(localized: "Stand (Count: 12v3 at TC≥+2)")
}
}
// 12 vs 4: Hit at TC < 0 (basic says Stand)
if playerValue == 12 && !isSoft && dealerValue == 4 {
if tc < 0 {
return String(localized: "Hit (Count: 12v4 at TC<0)")
}
}
// 13 vs 2: Hit at TC < -1 (basic says Stand)
if playerValue == 13 && !isSoft && dealerValue == 2 {
if tc < -1 {
return String(localized: "Hit (Count: 13v2 at TC<-1)")
}
}
// 16 vs 9: Stand at TC +5+ (basic says Hit)
if playerValue == 16 && !isSoft && dealerValue == 9 {
if tc >= 5 {
return String(localized: "Stand (Count: 16v9 at TC≥+5)")
}
}
// 10 vs 10: Double at TC +4+ (basic says Hit)
if playerValue == 10 && !isSoft && playerHand.cards.count == 2 && dealerValue == 10 {
if tc >= 4 {
return String(localized: "Double (Count: 10v10 at TC≥+4)")
}
}
// 10 vs A: Double at TC +4+ (basic says Hit)
if playerValue == 10 && !isSoft && playerHand.cards.count == 2 && dealerValue == 1 {
if tc >= 4 {
return String(localized: "Double (Count: 10vA at TC≥+4)")
}
}
// 9 vs 2: Double at TC +1+ (basic says Hit)
if playerValue == 9 && !isSoft && playerHand.cards.count == 2 && dealerValue == 2 {
if tc >= 1 {
return String(localized: "Double (Count: 9v2 at TC≥+1)")
}
}
// 9 vs 7: Double at TC +3+ (basic says Hit)
if playerValue == 9 && !isSoft && playerHand.cards.count == 2 && dealerValue == 7 {
if tc >= 3 {
return String(localized: "Double (Count: 9v7 at TC≥+3)")
}
}
// Pair of 10s vs 5: Split at TC +5+ (basic says Stand)
if playerHand.canSplit && playerHand.cards[0].blackjackValue == 10 && dealerValue == 5 {
if tc >= 5 {
return String(localized: "Split (Count: 10,10v5 at TC≥+5)")
}
}
// Pair of 10s vs 6: Split at TC +4+ (basic says Stand)
if playerHand.canSplit && playerHand.cards[0].blackjackValue == 10 && dealerValue == 6 {
if tc >= 4 {
return String(localized: "Split (Count: 10,10v6 at TC≥+4)")
}
}
// No deviation applies, return basic strategy
return basicHint
}
}

View File

@ -1,780 +0,0 @@
//
// GameState.swift
// Blackjack
//
// Manages the game state machine for Blackjack.
//
import SwiftUI
import CasinoKit
/// Current phase of the game.
enum GamePhase: Equatable {
case betting
case dealing
case insurance
case playerTurn(handIndex: Int)
case dealerTurn
case roundComplete
}
/// Main game state manager.
@Observable
@MainActor
final class GameState {
// MARK: - Core State
/// Current player balance.
private(set) var balance: Int
/// Current game phase.
private(set) var currentPhase: GamePhase = .betting
/// The current bet amount (before deal).
var currentBet: Int = 0
/// Insurance bet amount.
var insuranceBet: Int = 0
/// Whether a reshuffle notification should be shown.
var showReshuffleNotification: Bool = false
// MARK: - Hands
/// Player's hands (can have multiple after splits).
private(set) var playerHands: [BlackjackHand] = []
/// Dealer's hand.
private(set) var dealerHand: BlackjackHand = BlackjackHand()
/// Index of the hand currently being played.
private(set) var activeHandIndex: Int = 0
/// The active player hand.
var activeHand: BlackjackHand? {
guard activeHandIndex < playerHands.count else { return nil }
return playerHands[activeHandIndex]
}
/// Dealer's face-up card.
var dealerUpCard: Card? {
dealerHand.cards.first
}
// MARK: - UI State
/// Whether to show the result banner.
var showResultBanner: Bool = false
/// The result of the last round.
private(set) var lastRoundResult: RoundResult?
/// Round history for statistics.
private(set) var roundHistory: [RoundResult] = []
// MARK: - Statistics (persisted)
private(set) var totalWinnings: Int = 0
private(set) var biggestWin: Int = 0
private(set) var biggestLoss: Int = 0
private(set) var blackjackCount: Int = 0
private(set) var bustCount: Int = 0
// MARK: - Persistence
/// iCloud sync manager for game data.
let persistence: CloudSyncManager<BlackjackGameData>
// MARK: - Engine & Settings
/// The game engine.
let engine: BlackjackEngine
/// Game settings.
let settings: GameSettings
/// Sound manager.
private let sound = SoundManager.shared
// MARK: - Computed Properties
/// Total bet across all hands.
var totalBet: Int {
playerHands.reduce(0) { $0 + $1.bet * ($1.isDoubledDown ? 2 : 1) } + insuranceBet
}
/// Whether player can place a bet.
/// True if in betting phase, have balance, and haven't hit max bet.
var canBet: Bool {
currentPhase == .betting && balance > 0 && currentBet < settings.maxBet
}
/// Whether the current hand can hit.
var canHit: Bool {
guard case .playerTurn = currentPhase else { return false }
return activeHand?.canHit ?? false
}
/// Whether the current hand can stand.
var canStand: Bool {
guard case .playerTurn = currentPhase else { return false }
return !(activeHand?.isBusted ?? true)
}
/// Whether the current hand can double.
var canDouble: Bool {
guard case .playerTurn = currentPhase else { return false }
guard let hand = activeHand else { return false }
return engine.canDoubleDown(hand: hand, balance: balance)
}
/// Whether the current hand can split.
var canSplit: Bool {
guard case .playerTurn = currentPhase else { return false }
guard let hand = activeHand else { return false }
let splitCount = playerHands.count - 1
return engine.canSplit(hand: hand, balance: balance, currentSplitCount: splitCount)
}
/// Whether the player can surrender.
var canSurrender: Bool {
guard case .playerTurn = currentPhase else { return false }
guard let hand = activeHand else { return false }
return engine.canSurrender(hand: hand)
}
/// Whether the game is over (out of money and no active bet).
var isGameOver: Bool {
balance < settings.minBet && currentPhase == .betting && currentBet == 0
}
/// Total rounds played.
var roundsPlayed: Int {
roundHistory.count
}
/// Whether it's currently the player's turn.
var isPlayerTurn: Bool {
if case .playerTurn = currentPhase { return true }
return false
}
/// Whether the dealer's hole card should be revealed.
var shouldShowDealerHoleCard: Bool {
switch currentPhase {
case .dealerTurn, .roundComplete:
return true
default:
return false
}
}
// MARK: - Hints
/// Current gameplay hint based on basic strategy.
var currentHint: String? {
guard settings.showHints else { return nil }
guard isPlayerTurn else { return nil }
guard let hand = activeHand,
let upCard = dealerUpCard else { return nil }
// Use count-adjusted hints when card counting is enabled
if settings.showCardCount {
return engine.getCountAdjustedHint(playerHand: hand, dealerUpCard: upCard)
}
return engine.getHint(playerHand: hand, dealerUpCard: upCard)
}
/// Whether the current bet is below the minimum required.
var isBetBelowMinimum: Bool {
currentBet > 0 && currentBet < settings.minBet
}
/// Amount needed to reach minimum bet.
var amountNeededForMinimum: Int {
max(0, settings.minBet - currentBet)
}
/// Whether the current bet has reached the maximum.
var isBetAtMaximum: Bool {
currentBet >= settings.maxBet
}
/// Betting recommendation based on the true count.
var bettingHint: String? {
guard settings.showCardCount else { return nil }
guard currentPhase == .betting else { return nil }
let tc = Int(engine.trueCount.rounded())
switch tc {
case ...(-2):
return String(localized: "Bet minimum or sit out")
case -1:
return String(localized: "Bet minimum")
case 0:
return String(localized: "Bet minimum (neutral)")
case 1:
return String(localized: "Bet 2x minimum")
case 2:
return String(localized: "Bet 4x minimum")
case 3:
return String(localized: "Bet 6x minimum")
case 4:
return String(localized: "Bet 8x minimum")
case 5...:
return String(localized: "Bet maximum!")
default:
return nil
}
}
// MARK: - Initialization
init(settings: GameSettings) {
self.settings = settings
self.balance = settings.startingBalance
self.engine = BlackjackEngine(settings: settings)
self.persistence = CloudSyncManager<BlackjackGameData>()
syncSoundSettings()
loadSavedGame()
}
/// Syncs sound settings with SoundManager.
private func syncSoundSettings() {
sound.soundEnabled = settings.soundEnabled
sound.hapticsEnabled = settings.hapticsEnabled
sound.volume = settings.soundVolume
}
/// Called when deck count setting changes - reshuffles with new deck count.
func applyDeckCountChange() {
engine.reshuffle()
}
// MARK: - Persistence
/// Loads saved game data from iCloud or local storage.
private func loadSavedGame() {
let data = persistence.load()
self.balance = data.balance
self.totalWinnings = data.totalWinnings
self.biggestWin = data.biggestWin
self.biggestLoss = data.biggestLoss
self.blackjackCount = data.blackjackCount
self.bustCount = data.bustCount
// Set up callback for when iCloud data arrives later
persistence.onCloudDataReceived = { [weak self] newData in
guard let self else { return }
self.balance = newData.balance
self.totalWinnings = newData.totalWinnings
self.biggestWin = newData.biggestWin
self.biggestLoss = newData.biggestLoss
self.blackjackCount = newData.blackjackCount
self.bustCount = newData.bustCount
}
}
/// Saves current game data to iCloud and local storage.
private func saveGameData() {
let savedRounds: [SavedRoundResult] = roundHistory.map { result in
SavedRoundResult(
date: Date(),
mainResult: result.mainHandResult.saveName,
hadSplit: result.hadSplit,
totalWinnings: result.totalWinnings
)
}
let data = BlackjackGameData(
lastModified: Date(),
balance: balance,
roundHistory: savedRounds,
totalWinnings: totalWinnings,
biggestWin: biggestWin,
biggestLoss: biggestLoss,
blackjackCount: blackjackCount,
bustCount: bustCount
)
persistence.save(data)
}
/// Clears all saved data.
func clearAllData() {
persistence.reset()
balance = settings.startingBalance
totalWinnings = 0
biggestWin = 0
biggestLoss = 0
blackjackCount = 0
bustCount = 0
roundHistory = []
newRound()
}
// MARK: - Betting
/// Places a bet.
func placeBet(amount: Int) {
guard canBet else { return }
guard currentBet + amount <= settings.maxBet else { return }
guard balance >= amount else { return }
currentBet += amount
balance -= amount
sound.play(.chipPlace)
}
/// Clears the current bet.
func clearBet() {
balance += currentBet
currentBet = 0
sound.play(.chipPlace)
}
// MARK: - Dealing
/// Whether the player can deal (betting phase with valid bet).
var canDeal: Bool {
currentPhase == .betting && currentBet >= settings.minBet
}
/// Deals the initial cards.
func deal() async {
guard canDeal else { return }
// Ensure enough cards for a full hand - reshuffle if needed
if !engine.canDealNewHand {
engine.reshuffle()
showReshuffleNotification = true
Task {
try? await Task.sleep(for: .seconds(2))
showReshuffleNotification = false
}
}
currentPhase = .dealing
playerHands = [BlackjackHand(bet: currentBet)]
dealerHand = BlackjackHand()
activeHandIndex = 0
insuranceBet = 0
let delay = settings.showAnimations ? 0.3 * settings.dealingSpeed : 0
// European no-hole-card: deal 3 cards (player, dealer, player)
// American style: deal 4 cards (player, dealer, player, dealer)
let cardCount = settings.noHoleCard ? 3 : 4
for i in 0..<cardCount {
if let card = engine.dealCard() {
if i % 2 == 0 {
playerHands[0].cards.append(card)
} else {
dealerHand.cards.append(card)
}
sound.play(.cardDeal)
if delay > 0 {
try? await Task.sleep(for: .seconds(delay))
}
}
}
// Check for insurance offer (only in American style with hole card)
if !settings.noHoleCard, let upCard = dealerUpCard, engine.shouldOfferInsurance(dealerUpCard: upCard) {
currentPhase = .insurance
return
}
// Check for immediate blackjacks (only in American style - European checks after player acts)
if !settings.noHoleCard {
await checkForBlackjacks()
} else {
// European: just go to player turn (blackjacks checked after player acts)
if playerHands[0].isBlackjack {
// Player blackjack - will be handled after dealer gets second card
currentPhase = .playerTurn(handIndex: 0)
} else {
currentPhase = .playerTurn(handIndex: 0)
}
}
}
/// Checks for blackjacks and handles accordingly.
private func checkForBlackjacks() async {
let playerBJ = playerHands[0].isBlackjack
let dealerBJ = dealerHand.isBlackjack
if playerBJ || dealerBJ {
// Reveal dealer card
sound.play(.cardFlip)
if playerBJ && dealerBJ {
// Push
playerHands[0].result = .push
await completeRound()
} else if playerBJ {
// Player wins
playerHands[0].result = .blackjack
await completeRound()
} else {
// Dealer wins
playerHands[0].result = .lose
await completeRound()
}
} else {
currentPhase = .playerTurn(handIndex: 0)
}
}
// MARK: - Insurance
/// Takes insurance bet.
func takeInsurance() async {
let insuranceAmount = currentBet / 2
guard balance >= insuranceAmount else {
declineInsurance()
return
}
insuranceBet = insuranceAmount
balance -= insuranceAmount
sound.play(.chipPlace)
// Check dealer blackjack
if dealerHand.isBlackjack {
sound.play(.cardFlip)
// Insurance wins
let payout = insuranceBet * 3
balance += payout
playerHands[0].result = .lose
await completeRound()
} else {
// Insurance loses, continue game
insuranceBet = 0 // Lost the insurance bet
await checkForBlackjacks()
}
}
/// Declines insurance.
func declineInsurance() {
Task {
await checkForBlackjacks()
}
}
// MARK: - Player Actions
/// Player hits (takes another card).
func hit() async {
guard canHit else { return }
guard let card = engine.dealCard() else { return }
playerHands[activeHandIndex].cards.append(card)
sound.play(.cardDeal)
// Check for bust or 21
if playerHands[activeHandIndex].isBusted {
playerHands[activeHandIndex].result = .bust
await moveToNextHand()
} else if playerHands[activeHandIndex].value == 21 {
playerHands[activeHandIndex].isStanding = true
await moveToNextHand()
}
}
/// Player stands.
func stand() async {
guard canStand else { return }
playerHands[activeHandIndex].isStanding = true
await moveToNextHand()
}
/// Player doubles down.
func doubleDown() async {
guard canDouble else { return }
let additionalBet = playerHands[activeHandIndex].bet
balance -= additionalBet
playerHands[activeHandIndex].isDoubledDown = true
sound.play(.chipPlace)
// Deal one card and stand
if let card = engine.dealCard() {
playerHands[activeHandIndex].cards.append(card)
sound.play(.cardDeal)
}
if playerHands[activeHandIndex].isBusted {
playerHands[activeHandIndex].result = .bust
} else {
playerHands[activeHandIndex].isStanding = true
}
await moveToNextHand()
}
/// Player splits the hand.
func split() async {
guard canSplit else { return }
let originalHand = playerHands[activeHandIndex]
let splitCard = originalHand.cards[1]
// Create two new hands
var hand1 = BlackjackHand(cards: [originalHand.cards[0]], bet: originalHand.bet)
hand1.isSplit = true
var hand2 = BlackjackHand(cards: [splitCard], bet: originalHand.bet)
hand2.isSplit = true
// Deduct bet for second hand
balance -= originalHand.bet
sound.play(.chipPlace)
// Deal one card to each hand
if let card1 = engine.dealCard() {
hand1.cards.append(card1)
sound.play(.cardDeal)
}
if let card2 = engine.dealCard() {
hand2.cards.append(card2)
sound.play(.cardDeal)
}
// Replace original with split hands
playerHands.remove(at: activeHandIndex)
playerHands.insert(hand1, at: activeHandIndex)
playerHands.insert(hand2, at: activeHandIndex + 1)
// If split aces, typically only one card each and stand
if originalHand.cards[0].rank == .ace && !settings.resplitAces {
playerHands[activeHandIndex].isStanding = true
playerHands[activeHandIndex + 1].isStanding = true
await moveToNextHand()
} else {
currentPhase = .playerTurn(handIndex: activeHandIndex)
}
}
/// Player surrenders.
func surrender() async {
guard canSurrender else { return }
playerHands[activeHandIndex].result = .surrender
await completeRound()
}
// MARK: - Hand Progression
/// Moves to the next hand or dealer turn.
private func moveToNextHand() async {
// Check if there are more hands to play
let nextIndex = activeHandIndex + 1
if nextIndex < playerHands.count {
if !playerHands[nextIndex].isStanding && !playerHands[nextIndex].isBusted {
activeHandIndex = nextIndex
currentPhase = .playerTurn(handIndex: nextIndex)
return
}
}
// Check if all hands are busted
let allBusted = playerHands.allSatisfy { $0.isBusted }
if allBusted {
await completeRound()
return
}
// Dealer's turn
await dealerTurn()
}
// MARK: - Dealer Turn
/// Plays out the dealer's hand.
private func dealerTurn() async {
currentPhase = .dealerTurn
let delay = settings.showAnimations ? 0.5 * settings.dealingSpeed : 0
// European no-hole-card: deal the second card now
if settings.noHoleCard && dealerHand.cards.count == 1 {
if let card = engine.dealCard() {
dealerHand.cards.append(card)
sound.play(.cardDeal)
if delay > 0 {
try? await Task.sleep(for: .seconds(delay))
}
}
// Check for dealer blackjack in European mode
// Player loses everything (no early check in European)
if dealerHand.isBlackjack {
// Mark player hands as lost if they don't have blackjack
for i in 0..<playerHands.count {
if playerHands[i].result == nil {
if playerHands[i].isBlackjack {
playerHands[i].result = .push
} else {
playerHands[i].result = .lose
}
}
}
await completeRound()
return
}
} else {
// American style: reveal hole card
sound.play(.cardFlip)
if delay > 0 {
try? await Task.sleep(for: .seconds(delay))
}
}
// Dealer draws
while engine.dealerShouldHit(hand: dealerHand) {
if let card = engine.dealCard() {
dealerHand.cards.append(card)
sound.play(.cardDeal)
if delay > 0 {
try? await Task.sleep(for: .seconds(delay))
}
}
}
await completeRound()
}
// MARK: - Round Completion
/// Completes the round and calculates payouts.
private func completeRound() async {
currentPhase = .roundComplete
var roundWinnings = 0
var wasBlackjack = false
var hadBust = false
// Evaluate each hand
for i in 0..<playerHands.count {
if playerHands[i].result == nil {
playerHands[i].result = engine.determineResult(
playerHand: playerHands[i],
dealerHand: dealerHand
)
}
if let result = playerHands[i].result {
let payout = engine.calculatePayout(
bet: playerHands[i].bet,
result: result,
isDoubled: playerHands[i].isDoubledDown
)
balance += payout
roundWinnings += payout - playerHands[i].bet * (playerHands[i].isDoubledDown ? 2 : 1)
if result == .blackjack {
wasBlackjack = true
}
if result == .bust {
hadBust = true
}
}
}
// Update statistics
totalWinnings += roundWinnings
if roundWinnings > biggestWin {
biggestWin = roundWinnings
}
if roundWinnings < biggestLoss {
biggestLoss = roundWinnings
}
if wasBlackjack {
blackjackCount += 1
}
if hadBust {
bustCount += 1
}
// Create round result with all hand results
let allHandResults = playerHands.map { $0.result ?? .lose }
lastRoundResult = RoundResult(
handResults: allHandResults,
insuranceResult: insuranceBet > 0 ? (dealerHand.isBlackjack ? .insuranceWin : .insuranceLose) : nil,
totalWinnings: roundWinnings,
wasBlackjack: wasBlackjack
)
roundHistory.append(lastRoundResult!)
// Save game data to iCloud
saveGameData()
// Play appropriate sound
if roundWinnings > 0 {
sound.play(.win)
} else if roundWinnings < 0 {
sound.play(.lose)
} else {
sound.play(.push)
}
// Reset bet for next round
currentBet = 0
showResultBanner = true
// Check if shoe needs reshuffling
if engine.needsReshuffle {
engine.reshuffle()
showReshuffleNotification = true
// Auto-dismiss after a delay
Task {
try? await Task.sleep(for: .seconds(2))
showReshuffleNotification = false
}
}
}
// MARK: - New Round
/// Starts a new round.
func newRound() {
// Reset all hand state
playerHands = []
dealerHand = BlackjackHand()
activeHandIndex = 0
// Reset bets
currentBet = 0
insuranceBet = 0
// Reset UI state
showResultBanner = false
lastRoundResult = nil
currentPhase = .betting
sound.play(.newRound)
}
// MARK: - Game Reset
/// Resets the entire game (keeps statistics).
func resetGame() {
balance = settings.startingBalance
roundHistory = []
engine.reshuffle()
newRound()
saveGameData()
}
}

View File

@ -1,661 +0,0 @@
# Casino Game Development Guide
This guide explains how to build a new casino card game (like Blackjack, Poker, etc.) using CasinoKit, following the patterns established in the Baccarat app.
## Project Structure
```
YourGame/
├── YourGameApp.swift # App entry point
├── ContentView.swift # Root view (usually just GameTableView)
├── Engine/
│ ├── YourGameEngine.swift # Game rules & logic
│ └── GameState.swift # Game state machine
├── Models/
│ ├── BetType.swift # Game-specific bet types
│ ├── GameResult.swift # Win/loss/push outcomes
│ ├── GameSettings.swift # User settings (can reuse pattern)
│ ├── Hand.swift # Hand representation (if needed)
│ └── Shoe.swift # Card shoe (if multi-deck)
├── Storage/
│ └── YourGameData.swift # Persistence model (PersistableGameData)
├── Theme/
│ └── DesignConstants.swift # Game-specific design tokens
├── Views/
│ ├── GameTableView.swift # Main game screen
│ ├── YourTableLayoutView.swift # Game-specific table layout
│ ├── ResultBannerView.swift # Win/loss display
│ ├── RulesHelpView.swift # Game rules explanation
│ ├── SettingsView.swift # Settings screen
│ └── StatisticsSheetView.swift # Stats display
└── Resources/
└── Localizable.xcstrings # Translations
```
## What CasinoKit Provides
### Core Components (Import `CasinoKit`)
| Category | Components | Usage |
|----------|------------|-------|
| **Cards** | `Card`, `Suit`, `Rank`, `Deck` | Card models |
| **Card Views** | `CardView`, `CardPlaceholderView` | Card display |
| **Chips** | `ChipDenomination`, `ChipView`, `ChipStackView`, `ChipSelectorView` | Betting chips |
| **Table** | `TableBackgroundView`, `FeltPatternView` | Casino felt background |
| **Overlays** | `GameOverView`, `ConfettiView` | Game over & celebrations |
| **Top Bar** | `TopBarView` | Balance, settings, stats buttons |
| **Badges** | `ValueBadge` | Numeric value display |
| **Settings** | `SettingsToggle`, `SpeedPicker`, `VolumePicker`, `BalancePicker` | Settings UI |
| **Sheets** | `SheetContainerView`, `SheetSection` | Modal sheets |
| **Branding** | `AppIconView`, `LaunchScreenView` | App icons & splash |
| **Audio** | `SoundManager`, `GameSound` | Sound effects & haptics |
| **Storage** | `CloudSyncManager`, `PersistableGameData` | iCloud persistence |
| **Models** | `TableLimits` | Betting limits presets |
| **Design** | `CasinoDesign` | Spacing, colors, animations |
### Using CasinoKit Components
```swift
import SwiftUI
import CasinoKit
struct GameTableView: View {
@State private var settings = GameSettings()
@State private var gameState: GameState?
@State private var selectedChip: ChipDenomination = .hundred
var body: some View {
ZStack {
// 1. Table Background (from CasinoKit)
TableBackgroundView()
VStack {
// 2. Top Bar (from CasinoKit)
TopBarView(
balance: state.balance,
secondaryInfo: "\(state.engine.shoe.cardsRemaining)",
secondaryIcon: "rectangle.portrait.on.rectangle.portrait.fill",
onReset: { state.resetGame() },
onSettings: { showSettings = true },
onHelp: { showRules = true },
onStats: { showStats = true }
)
// 3. Your Game-Specific Table Layout
YourTableLayoutView(...)
// 4. Chip Selector (from CasinoKit)
ChipSelectorView(
selectedChip: $selectedChip,
availableChips: ChipDenomination.allCases
)
// 5. Action Buttons (game-specific)
ActionButtonsView(...)
}
// 6. Result Banner (game-specific, but follows pattern)
if state.showResultBanner {
ResultBannerView(...)
}
// 7. Confetti for Wins (from CasinoKit)
if state.lastWinnings > 0 {
ConfettiView()
}
// 8. Game Over (from CasinoKit)
if state.isGameOver {
GameOverView(
roundsPlayed: state.roundsPlayed,
onPlayAgain: { state.resetGame() }
)
}
}
.sheet(isPresented: $showSettings) {
SettingsView(settings: settings, gameState: state) { ... }
}
}
}
```
## Game-Specific Implementation
### 1. Game Engine (Required)
Create your game's rule engine. This handles:
- Card dealing logic
- Hand evaluation
- Win/loss determination
- Payout calculations
```swift
// Engine/YourGameEngine.swift
import CasinoKit
@Observable
@MainActor
final class YourGameEngine {
var shoe: Shoe
var playerHand: [Card] = []
var dealerHand: [Card] = []
init(deckCount: Int = 6) {
self.shoe = Shoe(deckCount: deckCount)
}
func dealInitialCards() { ... }
func evaluateHand(_ cards: [Card]) -> Int { ... }
func determineWinner() -> GameResult { ... }
func calculatePayout(bet: Int, result: GameResult) -> Int { ... }
}
```
### 2. Game State (Required)
Manages the state machine for your game:
```swift
// Engine/GameState.swift
import SwiftUI
import CasinoKit
enum GamePhase {
case betting
case dealing
case playerTurn // Blackjack-specific
case dealerTurn // Blackjack-specific
case roundComplete
}
@Observable
@MainActor
final class GameState {
// Core state
var balance: Int
var currentBets: [BetType: Int] = [:]
var currentPhase: GamePhase = .betting
var showResultBanner = false
// Engine
let engine: YourGameEngine
// Persistence
private let persistence: CloudSyncManager<YourGameData>
// Sound
private let sound = SoundManager.shared
init(settings: GameSettings) {
self.engine = YourGameEngine(deckCount: settings.deckCount.rawValue)
self.balance = settings.startingBalance
self.persistence = CloudSyncManager<YourGameData>()
loadSavedGame()
}
func placeBet(type: BetType, amount: Int) {
currentBets[type, default: 0] += amount
balance -= amount
sound.play(.chipPlace)
}
func deal() async {
currentPhase = .dealing
sound.play(.cardDeal)
// Game-specific dealing logic
}
func newRound() {
currentPhase = .betting
showResultBanner = false
sound.play(.newRound)
}
}
```
### 3. Bet Types (Game-Specific)
```swift
// Models/BetType.swift
enum BetType: String, CaseIterable, Identifiable {
// Blackjack example:
case main = "main"
case insurance = "insurance"
case doubleDown = "double"
case split = "split"
// Baccarat example:
// case player, banker, tie, playerPair, bankerPair, dragonBonusPlayer, dragonBonusBanker
var id: String { rawValue }
var displayName: String { ... }
var payoutMultiplier: Double { ... }
}
```
### 4. Game Result (Game-Specific)
```swift
// Models/GameResult.swift
enum GameResult: Equatable {
// Blackjack example:
case playerWins
case dealerWins
case push
case blackjack
case bust
var displayText: String { ... }
var color: Color { ... }
}
```
### 5. Table Layout (Game-Specific)
This is the main visual difference between games:
```swift
// Views/YourTableLayoutView.swift
struct BlackjackTableView: View {
// Shows dealer hand at top, player hand(s) below
// Hit/Stand/Double/Split buttons
// Insurance betting zone
}
struct BaccaratTableView: View {
// Shows Player/Banker/Tie betting zones
// Side bet zones (pairs, dragon bonus)
}
struct PokerTableView: View {
// Community cards in center
// Player positions around table
// Pot display
}
```
### 6. Settings View (Mostly Reusable)
```swift
// Views/SettingsView.swift
import CasinoKit
struct SettingsView: View {
@Bindable var settings: GameSettings
let gameState: GameState
var body: some View {
SheetContainerView(title: "Settings") {
// Table Limits (from CasinoKit pattern)
SheetSection(title: "TABLE LIMITS", icon: "banknote") {
// Use TableLimits enum from CasinoKit
}
// Deck Settings (game-specific)
SheetSection(title: "DECK SETTINGS", icon: "rectangle.portrait.on.rectangle.portrait") {
// DeckCount options
}
// Display Settings (reusable)
SheetSection(title: "DISPLAY", icon: "eye") {
SettingsToggle(title: "...", subtitle: "...", isOn: $settings.showX)
}
// Sound (from CasinoKit)
SheetSection(title: "SOUND & HAPTICS", icon: "speaker.wave.2") {
SettingsToggle(...)
VolumePicker(volume: $settings.soundVolume)
}
// iCloud Sync (pattern from Baccarat)
SheetSection(title: "CLOUD SYNC", icon: "icloud") { ... }
}
}
}
```
## Sound Integration
```swift
// In your GameState
let sound = SoundManager.shared
// Play sounds at appropriate moments:
sound.play(.chipPlace) // When placing a bet
sound.play(.cardDeal) // When dealing cards
sound.play(.cardFlip) // When flipping cards
sound.play(.win) // On player win
sound.play(.lose) // On player loss
sound.play(.push) // On tie/push
sound.play(.newRound) // Starting new round
sound.play(.gameOver) // When out of chips
```
## Persistence
```swift
// Storage/YourGameData.swift
import CasinoKit
struct BlackjackGameData: PersistableGameData {
static let gameIdentifier = "blackjack"
var roundsPlayed: Int { roundHistory.count }
var lastModified: Date
static var empty: BlackjackGameData {
BlackjackGameData(
lastModified: Date(),
balance: 10_000,
roundHistory: [],
totalWinnings: 0,
blackjackCount: 0, // Game-specific stat
bustCount: 0 // Game-specific stat
)
}
var balance: Int
var roundHistory: [SavedRoundResult]
var totalWinnings: Int
var blackjackCount: Int
var bustCount: Int
}
```
## Design Constants
Extend CasinoDesign for game-specific values:
```swift
// Theme/DesignConstants.swift
enum Design {
// Reuse CasinoDesign values
typealias Spacing = CasinoDesign.Spacing
typealias CornerRadius = CasinoDesign.CornerRadius
typealias Animation = CasinoDesign.Animation
// Game-specific sizes
enum Size {
static let playerCardWidth: CGFloat = 55
static let dealerCardWidth: CGFloat = 50
// ... game-specific dimensions
}
// Game-specific colors (extend Color)
}
extension Color {
enum BettingZone {
static let main = Color.blue.opacity(0.3)
static let insurance = Color.yellow.opacity(0.3)
// ... game-specific colors
}
}
```
## Localization
Use String Catalogs (`.xcstrings`):
```swift
// Game-specific strings
Text(String(localized: "Hit"))
Text(String(localized: "Stand"))
Text(String(localized: "Double Down"))
Text(String(localized: "Split"))
Text(String(localized: "Blackjack!"))
Text(String(localized: "Bust!"))
```
## Responsive Layout (iPhone vs iPad)
### The Problem
On iPad, content that looks great on iPhone will **stretch to fill the screen**, making:
- Betting areas look awkward and disproportionate
- Cards appear too spread out
- Buttons become oversized
- Overlays cover the entire screen unnecessarily
### The Solution: Constrained Width Containers
Use `horizontalSizeClass` to detect iPad and constrain content width:
```swift
struct GameTableView: View {
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@Environment(\.verticalSizeClass) private var verticalSizeClass
/// Whether we're on iPad (regular horizontal size class)
private var isIPad: Bool {
horizontalSizeClass == .regular
}
/// Maximum content width based on device and orientation
private var maxContentWidth: CGFloat {
if isIPad {
// Landscape on iPad gets more width
return verticalSizeClass == .compact
? CasinoDesign.Size.maxContentWidthLandscape // 800pt
: CasinoDesign.Size.maxContentWidthPortrait // 500pt
}
return .infinity // iPhone uses full width
}
var body: some View {
ZStack {
TableBackgroundView()
VStack {
TopBarView(...)
.frame(maxWidth: maxContentWidth)
// Game table - constrained on iPad
YourTableLayoutView(...)
.frame(maxWidth: maxContentWidth)
// Chip selector - constrained
ChipSelectorView(...)
.frame(maxWidth: maxContentWidth)
// Action buttons - constrained
ActionButtonsView(...)
.frame(maxWidth: maxContentWidth)
}
.frame(maxWidth: .infinity) // Centers constrained content
// Overlays - full screen background, constrained content
if showResultBanner {
ResultBannerView(...)
}
}
}
}
```
### Overlay Pattern (Result Banner, Game Over)
Overlays need special handling: **full-screen dim background** with **constrained content card**:
```swift
struct ResultBannerView: View {
var body: some View {
ZStack {
// 1. Full-screen dark background
Color.black.opacity(0.7)
.ignoresSafeArea()
// 2. Constrained content card
VStack {
// Your content
}
.padding()
.background(RoundedRectangle(cornerRadius: 24).fill(...))
.frame(maxWidth: CasinoDesign.Size.maxModalWidth) // 450pt
// Centered automatically by ZStack
}
}
}
```
### Fixed-Size Containers (Prevent Layout Shifts)
When content changes (cards dealt, buttons appear/disappear), prevent jarring layout shifts:
```swift
// ❌ BAD: Container resizes as cards are added
HStack {
ForEach(cards) { card in
CardView(card: card)
}
}
// ✅ GOOD: Fixed container based on max possible content
ZStack {
// Reserve space for max cards (e.g., 3 cards with overlap)
Color.clear
.frame(width: calculateMaxWidth(), height: cardHeight)
// Actual cards centered within
HStack(spacing: cardSpacing) {
ForEach(cards) { card in
CardView(card: card)
}
}
}
```
Same for buttons:
```swift
// ✅ GOOD: Fixed height container for buttons
ZStack {
Color.clear
.frame(height: 60) // Fixed height
// Buttons animate in/out within fixed space
if showDealButton {
ActionButton("Deal", ...)
.transition(.scale.combined(with: .opacity))
}
}
.animation(.spring(duration: 0.3), value: currentPhase)
```
### Confetti Full-Screen Fix
Confetti must cover the entire screen on iPad:
```swift
struct ConfettiView: View {
var body: some View {
GeometryReader { geometry in
ZStack {
ForEach(0..<50, id: \.self) { _ in
ConfettiPiece(containerSize: geometry.size)
}
}
}
.ignoresSafeArea() // Critical!
.allowsHitTesting(false)
}
}
```
### Design Constants for Responsive Layout
```swift
// In CasinoDesign.swift
enum Size {
// Max widths for iPad constraint
static let maxContentWidthPortrait: CGFloat = 500
static let maxContentWidthLandscape: CGFloat = 800
static let maxModalWidth: CGFloat = 450
}
```
### Common Pitfalls
| Issue | Symptom | Fix |
|-------|---------|-----|
| Stretched table | Betting zones look huge on iPad | Add `.frame(maxWidth: maxContentWidth)` |
| Overlay too wide | Result banner covers entire iPad screen | Use full-screen bg + constrained content card |
| Layout shifts | Cards/buttons cause content to jump | Use fixed-size `ZStack` containers |
| Confetti cut off | Only shows in center portion | Use `GeometryReader` + `.ignoresSafeArea()` |
| Settings rows cramped | Title/subtitle too close to divider | Add `.padding(.vertical, ...)` |
### Testing Checklist
- [ ] iPhone SE (smallest)
- [ ] iPhone Pro Max (largest iPhone)
- [ ] iPad Portrait
- [ ] iPad Landscape
- [ ] iPad Split View
- [ ] Dynamic Type at maximum accessibility size
## Checklist for New Game
### Setup
- [ ] Create new target in Xcode
- [ ] Add CasinoKit as dependency
- [ ] Copy `DesignConstants.swift` and customize
- [ ] Create `Localizable.xcstrings`
### Models
- [ ] Define `BetType` enum
- [ ] Define `GameResult` enum
- [ ] Create `YourGameData` for persistence
- [ ] Create `GameSettings` (or reuse pattern)
### Engine
- [ ] Implement game rules in `YourGameEngine`
- [ ] Implement `GameState` with phases
### Views
- [ ] Create `GameTableView` (main container)
- [ ] Create game-specific table layout
- [ ] Create `ResultBannerView` (follow pattern)
- [ ] Create `RulesHelpView` (game rules)
- [ ] Customize `SettingsView`
- [ ] Create `StatisticsSheetView`
### Integration
- [ ] Wire up `SoundManager` for game events
- [ ] Implement `CloudSyncManager` for persistence
- [ ] Add accessibility labels
- [ ] Add localization for all strings
### Polish
- [ ] Test Dynamic Type scaling (all sizes including accessibility)
- [ ] Test VoiceOver navigation
- [ ] Create app icon using `AppIconView`
### Responsive Layout (iPad)
- [ ] Add `maxContentWidth` constraint to main views
- [ ] Test iPad Portrait - content centered, not stretched
- [ ] Test iPad Landscape - wider constraint works well
- [ ] Verify overlays: full-screen bg, constrained content
- [ ] Fixed-size containers prevent layout shifts
- [ ] Confetti covers full screen
- [ ] Settings rows have proper padding
## What Could Be Added to CasinoKit
The following patterns from Baccarat could be abstracted:
1. **Generic ResultBannerView** - Win/loss display with bet breakdown
2. **BettingZone protocol** - Common betting zone behavior
3. **GameStateProtocol** - Common state machine patterns
4. **HandDisplayView** - Generic card hand display
5. **ActionButtonsView** - Deal/Clear/New Round pattern
6. **StatisticsView** - Generic stats display
---
*This guide is based on the Baccarat implementation. For reference, see the Baccarat app structure and CasinoKit source code.*

View File

@ -1,49 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<document type="com.apple.InterfaceBuilder3.CocoaTouch.Storyboard.XIB" version="3.0" toolsVersion="24127" targetRuntime="iOS.CocoaTouch" propertyAccessControl="none" useAutolayout="YES" launchScreen="YES" useTraitCollections="YES" useSafeAreas="YES" colorMatched="YES" initialViewController="01J-lp-oVM">
<device id="retina6_12" orientation="portrait" appearance="light"/>
<dependencies>
<plugIn identifier="com.apple.InterfaceBuilder.IBCocoaTouchPlugin" version="24063"/>
<capability name="Safe area layout guides" minToolsVersion="9.0"/>
<capability name="documents saved in the Xcode 8 format" minToolsVersion="8.0"/>
</dependencies>
<scenes>
<!--View Controller-->
<scene sceneID="EHf-IW-A2E">
<objects>
<viewController id="01J-lp-oVM" sceneMemberID="viewController">
<view key="view" contentMode="scaleToFill" id="Ze5-6b-2t3">
<rect key="frame" x="0.0" y="0.0" width="393" height="852"/>
<autoresizingMask key="autoresizingMask" widthSizable="YES" heightSizable="YES"/>
<subviews>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="🃏 ♠️" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="iconsLabel">
<rect key="frame" x="147.33333333333334" y="351" width="98.666666666666657" height="50"/>
<constraints>
<constraint firstAttribute="height" constant="50" id="iconHeight"/>
</constraints>
<fontDescription key="fontDescription" type="system" pointSize="40"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<label opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" misplaced="YES" text="BLACKJACK" textAlignment="center" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" adjustsFontSizeToFit="NO" translatesAutoresizingMaskIntoConstraints="NO" id="titleLabel">
<rect key="frame" x="81" y="409" width="215" height="41"/>
<fontDescription key="fontDescription" type="boldSystem" pointSize="34"/>
<color key="textColor" red="1" green="0.84313725490196079" blue="0.0" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
</subviews>
<viewLayoutGuide key="safeArea" id="6Tk-OE-BBY"/>
<color key="backgroundColor" red="0.050980392156862744" green="0.34901960784313724" blue="0.14901960784313725" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<constraints>
<constraint firstItem="iconsLabel" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="iconsCenterX"/>
<constraint firstItem="iconsLabel" firstAttribute="centerY" secondItem="Ze5-6b-2t3" secondAttribute="centerY" constant="-50" id="iconsCenterY"/>
<constraint firstItem="titleLabel" firstAttribute="centerX" secondItem="Ze5-6b-2t3" secondAttribute="centerX" id="titleCenterX"/>
<constraint firstItem="titleLabel" firstAttribute="top" secondItem="iconsLabel" secondAttribute="bottom" constant="8" id="titleTopToIcons"/>
</constraints>
</view>
</viewController>
<placeholder placeholderIdentifier="IBFirstResponder" id="iYj-Kq-Ea1" userLabel="First Responder" customClass="UIResponder" sceneMemberID="firstResponder"/>
</objects>
<point key="canvasLocation" x="52.671755725190835" y="374.64788732394368"/>
</scene>
</scenes>
</document>

View File

@ -1,41 +0,0 @@
//
// BetType.swift
// Blackjack
//
// Available betting options in Blackjack.
//
import Foundation
/// Types of bets available in Blackjack.
enum BetType: String, CaseIterable, Identifiable {
case main = "main"
case insurance = "insurance"
case doubleDown = "double"
case split = "split"
var id: String { rawValue }
var displayName: String {
switch self {
case .main: return String(localized: "Main Bet")
case .insurance: return String(localized: "Insurance")
case .doubleDown: return String(localized: "Double Down")
case .split: return String(localized: "Split")
}
}
var payoutMultiplier: Double {
switch self {
case .main: return 1.0 // 1:1
case .insurance: return 2.0 // 2:1
case .doubleDown: return 1.0 // 1:1 on doubled bet
case .split: return 1.0 // 1:1 per hand
}
}
var blackjackPayout: Double {
1.5 // 3:2 for blackjack
}
}

View File

@ -1,96 +0,0 @@
//
// GameResult.swift
// Blackjack
//
// Possible outcomes for a Blackjack hand.
//
import SwiftUI
/// Result of a single Blackjack hand.
enum HandResult: Equatable {
case blackjack // Natural 21
case win // Beat dealer
case lose // Lost to dealer or bust
case push // Tie
case bust // Over 21
case surrender // Gave up half bet
case insuranceWin // Dealer had blackjack
case insuranceLose // Dealer didn't have blackjack
/// String identifier for persistence.
var saveName: String {
switch self {
case .blackjack: return "blackjack"
case .win: return "win"
case .lose: return "lose"
case .push: return "push"
case .bust: return "bust"
case .surrender: return "surrender"
case .insuranceWin: return "insuranceWin"
case .insuranceLose: return "insuranceLose"
}
}
var displayText: String {
switch self {
case .blackjack: return String(localized: "BLACKJACK!")
case .win: return String(localized: "WIN!")
case .lose: return String(localized: "LOSE")
case .push: return String(localized: "PUSH")
case .bust: return String(localized: "BUST!")
case .surrender: return String(localized: "SURRENDER")
case .insuranceWin: return String(localized: "INSURANCE WINS")
case .insuranceLose: return String(localized: "INSURANCE LOSES")
}
}
var color: Color {
switch self {
case .blackjack: return .yellow
case .win: return .green
case .lose, .bust, .insuranceLose: return .red
case .push: return .blue
case .surrender: return .orange
case .insuranceWin: return .green
}
}
var isWin: Bool {
switch self {
case .blackjack, .win, .insuranceWin: return true
default: return false
}
}
}
/// Overall game result for the round.
struct RoundResult: Equatable {
/// Results for all player hands (index 0 = Hand 1, index 1 = Hand 2, etc.)
let handResults: [HandResult]
let insuranceResult: HandResult?
let totalWinnings: Int
let wasBlackjack: Bool
/// The main/best result for display purposes (first hand, or best if split)
var mainHandResult: HandResult {
// Return the best result for the headline
if wasBlackjack { return .blackjack }
if handResults.contains(.win) { return .win }
if handResults.contains(.push) { return .push }
if handResults.contains(.surrender) { return .surrender }
if handResults.allSatisfy({ $0 == .bust }) { return .bust }
return handResults.first ?? .lose
}
/// Whether this round had split hands
var hadSplit: Bool {
handResults.count > 1
}
/// Legacy accessor for backwards compatibility
var splitHandResult: HandResult? {
handResults.count > 1 ? handResults[1] : nil
}
}

View File

@ -1,273 +0,0 @@
//
// GameSettings.swift
// Blackjack
//
// User-configurable game settings including Blackjack rule variations.
//
import Foundation
import SwiftUI
import CasinoKit
/// Blackjack rule variation presets.
enum BlackjackStyle: String, CaseIterable, Identifiable {
case vegas = "vegas" // Vegas Strip rules
case atlantic = "atlantic" // Atlantic City rules
case european = "european" // European no-hole-card
case custom = "custom"
var id: String { rawValue }
var displayName: String {
switch self {
case .vegas: return String(localized: "Vegas Strip")
case .atlantic: return String(localized: "Atlantic City")
case .european: return String(localized: "European")
case .custom: return String(localized: "Custom")
}
}
var description: String {
switch self {
case .vegas: return String(localized: "Dealer stands on soft 17, double after split, 3:2 blackjack")
case .atlantic: return String(localized: "Dealer stands on soft 17, late surrender, 8 decks")
case .european: return String(localized: "No hole card, dealer stands on soft 17, no surrender")
case .custom: return String(localized: "Customize all rules")
}
}
}
/// Number of decks in the shoe.
enum DeckCount: Int, CaseIterable, Identifiable {
case one = 1
case two = 2
case four = 4
case six = 6
case eight = 8
var id: Int { rawValue }
var displayName: String {
switch self {
case .one: return "1 Deck"
case .two: return "2 Decks"
case .four: return "4 Decks"
case .six: return "6 Decks"
case .eight: return "8 Decks"
}
}
var description: String {
switch self {
case .one: return String(localized: "Single deck, higher variance")
case .two: return String(localized: "Lower house edge")
case .four: return String(localized: "Common shoe game")
case .six: return String(localized: "Standard casino")
case .eight: return String(localized: "Maximum penetration")
}
}
}
/// Observable settings class for Blackjack configuration.
@Observable
@MainActor
final class GameSettings {
// MARK: - Game Style
/// The preset rule variation.
var gameStyle: BlackjackStyle = .vegas {
didSet {
applyStylePreset()
save()
}
}
// MARK: - Rule Options
/// Number of decks in the shoe.
var deckCount: DeckCount = .six { didSet { save() } }
/// Whether dealer hits on soft 17.
var dealerHitsSoft17: Bool = false { didSet { save() } }
/// Whether player can double after split.
var doubleAfterSplit: Bool = true { didSet { save() } }
/// Whether player can re-split aces.
var resplitAces: Bool = false { didSet { save() } }
/// Whether late surrender is allowed.
var lateSurrender: Bool = true { didSet { save() } }
/// Whether European no-hole-card rule is used (dealer gets second card after player acts).
var noHoleCard: Bool = false { didSet { save() } }
/// Whether insurance is offered.
var insuranceAllowed: Bool = true { didSet { save() } }
/// Blackjack payout ratio (1.5 = 3:2, 1.2 = 6:5)
var blackjackPayout: Double = 1.5 { didSet { save() } }
// MARK: - Betting Limits
/// The table limits preset.
var tableLimits: TableLimits = .low { didSet { save() } }
/// Minimum bet amount.
var minBet: Int { tableLimits.minBet }
/// Maximum bet amount.
var maxBet: Int { tableLimits.maxBet }
// MARK: - Starting Balance
/// The starting balance for new games.
var startingBalance: Int = 10_000 { didSet { save() } }
// MARK: - Animation Settings
/// Whether to show dealing animations.
var showAnimations: Bool = true { didSet { save() } }
/// Speed of card dealing (1.0 = normal)
var dealingSpeed: Double = 1.0 { didSet { save() } }
// MARK: - Display Settings
/// Whether to show the cards remaining indicator.
var showCardsRemaining: Bool = true { didSet { save() } }
/// Whether to show hand history.
var showHistory: Bool = true { didSet { save() } }
/// Whether to show dealer hints (suggested action).
var showHints: Bool = true { didSet { save() } }
/// Whether to show the running card count (Hi-Lo system).
var showCardCount: Bool = false { didSet { save() } }
// MARK: - Sound Settings
/// Whether sound effects are enabled.
var soundEnabled: Bool = true { didSet { save() } }
/// Whether haptic feedback is enabled.
var hapticsEnabled: Bool = true { didSet { save() } }
/// Volume level for sound effects.
var soundVolume: Float = 1.0 { didSet { save() } }
// MARK: - Initialization
init() {
self.persistence = CloudSyncManager<BlackjackSettingsData>()
load()
applyStylePreset()
}
// MARK: - Style Presets
private func applyStylePreset() {
guard gameStyle != .custom else { return }
switch gameStyle {
case .vegas:
deckCount = .six
dealerHitsSoft17 = false
doubleAfterSplit = true
resplitAces = false
lateSurrender = false
noHoleCard = false // American: dealer gets hole card upfront
blackjackPayout = 1.5
case .atlantic:
deckCount = .eight
dealerHitsSoft17 = false
doubleAfterSplit = true
resplitAces = true
lateSurrender = true
noHoleCard = false // American: dealer gets hole card upfront
blackjackPayout = 1.5
case .european:
deckCount = .six
dealerHitsSoft17 = false
doubleAfterSplit = true
resplitAces = false
lateSurrender = false
noHoleCard = true // European: dealer gets second card after player acts
blackjackPayout = 1.5
case .custom:
break
}
}
// MARK: - Persistence
private let persistence: CloudSyncManager<BlackjackSettingsData>
var iCloudAvailable: Bool {
FileManager.default.ubiquityIdentityToken != nil
}
func load() {
let data = persistence.load()
if let style = BlackjackStyle(rawValue: data.gameStyle) {
self.gameStyle = style
}
if let count = DeckCount(rawValue: data.deckCount) {
self.deckCount = count
}
if let limits = TableLimits(rawValue: data.tableLimits) {
self.tableLimits = limits
}
self.startingBalance = data.startingBalance
self.dealerHitsSoft17 = data.dealerHitsSoft17
self.doubleAfterSplit = data.doubleAfterSplit
self.resplitAces = data.resplitAces
self.lateSurrender = data.lateSurrender
self.noHoleCard = data.noHoleCard
self.blackjackPayout = data.blackjackPayout
self.insuranceAllowed = data.insuranceAllowed
self.showAnimations = data.showAnimations
self.dealingSpeed = data.dealingSpeed
self.showCardsRemaining = data.showCardsRemaining
self.showHistory = data.showHistory
self.showHints = data.showHints
self.showCardCount = data.showCardCount
self.soundEnabled = data.soundEnabled
self.hapticsEnabled = data.hapticsEnabled
self.soundVolume = data.soundVolume
}
func save() {
let data = BlackjackSettingsData(
lastModified: Date(),
gameStyle: gameStyle.rawValue,
deckCount: deckCount.rawValue,
tableLimits: tableLimits.rawValue,
startingBalance: startingBalance,
dealerHitsSoft17: dealerHitsSoft17,
doubleAfterSplit: doubleAfterSplit,
resplitAces: resplitAces,
lateSurrender: lateSurrender,
noHoleCard: noHoleCard,
blackjackPayout: blackjackPayout,
insuranceAllowed: insuranceAllowed,
showAnimations: showAnimations,
dealingSpeed: dealingSpeed,
showCardsRemaining: showCardsRemaining,
showHistory: showHistory,
showHints: showHints,
showCardCount: showCardCount,
soundEnabled: soundEnabled,
hapticsEnabled: hapticsEnabled,
soundVolume: soundVolume
)
persistence.save(data)
}
}

View File

@ -1,160 +0,0 @@
//
// Hand.swift
// Blackjack
//
// Represents a Blackjack hand with value calculation.
//
import Foundation
import CasinoKit
/// A hand of cards in Blackjack.
struct BlackjackHand: Identifiable, Equatable {
let id = UUID()
var cards: [Card]
var bet: Int
var isDoubledDown: Bool = false
var isSplit: Bool = false
var isStanding: Bool = false
var result: HandResult?
init(cards: [Card] = [], bet: Int = 0) {
self.cards = cards
self.bet = bet
}
/// The best possible value (highest without busting, or lowest if busted).
var value: Int {
let (hard, soft) = calculateValues()
if soft <= 21 {
return soft
}
return hard
}
/// Whether this hand has a soft value (usable ace).
var isSoft: Bool {
let (hard, soft) = calculateValues()
return soft <= 21 && soft != hard
}
/// Whether the hand is over 21.
var isBusted: Bool {
value > 21
}
/// Whether this is a natural blackjack (two cards totaling 21).
var isBlackjack: Bool {
cards.count == 2 && value == 21 && !isSplit
}
/// Whether this hand has a splittable pair (two cards of same rank).
/// Note: Additional conditions (balance, max splits, resplit aces) are checked by the engine.
var canSplit: Bool {
cards.count == 2 && cards[0].rank == cards[1].rank
}
/// Whether this hand has the card count to double down.
/// Note: Additional conditions (balance, DAS rule) are checked by the engine.
var canDoubleDown: Bool {
cards.count == 2 && !isDoubledDown
}
/// Whether this hand can hit.
/// Note: Standard Blackjack has NO card limit - you can hit until you bust or stand.
var canHit: Bool {
!isBusted && !isStanding && !isBlackjack
}
/// Calculates both hard and soft values.
private func calculateValues() -> (hard: Int, soft: Int) {
var hardValue = 0
var aceCount = 0
for card in cards {
switch card.rank {
case .ace:
hardValue += 1
aceCount += 1
case .two: hardValue += 2
case .three: hardValue += 3
case .four: hardValue += 4
case .five: hardValue += 5
case .six: hardValue += 6
case .seven: hardValue += 7
case .eight: hardValue += 8
case .nine: hardValue += 9
case .ten, .jack, .queen, .king:
hardValue += 10
}
}
// Calculate soft value (one ace as 11)
var softValue = hardValue
if aceCount > 0 && hardValue + 10 <= 21 {
softValue = hardValue + 10
}
return (hardValue, softValue)
}
/// Display string for the hand value.
var valueDisplay: String {
if isBlackjack {
return "BJ"
}
let (hard, soft) = calculateValues()
if isBusted {
return "\(hard) 💥"
}
if isSoft && soft != hard {
return "\(hard)/\(soft)"
}
return "\(value)"
}
}
// MARK: - Card Value Extension
extension Card {
/// The blackjack value of this card (Ace = 1 or 11, face cards = 10).
var blackjackValue: Int {
switch rank {
case .ace: return 1 // Or 11, handled by hand calculation
case .two: return 2
case .three: return 3
case .four: return 4
case .five: return 5
case .six: return 6
case .seven: return 7
case .eight: return 8
case .nine: return 9
case .ten, .jack, .queen, .king: return 10
}
}
/// The Hi-Lo card counting value.
/// Low cards (2-6): +1 (good for player when removed)
/// Neutral (7-9): 0
/// High cards (10-A): -1 (bad for player when removed)
var hiLoValue: Int {
switch rank {
case .two, .three, .four, .five, .six:
return 1 // Low cards
case .seven, .eight, .nine:
return 0 // Neutral
case .ten, .jack, .queen, .king, .ace:
return -1 // High cards
}
}
/// Display text for the Hi-Lo count value.
var hiLoDisplayText: String {
switch hiLoValue {
case 1: return "+1"
case -1: return "-1"
default: return "0"
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,102 +0,0 @@
//
// BlackjackGameData.swift
// Blackjack
//
// Persistent game data model for iCloud sync.
//
import Foundation
import CasinoKit
/// Saved round result for history.
struct SavedRoundResult: Codable, Equatable {
let date: Date
let mainResult: String // "blackjack", "win", "lose", "push", "bust", "surrender"
let hadSplit: Bool
let totalWinnings: Int
}
/// Persistent game data that syncs to iCloud.
struct BlackjackGameData: PersistableGameData {
static let gameIdentifier = "blackjack"
var roundsPlayed: Int { roundHistory.count }
var lastModified: Date
static var empty: BlackjackGameData {
BlackjackGameData(
lastModified: Date(),
balance: 10_000,
roundHistory: [],
totalWinnings: 0,
biggestWin: 0,
biggestLoss: 0,
blackjackCount: 0,
bustCount: 0
)
}
var balance: Int
var roundHistory: [SavedRoundResult]
var totalWinnings: Int
var biggestWin: Int
var biggestLoss: Int
var blackjackCount: Int
var bustCount: Int
}
/// Persistent settings data that syncs to iCloud.
struct BlackjackSettingsData: PersistableGameData {
static let gameIdentifier = "blackjack_settings"
var lastModified: Date
var roundsPlayed: Int = 0 // Settings don't track rounds, use 0
static var empty: BlackjackSettingsData {
BlackjackSettingsData(
lastModified: Date(),
roundsPlayed: 0,
gameStyle: "vegas",
deckCount: 6,
tableLimits: "low",
startingBalance: 10_000,
dealerHitsSoft17: false,
doubleAfterSplit: true,
resplitAces: false,
lateSurrender: true,
noHoleCard: false,
blackjackPayout: 1.5,
insuranceAllowed: true,
showAnimations: true,
dealingSpeed: 1.0,
showCardsRemaining: true,
showHistory: true,
showHints: true,
showCardCount: false,
soundEnabled: true,
hapticsEnabled: true,
soundVolume: 1.0
)
}
var gameStyle: String
var deckCount: Int
var tableLimits: String
var startingBalance: Int
var dealerHitsSoft17: Bool
var doubleAfterSplit: Bool
var resplitAces: Bool
var lateSurrender: Bool
var noHoleCard: Bool
var blackjackPayout: Double
var insuranceAllowed: Bool
var showAnimations: Bool
var dealingSpeed: Double
var showCardsRemaining: Bool
var showHistory: Bool
var showHints: Bool
var showCardCount: Bool
var soundEnabled: Bool
var hapticsEnabled: Bool
var soundVolume: Float
}

View File

@ -1,170 +0,0 @@
//
// DesignConstants.swift
// Blackjack
//
// Centralized design constants for the Blackjack app.
// Uses CasinoDesign from CasinoKit for shared values, with game-specific overrides.
//
import SwiftUI
import CasinoKit
// MARK: - Design Namespace
/// Design constants for the Blackjack app.
/// Shared constants are imported from CasinoDesign; game-specific values are defined here.
enum Design {
// MARK: - Debug
/// Set to true to show layout debug borders on views
static let showDebugBorders = false
// MARK: - Shared Constants (from CasinoKit)
typealias Spacing = CasinoDesign.Spacing
typealias CornerRadius = CasinoDesign.CornerRadius
typealias LineWidth = CasinoDesign.LineWidth
typealias Shadow = CasinoDesign.Shadow
typealias Opacity = CasinoDesign.Opacity
typealias Animation = CasinoDesign.Animation
typealias Scale = CasinoDesign.Scale
typealias MinScaleFactor = CasinoDesign.MinScaleFactor
typealias BaseFontSize = CasinoDesign.BaseFontSize
typealias IconSize = CasinoDesign.IconSize
// MARK: - Blackjack-Specific Component Sizes
enum Size {
// Hand scaling factor (1.5 = 50% larger hands)
static let handScale: CGFloat = 1.5
// Cards - scaled for better visibility
static let cardWidth: CGFloat = 60 * handScale // 90pt at 1.5x
static let cardWidthSmall: CGFloat = CasinoDesign.Size.cardWidthSmall
static let cardOverlap: CGFloat = CasinoDesign.Size.cardOverlap * handScale // Scaled overlap
// Player hands container height (accommodates larger cards + labels)
// Reduced from 180 to fit content more snugly
static let playerHandsHeight: CGFloat = 160 * handScale // 240pt at 1.5x
// Hand label font sizes (scaled)
static let handLabelFontSize: CGFloat = CasinoDesign.BaseFontSize.medium * handScale
static let handNumberFontSize: CGFloat = CasinoDesign.BaseFontSize.medium * handScale // Same as label
static let handValueFontSize: CGFloat = CasinoDesign.BaseFontSize.xLarge * handScale
// Hint font size (scaled to match hands)
static let hintFontSize: CGFloat = CasinoDesign.BaseFontSize.small * handScale
static let hintIconSize: CGFloat = CasinoDesign.IconSize.medium * handScale
static let hintPaddingH: CGFloat = CasinoDesign.Spacing.medium * handScale
static let hintPaddingV: CGFloat = CasinoDesign.Spacing.small * handScale
// Hand icons (scaled)
static let handIconSize: CGFloat = CasinoDesign.IconSize.medium * handScale
// Hi-Lo count badge (scaled)
static let countBadgeFontSize: CGFloat = CasinoDesign.BaseFontSize.xxSmall * handScale
static let countBadgePaddingH: CGFloat = CasinoDesign.Spacing.xSmall * handScale
static let countBadgePaddingV: CGFloat = CasinoDesign.Spacing.xxxSmall * handScale
static let countBadgeOffset: CGFloat = CasinoDesign.Spacing.xSmall * handScale
// Betting zone (chip scales, but zone height stays reasonable)
static let bettingChipSize: CGFloat = 36 * handScale // 54pt at 1.5x
static let bettingZoneHeightScaled: CGFloat = CasinoDesign.Size.bettingZoneHeight // Keep original height to save space
// Card count display (scaled)
static let cardCountLabelSize: CGFloat = CasinoDesign.BaseFontSize.xSmall * handScale
static let cardCountValueSize: CGFloat = CasinoDesign.BaseFontSize.large * handScale
// Chips - use CasinoDesign values
static let chipBadgeSize: CGFloat = CasinoDesign.Size.chipBadge
// Buttons - use CasinoDesign values
static let actionButtonHeight: CGFloat = CasinoDesign.Size.actionButtonHeight
static let actionButtonMinWidth: CGFloat = CasinoDesign.Size.actionButtonMinWidth
static let bettingZoneHeight: CGFloat = CasinoDesign.Size.bettingZoneHeight
// Responsive - use CasinoDesign values
static let maxContentWidthPortrait: CGFloat = CasinoDesign.Size.maxContentWidthPortrait
static let maxContentWidthLandscape: CGFloat = CasinoDesign.Size.maxContentWidthLandscape
static let maxModalWidth: CGFloat = CasinoDesign.Size.maxModalWidth
// Blackjack-specific
static let tableHeight: CGFloat = 280
// Settings - use CasinoDesign values
static let checkmark: CGFloat = CasinoDesign.Size.checkmark
}
}
// MARK: - Blackjack App Colors
extension Color {
// MARK: - Table Colors (use CasinoTable from CasinoKit for consistency)
/// Typealias for consistent table colors across all games.
typealias Table = CasinoTable
// MARK: - Betting Zone Colors
enum BettingZone {
static let main = Color(red: 0.2, green: 0.4, blue: 0.3)
static let mainBorder = Color(red: 0.4, green: 0.6, blue: 0.4)
static let insurance = Color(red: 0.5, green: 0.4, blue: 0.2)
static let insuranceBorder = Color(red: 0.7, green: 0.6, blue: 0.3)
}
// MARK: - Hand Colors
enum Hand {
static let player = Color(red: 0.2, green: 0.5, blue: 0.8)
static let dealer = Color(red: 0.8, green: 0.3, blue: 0.3)
static let active = Color.yellow
static let inactive = Color.white.opacity(CasinoDesign.Opacity.medium)
}
// MARK: - Result Colors
enum Result {
static let win = Color.green
static let lose = Color.red
static let push = Color.blue
static let blackjack = Color.yellow
}
// MARK: - Action Button Colors
enum Button {
static let hit = Color(red: 0.2, green: 0.6, blue: 0.3)
static let stand = Color(red: 0.6, green: 0.4, blue: 0.1)
static let doubleDown = Color(red: 0.5, green: 0.3, blue: 0.6)
static let split = Color(red: 0.3, green: 0.5, blue: 0.7)
static let surrender = Color(red: 0.6, green: 0.3, blue: 0.3)
static let insurance = Color(red: 0.7, green: 0.6, blue: 0.2)
static let goldLight = Color(red: 1.0, green: 0.85, blue: 0.3)
static let goldDark = Color(red: 0.9, green: 0.7, blue: 0.2)
}
// MARK: - Settings Colors
enum Settings {
static let background = Color(red: 0.08, green: 0.12, blue: 0.18)
static let cardBackground = Color.white.opacity(CasinoDesign.Opacity.verySubtle)
static let accent = Color(red: 0.9, green: 0.75, blue: 0.3)
}
// MARK: - Modal Colors
enum Modal {
static let background = Color(red: 0.12, green: 0.18, blue: 0.25)
static let backgroundLight = Color(red: 0.15, green: 0.2, blue: 0.3)
static let backgroundDark = Color(red: 0.1, green: 0.15, blue: 0.25)
}
// MARK: - TopBar Colors
enum TopBar {
static let balance = Color(red: 0.95, green: 0.85, blue: 0.4)
}
}

View File

@ -1,130 +0,0 @@
//
// BrandingPreviewView.swift
// Blackjack
//
// Development view for previewing and exporting app icons and launch screens.
// Access this during development to generate icon assets.
//
import SwiftUI
import CasinoKit
/// Preview view for app branding assets.
/// Use this during development to preview and export icons.
struct BrandingPreviewView: View {
var body: some View {
TabView {
// App Icon Preview
ScrollView {
VStack(spacing: Design.Spacing.xxxLarge) {
Text("App Icon")
.font(.largeTitle.bold())
AppIconView(config: .blackjack, size: 300)
.clipShape(.rect(cornerRadius: 300 * 0.22))
.shadow(radius: Design.Shadow.radiusXLarge)
Text("All Sizes")
.font(.title2.bold())
LazyVGrid(columns: [GridItem(.adaptive(minimum: 100))], spacing: Design.Spacing.xLarge) {
ForEach([180, 120, 87, 60, 40], id: \.self) { size in
VStack {
AppIconView(config: .blackjack, size: CGFloat(size))
.clipShape(.rect(cornerRadius: CGFloat(size) * 0.22))
Text("\(size)px")
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
instructionsSection
}
.padding()
}
.tabItem {
Label("Icon", systemImage: "app.fill")
}
// Launch Screen Preview
LaunchScreenView(config: .blackjack)
.tabItem {
Label("Launch", systemImage: "rectangle.portrait.fill")
}
// Other Games Preview
ScrollView {
VStack(spacing: Design.Spacing.xxxLarge) {
Text("Other Game Icons")
.font(.largeTitle.bold())
HStack(spacing: Design.Spacing.xLarge) {
VStack {
AppIconView(config: .baccarat, size: 150)
.clipShape(.rect(cornerRadius: 150 * 0.22))
Text("Baccarat")
.font(.caption)
}
VStack {
AppIconView(config: .poker, size: 150)
.clipShape(.rect(cornerRadius: 150 * 0.22))
Text("Poker")
.font(.caption)
}
VStack {
AppIconView(config: .roulette, size: 150)
.clipShape(.rect(cornerRadius: 150 * 0.22))
Text("Roulette")
.font(.caption)
}
}
Text("These show how the same pattern works for other games")
.font(.callout)
.foregroundStyle(.secondary)
}
.padding()
}
.tabItem {
Label("Others", systemImage: "square.grid.2x2")
}
}
}
private var instructionsSection: some View {
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
Text("How to Export Icons")
.font(.headline)
VStack(alignment: .leading, spacing: Design.Spacing.small) {
Text("Option 1: Screenshot from Preview")
.font(.subheadline.bold())
Text("• Run the preview in Xcode")
Text("• Screenshot the 1024px icon")
Text("• Use an online tool to generate all sizes")
}
VStack(alignment: .leading, spacing: Design.Spacing.small) {
Text("Option 2: Use IconRenderer in Code")
.font(.subheadline.bold())
Text("• Call IconRenderer.renderAppIcon(config: .blackjack)")
Text("• Save the resulting UIImage to files")
Text("• Add to Assets.xcassets/AppIcon")
}
}
.font(.callout)
.foregroundStyle(.secondary)
.frame(maxWidth: .infinity, alignment: .leading)
.padding()
.background(Color.gray.opacity(Design.Opacity.subtle))
.clipShape(.rect(cornerRadius: Design.CornerRadius.medium))
}
}
#Preview {
BrandingPreviewView()
}

View File

@ -1,196 +0,0 @@
//
// IconGeneratorView.swift
// Blackjack
//
// Development tool to generate and export app icon images.
// Run this view, tap the button, then find the icons in the Files app.
//
import SwiftUI
import CasinoKit
/// A development view that generates and saves app icon images.
/// After running, find the icons in Files app On My iPhone Blackjack
struct IconGeneratorView: View {
@State private var status: String = "Tap the button to generate icons"
@State private var isGenerating = false
@State private var generatedIcons: [GeneratedIcon] = []
// Development view: hardcoded sizes acceptable
private let previewSize: CGFloat = 200
private let iconCornerRadiusRatio: CGFloat = 0.22
var body: some View {
NavigationStack {
ScrollView {
VStack(spacing: Design.Spacing.xxLarge) {
// Preview
AppIconView(config: .blackjack, size: previewSize)
.clipShape(.rect(cornerRadius: previewSize * iconCornerRadiusRatio))
.shadow(radius: 10)
Text("App Icon Preview")
.font(.headline)
// Generate button
Button {
Task {
await generateIcons()
}
} label: {
HStack {
if isGenerating {
ProgressView()
.tint(.white)
}
Text(isGenerating ? "Generating..." : "Generate & Save Icons")
}
.font(.headline)
.foregroundStyle(.white)
.frame(maxWidth: .infinity)
.padding()
.background(isGenerating ? Color.gray : Color.blue)
.clipShape(.rect(cornerRadius: 12))
}
.disabled(isGenerating)
.padding(.horizontal)
// Status
Text(status)
.font(.callout)
.foregroundStyle(.secondary)
.multilineTextAlignment(.center)
.padding(.horizontal)
// Generated icons
if !generatedIcons.isEmpty {
VStack(alignment: .leading, spacing: 12) {
Text("Generated Icons:")
.font(.headline)
ForEach(generatedIcons) { icon in
HStack {
Image(systemName: "checkmark.circle.fill")
.foregroundStyle(.green)
Text(icon.filename)
.font(.caption.monospaced())
Spacer()
Text("\(Int(icon.size))px")
.font(.caption)
.foregroundStyle(.secondary)
}
}
}
.padding()
.background(Color.green.opacity(Design.Opacity.subtle))
.clipShape(.rect(cornerRadius: Design.CornerRadius.medium))
.padding(.horizontal)
}
// Instructions
instructionsSection
}
.padding(.vertical)
}
.navigationTitle("Icon Generator")
}
}
private var instructionsSection: some View {
VStack(alignment: .leading, spacing: 12) {
Text("After generating:")
.font(.headline)
VStack(alignment: .leading, spacing: Design.Spacing.small) {
instructionRow(number: 1, text: "Open Files app on your device/simulator")
instructionRow(number: 2, text: "Navigate to: On My iPhone → Blackjack")
instructionRow(number: 3, text: "Find the AppIcon-1024.png file")
instructionRow(number: 4, text: "AirDrop or share to your Mac")
instructionRow(number: 5, text: "Drag into Xcode's Assets.xcassets/AppIcon")
}
Divider()
Text("Alternative: Use an online tool")
.font(.subheadline.bold())
Text("Upload the 1024px icon to appicon.co or makeappicon.com to generate all sizes automatically.")
.font(.caption)
.foregroundStyle(.secondary)
}
.padding()
.background(Color.gray.opacity(Design.Opacity.subtle))
.clipShape(.rect(cornerRadius: Design.CornerRadius.medium))
.padding(.horizontal)
}
private func instructionRow(number: Int, text: String) -> some View {
HStack(alignment: .top, spacing: 8) {
Text("\(number).")
.font(.callout.bold())
.foregroundStyle(.blue)
Text(text)
.font(.callout)
}
}
@MainActor
private func generateIcons() async {
isGenerating = true
generatedIcons = []
status = "Generating icons..."
let sizes: [(CGFloat, String)] = [
(1024, "AppIcon-1024"),
(180, "AppIcon-180"),
(120, "AppIcon-120"),
(87, "AppIcon-87"),
(80, "AppIcon-80"),
(60, "AppIcon-60"),
(40, "AppIcon-40")
]
let documentsPath = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask)[0]
for (size, name) in sizes {
// Render the icon
let view = AppIconView(config: .blackjack, size: size)
let renderer = ImageRenderer(content: view)
renderer.scale = 1.0
if let uiImage = renderer.uiImage,
let data = uiImage.pngData() {
let filename = "\(name).png"
let fileURL = documentsPath.appending(path: filename)
do {
try data.write(to: fileURL)
generatedIcons.append(GeneratedIcon(filename: filename, size: size))
} catch {
status = "Error saving \(filename): \(error.localizedDescription)"
}
}
// Small delay for UI feedback
try? await Task.sleep(for: .milliseconds(100))
}
if generatedIcons.count == sizes.count {
status = "✅ All icons saved to Documents folder!\nOpen Files app to find them."
} else {
status = "⚠️ Some icons failed to generate"
}
isGenerating = false
}
}
struct GeneratedIcon: Identifiable {
let id = UUID()
let filename: String
let size: CGFloat
}
#Preview {
IconGeneratorView()
}

View File

@ -1,108 +0,0 @@
//
// ActionButton.swift
// Blackjack
//
// Reusable styled button for game actions.
//
import SwiftUI
import CasinoKit
struct ActionButton: View {
let title: String
let icon: String?
let style: ButtonStyle
let action: () -> Void
enum ButtonStyle {
case primary // Gold gradient (Deal, New Round)
case destructive // Red (Clear)
case secondary // Subtle white
case custom(Color) // Game-specific colors (Hit, Stand, etc.)
var foregroundColor: Color {
switch self {
case .primary: return .black
case .destructive, .secondary, .custom: return .white
}
}
}
init(_ title: String, icon: String? = nil, style: ButtonStyle = .primary, action: @escaping () -> Void) {
self.title = title
self.icon = icon
self.style = style
self.action = action
}
var body: some View {
Button(action: action) {
HStack(spacing: Design.Spacing.small) {
if let icon = icon {
Image(systemName: icon)
}
Text(title)
.lineLimit(1)
.minimumScaleFactor(CasinoDesign.MinScaleFactor.relaxed)
}
.font(.system(size: Design.BaseFontSize.large, weight: .semibold))
.foregroundStyle(style.foregroundColor)
.padding(.horizontal, Design.Spacing.large)
.padding(.vertical, Design.Spacing.medium)
.background(backgroundView)
}
.accessibilityLabel(title)
}
@ViewBuilder
private var backgroundView: some View {
switch style {
case .primary:
Capsule()
.fill(
LinearGradient(
colors: [Color.Button.goldLight, Color.Button.goldDark],
startPoint: .top,
endPoint: .bottom
)
)
case .destructive:
Capsule()
.fill(Color.red.opacity(Design.Opacity.heavy))
case .secondary:
Capsule()
.fill(Color.white.opacity(Design.Opacity.hint))
case .custom(let color):
Capsule()
.fill(color)
}
}
}
// MARK: - Previews
#Preview("Primary") {
ZStack {
Color.Table.felt.ignoresSafeArea()
ActionButton("Deal", icon: "play.fill", style: .primary) {}
}
}
#Preview("Destructive") {
ZStack {
Color.Table.felt.ignoresSafeArea()
ActionButton("Clear", icon: "xmark.circle", style: .destructive) {}
}
}
#Preview("Custom Colors") {
ZStack {
Color.Table.felt.ignoresSafeArea()
HStack(spacing: Design.Spacing.medium) {
ActionButton("Hit", style: .custom(Color.Button.hit)) {}
ActionButton("Stand", style: .custom(Color.Button.stand)) {}
ActionButton("Double", style: .custom(Color.Button.doubleDown)) {}
}
}
}

View File

@ -1,300 +0,0 @@
//
// ActionButtonsView.swift
// Blackjack
//
// Container for game action buttons (betting, player turn).
//
import SwiftUI
import CasinoKit
struct ActionButtonsView: View {
@Bindable var state: GameState
// Scaled metrics
@ScaledMetric(relativeTo: .headline) private var buttonFontSize: CGFloat = Design.BaseFontSize.large
@ScaledMetric(relativeTo: .body) private var iconSize: CGFloat = Design.IconSize.large
// Scaled container height - base 60pt, scales with accessibility
@ScaledMetric(relativeTo: .body) private var containerHeight: CGFloat = 60
var body: some View {
VStack(spacing: Design.Spacing.small) {
// Primary actions
HStack(spacing: Design.Spacing.medium) {
switch state.currentPhase {
case .betting:
bettingButtons
case .playerTurn:
playerTurnButtons
case .roundComplete:
// Empty - handled by result banner
EmptyView()
default:
// Dealing, dealer turn - show nothing
EmptyView()
}
}
.animation(.spring(duration: Design.Animation.quick), value: state.currentPhase)
}
.frame(minHeight: containerHeight)
.padding(.horizontal, Design.Spacing.large)
}
// MARK: - Betting Phase Buttons
@ViewBuilder
private var bettingButtons: some View {
if state.currentBet > 0 {
VStack(spacing: Design.Spacing.small) {
// Show hint if bet is below minimum
if state.isBetBelowMinimum {
Text(String(localized: "Add $\(state.amountNeededForMinimum) more to meet minimum"))
.font(.system(size: Design.BaseFontSize.small, weight: .medium))
.foregroundStyle(.orange)
.transition(.opacity)
}
HStack(spacing: Design.Spacing.medium) {
ActionButton(
String(localized: "Clear"),
icon: "xmark.circle",
style: .destructive
) {
state.clearBet()
}
// Always show Deal button, but disable if below minimum
ActionButton(
String(localized: "Deal"),
icon: "play.fill",
style: .primary
) {
Task { await state.deal() }
}
.opacity(state.canDeal ? 1.0 : Design.Opacity.medium)
.disabled(!state.canDeal)
}
}
}
}
// MARK: - Player Turn Buttons
@ViewBuilder
private var playerTurnButtons: some View {
// All player actions in a single row
HStack(spacing: Design.Spacing.medium) {
if state.canHit {
ActionButton(
String(localized: "Hit"),
style: .custom(Color.Button.hit)
) {
Task { await state.hit() }
}
}
if state.canStand {
ActionButton(
String(localized: "Stand"),
style: .custom(Color.Button.stand)
) {
Task { await state.stand() }
}
}
if state.canDouble {
ActionButton(
String(localized: "Double"),
style: .custom(Color.Button.doubleDown)
) {
Task { await state.doubleDown() }
}
}
if state.canSplit {
ActionButton(
String(localized: "Split"),
style: .custom(Color.Button.split)
) {
Task { await state.split() }
}
}
if state.canSurrender {
ActionButton(
String(localized: "Surrender"),
style: .custom(Color.Button.surrender)
) {
Task { await state.surrender() }
}
}
}
}
}
// MARK: - Previews
#Preview("Betting Phase - No Bet") {
ZStack {
Color.Table.felt.ignoresSafeArea()
ActionButtonsView(state: {
let state = GameState(settings: GameSettings())
return state
}())
}
}
#Preview("Betting Phase - With Bet") {
ZStack {
Color.Table.felt.ignoresSafeArea()
ActionButtonsView(state: {
let state = GameState(settings: GameSettings())
state.placeBet(amount: 100)
return state
}())
}
}
#Preview("Betting Phase - Below Minimum") {
ZStack {
Color.Table.felt.ignoresSafeArea()
ActionButtonsView(state: {
let state = GameState(settings: GameSettings())
state.placeBet(amount: 25)
return state
}())
}
}
// MARK: - Player Turn Button Previews
/// Preview helper that shows player turn button layouts without needing real game state.
private struct PlayerTurnButtonsPreview: View {
let showHit: Bool
let showStand: Bool
let showDouble: Bool
let showSplit: Bool
let showSurrender: Bool
private let containerHeight: CGFloat = 120
var body: some View {
ZStack {
Color.clear
.frame(height: containerHeight)
// Single row of buttons matching actual game layout
HStack(spacing: Design.Spacing.medium) {
if showHit {
ActionButton("Hit", style: .custom(Color.Button.hit)) {}
}
if showStand {
ActionButton("Stand", style: .custom(Color.Button.stand)) {}
}
if showDouble {
ActionButton("Double", style: .custom(Color.Button.doubleDown)) {}
}
if showSplit {
ActionButton("Split", style: .custom(Color.Button.split)) {}
}
if showSurrender {
ActionButton("Surrender", style: .custom(Color.Button.surrender)) {}
}
}
}
.padding(.horizontal, Design.Spacing.large)
}
}
#Preview("Player Turn - Hit & Stand Only") {
ZStack {
Color.Table.felt.ignoresSafeArea()
PlayerTurnButtonsPreview(
showHit: true,
showStand: true,
showDouble: false,
showSplit: false,
showSurrender: false
)
}
}
#Preview("Player Turn - With Double") {
ZStack {
Color.Table.felt.ignoresSafeArea()
PlayerTurnButtonsPreview(
showHit: true,
showStand: true,
showDouble: true,
showSplit: false,
showSurrender: false
)
}
}
#Preview("Player Turn - With Split") {
ZStack {
Color.Table.felt.ignoresSafeArea()
PlayerTurnButtonsPreview(
showHit: true,
showStand: true,
showDouble: true,
showSplit: true,
showSurrender: false
)
}
}
#Preview("Player Turn - With Surrender") {
ZStack {
Color.Table.felt.ignoresSafeArea()
PlayerTurnButtonsPreview(
showHit: true,
showStand: true,
showDouble: true,
showSplit: false,
showSurrender: true
)
}
}
#Preview("Player Turn - All Options") {
ZStack {
Color.Table.felt.ignoresSafeArea()
PlayerTurnButtonsPreview(
showHit: true,
showStand: true,
showDouble: true,
showSplit: true,
showSurrender: true
)
}
}
#Preview("Player Turn - After Hit (No Double/Split)") {
ZStack {
Color.Table.felt.ignoresSafeArea()
PlayerTurnButtonsPreview(
showHit: true,
showStand: true,
showDouble: false,
showSplit: false,
showSurrender: false
)
}
}
#Preview("Player Turn - Split Hand (No Resplit)") {
ZStack {
Color.Table.felt.ignoresSafeArea()
PlayerTurnButtonsPreview(
showHit: true,
showStand: true,
showDouble: true,
showSplit: false,
showSurrender: false
)
}
}

View File

@ -1,88 +0,0 @@
//
// CardCountView.swift
// Blackjack
//
// Displays the Hi-Lo running and true count for card counting practice.
//
import SwiftUI
import CasinoKit
/// Displays the Hi-Lo running count for card counting practice.
struct CardCountView: View {
let runningCount: Int
let trueCount: Double
var body: some View {
HStack(spacing: Design.Spacing.large) {
// Running count
VStack(spacing: Design.Spacing.xxSmall) {
Text("Running")
.font(.system(size: Design.Size.cardCountLabelSize, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
Text(runningCount >= 0 ? "+\(runningCount)" : "\(runningCount)")
.font(.system(size: Design.Size.cardCountValueSize, weight: .bold, design: .monospaced))
.foregroundStyle(countColor(for: runningCount))
}
Divider()
.frame(height: Design.Spacing.xLarge)
.background(Color.white.opacity(Design.Opacity.hint))
// True count
VStack(spacing: Design.Spacing.xxSmall) {
Text("True")
.font(.system(size: Design.Size.cardCountLabelSize, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
Text(trueCount >= 0 ? "+\(trueCount, format: .number.precision(.fractionLength(1)))" : "\(trueCount, format: .number.precision(.fractionLength(1)))")
.font(.system(size: Design.Size.cardCountValueSize, weight: .bold, design: .monospaced))
.foregroundStyle(countColor(for: Int(trueCount.rounded())))
}
}
.padding(.horizontal, Design.Spacing.large)
.padding(.vertical, Design.Spacing.small)
.background(
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
.fill(Color.black.opacity(Design.Opacity.subtle))
)
.accessibilityElement(children: .ignore)
.accessibilityLabel(String(localized: "Card Count"))
.accessibilityValue(String(localized: "Running \(runningCount), True \(trueCount, format: .number.precision(.fractionLength(1)))"))
}
private func countColor(for count: Int) -> Color {
if count > 0 {
return .green // Positive count favors player
} else if count < 0 {
return .red // Negative count favors house
} else {
return .white // Neutral
}
}
}
// MARK: - Previews
#Preview("Neutral Count") {
ZStack {
Color.Table.felt.ignoresSafeArea()
CardCountView(runningCount: 0, trueCount: 0.0)
}
}
#Preview("Positive Count") {
ZStack {
Color.Table.felt.ignoresSafeArea()
CardCountView(runningCount: 7, trueCount: 2.3)
}
}
#Preview("Negative Count") {
ZStack {
Color.Table.felt.ignoresSafeArea()
CardCountView(runningCount: -4, trueCount: -1.5)
}
}

View File

@ -1,190 +0,0 @@
//
// GameTableView.swift
// Blackjack
//
// Main game container view.
//
import SwiftUI
import CasinoKit
struct GameTableView: View {
@State private var settings = GameSettings()
@State private var gameState: GameState?
@State private var selectedChip: ChipDenomination = .twentyFive
// MARK: - Sheet State
@State private var showSettings = false
@State private var showRules = false
@State private var showStats = false
// MARK: - Environment
@Environment(\.horizontalSizeClass) private var horizontalSizeClass
@Environment(\.verticalSizeClass) private var verticalSizeClass
/// Whether we're on iPad
private var isIPad: Bool {
horizontalSizeClass == .regular
}
/// Maximum content width based on device
private var maxContentWidth: CGFloat {
if isIPad {
return verticalSizeClass == .compact
? Design.Size.maxContentWidthLandscape
: Design.Size.maxContentWidthPortrait
}
return .infinity
}
// MARK: - Body
var body: some View {
Group {
if let state = gameState {
mainGameView(state: state)
} else {
ProgressView()
.task {
gameState = GameState(settings: settings)
}
}
}
.sheet(isPresented: $showSettings) {
SettingsView(settings: settings, gameState: gameState)
}
.sheet(isPresented: $showRules) {
RulesHelpView()
}
.sheet(isPresented: $showStats) {
if let state = gameState {
StatisticsSheetView(state: state)
}
}
}
// Use global debug flag from Design constants
private var showDebugBorders: Bool { Design.showDebugBorders }
// MARK: - Main Game View
@ViewBuilder
private func mainGameView(state: GameState) -> some View {
ZStack {
// Background
TableBackgroundView()
VStack(spacing: 0) {
// Top bar
TopBarView(
balance: state.balance,
secondaryInfo: settings.showCardsRemaining ? "\(state.engine.cardsRemaining)" : nil,
secondaryIcon: settings.showCardsRemaining ? "rectangle.portrait.on.rectangle.portrait.fill" : nil,
onReset: { state.resetGame() },
onSettings: { showSettings = true },
onHelp: { showRules = true },
onStats: { showStats = true }
)
.frame(maxWidth: maxContentWidth)
.debugBorder(showDebugBorders, color: .cyan, label: "TopBar")
// Card count display (when enabled)
if settings.showCardCount {
CardCountView(
runningCount: state.engine.runningCount,
trueCount: state.engine.trueCount
)
.frame(maxWidth: maxContentWidth)
.debugBorder(showDebugBorders, color: .mint, label: "CardCount")
}
// Reshuffle notification
if state.showReshuffleNotification {
ReshuffleNotificationView(showCardCount: settings.showCardCount)
.frame(maxWidth: maxContentWidth)
.transition(.move(edge: .top).combined(with: .opacity))
}
// Table layout - fills available space
BlackjackTableView(
state: state,
onPlaceBet: { placeBet(state: state) }
)
.frame(maxWidth: maxContentWidth)
// Chip selector - only shown during betting phase
if state.currentPhase == .betting {
Spacer()
.debugBorder(showDebugBorders, color: .yellow, label: "ChipSpacer")
ChipSelectorView(
selectedChip: $selectedChip,
balance: state.balance,
currentBet: state.currentBet,
maxBet: state.settings.maxBet
)
.frame(maxWidth: maxContentWidth)
.transition(.opacity.combined(with: .move(edge: .bottom)))
.debugBorder(showDebugBorders, color: .pink, label: "ChipSelector")
}
// Action buttons - minimal spacing during player turn
ActionButtonsView(state: state)
.frame(maxWidth: maxContentWidth)
.padding(.bottom, Design.Spacing.small)
.debugBorder(showDebugBorders, color: .blue, label: "ActionBtns")
}
.frame(maxWidth: .infinity)
// Insurance popup overlay (covers entire screen)
if state.currentPhase == .insurance {
InsurancePopupView(
betAmount: state.currentBet / 2,
balance: state.balance,
onTake: { Task { await state.takeInsurance() } },
onDecline: { state.declineInsurance() }
)
.transition(.opacity.combined(with: .scale(scale: 0.9)))
}
// Result banner overlay
if state.showResultBanner, let result = state.lastRoundResult {
ResultBannerView(
result: result,
currentBalance: state.balance,
minBet: state.settings.minBet,
onNewRound: { state.newRound() },
onPlayAgain: { state.resetGame() }
)
}
// Confetti for wins (matching Baccarat pattern)
if state.showResultBanner && (state.lastRoundResult?.totalWinnings ?? 0) > 0 {
ConfettiView()
}
// Game over
if state.isGameOver && !state.showResultBanner {
GameOverView(
roundsPlayed: state.roundsPlayed,
onPlayAgain: { state.resetGame() }
)
}
}
}
// MARK: - Betting
private func placeBet(state: GameState) {
state.placeBet(amount: selectedChip.rawValue)
}
}
// MARK: - Preview
#Preview {
GameTableView()
}

View File

@ -1,60 +0,0 @@
//
// ReshuffleNotificationView.swift
// Blackjack
//
// Shows a notification when the shoe is reshuffled.
//
import SwiftUI
import CasinoKit
/// Shows a notification when the shoe is reshuffled.
struct ReshuffleNotificationView: View {
let showCardCount: Bool
var body: some View {
HStack(spacing: Design.Spacing.small) {
Image(systemName: "shuffle")
.foregroundStyle(.white)
VStack(alignment: .leading, spacing: Design.Spacing.xxSmall) {
Text("Shoe Reshuffled")
.font(.system(size: Design.BaseFontSize.medium, weight: .bold))
.foregroundStyle(.white)
if showCardCount {
Text("Count reset to 0")
.font(.system(size: Design.BaseFontSize.small))
.foregroundStyle(.white.opacity(Design.Opacity.strong))
}
}
}
.padding(.horizontal, Design.Spacing.large)
.padding(.vertical, Design.Spacing.medium)
.background(
Capsule()
.fill(Color.blue.opacity(Design.Opacity.heavy))
)
.accessibilityElement(children: .ignore)
.accessibilityLabel(showCardCount
? String(localized: "Shoe reshuffled, count reset to zero")
: String(localized: "Shoe reshuffled"))
}
}
// MARK: - Previews
#Preview("Without Count") {
ZStack {
Color.Table.felt.ignoresSafeArea()
ReshuffleNotificationView(showCardCount: false)
}
}
#Preview("With Count Reset") {
ZStack {
Color.Table.felt.ignoresSafeArea()
ReshuffleNotificationView(showCardCount: true)
}
}

View File

@ -1,237 +0,0 @@
//
// ResultBannerView.swift
// Blackjack
//
// Displays the result of a round with breakdown.
//
import SwiftUI
import CasinoKit
struct ResultBannerView: View {
let result: RoundResult
let currentBalance: Int
let minBet: Int
let onNewRound: () -> Void
let onPlayAgain: () -> Void
@State private var showContent = false
// MARK: - Scaled Metrics
@ScaledMetric(relativeTo: .largeTitle) private var titleFontSize: CGFloat = Design.BaseFontSize.largeTitle
@ScaledMetric(relativeTo: .title) private var resultFontSize: CGFloat = Design.BaseFontSize.title
@ScaledMetric(relativeTo: .headline) private var amountFontSize: CGFloat = Design.BaseFontSize.xLarge
@ScaledMetric(relativeTo: .body) private var buttonFontSize: CGFloat = Design.BaseFontSize.medium
// MARK: - Computed
private var isGameOver: Bool {
currentBalance < minBet
}
private var mainResultColor: Color {
result.mainHandResult.color
}
private var winningsText: String {
if result.totalWinnings > 0 {
return "+$\(result.totalWinnings)"
} else if result.totalWinnings < 0 {
return "-$\(abs(result.totalWinnings))"
} else {
return "$0"
}
}
private var winningsColor: Color {
if result.totalWinnings > 0 { return .green }
if result.totalWinnings < 0 { return .red }
return .blue
}
var body: some View {
ZStack {
// Full screen dark background
Color.black.opacity(Design.Opacity.strong)
.ignoresSafeArea()
// Content card
VStack(spacing: Design.Spacing.xLarge) {
// Main result
Text(result.mainHandResult.displayText)
.font(.system(size: titleFontSize, weight: .black, design: .rounded))
.foregroundStyle(mainResultColor)
// Winnings
Text(winningsText)
.font(.system(size: amountFontSize, weight: .bold, design: .rounded))
.foregroundStyle(winningsColor)
// Breakdown - all hands
VStack(spacing: Design.Spacing.small) {
ForEach(result.handResults.indices, id: \.self) { index in
let handResult = result.handResults[index]
// Hand numbering: index 0 = Hand 1 (played first, displayed rightmost)
let handLabel = result.handResults.count > 1
? String(localized: "Hand \(index + 1)")
: String(localized: "Main Hand")
ResultRow(label: handLabel, result: handResult)
}
if let insuranceResult = result.insuranceResult {
ResultRow(label: String(localized: "Insurance"), result: insuranceResult)
}
}
.padding(Design.Spacing.medium)
.background(
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
.fill(Color.white.opacity(Design.Opacity.subtle))
)
// Game over message
if isGameOver {
VStack(spacing: Design.Spacing.small) {
Text(String(localized: "You've run out of chips!"))
.font(.system(size: Design.BaseFontSize.medium, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.strong))
Button(action: onPlayAgain) {
HStack(spacing: Design.Spacing.small) {
Image(systemName: "arrow.counterclockwise")
Text(String(localized: "Play Again"))
}
.font(.system(size: buttonFontSize, weight: .bold))
.foregroundStyle(.black)
.padding(.horizontal, Design.Spacing.xxLarge)
.padding(.vertical, Design.Spacing.medium)
.background(
Capsule()
.fill(
LinearGradient(
colors: [Color.Button.goldLight, Color.Button.goldDark],
startPoint: .top,
endPoint: .bottom
)
)
)
}
}
} else {
// New Round button
Button(action: onNewRound) {
HStack(spacing: Design.Spacing.small) {
Image(systemName: "arrow.clockwise")
Text(String(localized: "New Round"))
}
.font(.system(size: buttonFontSize, weight: .bold))
.foregroundStyle(.black)
.padding(.horizontal, Design.Spacing.xxLarge)
.padding(.vertical, Design.Spacing.medium)
.background(
Capsule()
.fill(
LinearGradient(
colors: [Color.Button.goldLight, Color.Button.goldDark],
startPoint: .top,
endPoint: .bottom
)
)
)
}
}
}
.padding(Design.Spacing.xxLarge)
.background(
RoundedRectangle(cornerRadius: Design.CornerRadius.xxLarge)
.fill(
LinearGradient(
colors: [Color.Modal.backgroundLight, Color.Modal.backgroundDark],
startPoint: .top,
endPoint: .bottom
)
)
.overlay(
RoundedRectangle(cornerRadius: Design.CornerRadius.xxLarge)
.strokeBorder(
mainResultColor.opacity(Design.Opacity.medium),
lineWidth: Design.LineWidth.medium
)
)
)
.shadow(color: mainResultColor.opacity(Design.Opacity.hint), radius: Design.Shadow.radiusXLarge)
.frame(maxWidth: Design.Size.maxModalWidth)
.padding(.horizontal, Design.Spacing.large) // Prevent clipping on sides
.scaleEffect(showContent ? 1.0 : 0.8)
.opacity(showContent ? 1.0 : 0)
}
.onAppear {
withAnimation(.spring(duration: Design.Animation.springDuration, bounce: 0.3)) {
showContent = true
}
// Play game over sound if out of chips (after a delay so it doesn't overlap with result sound)
if isGameOver {
Task {
try? await Task.sleep(for: .seconds(1))
SoundManager.shared.play(.gameOver)
}
}
}
.accessibilityElement(children: .contain)
.accessibilityLabel(String(localized: "Round result: \(result.mainHandResult.displayText)"))
.accessibilityAddTraits(AccessibilityTraits.isModal)
}
}
// MARK: - Result Row
struct ResultRow: View {
let label: String
let result: HandResult
var body: some View {
HStack {
Text(label)
.font(.system(size: Design.BaseFontSize.body))
.foregroundStyle(.white.opacity(Design.Opacity.strong))
Spacer()
Text(result.displayText)
.font(.system(size: Design.BaseFontSize.body, weight: .bold))
.foregroundStyle(result.color)
}
}
}
#Preview("Single Hand") {
ResultBannerView(
result: RoundResult(
handResults: [.blackjack],
insuranceResult: nil,
totalWinnings: 150,
wasBlackjack: true
),
currentBalance: 10150,
minBet: 10,
onNewRound: {},
onPlayAgain: {}
)
}
#Preview("Multiple Split Hands") {
ResultBannerView(
result: RoundResult(
handResults: [.bust, .win, .push],
insuranceResult: nil,
totalWinnings: 25,
wasBlackjack: false
),
currentBalance: 1025,
minBet: 10,
onNewRound: {},
onPlayAgain: {}
)
}

View File

@ -1,290 +0,0 @@
//
// RulesHelpView.swift
// Blackjack
//
// Game rules and how to play guide.
//
import SwiftUI
import CasinoKit
struct RulesHelpView: View {
@Environment(\.dismiss) private var dismiss
@State private var currentPage = 0
private let pages: [RulePage] = [
RulePage(
title: String(localized: "Objective"),
icon: "target",
content: [
String(localized: "Beat the dealer by getting a hand value closer to 21 without going over."),
String(localized: "If you go over 21, you 'bust' and lose immediately."),
String(localized: "If the dealer busts and you haven't, you win.")
]
),
RulePage(
title: String(localized: "Card Values"),
icon: "suit.spade.fill",
content: [
String(localized: "2-10: Face value"),
String(localized: "Jack, Queen, King: 10"),
String(localized: "Ace: 1 or 11 (whichever helps your hand)"),
String(localized: "A 'soft' hand has an Ace counting as 11.")
]
),
RulePage(
title: String(localized: "Blackjack"),
icon: "star.fill",
content: [
String(localized: "An Ace + 10-value card dealt initially is 'Blackjack'."),
String(localized: "Blackjack pays 3:2 (1.5x your bet)."),
String(localized: "If both you and dealer have Blackjack, it's a push (tie).")
]
),
RulePage(
title: String(localized: "Actions"),
icon: "hand.tap.fill",
content: [
String(localized: "Hit: Take another card"),
String(localized: "Stand: Keep your current hand"),
String(localized: "Double Down: Double your bet, take one card, then stand"),
String(localized: "Split: If you have two cards of the same value, split into two hands"),
String(localized: "Surrender: Give up half your bet and end the hand")
]
),
RulePage(
title: String(localized: "Insurance"),
icon: "shield.fill",
content: [
String(localized: "Offered when dealer shows an Ace."),
String(localized: "Costs half your original bet."),
String(localized: "Pays 2:1 if dealer has Blackjack."),
String(localized: "Generally not recommended by basic strategy.")
]
),
RulePage(
title: String(localized: "Dealer Rules"),
icon: "person.fill",
content: [
String(localized: "Dealer must hit on 16 or less."),
String(localized: "Dealer must stand on 17 or more (varies by rules)."),
String(localized: "Some games: Dealer hits on 'soft 17' (Ace + 6).")
]
),
RulePage(
title: String(localized: "Payouts"),
icon: "dollarsign.circle.fill",
content: [
String(localized: "Win: 1:1 (even money)"),
String(localized: "Blackjack: 3:2"),
String(localized: "Insurance: 2:1"),
String(localized: "Push: Bet returned"),
String(localized: "Surrender: Half bet returned")
]
),
RulePage(
title: String(localized: "Vegas Strip"),
icon: "sparkles",
content: [
String(localized: "Most popular style on the Las Vegas Strip."),
String(localized: "6 decks shuffled together."),
String(localized: "Dealer stands on all 17s (including soft 17)."),
String(localized: "Double down allowed on any two cards."),
String(localized: "Double after split (DAS) allowed."),
String(localized: "Split up to 4 hands, but not aces."),
String(localized: "No surrender option."),
String(localized: "Blackjack pays 3:2.")
]
),
RulePage(
title: String(localized: "Atlantic City"),
icon: "building.2.fill",
content: [
String(localized: "Standard rules on the East Coast."),
String(localized: "8 decks shuffled together."),
String(localized: "Dealer stands on all 17s."),
String(localized: "Double down on any two cards."),
String(localized: "Double after split allowed."),
String(localized: "Re-split aces allowed."),
String(localized: "Late surrender available."),
String(localized: "Blackjack pays 3:2.")
]
),
RulePage(
title: String(localized: "European"),
icon: "globe.europe.africa.fill",
content: [
String(localized: "Traditional European casino style."),
String(localized: "6 decks shuffled together."),
String(localized: "No hole card: dealer takes second card after player acts."),
String(localized: "Dealer stands on all 17s."),
String(localized: "Double on 9, 10, or 11 only (some venues)."),
String(localized: "Double after split allowed."),
String(localized: "No surrender option."),
String(localized: "Higher house edge due to no hole card.")
]
),
RulePage(
title: String(localized: "Deck Count"),
icon: "rectangle.stack.fill",
content: [
String(localized: "1 Deck: Lowest house edge (~0.17%), rare to find."),
String(localized: "2 Decks: Low house edge (~0.35%), common online."),
String(localized: "4 Decks: Moderate house edge (~0.45%)."),
String(localized: "6 Decks: Standard in Vegas (~0.50%)."),
String(localized: "8 Decks: Standard in Atlantic City (~0.55%)."),
String(localized: "More decks = harder to count cards."),
String(localized: "Fewer decks favor the player slightly.")
]
),
RulePage(
title: String(localized: "Rule Variations"),
icon: "slider.horizontal.3",
content: [
String(localized: "Dealer Hits Soft 17: Increases house edge by ~0.2%."),
String(localized: "Double After Split (DAS): Reduces house edge by ~0.15%."),
String(localized: "Re-split Aces: Reduces house edge by ~0.05%."),
String(localized: "Late Surrender: Reduces house edge by ~0.07%."),
String(localized: "6:5 Blackjack (avoid!): Increases house edge by ~1.4%.")
]
),
RulePage(
title: String(localized: "Basic Strategy"),
icon: "lightbulb.fill",
content: [
String(localized: "Always split Aces and 8s."),
String(localized: "Never split 10s or 5s."),
String(localized: "Double on 11 vs dealer 2-10."),
String(localized: "Double on 10 vs dealer 2-9."),
String(localized: "Stand on 17+ always."),
String(localized: "Hit on soft 17 or less."),
String(localized: "Surrender 16 vs dealer 9, 10, Ace.")
]
),
RulePage(
title: String(localized: "Card Counting"),
icon: "number.circle.fill",
content: [
String(localized: "Hi-Lo is the most popular counting system."),
String(localized: "2-6: +1 (low cards favor house)"),
String(localized: "7-9: 0 (neutral cards)"),
String(localized: "10-A: -1 (high cards favor player)"),
String(localized: "Running Count: Sum of all card values seen."),
String(localized: "True Count: Running count ÷ decks remaining."),
String(localized: "Positive count = more high cards remain = player advantage.")
]
),
RulePage(
title: String(localized: "Using the Count"),
icon: "chart.line.uptrend.xyaxis",
content: [
String(localized: "True count of +2 or higher favors the player."),
String(localized: "Increase bets when the count is positive."),
String(localized: "Decrease bets when the count is negative."),
String(localized: "Fewer decks = easier to count accurately."),
String(localized: "Count resets to 0 when the shoe is shuffled."),
String(localized: "Enable 'Card Count' in Settings to practice.")
]
)
]
var body: some View {
NavigationStack {
ZStack {
Color.Settings.background
.ignoresSafeArea()
VStack(spacing: 0) {
// Page content
TabView(selection: $currentPage) {
ForEach(pages.indices, id: \.self) { index in
RulePageView(page: pages[index])
.tag(index)
}
}
.tabViewStyle(.page(indexDisplayMode: .never))
// Page indicator
HStack(spacing: Design.Spacing.small) {
ForEach(pages.indices, id: \.self) { index in
Circle()
.fill(index == currentPage ? Color.Settings.accent : Color.white.opacity(Design.Opacity.light))
.frame(width: 8, height: 8)
}
}
.padding(.vertical, Design.Spacing.medium)
}
}
.navigationTitle(String(localized: "How to Play"))
.navigationBarTitleDisplayMode(.inline)
.toolbar {
ToolbarItem(placement: .topBarTrailing) {
Button(String(localized: "Done")) {
dismiss()
}
.foregroundStyle(Color.Settings.accent)
}
}
.toolbarBackground(Color.Settings.background, for: .navigationBar)
.toolbarColorScheme(.dark, for: .navigationBar)
}
}
}
// MARK: - Rule Page Model
struct RulePage: Identifiable {
let id = UUID()
let title: String
let icon: String
let content: [String]
}
// MARK: - Rule Page View
struct RulePageView: View {
let page: RulePage
@ScaledMetric(relativeTo: .title) private var iconSize: CGFloat = Design.BaseFontSize.display
@ScaledMetric(relativeTo: .title) private var titleSize: CGFloat = Design.BaseFontSize.title
@ScaledMetric(relativeTo: .body) private var bodySize: CGFloat = Design.BaseFontSize.body
var body: some View {
ScrollView {
VStack(spacing: Design.Spacing.xLarge) {
// Icon
Image(systemName: page.icon)
.font(.system(size: iconSize))
.foregroundStyle(Color.Settings.accent)
.padding(.top, Design.Spacing.xxLarge)
// Title
Text(page.title)
.font(.system(size: titleSize, weight: .bold))
.foregroundStyle(.white)
// Content
VStack(alignment: .leading, spacing: Design.Spacing.medium) {
ForEach(page.content.indices, id: \.self) { index in
HStack(alignment: .top, spacing: Design.Spacing.medium) {
Text("")
.foregroundStyle(Color.Settings.accent)
Text(page.content[index])
.font(.system(size: bodySize))
.foregroundStyle(.white.opacity(Design.Opacity.heavy))
}
}
}
.padding(.horizontal, Design.Spacing.xxLarge)
Spacer()
}
}
}
}
#Preview {
RulesHelpView()
}

View File

@ -1,237 +0,0 @@
//
// SettingsView.swift
// Blackjack
//
// Game settings and rule configuration.
//
import SwiftUI
import CasinoKit
struct SettingsView: View {
@Bindable var settings: GameSettings
let gameState: GameState?
@Environment(\.dismiss) private var dismiss
var body: some View {
SheetContainerView(
title: String(localized: "Settings"),
content: {
// Game Style
SheetSection(title: String(localized: "GAME STYLE"), icon: "suit.club.fill") {
GameStylePicker(selection: $settings.gameStyle)
}
// Deck Settings
SheetSection(title: String(localized: "DECK SETTINGS"), icon: "rectangle.portrait.on.rectangle.portrait") {
DeckCountPicker(selection: $settings.deckCount)
}
.onChange(of: settings.deckCount) { _, _ in
// Reshuffle with new deck count
gameState?.applyDeckCountChange()
}
// Table Limits
SheetSection(title: String(localized: "TABLE LIMITS"), icon: "banknote") {
TableLimitsPicker(selection: $settings.tableLimits)
}
// Rule Options (for custom style)
if settings.gameStyle == .custom {
SheetSection(title: String(localized: "RULES"), icon: "list.bullet.clipboard") {
VStack(spacing: Design.Spacing.small) {
SettingsToggle(
title: String(localized: "Dealer Hits Soft 17"),
subtitle: String(localized: "H17 rule, increases house edge"),
isOn: $settings.dealerHitsSoft17
)
Divider().background(Color.white.opacity(Design.Opacity.hint))
SettingsToggle(
title: String(localized: "Double After Split"),
subtitle: String(localized: "Allow doubling on split hands"),
isOn: $settings.doubleAfterSplit
)
Divider().background(Color.white.opacity(Design.Opacity.hint))
SettingsToggle(
title: String(localized: "Re-split Aces"),
subtitle: String(localized: "Allow splitting aces again"),
isOn: $settings.resplitAces
)
Divider().background(Color.white.opacity(Design.Opacity.hint))
SettingsToggle(
title: String(localized: "Late Surrender"),
subtitle: String(localized: "Surrender after dealer checks for blackjack"),
isOn: $settings.lateSurrender
)
}
}
}
// Display
SheetSection(title: String(localized: "DISPLAY"), icon: "eye") {
VStack(spacing: Design.Spacing.small) {
SettingsToggle(
title: String(localized: "Show Animations"),
subtitle: String(localized: "Card dealing animations"),
isOn: $settings.showAnimations
)
Divider().background(Color.white.opacity(Design.Opacity.hint))
SettingsToggle(
title: String(localized: "Show Hints"),
subtitle: String(localized: "Basic strategy suggestions"),
isOn: $settings.showHints
)
Divider().background(Color.white.opacity(Design.Opacity.hint))
SettingsToggle(
title: String(localized: "Card Count"),
subtitle: String(localized: "Show Hi-Lo running count & card values"),
isOn: $settings.showCardCount
)
Divider().background(Color.white.opacity(Design.Opacity.hint))
SettingsToggle(
title: String(localized: "Cards Remaining"),
subtitle: String(localized: "Show cards left in shoe"),
isOn: $settings.showCardsRemaining
)
Divider().background(Color.white.opacity(Design.Opacity.hint))
SpeedPicker(speed: $settings.dealingSpeed)
}
}
// Sound & Haptics
SheetSection(title: String(localized: "SOUND & HAPTICS"), icon: "speaker.wave.2") {
VStack(spacing: Design.Spacing.small) {
SettingsToggle(
title: String(localized: "Sound Effects"),
subtitle: String(localized: "Chips, cards, and results"),
isOn: $settings.soundEnabled
)
.onChange(of: settings.soundEnabled) { _, newValue in
SoundManager.shared.soundEnabled = newValue
}
Divider().background(Color.white.opacity(Design.Opacity.hint))
SettingsToggle(
title: String(localized: "Haptic Feedback"),
subtitle: String(localized: "Vibration on actions"),
isOn: $settings.hapticsEnabled
)
.onChange(of: settings.hapticsEnabled) { _, newValue in
SoundManager.shared.hapticsEnabled = newValue
}
Divider().background(Color.white.opacity(Design.Opacity.hint))
VolumePicker(volume: $settings.soundVolume)
.onChange(of: settings.soundVolume) { _, newValue in
SoundManager.shared.volume = newValue
}
}
}
// Starting Balance
SheetSection(title: String(localized: "NEW GAME"), icon: "dollarsign.circle") {
BalancePicker(balance: $settings.startingBalance)
}
// Version info
if let version = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String,
let build = Bundle.main.infoDictionary?["CFBundleVersion"] as? String {
Text(String(localized: "Version \(version) (\(build))"))
.font(.system(size: Design.BaseFontSize.small))
.foregroundStyle(.white.opacity(Design.Opacity.light))
.frame(maxWidth: .infinity)
.padding(.top, Design.Spacing.large)
}
},
onCancel: nil,
onDone: {
settings.save()
dismiss()
},
doneButtonText: String(localized: "Done")
)
}
}
// MARK: - Game Style Picker
struct GameStylePicker: View {
@Binding var selection: BlackjackStyle
var body: some View {
VStack(spacing: Design.Spacing.small) {
ForEach(BlackjackStyle.allCases) { style in
SelectableRow(
title: style.displayName,
subtitle: style.description,
isSelected: selection == style,
accentColor: Color.Settings.accent,
action: { selection = style }
)
}
}
}
}
// MARK: - Deck Count Picker
struct DeckCountPicker: View {
@Binding var selection: DeckCount
var body: some View {
VStack(spacing: Design.Spacing.medium) {
ForEach(DeckCount.allCases) { count in
SelectableRow(
title: count.displayName,
subtitle: count.description,
isSelected: selection == count,
accentColor: Color.Settings.accent,
action: { selection = count }
)
}
}
}
}
// MARK: - Table Limits Picker
struct TableLimitsPicker: View {
@Binding var selection: TableLimits
var body: some View {
VStack(spacing: Design.Spacing.small) {
ForEach(TableLimits.allCases) { limit in
SelectableRow(
title: limit.displayName,
subtitle: limit.detailedDescription,
isSelected: selection == limit,
accentColor: Color.Settings.accent,
badge: { BadgePill(text: limit.description, isSelected: selection == limit, accentColor: Color.Settings.accent) },
action: { selection = limit }
)
}
}
}
}
#Preview {
SettingsView(settings: GameSettings(), gameState: nil)
}

View File

@ -1,228 +0,0 @@
//
// StatisticsSheetView.swift
// Blackjack
//
// Game statistics and history.
//
import SwiftUI
import CasinoKit
struct StatisticsSheetView: View {
let state: GameState
@Environment(\.dismiss) private var dismiss
// MARK: - Computed Stats
private var totalRounds: Int {
state.roundHistory.count
}
private var wins: Int {
state.roundHistory.filter { $0.mainHandResult.isWin }.count
}
private var losses: Int {
state.roundHistory.filter {
$0.mainHandResult == .lose || $0.mainHandResult == .bust
}.count
}
private var pushes: Int {
state.roundHistory.filter { $0.mainHandResult == .push }.count
}
private var blackjacks: Int {
state.roundHistory.filter { $0.mainHandResult == .blackjack }.count
}
private var busts: Int {
state.roundHistory.filter { $0.mainHandResult == .bust }.count
}
private var surrenders: Int {
state.roundHistory.filter { $0.mainHandResult == .surrender }.count
}
private var winRate: Double {
guard totalRounds > 0 else { return 0 }
return Double(wins) / Double(totalRounds) * 100
}
private var totalWinnings: Int {
state.roundHistory.reduce(0) { $0 + $1.totalWinnings }
}
private var biggestWin: Int {
state.roundHistory.map { $0.totalWinnings }.filter { $0 > 0 }.max() ?? 0
}
private var biggestLoss: Int {
state.roundHistory.map { $0.totalWinnings }.filter { $0 < 0 }.min() ?? 0
}
var body: some View {
SheetContainerView(
title: String(localized: "Statistics"),
content: {
// Session Summary
SheetSection(title: String(localized: "SESSION SUMMARY"), icon: "chart.bar.fill") {
LazyVGrid(columns: [GridItem(.flexible()), GridItem(.flexible())], spacing: Design.Spacing.medium) {
StatBox(title: String(localized: "Rounds"), value: "\(totalRounds)", color: .white)
StatBox(title: String(localized: "Win Rate"), value: formatPercent(winRate), color: winRate >= 50 ? .green : .orange)
StatBox(title: String(localized: "Net"), value: formatMoney(totalWinnings), color: totalWinnings >= 0 ? .green : .red)
StatBox(title: String(localized: "Balance"), value: "$\(state.balance)", color: Color.Settings.accent)
}
}
// Win Distribution
SheetSection(title: String(localized: "OUTCOMES"), icon: "chart.pie.fill") {
VStack(spacing: Design.Spacing.small) {
OutcomeRow(label: String(localized: "Blackjacks"), count: blackjacks, total: totalRounds, color: .yellow)
OutcomeRow(label: String(localized: "Wins"), count: wins - blackjacks, total: totalRounds, color: .green)
OutcomeRow(label: String(localized: "Pushes"), count: pushes, total: totalRounds, color: .blue)
OutcomeRow(label: String(localized: "Losses"), count: losses - busts, total: totalRounds, color: .orange)
OutcomeRow(label: String(localized: "Busts"), count: busts, total: totalRounds, color: .red)
if surrenders > 0 {
OutcomeRow(label: String(localized: "Surrenders"), count: surrenders, total: totalRounds, color: .gray)
}
}
}
// Biggest Swings
if totalRounds > 0 {
SheetSection(title: String(localized: "BIGGEST SWINGS"), icon: "arrow.up.arrow.down") {
HStack(spacing: Design.Spacing.large) {
VStack(spacing: Design.Spacing.xSmall) {
Text(String(localized: "Best"))
.font(.system(size: Design.BaseFontSize.small))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
Text(formatMoney(biggestWin))
.font(.system(size: Design.BaseFontSize.xLarge, weight: .bold, design: .rounded))
.foregroundStyle(.green)
}
.frame(maxWidth: .infinity)
Divider()
.frame(height: 40)
.background(Color.white.opacity(Design.Opacity.hint))
VStack(spacing: Design.Spacing.xSmall) {
Text(String(localized: "Worst"))
.font(.system(size: Design.BaseFontSize.small))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
Text(formatMoney(biggestLoss))
.font(.system(size: Design.BaseFontSize.xLarge, weight: .bold, design: .rounded))
.foregroundStyle(.red)
}
.frame(maxWidth: .infinity)
}
}
}
},
onCancel: nil,
onDone: { dismiss() },
doneButtonText: String(localized: "Done")
)
}
private func formatMoney(_ amount: Int) -> String {
if amount >= 0 {
return "+$\(amount)"
} else {
return "-$\(abs(amount))"
}
}
private func formatPercent(_ value: Double) -> String {
value.formatted(.number.precision(.fractionLength(1))) + "%"
}
}
// MARK: - Stat Box
struct StatBox: View {
let title: String
let value: String
let color: Color
var body: some View {
VStack(spacing: Design.Spacing.xSmall) {
Text(title)
.font(.system(size: Design.BaseFontSize.small))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
Text(value)
.font(.system(size: Design.BaseFontSize.xLarge, weight: .bold, design: .rounded))
.foregroundStyle(color)
.lineLimit(1)
.minimumScaleFactor(0.7)
}
.frame(maxWidth: .infinity)
.padding(Design.Spacing.medium)
.background(
RoundedRectangle(cornerRadius: Design.CornerRadius.small)
.fill(Color.white.opacity(Design.Opacity.subtle))
)
}
}
// MARK: - Outcome Row
struct OutcomeRow: View {
let label: String
let count: Int
let total: Int
let color: Color
private var percentage: Double {
guard total > 0 else { return 0 }
return Double(count) / Double(total) * 100
}
private func formatPercentWhole(_ value: Double) -> String {
value.formatted(.number.precision(.fractionLength(0))) + "%"
}
var body: some View {
HStack {
// Label
Text(label)
.font(.system(size: Design.BaseFontSize.body))
.foregroundStyle(.white.opacity(Design.Opacity.strong))
Spacer()
// Count
Text("\(count)")
.font(.system(size: Design.BaseFontSize.body, weight: .bold))
.foregroundStyle(color)
// Progress bar
GeometryReader { geometry in
ZStack(alignment: .leading) {
RoundedRectangle(cornerRadius: Design.CornerRadius.xSmall)
.fill(Color.white.opacity(Design.Opacity.subtle))
RoundedRectangle(cornerRadius: Design.CornerRadius.xSmall)
.fill(color)
.frame(width: geometry.size.width * CGFloat(percentage / 100))
}
}
.frame(width: 60, height: 8)
// Percentage
Text(formatPercentWhole(percentage))
.font(.system(size: Design.BaseFontSize.small, design: .rounded))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
.frame(width: 40, alignment: .trailing)
}
.padding(.vertical, Design.Spacing.xSmall)
}
}
#Preview {
StatisticsSheetView(state: GameState(settings: GameSettings()))
}

View File

@ -1,106 +0,0 @@
//
// BettingZoneView.swift
// Blackjack
//
// The betting area where players place their bets.
//
import SwiftUI
import CasinoKit
struct BettingZoneView: View {
let betAmount: Int
let minBet: Int
let maxBet: Int
let onTap: () -> Void
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.Size.handLabelFontSize
private var isAtMax: Bool {
betAmount >= maxBet
}
var body: some View {
Button(action: onTap) {
ZStack {
// Background
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
.fill(Color.BettingZone.main)
.overlay(
RoundedRectangle(cornerRadius: Design.CornerRadius.large)
.strokeBorder(Color.BettingZone.mainBorder, lineWidth: Design.LineWidth.medium)
)
// Content
if betAmount > 0 {
// Show chip with amount (scaled)
ChipOnTableView(amount: betAmount, showMax: isAtMax, size: Design.Size.bettingChipSize)
} else {
// Empty state
VStack(spacing: Design.Spacing.small) {
Text(String(localized: "TAP TO BET"))
.font(.system(size: labelFontSize, weight: .bold))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
HStack(spacing: Design.Spacing.medium) {
Text(String(localized: "Min: $\(minBet)"))
.font(.system(size: Design.Size.handNumberFontSize, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.light))
Text(String(localized: "Max: $\(maxBet.formatted())"))
.font(.system(size: Design.Size.handNumberFontSize, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.light))
}
}
}
}
.frame(maxWidth: .infinity)
.frame(height: Design.Size.bettingZoneHeightScaled)
}
.buttonStyle(.plain)
.accessibilityLabel(betAmount > 0 ? "$\(betAmount) bet" + (isAtMax ? ", maximum" : "") : "Place bet")
.accessibilityHint("Double tap to add chips")
}
}
// MARK: - Previews
#Preview("Empty") {
ZStack {
Color.Table.felt.ignoresSafeArea()
BettingZoneView(
betAmount: 0,
minBet: 10,
maxBet: 1000,
onTap: {}
)
.padding()
}
}
#Preview("With Bet") {
ZStack {
Color.Table.felt.ignoresSafeArea()
BettingZoneView(
betAmount: 250,
minBet: 10,
maxBet: 1000,
onTap: {}
)
.padding()
}
}
#Preview("Max Bet") {
ZStack {
Color.Table.felt.ignoresSafeArea()
BettingZoneView(
betAmount: 1000,
minBet: 10,
maxBet: 1000,
onTap: {}
)
.padding()
}
}

View File

@ -1,112 +0,0 @@
//
// BlackjackTableView.swift
// Blackjack
//
// The main table layout showing dealer and player hands.
//
import SwiftUI
import CasinoKit
struct BlackjackTableView: View {
@Bindable var state: GameState
let onPlaceBet: () -> Void
/// Whether to show Hi-Lo card count values on cards.
var showCardCount: Bool { state.settings.showCardCount }
// MARK: - Scaled Metrics
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.BaseFontSize.medium
@ScaledMetric(relativeTo: .title) private var valueFontSize: CGFloat = Design.BaseFontSize.xLarge
@ScaledMetric(relativeTo: .caption) private var hintFontSize: CGFloat = Design.BaseFontSize.small
// MARK: - Layout
private let cardWidth: CGFloat = Design.Size.cardWidth
private let cardSpacing: CGFloat = Design.Size.cardOverlap
/// Fixed height for the hint area to prevent layout shifts
private let hintAreaHeight: CGFloat = 44
// Use global debug flag from Design constants
private var showDebugBorders: Bool { Design.showDebugBorders }
var body: some View {
VStack(spacing: Design.Spacing.small) {
// Dealer area
DealerHandView(
hand: state.dealerHand,
showHoleCard: state.shouldShowDealerHoleCard,
showCardCount: showCardCount,
cardWidth: cardWidth,
cardSpacing: cardSpacing
)
.debugBorder(showDebugBorders, color: .red, label: "Dealer")
// Flexible space between dealer and player (minimum 60pt)
Spacer(minLength: 60)
.debugBorder(showDebugBorders, color: .yellow, label: "Spacer")
// Player hands area - only show when there are cards dealt
if state.playerHands.first?.cards.isEmpty == false {
PlayerHandsView(
hands: state.playerHands,
activeHandIndex: state.activeHandIndex,
isPlayerTurn: state.isPlayerTurn,
showCardCount: showCardCount,
cardWidth: cardWidth,
cardSpacing: cardSpacing
)
.transition(.opacity)
.debugBorder(showDebugBorders, color: .green, label: "Player")
}
// Betting zone (when betting)
if state.currentPhase == .betting {
Spacer()
.debugBorder(showDebugBorders, color: .yellow, label: "Spacer2")
BettingZoneView(
betAmount: state.currentBet,
minBet: state.settings.minBet,
maxBet: state.settings.maxBet,
onTap: onPlaceBet
)
.transition(.scale.combined(with: .opacity))
.debugBorder(showDebugBorders, color: .blue, label: "BetZone")
// Betting hint based on count (only when card counting enabled)
if let hint = state.bettingHint {
BettingHintView(hint: hint, trueCount: state.engine.trueCount)
.transition(.opacity)
.debugBorder(showDebugBorders, color: .purple, label: "BetHint")
}
} else {
// Fixed-height hint area to prevent layout shifts during player turn
ZStack {
if let hint = state.currentHint {
HintView(hint: hint)
.transition(.opacity)
}
}
.frame(height: hintAreaHeight)
.debugBorder(showDebugBorders, color: .orange, label: "HintArea")
}
}
.padding(.horizontal, Design.Spacing.large)
.padding(.vertical, Design.Spacing.medium)
.debugBorder(showDebugBorders, color: .white, label: "TableView")
.animation(.spring(duration: Design.Animation.springDuration), value: state.currentPhase)
}
}
// MARK: - Previews
#Preview {
ZStack {
Color.Table.felt.ignoresSafeArea()
Text("Use GameTableView for full preview")
.foregroundStyle(.white)
}
}

View File

@ -1,157 +0,0 @@
//
// DealerHandView.swift
// Blackjack
//
// Displays the dealer's hand with cards and value.
//
import SwiftUI
import CasinoKit
struct DealerHandView: View {
let hand: BlackjackHand
let showHoleCard: Bool
let showCardCount: Bool
let cardWidth: CGFloat
let cardSpacing: CGFloat
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.Size.handLabelFontSize
var body: some View {
VStack(spacing: Design.Spacing.small) {
// Label and value
HStack(spacing: Design.Spacing.small) {
Text(String(localized: "DEALER"))
.font(.system(size: labelFontSize, weight: .bold, design: .rounded))
.foregroundStyle(.white)
// Show value: always show if hole card visible, or show single card value in European mode
if !hand.cards.isEmpty {
if showHoleCard {
ValueBadge(value: hand.value, color: Color.Hand.dealer)
} else if hand.cards.count == 1 {
// European mode: show single visible card value
ValueBadge(value: hand.cards[0].blackjackValue, color: Color.Hand.dealer)
}
}
}
// Cards
HStack(spacing: hand.cards.isEmpty ? Design.Spacing.small : cardSpacing) {
if hand.cards.isEmpty {
CardPlaceholderView(width: cardWidth)
CardPlaceholderView(width: cardWidth)
} else {
ForEach(hand.cards.indices, id: \.self) { index in
let isFaceUp = index == 0 || showHoleCard
CardView(
card: hand.cards[index],
isFaceUp: isFaceUp,
cardWidth: cardWidth
)
.overlay(alignment: .bottomLeading) {
if showCardCount && isFaceUp {
HiLoCountBadge(card: hand.cards[index])
}
}
.zIndex(Double(index))
}
// Show placeholder for second card in European mode (no hole card)
if hand.cards.count == 1 && !showHoleCard {
CardPlaceholderView(width: cardWidth)
.opacity(Design.Opacity.medium)
}
}
}
// Result badge
if let result = hand.cards.count >= 2 && showHoleCard ? handResultText : nil {
Text(result)
.font(.system(size: labelFontSize, weight: .black))
.foregroundStyle(handResultColor)
.padding(.horizontal, Design.Spacing.medium)
.padding(.vertical, Design.Spacing.xSmall)
.background(
Capsule()
.fill(handResultColor.opacity(Design.Opacity.hint))
)
}
}
.accessibilityElement(children: .ignore)
.accessibilityLabel(dealerAccessibilityLabel)
}
// MARK: - Computed Properties
private var handResultText: String? {
if hand.isBlackjack {
return String(localized: "BLACKJACK")
}
if hand.isBusted {
return String(localized: "BUST")
}
return nil
}
private var handResultColor: Color {
if hand.isBlackjack { return .yellow }
if hand.isBusted { return .green } // Good for player
return .white
}
private var dealerAccessibilityLabel: String {
if hand.cards.isEmpty {
return String(localized: "Dealer: No cards")
}
let visibleCards = showHoleCard ? hand.cards : [hand.cards[0]]
let cardsDescription = visibleCards.map { $0.accessibilityDescription }.joined(separator: ", ")
return String(localized: "Dealer: \(cardsDescription). Value: \(showHoleCard ? String(hand.value) : "hidden")")
}
}
#Preview("Empty Hand") {
ZStack {
Color.Table.felt.ignoresSafeArea()
DealerHandView(
hand: BlackjackHand(),
showHoleCard: false,
showCardCount: false,
cardWidth: 60,
cardSpacing: -20
)
}
}
#Preview("Two Cards - Hole Hidden") {
ZStack {
Color.Table.felt.ignoresSafeArea()
DealerHandView(
hand: BlackjackHand(cards: [
Card(suit: .spades, rank: .ace),
Card(suit: .hearts, rank: .king)
]),
showHoleCard: false,
showCardCount: false,
cardWidth: 60,
cardSpacing: -20
)
}
}
#Preview("Blackjack - Revealed") {
ZStack {
Color.Table.felt.ignoresSafeArea()
DealerHandView(
hand: BlackjackHand(cards: [
Card(suit: .spades, rank: .ace),
Card(suit: .hearts, rank: .king)
]),
showHoleCard: true,
showCardCount: true,
cardWidth: 60,
cardSpacing: -20
)
}
}

View File

@ -1,81 +0,0 @@
//
// HiLoCountBadge.swift
// Blackjack
//
// Badge showing the Hi-Lo counting value of a card.
//
import SwiftUI
import CasinoKit
/// A small badge showing the Hi-Lo counting value of a card.
struct HiLoCountBadge: View {
let card: Card
var body: some View {
Text(card.hiLoDisplayText)
.font(.system(size: Design.Size.countBadgeFontSize, weight: .bold, design: .rounded))
.foregroundStyle(badgeTextColor)
.padding(.horizontal, Design.Size.countBadgePaddingH)
.padding(.vertical, Design.Size.countBadgePaddingV)
.background(
Capsule()
.fill(badgeBackgroundColor)
)
.offset(x: -Design.Size.countBadgeOffset, y: Design.Size.countBadgeOffset)
}
private var badgeBackgroundColor: Color {
switch card.hiLoValue {
case 1: return .green // Low cards = positive for player
case -1: return .red // High cards = negative for player
default: return .gray // Neutral
}
}
private var badgeTextColor: Color {
.white
}
}
// MARK: - Card Accessibility Extension
extension Card {
/// Accessibility description for VoiceOver.
var accessibilityDescription: String {
"\(rank.accessibilityName) of \(suit.accessibilityName)"
}
}
// MARK: - Previews
#Preview("Low Card (+1)") {
ZStack {
Color.Table.felt.ignoresSafeArea()
CardView(card: Card(suit: .hearts, rank: .five), isFaceUp: true, cardWidth: 70)
.overlay(alignment: .bottomLeading) {
HiLoCountBadge(card: Card(suit: .hearts, rank: .five))
}
}
}
#Preview("High Card (-1)") {
ZStack {
Color.Table.felt.ignoresSafeArea()
CardView(card: Card(suit: .spades, rank: .king), isFaceUp: true, cardWidth: 70)
.overlay(alignment: .bottomLeading) {
HiLoCountBadge(card: Card(suit: .spades, rank: .king))
}
}
}
#Preview("Neutral Card (0)") {
ZStack {
Color.Table.felt.ignoresSafeArea()
CardView(card: Card(suit: .diamonds, rank: .seven), isFaceUp: true, cardWidth: 70)
.overlay(alignment: .bottomLeading) {
HiLoCountBadge(card: Card(suit: .diamonds, rank: .seven))
}
}
}

View File

@ -1,128 +0,0 @@
//
// HintViews.swift
// Blackjack
//
// Views for displaying game hints and betting recommendations.
//
import SwiftUI
import CasinoKit
// MARK: - General Hint View
/// Displays a gameplay hint (hit, stand, double, etc.)
struct HintView: View {
let hint: String
var body: some View {
HStack(spacing: Design.Spacing.small) {
Image(systemName: "lightbulb.fill")
.font(.system(size: Design.Size.hintIconSize))
.foregroundStyle(.yellow)
Text(String(localized: "Hint: \(hint)"))
.font(.system(size: Design.Size.hintFontSize, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.strong))
}
.padding(.horizontal, Design.Size.hintPaddingH)
.padding(.vertical, Design.Size.hintPaddingV)
.background(
Capsule()
.fill(Color.black.opacity(Design.Opacity.light))
)
.accessibilityElement(children: .ignore)
.accessibilityLabel(String(localized: "Hint"))
.accessibilityValue(hint)
}
}
// MARK: - Betting Hint View
/// Shows betting recommendations based on the current card count.
struct BettingHintView: View {
let hint: String
let trueCount: Double
private var hintColor: Color {
let tc = Int(trueCount.rounded())
if tc >= 2 {
return .green // Player advantage - bet more
} else if tc <= -1 {
return .red // House advantage - bet less
} else {
return .yellow // Neutral
}
}
private var icon: String {
let tc = Int(trueCount.rounded())
if tc >= 2 {
return "arrow.up.circle.fill" // Increase bet
} else if tc <= -1 {
return "arrow.down.circle.fill" // Decrease bet
} else {
return "equal.circle.fill" // Neutral
}
}
var body: some View {
HStack(spacing: Design.Spacing.small) {
Image(systemName: icon)
.font(.system(size: Design.Size.hintIconSize))
.foregroundStyle(hintColor)
Text(hint)
.font(.system(size: Design.Size.hintFontSize, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.strong))
}
.padding(.horizontal, Design.Size.hintPaddingH)
.padding(.vertical, Design.Size.hintPaddingV)
.background(
Capsule()
.fill(Color.black.opacity(Design.Opacity.light))
.overlay(
Capsule()
.strokeBorder(hintColor.opacity(Design.Opacity.medium), lineWidth: Design.LineWidth.thin)
)
)
.accessibilityElement(children: .ignore)
.accessibilityLabel(String(localized: "Betting Hint"))
.accessibilityValue(hint)
}
}
// MARK: - Previews
#Preview("Game Hint - Hit") {
ZStack {
Color.Table.felt.ignoresSafeArea()
HintView(hint: "Hit")
}
}
#Preview("Game Hint - Stand") {
ZStack {
Color.Table.felt.ignoresSafeArea()
HintView(hint: "Stand")
}
}
#Preview("Betting Hint - Positive Count") {
ZStack {
Color.Table.felt.ignoresSafeArea()
BettingHintView(hint: "Bet 4x minimum", trueCount: 2.5)
}
}
#Preview("Betting Hint - Negative Count") {
ZStack {
Color.Table.felt.ignoresSafeArea()
BettingHintView(hint: "Bet minimum", trueCount: -1.5)
}
}
#Preview("Betting Hint - Neutral") {
ZStack {
Color.Table.felt.ignoresSafeArea()
BettingHintView(hint: "Bet minimum (neutral)", trueCount: 0.0)
}
}

View File

@ -1,119 +0,0 @@
//
// InsurancePopupView.swift
// Blackjack
//
// Modal popup for insurance decision when dealer shows an Ace.
//
import SwiftUI
import CasinoKit
struct InsurancePopupView: View {
let betAmount: Int
let balance: Int
let onTake: () -> Void
let onDecline: () -> Void
var body: some View {
ZStack {
// Dimmed background
Color.black.opacity(Design.Opacity.medium)
.ignoresSafeArea()
.onTapGesture { } // Prevent taps passing through
// Popup card
VStack(spacing: Design.Spacing.large) {
// Icon
Image(systemName: "shield.fill")
.font(.system(size: Design.IconSize.xLarge))
.foregroundStyle(.yellow)
// Title
Text(String(localized: "INSURANCE?"))
.font(.system(size: Design.BaseFontSize.xLarge, weight: .bold))
.foregroundStyle(.white)
// Subtitle
Text(String(localized: "Dealer showing Ace"))
.font(.system(size: Design.BaseFontSize.medium))
.foregroundStyle(.white.opacity(Design.Opacity.strong))
// Cost info
Text(String(localized: "Cost: $\(betAmount) (half your bet)"))
.font(.system(size: Design.BaseFontSize.small))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
.padding(.bottom, Design.Spacing.small)
// Buttons
HStack(spacing: Design.Spacing.large) {
// Decline button
Button(action: onDecline) {
Text(String(localized: "No Thanks"))
.font(.system(size: Design.BaseFontSize.medium, weight: .semibold))
.foregroundStyle(.white)
.padding(.horizontal, Design.Spacing.xLarge)
.padding(.vertical, Design.Spacing.medium)
.background(
Capsule()
.fill(Color.red.opacity(Design.Opacity.heavy))
)
}
// Accept button (only if can afford)
if balance >= betAmount {
Button(action: onTake) {
Text(String(localized: "Yes ($\(betAmount))"))
.font(.system(size: Design.BaseFontSize.medium, weight: .bold))
.foregroundStyle(.black)
.padding(.horizontal, Design.Spacing.xLarge)
.padding(.vertical, Design.Spacing.medium)
.background(
Capsule()
.fill(
LinearGradient(
colors: [Color.Button.goldLight, Color.Button.goldDark],
startPoint: .top,
endPoint: .bottom
)
)
)
}
}
}
}
.padding(Design.Spacing.xLarge)
.background(
RoundedRectangle(cornerRadius: Design.CornerRadius.xLarge)
.fill(Color.Modal.background)
.shadow(color: .black.opacity(Design.Opacity.medium), radius: Design.Shadow.radiusXLarge)
)
.overlay(
RoundedRectangle(cornerRadius: Design.CornerRadius.xLarge)
.strokeBorder(Color.yellow.opacity(Design.Opacity.light), lineWidth: Design.LineWidth.thin)
)
}
.accessibilityElement(children: .contain)
.accessibilityAddTraits(.isModal)
}
}
// MARK: - Previews
#Preview("Can Afford") {
InsurancePopupView(
betAmount: 500,
balance: 4500,
onTake: {},
onDecline: {}
)
}
#Preview("Cannot Afford") {
InsurancePopupView(
betAmount: 500,
balance: 200,
onTake: {},
onDecline: {}
)
}

View File

@ -1,264 +0,0 @@
//
// PlayerHandView.swift
// Blackjack
//
// Displays player hands in a horizontally scrollable container.
//
import SwiftUI
import CasinoKit
// MARK: - Player Hands Container
/// Container for multiple player hands with horizontal scrolling.
struct PlayerHandsView: View {
let hands: [BlackjackHand]
let activeHandIndex: Int
let isPlayerTurn: Bool
let showCardCount: Bool
let cardWidth: CGFloat
let cardSpacing: CGFloat
/// Total card count across all hands - used to trigger scroll when hitting
private var totalCardCount: Int {
hands.reduce(0) { $0 + $1.cards.count }
}
var body: some View {
GeometryReader { geometry in
ScrollViewReader { proxy in
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: Design.Spacing.large) {
// Display hands in reverse order (right to left play order)
// Visual order: Hand 3, Hand 2, Hand 1 (left to right)
// Play order: Hand 1 played first (rightmost), then Hand 2, etc.
ForEach(hands.indices.reversed(), id: \.self) { index in
PlayerHandView(
hand: hands[index],
isActive: index == activeHandIndex && isPlayerTurn,
showCardCount: showCardCount,
// Hand numbers: rightmost (index 0) is Hand 1, played first
handNumber: hands.count > 1 ? index + 1 : nil,
cardWidth: cardWidth,
cardSpacing: cardSpacing
)
.id(index)
}
}
.padding(.horizontal, Design.Spacing.large)
.frame(minWidth: geometry.size.width)
}
.scrollClipDisabled()
.scrollBounceBehavior(.basedOnSize)
.onChange(of: activeHandIndex) { _, newIndex in
scrollToHand(proxy: proxy, index: newIndex)
}
.onChange(of: totalCardCount) { _, _ in
// Scroll to active hand when cards are added (hit)
scrollToHand(proxy: proxy, index: activeHandIndex)
}
.onChange(of: hands.count) { _, _ in
// Scroll to active hand when split occurs
scrollToHand(proxy: proxy, index: activeHandIndex)
}
.onAppear {
scrollToHand(proxy: proxy, index: activeHandIndex)
}
}
}
.frame(height: Design.Size.playerHandsHeight)
}
private func scrollToHand(proxy: ScrollViewProxy, index: Int) {
withAnimation(.easeInOut(duration: Design.Animation.quick)) {
proxy.scrollTo(index, anchor: .center)
}
}
}
// MARK: - Single Player Hand
/// Displays a single player hand with cards, value, and result.
struct PlayerHandView: View {
let hand: BlackjackHand
let isActive: Bool
let showCardCount: Bool
let handNumber: Int?
let cardWidth: CGFloat
let cardSpacing: CGFloat
@ScaledMetric(relativeTo: .headline) private var labelFontSize: CGFloat = Design.Size.handLabelFontSize
@ScaledMetric(relativeTo: .caption) private var handNumberSize: CGFloat = Design.Size.handNumberFontSize
var body: some View {
VStack(spacing: Design.Spacing.small) {
// Cards with container
HStack(spacing: hand.cards.isEmpty ? Design.Spacing.small : cardSpacing) {
if hand.cards.isEmpty {
CardPlaceholderView(width: cardWidth)
CardPlaceholderView(width: cardWidth)
} else {
ForEach(hand.cards.indices, id: \.self) { index in
CardView(
card: hand.cards[index],
isFaceUp: true,
cardWidth: cardWidth
)
.overlay(alignment: .bottomLeading) {
if showCardCount {
HiLoCountBadge(card: hand.cards[index])
}
}
.zIndex(Double(index))
}
}
}
.padding(.horizontal, Design.Spacing.medium)
.padding(.vertical, Design.Spacing.medium)
.background(
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
.fill(Color.Table.feltDark.opacity(Design.Opacity.light))
.overlay(
RoundedRectangle(cornerRadius: Design.CornerRadius.medium)
.strokeBorder(
isActive ? Color.Hand.active : Color.white.opacity(Design.Opacity.hint),
lineWidth: isActive ? Design.LineWidth.thick : Design.LineWidth.thin
)
)
)
.contentShape(Rectangle())
.animation(.easeInOut(duration: Design.Animation.quick), value: isActive)
// Hand info
HStack(spacing: Design.Spacing.small) {
if let number = handNumber {
Text(String(localized: "Hand \(number)"))
.font(.system(size: handNumberSize, weight: .medium))
.foregroundStyle(.white.opacity(Design.Opacity.medium))
}
if !hand.cards.isEmpty {
Text(hand.valueDisplay)
.font(.system(size: labelFontSize, weight: .bold, design: .rounded))
.foregroundStyle(valueColor)
}
if hand.isDoubledDown {
Image(systemName: "xmark.circle.fill")
.font(.system(size: Design.Size.handIconSize))
.foregroundStyle(.purple)
}
}
// Result badge
if let result = hand.result {
Text(result.displayText)
.font(.system(size: labelFontSize, weight: .black))
.foregroundStyle(result.color)
.padding(.horizontal, Design.Size.hintPaddingH)
.padding(.vertical, Design.Size.hintPaddingV)
.background(
Capsule()
.fill(result.color.opacity(Design.Opacity.hint))
)
}
// Bet amount
if hand.bet > 0 {
HStack(spacing: Design.Spacing.xSmall) {
Image(systemName: "dollarsign.circle.fill")
.font(.system(size: Design.Size.handIconSize))
.foregroundStyle(.yellow)
Text("\(hand.bet * (hand.isDoubledDown ? 2 : 1))")
.font(.system(size: handNumberSize, weight: .bold, design: .rounded))
.foregroundStyle(.yellow)
}
}
}
.accessibilityElement(children: .ignore)
.accessibilityLabel(playerAccessibilityLabel)
}
// MARK: - Computed Properties
private var valueColor: Color {
if hand.isBlackjack { return .yellow }
if hand.isBusted { return .red }
if hand.value == 21 { return .green }
return .white
}
private var playerAccessibilityLabel: String {
let cardsDescription = hand.cards.map { $0.accessibilityDescription }.joined(separator: ", ")
var label = String(localized: "Player hand: \(cardsDescription). Value: \(hand.valueDisplay)")
if let result = hand.result {
label += ". \(result.displayText)"
}
return label
}
}
// MARK: - Previews
#Preview("Single Hand - Empty") {
ZStack {
Color.Table.felt.ignoresSafeArea()
PlayerHandsView(
hands: [BlackjackHand()],
activeHandIndex: 0,
isPlayerTurn: true,
showCardCount: false,
cardWidth: 60,
cardSpacing: -20
)
}
}
#Preview("Single Hand - Cards") {
ZStack {
Color.Table.felt.ignoresSafeArea()
PlayerHandsView(
hands: [BlackjackHand(cards: [
Card(suit: .clubs, rank: .eight),
Card(suit: .hearts, rank: .nine)
], bet: 100)],
activeHandIndex: 0,
isPlayerTurn: true,
showCardCount: false,
cardWidth: 60,
cardSpacing: -20
)
}
}
#Preview("Split Hands") {
ZStack {
Color.Table.felt.ignoresSafeArea()
PlayerHandsView(
hands: [
BlackjackHand(cards: [
Card(suit: .clubs, rank: .eight),
Card(suit: .spades, rank: .jack)
], bet: 100),
BlackjackHand(cards: [
Card(suit: .hearts, rank: .eight),
Card(suit: .diamonds, rank: .five)
], bet: 100),
BlackjackHand(cards: [
Card(suit: .hearts, rank: .eight),
Card(suit: .diamonds, rank: .five)
], bet: 100),
BlackjackHand(cards: [
Card(suit: .hearts, rank: .eight),
Card(suit: .diamonds, rank: .five)
], bet: 100)
],
activeHandIndex: 1,
isPlayerTurn: true,
showCardCount: true,
cardWidth: 60,
cardSpacing: -20
)
}
}

View File

@ -1,17 +0,0 @@
//
// BlackjackTests.swift
// BlackjackTests
//
// Created by Matt Bruce on 12/17/25.
//
import Testing
@testable import Blackjack
struct BlackjackTests {
@Test func example() async throws {
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
}
}

View File

@ -1,41 +0,0 @@
//
// BlackjackUITests.swift
// BlackjackUITests
//
// Created by Matt Bruce on 12/17/25.
//
import XCTest
final class BlackjackUITests: XCTestCase {
override func setUpWithError() throws {
// 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.
continueAfterFailure = false
// In UI tests its 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 {
// Put teardown code here. This method is called after the invocation of each test method in the class.
}
@MainActor
func testExample() throws {
// UI tests must launch the application that they test.
let app = XCUIApplication()
app.launch()
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
@MainActor
func testLaunchPerformance() throws {
// This measures how long it takes to launch your application.
measure(metrics: [XCTApplicationLaunchMetric()]) {
XCUIApplication().launch()
}
}
}

View File

@ -1,33 +0,0 @@
//
// BlackjackUITestsLaunchTests.swift
// BlackjackUITests
//
// Created by Matt Bruce on 12/17/25.
//
import XCTest
final class BlackjackUITestsLaunchTests: XCTestCase {
override class var runsForEachTargetApplicationUIConfiguration: Bool {
true
}
override func setUpWithError() throws {
continueAfterFailure = false
}
@MainActor
func testLaunch() throws {
let app = XCUIApplication()
app.launch()
// Insert steps here to perform after app launch but before taking a screenshot,
// such as logging into a test account or navigating somewhere in the app
let attachment = XCTAttachment(screenshot: app.screenshot())
attachment.name = "Launch Screen"
attachment.lifetime = .keepAlways
add(attachment)
}
}

View File

@ -1 +0,0 @@
59776

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,17 +0,0 @@
import Foundation
extension Foundation.Bundle {
static let module: Bundle = {
let mainPath = Bundle.main.bundleURL.appendingPathComponent("CasinoKit_CasinoKit.bundle").path
let buildPath = "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit_CasinoKit.bundle"
let preferredBundle = Bundle(path: mainPath)
guard let bundle = preferredBundle ?? Bundle(path: buildPath) else {
// Users can write a function called fatalError themselves, we should be resilient against that.
Swift.fatalError("could not load resource bundle: from \(mainPath) or \(buildPath)")
}
return bundle
}()
}

File diff suppressed because one or more lines are too long

View File

@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
</dict>
</plist>

View File

@ -1,311 +0,0 @@
// Generated by Apple Swift version 6.2 (swiftlang-6.2.0.19.9 clang-1700.3.19.1)
#ifndef CASINOKIT_SWIFT_H
#define CASINOKIT_SWIFT_H
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wgcc-compat"
#if !defined(__has_include)
# define __has_include(x) 0
#endif
#if !defined(__has_attribute)
# define __has_attribute(x) 0
#endif
#if !defined(__has_feature)
# define __has_feature(x) 0
#endif
#if !defined(__has_warning)
# define __has_warning(x) 0
#endif
#if __has_include(<swift/objc-prologue.h>)
# include <swift/objc-prologue.h>
#endif
#pragma clang diagnostic ignored "-Wauto-import"
#if defined(__OBJC__)
#include <Foundation/Foundation.h>
#endif
#if defined(__cplusplus)
#include <cstdint>
#include <cstddef>
#include <cstdbool>
#include <cstring>
#include <stdlib.h>
#include <new>
#include <type_traits>
#else
#include <stdint.h>
#include <stddef.h>
#include <stdbool.h>
#include <string.h>
#endif
#if defined(__cplusplus)
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wnon-modular-include-in-framework-module"
#if defined(__arm64e__) && __has_include(<ptrauth.h>)
# include <ptrauth.h>
#else
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Wreserved-macro-identifier"
# ifndef __ptrauth_swift_value_witness_function_pointer
# define __ptrauth_swift_value_witness_function_pointer(x)
# endif
# ifndef __ptrauth_swift_class_method_pointer
# define __ptrauth_swift_class_method_pointer(x)
# endif
#pragma clang diagnostic pop
#endif
#pragma clang diagnostic pop
#endif
#if !defined(SWIFT_TYPEDEFS)
# define SWIFT_TYPEDEFS 1
# if __has_include(<uchar.h>)
# include <uchar.h>
# elif !defined(__cplusplus)
typedef unsigned char char8_t;
typedef uint_least16_t char16_t;
typedef uint_least32_t char32_t;
# endif
typedef float swift_float2 __attribute__((__ext_vector_type__(2)));
typedef float swift_float3 __attribute__((__ext_vector_type__(3)));
typedef float swift_float4 __attribute__((__ext_vector_type__(4)));
typedef double swift_double2 __attribute__((__ext_vector_type__(2)));
typedef double swift_double3 __attribute__((__ext_vector_type__(3)));
typedef double swift_double4 __attribute__((__ext_vector_type__(4)));
typedef int swift_int2 __attribute__((__ext_vector_type__(2)));
typedef int swift_int3 __attribute__((__ext_vector_type__(3)));
typedef int swift_int4 __attribute__((__ext_vector_type__(4)));
typedef unsigned int swift_uint2 __attribute__((__ext_vector_type__(2)));
typedef unsigned int swift_uint3 __attribute__((__ext_vector_type__(3)));
typedef unsigned int swift_uint4 __attribute__((__ext_vector_type__(4)));
#endif
#if !defined(SWIFT_PASTE)
# define SWIFT_PASTE_HELPER(x, y) x##y
# define SWIFT_PASTE(x, y) SWIFT_PASTE_HELPER(x, y)
#endif
#if !defined(SWIFT_METATYPE)
# define SWIFT_METATYPE(X) Class
#endif
#if !defined(SWIFT_CLASS_PROPERTY)
# if __has_feature(objc_class_property)
# define SWIFT_CLASS_PROPERTY(...) __VA_ARGS__
# else
# define SWIFT_CLASS_PROPERTY(...)
# endif
#endif
#if !defined(SWIFT_RUNTIME_NAME)
# if __has_attribute(objc_runtime_name)
# define SWIFT_RUNTIME_NAME(X) __attribute__((objc_runtime_name(X)))
# else
# define SWIFT_RUNTIME_NAME(X)
# endif
#endif
#if !defined(SWIFT_COMPILE_NAME)
# if __has_attribute(swift_name)
# define SWIFT_COMPILE_NAME(X) __attribute__((swift_name(X)))
# else
# define SWIFT_COMPILE_NAME(X)
# endif
#endif
#if !defined(SWIFT_METHOD_FAMILY)
# if __has_attribute(objc_method_family)
# define SWIFT_METHOD_FAMILY(X) __attribute__((objc_method_family(X)))
# else
# define SWIFT_METHOD_FAMILY(X)
# endif
#endif
#if !defined(SWIFT_NOESCAPE)
# if __has_attribute(noescape)
# define SWIFT_NOESCAPE __attribute__((noescape))
# else
# define SWIFT_NOESCAPE
# endif
#endif
#if !defined(SWIFT_RELEASES_ARGUMENT)
# if __has_attribute(ns_consumed)
# define SWIFT_RELEASES_ARGUMENT __attribute__((ns_consumed))
# else
# define SWIFT_RELEASES_ARGUMENT
# endif
#endif
#if !defined(SWIFT_WARN_UNUSED_RESULT)
# if __has_attribute(warn_unused_result)
# define SWIFT_WARN_UNUSED_RESULT __attribute__((warn_unused_result))
# else
# define SWIFT_WARN_UNUSED_RESULT
# endif
#endif
#if !defined(SWIFT_NORETURN)
# if __has_attribute(noreturn)
# define SWIFT_NORETURN __attribute__((noreturn))
# else
# define SWIFT_NORETURN
# endif
#endif
#if !defined(SWIFT_CLASS_EXTRA)
# define SWIFT_CLASS_EXTRA
#endif
#if !defined(SWIFT_PROTOCOL_EXTRA)
# define SWIFT_PROTOCOL_EXTRA
#endif
#if !defined(SWIFT_ENUM_EXTRA)
# define SWIFT_ENUM_EXTRA
#endif
#if !defined(SWIFT_CLASS)
# if __has_attribute(objc_subclassing_restricted)
# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_CLASS_EXTRA
# define SWIFT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_subclassing_restricted)) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
# else
# define SWIFT_CLASS(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
# define SWIFT_CLASS_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_CLASS_EXTRA
# endif
#endif
#if !defined(SWIFT_RESILIENT_CLASS)
# if __has_attribute(objc_class_stub)
# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME) __attribute__((objc_class_stub))
# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) __attribute__((objc_class_stub)) SWIFT_CLASS_NAMED(SWIFT_NAME)
# else
# define SWIFT_RESILIENT_CLASS(SWIFT_NAME) SWIFT_CLASS(SWIFT_NAME)
# define SWIFT_RESILIENT_CLASS_NAMED(SWIFT_NAME) SWIFT_CLASS_NAMED(SWIFT_NAME)
# endif
#endif
#if !defined(SWIFT_PROTOCOL)
# define SWIFT_PROTOCOL(SWIFT_NAME) SWIFT_RUNTIME_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA
# define SWIFT_PROTOCOL_NAMED(SWIFT_NAME) SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_PROTOCOL_EXTRA
#endif
#if !defined(SWIFT_EXTENSION)
# define SWIFT_EXTENSION(M) SWIFT_PASTE(M##_Swift_, __LINE__)
#endif
#if !defined(OBJC_DESIGNATED_INITIALIZER)
# if __has_attribute(objc_designated_initializer)
# define OBJC_DESIGNATED_INITIALIZER __attribute__((objc_designated_initializer))
# else
# define OBJC_DESIGNATED_INITIALIZER
# endif
#endif
#if !defined(SWIFT_ENUM_ATTR)
# if __has_attribute(enum_extensibility)
# define SWIFT_ENUM_ATTR(_extensibility) __attribute__((enum_extensibility(_extensibility)))
# else
# define SWIFT_ENUM_ATTR(_extensibility)
# endif
#endif
#if !defined(SWIFT_ENUM)
# define SWIFT_ENUM(_type, _name, _extensibility) enum _name : _type _name; enum SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type
# if __has_feature(generalized_swift_name)
# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) enum _name : _type _name SWIFT_COMPILE_NAME(SWIFT_NAME); enum SWIFT_COMPILE_NAME(SWIFT_NAME) SWIFT_ENUM_ATTR(_extensibility) SWIFT_ENUM_EXTRA _name : _type
# else
# define SWIFT_ENUM_NAMED(_type, _name, SWIFT_NAME, _extensibility) SWIFT_ENUM(_type, _name, _extensibility)
# endif
#endif
#if !defined(SWIFT_UNAVAILABLE)
# define SWIFT_UNAVAILABLE __attribute__((unavailable))
#endif
#if !defined(SWIFT_UNAVAILABLE_MSG)
# define SWIFT_UNAVAILABLE_MSG(msg) __attribute__((unavailable(msg)))
#endif
#if !defined(SWIFT_AVAILABILITY)
# define SWIFT_AVAILABILITY(plat, ...) __attribute__((availability(plat, __VA_ARGS__)))
#endif
#if !defined(SWIFT_WEAK_IMPORT)
# define SWIFT_WEAK_IMPORT __attribute__((weak_import))
#endif
#if !defined(SWIFT_DEPRECATED)
# define SWIFT_DEPRECATED __attribute__((deprecated))
#endif
#if !defined(SWIFT_DEPRECATED_MSG)
# define SWIFT_DEPRECATED_MSG(...) __attribute__((deprecated(__VA_ARGS__)))
#endif
#if !defined(SWIFT_DEPRECATED_OBJC)
# if __has_feature(attribute_diagnose_if_objc)
# define SWIFT_DEPRECATED_OBJC(Msg) __attribute__((diagnose_if(1, Msg, "warning")))
# else
# define SWIFT_DEPRECATED_OBJC(Msg) SWIFT_DEPRECATED_MSG(Msg)
# endif
#endif
#if defined(__OBJC__)
#if !defined(IBSegueAction)
# define IBSegueAction
#endif
#endif
#if !defined(SWIFT_EXTERN)
# if defined(__cplusplus)
# define SWIFT_EXTERN extern "C"
# else
# define SWIFT_EXTERN extern
# endif
#endif
#if !defined(SWIFT_CALL)
# define SWIFT_CALL __attribute__((swiftcall))
#endif
#if !defined(SWIFT_INDIRECT_RESULT)
# define SWIFT_INDIRECT_RESULT __attribute__((swift_indirect_result))
#endif
#if !defined(SWIFT_CONTEXT)
# define SWIFT_CONTEXT __attribute__((swift_context))
#endif
#if !defined(SWIFT_ERROR_RESULT)
# define SWIFT_ERROR_RESULT __attribute__((swift_error_result))
#endif
#if defined(__cplusplus)
# define SWIFT_NOEXCEPT noexcept
#else
# define SWIFT_NOEXCEPT
#endif
#if !defined(SWIFT_C_INLINE_THUNK)
# if __has_attribute(always_inline)
# if __has_attribute(nodebug)
# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline)) __attribute__((nodebug))
# else
# define SWIFT_C_INLINE_THUNK inline __attribute__((always_inline))
# endif
# else
# define SWIFT_C_INLINE_THUNK inline
# endif
#endif
#if defined(_WIN32)
#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL)
# define SWIFT_IMPORT_STDLIB_SYMBOL __declspec(dllimport)
#endif
#else
#if !defined(SWIFT_IMPORT_STDLIB_SYMBOL)
# define SWIFT_IMPORT_STDLIB_SYMBOL
#endif
#endif
#if defined(__OBJC__)
#if __has_feature(objc_modules)
#if __has_warning("-Watimport-in-framework-header")
#pragma clang diagnostic ignored "-Watimport-in-framework-header"
#endif
#endif
#endif
#pragma clang diagnostic ignored "-Wproperty-attribute-mismatch"
#pragma clang diagnostic ignored "-Wduplicate-method-arg"
#if __has_warning("-Wpragma-clang-attribute")
# pragma clang diagnostic ignored "-Wpragma-clang-attribute"
#endif
#pragma clang diagnostic ignored "-Wunknown-pragmas"
#pragma clang diagnostic ignored "-Wnullability"
#pragma clang diagnostic ignored "-Wdollar-in-identifier-extension"
#pragma clang diagnostic ignored "-Wunsafe-buffer-usage"
#if __has_attribute(external_source_symbol)
# pragma push_macro("any")
# undef any
# pragma clang attribute push(__attribute__((external_source_symbol(language="Swift", defined_in="CasinoKit",generated_declaration))), apply_to=any(function,enum,objc_interface,objc_category,objc_protocol))
# pragma pop_macro("any")
#endif
#if defined(__OBJC__)
#endif
#if __has_attribute(external_source_symbol)
# pragma clang attribute pop
#endif
#if defined(__cplusplus)
#endif
#pragma clang diagnostic pop
#endif

View File

@ -1,3 +0,0 @@
module CasinoKit {
header "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/include/CasinoKit-Swift.h"
}

View File

@ -1,89 +0,0 @@
{
"": {
"swift-dependencies": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/master.swiftdeps"
},
"/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/Sources/CasinoKit/CasinoKit.swift": {
"dependencies": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/CasinoKit.d",
"object": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/CasinoKit.swift.o",
"swiftmodule": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/CasinoKit~partial.swiftmodule",
"swift-dependencies": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/CasinoKit.swiftdeps",
"diagnostics": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/CasinoKit.dia"
},
"/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/Sources/CasinoKit/Exports.swift": {
"dependencies": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/Exports.d",
"object": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/Exports.swift.o",
"swiftmodule": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/Exports~partial.swiftmodule",
"swift-dependencies": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/Exports.swiftdeps",
"diagnostics": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/Exports.dia"
},
"/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/Sources/CasinoKit/Models/Card.swift": {
"dependencies": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/Card.d",
"object": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/Card.swift.o",
"swiftmodule": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/Card~partial.swiftmodule",
"swift-dependencies": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/Card.swiftdeps",
"diagnostics": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/Card.dia"
},
"/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/Sources/CasinoKit/Models/ChipDenomination.swift": {
"dependencies": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/ChipDenomination.d",
"object": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/ChipDenomination.swift.o",
"swiftmodule": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/ChipDenomination~partial.swiftmodule",
"swift-dependencies": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/ChipDenomination.swiftdeps",
"diagnostics": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/ChipDenomination.dia"
},
"/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/Sources/CasinoKit/Models/Deck.swift": {
"dependencies": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/Deck.d",
"object": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/Deck.swift.o",
"swiftmodule": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/Deck~partial.swiftmodule",
"swift-dependencies": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/Deck.swiftdeps",
"diagnostics": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/Deck.dia"
},
"/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/Sources/CasinoKit/Theme/CasinoDesign.swift": {
"dependencies": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/CasinoDesign.d",
"object": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/CasinoDesign.swift.o",
"swiftmodule": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/CasinoDesign~partial.swiftmodule",
"swift-dependencies": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/CasinoDesign.swiftdeps",
"diagnostics": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/CasinoDesign.dia"
},
"/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/Sources/CasinoKit/Theme/CasinoTheme.swift": {
"dependencies": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/CasinoTheme.d",
"object": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/CasinoTheme.swift.o",
"swiftmodule": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/CasinoTheme~partial.swiftmodule",
"swift-dependencies": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/CasinoTheme.swiftdeps",
"diagnostics": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/CasinoTheme.dia"
},
"/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/Sources/CasinoKit/Views/Cards/CardView.swift": {
"dependencies": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/CardView.d",
"object": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/CardView.swift.o",
"swiftmodule": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/CardView~partial.swiftmodule",
"swift-dependencies": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/CardView.swiftdeps",
"diagnostics": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/CardView.dia"
},
"/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/Sources/CasinoKit/Views/Chips/ChipSelectorView.swift": {
"dependencies": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/ChipSelectorView.d",
"object": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/ChipSelectorView.swift.o",
"swiftmodule": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/ChipSelectorView~partial.swiftmodule",
"swift-dependencies": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/ChipSelectorView.swiftdeps",
"diagnostics": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/ChipSelectorView.dia"
},
"/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/Sources/CasinoKit/Views/Chips/ChipStackView.swift": {
"dependencies": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/ChipStackView.d",
"object": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/ChipStackView.swift.o",
"swiftmodule": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/ChipStackView~partial.swiftmodule",
"swift-dependencies": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/ChipStackView.swiftdeps",
"diagnostics": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/ChipStackView.dia"
},
"/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/Sources/CasinoKit/Views/Chips/ChipView.swift": {
"dependencies": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/ChipView.d",
"object": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/ChipView.swift.o",
"swiftmodule": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/ChipView~partial.swiftmodule",
"swift-dependencies": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/ChipView.swiftdeps",
"diagnostics": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/ChipView.dia"
},
"/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/DerivedSources/resource_bundle_accessor.swift": {
"dependencies": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/resource_bundle_accessor.d",
"object": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/resource_bundle_accessor.swift.o",
"swiftmodule": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/resource_bundle_accessor~partial.swiftmodule",
"swift-dependencies": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/resource_bundle_accessor.swiftdeps",
"diagnostics": "/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/resource_bundle_accessor.dia"
}
}

File diff suppressed because one or more lines are too long

View File

@ -1,12 +0,0 @@
/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/Sources/CasinoKit/CasinoKit.swift
/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/Sources/CasinoKit/Exports.swift
/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/Sources/CasinoKit/Models/Card.swift
/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/Sources/CasinoKit/Models/ChipDenomination.swift
/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/Sources/CasinoKit/Models/Deck.swift
/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/Sources/CasinoKit/Theme/CasinoDesign.swift
/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/Sources/CasinoKit/Theme/CasinoTheme.swift
/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/Sources/CasinoKit/Views/Cards/CardView.swift
/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/Sources/CasinoKit/Views/Chips/ChipSelectorView.swift
/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/Sources/CasinoKit/Views/Chips/ChipStackView.swift
/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/Sources/CasinoKit/Views/Chips/ChipView.swift
/Users/mattbruce/Documents/Projects/iPhone/Baccarat/Baccarat/CasinoKit/.build/arm64-apple-macosx/debug/CasinoKit.build/DerivedSources/resource_bundle_accessor.swift

Some files were not shown because too many files have changed in this diff Show More