Browse Source

skirmish

main
parent
commit
da50d8f301
25 changed files with 2761 additions and 1 deletions
  1. +3
    -1
      .claude/settings.local.json
  2. +118
    -0
      skirmish-game/IMPLEMENTATION.md
  3. +30
    -0
      skirmish-game/Makefile
  4. +48
    -0
      skirmish-game/README.md
  5. +13
    -0
      skirmish-game/build.bat
  6. BIN
      skirmish-game/data.o
  7. +6
    -0
      skirmish-game/debug-vice.bat
  8. +333
    -0
      skirmish-game/docs/C64-CHEATSHEET.md
  9. +158
    -0
      skirmish-game/docs/GDD.md
  10. +300
    -0
      skirmish-game/docs/TECHNICAL-PLAN.md
  11. BIN
      skirmish-game/enemy-ai.o
  12. BIN
      skirmish-game/game-logic.o
  13. BIN
      skirmish-game/input.o
  14. BIN
      skirmish-game/render.o
  15. +6
    -0
      skirmish-game/run-vice.bat
  16. BIN
      skirmish-game/skirmish.o
  17. BIN
      skirmish-game/skirmish.prg
  18. +19
    -0
      skirmish-game/src/c64.cfg
  19. +132
    -0
      skirmish-game/src/data.asm
  20. +303
    -0
      skirmish-game/src/enemy-ai.asm
  21. +391
    -0
      skirmish-game/src/game-logic.asm
  22. +112
    -0
      skirmish-game/src/input.asm
  23. +613
    -0
      skirmish-game/src/render.asm
  24. +136
    -0
      skirmish-game/src/skirmish.asm
  25. +40
    -0
      skirmish-game/src/skirmish.inc

+ 3
- 1
.claude/settings.local.json View File

@@ -13,7 +13,9 @@
"Bash(ca65 \"c:/Development/Commodore/os Experiments/c64os/samples/kernal-os-skeleton/kernal_os.asm\" -o \"c:/Development/Commodore/os Experiments/c64os/samples/kernal-os-skeleton/kernal_os.o\")",
"Bash(cmd /c build_all.bat)",
"Bash(cmd /c \"build_all.bat && echo BUILD_OK || echo BUILD_FAILED\")",
"Bash(echo \"EXIT: $?\")"
"Bash(echo \"EXIT: $?\")",
"PowerShell(Get-ChildItem -Path \"c:\\\\Development\\\\Commodore\\\\os Experiments\\\\c64os\\\\skirmish-game\\\\\" -Recurse -File | Select-Object FullName | ForEach-Object { $_.FullName })",
"PowerShell(Test-Path \"c:\\\\Development\\\\Commodore\\\\os Experiments\\\\c64os\\\\skirmish-game\\\\skirmish.prg\" && \\(Get-Item \"c:\\\\Development\\\\Commodore\\\\os Experiments\\\\c64os\\\\skirmish-game\\\\skirmish.prg\"\\).Length)"
]
}
}

+ 118
- 0
skirmish-game/IMPLEMENTATION.md View File

@@ -0,0 +1,118 @@
# Implementation Progress — Skirmish (C64)

## Status: BUILDABLE MVP BASELINE

Last updated: 2026-05-28

---

## What Exists Now

### Project structure

```
skirmish-game/
├── src/
│ ├── skirmish.asm # Entry point, raster sync, shared state
│ ├── input.asm # Keyboard + joystick polling
│ ├── game-logic.asm # Turn flow, movement, combat, occupancy
│ ├── enemy-ai.asm # Closest-target AI and movement
│ ├── render.asm # Sprites, HUD, board markers
│ ├── data.asm # Tables, text, sprite bitmaps
│ ├── skirmish.inc # Shared constants
│ └── c64.cfg # ld65 memory layout
├── docs/
│ ├── GDD.md
│ ├── TECHNICAL-PLAN.md
│ └── C64-CHEATSHEET.md
├── build.bat
├── run-vice.bat
├── debug-vice.bat
├── Makefile
├── README.md
└── skirmish.prg
```

### Core gameplay implemented

- 8x8 tactical board with coordinate markers
- 3 player units and 3 enemy units
- Single-step turn flow:
- player selects a unit
- player moves or attacks
- enemy team takes its turn
- win/lose is checked
- Adjacent melee combat for both teams
- Unit HP tracking and death removal
- Greedy enemy AI using Manhattan distance
- Selected-unit highlight via sprite color
- Restart flow after win or loss

### Controls implemented

- `Q` or joystick fire: cycle selected player unit
- `W`, `A`, `S`, `D` or joystick directions: move or attack
- `E` or space: end turn
- `R`: restart after game over

### Engine and rendering implemented

- BASIC stub starts game with `RUN` -> `SYS 2061`
- Raster-driven frame sync installed through IRQ vector
- Main loop updates once per IRQ tick
- 6 hardware sprites used for units
- HUD shows current phase, turn count, selected unit, and all unit HP
- Sprite bitmaps and lookup tables are linked into C64-visible RAM
- Occupancy grid rebuilt from unit state to avoid stale collision state

---

## Build Verification

### Confirmed

- `build.bat` assembles and links successfully with local `ca65` and `ld65`
- Output artifact currently builds as `skirmish.prg`

### Current artifact

- `skirmish.prg` size: 2617 bytes
- Most recent successful local build: 2026-05-28

### Not yet verified

- Full gameplay smoke test inside VICE
- Sprite alignment and board readability in the live emulator
- Joystick port 2 behavior in VICE input mapping
- End-to-end win/lose flow by playtesting

---

## Known Gaps

### High priority

1. Run the build in `x64sc` and verify the boot path, controls, and turn flow.
2. Confirm the raster-driven loop behaves correctly on a real VICE session.
3. Tune board readability if the current dot-grid is too sparse under sprites.

### Medium priority

1. Add attack and death feedback such as flash or simple SID beeps.
2. Add a stronger title/opening screen.
3. Add a clearer wait indicator and maybe a per-unit action prompt.

### Low priority

1. Add obstacles and richer pathing.
2. Add ranged attacks or special abilities.
3. Add difficulty scaling or multiple waves.

---

## Notes

- The previous tracker text described files that did not actually exist yet.
- The source tree and build scripts now exist and match this document.
- The PRG boot path was corrected so the BASIC stub jumps to the real contiguous load address.
- The next milestone should be emulator validation and gameplay polish, not project scaffolding.

+ 30
- 0
skirmish-game/Makefile View File

@@ -0,0 +1,30 @@
CA65 ?= ca65
LD65 ?= ld65

OBJS = skirmish.o input.o game-logic.o enemy-ai.o render.o data.o

all: skirmish.prg

skirmish.prg: $(OBJS)
$(LD65) $(OBJS) -C src/c64.cfg -o $@

skirmish.o: src/skirmish.asm src/skirmish.inc
$(CA65) src/skirmish.asm -o $@

input.o: src/input.asm src/skirmish.inc
$(CA65) src/input.asm -o $@

game-logic.o: src/game-logic.asm src/skirmish.inc
$(CA65) src/game-logic.asm -o $@

enemy-ai.o: src/enemy-ai.asm src/skirmish.inc
$(CA65) src/enemy-ai.asm -o $@

render.o: src/render.asm src/skirmish.inc
$(CA65) src/render.asm -o $@

data.o: src/data.asm src/skirmish.inc
$(CA65) src/data.asm -o $@

clean:
rm -f $(OBJS) skirmish.prg

+ 48
- 0
skirmish-game/README.md View File

@@ -0,0 +1,48 @@
# Skirmish

`Skirmish` is a single-screen C64 tactical prototype built in 6510 assembly with `ca65`/`ld65`.

## Controls

- `Q` or joystick fire: cycle the selected player unit
- `W`, `A`, `S`, `D` or joystick directions: move or attack
- `E` or space: end your turn without moving
- `R`: restart after a win or loss

## Build

### Windows

```bat
build.bat
```

### Cross-platform

```bash
make
```

## Run

### Normal

```bat
run-vice.bat
```

If you load it manually in BASIC, use `RUN`. The built PRG contains a BASIC stub that executes `SYS 2061`.

### With monitor

