new projects
Signed-off-by: Matt Bruce <mbrucedogs@gmail.com>
This commit is contained in:
parent
fa5d9f4c75
commit
7759edfcd2
@ -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 */ = {
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 app’s 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 tap’s 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`.
|
||||
- Don’t 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.
|
||||
|
||||
@ -1,11 +0,0 @@
|
||||
{
|
||||
"colors" : [
|
||||
{
|
||||
"idiom" : "universal"
|
||||
}
|
||||
],
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 1.3 MiB |
@ -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
|
||||
}
|
||||
}
|
||||
@ -1,6 +0,0 @@
|
||||
{
|
||||
"info" : {
|
||||
"author" : "xcode",
|
||||
"version" : 1
|
||||
}
|
||||
}
|
||||
@ -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>
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.*
|
||||
|
||||
@ -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>
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
@ -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
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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)) {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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: {}
|
||||
)
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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()))
|
||||
}
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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: {}
|
||||
)
|
||||
}
|
||||
|
||||
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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.
|
||||
}
|
||||
|
||||
}
|
||||
@ -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 it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
|
||||
}
|
||||
|
||||
override func tearDownWithError() throws {
|
||||
// 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()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
@ -1 +0,0 @@
|
||||
59776
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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>
|
||||
@ -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
|
||||
@ -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"
|
||||
}
|
||||
Binary file not shown.
@ -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
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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
Loading…
Reference in New Issue
Block a user