9.7 KiB
CLAUDE.md — Collapse Logic iOS
Project Overview
Collapse Logic is a grid-based puzzle game for iPhone where the player pushes colored blocks to trigger same-color destruction and different-color merging.
Studio: Vulcara Games
Platform: iOS 17.0+ (iPhone primary)
Language: Swift 5.9+
Frameworks: SpriteKit (gameplay), SwiftUI (menus/HUD)
Architecture: MVVM with pure-struct game state
Current Build Status
- World 1 — COMPLETE. All 10 levels playable. Engine, SpriteKit scene, SwiftUI shell, undo, star rating, and persistence are all working.
- World 2 — IN PROGRESS. Adding color merging. See
TASKS_World2.mdfor the step-by-step build plan.
Architecture
CollapseLogic/
├── App/
│ └── CollapseLogicApp.swift
├── Models/
│ ├── GameState.swift
│ ├── Block.swift # BlockColor enum lives here — add yellow + secondaries
│ ├── GridPosition.swift
│ ├── Direction.swift
│ ├── MoveResult.swift # .merged event already stubbed — wire it up
│ └── LevelDefinition.swift
├── Engine/
│ ├── GameEngine.swift # Core change: different-color collision now merges
│ └── LevelLoader.swift
├── Views/
│ ├── MainMenuView.swift
│ ├── LevelSelectView.swift # Needs world selector (World 1 / World 2)
│ └── GameContainerView.swift
├── Scene/
│ ├── GameScene.swift
│ ├── BlockNode.swift # Add color rendering for yellow + 3 secondaries
│ └── GridRenderer.swift
├── Audio/
│ └── SFXManager.swift # Add merge.wav
├── Persistence/
│ └── ProgressStore.swift
├── Levels/
│ ├── metadata.json # Add World 2 entry
│ └── world1/
│ └── level_01.json … level_10.json
│ └── world2/
│ └── level_01.json … level_12.json
└── Resources/
└── SFX/
Block Colors — Full Set
Primary colors (can be pushed into each other to merge)
| Color | Hex | Name |
|---|---|---|
red |
#E63946 | Ruby |
blue |
#457B9D | Sapphire |
yellow |
#F4D35E | Topaz |
Secondary colors (result of merges — can destroy each other but cannot merge further)
| Color | Created From | Hex | Name |
|---|---|---|---|
purple |
red + blue | #7B2D8B | Amethyst |
orange |
red + yellow | #E76F51 | Ember |
green |
blue + yellow | #2A9D8F | Jade |
Secondary blocks can be pre-placed in level JSON. They can destroy each other on same-color collision. They cannot merge — two different secondaries just stop (no interaction).
Merge Rules (World 2 Engine Logic)
When a moving block collides with a stationary block of a different color:
if both are PRIMARY colors → merge into the secondary color, placed at the stationary block's position
if moving is PRIMARY, stationary is SECONDARY → block stops (no interaction)
if moving is SECONDARY, stationary is PRIMARY → block stops (no interaction)
if both are SECONDARY colors (different) → block stops (no interaction)
if same color (primary or secondary) → both destroyed (existing World 1 rule, unchanged)
Merge color table
static func mergeColor(_ a: BlockColor, _ b: BlockColor) -> BlockColor? {
let pair = Set([a, b])
if pair == [.red, .blue] { return .purple }
if pair == [.red, .yellow] { return .orange }
if pair == [.blue, .yellow] { return .green }
return nil // no merge possible
}
When a merge occurs:
- Both source blocks are removed from state.
- A new block of the merged color is placed at the stationary block's position.
- A
.mergedevent is emitted (for animation). - Move count increments (merge is a valid move).
- Win condition is checked after the merge.
GameEngine Changes (World 2)
The only change to GameEngine.processMove is step 5 of the movement algorithm:
Before (World 1):
5. Check cell adjacent to T in direction D:
- Same color → destroy both
- Different color → block stops
After (World 2):
5. Check cell adjacent to T in direction D:
- Same color (any) → destroy both [unchanged]
- Both are primary, different colors → merge; new secondary block at stationary position
- Any other different-color combination → block stops [unchanged]
No other engine logic changes. The pure-struct GameState design means this is fully unit-testable in isolation before touching SpriteKit.
MoveResult Events
enum GameEvent {
case slid(blockIndex: Int, from: GridPosition, to: GridPosition)
case destroyed(blockIndex1: Int, blockIndex2: Int, at: GridPosition)
case merged(blockIndex1: Int, blockIndex2: Int, resultColor: BlockColor, at: GridPosition)
}
The .merged event was already stubbed in World 1. Wire it up now.
Animation — Merge
In GameScene, handle .merged events:
- Slide the moving block to position T (same 0.15s ease-out as existing slide).
- Run a merge burst: scale both blocks up to 1.2× over 0.1s, then scale down and fade out over 0.1s simultaneously.
- Spawn a new block node of the merged color at the stationary position, starting at scale 0.0, and animate it scaling up to 1.0 over 0.15s with a slight overshoot (spring feel — scale to 1.15 then settle to 1.0).
- Total merge animation duration: ~0.35s.
Keep it snappy. The new merged block "popping" into existence is the payoff moment.
New Color Rendering (BlockNode)
Add cases for yellow, purple, orange, green to the color switch in BlockNode. Use the hex values from the color table above.
Visual treatment:
- Primary blocks (red, blue, yellow): same rounded rect style as World 1.
- Secondary blocks (purple, orange, green): add a subtle white inner ring (2px, 40% opacity) to visually distinguish them as merged/special. This gives players a quick read that these are second-tier blocks.
World Select UI
LevelSelectView currently shows World 1 levels only. For World 2, add a simple world selector at the top — two tappable pills: "World 1" and "World 2". World 2 unlocks when the player has earned at least 15 stars in World 1 (out of 30 possible).
Lock state: if World 2 is locked, show the pill grayed out with a lock icon and "15 ★ to unlock" label.
Do not over-engineer this — a simple @State var selectedWorld: Int in LevelSelectView is enough.
Sound Effects
Add one new sound:
merge.wav— A two-tone "blip-bloom" sound (0.25s). Higher pitch than destroy. Suggests transformation rather than destruction.
Use SKAction.playSoundFileNamed() same as the existing four sounds.
Level Design — World 2 (12 levels)
World 2 introduces merging progressively across 12 levels. All level JSON files are in Levels/world2/. Grid sizes 5×5 to 6×6.
Teaching sequence:
- Levels 1–3: Introduce one merge pair (red+blue=purple). No pre-placed secondaries.
- Levels 4–6: Add yellow. Teach red+yellow=orange and blue+yellow=green.
- Levels 7–9: Pre-placed secondary blocks appear. Player must match them.
- Levels 10–11: Multi-step chains — merge, then use the result to destroy another secondary.
- Level 12: Boss level — all three primaries, all three secondaries, careful sequencing required.
See Levels/world2/level_*.json for the hand-crafted level files.
Testing Priorities (World 2 additions)
Engine unit tests — add these:
red+bluecollision → producespurpleblock at stationary positionred+yellowcollision → producesorangeblue+yellowcollision → producesgreen- Secondary + primary collision → block stops (no merge)
- Secondary + different secondary collision → block stops (no merge)
- Same secondary collision → both destroyed (e.g. purple + purple)
- Merged block is placed at correct position (stationary block's cell)
- Move count increments on merge
- Undo correctly restores state before a merge
- Win condition detected after a merge clears the last blocks
Level loading tests:
- All 12 World 2 JSON files parse without errors
- Pre-placed secondary color blocks load correctly
Manual playtesting:
- Each World 2 level is solvable at par
- Merge animation feels satisfying, not jarring
- World 2 unlock (15 stars) triggers correctly
Style & Conventions
- Swift naming conventions (camelCase properties, PascalCase types)
- Prefer value types (structs, enums) over classes except for SpriteKit nodes
- No force unwraps except in tests
- Keep files under 200 lines. Split if needed.
- Comment non-obvious game logic
// MARK: -sections in longer files
Build & Run
- Xcode 15+
- iOS 17.0 deployment target
- No external dependencies
- Primary simulator: iPhone 15 Pro
- Small-screen test: iPhone SE (3rd gen)
Definition of Done — World 2
mergeColor(_:_:)function implemented and all merge combinations correctGameEngine.processMovehandles primary+primary different-color collision as merge.mergedGameEvent emitted with correct blockIndex values and resultColor- All existing World 1 unit tests still pass (no regressions)
- New merge unit tests pass (see Testing Priorities above)
yellow,purple,orange,greenrender correctly inBlockNode- Secondary blocks have white inner ring visual treatment
- Merge animation plays: slide → burst → new block pop-in (~0.35s total)
merge.wavplays on merge- All 12 World 2 level JSONs load and are playable
- World selector UI in
LevelSelectView(two pills, lock at <15 stars) metadata.jsonupdated with World 2 entry- World 2 levels are fun and teach the mechanic clearly