```bat
debug-vice.bat
```

## Current MVP

- 3 player units and 3 enemy units on an 8x8 board
- Keyboard and joystick input
- Single-step movement or adjacent melee attacks
- Greedy enemy AI using Manhattan distance
- Win/lose flow with restart

+ 13
- 0
skirmish-game/build.bat View File

@@ -0,0 +1,13 @@
@echo off
setlocal
cd /d "%~dp0"

ca65 src\skirmish.asm -o skirmish.o || exit /b 1
ca65 src\input.asm -o input.o || exit /b 1
ca65 src\game-logic.asm -o game-logic.o || exit /b 1
ca65 src\enemy-ai.asm -o enemy-ai.o || exit /b 1
ca65 src\render.asm -o render.o || exit /b 1
ca65 src\data.asm -o data.o || exit /b 1
ld65 skirmish.o input.o game-logic.o enemy-ai.o render.o data.o -C src\c64.cfg -o skirmish.prg || exit /b 1

echo Built skirmish.prg

BIN
skirmish-game/data.o View File


+ 6
- 0
skirmish-game/debug-vice.bat View File

@@ -0,0 +1,6 @@
@echo off
setlocal
cd /d "%~dp0"

call build.bat || exit /b 1
"C:\Program Files\GTK3VICE-3.10-win64\bin\x64sc.exe" -autostart skirmish.prg -autostart-warp -nativemonitor -keepmonopen -refreshonbreak

+ 333
- 0
skirmish-game/docs/C64-CHEATSHEET.md View File

@@ -0,0 +1,333 @@
# C64 6502 Assembly Cheat Sheet

---

## 1. Memory Map

```
$0000–$00FF Zero page (fast access, 2-byte instructions)
$0100–$01FF Stack (grows downward from $01FF)
$0200–$1FFF System/free area
$2000–$3FFF User RAM (40 KB available)
$D000–$DFFF I/O ports (VIC-II, SID, CIA1, CIA2)
$E000–$FFFF Kernal ROM (can be banked out for more RAM)
```

---

## 2. Key VIC-II Registers

All addresses relative to $D000 (e.g., $D010 = $D000 + $10).

### Sprite Coordinates

```
$D000–$D00F Sprite X positions (16 bytes, 2 per sprite)
$D000 = Sprite 0 X (low byte)
$D010 = X high bits (bit 0 = Sprite 0 X bit 8, etc.)

$D001–$D00F Sprite Y positions (16 bytes, 2 per sprite)
$D001 = Sprite 0 Y
$D003 = Sprite 1 Y
... (every other byte)
```

### Sprite Control

```
$D015 Sprite enable ($00 = all off, $3F = all 6 on)
$D017 Sprite Y expand (1 = double height)
$D01D Sprite X expand (1 = double width)
$D025 Sprite 0 color
$D026 Sprite 1 color
... (one byte per sprite)

$D027 Sprite 0 multicolor (0–3 for multicolor mode)
$D028 Sprite 1 multicolor
...
$D01C Multicolor mode enable (bit = sprite index)
```

### Screen Control

```
$D011 VIC control register 1
Bit 7: Raster interrupt request
Bit 6: Extended color text mode
Bit 5: Bitmap mode
Bit 4: Blank screen (1 = blank)
Bits 3–0: Scroll Y

$D012 Raster line (read for current, write for interrupt trigger)

$D016 VIC control register 2 (scroll X, multicolor, etc.)

$D018 Memory control (screen addr, bitmap addr)
Default: $14 (screen at $0400, bitmap at $2000)
```

### Colors

```
$D020 Border color
$D021 Background color 0
$D022 Background color 1 (multicolor mode)
$D023 Background color 2 (multicolor mode)
$D024 Background color 3 (multicolor mode)
```

---

## 3. CIA (Complex Interface Adapter) — Input

### CIA1 ($DC00–$DC0F)

```
$DC00 Data direction register A (keyboard columns)
$DC01 Keyboard matrix input register
Bit 0 = Key Row
Bit 1 = Key Row, etc.

$DC02 Data direction register B
$DC03 Joystick / paddle input

$DC04–$DC05 Timer A (low/high)
$DC06–$DC07 Timer B (low/high)
$DC0E Control register A (timer, interrupt)
$DC0F Interrupt enable register
```

### Simple Joystick Read

```
LDA #$00 ; Set port B for input
STA $DC02
LDA $DC01 ; Read joystick (port 2) or keyboard
; Bit pattern: 7=up, 6=down, 5=left, 4=right, 3=fire
```

---

## 4. Common 6502 Instructions

### Load/Store

```asm
LDA #$42 ; Load immediate
LDA $42 ; Load from zero page
LDA $4200 ; Load from absolute address
LDA $4200,X ; Indexed
LDA ($42,X) ; Indirect indexed
STA $42 ; Store
```

### Arithmetic

```asm
ADC #$10 ; Add with carry
SBC #$10 ; Subtract with carry
INC $42 ; Increment memory
DEC $42 ; Decrement memory
INX, INY, DEX, DEY ; Increment/decrement registers
```

### Branching

```asm
BNE $4000 ; Branch if not equal (after CMP, etc.)
BEQ $4000 ; Branch if equal
BCS $4000 ; Branch if carry set
BCC $4000 ; Branch if carry clear
BPL $4000 ; Branch if plus (MSB = 0)
BMI $4000 ; Branch if minus (MSB = 1)
JMP $4000 ; Unconditional jump
JSR $4000 ; Jump to subroutine (push return address)
RTS ; Return from subroutine
```

### Comparisons

```asm
CMP #$10 ; Compare A with immediate (sets flags)
CMP $42 ; Compare A with memory
CPX #$10 ; Compare X
CPY #$10 ; Compare Y
; Flags: Z=zero (equal), C=carry (unsigned <), N=minus
```

### Bit Operations

```asm
ASL ; Arithmetic shift left (A or M)
LSR ; Logical shift right
ROL ; Rotate left
ROR ; Rotate right
AND #$0F ; Bitwise AND
ORA #$F0 ; Bitwise OR
EOR #$FF ; Bitwise XOR
BIT $42 ; Test bits (sets N, V, Z)
```

### Stack

```asm
PHA ; Push A
PLA ; Pull A
PHP ; Push status
PLP ; Pull status
```

---

## 5. Interrupt Handling

### Setup IRQ Handler

```asm
; In initialization:
LDA #<IRQ_HANDLER ; Low byte of handler address
STA $0314
LDA #>IRQ_HANDLER ; High byte
STA $0315

CLI ; Clear interrupt disable flag
```

### IRQ Handler Structure

```asm
IRQ_HANDLER:
PHA ; Save registers
TXA
PHA
TYA
PHA
; Check if it's our interrupt (raster or timer)
LDA $D019 ; VIC interrupt status
BIT #$01 ; Test raster interrupt
BEQ NOT_VIC
; Handle VIC/raster interrupt
; (your game logic here)
LDA #$01 ; Acknowledge interrupt
STA $D019
NOT_VIC:
PLA ; Restore registers
TAY
PLA
TAX
PLA
RTI
```

---

## 6. Sprite Data Format

C64 sprites are 24 pixels wide × 21 pixels high, stored as 63 bytes (3 bytes/line).

**Location:** Default at $3000–$3FFF (can be changed via $D018)

**Format:**
```
Bytes 0–2: Line 0 (pixel data)
Bytes 3–5: Line 1
...
Bytes 60–62: Line 20
```

Each 3-byte line = 24 pixels (bit pattern).

**Example: Simple square**
```
$38 $38 $00 (11000000 11000000 00000000 = two vertical bars)
$38 $38 $00
...
```

---

## 7. Useful Zero-Page Patterns

```asm
; Temporary registers (safe to overwrite)
TEMP0 = $F0
TEMP1 = $F1
TEMP2 = $F2
TEMP3 = $F3

; Loop counters
LOOP_X = $F4
LOOP_Y = $F5
LOOP_C = $F6

; Pointers (16-bit address)
PTR0 = $F8 ; PTR0_L = $F8, PTR0_H = $F9
PTR1 = $FA
```

---

## 8. Quick Patterns

### Wait for Raster (Sync to VIC)

