Changelog
Development progress and implementation history for True Count.
Track what has been built, what is in progress, and what changed with each development milestone. Entries are added as the app development agent reports progress via GitHub Issues labeled dev-progress.
2026-03-25: S-IAP — IAP Credit Safety (Plans A–E)
Complete purchase protection and customer support pipeline. Five plans implemented in a single session:
Plan A — Transaction Safety: Idempotent credit delivery via ProcessedTransaction receipt ledger. Crash recovery on launch via Transaction.unfinished. Atomic saves (bankroll + receipt in one modelContext.save()).
Plan B — Refund Handling: Transaction.updates listener detects revocations and reverses credits atomically. Marks receipts as revoked to prevent double-reversal.
Plan C — CloudKit Bankroll Backup: BankrollBackupService writes bankroll to CloudKit public database on purchases, background, and every 5 minutes. On reinstall, restores bankroll in seconds (vs. waiting for SwiftData sync). Admin bankroll edits detected via timestamp comparison.
Plan D — Purchase Support Tooling: Purchase History section in Settings (transaction log, Support ID, Verify Purchases reconciliation, backup status). CreditGrantService polls for remote credit grants on launch/foreground/refresh. Silent push via CKQuerySubscription + AppDelegate. Bankroll display + refresh button in Store screen. grant-credits.sh CLI script.
Plan E — Admin Panel: Next.js + shadcn/ui web app in admin/ directory. Dashboard, Users list, User detail (editable bankroll), Grants list with status filter, New Grant form, Delete All Grants. Environment switcher (Dev/Prod) with confirmation dialogs. CloudKit REST API with ECDSA server-to-server key signing.
Infrastructure: CloudKit entitlements, Push Notifications capability, remote-notification background mode, declarative schema (cloudkit/CloudKitSchema.ckdb), INFRASTRUCTURE.md documenting all cloud services and migration checklist.
PR: #90
2026-03-23: Fix — Per-Hand Split Payout Animation and Banner
Customer-reported fix: split hand payouts now animate independently to each hand's chip stack position instead of collapsing into a single combined stack at screen center. The result banner shows per-hand outcomes for all split rounds.
What shipped:
- Per-hand chip payout animation: win chips fly to that hand's position, loss chips sweep from that hand, push chips bounce in place — both hands animate in parallel
splitResultbanner case showing H1/H2 outcomes with color-coded per-hand amounts (green wins, red losses) and net totalHandPayoutInfodata pipeline threading per-hand payouts from engine → ViewModel → Scene- Bet total labels now fade out when chips start moving during payout
- Fixed pre-existing bug where debug deals from bet screen left action buttons hidden (
isBettingModestuck)
PR: #87
2026-03-23: S06/S22 — Swipeable Multi-Tier Chip Rack
Added a two-page swipeable chip rack so players can always bet the table minimum regardless of bankroll size. Previously, high-bankroll players at tables like Lady Lux (25K min) only saw Tier 6 chips (500K–10M) and couldn't place the minimum bet.
What shipped:
BettingMath.chipPages(forBankroll:tableMinimum:)— new engine API returning 1–2 pages of chip denominations spanning from min-bet tier to bankroll tier- Paged chip rack with drag gesture, spring animation, and clamped offset in
BettingModeOverlay - Dynamic chip sizing when paged — chips shrink slightly so all 5 fit on screen with a left-edge peek hint for swipe discoverability
- Scene chip breakdown uses flattened multi-page denominations for accurate mixed-tier bet visuals
- 140+ lines of new
BettingMathtests covering all 5 dealers and edge cases
2026-03-20: Animation — Card Flip Refinement
Refined the card flip animation based on user feedback that the bounce "looks silly." Iteratively tuned on simulator to find the right balance of snap energy without rubbery wobble.
What shipped:
- Removed rotation tilt and horizontal drift during flip (no more diagonal peel)
- Reduced lift height from 14pt to 7pt for subtler vertical movement
- Halved bounce intensity: xScale overshoot 6%→3%, undershoot 2%→1%, vertical bounce removed
- Custom 4-segment timing curve: 5% slow start → 5% ramp → 85% fast → 5% brake
- Applied custom timing to both flip halves for a cohesive whip-and-stick feel
PR: #86
2026-03-20: UI — Glass Button Pressability Enhancement
Enhanced glass action buttons (hit/stand/double/split) to feel more physically pressable, targeting an older audience while keeping the existing design.
What shipped:
- Press-responsive drop shadow that collapses on tap to simulate physical button depression
- Top-edge white gradient highlight for raised 3D look
- Expanded invisible tap targets (8pt beyond visual bounds) via contentShape
- Corner radius increased from 7 to 16pt for softer, more button-like shape
- Synced hint breathing animation: button scales up + icon brightens on inhale, shrinks + dims on exhale
- Action tap haptic upgraded from light to medium impact for stronger tactile feedback
PR: #85
2026-03-20: Visual — Reduce Table Background Grain
Re-exported all 5 dealer table backgrounds from Figma with reduced grain/noise texture and baked-in vignette for a cleaner felt look.
What shipped:
- All 5 table background PNGs (green, bourbon, dark, blue, purple) replaced with updated Figma exports
- Reduced grain/noise texture across all dealer tables
- Baked-in vignette from Figma combined with existing programmatic vignette for stronger depth
- No code changes — pure asset swap
PR: #84
2026-03-20: UI — Speech Bubble Hand Value Badge with A/B Toggle
Added a new translucent speech bubble style for hand value badges as an alternative to the ornate golden teardrop, with a debug picker to switch between styles.
What shipped:
- Translucent white speech bubble badge (rounded rect + triangular tail) as new default hand value indicator
- Single hand: badge positioned to the right of cards (same side as dealer) with left-pointing tail
- Split hands: badge positioned above cards with downward-pointing tail, 25% taller for readability
- Debug picker in Settings > Scene to A/B toggle between speech bubble and teardrop styles at runtime
- Original golden teardrop style fully preserved and selectable via debug panel
PR: #81
2026-03-20: UI — Auto Deal Settings Toggle & Debug Hide Button
Added Auto Deal toggle to Settings for controlling auto-deal without table buttons, plus a debug toggle to hide the auto-deal side button from the table.
What shipped:
- "Auto Deal" toggle in Settings → Gameplay section, bound to existing persisted setting
- "Hide Auto Deal Button" debug toggle in Settings → Debug → Scene (defaults ON)
- Auto-deal controllable entirely from Settings — testing cleaner table UI without side buttons
- BettingModeOverlay auto-deal button also hidden when debug toggle is active
PR: #80
2026-03-20: UI — Stats Button Vertical Stack & Icon Update
Moved the stats button from beside settings to vertically stacked below it, and replaced the SF Symbol with a custom Figma-exported icon matching the design.
What shipped:
- Stats button vertically stacked below settings button (VStack layout)
- Custom SVG stats icon exported from Figma (node 123:9), added as template image
- Top-aligned bankroll pill with settings row, dealer avatar offset for visual balance
- Removed unused glassCircleButton convenience overload (dead code cleanup)
PR: #79
2026-03-19: S29 — Daily Challenge UI, S-DD — Dealer Selector Design, Layout & Typography Overhaul
Shipped daily challenge UI with SpriteKit animations, dealer oval portrait on the betting screen, dealer card carousel redesign, full Roboto font migration, and comprehensive layout tuning across betting and gameplay screens.
What shipped:
- Daily challenge SpriteKit card deal/clear animations, challenge prompt overlay, result overlay, top bar, and reward ad screen
- Dealer oval portrait with gold border on betting screen (SKCropNode + aspect-fill, fade-in/out animations)
- Dealer selection carousel redesigned with card-style layout and 5 dealer portrait assets
- Full typography migration from PlayfairDisplay/Inter to Roboto family (bankroll pill, bet bubble, teardrop badges, minimum bet, buttons, splash screen)
- Betting screen layout refinements: tighter vertical spacing, "Place your bet" auto-hides when chips placed, adjusted chip/text/button positions
- Gameplay layout tuning: dealer cards, player cards, and chip stack positions refined for better visual balance
- GameTableView refactored into composable ViewModifiers (GameTableObservers, DebugActionObserver, ChallengeAndTopBarOverlay)
- Auto-deal button Y-position fixed to reference shared scene constant instead of stale magic number
- Feature complete: daily-challenge (S18, S29)
PR: #76
2026-03-18: Professional Game Engine Audit
Added a casino-grade audit suite for the TrueCountEngine blackjack engine, modeled after GLI-11/eCOGRA professional gaming lab standards. 76 tests across 8 categories — all passing. Published a two-page audit section on the docs site: an explainer covering what we test and why, and a full audit report with results, statistical methodology, and source code.
What shipped:
TrueCountEngineAudittest target with 8 audit categories: RNG evaluation, deck integrity, rule compliance, payout accuracy, statistical fairness, basic strategy validation, Hi-Lo counting, and round lifecycle integrity- Statistical tests (chi-square, correlation, runs, proportion) with alpha = 0.001 significance level
- House edge convergence verified over 500K hands per table against Wizard of Odds reference values
- 280 basic strategy cells verified cell-by-cell against published charts
- Docs: Game Engine Audit explainer and Audit Report with collapsible source code sections
PR: #75
2026-03-18: Leaderboard Avatars and Game Center UX
Added Game Center player avatars to leaderboard rows and improved the Game Center experience for both authenticated and unauthenticated users.
What shipped:
- Player photos loaded concurrently via TaskGroup during leaderboard fetch, displayed as 32pt circular avatars
- Game Center dashboard accessible via profile button in stats toolbar (uses GKAccessPoint)
- Apple HIG-compliant sign-in guidance for unauthenticated users (directs to Settings › Game Center)
- Configured leaderboard metadata (display names, score suffixes) in App Store Connect
PR: #74
2026-03-18: S31 — Settings Screen Implemented, Beta Tweaks Removed
Marked S31 as implemented. Beta Tweaks removed from scope — it remains as an internal debug tool in code (#if DEBUG || DEV_BUILD) but is no longer a documented user-facing feature. The Beta Tweaks doc page and all references have been removed. Feature complete: settings (S19, S31).
What shipped:
- Removed
beta-tweaks.mdxfeature page - Removed Beta Tweaks references from Rules Reference, Features index, Home, and feature metadata
- Renamed S31 from "Beta Tweaks and Settings Screens" to "Settings Screen"
- Updated Progress counters: 32 implemented, 0 in progress, 4 not started
2026-03-18: Docs — Documentation Audit and Sync with Codebase
Audited all 22 documentation pages against the current codebase to identify and fix discrepancies where code decisions had diverged from docs. Code is treated as source of truth.
What shipped:
- Fixed Dealer Tables: Jack's hints corrected from "off by default" to "on by default" (code default includes Jack)
- Fixed Settings: Auto Hint default corrected from "Off" to "On", added missing Sound Effects and Auto-Deal toggles to overview table
- Fixed Auto-Deal: Removed hardcoded "1.2 seconds" delay (code uses 240ms)
- Fixed Beta Tweaks: Updated access location from betting screen to Settings > Debug section
Still open: Split limit discrepancy (docs say 4 hands, code allows 2) tracked in issue #62 — pending human decision.
2026-03-18: Fix — Dealer Cards Missing on Hit to 21
When a hit brought the player to exactly 21 (e.g., 4+6+A), the game auto-stood and showed the result banner without animating the dealer's hole card flip or drawn cards. The dealer turn animation was skipped entirely.
What shipped:
- Fixed
.hitcase inBlackjackTableSceneto chain intoanimateDealerTurnwhen the round auto-completes on 21 - Matches the pattern already used by
.doubleDown,.split, and.standcases - Added debug scenario "Deal 4,6 vs 5,5 (Hit to 21)" to reproduce and verify the fix
PR: #73
2026-03-18: Refactor — XP Progression Rebalance
Replaced the linear XP formula with a compressed power-law formula to prevent single large bets from causing massive level jumps. A 50K bet loss previously jumped from Level 1 to Level 62 — now it stays at Level 1. Min-bet XP is virtually unchanged.
What shipped:
- Compressed XP formula:
minimumXP + outcomeWeight × bet^0.35 × 1.5replaces linearxpPerUnit × bet - Participation floor ensures every hand earns meaningful XP (3-15 XP regardless of bet)
- Named tuning constants (
betScalingExponent,betScalingCoefficient) for future balance adjustments - Streak multiplier reduced from max 3.0x to 2.0x, spread across 5 tiers instead of 4
- Bet incentive ratio compressed from 10,000x to ~12x between min and max bets
- Level curve unchanged (
100 × N^1.5) — zero migration needed, existing player levels preserved - 5 new test suites: bet compression, min XP, single-hand max level, monotonicity, updated streaks
Docs updated: Player Rank, Level Progression Audit, Progress — S09
PR: #72
2026-03-17: S33 — Game Center Dashboard Implemented
Implemented Game Center leaderboard dashboard with monthly and all-time scores, redesigned the top navigation bar with gold liquid glass styling, and replaced Morach branding with Monarch Games.
What shipped:
- Game Center leaderboard integration with
LeaderboardViewModel, parallel score fetching, and monthly/all-time segmented picker (monthly default) - Top nav bar redesign: gold accent icons, clear liquid glass on settings/stats/+ buttons
- Monarch Games logo on splash screen (exported from Figma, replaces Morach)
- Settings, stats, and store accessible during card deal animations
- Dealer avatar shows error banner "Can't change table during a hand" instead of being disabled
- Error banner repositioned and widened for better visibility
- Code quality: extracted
goldAccentcolor constant, movederrorReporterout of View per SOLID/DIP
PR: #71
2026-03-10: Fix — Split Hand Freeze and Chip Visibility
Fixed app freeze when splitting face cards, added chip stack visualization for split hands, and limited splits to once (max 2 hands) based on customer feedback.
What shipped:
- Fix app freeze when splitting Queens — animation completion was missing when round ended during split
- Split chip stacks with per-hand bet bubbles so both hands show their wager
- Limit to one split (max 2 hands) to keep UI manageable
- Fix orphaned split chips after push outcome with merge-to-center animation
- Locale-aware number formatting on bet bubbles (e.g. "1,000" instead of "1000")
- $100M debug bankroll button and Deal Split Qs debug action
PR: #70
2026-03-10: UX — Dealer Total Badge on Blackjack Reveal
Show the dealer's teardrop total badge during the blackjack hole card reveal so the player can follow the count up to 21 before the result banner appears.
What shipped:
- Dealer upcard badge (e.g. "10") appears before the hole card flips
- Badge updates to "21" after flip with a brief pause before "Dealer Wins" banner
- Applies to both 10-value upcard and Ace upcard dealer blackjack paths
PR: #69
2026-03-09: Debug — Dealer Blackjack Peek Simulation
Added debug buttons to test both dealer blackjack peek paths (10-value upcard and Ace upcard), with insurance toggle awareness. Refactored existing debug deal methods to share a common helper.
What shipped:
- "Deal Dealer BJ (10 Up)" debug button — King upcard + Ace hole, silent peek, hand ends immediately
- "Deal Dealer BJ (Ace Up)" debug button — Ace upcard + King hole, respects user's insurance setting
- Shared
debugDealWithCardshelper replacing duplicated deal setup logic indebugDealInsurance
PR: #68
2026-03-09: S32 — Store Screen Implemented
Implemented Store Screen with IAP product list, post-purchase auto-dismiss, mock reward ad with 3-2-1 countdown for Quick Chips, and bankroll sync on store dismiss. Feature complete: credits-and-pricing (S16, S32).
What shipped:
- StoreKit 2 IAP product list with Budget Packs, Ad-Free upgrade, and Premium Packs sections
- Purchase flow with auto-dismiss on success and proper error handling
- Mock reward ad screen (black fullscreen, 3-2-1 countdown, X button) gating Quick Chips free credits
- Ad-free removes gameplay ads only — reward ad for free credits always shown
- Bankroll sync scoped to store sheet dismiss to prevent reset bankroll regression
- Restore Ad-Free Purchase button (hidden when already ad-free)
PR: #67
2026-03-09: S30 — Statistics Screen Implemented
Statistics screen with daily/weekly/monthly bar charts, lifetime metrics, rates, and win streaks. Feature complete: statistics (S12, S15, S30).
What shipped:
- New
DailyStatisticsSwiftData model for per-day granularity alongside existing monthly stats - Statistics screen with segmented Daily/Weekly/Monthly picker above SwiftUI Charts bar chart
- Lifetime overview section (10 metrics), rates section (7 computed rates), and win streaks
- Stats button in top navigation bar using existing
glassCircleButtonpattern - Weekly aggregation computed from daily data using ISO week bucketing
- Value-type snapshots (
DailySnapshot,MonthlySnapshot) decoupling views from live SwiftData models
PR: #66
2026-03-09: S25 — Insurance UI Implemented
Added insurance prompt overlay that appears after deal when dealer shows Ace, with chip fade animations and balance deduction on accept.
What shipped:
- Blocking "INSURANCE?" prompt overlay with Accept/Decline buttons after deal animation
- Chips and "INSURANCE PAYS 2 TO 1" ribbon fade out during prompt, fade back in after decision
- Accepting insurance deducts the side bet cost from displayed bankroll
- Settings toggle activated (no longer disabled/"Coming soon")
- Debug panel "Deal Insurance Hand" button for testing
- Animated dealer peek/hole card reveal after insurance decision
PR: #65
2026-03-09: Animate Cards Off Table After Settlement
After chip payout/bounce completes, dealt cards now slide upward off-screen with staggered timing before the next deal or betting mode transition begins.
What shipped:
- New
animateCardDismissal()in BlackjackTableScene — cards slide up in reverse deal order (0.04s stagger, 0.30s move, easeIn) - Dual-signal gate in GameViewModel: next action waits for both banner dismissal and card-dismiss completion
- Handles all outcomes: win, loss, push, surrender, blackjack, and split hands
- Guards
animateEnterBettingModeagainst already-cleared cards to prevent ghost flashes
PR: #64
2026-03-06: Fix Dealer Badge Animation Desync
Fixed the dealer hand-value teardrop where the number and shape animated separately when new cards arrived. Position now slides with the card fan (0.1s easeInOut), value snaps instantly after flip.
PR: #62
2026-03-06: Fix Bet Bubble Position Jump on Deal
Fixed the bet bubble (chip count label) jumping ~2px right when transitioning from betting to game screen. Animation targets now use the same dynamic offset as the resting position.
PR: #61
2026-03-06: Unified Auto-Deal Button as Small Pill
Replaced the full-size AUTO CasinoButton with a compact side button matching the Stop Auto Deal style, consistent across both betting and game screens.
What shipped:
- Extracted shared
autoDealSideButtonhelper in GameTableView for start/stop reuse - Betting screen: small "START AUTO DEAL" pill pinned left, Clear/Deal stay centered via ZStack layout
- Game screen: small "START AUTO DEAL" pill floats left of chip stack (same position as Stop Auto Deal)
- Removed full-size AUTO button from both ActionButtonsOverlay and BettingModeOverlay HStacks
- Deduplicated bet validation using
BettingMath.isValidBet
PR: #60
2026-03-06: S22 — Betting Screen Table Rules & Plain Text Labels
Refined the betting screen so table rules stay visible during betting mode instead of sliding off-screen, and replaced arced betting text with plain white labels.
What shipped:
- Table rules (payout, dealer rule, insurance) animate upward to stay visible during betting mode
- Replaced arced CoreText "PLACE YOUR BETS" / "MINIMUM BET" with plain white
SKLabelNodetext ("Place your bet" / "Minimum bet X") - Consolidated
minimumBetContainerintoplaceYourBetsContainer, eliminating ~100 lines of duplicate node lifecycle - Extracted
ruleContainerTargets(centerFraction:)helper to share position math between snap and animated transitions - Betting-mode layout: rules at top (76%), chips in middle (55%), bet text below chips (41%)
PR: #59
2026-03-06: Instant Bankroll Deduction on Deal
Bankroll display now drops by the bet amount immediately when the player taps Deal, instead of staying at the full balance until round settlement.
What shipped:
displayedBankrollset tobankroll - amountinplaceBet()for instant visual feedback- Existing
numericText()content transition animates the digit roll-down automatically - Settlement paths (
showBanner,advanceUntilInputNeeded) still reconcile the final balance correctly
PR: #57
2026-03-06: Debug Panel Moved to Settings Screen
Moved the floating debug panel (hidden behind a 5-second long-press on the settings gear) into the Settings screen as native iOS Form sections, making it discoverable and consistent with the rest of the settings UI.
What shipped:
- Debug section in Settings below Reset with native Form styling: status display, bankroll overrides ($1K/$1M), level picker (1-5), shuffle, deal split 8s, deal re-split, slow motion toggle, error testing (StoreKit/Game Center/Persistence)
- Scene-dependent actions (shuffle, deal pairs) dismiss the settings sheet first, then execute via
PendingDebugActionobserved byGameTableView - Buttons disabled when a round is active to prevent game state corruption
- Deleted old
DebugPanel.swiftand removed 5-second long-press gesture fromTopNavigationBar - All debug code remains
#if DEBUG— zero impact on release builds
PR: #56
2026-03-05: Minimum Bet Gate on Table Switch
Added minimum bet enforcement when switching dealer tables, automatic betting mode entry when lastBet becomes invalid, and instant bankroll display sync on round settlement.
What shipped:
canDealvalidateslastBet >= minBet— no more silent Deal button failuresselectTable()clears lastBet when below new table minimum, fully stops auto-deal- Two-phase betting mode signal: ViewModel flags pending entry → ContentView fires on sheet dismiss → GameTableView enters betting mode (no more animations running behind sheets)
- Auto-enter betting mode after round loss when lastBet exceeds bankroll
- "MINIMUM BET XXX" arced text on betting screen (same font/style as "DEALER MUST STAND ON ALL 17'S")
.gameplay(String)error toast when bankroll < table minimumdisplayedBankrollupdates instantly when result banner appears (no delayed subtraction)enterBettingMode()resets pendingBet when lastBet > bankroll or < minBet- Reset Game confirmation dialog anchored to the button
- Same-table switch guard (no-op)
- 6 new unit tests for minimum bet gate logic
PR: #55
2026-03-05: S28 — Count Peek Easter Egg Implemented
Implemented the Count Peek easter egg with force-press detection in SpriteKit, Liquid Glass overlay showing Hi-Lo card counting metrics, and unit tests. Also fixed the double-down dealer animation bug, hardened the debug panel during active rounds, and added several UI improvements.
What shipped:
- Count Peek overlay: force-press on table felt reveals running count, decks remaining, true count, and player edge with Liquid Glass styling and iOS 18 fallback
- Force touch detection handled entirely in SpriteKit touch methods (avoids SwiftUI gesture interference with SpriteView)
- Fixed double-down dealer animation: dealer hit cards and total now display after player doubles and wins
- Debug panel protected during active rounds with error toast ("Finish the hand first")
- Avatar/table-change button disabled and faded during active hand
- Portrait-only orientation lock
- 4 count peek unit tests (snapshot before/during/after rounds, reset on table switch)
PR: #54
2026-03-05: S24 — Auto-Hint UI Implemented
Implemented the auto-hint system with button pulse animation, per-table toggle, strategy fallback chain, and double-down chip animation. Changed hints from always-on to optional at Jean-Luc and Czarita tables.
What shipped:
- CasinoButton hint pulse animation (subtle dark overlay breathing effect)
- Per-table auto-hint toggle stored in UserSettings (CloudKit-compatible comma-separated format)
- Hints default to ON for new players at all hint-available tables (Jean-Luc, Czarita, Jack)
- Strategy fallback chain: Double->Hit, Split->recommendationIgnoringPairs()->cascade
- Double-down chip animation showing bet being doubled with staggered chip entry
BasicStrategy.recommendationIgnoringPairs()API for split fallback lookups- Accessibility: hinted buttons announce "Recommended" via VoiceOver
PR: #53
2026-03-05: S23 — Accessibility Layer Deferred to Phase F
S23 (Accessibility Layer) moved from Phase D to Phase F. Doing accessibility now would mean instrumenting only the current surfaces, then repeating the work for every Phase E screen. Instead, S23 will run as a single comprehensive pass after all UI screens exist.
What changed:
- S23 moved from Phase D to Phase F
- Dependencies expanded from S20/S21 to all 11 UI-producing slices (S20–S33)
- Scope broadened to cover every screen: SpriteKit scene, game shell, betting, auto-hint, insurance, count peek, daily challenge, statistics, settings, store, and Game Center
- Phase D now fully implemented (3/3 slices complete)
- Phase F renamed to "Design & Polish" to reflect accessibility addition
2026-03-05: S31 — Settings Screen UI (In Progress)
Built the Settings screen UI with native iOS Form — Feedback, Gameplay, Rules, and Reset Game sections. Insurance and Auto Hint shown as Coming Soon. Beta Tweaks deferred.
What shipped:
- Native iOS Settings-style Form with 4 grouped sections (Feedback, Gameplay, Rules, Reset)
- Haptic Feedback, Sound Effects, Hand Count toggles fully functional with SwiftData persistence
- Dealer on Soft 17 segmented picker (Stands/Hits) with house edge footer note
- Conditional Auto Hint / Insurance rows: disabled "Coming Soon" at supported tables, hidden at unsupported tables
- Reset Game with confirmation dialog and auto-dismiss
- GameViewModel wired to SettingsViewModel via lazy-init pattern
- New Phase F slice S-DS added for future design refinement of the settings screen
PR: #52
2026-03-05: S21-D — Hand Count, Auto-Deal Controls, and S21 Completion
Completed the final S21 sub-plan with hand count display, auto-deal controls on both the action bar and betting screen, and stop auto-deal button. S21 (SwiftUI App Shell) is now fully implemented.
What shipped:
- Hand count display below deck on right side, resets on shuffle and table switch (per-shoe tracking)
- Deal + Auto Deal buttons on action bar and betting screen
- Stop Auto Deal button (50% scaled red button with 3-line "STOP / AUTO / DEAL" label) left of chip stack
- Auto-deal delay reduced to 240ms; deal buttons hidden during auto-deal
- CasinoButton icon centering fix when label is empty
- GameTableView body extracted into helpers to fix Swift compiler complexity error
- ErrorReporter changed to concrete type for @Observable tracking through SwiftUI environment
- Debug panel shows all controls regardless of game phase; shuffle resets hand count
PR: #51
2026-03-05: Bugfix — Shuffle Animation Regression
Fixed S22 regression where tapping Shuffle caused the shoe and all cards to disappear off-screen. The riffle animation was running invisibly because the shoe node was being slid away with the rest of the table content.
What shipped:
- Introduced
shuffleSlideNodescomputed property that excludes shoe/cover overlay from the slide-out list - Shoe now stays stationary as the visual anchor during the riffle animation (restoring S20-F behavior)
- Hand-value badges (teardrop totals) now clear before shuffle starts instead of lingering on screen
- Removed unused
allTableNodesproperty (dead code after fix)
PR: #50
2026-03-05: S21-E — Error Banner with Liquid Glass Styling
Non-blocking error banner that surfaces StoreKit, Game Center, and persistence failures as a toast at the top of the game screen without interrupting gameplay.
What shipped:
ErrorBannerViewwith Liquid Glass.clearcapsule on iOS 26,.ultraThinMaterialfallback on iOS 18- Auto-dismiss after 4 seconds using
.task(id:)pattern with identity-guardedclear()to prevent stale timers - SF Symbol icons and display titles added to
AppErrorenum (title,iconNamecomputed properties) .sensoryFeedback(.warning)haptic and VoiceOver.announcementon banner appear- Debug panel: 3 error trigger buttons (SK Err, GC Err, DB Err) for visual testing
PR: #49
2026-03-05: Level System Audit — Comprehensive Testing and Documentation
Full audit of the XP/level system with 30 new tests, player progression simulations, and stress testing of debug panel edge cases. Confirmed Level 62 from a single debug-inflated hand is by-design behavior, not a bug.
What shipped:
- 18 engine-level tests: bug scenario reproduction, normal player simulations (conservative/aggressive/losing streak), level boundary verification (all 100 levels), win streak multiplier analysis, table unlock economics
- 12 ViewModel-level tests: debug panel edge cases (high bankroll + low level, extreme values, persistence)
- Fixed pre-existing
DoubleTestsname collision inBettingMathTests.swift - New docs page: Level Progression Audit with real simulation data, progression tables, and stress test results
- Audit report:
docs/plans/2026-03-05-level-system-audit-report.md
PR: #48
2026-03-05: S21-D — Level Badge on Dynamic Island, XP Progress Ring, and Splash Screen
Level badge with XP progress arc positioned on a fake extended Dynamic Island capsule, branded splash screen, and nav bar cleanup with dead code removal.
What shipped:
- LevelBadgeView showing "LVL" + current level on a black capsule that extends the Dynamic Island
- Gold/amber XP progress arc (
Capsule().trim) with smooth.easeOutanimation, powered byPlayerRank.levelProgress - Branded splash screen with "TRUE COUNT" title and Morach Games logo, 2s display then fade out
- Status bar and persistent system overlays hidden for immersive full-screen gameplay
- Nav bar cleanup: removed stats button, replaced with settings gear icon, removed dead
onStatsTapplumbing and unused method overloads
PR: #47
2026-03-04: S21-C — Dealer Selector Sheet, Debug Panel Cleanup, and Bug Fixes
Native UIKit dealer selector sheet with avatars, table info, and level-gated unlocks. Debug panel simplified and critical SwiftUI gesture-blocking SpriteKit touches bug fixed.
What shipped:
- Dealer selector sheet with native iOS list: dealer avatars, names, table rule summaries, and level-gated lock states
- Debug panel simplified: removed player action buttons (hit/double/stand/surrender), kept bankroll overrides, shuffle, split deals, and level milestone buttons
- Debug panel trigger changed from triple-tap to 5-second long press using
.simultaneousGesture - Fixed SwiftUI
.onLongPressGestureblocking ALL SpriteKit touch events — replaced with.simultaneousGesture(LongPressGesture(...)) - Fixed chip denomination text overflow in
ChipButtonView(SwiftUI) — extracted sharedChipNode.denominationFontSize(for:chipDiameter:)static method - Removed stale
!chipNodes.isEmptyguard that blocked chip stack taps after round completion
PR: #46
2026-03-04: Round-End Result Banner and Chip Payout Animations
Directional chip payout animations and round-end result banner with auto-dismiss, plus architectural review fixes for animation lifecycle, layout deduplication, and outcome classification.
What shipped:
- Round-end result banner overlay (BUST!, YOU WON +500, PUSH, DEALER WINS, SURRENDER) with auto-dismiss timer and tap-to-dismiss
- Directional chip payout animations: dealer chips slide down on wins, player chips sweep up off-screen on losses, bounce in place on push
- Standing bet chips restore from bottom of screen after payout clears
- Dealer hole card flips before banner on non-stand completions (bust, blackjack)
- Bankroll count-up animation in nav bar after banner dismisses
- Shared SplitHandLayout utility eliminating duplicated Scene/View layout functions
- Consolidated net-outcome classification via RoundResult.PayoutOutcome (single source of truth)
- Keyed SpriteKit actions preventing orphaned chips on rapid Deal tap
- 17 new RoundResult unit tests covering all single-hand and split-hand outcome branches
PR: #45
2026-03-04: Hand Value Badges — Teardrop Indicators for Dealer and Player Cards
Animated teardrop-shaped badges showing hand totals next to dealer and player card fans, with SpriteKit-to-SwiftUI coordinate bridging and animation-synced state updates.
What shipped:
- HandValueBadgeView with TeardropShape (InsettableShape) and golden casino-style border matching CasinoButton design
- SpriteKit-to-SwiftUI coordinate mapping via shared layout fractions with safe area compensation
- Scene-driven badge state: values update on card flip completion callbacks, not roundState mutations
- BadgeState struct replacing anonymous tuples and 3 scattered dealer vars, with Equatable for clean animations
- Hand total computation delegated to TrueCountEngine.Hand (eliminated duplicated arithmetic in scene)
- Native shape mirroring for player badge (avoids scaleEffect text inversion)
- Avatar placeholder button in top navigation bar
PR: #44
2026-03-04: Split Hand UI — Dynamic Scaling, Arrow Indicator, and Re-Split Support
Split hand visual system for up to 4 hands via re-splits, with scale-aware animations and active hand indication.
What shipped:
- Dynamic card scaling based on hand count (1.0 → 0.7 → 0.55 → 0.5) with auto-compressing card fans
- Bouncing arrow indicator below the active split hand, removed during animations
- Scale-aware flip animations capturing baseX/baseY before animation sequences
- Pre-mutation state capture (
pendingActionHandIndex) to target correct hand for hit animations - Generalized split animation supporting re-splits at any hand index
- Debug panel: "Split 8s" and "Re-split" buttons with rigged shoe for testing
PR: #43
2026-03-04: S22 — Betting Screen UI Implemented
Betting mode overlay with SwiftUI chip rack, SpriteKit chip stack animations, programmatic CGPath ribbon on insurance text, and table content slide transitions.
What shipped:
- BettingModeOverlay (SwiftUI): chip rack on wood rail with bounce-animated chip buttons, Clear/Deal action buttons, bankrupt state handling
- ChipButtonView: pre-rendered casino chip images matching ChipNode design, delegating to ChipColor for single source of truth
- SpriteKit chip stack: animated chip placement from rack to felt with spring physics, bet label updates, preserve-on-deal behavior
- Programmatic CGPath ribbon with quad-curve fold ends around "INSURANCE PAYS 2 TO 1" arced text
- Table content slide-off/slide-on transitions for entering/exiting betting mode with rail reveal
- P2 review fixes: consolidated duplicate chip colors and formatDenomination across layers, explicit pendingBet total in callbacks
PR: #42
2026-03-04: S21 — SwiftUI App Shell In Progress
App shell navigation structure and game HUD action buttons delivered across two sub-plans. Remaining work: dealer selector, rank bar, error banner.
What shipped:
- S21a: App entry point, ContentView with SpriteView + ZStack HUD overlay, navigation structure with Liquid Glass UI (
if #available(iOS 26, *)) - S21b: Game HUD action buttons (Hit, Stand, Double, Split) with casino-themed press animation, disabled during animations, VoiceOver labels
2026-02-27: Full Codebase Review Cleanup — 19 Findings Resolved
Post-incremental-development review using 6 parallel agents identified 22 findings. Addressed 19 across 3 phases — correctness, duplication removal, and dead code cleanup. Net -178 lines.
What shipped:
- P1: StoreKitService routes credits through BankrollProviding, truecountApp 3-tier crash recovery, BankrollService input validation
- P2: StatisticsReadable protocol (eliminated 28 duplicate properties), generic fetchOrCreateSingleton (4→1), shared TestHelpers (7→1), Shoe.deal() O(1), ShadowTextureFactory, QuickChips rate limiting, consumable revocation tracking
- P3: Dead code removal (splitBetCost, unused Comparable, empty template), betaTweaks behind #if DEBUG, DateFormatter caching, GameViewModel.performAction simplified
- Review catch: Card.id
nonisolated(unsafe)data race reverted to UUID
PR: #39
2026-02-27: S20 — SpriteKit Table Scene Implemented
BlackjackTableScene with table felt backgrounds, card texture atlas, deal/flip/chip/shuffle animations, shoe node, and SpriteView bridge — the first visual slice, delivered across 6 sub-plans.
What shipped:
- S20-A: BlackjackTableScene with 5 table felt backgrounds, Playfair Display Bold font, table rules text, SpriteView bridge, GameViewModel wiring in composition root
- S20-B: 2048x2048 card texture atlas (52 faces + back), CardNode sprite with face-up/face-down states, pre-rendered cached shadows
- S20-C: Card deal animation infrastructure — pendingAction/isAnimating on GameViewModel, update() loop, pause/unpause lifecycle, alternating P1→D1→P2→D2 deal sequence
- S20-D: Card flip animation with 3D xScale effect (lift, squeeze, snap, land phases), sequential deal-then-flip orchestration, dealer turn with thinking pauses
- S20-E: ChipNode with programmatic Core Graphics rendering (5 denomination colors matching Figma), chip placement/payout/sweep animations
- S20-F: ShoeNode 3-layer composite (FloorDeck, card backs, Cover with arch cutout), penetration tracking, shuffle animation with card spread and riffle
2026-02-20: SOLID Compliance Refactoring — Bankroll Centralization + Generator Extraction
Cross-cutting refactoring to eliminate scattered bankroll mutations and reduce service responsibilities. Not tied to a specific slice — improves architecture for all current and future consumers.
What shipped:
- BankrollProviding protocol + BankrollService: single point of persistence for all bankroll mutations
- DailyChallengeGenerator: extracted ~210 lines of pure generation logic into nonisolated enum namespace
- StoreViewModel no longer imports SwiftData — depends on BankrollProviding abstraction
- DailyChallengeService.claimReward() uses bankrollService.addCredits() instead of direct GameState mutation
- Fixed critical @Observable chain bug: stored property + manual sync pattern (computed property through protocol existential breaks observation)
- Shared-ModelContext invariant documented and enforced at composition root
- Two solution docs in docs/solutions/ for future reference
PR: #29
2026-02-20: S27 — Haptic and Audio Integration Implemented
CoreHaptics hybrid service with pre-allocated generators, AVFoundation audio service, and unified feedback dispatch gated by per-channel settings toggles.
What shipped:
- HapticsService: CHHapticEngine for win celebration pattern, pre-allocated UIImpactFeedbackGenerators for zero-latency taps
- AudioService: AVAudioPlayer-based sound effects with ambient audio session category
- HapticPlaying and AudioPlaying protocols with MockHapticsService and MockAudioService for testability
- Unified playFeedback(_:) dispatcher in GameViewModel with independent haptic/sound settings gates
- soundEffects toggle added to UserSettings, SettingsViewModel, and GameViewModel
- DependencyContainer wiring real services at composition root
- 2 new tests (haptic suppression, sound suppression) plus updated settings tests
PR: #27
2026-02-20: S26 — Auto-Deal and Hand Count Logic Implemented
Auto-deal timer orchestration (1.2s delay, same bet, cancellable) and hand count bug fix for split rounds.
What shipped:
- Fixed hand count: increments by 1 per round, not per split hand outcome
- Added
autoDealEnabledtoggle to UserSettings (SwiftData persisted) - Added auto-deal timer to GameViewModel with cancellation wiring (placeBet, selectTable, resetGame)
- Exposed
handCountandisAutoDealTimerActiveobservable state for future S21 HUD binding - Added 8 tests covering auto-deal activation, cancellation, and hand count correctness
PR: #26
2026-02-20: S19 — Settings ViewModel Implemented
Implemented SettingsViewModel with 5 user-setting toggles, table-aware visibility, and closure-based Reset Game delegation to GameViewModel.
What shipped:
- @MainActor @Observable SettingsViewModel proxying 5 settings through cached SwiftData with single
update(_:)helper - Table-aware computed properties via closures: auto-hint visibility/forced, insurance visibility based on DealerTable rules
- Closure-based dependency inversion (
() -> DealerTable,() -> Void) for table selection and reset game delegation - GameViewModel.resetGame() method clearing bankroll, XP, statistics, hand count, and table selection to defaults
- Insurance guard hardening in GameViewModel.placeBet ensuring table rules are respected
- 16 SettingsViewModel tests + 9 GameViewModel reset/insurance tests using Swift Testing framework
PR: #25
2026-02-20: S18 — Daily Challenge Logic Implemented
Implemented daily challenge service with difficulty-scaled basic-strategy questions, answer validation, reward crediting, and CloudKit-safe persistence with dedup.
What shipped:
- DailyChallengeState SwiftData model with JSON-encoded questions, UTC date tracking, and CloudKit dedup (latest date wins, same-date highest progress wins)
- DailyChallengeService generating 5 questions per day from BasicStrategy pool, scaled by 5 difficulty tiers (beginner through master) based on player level
- DailyChallengeViewModel as thin @Observable shell syncing state from service
- DailyChallengeServiceProtocol added to service protocols with MockDailyChallengeService for testability
- 25 service tests + 9 ViewModel tests covering generation, validation, rewards, tier boundaries, and dedup
- 2 solution documents: @MainActor static method isolation in tests, point-value vs rank equality gotcha
PR: #24
2026-02-20: S17 — Game Center Service Implemented
Implemented GameCenterService wrapping GameKit for authentication, dual-leaderboard score submission, and offline score queueing with in-memory coalesced pending scores.
What shipped:
- GameCenterService conforming to GameCenterServiceProtocol with @MainActor @Observable pattern matching StoreKitService
- GKLocalPlayer authentication via withCheckedThrowingContinuation with didResume guard preventing double-resume crashes
- Dual-leaderboard score submission (all-time and monthly) via extracted submitToLeaderboards helper
- In-memory coalesced pending score queue (latest cumulative value only) with flush-on-auth recovery
- Game Center entitlement added to Xcode project
- 8 mock contract tests covering authentication, score submission, and leaderboard fetch flows
- 2 solution documents capturing GameKit API gotchas (tuple order, TimeScope, cumulative scores, authenticateHandler multi-fire)
PR: #23
2026-02-20: S16 — StoreKit Service + Store ViewModel Implemented
Implemented StoreKit 2 service with JWS verification, transaction recovery, revocation handling, cached persistence, and Store ViewModel with purchase flow orchestration.
What shipped:
- StoreKitService wrapping StoreKit 2 with JWS verification, crash-safe credit delivery (persist before transaction.finish()), and revocation handling
- StoreProductCatalog as static single source of truth for 10 IAP product IDs, credit amounts, and ad-removal flags
- StoreViewModel orchestrating purchase flow with guard-based state machine (idle/purchasing/restoring) and defer-based cleanup
- Cached GameState and UserSettings references in the service to eliminate redundant SQLite queries during transaction processing
- Transaction observer with double-invocation guard via stored Task handle with cancellation
- 19 unit tests covering init, load products, purchase flows, quick chips, restore, and product categorization
- 10 IAP products registered in App Store Connect with localizations, pricing, and territory availability
PR: #22
2026-02-19: S15 — Statistics ViewModel Implemented
Implemented StatisticsViewModel with per-navigation lifecycle and value-type MonthlySnapshot, exposing SwiftData statistics to the UI layer without mutation risk.
What shipped:
- @MainActor @Observable StatisticsViewModel reading LifetimeStatistics, GameState, and MonthlyStatistics via per-navigation initialization
- MonthlySnapshot value-type struct decoupling views from live @Model objects, preventing deleted-object crashes after stats reset
- 7 computed rate properties (winRate, lossRate, pushRate, blackjackRate, playerBustRate, dealerBustRate, realizedHouseEdge) derived from raw stored metrics
- Current and best-ever win streak exposure via GameState.consecutiveWins and LifetimeStatistics.bestEverStreak
- 16 comprehensive test cases covering zero data, populated data, monthly history, and error handling
PR: #20
2026-02-19: S14 — GameViewModel Implemented
Added GameViewModel with round orchestration, cached SwiftData persistence, StatisticsAccumulating protocol, and dealerBlackjacks tracking.
What shipped:
- @Observable GameViewModel with @MainActor isolation driving full round flow (placeBet → deal → insurance → playerAction → dealerTurn → settle → persist)
- StatisticsAccumulating protocol eliminating duplicated stat recording between LifetimeStatistics and MonthlyStatistics
- Cached SwiftData singleton references (GameState, LifetimeStatistics, MonthlyStatistics) with month-boundary guard
- DealerBlackjacks tracking alongside dealerBusts in per-round persistence
- 24 unit tests using mock services covering all round flows, edge cases, and persistence
PR: #19
2026-02-19: S13 — Service Protocols and Composition Root Implemented
Added protocol abstractions for Game Center, StoreKit, Haptics, Audio, and Error Reporting with DependencyContainer composition root and SwiftData model wiring.
What shipped:
- GameCenterServiceProtocol, StoreKitServiceProtocol, HapticPlaying, AudioPlaying, ErrorReporting protocols
- DependencyContainer composition root with production() and mock() factories
- Placeholder no-op services, configurable spy mocks (#if DEBUG), and SwiftUI @Entry environment key
- GameEvent shared enum for haptic/audio triggers and AppError for recoverable error surfacing
- SwiftData modelContainer wired at app entry point for S11/S12 models
PR: #18
2026-02-19: S12 — SwiftData Models: Statistics Implemented
Add LifetimeStatistics and MonthlyStatistics SwiftData models with computed rates, CloudKit deduplication, and 27 tests. Phase B (Persistence) is now complete.
What shipped:
- LifetimeStatistics @Model singleton with 10 tracked metrics + bestEverStreak
- MonthlyStatistics @Model with indexed yearMonth (YYYYMM Int) for per-month grouping
- 7 computed rates on both models (win%, loss%, push%, BJ%, bust%, dealer bust%, realized house edge)
- CloudKit-safe fetch-or-create with dedup (keep most hands played) and batch deleteAll
- 27 unit tests covering defaults, rates, zero-denominators, dedup, reset, monthly ordering, and integration
PR: #17
2026-02-19: S11 — SwiftData Models: Game State Implemented
Added SwiftData persistence for game state and user settings with CloudKit-safe singleton pattern, fetch-or-create deduplication, and engine enum raw value storage.
What shipped:
- GameState @Model with bankroll, totalXP, consecutiveWins, selectedTable, handCount
- UserSettings @Model with 5 toggles (haptic, insurance, autoHint, handCount, dealerSoft17)
- Computed level derivation from totalXP via PlayerRank.level(forTotalXP:)
- Fetch-or-create singleton pattern with CloudKit deduplication (max XP for GameState, first-row for UserSettings)
- 16 unit tests covering defaults, enum conversion, reset, dedup, and singleton behavior
PR: #16
2026-02-19: S10 — Game State Machine Implemented
Implemented Game State Machine with pure Swift round orchestration (RoundPhase, RoundState, RoundEngine), step-based API for animation pacing, and comprehensive bankroll tracking. Phase A (Core Engine) is now complete.
What shipped:
- RoundPhase enum with 8 states and validated transitions
- RoundState mutable struct holding all round data (hands, bets, bankroll, insurance)
- RoundEngine static orchestrator with step-based API (perform/advancePhase)
- BlackjackRules.evaluateOutcome for hand settlement
- BettingMath insurance helpers (insuranceCost, insuranceGrossReturn)
- 16 test suites covering all round flows (410 total engine tests)
PR: #15
2026-02-19: S09 — Player Rank and XP System Implemented
Added PlayerRank struct with XP accumulation, win streak multipliers, level formula with floating-point precision handling, and table unlock checks.
What shipped:
- PlayerRank value-type struct tracking totalXP, consecutiveWins, and computed level
- XPGain result type capturing XP earned, level transitions, and level-up detection
- XP rates: loss/push/surrender = 1/unit, win = 3/unit, blackjack = 5/unit
- Win streak multiplier tiers (1.0x, 1.5x, 2.0x, 3.0x) applied only to win outcomes
- Level formula using rounded() + inverse verification to handle IEEE 754 boundary precision
- Table unlock checks against DealerTable.unlockLevel thresholds (1, 10, 25, 50, 100)
- 34 unit tests across 11 test groups including monotonicity property test
PR: #14
2026-02-19: S08 — Hi-Lo Card Counting Engine Implemented
Implemented Hi-Lo card counting engine with running count tracking, true count computation, and table-aware player edge estimation.
What shipped:
- HiLoCounter value-type struct with running count state and on-demand metric computation
- Dual track() overloads accepting Card or Rank for flexible integration
- True count calculation with decks-remaining floor clamp (minimum 0.5 decks)
- Player edge estimation with configurable base house edge parameter for table-specific accuracy
- 14 unit tests including parametrized full shoe deal-downs for 1, 2, 4, 6, and 8 deck configurations
PR: #13
2026-02-19: S07 — Basic Strategy Engine Implemented
Implemented BasicStrategy lookup tables for hard totals, soft totals, and pairs with S17/H17 variant support. 274 exhaustive tests validate all chart cells.
What shipped:
- BasicStrategy caseless enum with recommendation(hand:dealerUpcard:soft17Rule:) public API
- Three lookup tables: hard totals (5-17+), soft totals (A,2-A,9), and pairs (2,2-A,A)
- H17 overlay with 3 cell overrides (soft 18 vs 2, soft 18 vs A, soft 19 vs 6)
- Precondition guards for hand validity (count, bust, blackjack)
- 274 unit tests covering all 310 chart cells, H17 differences, and edge cases
PR: #12
2026-02-18: S06 — Betting Math and Chip System Implemented
Implemented BettingMath namespace with tier-based chip denominations, payout calculations, and bet validation using pure integer arithmetic.
What shipped:
- BettingMath caseless enum with chipDenominations, grossReturn, isValidBet, splitBetCost, and doubleBetCost
- HandOutcome domain enum representing all 5 hand settlement outcomes (blackjack, win, push, loss, surrender)
- 6-tier chip system scaling by bankroll order of magnitude (base 5 to 500,000)
- Integer division loop replacing log10 to avoid floating-point precision issues at power-of-10 boundaries
- 35+ unit tests covering tier boundaries, payout edge cases, bet validation, and DealerTable fixture integration
PR: #11
2026-02-18: S05 — Dealer Logic Implemented
Implemented dealer play algorithm with S17/H17 branching via standalone Soft17Rule enum, peek-for-blackjack detection, and skip-dealer-turn optimization.
What shipped:
- Soft17Rule standalone enum as a global user setting (not embedded in TableRules)
- DealerLogic caseless enum with dealerShouldHit, shouldPeekForBlackjack, hasDealerBlackjack, shouldSkipDealerTurn, and dealerPlay
- Full dealer turn loop drawing from Shoe until standing threshold reached
- 47 unit tests covering S17/H17 branching, multi-card soft hands, peek logic, blackjack detection, and integration scenarios
PR: #10
2026-02-18: S04 — Action Validation Implemented
Added pure BlackjackRules validation engine with PlayerAction enum, covering Hit, Stand, Double Down, Split, Surrender, and Insurance eligibility across all five dealer tables.
What shipped:
- PlayerAction enum as canonical action type for all downstream slices (S07, S10, S25)
- BlackjackRules caseless enum with static validators: canHit, canDouble, canSplit, canSurrender
- Aggregate availableActions function returning correct action sets for any hand/rules/bankroll combination
- Separate isInsuranceAvailable for the insurance game phase (dealer Ace + table allows)
- 37 unit tests covering all action types, bankroll boundaries, DAS rules, double restrictions, and integration scenarios
PR: #9
2026-02-18: S-FW — TrueCountEngine Swift Package Implemented
Extracted Card, Shoe, Hand, and TableRules into a local Swift Package called TrueCountEngine, creating a compiler-enforced boundary between game logic and the app shell.
What shipped:
- TrueCountEngine/ local Swift Package (swift-tools-version 6.0, iOS 18 platform)
- Card.swift, Shoe.swift, Hand.swift, TableRules.swift moved with full public access control
- 99 engine tests migrated and passing via swift test
- Xcode project wired with XCLocalSwiftPackageReference
- CLAUDE.md updated with package architecture and conventions
PR: #8
2026-02-18: S-FW — TrueCountEngine Swift Package Extraction Planned
Architectural decision to extract all game logic into a local Swift Package called TrueCountEngine, creating a compile-time boundary between the engine (pure logic) and the app (UI, persistence, services). Proposed by lead iOS developer Mihai Dumitrache, modeled after the "Tethered" project architecture.
What changed:
- S-FW slice added to the master plan as the next Phase A item, with dependencies on S01, S02, S03
- Architecture layers diagram updated to show the TrueCountEngine package boundary
- Architecture principles updated with a new Modularization section
- Full plan documented in
docs/plans/2026-02-18-refactor-game-engine-swift-package-plan.md
Why: With only 3 slices implemented (all pure Swift, zero UI dependencies), this is the cheapest time to modularize. The package boundary enforces that game logic cannot import UIKit or SwiftUI, enables 4-5x faster engine test cycles, and prepares the codebase for potential platform expansion.
2026-02-17: S03 — Table Rules Configuration Implemented
Pure data types encoding all five dealer table configurations with progressive difficulty. BlackjackPayout, SurrenderMode, DoubleRestriction, and HintsAvailability enums plus DealerTable enum with metadata and rules.
What shipped:
- TableRules.swift: BlackjackPayout (3:2/6:5/1:1), SurrenderMode, DoubleRestriction, HintsAvailability enums
- TableRules struct with 9 configurable parameters per table
- DealerTable enum with 5 cases (Jean-Luc through Aunty) including name, unlockLevel, tableColor, and tableRules
- Beta Tweaks presets (Player+, Standard, Casino) as static TableRules constants
- Shoe compatibility tests validating cross-slice contracts with S01
PR: #7
2026-02-17: S02 — Hand Model and Evaluation Implemented
Pure Swift domain type for hand evaluation: hard/soft totals with Ace demotion, blackjack detection, bust determination, and pair identification. Single-pass O(n) algorithm with no iterative logic.
What shipped:
- Hand.swift: struct with cards, split tracking (isFromSplit, isSplitAce), and computed properties
- Rank.pointValue extension: shared by Hand (S02) and Hi-Lo counter (S08)
- 85 unit tests covering multi-Ace hands, soft 21 vs blackjack, Ace demotion chains
- SpecFlow analysis identified 3 critical cross-slice contracts: isSplitAce (S04), pairRank (S07), card ordering
- Solution documentation: Ace algorithm and cross-slice contract patterns
PR: #6
2026-02-17: S01 — Card and Deck Models Implemented
Pure Swift domain types for the blackjack engine: Suit, Rank, Card, and Shoe. Deterministic Fisher-Yates shuffling via GKMersenneTwisterRandomSource with seeded replays and cut-card penetration tracking for all five dealer tables.
What shipped:
- Card.swift: Suit (4 cases), Rank (13 cases with isTenValue), Card struct with UUID identity
- Shoe.swift: Multi-deck shoe (1/2/4/6/8 decks), seeded shuffle, cut-card position, shouldReshuffle flag
- 41 unit tests covering all deck configs, penetration thresholds, and seeded replay determinism
- Card equality based on suit+rank only (UUID for SpriteKit node identity)
PR: #4
2026-02-17: Dealer on Soft 17 Is Now a Settings Toggle
Soft 17 behavior (S17/H17) is now fully player-controlled through the Settings menu. It is no longer tied to individual dealer tables. Choose S17 or H17, and that rule applies at every table.
What changed:
- New "Dealer on Soft 17" toggle in Settings (Stands / Hits, default: Stands)
- S17/H17 removed from dealer table profiles (no longer a table property)
- Auto Hint adjusts recommendations to the active setting automatically
- Basic strategy page now includes an H17 Differences section
- Daily Challenge remains fixed at S17 regardless of the setting
- House edge values updated to S17 baselines with +0.22% H17 footnote
- Beta Tweaks still overrides the Settings toggle when active
Why: Reader feedback on the Dealer Rules page requested the ability to customize soft 17 behavior. (#52)
2026-02-17: Surrender Hidden from User Experience
Surrender has been removed from all user-facing gameplay and documentation. The engine retains full surrender support (None, Late, Early modes) for potential future use, and surrender can still be toggled via Beta Tweaks for internal testing.
What changed:
- All dealer tables now set surrender to None (previously Jean-Luc had Late Surrender)
- Jean-Luc's house edge updated from ~0.23% to ~0.30%
- Basic strategy charts updated: "R" entries replaced with "H"
- Daily Challenge Master tier action set: H/S/D/P (removed surrender)
- Statistics: surrender count and surrender rate removed from tracked metrics
Why: Most blackjack apps do not offer surrender. Hiding it by default aligns with standard user expectations while maintaining the feature for future activation. (#50)
How is this guide?