CollapseLogic/CLAUDE.md

9.7 KiB
Raw Permalink Blame History

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.md for 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:

  1. Both source blocks are removed from state.
  2. A new block of the merged color is placed at the stationary block's position.
  3. A .merged event is emitted (for animation).
  4. Move count increments (merge is a valid move).
  5. 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:

  1. Slide the moving block to position T (same 0.15s ease-out as existing slide).
  2. Run a merge burst: scale both blocks up to 1.2× over 0.1s, then scale down and fade out over 0.1s simultaneously.
  3. 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).
  4. 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:

  1. 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 13: Introduce one merge pair (red+blue=purple). No pre-placed secondaries.
  • Levels 46: Add yellow. Teach red+yellow=orange and blue+yellow=green.
  • Levels 79: Pre-placed secondary blocks appear. Player must match them.
  • Levels 1011: 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 + blue collision → produces purple block at stationary position
  • red + yellow collision → produces orange
  • blue + yellow collision → produces green
  • 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 correct
  • GameEngine.processMove handles primary+primary different-color collision as merge
  • .merged GameEvent 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, green render correctly in BlockNode
  • Secondary blocks have white inner ring visual treatment
  • Merge animation plays: slide → burst → new block pop-in (~0.35s total)
  • merge.wav plays on merge
  • All 12 World 2 level JSONs load and are playable
  • World selector UI in LevelSelectView (two pills, lock at <15 stars)
  • metadata.json updated with World 2 entry
  • World 2 levels are fun and teach the mechanic clearly