```asm
WAIT_RASTER:
LDA #<RASTER_LINE ; e.g., $80 (line 128)
CMP $D012
BNE WAIT_RASTER
```

### Copy Memory Block

```asm
; Copy from $2000 to $4000, 256 bytes
LDX #$00
COPY_LOOP:
LDA $2000,X
STA $4000,X
INX
BNE COPY_LOOP
```

### Fill Memory

```asm
; Fill $2000–$27FF with $00
LDA #$00
LDX #$00
FILL_LOOP:
STA $2000,X
INX
BNE FILL_LOOP
INC $2001 ; Move to next page
CPX #$08 ; 8 pages × 256 bytes = 2048
BNE FILL_LOOP
```

---

## 9. Useful Resources

- **nesdev.com** — 6502 reference (works for C64, mostly)
- **c64-wiki.de** — C64 hardware memory map and I/O
- **VICE documentation** — Emulator & debugger help
- **Commodore 64 Programmer's Reference Guide** (online PDF)

---

## 10. Common Mistakes

| Mistake | Fix |
|---------|-----|
| Forgetting to acknowledge interrupt ($D019) | Add `LDA #$01; STA $D019` before RTI |
| Sprite addresses hard-coded | Use $D018 to configure memory address |
| Not disabling interrupts when modifying $D018 | Use SEI before, CLI after |
| Assuming screen is at $0400 | Verify with $D018 or use absolute address |
| Overflow in 8-bit math (ADC/SBC) | Check carry flag after operation |
| Infinite loop in interrupt | Ensure interrupt flag is cleared and handler returns quickly |


+ 158
- 0
skirmish-game/docs/GDD.md View File

@@ -0,0 +1,158 @@
# Game Design Document — Skirmish
## Commodore 64 | Single-Screen Strategy Skirmish

---

## 1. High Concept

**Skirmish** is a turn-based tactical game where the player commands 3–4 units on a single-screen grid to defeat an enemy army. Each turn, the player moves one unit, then all enemy units act via AI. The game ends when either the player or enemy is eliminated.

**Target player:** You (learning 6502 assembly + game systems design)
**Duration:** 15–30 min per playthrough
**Tone:** Minimal, abstract, focused on strategy

---

## 2. Core Mechanics

### 2.1 Grid & Movement

- **Playfield:** 8×8 grid (64 cells)
- **Each cell:** 1 sprite maximum (unit or obstacle)
- **Player units (color 1):** 3 units, start at left side
- **Enemy units (color 2):** 3 units, start at right side
- **Movement:** Each player unit can move 1–2 cells per turn (no diagonal, or allow 8-directional)

### 2.2 Combat

- **Attack:** Unit attacks adjacent enemy (no projectiles yet)
- **Damage:** 1 HP per hit; units die at 0 HP
- **Defense:** Simple: no mitigation. (Optional: units can "defend" to reduce damage next turn)
- **Range:** Melee only (adjacent cells)

### 2.3 Turn Order

1. **Player phase:** Select active unit → move OR attack → advance turn
2. **Enemy phase:** Each enemy AI acts once (move toward nearest player unit, or attack if adjacent)
3. **Check:** Win/lose condition
4. **Repeat**

### 2.4 Win & Lose Conditions

- **Win:** Eliminate all enemy units (player score: turns survived)
- **Lose:** All player units eliminated (game over)
- **No time limit:** Skirmish ends when one army is destroyed

---

## 3. Player Experience

### Single Turn (High Level)

1. Player sees 8×8 grid with 6 units, each labeled with HP
2. Player selects a unit (highlight it, show valid moves)
3. Player moves the unit or confirms an attack
4. Enemy units move and attack simultaneously (or sequentially, player watches)
5. Feedback: HP updates, units die and vanish
6. Next turn or game end

### Feedback & Clarity

- **Cursor/highlight:** Show selected unit in white or inverted video
- **Valid moves:** Highlight reachable cells (optional: show with different color)
- **Damage/death:** Flash sprite or simple removal
- **Text:** "PLAYER 1" / "ENEMY 1" labels; HP display (optional: on-screen or in status line)
- **Audio:** Simple beep for move, different tone for attack/death (SID optional for MVP)

---

## 4. Scope & Assets

### 4.1 Sprites

- **Player unit:** 1 sprite design (3 copies, different color)
- **Enemy unit:** 1 sprite design (3 copies, different color)
- **Optional selection indicator:** White box or highlight (can be software-rendered text)

### 4.2 Map/Tiles

- **Option A (Simple):** Solid color background, 8×8 grid drawn with text or thin lines
- **Option B (Nice):** Simple tile-based floor (grass/stone pattern)
- **No scrolling, no complex collision tiles**

### 4.3 Text & UI

- **Status line:** "YOUR TURN" / "ENEMY TURN" / "YOU WIN" / "GAME OVER"
- **Unit display:** Sprite label + HP in top-left corner
- **Selection:** Text prompt "SELECT UNIT: 1–3" or directional highlight

### 4.4 Audio

- **MVP:** None or single-tone beeps via SID register write
- **Nice-to-have:** Attack sound, death sound, win/lose fanfare (SID notes, not music yet)

---

## 5. Rules Summary (Cheat Sheet)

| Action | Cost | Effect |
|--------|------|--------|
| Move unit | 1 turn | Occupies new cell |
| Attack enemy | 1 turn | Adjacent only, 1 HP damage |
| End turn | — | All enemies act |
| Unit reaches 0 HP | — | Unit removed from grid |

---

## 6. AI Behavior (Enemy Logic)

Each enemy unit, on its turn:

1. **Find closest player unit** (using Manhattan distance)
2. **If adjacent:** Attack it
3. **Else:** Move 1 step toward it (greedy: move horizontally or vertically closer)
4. **Tie-break:** If two closest units, pick the one closest to the grid center (tie-break)

This creates simple but engaging enemy behavior with minimal CPU cost.

---

## 7. Technical Constraints (C64-Specific)

- **Resolution:** 320×200 (standard NTSC, 280×192 on PAL)
- **Grid rendering:** Text or sprite-based; ideally ~40×25 cells → small cell size
- **Sprites:** 3 player + 3 enemy + 1 selection cursor = 7 max (can multiplex if needed)
- **Memory:** Code + zero-page state + game state array fits in ~8 KB
- **No sound initially:** Skip SID; add it after mechanics work

---

## 8. Success Criteria (MVP)

- [ ] Grid draws on screen, shows 6 units
- [ ] Player can select and move a unit (no collision yet)
- [ ] Enemy units move toward player
- [ ] Attack damage and death work
- [ ] Win/lose detection functional
- [ ] Game is playable end-to-end in <30 min

---

## 9. Future Enhancements (Post-MVP)

- Obstacles/walls on grid
- Ranged attacks
- Special abilities (e.g., "defend" or "heal")
- Multiple waves of enemies
- Score/leaderboard
- SID audio (music, sfx)
- Difficulty levels

---

## 10. References & Inspiration

- **Chess/Checkers:** Simple grid, turn order
- **Tactics Ogre / Fire Emblem:** Turn-based tactical structure (scaled down)
- **Worms / Cannon Fodder:** Unit-focused combat (simplified)


+ 300
- 0
skirmish-game/docs/TECHNICAL-PLAN.md View File

@@ -0,0 +1,300 @@
# Technical Plan — Skirmish (C64 6502 Assembly)

---

## 1. Architecture Overview

```
┌─────────────────────────────────────┐
│ 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) │
└─────────────────────────────────────┘
```

---

## 2. Memory Layout (64 KB)

```
$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.)
```

---

## 3. Zero-Page Variables (Critical Path)

```asm
$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)
```

---

## 4. Grid Representation

**Physical Grid:** 8×8 = 64 cells
**Occupancy Array:** $6000–$603F (one byte per cell, index = Y*8 + X)
- $FF = empty
- 0–5 = unit index (0–5)

On each frame, rebuild occupancy from unit state array.

**Sprite Rendering:**
- Each unit is a single C64 sprite (13×21 pixels, or 12×21)
- Position calculation: `X_pixel = (unit_x * 40) + offset`, `Y_pixel = (unit_y * 25) + offset`
- Adjust offsets to center sprite in grid cell

