┌─────────────────────────────────────┐
│ Main Loop (Interrupt) │
├─────────────────────────────────────┤
│ 1. Read Input │
│ 2. Update Game State │
│ 3. Update Sprites/Screen │
│ 4. Check Win/Lose │
└─────────────────────────────────────┘
↓
┌─────────────────────────────────────┐
│ Game State Machine │
├─────────────────────────────────────┤
│ WAITING_FOR_INPUT (player turn) │
│ EXECUTING_MOVE (animate & check) │
│ ENEMY_TURN (AI + animation) │
│ CHECK_WIN (end condition) │
└─────────────────────────────────────┘
$0000–$00FF: Zero page (save $F0–$FF for OS)
$0100–$01FF: Stack
$0200–$1FFF: BASIC ROM area (will be replaced by our code)
$2000–$2FFF: Screen RAM (1000 bytes @ $2000)
$3000–$3FFF: Color RAM (1000 bytes @ $3000)
$4000–$5FFF: Game code + data
$6000–$CFFF: Free (more game code, sprites, etc.)
$D000–$DFFF: I/O (VIC-II, SID, CIA) — READ-ONLY, memory-mapped
$E000–$FFFF: Kernal ROM (on-chip, can be banked out for RAM)
Key allocation:
– Code (main + subroutines): $4000–$5FFF (~8 KB)
– Sprite data: $5800–$5FFF (shared with code, or higher if we're careful)
– Game state: $0200–$02FF (zero page + 256 bytes)
– Lookup tables: $6000–$7FFF (grids, distances, etc.)
$F0–$FE: Temporary registers (reusable per subroutine)
Game State Variables:
$30 INPUT_KEY ; Last key pressed ($00 = none)
$31 GAME_STATE ; 0=INPUT, 1=MOVE, 2=ENEMY, 3=CHECK
$32 SELECTED_UNIT ; 0–2 (player unit index) or 255 (none)
$33 TURN_COUNTER ; Turn number (0–255+)
$34 PLAYER_UNITS ; Count of alive player units (0–3)
$35 ENEMY_UNITS ; Count of alive enemy units (0–3)
Unit State Array (6 units × 4 bytes each = 24 bytes, $40–$57):
Each unit:
+0 X position (0–7)
+1 Y position (0–7)
+2 HP (0–3, or 0 = dead)
+3 Team (0 = player, 1 = enemy)
Layout:
$40–$43: Unit 0 (player)
$44–$47: Unit 1 (player)
$48–$4B: Unit 2 (player)
$4C–$4F: Unit 3 (enemy)
$50–$53: Unit 4 (enemy)
$54–$57: Unit 5 (enemy)
$58 MOVE_DEST_X ; Target X for current move
$59 MOVE_DEST_Y ; Target Y for current move
$5A MOVE_VALID ; Flag: move is valid (0/1)
Physical Grid: 8×8 = 64 cells
Occupancy Array: $6000–$603F (one byte per cell, index = Y*8 + X)
On each frame, rebuild occupancy from unit state array.
Sprite Rendering:
X_pixel = (unit_x * 40) + offset, Y_pixel = (unit_y * 25) + offset; Interrupt handler (triggered 60x/sec on NTSC, 50x on PAL)
IRQ_HANDLER:
Save registers (A, X, Y)
; 1. Read input
JSR INPUT_READ
; 2. Update game state
CASE GAME_STATE
0 => Player input → select/move/attack
1 => Animate move, resolve collision/damage
2 => Enemy AI, animate enemy moves
3 => Check win/lose, advance state
; 3. Render sprites & text
JSR SPRITE_UPDATE
JSR TEXT_UPDATE
Restore registers
RTI
| Name | In | Out | Cost |
|---|---|---|---|
INPUT_READ |
— | $30 (key code) |
10 cyc |
UNIT_SELECT |
$30 (key) |
$32 (unit) |
20 cyc |
UNIT_MOVE |
$32, $58, $59 |
$5A (valid) |
40 cyc |
UNIT_ATTACK |
$32, target unit |
HP update | 30 cyc |
| Name | In | Out | Cost |
|---|---|---|---|
FIND_CLOSEST_ENEMY |
$32 (player unit) |
$F0 (enemy idx) |
50 cyc |
ENEMY_MOVE |
$F0 (enemy idx) |
Update unit pos | 80 cyc |
ENEMY_AI |
— | Execute all enemies | 300 cyc |
CHECK_WIN |
— | Set GAME_STATE |
20 cyc |
| Name | In | Out | Cost |
|---|---|---|---|
SPRITE_UPDATE |
Unit state | VIC-II regs | 100 cyc |
TEXT_UPDATE |
Game state | Screen RAM | 50 cyc |
REBUILD_OCCUPANCY |
Unit state | Occupancy array | 60 cyc |
; Input: X in $F0, Y in $F1 (target); A (own X), $F2 (own Y)
; Output: A (distance)
MANHATTAN:
SEC
SBC $F0 ; A = own_x - target_x
BPL + ; If positive, skip
EOR #$FF
ADC #$01 ; Negate (two's complement)
+ STA $F3 ; Save |dx|
LDA $F2
SEC
SBC $F1 ; A = own_y - target_y
BPL + ; If positive, skip
EOR #$FF
ADC #$01 ; Negate
+ CLC
ADC $F3 ; A = |dx| + |dy|
RTS
; Input: X in $F0, Y in $F1
; Output: A = unit index (or $FF if empty)
GRID_OCCUPIED:
LDA $F1
ASL
ASL
ASL ; A = Y * 8
CLC
ADC $F0 ; A = Y*8 + X
TAX
LDA $6000,X ; Lookup in occupancy array
RTS
WAITING_FOR_INPUT
↓ [Player selects unit]
↓ [Player moves/attacks]
EXECUTING_MOVE
↓ [Move resolves, damage applied]
ENEMY_TURN
↓ [Each enemy acts]
↓ [Wait for animations]
CHECK_WIN
↓ [If enemy count = 0 or player count = 0]
GAME_OVER (win/lose)
↓ [Display result, wait for restart key]
WAITING_FOR_INPUT [loop]
Development tools:
ca65 and linker ld65)Command sequence:
ca65 main.asm -o main.o # Assemble
ld65 main.o -C c64.cfg -o game.prg # Link
x64sc game.prg # Run in VICE
| Risk | Mitigation |
|---|---|
| Interrupt handler exceeds raster line | Use raster-stable jitter removal; keep subroutines cycle-predictable |
| Sprite overlap (>8 on screen) | Multiplex later; for now, 6 sprites fit without overlap |
| Occupancy array corruption | Rebuild every frame from unit state; don't write directly |
| AI hangs/slowdown | Limit enemy AI to 1 move each; use lookup tables for distance |
| Collision bugs | Test grid occupancy before allowing move; confirm unit dies at 0 HP |
skirmish-game/
src/
main.asm # Entry point, IRQ handler, main loop
input.asm # Input polling, key handling
game-logic.asm # Unit selection, movement, combat
enemy-ai.asm # Pathfinding, enemy turn
render.asm # Sprite updates, text output
data.asm # Lookup tables, sprite/tile data
assets/
sprites.chr # C64 sprite design (binary)
tiles.chr # Tile data (if used)
docs/
GDD.md # Game design (this file)
TECHNICAL-PLAN.md # Technical details (this file)
C64-CHEATSHEET.md # Quick register/memory reference
Makefile # Build script
c64.cfg # cc65 linker config (if using cc65)
README.md # Build & run instructions
Powered by TurnKey Linux.