From 0fb584bcd7ffc4a975d535d75f7676ed591a7ef8 Mon Sep 17 00:00:00 2001 From: Zsolt Tasnadi Date: Wed, 15 Jul 2026 21:31:51 +0200 Subject: [PATCH] initial commit --- .gitignore | 2 + .vscode/tasks.json | 45 +++++ Makefile | 34 ++++ README.md | 56 ++++++ common.asm | 484 +++++++++++++++++++++++++++++++++++++++++++++ defs.asm | 98 +++++++++ main.asm | 111 +++++++++++ over.asm | 112 +++++++++++ play.asm | 291 +++++++++++++++++++++++++++ sprites.asm | 231 ++++++++++++++++++++++ tables.asm | 47 +++++ title.asm | 94 +++++++++ 12 files changed, 1605 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/tasks.json create mode 100644 Makefile create mode 100644 README.md create mode 100644 common.asm create mode 100644 defs.asm create mode 100644 main.asm create mode 100644 over.asm create mode 100644 play.asm create mode 100644 sprites.asm create mode 100644 tables.asm create mode 100644 title.asm diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..15b5fdc --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.claude +*.prg diff --git a/.vscode/tasks.json b/.vscode/tasks.json new file mode 100644 index 0000000..9de1d66 --- /dev/null +++ b/.vscode/tasks.json @@ -0,0 +1,45 @@ +{ + "version": "2.0.0", + "tasks": [ + { + "label": "deps", + "type": "shell", + "command": "make deps", + "problemMatcher": [] + }, + { + "label": "build", + "type": "shell", + "command": "make build", + "group": { + "kind": "build", + "isDefault": true + }, + "problemMatcher": [] + }, + { + "label": "run", + "type": "shell", + "command": "make run", + "problemMatcher": [] + }, + { + "label": "clear", + "type": "shell", + "command": "make clear", + "problemMatcher": [] + }, + { + "label": "rebuild", + "type": "shell", + "command": "make rebuild", + "problemMatcher": [] + }, + { + "label": "rebuildrun", + "type": "shell", + "command": "make rebuildrun", + "problemMatcher": [] + } + ] +} diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..2eb1285 --- /dev/null +++ b/Makefile @@ -0,0 +1,34 @@ +ASM = acme +EMU = x64sc +SRC = main.asm +SRCS = $(wildcard *.asm) +TARGET = rabbit.prg + +UNAME_S := $(shell uname -s) + +.PHONY: build run clear rebuild rebuildrun deps + +deps: +ifeq ($(UNAME_S),Darwin) + @command -v brew >/dev/null 2>&1 || { echo "Homebrew kell ehhez. Telepitsd: https://brew.sh"; exit 1; } + brew install acme vice +else + @echo "A 'deps' target csak macOS-en (Darwin) automatikus. Detektalt OS: $(UNAME_S)" + @echo "Telepitsd kezzel: acme assembler + VICE emulator (x64sc)." + @exit 1 +endif + +build: $(TARGET) + +$(TARGET): $(SRCS) + $(ASM) -f cbm -o $(TARGET) $(SRC) + +run: build + $(EMU) $(TARGET) + +clear: + rm -f $(TARGET) + +rebuild: clear build + +rebuildrun: clear build run diff --git a/README.md b/README.md new file mode 100644 index 0000000..5c85425 --- /dev/null +++ b/README.md @@ -0,0 +1,56 @@ +# Rabbit Scooter — C64 + +A Commodore 64 port of [rabbitroller](../rabbitroller), the Game & Watch +style LCD mini game, written in 6502 assembly (built with the ACME +assembler, using the same toolchain and layout as [c64-demo](../c64-demo)). + +Rabbits hop toward you on a five-lane road. Steer your scooter left and +right to dodge them: a rabbit that reaches your lane is a **miss**, one +that passes in any other lane is a **point**. Three misses end the round. +The spawn rate and the rabbits' speed ramp up with your score, just like +in the original. + +## Controls + +| Input | Action | +|-------|--------| +| Joystick port 2 left/right | change lane (auto-repeats while held) | +| Fire (or `SPACE`) | start / retry | + +## How it works + +- PAL frame loop synced by polling the raster beam (no interrupts), one + state update per frame — title, play and game over are `updvec` states. +- The player is hardware sprite 0; up to seven rabbits live on sprites + 1–7. Rabbits follow the road's perspective: a linear y ramp and a t² + horizontal fan-out, both precomputed into tables at assembly time + (`tables.asm`). Far rabbits use small sprite frames, near ones switch + to big frames and finally to double-sized expanded sprites. +- Random lanes and speeds come from an 8-bit LFSR stirred with the + free-running CIA timer. +- Sprite art sits below `$1000` so the VIC never sees the character ROM + shadow instead of it. + +## Files + +| File | Contents | +|------|----------| +| `main.asm` | BASIC stub, init, frame loop, state dispatch | +| `defs.asm` | registers, constants, zero page, macros | +| `title.asm` | title screen: logo, parked scooters, demo rabbits | +| `play.asm` | gameplay: input, spawning, scoring, HUD | +| `over.asm` | game over box + retry | +| `common.asm` | shared routines: printing, road, input, RNG, rabbit positioning | +| `tables.asm` | assembly-time perspective and speed tables | +| `sprites.asm` | sprite art (scooter, 4+4 rabbit frames) | + +## Build & run + +``` +make deps # macOS: brew install acme vice +make build # -> rabbit.prg +make run # build + launch in VICE (x64sc) +``` + +The same targets are available as VS Code tasks. A test build that boots +straight into gameplay: `acme -DAUTOPLAY=1 -f cbm -o test.prg main.asm`. diff --git a/common.asm b/common.asm new file mode 100644 index 0000000..7d81dfc --- /dev/null +++ b/common.asm @@ -0,0 +1,484 @@ +; ----------------------------------------------------------- +; common.asm - routines shared by the states: screen drawing, +; text printing, input, RNG and the rabbit sprite positioning +; ----------------------------------------------------------- + +; ---- clear screen RAM, fill color RAM with black ---- +clear_screen: + ldx #0 +cls_loop: + lda #$20 ; space + sta SCREEN,x + sta SCREEN+$100,x + sta SCREEN+$200,x + sta SCREEN+$2e8,x + lda #BLACK + sta COLRAM,x + sta COLRAM+$100,x + sta COLRAM+$200,x + sta COLRAM+$2e8,x + inx + bne cls_loop + rts + +; ---- print a text line: txtlo/hi, prow, pcol, pcolor, plen ---- +print_text: + ldx prow + lda mul40lo,x + clc + adc pcol + sta scrlo + sta collo + lda mul40hi,x + adc #>SCREEN ; carry still valid from the low-byte add + sta scrhi + clc + adc #$d4 ; COLRAM - SCREEN high byte distance + sta colhi + ldy #0 +pt_loop: + lda (txtlo),y + sta (scrlo),y + lda pcolor + sta (collo),y + iny + cpy plen + bne pt_loop + rts + +; ---- the road: two diagonals from the horizon apex down to +; the screen corners, plus a tree on the left shoulder ---- +draw_road: + ldx #0 +dr_loop: + txa + clc + adc #6 ; road runs from char row 6 to row 24 + tay + lda mul40lo,y + sta scrlo + lda mul40hi,y + clc + adc #>SCREEN + sta scrhi + stx drtmp + lda #19 ; left edge: one column left per row + sec + sbc drtmp + tay + lda #$4e ; '/' diagonal + sta (scrlo),y + lda #20 ; right edge: one column right per row + clc + adc drtmp + tay + lda #$4d ; '\' diagonal + sta (scrlo),y + inx + cpx #19 + bne dr_loop + ; tree on the left shoulder + lda #$51 ; ball + sta SCREEN+10*40+3 + sta SCREEN+11*40+2 + sta SCREEN+11*40+3 + sta SCREEN+11*40+4 + lda #$5d ; trunk + sta SCREEN+12*40+3 + rts + +; ---- input ---- + +; read joystick port 2, return active-high bits in A and joyb +read_joy: + lda #$ff ; deselect every keyboard column first + sta CIA1_PA + lda CIA1_PA + eor #$ff + and #$1f + sta joyb + rts + +; fire or space held? returns A != 0 when pressed +start_pressed: + jsr read_joy + and #JOY_FIRE + bne sp_done + lda #$7f ; space: keyboard column 7, row bit 4 + sta CIA1_PA + lda CIA1_PB + and #$10 + eor #$10 +sp_done: + rts + +; edge-detected start: A = 1 only on the frame fire/space goes down +start_edge: + jsr start_pressed + beq se_up + lda fheld + bne se_no + lda #1 + sta fheld + rts ; A = 1: fresh press +se_up: + lda #0 + sta fheld +se_no: + lda #0 + rts + +; ---- RNG: 8-bit LFSR stirred with the free-running CIA timer +; (the timer counts every cycle, so call timing = entropy) ---- +rand8: + lda seed + asl + bcc r8_tap + eor #$1d +r8_tap: + eor CIA1_TA + sta seed + rts + +; random lane 0-4 +rand5: + jsr rand8 + and #7 + cmp #LANES + bcs rand5 ; every retry steps the LFSR, so it terminates + rts + +; ---- write the per-frame sprite register shadows ---- +write_shadows: + lda en_sh + sta SPR_EN + lda msb_sh + sta SPR_MSBX + lda xex_sh + sta SPR_XEX + lda yex_sh + sta SPR_YEX + rts + +; ============================================================ +; rab_pos - place rabbit slot X on hardware sprite X+1 +; reads r_thi/r_lane, picks size stage + hop frame, writes +; the position registers and ORs the shadow bits; keeps X +; ============================================================ +rab_pos: + stx rslot + lda r_thi,x + sta rt + lda r_lane,x + sta rlane + + ; hop offset: 8-entry wave, one hop per 64 t units + lda rt + lsr + lsr + lsr + and #7 + tay + lda hoptab,y + sta rhop + + ; animation frame 0-3 + lda rt + lsr + lsr + lsr + lsr + and #3 + sta rfrm + + lda #0 + sta rexp + lda rt + cmp #T_BIG + bcs rp_big + ; -- far: small frames -- + lda #SPRP_SMALL + clc + adc rfrm + sta rptr + lda #12 + sta ranchor + jmp rp_y +rp_big: + ; -- near: big frames, stronger hop -- + lda #SPRP_BIG + clc + adc rfrm + sta rptr + asl rhop + lda #12 + sta ranchor + lda rt + cmp #T_EXP + bcc rp_y + ; -- closest: the same frames double-sized -- + lda #1 + sta rexp + lda #24 + sta ranchor +rp_y: + ; sprite y: feet ride the perspective line (art bottom row) + ldy rt + lda ytab,y + sec + sbc rhop + sec + sbc #21 + ldx rexp + beq rp_ydone + sec + sbc #21 ; expanded sprite is 42 tall +rp_ydone: + sta ry + + ; x center = ROAD_CX +/- lane offset on the t^2 curve + ldy rt + lda offtab,y + sta roff + lda #ROAD_CX + sta rxlo + lda #0 + sta rxhi + ldy rlane + cpy #2 + beq rp_xdone ; middle lane: stay on the center line + bcs rp_right + lda roff ; lanes 0-1: left of center + cpy #1 + bne rp_lfull + lsr ; lane 1 fans out half as far +rp_lfull: + sta rtmp + lda rxlo + sec + sbc rtmp ; max offset is 107, never underflows + sta rxlo + jmp rp_xdone +rp_right: + lda roff ; lanes 3-4: right of center + cpy #3 + bne rp_rfull + lsr +rp_rfull: + sta rtmp + lda rxlo + clc + adc rtmp + sta rxlo + lda rxhi + adc #0 + sta rxhi +rp_xdone: + ; sprite left edge = center - anchor (16 bit) + lda rxlo + sec + sbc ranchor + sta rxlo + lda rxhi + sbc #0 + sta rxhi + + ; ---- write the hardware registers + shadow bits ---- + ldx rslot + ldy sprxreg,x + lda rxlo + sta SPRPOS,y + lda ry + sta SPRPOS+1,y + lda rptr + sta SPRPTR+1,x + lda sprbit,x + ora en_sh + sta en_sh + lda rxhi + beq rp_nomsb + lda sprbit,x + ora msb_sh + sta msb_sh +rp_nomsb: + lda rexp + beq rp_done + lda sprbit,x + ora xex_sh + sta xex_sh + lda sprbit,x + ora yex_sh + sta yex_sh +rp_done: + rts + +; ---- draw a text line with big 3x5 block letters ---- +; txtlo/hi = text (screen codes), bigrow/bigcol = top-left cell, +; bigcolor = color, biglen = char count; each letter is 3 cells +; wide + 1 gap, 5 cells tall +draw_big: + lda #0 + sta bigi +db_char: + ldy bigi + lda (txtlo),y + cmp #$20 ; space: leave the cell empty + beq db_next + tax + lda bigmap,x ; screen code -> glyph index + sta glyphbase + asl ; glyph base = index * 5 + asl + clc + adc glyphbase + sta glyphbase + lda bigi ; cell x = bigcol + charindex * 4 + asl + asl + clc + adc bigcol + sta charx + lda #0 + sta rowidx +db_row: + lda bigrow ; row pointers from the row*40 tables + clc + adc rowidx + tax + lda mul40lo,x + sta scrlo + sta collo + lda mul40hi,x + clc + adc #>SCREEN + sta scrhi + clc + adc #$d4 + sta colhi + lda glyphbase + clc + adc rowidx + tax + lda bigglyphs,x + asl ; 3-bit pattern up to bits 7-5 + asl + asl + asl + asl + sta curbits + ldy charx + lda #3 + sta bitcnt +db_bit: + asl curbits ; leftmost pixel first + bcc db_nobit + lda #$a0 ; inverse space = solid block + sta (scrlo),y + lda bigcolor + sta (collo),y +db_nobit: + iny + dec bitcnt + bne db_bit + inc rowidx + lda rowidx + cmp #5 + bne db_row +db_next: + inc bigi + lda bigi + cmp biglen + beq db_done + jmp db_char ; too far for a relative branch +db_done: + rts + +; ---- shared tables ---- + +; row * 40 lookup tables (25 rows) +mul40lo: + !byte $00, $28, $50, $78, $a0, $c8, $f0, $18, $40, $68 + !byte $90, $b8, $e0, $08, $30, $58, $80, $a8, $d0, $f8 + !byte $20, $48, $70, $98, $c0 +mul40hi: + !byte $00, $00, $00, $00, $00, $00, $00, $01, $01, $01 + !byte $01, $01, $01, $02, $02, $02, $02, $02, $02, $02 + !byte $03, $03, $03, $03, $03 + +; 3x5 block font for the big title (5 bytes per glyph, +; low 3 bits used, MSB side = left) +bigglyphs: + !byte 0, 0, 0, 0, 0 ; 0 space + !byte 2, 5, 7, 5, 5 ; 1 A + !byte 3, 4, 4, 4, 3 ; 2 C + !byte 6, 5, 5, 5, 6 ; 3 D + !byte 7, 4, 6, 4, 7 ; 4 E + !byte 7, 4, 6, 4, 4 ; 5 F + !byte 7, 2, 2, 2, 7 ; 6 I + !byte 4, 4, 4, 4, 7 ; 7 L + !byte 5, 7, 7, 5, 5 ; 8 M + !byte 6, 5, 5, 5, 5 ; 9 N + !byte 7, 5, 5, 5, 7 ; 10 O + !byte 6, 5, 6, 4, 4 ; 11 P + !byte 6, 5, 6, 5, 5 ; 12 R + !byte 3, 4, 2, 1, 6 ; 13 S + !byte 7, 2, 2, 2, 2 ; 14 T + !byte 5, 5, 2, 2, 2 ; 15 Y + !byte 3, 4, 5, 5, 7 ; 16 G + !byte 6, 5, 6, 5, 6 ; 17 B + +bigmap: ; screen code (a=1..z=26) -> glyph index + !byte 0 ; (unused) + !byte 1, 17, 2, 3, 4, 5, 16, 0, 6, 0 ; a-j + !byte 0, 7, 8, 9, 10, 11, 0, 12, 13, 14 ; k-t + !byte 0, 0, 0, 0, 15, 0 ; u-z + +; ---- texts (screen codes) ---- +txt_score: !scr "score" +txt_miss: !scr "miss" +txt_rabbit: !scr "rabbit" +txt_scooter: !scr "scooter" +txt_press: !scr "press fire to start" +txt_info: !scr "joystick 2 moves fire starts" +txt_over: !scr "game over" +txt_retry: !scr "fire = retry" +txt_blank: !fill 19, $20 + +; ---- shared variables ---- +framectr: !byte 0 ; frame counter, incremented by the main loop +seed: !byte $a7 ; rand8 LFSR state +fheld: !byte 0 ; fire/space edge detection state +joyb: !byte 0 ; joystick bits from read_joy (active high) +en_sh: !byte 0 ; per-frame sprite register shadows +msb_sh: !byte 0 +xex_sh: !byte 0 +yex_sh: !byte 0 +prow: !byte 0 ; print_text: row, column, color, length +pcol: !byte 0 +pcolor: !byte 0 +plen: !byte 0 +bigrow: !byte 0 ; draw_big: row, column, color, length +bigcol: !byte 0 +bigcolor: !byte 0 +biglen: !byte 0 +bigi: !byte 0 ; draw_big internals +glyphbase: !byte 0 +charx: !byte 0 +rowidx: !byte 0 +bitcnt: !byte 0 +curbits: !byte 0 +drtmp: !byte 0 ; draw_road internals +rslot: !byte 0 ; rab_pos internals +rt: !byte 0 +rlane: !byte 0 +rhop: !byte 0 +rfrm: !byte 0 +rexp: !byte 0 +rptr: !byte 0 +ranchor: !byte 0 +roff: !byte 0 +rxlo: !byte 0 +rxhi: !byte 0 +ry: !byte 0 +rtmp: !byte 0 +rtmp2: !byte 0 diff --git a/defs.asm b/defs.asm new file mode 100644 index 0000000..7fbf802 --- /dev/null +++ b/defs.asm @@ -0,0 +1,98 @@ +; ----------------------------------------------------------- +; defs.asm - hardware registers, constants, zero page, macros +; included first by main.asm +; ----------------------------------------------------------- + +; ---- hardware registers ---- +RASTER = $d012 ; current raster line +BORDER = $d020 ; border color register +BG = $d021 ; background color register +SCREEN = $0400 ; screen RAM (40x25 chars) +COLRAM = $d800 ; color RAM +SPRPTR = $07f8 ; sprite pointers (screen RAM + $3f8) +CIA1_PA = $dc00 ; CIA1 port A: keyboard columns / joystick 2 +CIA1_PB = $dc01 ; CIA1 port B: keyboard matrix row read +CIA1_TA = $dc04 ; CIA1 timer A low byte: free-running entropy + +; ---- sprite registers ---- +SPRPOS = $d000 ; sprite 0 x; +1 = y, +2 = sprite 1 x, ... +SPR_MSBX = $d010 ; 9th bit of each sprite's x coordinate +SPR_EN = $d015 ; sprite enable bits +SPR_YEX = $d017 ; vertical expand +SPR_PRIO = $d01b ; 1 = sprite behind the characters +SPR_MC = $d01c ; multicolor enable +SPR_XEX = $d01d ; horizontal expand +SPR_COL = $d027 ; sprite 0 color, +1 = sprite 1, ... + + +; ---- colors ---- +BLACK = 0 +WHITE = 1 +LGREY = 15 + +; ---- joystick port 2 bits (after read_joy, active high) ---- +JOY_LEFT = 4 +JOY_RIGHT = 8 +JOY_FIRE = 16 + +; ---- game constants ---- +LANES = 5 +NRABBITS = 7 ; rabbit slots, hardware sprites 1-7 +MAX_MISS = 3 +ROAD_CX = 184 ; road center in sprite x coordinates +PLAYER_SY = 200 ; scooter sprite top (feet on raster line 221) +T_BIG = 112 ; rabbit distance where the big frames start +T_EXP = 192 ; ... and where the sprite gets double-sized +REP_HOLD = 12 ; frames before a held direction auto-repeats +REP_PERIOD = 6 ; frames between auto-repeat steps +RAS_SYNC = 251 ; raster line polled for the frame sync + +; ---- zero page pointers (same slots as the c64-demo) ---- +txtlo = $f5 ; text pointer for the print routines (2 bytes) +txthi = $f6 +updvec = $f9 ; state update vector (2 bytes) +scrlo = $fb +scrhi = $fc +collo = $fd +colhi = $fe + +; ---- helper macros: print a normal / big text line ---- +!macro prtext .row, .col, .color, .txt, .len { + lda #<.txt + sta txtlo + lda #>.txt + sta txthi + lda #.row + sta prow + lda #.col + sta pcol + lda #.color + sta pcolor + lda #.len + sta plen + jsr print_text +} + +!macro prbig .row, .col, .color, .txt, .len { + lda #<.txt + sta txtlo + lda #>.txt + sta txthi + lda #.row + sta bigrow + lda #.col + sta bigcol + lda #.color + sta bigcolor + lda #.len + sta biglen + jsr draw_big +} + +; ---- set the state update vector ---- +!macro setupd .addr { + lda #<.addr + sta updvec + lda #>.addr + sta updvec+1 +} diff --git a/main.asm b/main.asm new file mode 100644 index 0000000..7a8e3bd --- /dev/null +++ b/main.asm @@ -0,0 +1,111 @@ +; ----------------------------------------------------------- +; main.asm - RABBIT SCOOTER for the C64 +; port of the rabbitroller Game & Watch style LCD game: +; rabbits hop toward you on a five-lane road, dodge them +; on your scooter - a rabbit reaching your lane is a miss, +; one passing in another lane is a point; 3 misses = over +; +; Files: +; defs.asm - constants, zero page, macros +; title.asm - title screen: demo rabbits + press fire +; play.asm - the game itself +; over.asm - game over box + retry +; common.asm - shared routines, input, rabbit positioning +; tables.asm - perspective / speed tables (assembly-time) +; sprites.asm - sprite art (scooter, small + big rabbits) +; +; Build: acme -f cbm -o rabbit.prg main.asm +; ----------------------------------------------------------- + +!to "rabbit.prg", cbm + +* = $0801 + +; ---- BASIC stub: 10 SYS 2064 ---- +; so after LOAD"*",8,1 it's enough to type RUN +!byte $0c, $08 ; address of next line +!byte $0a, $00 ; line number: 10 +!byte $9e ; SYS token +!byte $20, $32, $30, $36, $34 ; " 2064" +!byte $00 ; end of line +!byte $00, $00 ; end of program + +* = $0810 ; = 2064 in decimal + +!src "defs.asm" + +start: + sei ; no KERNAL IRQ - raster polling must not jitter + + ; LCD look: black bezel border, pale screen, black artwork + lda #BLACK + sta BORDER + lda #LGREY + sta BG + + ; sprites: all black, hires, in front of the characters + ldx #7 +sprcol: + lda #BLACK + sta SPR_COL,x + dex + bpl sprcol + lda #0 + sta SPR_EN + sta SPR_MC + sta SPR_PRIO + sta SPR_MSBX + sta SPR_XEX + sta SPR_YEX + sta framectr + sta fheld + +!ifdef AUTOPLAY { ; test builds boot straight into the game + jsr play_init +} else { + jsr title_init +} + +; ============================================================ +; main loop - one state update per PAL frame, synced to the +; raster beam in the lower border +; ============================================================ +main: + jsr wait_frame + inc framectr + jsr rand8 ; stir the RNG once per frame + jsr call_update + jmp main + +call_update: + jmp (updvec) ; each update runs exactly one frame, ends with rts + +; ---- wait for raster line RAS_SYNC (leave it first, so a +; fast update cannot run twice in the same frame) ---- +wait_frame: + lda RASTER + cmp #RAS_SYNC + beq wait_frame +wf_hit: + lda RASTER + cmp #RAS_SYNC + bne wf_hit + rts + +; ============================================================ +; sprite art first: it must stay below $1000, where the VIC +; would see the character ROM shadow instead of our RAM +; ============================================================ +!src "sprites.asm" +!if * > $1000 { + !error "sprite data ran into the char ROM shadow at $1000" +} + +; ============================================================ +; the states and everything they share +; ============================================================ +!src "title.asm" +!src "play.asm" +!src "over.asm" +!src "common.asm" +!src "tables.asm" diff --git a/over.asm b/over.asm new file mode 100644 index 0000000..5f182aa --- /dev/null +++ b/over.asm @@ -0,0 +1,112 @@ +; ----------------------------------------------------------- +; over.asm - game over box over the frozen playfield: final +; score + blinking retry prompt, fire restarts the round +; ----------------------------------------------------------- + +BOXROW = 10 ; box: rows 10-14, columns 12-27 +BOXCOL = 12 +BOXW = 16 + +over_init: + lda en_sh ; the scooter may be mid-blink: show it + ora #1 + sta SPR_EN + + ; box interior + frame, drawn over the frozen road + ldx #0 +oi_row: + txa + clc + adc #BOXROW + tay + lda mul40lo,y + clc + adc #BOXCOL + sta scrlo + lda mul40hi,y + adc #>SCREEN + sta scrhi + ldy #BOXW-1 +oi_col: + jsr box_char + sta (scrlo),y + dey + bpl oi_col + inx + cpx #5 + bne oi_row + + +prtext BOXROW+1, 15, BLACK, txt_over, 9 + +prtext BOXROW+2, 15, BLACK, txt_score, 5 + + ; final score digits next to the label + lda sc_d0 + ora #48 + sta SCREEN+(BOXROW+2)*40+21 + lda sc_d1 + ora #48 + sta SCREEN+(BOXROW+2)*40+22 + lda sc_d2 + ora #48 + sta SCREEN+(BOXROW+2)*40+23 + + +setupd over_update + rts + +; pick the frame/fill character for box row X, column Y (keeps Y) +box_char: + cpx #0 + beq bc_top + cpx #4 + beq bc_bottom + cpy #0 + beq bc_side + cpy #BOXW-1 + beq bc_side + lda #$20 ; interior: erase the road behind the box + rts +bc_side: + lda #$5d ; vertical bar + rts +bc_top: + cpy #0 + beq bc_tl + cpy #BOXW-1 + beq bc_tr + lda #$40 ; horizontal bar + rts +bc_tl: + lda #$55 ; rounded corners + rts +bc_tr: + lda #$49 + rts +bc_bottom: + cpy #0 + beq bc_bl + cpy #BOXW-1 + beq bc_br + lda #$40 + rts +bc_bl: + lda #$4a + rts +bc_br: + lda #$4b + rts + +over_update: + ; blink the retry prompt + lda framectr + and #32 + bne ou_hide + +prtext BOXROW+3, 14, BLACK, txt_retry, 12 + jmp ou_start +ou_hide: + +prtext BOXROW+3, 14, BLACK, txt_blank, 12 +ou_start: + jsr start_edge + beq ou_done + jmp play_init ; its rts returns to the main loop +ou_done: + rts diff --git a/play.asm b/play.asm new file mode 100644 index 0000000..1b2e38f --- /dev/null +++ b/play.asm @@ -0,0 +1,291 @@ +; ----------------------------------------------------------- +; play.asm - the game: lane movement with key-repeat, rabbit +; spawning with a score-driven interval, arrivals scored or +; counted as a miss, HUD updates +; ----------------------------------------------------------- + +; ---- start (or restart) a round ---- +play_init: + jsr clear_screen + jsr draw_road + + ; HUD: score on the left, miss slots on the right + +prtext 0, 1, BLACK, txt_score, 5 + +prtext 0, 30, BLACK, txt_miss, 4 + + lda #0 + sta score8 + sta sc_d0 + sta sc_d1 + sta sc_d2 + sta miss + sta flash + sta spawnt + sta lcnt + sta rcnt + jsr draw_score + + lda #0 ; draw_score leaves a digit code in A + ldx #NRABBITS-1 +pi_clr: + sta r_act,x + dex + bpl pi_clr + + lda #2 ; start in the middle lane + sta player_lane + + ; player sprite: scooter, fixed row, rabbits come per frame + lda #SPRP_SCOOT + sta SPRPTR + lda #PLAYER_SY + sta SPRPOS+1 + lda #1 + sta SPR_EN + lda #0 + sta SPR_XEX + sta SPR_YEX + + +setupd play_update + rts + +; ---- one frame of gameplay ---- +play_update: + jsr read_joy + + ; -- left: move on press, then auto-repeat while held -- + lda joyb + and #JOY_LEFT + beq pu_lrel + inc lcnt + lda lcnt + cmp #1 + beq pu_lmove + cmp #REP_HOLD+REP_PERIOD + bne pu_lend + lda #REP_HOLD + sta lcnt +pu_lmove: + lda player_lane + beq pu_lend + dec player_lane + jmp pu_lend +pu_lrel: + lda #0 + sta lcnt +pu_lend: + + ; -- right, same dance -- + lda joyb + and #JOY_RIGHT + beq pu_rrel + inc rcnt + lda rcnt + cmp #1 + beq pu_rmove + cmp #REP_HOLD+REP_PERIOD + bne pu_rend + lda #REP_HOLD + sta rcnt +pu_rmove: + lda player_lane + cmp #LANES-1 + beq pu_rend + inc player_lane + jmp pu_rend +pu_rrel: + lda #0 + sta rcnt +pu_rend: + + ; -- shadow init: player sprite bit + its x MSB -- + ldx player_lane + lda pxlo,x + sta SPRPOS ; sprite 0 x + lda pxhi,x + sta msb_sh ; lane 4 sits past x=255 + lda #0 + sta xex_sh + sta yex_sh + lda #1 + sta en_sh + + ; -- a hit blinks the scooter for a dozen frames -- + lda flash + beq pu_noflash + dec flash + lda framectr + and #4 + bne pu_noflash + lda #0 + sta en_sh +pu_noflash: + + ; -- spawn: interval = 70 - score/2, floor 22 (as the original) -- + inc spawnt + lda score8 + cmp #96 + bcc pu_ivcalc + lda #22 + jmp pu_ivgot +pu_ivcalc: + lsr + sta rtmp + lda #70 + sec + sbc rtmp +pu_ivgot: + cmp spawnt + bcs pu_nospawn + lda #0 + sta spawnt + jsr spawn_rabbit +pu_nospawn: + + ; -- advance every live rabbit -- + ldx #NRABBITS-1 +pu_rab: + lda r_act,x + beq pu_rnext + lda r_tlo,x ; t += speed (8.8 fixed point) + clc + adc r_splo,x + sta r_tlo,x + lda r_thi,x + adc r_sphi,x + sta r_thi,x + bcc pu_ralive + ; arrived at the player line + lda #0 + sta r_act,x + lda r_lane,x + cmp player_lane + beq pu_rhit + jsr add_score ; dodged: one point + jmp pu_rnext +pu_rhit: + jsr add_miss ; ran into you + jmp pu_rnext +pu_ralive: + jsr rab_pos +pu_rnext: + dex + bpl pu_rab + + jsr write_shadows + + ; -- third miss ends the round -- + lda miss + cmp #MAX_MISS + bcc pu_done + jmp over_init ; its rts returns to the main loop +pu_done: + rts + +; ---- claim a free rabbit slot and roll its lane + speed ---- +spawn_rabbit: + ldx #NRABBITS-1 +sr_find: + lda r_act,x + beq sr_got + dex + bpl sr_find + rts ; every slot busy: skip this spawn +sr_got: + lda #1 + sta r_act,x + jsr rand5 + sta r_lane,x + lda #0 + sta r_tlo,x + sta r_thi,x + ; speed = $0260 base + random byte + score ramp (tops out at 64) + jsr rand8 + clc + adc #$60 + sta rtmp + lda #2 + adc #0 + sta rtmp2 + lda score8 + cmp #64 + bcc sr_ramp + lda #64 +sr_ramp: + tay + lda rtmp + clc + adc sptab_lo,y + sta r_splo,x + lda rtmp2 + adc sptab_hi,y + sta r_sphi,x + rts + +; ---- score: bump the decimal digits and the difficulty byte ---- +add_score: + lda score8 + cmp #$ff + beq as_digits + inc score8 +as_digits: + inc sc_d2 + lda sc_d2 + cmp #10 + bne draw_score + lda #0 + sta sc_d2 + inc sc_d1 + lda sc_d1 + cmp #10 + bne draw_score + lda #0 + sta sc_d1 + inc sc_d0 + lda sc_d0 + cmp #10 + bne draw_score + lda #0 + sta sc_d0 +; falls through: repaint the three HUD digits +draw_score: + lda sc_d0 + ora #48 ; digit screen codes start at 48 + sta SCREEN+7 + lda sc_d1 + ora #48 + sta SCREEN+8 + lda sc_d2 + ora #48 + sta SCREEN+9 + rts + +; ---- miss: X mark on the HUD + blink the scooter ---- +add_miss: + inc miss + ldy miss ; Y: the rabbit loop still needs X + lda #24 ; 'x' + sta SCREEN+34,y ; marks land on columns 35-37 + lda #12 + sta flash + rts + +; ---- gameplay variables ---- +player_lane: !byte 2 +score8: !byte 0 ; score capped at 255, drives the difficulty +sc_d0: !byte 0 ; score digits: hundreds, tens, ones +sc_d1: !byte 0 +sc_d2: !byte 0 +miss: !byte 0 +flash: !byte 0 ; frames left of the post-hit blink +spawnt: !byte 0 ; frames since the last spawn +lcnt: !byte 0 ; held-direction counters for the auto-repeat +rcnt: !byte 0 + +; ---- rabbit slots (hardware sprites 1-7) ---- +r_act: !fill NRABBITS, 0 +r_lane: !fill NRABBITS, 0 +r_tlo: !fill NRABBITS, 0 ; distance along the lane, 8.8 fixed point +r_thi: !fill NRABBITS, 0 +r_splo: !fill NRABBITS, 0 ; per-rabbit speed, 8.8 fixed point +r_sphi: !fill NRABBITS, 0 diff --git a/sprites.asm b/sprites.asm new file mode 100644 index 0000000..1dea234 --- /dev/null +++ b/sprites.asm @@ -0,0 +1,231 @@ +; ----------------------------------------------------------- +; sprites.asm - hires sprite art, black silhouettes in the +; spirit of the original PNGs; one !be24 row per sprite line +; (`.` = transparent, `#` = pixel), 64-byte aligned +; ----------------------------------------------------------- + +!align 63, 0 + +spr_data: +SPRP_SCOOT = spr_data DIV 64 ; sprite pointer of the scooter +SPRP_SMALL = SPRP_SCOOT + 1 ; 4 far rabbit frames +SPRP_BIG = SPRP_SCOOT + 5 ; 4 near rabbit frames + +; ---- rabbit on a scooter, seen from behind ---- +spr_scooter: + !be24 %.........##..##......... + !be24 %.........##..##......... + !be24 %.........##..##......... + !be24 %.........##..##......... + !be24 %........########........ + !be24 %........########........ + !be24 %........########........ + !be24 %..........####.......... + !be24 %....################.... + !be24 %....##..########..##.... + !be24 %........########........ + !be24 %........########........ + !be24 %.......##########....... + !be24 %.......##########....... + !be24 %.......##########....... + !be24 %.....##############..... + !be24 %.....##############..... + !be24 %..........####.......... + !be24 %..........####.......... + !be24 %..........####.......... + !be24 %..........####.......... + !byte 0 + +; ---- far rabbit, four hop frames (art hugs the bottom row) ---- +spr_small0: + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %.........#..#........... + !be24 %.........#..#........... + !be24 %........######.......... + !be24 %.....##########......... + !be24 %....############........ + !be24 %.....##########......... + !be24 %....##...##...##........ + !byte 0 + +spr_small1: + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %.........#..#........... + !be24 %.........#..#........... + !be24 %........######.......... + !be24 %.....###########........ + !be24 %....#############....... + !be24 %.....###.....###........ + !byte 0 + +spr_small2: + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %..........##............ + !be24 %..........##............ + !be24 %........######.......... + !be24 %.....##########......... + !be24 %....############........ + !be24 %....############........ + !be24 %......##.....##......... + !byte 0 + +spr_small3: + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %..........#..#.......... + !be24 %..........#..#.......... + !be24 %.........######......... + !be24 %......###########....... + !be24 %.....############....... + !be24 %....###.......###....... + !byte 0 + +; ---- near rabbit, four gallop frames (body shared, legs cycle) ---- +spr_big0: ; stride: legs stretched wide + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........##...##......... + !be24 %........##...##......... + !be24 %.......###..###......... + !be24 %......###########....... + !be24 %...###############...... + !be24 %..#################..... + !be24 %..##################.... + !be24 %...################..... + !be24 %....##############...... + !be24 %...####......#####...... + !be24 %..####.........####..... + !be24 %..###...........####.... + !be24 %.###.............###.... + !be24 %.##...............##.... + !be24 %##.................##... + !byte 0 + +spr_big1: ; gathering the legs + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........##...##......... + !be24 %........##...##......... + !be24 %.......###..###......... + !be24 %......###########....... + !be24 %...###############...... + !be24 %..#################..... + !be24 %..##################.... + !be24 %...################..... + !be24 %....##############...... + !be24 %.....####...#####....... + !be24 %.....###.....####....... + !be24 %....###.......###....... + !be24 %....##.........##....... + !be24 %....##.........##....... + !be24 %...###.........###...... + !byte 0 + +spr_big2: ; tucked into a ball + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........##...##......... + !be24 %........##...##......... + !be24 %.......###..###......... + !be24 %......###########....... + !be24 %...###############...... + !be24 %..#################..... + !be24 %..##################.... + !be24 %...################..... + !be24 %....##############...... + !be24 %.....#####....####...... + !be24 %......####....###....... + !be24 %......###.....###....... + !be24 %......###.....###....... + !be24 %.....####.....####...... + !be24 %....#####.....#####..... + !byte 0 + +spr_big3: ; reaching forward again + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........................ + !be24 %........##...##......... + !be24 %........##...##......... + !be24 %.......###..###......... + !be24 %......###########....... + !be24 %...###############...... + !be24 %..#################..... + !be24 %..##################.... + !be24 %...################..... + !be24 %....##############...... + !be24 %....####......####...... + !be24 %...####........###...... + !be24 %..####..........###..... + !be24 %..###............###.... + !be24 %..##..............##.... + !be24 %.###...............##... + !byte 0 diff --git a/tables.asm b/tables.asm new file mode 100644 index 0000000..b4c3ecd --- /dev/null +++ b/tables.asm @@ -0,0 +1,47 @@ +; ----------------------------------------------------------- +; tables.asm - perspective and speed tables, generated at +; assembly time; t is the rabbit's distance (0 = horizon, +; 255 = the player's line) +; ----------------------------------------------------------- + +; feet y along the lane: linear from the horizon to the player +ytab: +!for i, 0, 255 { + !byte 100 + (111*i) DIV 256 +} + +; horizontal fan-out on a t^2 curve (full offset of the outer +; lanes; the inner lanes use half of it) +offtab: +!for i, 0, 255 { + !byte (107*i*i) DIV 65536 +} + +; hop wave, subtracted from the feet y (doubled for big rabbits) +hoptab: + !byte 0, 1, 2, 4, 4, 3, 2, 1 + +; speed ramp: score (capped at 64) -> 16-bit speed bonus +sptab_lo: +!for i, 0, 64 { + !byte <(i*13) +} +sptab_hi: +!for i, 0, 64 { + !byte >(i*13) +} + +; scooter sprite x per lane (lane center - 12); lane 4 crosses +; the 8-bit boundary, pxhi is its MSB flag +pxlo: + !byte 66, 119, 172, 225, 22 +pxhi: + !byte 0, 0, 0, 0, 1 + +; rabbit slot -> sprite register offset (sprite = slot + 1) +sprxreg: + !byte 2, 4, 6, 8, 10, 12, 14 + +; rabbit slot -> sprite enable/MSB/expand bit +sprbit: + !byte 2, 4, 8, 16, 32, 64, 128 diff --git a/title.asm b/title.asm new file mode 100644 index 0000000..d84dc1b --- /dev/null +++ b/title.asm @@ -0,0 +1,94 @@ +; ----------------------------------------------------------- +; title.asm - big RABBIT logo over the road, scooters parked +; on all five lanes, three demo rabbits hopping in, blinking +; "press fire to start" +; ----------------------------------------------------------- + +title_init: + jsr clear_screen + jsr draw_road + + +prbig 1, 8, BLACK, txt_rabbit, 6 + +prtext 6, 16, BLACK, txt_scooter, 7 + +prtext 17, 5, BLACK, txt_info, 30 + + ; one parked scooter per lane on sprites 0-4 + ldx #4 +ti_scoot: + txa + asl + tay + lda pxlo,x + sta SPRPOS,y + lda #PLAYER_SY + sta SPRPOS+1,y + lda #SPRP_SCOOT + sta SPRPTR,x + dex + bpl ti_scoot + lda #0 + sta SPR_XEX + sta SPR_YEX + + ; demo rabbits on slots 4-6 (sprites 5-7), staggered starts + ldx #4 +ti_demo: + jsr rand5 + sta r_lane,x + lda demo_t-4,x + sta r_thi,x + lda #1 + sta r_act,x + inx + cpx #NRABBITS + bne ti_demo + + +setupd title_update + rts + +title_update: + ; shadows: five scooters (lane 4 needs the x MSB), no expand + lda #%00011111 + sta en_sh + lda #%00010000 + sta msb_sh + lda #0 + sta xex_sh + sta yex_sh + + ; demo rabbits: constant lope, respawn on a random lane + ldx #4 +tu_rab: + lda r_thi,x + clc + adc #3 + sta r_thi,x + bcc tu_pos + lda #0 + sta r_thi,x + jsr rand5 + sta r_lane,x +tu_pos: + jsr rab_pos + inx + cpx #NRABBITS + bne tu_rab + + jsr write_shadows + + ; blink the start line + lda framectr + and #32 + bne tu_hide + +prtext 15, 10, BLACK, txt_press, 19 + jmp tu_start +tu_hide: + +prtext 15, 10, BLACK, txt_blank, 19 +tu_start: + jsr start_edge + beq tu_done + jmp play_init ; its rts returns to the main loop +tu_done: + rts + +demo_t: !byte 0, 85, 170