---

## 5. Main Game Loop

```asm
; 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
```

---

## 6. Subroutine Checklist

### Input & Selection

| 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 |

### Game Logic

| 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 |

### Rendering

| 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 |

---

## 7. Key Algorithms

### Manhattan Distance

```asm
; 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
```

### Grid Occupancy Check

```asm
; 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
```

---

## 8. State Transitions

```
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]
```

---

## 9. Build & Run

**Development tools:**
- **cc65** (C-to-6502 compiler, includes assembler `ca65` and linker `ld65`)
- **VICE** (Commodore 64 emulator, with debugger)
- **Makefile** or shell script to assemble & link

**Command sequence:**
```bash
ca65 main.asm -o main.o # Assemble
ld65 main.o -C c64.cfg -o game.prg # Link
x64sc game.prg # Run in VICE
```

---

## 10. Implementation Order (MVP Priority)

1. **Kernel & Interrupt Handler** — Get IRQ handler ticking
2. **Sprite Rendering** — Display 6 units on screen
3. **Grid & Occupancy** — Manage unit positions
4. **Input & Unit Selection** — Player can select a unit
5. **Movement & Collision** — Player can move units (no combat yet)
6. **Combat & Damage** — Attack resolves HP
7. **Enemy AI** — Simple greedy pathfinding
8. **Win/Lose Detection** — Game ends correctly
9. **Polish** — Text output, feedback, cleanup

---

## 11. Known Pitfalls & Mitigations

| 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 |

---

## 12. File Structure

```
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
```

---

## 13. Next Steps

1. **Choose assembler:** cc65 + ca65/ld65 (recommended) or ACME
2. **Set up build environment** on your machine
3. **Write skeleton main.asm** (entry point + IRQ stub)
4. **Create sprite test** (render 6 colored squares)
5. **Implement zero-page variables**
6. **Add input handler** (keyboard or joystick)
7. **Iterate gameplay logic**


BIN
skirmish-game/enemy-ai.o View File


BIN
skirmish-game/game-logic.o View File


BIN
skirmish-game/input.o View File


BIN
skirmish-game/render.o View File


+ 6
- 0
skirmish-game/run-vice.bat View File

@@ -0,0 +1,6 @@
@echo off
setlocal
cd /d "%~dp0"

call build.bat || exit /b 1
"C:\Program Files\GTK3VICE-3.10-win64\bin\x64sc.exe" -autostart skirmish.prg -autostart-warp

BIN
skirmish-game/skirmish.o View File


BIN
skirmish-game/skirmish.prg View File


+ 19
- 0
skirmish-game/src/c64.cfg View File

@@ -0,0 +1,19 @@
MEMORY {
ZP: start = $0030, size = $00D0, type = rw, define = yes;
LOADADDR: start = $07FF, size = $0002, type = rw, file = %O;
RAM: start = $0801, size = $17FF, type = rw, file = %O;
OCCUPY: start = $2000, size = $0040, type = rw, define = yes;
BSSRAM: start = $2040, size = $0200, type = rw, define = yes;
}

SEGMENTS {
ZEROPAGE: load = ZP, type = zp;
LOADADDR: load = LOADADDR, type = ro;
EXEHDR: load = RAM, type = ro;
CODE: load = RAM, type = ro;
RODATA: load = RAM, type = ro;
DATA: load = RAM, type = rw;
SPRITES: load = RAM, type = ro, align = $40;
OCCUPANCY: load = OCCUPY, type = bss, define = yes;
BSS: load = BSSRAM, type = bss, define = yes;
}

+ 132
- 0
skirmish-game/src/data.asm View File

@@ -0,0 +1,132 @@
.include "skirmish.inc"

.export grid_x_low
.export grid_x_msb
.export grid_y_pos
.export sprite_bits
.export screen_row_lo
.export screen_row_hi
.export color_row_lo
.export color_row_hi

.export title_text
.export player_turn_text
.export enemy_turn_text
.export win_text
.export lose_text
.export controls_text
.export turn_text
.export select_text
.export player_label_text
.export enemy_label_text

.export player_sprite
.export enemy_sprite

.segment "RODATA"
grid_x_low:
.byte 60, 92, 124, 156, 188, 220, 252, 28
grid_x_msb:
.byte 0, 0, 0, 0, 0, 0, 0, 1
grid_y_pos:
.byte 60, 81, 102, 123, 144, 165, 186, 207

sprite_bits:
.byte $01, $02, $04, $08, $10, $20

screen_row_lo:
.byte <(SCREEN_RAM + 40 * 0), <(SCREEN_RAM + 40 * 1), <(SCREEN_RAM + 40 * 2), <(SCREEN_RAM + 40 * 3), <(SCREEN_RAM + 40 * 4)
.byte <(SCREEN_RAM + 40 * 5), <(SCREEN_RAM + 40 * 6), <(SCREEN_RAM + 40 * 7), <(SCREEN_RAM + 40 * 8), <(SCREEN_RAM + 40 * 9)
.byte <(SCREEN_RAM + 40 * 10), <(SCREEN_RAM + 40 * 11), <(SCREEN_RAM + 40 * 12), <(SCREEN_RAM + 40 * 13), <(SCREEN_RAM + 40 * 14)
.byte <(SCREEN_RAM + 40 * 15), <(SCREEN_RAM + 40 * 16), <(SCREEN_RAM + 40 * 17), <(SCREEN_RAM + 40 * 18), <(SCREEN_RAM + 40 * 19)
.byte <(SCREEN_RAM + 40 * 20), <(SCREEN_RAM + 40 * 21), <(SCREEN_RAM + 40 * 22), <(SCREEN_RAM + 40 * 23), <(SCREEN_RAM + 40 * 24)
screen_row_hi:
.byte >(SCREEN_RAM + 40 * 0), >(SCREEN_RAM + 40 * 1), >(SCREEN_RAM + 40 * 2), >(SCREEN_RAM + 40 * 3), >(SCREEN_RAM + 40 * 4)
.byte >(SCREEN_RAM + 40 * 5), >(SCREEN_RAM + 40 * 6), >(SCREEN_RAM + 40 * 7), >(SCREEN_RAM + 40 * 8), >(SCREEN_RAM + 40 * 9)
.byte >(SCREEN_RAM + 40 * 10), >(SCREEN_RAM + 40 * 11), >(SCREEN_RAM + 40 * 12), >(SCREEN_RAM + 40 * 13), >(SCREEN_RAM + 40 * 14)
.byte >(SCREEN_RAM + 40 * 15), >(SCREEN_RAM + 40 * 16), >(SCREEN_RAM + 40 * 17), >(SCREEN_RAM + 40 * 18), >(SCREEN_RAM + 40 * 19)
.byte >(SCREEN_RAM + 40 * 20), >(SCREEN_RAM + 40 * 21), >(SCREEN_RAM + 40 * 22), >(SCREEN_RAM + 40 * 23), >(SCREEN_RAM + 40 * 24)

