commit 0fb584bcd7ffc4a975d535d75f7676ed591a7ef8 Author: Zsolt Tasnadi Date: Wed Jul 15 21:31:51 2026 +0200 initial commit 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