WheresTheDot

WheresTheDot — Claude Code Instructions

Project Overview

“Where’s The Dot” is a casual iOS memory game built with Swift. Players must remember and tap the newest dot added to the screen each round. A wrong tap ends the game. Score equals the number of dots successfully identified.

Architecture

Clean Architecture with strict layer separation:

WheresTheDot/
├── Domain/          # Entities, protocols, core business rules
│   └── Protocols/   # DotLayoutGenerating, GameSessionRepository, LevelProgression, ThemeRepository, …
├── Data/            # Concrete implementations (repositories, adapters)
├── Use Cases/       # Orchestrators between domain and presentation
├── Presentation/    # SwiftUI views, coordinators, state
│   ├── AnimatedDots/
│   ├── Components/
│   ├── Routes/
│   └── View Models/
├── Tools/           # Cross-cutting concerns (audio, haptics, analytics, config)
├── Utils/           # Extensions
├── GameScene.swift  # SpriteKit scene (rendering + touch input)
└── WheresTheDot.swift  # @main entry point + AppDelegate

Layer Responsibilities

Key Patterns

Use Cases as Callable Structs

struct StartGameUseCase {
    func callAsFunction(in area: CGRect) -> Round { ... }
}
// Used as: startGame(in: area)

Dependency Injection via AppContainer

@MainActor final class AppContainer: ObservableObject {
    let startGame: StartGameUseCase
    let addDotIfCorrect: AddDotIfCorrectUseCase
}
// Provided via .environmentObject(container) at root

Coordinator for Game State

@MainActor final class GameCoordinator: ObservableObject {
    @Published var roundIndex: Int
    @Published var score: Int
    // Owns game flow; drives SpriteKit scene via closures
}

SpriteKit ↔ SwiftUI Bridge

Protocols — Naming Convention

Protocol names use the -ing suffix: DotLayoutGenerating, RandomNumberGenerating, LevelProgression.

Naming Conventions

Category Convention Example
Types, structs, classes PascalCase GameSnapshot, AppContainer
Protocols PascalCase + -ing DotLayoutGenerating
Enums PascalCase GameMode, RoundOutcome
Files — Views *View.swift MainMenuView.swift
Files — Use Cases *UseCase.swift StartGameUseCase.swift
Files — Repositories *Repository.swift InMemoryGameSessionRepository.swift
Files — Adapters *Adapter.swift GKRandomAdapter.swift
Files — Styles *Style.swift DottoButtonStyle.swift
Files — Extensions Type+Extensions.swift Color+Extensions.swift

Tech Stack

Game Modes

Theme System

Themes are defined in Domain/Theme.swift. There are two unlock mechanisms:

Score-unlocked themes (cumulative lifetime score milestones):

Theme Unlock Background Grid Dot Shape
Neon Free #05060A Cyan circle
Forest 50 pts #050D07 Green randomAssets (forest_dot, 5 variants)
Ocean 200 pts #03080F Cyan-blue randomAssets (ocean_dot, 5 variants)
Cosmos 300 pts #080510 Purple randomAssets (cosmos_dot, 5 variants)

Premium IAP-unlocked themes (purchase via StoreKitManager):

Theme Product ID Background Grid Dot Shape
Aurora …theme.aurora dark blue #BAE6FD randomAssets (aurora_dot, 5 variants)
Inferno …theme.inferno dark red #F97316 asset (flame.fill)
DoctorPing …premium dark blue #64B5D9 asset (DoctorPing)
Space Travel …premium dark teal #143d4a asset (star.fill)

DotShape

enum DotShape: Equatable {
    case circle
    case asset(named: String, fallbackSymbol: String)
    case randomAssets(prefix: String, count: Int, fallbackSymbol: String)
}

Unlock Rules

Firebase Analytics

All events go through FirebaseEventsManager (static methods only). GameOverReason enum provides typed reason parameters (wrongTap / timeUp).

Key events: select_game_mode, game_over, game_ended, game_quit, round_correct, open_themes, select_theme, theme_unlocked, unlock_theme, open_store, purchase_initiated, purchase_completed, onboarding_started, onboarding_intro_dismissed, onboarding_completed, onboarding_skipped, leaderboard_opened, achievements_opened, open_settings, sound_enabled/disabled, haptics_enabled/disabled, color_blind_mode_enabled/disabled.

Firebase Remote Config

RemoteConfigManager.shared is the single access point. It has a local override layer (backed by UserDefaults with rc.override. prefix) used by the admin panel.

Always read values from RemoteConfigManager.shared — never hardcode the following:

Key Controls
arcade_time_limit_base Base seconds for arcade timer at level 1
arcade_difficulty_step Points between difficulty levels
memory_cover_duration Cover duration between rounds
onboarding_enabled Kill switch for onboarding flow
arcade_mode_enabled Feature flag for arcade mode
default_theme Theme for first-time users
theme_forest_milestone Forest unlock threshold
theme_ocean_milestone Ocean unlock threshold
theme_cosmos_milestone Cosmos unlock threshold

Offline defaults live in Supporting Files/remote_config_defaults.plist.

Monetization

In-App Purchases (StoreKit 2)

StoreKitManager.shared manages all IAP. Product IDs (bundle: com.optionalsankur.Dotto):

Product ID suffix Type
Remove Ads removeads Non-consumable
Aurora Theme theme.aurora Non-consumable
Inferno Theme theme.inferno Non-consumable
Premium Bundle (all) premium Non-consumable

Ads (AdMob)

AdsManager.shared shows interstitial ads every 3rd game-over, guarded by StoreKitManager.shared.isAdFree. Integration requires:

  1. SPM package: https://github.com/googleads/swift-package-manager-google-mobile-ads.git
  2. GADApplicationIdentifier key in Info.plist
  3. Real unit ID in AdsManager.swift

Call AdsManager.shared.recordGameOver() on both wrong-tap and time-up game-over paths.

Game Center

GameCenterManager handles leaderboards and achievements. There are 12 achievements:

Admin Panel

AdminConfig.isEnabled (default false) is a compile-time flag in Tools/AdminConfig.swift. When true, a wrench button appears in the main menu footer, routing to AdminView via .admin in AppRoute. AdminView includes gameplay tuning, feature flag overrides, theme milestone overrides, IAP simulation (Premium status), and ad counter controls. Never ship with isEnabled = true.

Visual Design

Color palette (Color+Extensions.swift):

Visual conventions:

Localization

Concurrency

Testing

Routes

AppRoute enum drives all navigation via RootView:

What to Avoid