color_row_lo:
.byte <(COLOR_RAM + 40 * 0), <(COLOR_RAM + 40 * 1), <(COLOR_RAM + 40 * 2), <(COLOR_RAM + 40 * 3), <(COLOR_RAM + 40 * 4)
.byte <(COLOR_RAM + 40 * 5), <(COLOR_RAM + 40 * 6), <(COLOR_RAM + 40 * 7), <(COLOR_RAM + 40 * 8), <(COLOR_RAM + 40 * 9)
.byte <(COLOR_RAM + 40 * 10), <(COLOR_RAM + 40 * 11), <(COLOR_RAM + 40 * 12), <(COLOR_RAM + 40 * 13), <(COLOR_RAM + 40 * 14)
.byte <(COLOR_RAM + 40 * 15), <(COLOR_RAM + 40 * 16), <(COLOR_RAM + 40 * 17), <(COLOR_RAM + 40 * 18), <(COLOR_RAM + 40 * 19)
.byte <(COLOR_RAM + 40 * 20), <(COLOR_RAM + 40 * 21), <(COLOR_RAM + 40 * 22), <(COLOR_RAM + 40 * 23), <(COLOR_RAM + 40 * 24)
color_row_hi:
.byte >(COLOR_RAM + 40 * 0), >(COLOR_RAM + 40 * 1), >(COLOR_RAM + 40 * 2), >(COLOR_RAM + 40 * 3), >(COLOR_RAM + 40 * 4)
.byte >(COLOR_RAM + 40 * 5), >(COLOR_RAM + 40 * 6), >(COLOR_RAM + 40 * 7), >(COLOR_RAM + 40 * 8), >(COLOR_RAM + 40 * 9)
.byte >(COLOR_RAM + 40 * 10), >(COLOR_RAM + 40 * 11), >(COLOR_RAM + 40 * 12), >(COLOR_RAM + 40 * 13), >(COLOR_RAM + 40 * 14)
.byte >(COLOR_RAM + 40 * 15), >(COLOR_RAM + 40 * 16), >(COLOR_RAM + 40 * 17), >(COLOR_RAM + 40 * 18), >(COLOR_RAM + 40 * 19)
.byte >(COLOR_RAM + 40 * 20), >(COLOR_RAM + 40 * 21), >(COLOR_RAM + 40 * 22), >(COLOR_RAM + 40 * 23), >(COLOR_RAM + 40 * 24)

title_text:
.byte 19, 11, 9, 18, 13, 9, 19, 8, 0
player_turn_text:
.byte 16, 12, 1, 25, 5, 18, 32, 20, 21, 18, 14, 0
enemy_turn_text:
.byte 5, 14, 5, 13, 25, 32, 20, 8, 9, 14, 11, 9, 14, 7, 0
win_text:
.byte 25, 15, 21, 32, 23, 9, 14, 0
lose_text:
.byte 7, 1, 13, 5, 32, 15, 22, 5, 18, 0
controls_text:
.byte 17, 32, 19, 5, 12, 32, 23, 1, 19, 4, 32, 13, 15, 22, 5, 32, 5, 32, 23, 1, 9, 20, 32, 18, 32, 18, 5, 19, 20, 1, 18, 20, 0
turn_text:
.byte 20, 21, 18, 14, 32, 0
select_text:
.byte 19, 5, 12, 32, 0
player_label_text:
.byte 16, 0
enemy_label_text:
.byte 5, 0

.segment "SPRITES"
.align 64
player_sprite:
.byte $00, $18, $00
.byte $00, $3C, $00
.byte $00, $7E, $00
.byte $00, $FF, $00
.byte $01, $FF, $80
.byte $03, $FF, $C0
.byte $07, $FF, $E0
.byte $0F, $FF, $F0
.byte $1F, $FF, $F8
.byte $3F, $FF, $FC
.byte $3F, $7E, $FC
.byte $1E, $3C, $78
.byte $0C, $18, $30
.byte $0C, $18, $30
.byte $1E, $3C, $78
.byte $3F, $24, $FC
.byte $33, $24, $CC
.byte $21, $24, $84
.byte $01, $C3, $80
.byte $00, $C3, $00
.byte $00, $42, $00
.byte $00

enemy_sprite:
.byte $00, $00, $00
.byte $03, $C3, $C0
.byte $07, $E7, $E0
.byte $0F, $FF, $F0
.byte $1E, $7E, $78
.byte $3C, $3C, $3C
.byte $78, $18, $1E
.byte $F0, $00, $0F
.byte $E0, $00, $07
.byte $C3, $81, $C3
.byte $C7, $FF, $E3
.byte $EF, $FF, $F7
.byte $7F, $FF, $FE
.byte $3F, $FF, $FC
.byte $1F, $FF, $F8
.byte $0F, $7E, $F0
.byte $06, $24, $60
.byte $0C, $3C, $30
.byte $18, $24, $18
.byte $30, $00, $0C
.byte $00, $00, $00
.byte $00

+ 303
- 0
skirmish-game/src/enemy-ai.asm View File

@@ -0,0 +1,303 @@
.include "skirmish.inc"

.export ENEMY_AI_TURN

.import REBUILD_OCCUPANCY
.import UNIT_ATTACK
.import ENSURE_SELECTED_UNIT

.importzp temp0
.importzp temp1
.importzp temp2
.importzp temp3
.importzp temp4
.importzp temp5
.importzp temp6
.importzp temp7

.import unit_x
.import unit_y
.import unit_hp
.import unit_team
.import grid_occupancy

.segment "CODE"
ENEMY_AI_TURN:
ldx #PLAYER_COUNT
enemy_loop:
cpx #UNIT_COUNT
beq enemy_done
lda unit_hp,x
beq next_enemy

stx temp7
jsr FIND_CLOSEST_PLAYER
cpy #$FF
beq restore_enemy
jsr ENEMY_ACT
restore_enemy:
ldx temp7
next_enemy:
inx
jmp enemy_loop

enemy_done:
jsr ENSURE_SELECTED_UNIT
rts

FIND_CLOSEST_PLAYER:
ldx temp7
lda unit_x,x
sta temp0
lda unit_y,x
sta temp1

lda #$FF
sta temp2
sta temp3
ldy #$FF

ldx #0
find_target_loop:
lda unit_hp,x
beq next_target

lda temp0
cmp unit_x,x
bcs :+
lda unit_x,x
sec
sbc temp0
jmp :++
: sec
sbc unit_x,x
: sta temp4

lda temp1
cmp unit_y,x
bcs :+
lda unit_y,x
sec
sbc temp1
jmp :++
: sec
sbc unit_y,x
: clc
adc temp4
sta temp4

lda unit_x,x
cmp #3
bcs :+
sec
lda #3
sbc unit_x,x
jmp :++
: sec
sbc #3
: sta temp5

lda unit_y,x
cmp #3
bcs :+
sec
lda #3
sbc unit_y,x
jmp :++
: sec
sbc #3
: clc
adc temp5
sta temp5

lda temp4
cmp temp2
bcc better_target
bne next_target
lda temp5
cmp temp3
bcs next_target

better_target:
lda temp4
sta temp2
lda temp5
sta temp3
txa
tay

next_target:
inx
cpx #PLAYER_COUNT
bne find_target_loop
rts

ENEMY_ACT:
sty temp6
ldx temp7

lda unit_x,x
sta temp2
lda unit_y,x
sta temp3

ldy temp6
lda unit_x,y
sta temp4
lda unit_y,y
sta temp5

lda temp2
cmp temp4
bcs :+
lda temp4
sec
sbc temp2
jmp :++
: sec
sbc temp4
: sta temp0

lda temp3
cmp temp5
bcs :+
lda temp5
sec
sbc temp3
jmp :++
: sec
sbc temp5
: sta temp1

lda temp0
clc
adc temp1
cmp #1
bne move_enemy

ldx temp7
ldy temp6
jsr UNIT_ATTACK
jsr REBUILD_OCCUPANCY
rts

move_enemy:
jsr CALC_X_STEP
sta temp2
jsr CALC_Y_STEP
sta temp3

lda temp0
cmp temp1
bcc try_y_first

try_x_first:
lda temp2
beq try_y_after_x
jsr TRY_X_MOVE
bcs enemy_move_done
try_y_after_x:
lda temp3
beq enemy_move_fail
jsr TRY_Y_MOVE
bcs enemy_move_done
jmp enemy_move_fail

try_y_first:
lda temp3
beq try_x_after_y
jsr TRY_Y_MOVE
bcs enemy_move_done
try_x_after_y:
lda temp2
beq enemy_move_fail
jsr TRY_X_MOVE
bcs enemy_move_done

enemy_move_fail:
clc
rts

enemy_move_done:
jsr REBUILD_OCCUPANCY
sec
rts

CALC_X_STEP:
ldx temp7
ldy temp6
lda unit_x,y
cmp unit_x,x
beq x_step_zero
bcc x_step_left
lda #1
rts
x_step_left:
lda #$FF
rts
x_step_zero:
lda #0
rts

CALC_Y_STEP:
ldx temp7
ldy temp6
lda unit_y,y
cmp unit_y,x
beq y_step_zero
bcc y_step_up
lda #1
rts
y_step_up:
lda #$FF
rts
y_step_zero:
lda #0
rts

TRY_X_MOVE:
ldx temp7
lda unit_x,x
clc
adc temp2
sta temp4
cmp #8
bcs move_fail
lda unit_y,x
sta temp5
jmp TRY_MOVE_COMMON

TRY_Y_MOVE:
ldx temp7
lda unit_x,x
sta temp4
lda unit_y,x
clc
adc temp3
sta temp5
cmp #8
bcs move_fail

TRY_MOVE_COMMON:
lda temp5
asl
asl
asl
clc
adc temp4
tay
lda grid_occupancy,y
cmp #EMPTY_CELL
bne move_fail

ldx temp7
lda temp4
sta unit_x,x
lda temp5
sta unit_y,x
sec
rts

move_fail:
clc
rts

+ 391
- 0
skirmish-game/src/game-logic.asm View File

@@ -0,0 +1,391 @@
.include "skirmish.inc"

.export INIT_GAME
.export UPDATE_GAME
.export REBUILD_OCCUPANCY
.export UNIT_ATTACK
.export ENSURE_SELECTED_UNIT

.import ENEMY_AI_TURN

.importzp input_flags
.importzp game_state
.importzp selected_unit
.importzp turn_counter
.importzp player_units
.importzp enemy_units
.importzp move_dx
.importzp move_dy
.importzp action_result
.importzp temp0
.importzp temp1
.importzp temp2
.importzp temp3
.importzp temp4
.importzp temp5

.import unit_x
.import unit_y
.import unit_hp
.import unit_team
.import grid_occupancy

.segment "CODE"
INIT_GAME:
lda #STATE_PLAYER
sta game_state
lda #0
sta turn_counter
sta action_result
lda #0
sta selected_unit

lda #PLAYER_COUNT
sta player_units
lda #ENEMY_COUNT
sta enemy_units

ldx #0
clear_units:
lda #0
sta unit_x,x
sta unit_y,x
sta unit_hp,x
sta unit_team,x
inx
cpx #UNIT_COUNT
bne clear_units

lda #0
sta unit_x+0
sta unit_x+1
sta unit_x+2
lda #7
sta unit_x+3
sta unit_x+4
sta unit_x+5

lda #1
sta unit_y+0
lda #3
sta unit_y+1
lda #5
sta unit_y+2
lda #1
sta unit_y+3
lda #3
sta unit_y+4
lda #5
sta unit_y+5

lda #3
ldx #0
hp_loop:
sta unit_hp,x
inx
cpx #UNIT_COUNT
bne hp_loop

lda #TEAM_PLAYER
sta unit_team+0
sta unit_team+1
sta unit_team+2
lda #TEAM_ENEMY
sta unit_team+3
sta unit_team+4
sta unit_team+5

jsr REBUILD_OCCUPANCY
rts

UPDATE_GAME:
jsr REBUILD_OCCUPANCY

lda game_state
cmp #STATE_PLAYER
beq update_player
cmp #STATE_ENEMY
beq update_enemy
cmp #STATE_CHECK
beq update_check
jmp update_end

update_player:
jsr ENSURE_SELECTED_UNIT

lda input_flags
and #INPUT_NEXT
beq :+
jsr CYCLE_SELECTED_UNIT
rts
:
lda input_flags
and #INPUT_FIRE
beq :+
jsr CYCLE_SELECTED_UNIT
rts
:
lda input_flags
and #INPUT_WAIT
beq :+
lda #STATE_ENEMY
sta game_state
rts
:
lda #0
sta move_dx
sta move_dy

lda input_flags
and #INPUT_UP
beq :+
lda #$FF
sta move_dy
jmp do_player_action
:
lda input_flags
and #INPUT_DOWN
beq :+
lda #1
sta move_dy
jmp do_player_action
:
lda input_flags
and #INPUT_LEFT
beq :+
lda #$FF
sta move_dx
jmp do_player_action
:
lda input_flags
and #INPUT_RIGHT
beq :+
lda #1
sta move_dx
jmp do_player_action
:
rts

do_player_action:
jsr UNIT_TRY_ACTION
lda action_result
beq :+
lda #STATE_ENEMY
sta game_state
:
rts

update_enemy:
jsr ENEMY_AI_TURN
inc turn_counter
lda #STATE_CHECK
sta game_state
rts

update_check:
jsr CHECK_WIN_LOSE
lda game_state
cmp #STATE_CHECK
bne :+
lda #STATE_PLAYER
sta game_state
:
rts

update_end:
lda input_flags
and #INPUT_RESTART
beq :+
jsr INIT_GAME
:
rts

UNIT_TRY_ACTION:
lda #0
sta action_result

ldx selected_unit
cpx #PLAYER_COUNT
bcs unit_try_done
lda unit_hp,x
beq unit_try_done

lda unit_x,x
clc
adc move_dx
sta temp0
cmp #8
bcs unit_try_done

lda unit_y,x
clc
adc move_dy
sta temp1
cmp #8
bcs unit_try_done

lda temp1
asl
asl
asl
clc
adc temp0
tay
lda grid_occupancy,y
cmp #EMPTY_CELL
beq move_player

tay
lda unit_team,y
cmp #TEAM_ENEMY
bne unit_try_done

ldx selected_unit
jsr UNIT_ATTACK_XY
lda #1
sta action_result
jmp unit_try_done

move_player:
ldx selected_unit
lda temp0
sta unit_x,x
lda temp1
sta unit_y,x
lda #1
sta action_result

unit_try_done:
rts

REBUILD_OCCUPANCY:
ldx #0
clear_grid:
lda #EMPTY_CELL
sta grid_occupancy,x
inx
cpx #64
bne clear_grid

ldx #0
grid_loop:
lda unit_hp,x
beq next_grid_unit
lda unit_y,x
asl
asl
asl
clc
adc unit_x,x
tay
txa
sta grid_occupancy,y
next_grid_unit:
inx
cpx #UNIT_COUNT
bne grid_loop
rts

CHECK_WIN_LOSE:
lda enemy_units
bne :+
lda #STATE_WIN
sta game_state
rts
:
lda player_units
bne :+
lda #STATE_LOSE
sta game_state
rts
:
lda #STATE_CHECK
sta game_state
rts

ENSURE_SELECTED_UNIT:
lda selected_unit
cmp #PLAYER_COUNT
bcs choose_first_player
tax
lda unit_hp,x
bne ensure_done

choose_first_player:
ldx #0
find_alive_player:
lda unit_hp,x
bne found_player
inx
cpx #PLAYER_COUNT
bne find_alive_player
lda #$FF
sta selected_unit
rts

found_player:
stx selected_unit
ensure_done:
rts

CYCLE_SELECTED_UNIT:
lda player_units
beq no_cycle_target

lda selected_unit
cmp #PLAYER_COUNT
bcc :+
lda #$FF
:
clc
adc #1
cmp #PLAYER_COUNT
bcc :+
lda #0
:
sta temp0
ldx #PLAYER_COUNT
cycle_search:
ldy temp0
lda unit_hp,y
bne cycle_found
inc temp0
lda temp0
cmp #PLAYER_COUNT
bcc :+
lda #0
sta temp0
:
dex
bne cycle_search

no_cycle_target:
lda #$FF
sta selected_unit
rts

cycle_found:
sty selected_unit
rts

UNIT_ATTACK:
jsr UNIT_ATTACK_XY
rts

UNIT_ATTACK_XY:
lda unit_hp,y
beq attack_done
sec
sbc #1
sta unit_hp,y
bne attack_done
lda unit_team,y
beq dead_player
dec enemy_units
jmp attack_done

dead_player:
dec player_units

attack_done:
rts

+ 112
- 0
skirmish-game/src/input.asm View File

@@ -0,0 +1,112 @@
.include "skirmish.inc"

.export INPUT_READ

.importzp input_flags
.importzp last_input_flags
.importzp temp0
.importzp temp1

.segment "CODE"
INPUT_READ:
lda #0
sta temp0

lda $DC00
sta temp1

lda temp1
and #$01
bne :+
lda temp0
ora #INPUT_UP
sta temp0
:
lda temp1
and #$02
bne :+
lda temp0
ora #INPUT_DOWN
sta temp0
:
lda temp1
and #$04
bne :+
lda temp0
ora #INPUT_LEFT
sta temp0
:
lda temp1
and #$08
bne :+
lda temp0
ora #INPUT_RIGHT
sta temp0
:
lda temp1
and #$10
bne :+
lda temp0
ora #INPUT_FIRE
sta temp0
:
jsr $FFE4
beq keyboard_done

cmp #'W'
bne :+
lda temp0
ora #INPUT_UP
sta temp0
:
cmp #'S'
bne :+
lda temp0
ora #INPUT_DOWN
sta temp0
:
cmp #'A'
bne :+
lda temp0
ora #INPUT_LEFT
sta temp0
:
cmp #'D'
bne :+
lda temp0
ora #INPUT_RIGHT
sta temp0
:
cmp #'Q'
bne :+
lda temp0
ora #INPUT_NEXT
sta temp0
:
cmp #' '
bne :+
lda temp0
ora #INPUT_WAIT
sta temp0
:
cmp #'E'
bne :+
lda temp0
ora #INPUT_WAIT
sta temp0
:
cmp #'R'
bne keyboard_done
lda temp0
ora #INPUT_RESTART
sta temp0

keyboard_done:
lda last_input_flags
eor #$FF
and temp0
sta input_flags

lda temp0
sta last_input_flags
rts

+ 613
- 0
skirmish-game/src/render.asm View File

@@ -0,0 +1,613 @@
.include "skirmish.inc"

.export INIT_RENDER
.export SPRITE_UPDATE
.export TEXT_UPDATE

.importzp game_state
.importzp selected_unit
.importzp turn_counter
.importzp player_units
.importzp enemy_units
.importzp temp0
.importzp temp1
.importzp temp2
.importzp temp3
.importzp temp4
.importzp temp5

.import unit_x
.import unit_y
.import unit_hp

.import grid_x_low
.import grid_x_msb
.import grid_y_pos
.import sprite_bits
.import screen_row_lo
.import screen_row_hi
.import color_row_lo
.import color_row_hi

.import title_text
.import player_turn_text
.import enemy_turn_text
.import win_text
.import lose_text
.import controls_text
.import turn_text
.import select_text
.import player_label_text
.import enemy_label_text
.import player_sprite
.import enemy_sprite

.segment "CODE"
INIT_RENDER:
lda #BORDER_COLOR
sta $D020
lda #BG_COLOR
sta $D021

lda #0
sta $D015
sta $D017
sta $D01B
sta $D01C
sta $D01D
sta $D010

jsr CLEAR_SCREEN_AND_COLOR
jsr DRAW_STATIC_UI
jsr SETUP_SPRITES
rts

SPRITE_UPDATE:
lda #0
sta temp0
sta temp1

ldx #0
sprite_loop:
lda unit_hp,x
beq hide_sprite

lda temp0
ora sprite_bits,x
sta temp0

stx temp4
txa
asl
tay

lda unit_x,x
tax
lda grid_x_low,x
sta $D000,y
lda grid_x_msb,x
beq :+
ldx temp4
lda temp1
ora sprite_bits,x
sta temp1
:
ldx temp4
lda unit_y,x
tax
lda grid_y_pos,x
sta $D001,y

ldx temp4
cpx #PLAYER_COUNT
bcs enemy_color
cpx selected_unit
bne player_color
lda #SPRITE_SELECTED_COLOR
bne store_color
player_color:
lda #SPRITE_PLAYER_COLOR
bne store_color
enemy_color:
lda #SPRITE_ENEMY_COLOR
store_color:
sta $D027,x
hide_sprite_after_pos:
jmp next_sprite

hide_sprite:
stx temp4
txa
asl
tay
lda #250
sta $D001,y
ldx temp4

next_sprite:
inx
cpx #UNIT_COUNT
bne sprite_loop

lda temp0
sta $D015
lda temp1
sta $D010
rts

TEXT_UPDATE:
ldx #1
jsr CLEAR_ROW
lda game_state
cmp #STATE_PLAYER
beq text_player
cmp #STATE_ENEMY
beq text_enemy
cmp #STATE_WIN
beq text_win
lda #<lose_text
sta temp2
lda #>lose_text
sta temp3
ldx #1
lda #14
jmp draw_state_line

text_player:
lda #<player_turn_text
sta temp2
lda #>player_turn_text
sta temp3
ldx #1
lda #13
jmp draw_state_line

text_enemy:
lda #<enemy_turn_text
sta temp2
lda #>enemy_turn_text
sta temp3
ldx #1
lda #7
jmp draw_state_line

text_win:
lda #<win_text
sta temp2
lda #>win_text
sta temp3
ldx #1
lda #5

draw_state_line:
sta temp4
jsr SET_SCREEN_PTR
lda #14
jsr ADD_TO_SCREEN_PTR
jsr WRITE_STRING
ldx #1
lda temp4
jsr COLOR_FULL_ROW

ldx #22
jsr CLEAR_ROW
ldx #22
jsr SET_SCREEN_PTR
lda #2
jsr ADD_TO_SCREEN_PTR
lda #<turn_text
sta temp2
lda #>turn_text
sta temp3
jsr WRITE_STRING
lda turn_counter
jsr WRITE_DECIMAL2

lda #14
jsr ADD_TO_SCREEN_PTR
lda #<select_text
sta temp2
lda #>select_text
sta temp3
jsr WRITE_STRING
lda selected_unit
cmp #$FF
beq no_selection
clc
adc #1
clc
adc #48
ldy #0
sta (temp0),y
jmp status_done
no_selection:
lda #45
ldy #0
sta (temp0),y
status_done:
ldx #22
lda #HUD_COLOR
jsr COLOR_FULL_ROW

ldx #23
jsr CLEAR_ROW
ldx #23
jsr SET_SCREEN_PTR
lda #2
jsr ADD_TO_SCREEN_PTR
lda #16
ldy #0
sta (temp0),y
iny
lda #49
sta (temp0),y
iny
lda #58
sta (temp0),y
iny
ldx #0
lda unit_hp,x
clc
adc #48
sta (temp0),y

iny
iny
lda #16
sta (temp0),y
iny
lda #50
sta (temp0),y
iny
lda #58
sta (temp0),y
iny
ldx #1
lda unit_hp,x
clc
adc #48
sta (temp0),y

iny
iny
lda #16
sta (temp0),y
iny
lda #51
sta (temp0),y
iny
lda #58
sta (temp0),y
iny
ldx #2
lda unit_hp,x
clc
adc #48
sta (temp0),y

iny
iny
lda #5
sta (temp0),y
iny
lda #49
sta (temp0),y
iny
lda #58
sta (temp0),y
iny
ldx #3
lda unit_hp,x
clc
adc #48
sta (temp0),y

iny
iny
lda #5
sta (temp0),y
iny
lda #50
sta (temp0),y
iny
lda #58
sta (temp0),y
iny
ldx #4
lda unit_hp,x
clc
adc #48
sta (temp0),y

iny
iny
lda #5
sta (temp0),y
iny
lda #51
sta (temp0),y
iny
lda #58
sta (temp0),y
iny
ldx #5
lda unit_hp,x
clc
adc #48
sta (temp0),y

ldx #23
lda #HUD_COLOR
jsr COLOR_FULL_ROW
rts

CLEAR_SCREEN_AND_COLOR:
ldx #0
clear_pages:
lda #32
sta SCREEN_RAM + $0000,x
sta SCREEN_RAM + $0100,x
sta SCREEN_RAM + $0200,x
sta SCREEN_RAM + $02E8,x

lda #HUD_COLOR
sta COLOR_RAM + $0000,x
sta COLOR_RAM + $0100,x
sta COLOR_RAM + $0200,x
sta COLOR_RAM + $02E8,x
inx
cpx #$E8
bne clear_pages
rts

DRAW_STATIC_UI:
ldx #0
jsr SET_SCREEN_PTR
lda #15
jsr ADD_TO_SCREEN_PTR
lda #<title_text
sta temp2
lda #>title_text
sta temp3
jsr WRITE_STRING
ldx #0
lda #7
jsr COLOR_FULL_ROW

ldx #24
jsr CLEAR_ROW
ldx #24
jsr SET_SCREEN_PTR
lda #1
jsr ADD_TO_SCREEN_PTR
lda #<controls_text
sta temp2
lda #>controls_text
sta temp3
jsr WRITE_STRING
ldx #24
lda #3
jsr COLOR_FULL_ROW

jsr DRAW_BOARD
rts

DRAW_BOARD:
ldx #0
board_top_labels:
lda screen_row_lo + (BOARD_ROW - 1)
sta temp0
lda screen_row_hi + (BOARD_ROW - 1)
sta temp1
txa
asl
asl
clc
adc #BOARD_COL
tay
txa
clc
adc #48
sta (temp0),y

lda color_row_lo + (BOARD_ROW - 1)
sta temp2
lda color_row_hi + (BOARD_ROW - 1)
sta temp3
lda #BOARD_COLOR
sta (temp2),y

txa
pha
txa
asl
clc
adc #BOARD_ROW
tax
jsr DRAW_BOARD_ROW
pla
tax
inx
cpx #8
bne board_top_labels
rts

DRAW_BOARD_ROW:
stx temp5
lda screen_row_lo,x
sta temp0
lda screen_row_hi,x
sta temp1
lda color_row_lo,x
sta temp2
lda color_row_hi,x
sta temp3

ldy #BOARD_COL
lda #46
sta (temp0),y
lda #BOARD_COLOR
sta (temp2),y
iny
iny
iny
lda #46
sta (temp0),y
lda #BOARD_COLOR
sta (temp2),y
iny
iny
iny
lda #46
sta (temp0),y
lda #BOARD_COLOR
sta (temp2),y
iny
iny
iny
lda #46
sta (temp0),y
lda #BOARD_COLOR
sta (temp2),y
iny
iny
iny
lda #46
sta (temp0),y
lda #BOARD_COLOR
sta (temp2),y
iny
iny
iny
lda #46
sta (temp0),y
lda #BOARD_COLOR
sta (temp2),y
iny
iny
iny
lda #46
sta (temp0),y
lda #BOARD_COLOR
sta (temp2),y
iny
iny
iny
lda #46
sta (temp0),y
lda #BOARD_COLOR
sta (temp2),y

lda temp5
sec
sbc #BOARD_ROW
lsr
clc
adc #48
ldy #2
sta (temp0),y
lda #BOARD_COLOR
sta (temp2),y
rts

SETUP_SPRITES:
lda #<(player_sprite / 64)
sta SPRITE_PTRS + 0
sta SPRITE_PTRS + 1
sta SPRITE_PTRS + 2
lda #<(enemy_sprite / 64)
sta SPRITE_PTRS + 3
sta SPRITE_PTRS + 4
sta SPRITE_PTRS + 5
rts

CLEAR_ROW:
jsr SET_SCREEN_PTR
ldy #0
clear_row_loop:
lda #32
sta (temp0),y
iny
cpy #40
bne clear_row_loop
rts

SET_SCREEN_PTR:
lda screen_row_lo,x
sta temp0
lda screen_row_hi,x
sta temp1
rts

ADD_TO_SCREEN_PTR:
clc
adc temp0
sta temp0
bcc :+
inc temp1
: rts

WRITE_STRING:
ldy #0
write_string_loop:
lda (temp2),y
beq write_string_done
sta (temp0),y
iny
bne write_string_loop
write_string_done:
tya
clc
adc temp0
sta temp0
bcc :+
inc temp1
: rts

WRITE_DECIMAL2:
sta temp4
ldx #0
decimal_tens_loop:
lda temp4
cmp #10
bcc decimal_store
sec
sbc #10
sta temp4
inx
jmp decimal_tens_loop

decimal_store:
ldy #0
txa
clc
adc #48
sta (temp0),y
iny
lda temp4
clc
adc #48
sta (temp0),y
iny
tya
clc
adc temp0
sta temp0
bcc :+
inc temp1
: rts

COLOR_FULL_ROW:
sta temp4
lda color_row_lo,x
sta temp2
lda color_row_hi,x
sta temp3
ldy #0
color_loop:
lda temp4
sta (temp2),y
iny
cpy #40
bne color_loop
rts

+ 136
- 0
skirmish-game/src/skirmish.asm View File

@@ -0,0 +1,136 @@
.include "skirmish.inc"

.exportzp input_flags
.exportzp last_input_flags
.exportzp game_state
.exportzp selected_unit
.exportzp turn_counter
.exportzp player_units
.exportzp enemy_units
.exportzp move_dx
.exportzp move_dy
.exportzp action_result
.exportzp frame_ready
.exportzp temp0
.exportzp temp1
.exportzp temp2
.exportzp temp3
.exportzp temp4
.exportzp temp5
.exportzp temp6
.exportzp temp7

.export unit_x
.export unit_y
.export unit_hp
.export unit_team
.export old_irq
.export grid_occupancy

.import INPUT_READ
.import INIT_GAME
.import UPDATE_GAME
.import INIT_RENDER
.import SPRITE_UPDATE
.import TEXT_UPDATE

.segment "LOADADDR"
.word $0801

.segment "EXEHDR"
.byte $0B, $08, $0A, $00, $9E
.byte "2061", $00, $00, $00

.segment "ZEROPAGE"
input_flags: .res 1
last_input_flags: .res 1
game_state: .res 1
selected_unit: .res 1
turn_counter: .res 1
player_units: .res 1
enemy_units: .res 1
move_dx: .res 1
move_dy: .res 1
action_result: .res 1
frame_ready: .res 1
temp0: .res 1
temp1: .res 1
temp2: .res 1
temp3: .res 1
temp4: .res 1
temp5: .res 1
temp6: .res 1
temp7: .res 1

.segment "BSS"
unit_x: .res UNIT_COUNT
unit_y: .res UNIT_COUNT
unit_hp: .res UNIT_COUNT
unit_team:.res UNIT_COUNT
old_irq: .res 2

.segment "OCCUPANCY"
grid_occupancy: .res 64

.segment "CODE"
start:
sei
jsr $FF81

lda #0
sta frame_ready
sta last_input_flags

lda $0314
sta old_irq
lda $0315
sta old_irq+1

jsr INIT_RENDER
jsr INIT_GAME
jsr install_irq
cli

main_loop:
lda frame_ready
beq main_loop
lda #0
sta frame_ready

jsr INPUT_READ
jsr UPDATE_GAME
jsr SPRITE_UPDATE
jsr TEXT_UPDATE
jmp main_loop

install_irq:
lda #<irq_handler
sta $0314
lda #>irq_handler
sta $0315

lda $D011
and #$7F
sta $D011
lda #$80
sta $D012

lda #$01
sta $D01A

lda $DC0D
lda $DD0D
lda #$01
sta $D019
rts

irq_handler:
lda $D019
and #$01
beq chain_irq
lda #$01
sta $D019
inc frame_ready

chain_irq:
jmp (old_irq)

+ 40
- 0
skirmish-game/src/skirmish.inc View File

@@ -0,0 +1,40 @@
UNIT_COUNT = 6
PLAYER_COUNT = 3
ENEMY_COUNT = 3

EMPTY_CELL = $FF

TEAM_PLAYER = 0
TEAM_ENEMY = 1

STATE_PLAYER = 0
STATE_ENEMY = 1
STATE_CHECK = 2
STATE_WIN = 3
STATE_LOSE = 4

INPUT_UP = $01
INPUT_DOWN = $02
INPUT_LEFT = $04
INPUT_RIGHT = $08
INPUT_FIRE = $10
INPUT_NEXT = $20
INPUT_WAIT = $40
INPUT_RESTART = $80

SCREEN_RAM = $0400
COLOR_RAM = $D800
SPRITE_PTRS = SCREEN_RAM + $03F8

BOARD_COL = 4
BOARD_ROW = 4
BOARD_PIXEL_X = 56
BOARD_PIXEL_Y = 60

SPRITE_PLAYER_COLOR = 13
SPRITE_SELECTED_COLOR = 7
SPRITE_ENEMY_COLOR = 10
HUD_COLOR = 1
BOARD_COLOR = 15
BG_COLOR = 6
BORDER_COLOR = 14

Loading…
Cancel
Save

Powered by TurnKey Linux.