From e9c69864f124b5bec6a4a4dc49129e0d53decf4c Mon Sep 17 00:00:00 2001 From: Zsolt Tasnadi Date: Sat, 18 Jul 2026 16:12:06 +0200 Subject: [PATCH] initial commit --- .gitignore | 1 + .vscode/tasks.json | 45 +++ .woodpecker.yaml | 35 ++ Makefile | 91 ++++++ README.md | 47 +++ common.asm | 393 +++++++++++++++++++++++ controls.asm | 52 +++ credits.asm | 164 ++++++++++ defs.asm | 137 ++++++++ intro.asm | 146 +++++++++ main.asm | 116 +++++++ menu.asm | 194 +++++++++++ metadata.json | 9 + over.asm | 56 ++++ play.asm | 783 +++++++++++++++++++++++++++++++++++++++++++++ rules.asm | 56 ++++ sound.asm | 71 ++++ win.asm | 76 +++++ 18 files changed, 2472 insertions(+) create mode 100644 .gitignore create mode 100644 .vscode/tasks.json create mode 100644 .woodpecker.yaml create mode 100644 Makefile create mode 100644 README.md create mode 100644 common.asm create mode 100644 controls.asm create mode 100644 credits.asm create mode 100644 defs.asm create mode 100644 intro.asm create mode 100644 main.asm create mode 100644 menu.asm create mode 100644 metadata.json create mode 100644 over.asm create mode 100644 play.asm create mode 100644 rules.asm create mode 100644 sound.asm create mode 100644 win.asm diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c7f21d1 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.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/.woodpecker.yaml b/.woodpecker.yaml new file mode 100644 index 0000000..a51c43f --- /dev/null +++ b/.woodpecker.yaml @@ -0,0 +1,35 @@ +steps: + - name: version + image: alpine + commands: + - 'apk add --no-cache make' + - 'make ci-version' + + - name: build + image: debian:stable-slim + commands: + - 'apt-get update && apt-get install -y --no-install-recommends make acme' + - 'make ci-build' + + - name: artifact + image: alpine + environment: + DROPAREA_HOST: vps.teletype.hu + DROPAREA_PORT: 2223 + DROPAREA_TARGET_PATH: /home/drop + DROPAREA_USER: drop + DROPAREA_SSH_PASSWORD: + from_secret: droparea_ssh_password + commands: + - 'apk add --no-cache make openssh-client sshpass' + - 'make ci-artifact' + + - name: update + image: alpine + environment: + UPDATE_SERVER: https://games.vps.teletype.hu + UPDATE_SECRET: + from_secret: update_secret_key + commands: + - 'apk add --no-cache make curl' + - 'make ci-update' diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..a9c626d --- /dev/null +++ b/Makefile @@ -0,0 +1,91 @@ +# ----------------------------------------- +# Makefile – C64 project builder (ACME + VICE) +# ----------------------------------------- + +PROJECT = labyrinth + +ASM = acme +EMU = x64sc +SRC = main.asm +SRCS = $(wildcard *.asm) +TARGET ?= $(PROJECT).prg + +METADATA = metadata.json + +UNAME_S := $(shell uname -s) + +# CI/CD variables +VERSION_FILE = .version +DROPAREA_HOST ?= vps.teletype.hu +DROPAREA_PORT ?= 2223 +DROPAREA_TARGET_PATH ?= /home/drop +DROPAREA_USER ?= drop +UPDATE_SERVER ?= https://games.vps.teletype.hu + +all: build + +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) $(PROJECT)-*.prg $(PROJECT)-*.metadata.json $(VERSION_FILE) + +rebuild: clear build + +rebuildrun: clear build run + +# ----------------------------------------- +# CI/CD Pipeline targets +# ----------------------------------------- + +ci-version: + @VERSION=$$(sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' $(METADATA) | head -n 1); \ + if [ -z "$$VERSION" ]; then \ + echo "ERROR: no \"version\" field in $(METADATA)!"; \ + exit 1; \ + fi; \ + BRANCH=$${CI_COMMIT_BRANCH:-$${WOODPECKER_BRANCH}}; \ + BRANCH=$$(echo "$$BRANCH" | tr '/' '-'); \ + if [ "$$BRANCH" != "main" ] && [ "$$BRANCH" != "master" ] && [ -n "$$BRANCH" ]; then \ + VERSION=dev-$$VERSION-$$BRANCH; \ + fi; \ + echo "VERSION is: $$VERSION"; \ + echo $$VERSION > $(VERSION_FILE) + +ci-build: build + @VERSION=$$(cat $(VERSION_FILE)); \ + echo "==> Creating versioned files for $$VERSION"; \ + cp $(TARGET) $(PROJECT)-$$VERSION.prg; \ + cp $(METADATA) $(PROJECT)-$$VERSION.metadata.json; \ + ls -lh $(PROJECT)-$$VERSION.* + +ci-artifact: + @VERSION=$$(cat $(VERSION_FILE)); \ + echo "==> Uploading artifacts for version $$VERSION"; \ + SCP_TARGET="$(DROPAREA_USER)@$(DROPAREA_HOST):$(DROPAREA_TARGET_PATH)/"; \ + sshpass -p "$(DROPAREA_SSH_PASSWORD)" scp -o StrictHostKeyChecking=no -P $(DROPAREA_PORT) \ + $(PROJECT)-$$VERSION.prg \ + $(PROJECT)-$$VERSION.metadata.json \ + $$SCP_TARGET + +ci-update: + @VERSION=$$(cat $(VERSION_FILE)); \ + echo "==> Triggering update for version $$VERSION"; \ + curl "$(UPDATE_SERVER)/update?secret=$(UPDATE_SECRET)&name=$(PROJECT)&platform=c64&version=$$VERSION" + +.PHONY: all deps build run clear rebuild rebuildrun ci-version ci-build ci-artifact ci-update diff --git a/README.md b/README.md new file mode 100644 index 0000000..656fe8d --- /dev/null +++ b/README.md @@ -0,0 +1,47 @@ +# labyrinth + +Two-player maze duel for the Commodore 64 (ACME + VICE), in the same +spirit as [rabbitc64](../rabbitc64). + +A fresh maze is carved every round on a 39x23 lattice where the walls +are exactly as wide as the corridors. **Player 1** (joystick 2, or +`w a s d`) runs the corridors toward the green diamond exit while +dodging the roaming colored enemies. **Player 2** (joystick 1, or +`i j k l`) rides the corners of the walls with a blinking cursor - +fire (or `return`) spins the four wall pieces around the cursor by +90 degrees, reshaping the maze in real time. A spin is denied while +anyone is standing in one of the four slots. + +Screens: TTG intro (ported from rabbitc64) -> main menu +(play / rules / controls / credits) -> game -> win (back to the menu) +or game over (fire retries with a fresh maze, `m` backs out to the +menu). + +## Build & run + +``` +make deps # macOS: brew install acme vice +make build # -> labyrinth.prg +make run # build + launch in x64sc +``` + +Test builds can boot straight into one state: + +``` +acme -DAUTOPLAY=1 -f cbm -o labyrinth.prg main.asm +# also: AUTORULES, AUTOCONTROLS, AUTOCREDITS, AUTOWIN, AUTOLOSE +``` + +## Files + +- `main.asm` - entry point, BASIC stub, per-frame state machine +- `defs.asm` - registers, constants, zero page, macros +- `intro.asm` - TTG boot logo with the gold color wash +- `menu.asm` - main menu card +- `rules.asm` - how to play +- `controls.asm` - joystick + keyboard mappings +- `credits.asm` - the crew +- `play.asm` - maze generation (recursive backtracker) + the game +- `win.asm` / `over.asm` - end screens +- `common.asm` - shared routines: printing, big block font, input, RNG +- `sound.asm` - single-voice SID SFX driver diff --git a/common.asm b/common.asm new file mode 100644 index 0000000..4b8d28a --- /dev/null +++ b/common.asm @@ -0,0 +1,393 @@ +; ----------------------------------------------------------- +; common.asm - routines shared by the states: screen drawing, +; text printing, big block font, input and the RNG +; (ported from rabbitc64, trimmed to what this game needs) +; ----------------------------------------------------------- + +; ---- 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 + +; ---- set scrlo/hi + collo/hi to the start of char row X ---- +row_ptr: + lda mul40lo,x + sta scrlo + sta collo + lda mul40hi,x + clc + adc #>SCREEN + sta scrhi + clc + adc #$d4 ; COLRAM - SCREEN high byte distance + sta colhi + rts + +; ---- input ---- +; joystick 1 shares the CIA port B lines the keyboard rows use, +; so before any key scan we note which rows the stick itself is +; grounding (jmask) and treat those rows as released - a held +; joystick 1 direction must not read as a phantom keypress + +; read joystick port 2 merged with w/a/s/d + space (the runner), +; active-high bits in A and joyb +read_joy: + lda #$ff ; deselect every keyboard column first + sta CIA1_PA + lda CIA1_PB + eor #$ff + and #$1f + sta jmask ; rows grounded by joystick 1 right now + lda CIA1_PA + eor #$ff + and #$1f + sta joyb ; joystick 2 bits + lda #$fd ; keyboard column 1: w / a / s + sta CIA1_PA + lda CIA1_PB + ora jmask + and #$02 ; 'w' = up: row bit 1 + bne rj_now + lda joyb + ora #JOY_UP + sta joyb +rj_now: + lda CIA1_PB + ora jmask + and #$04 ; 'a' = left: row bit 2 + bne rj_noa + lda joyb + ora #JOY_LEFT + sta joyb +rj_noa: + lda CIA1_PB + ora jmask + and #$20 ; 's' = down: row bit 5 + bne rj_nos + lda joyb + ora #JOY_DOWN + sta joyb +rj_nos: + lda #$fb ; 'd' = right: keyboard column 2, row bit 2 + sta CIA1_PA + lda CIA1_PB + ora jmask + and #$04 + bne rj_nod + lda joyb + ora #JOY_RIGHT + sta joyb +rj_nod: + lda #$7f ; space = fire: keyboard column 7, row bit 4 + sta CIA1_PA + lda CIA1_PB + ora jmask + and #$10 + bne rj_nosp + lda joyb + ora #JOY_FIRE + sta joyb +rj_nosp: + lda #$ff + sta CIA1_PA + lda joyb + rts + +; read joystick port 1 merged with i/j/k/l + return (the wall +; spinner), active-high bits in A and joyb2 +read_joy2: + lda #$ff ; no column selected: port B = joystick 1 + sta CIA1_PA + lda CIA1_PB + eor #$ff + and #$1f + sta joyb2 ; joystick 1 bits + sta jmask ; ... which also ground the key rows below + lda #$ef ; keyboard column 4: i / j / k + sta CIA1_PA + lda CIA1_PB + ora jmask + and #$02 ; 'i' = up: row bit 1 + bne rk_noi + lda joyb2 + ora #JOY_UP + sta joyb2 +rk_noi: + lda CIA1_PB + ora jmask + and #$04 ; 'j' = left: row bit 2 + bne rk_noj + lda joyb2 + ora #JOY_LEFT + sta joyb2 +rk_noj: + lda CIA1_PB + ora jmask + and #$20 ; 'k' = down: row bit 5 + bne rk_nok + lda joyb2 + ora #JOY_DOWN + sta joyb2 +rk_nok: + lda #$df ; 'l' = right: keyboard column 5, row bit 2 + sta CIA1_PA + lda CIA1_PB + ora jmask + and #$04 + bne rk_nol + lda joyb2 + ora #JOY_RIGHT + sta joyb2 +rk_nol: + lda #$fe ; return = fire: keyboard column 0, row bit 1 + sta CIA1_PA + lda CIA1_PB + ora jmask + and #$02 + bne rk_noret + lda joyb2 + ora #JOY_FIRE + sta joyb2 +rk_noret: + lda #$ff + sta CIA1_PA + lda joyb2 + rts + +; fire held on either side? (stick fire, space or return) +; returns A != 0 when pressed +start_pressed: + jsr read_joy + and #JOY_FIRE + bne sp_done + jsr read_joy2 + and #JOY_FIRE +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 + +; ---- 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 titles (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 + !byte 5, 5, 5, 5, 2 ; 18 V + !byte 5, 5, 7, 5, 5 ; 19 H + !byte 5, 5, 5, 5, 7 ; 20 U + !byte 5, 5, 7, 7, 5 ; 21 W + +bigmap: ; screen code (a=1..z=26) -> glyph index + !byte 0 ; (unused) + !byte 1, 17, 2, 3, 4, 5, 16, 19, 6, 0 ; a-j + !byte 0, 7, 8, 9, 10, 11, 0, 12, 13, 14 ; k-t + !byte 20, 18, 21, 0, 15, 0 ; u-z + +; ---- shared texts (screen codes) ---- +txt_title: !scr "labyrinth" +txt_ttgname: !scr "teletype games" +txt_presents:!scr "presents" +txt_return: !scr "fire = return" +txt_blank: !fill 16, $20 + +washtbl: ; gold glow ramp (16 entries, cyclic) + !byte $09, $02, $08, $0a, $07, $07, $01, $01 + !byte $01, $01, $07, $07, $0a, $08, $02, $09 + +; ---- shared variables ---- +framectr: !byte 0 ; frame counter, incremented by the main loop +washphase: !byte 0 ; gold wash offset on the big letters +seed: !byte $a7 ; rand8 LFSR state +fheld: !byte 0 ; fire/space edge detection state +joyb: !byte 0 ; player 1 input bits from read_joy +joyb2: !byte 0 ; player 2 input bits from read_joy2 +jmask: !byte 0 ; key rows grounded by joystick 1 this scan +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 +rtmp: !byte 0 ; scratch +rtmp2: !byte 0 diff --git a/controls.asm b/controls.asm new file mode 100644 index 0000000..716c8b2 --- /dev/null +++ b/controls.asm @@ -0,0 +1,52 @@ +; ----------------------------------------------------------- +; controls.asm - who steers what: joystick and keyboard +; mappings for both players, one static card; fire returns +; ----------------------------------------------------------- + +controls_init: + jsr clear_screen + + +prbig 1, 4, WHITE, txt_controls, 8 + + +prtext 8, 9, LGREEN, txt_c1, txt_c1e-txt_c1 + +prtext 10, 6, WHITE, txt_c2, txt_c2e-txt_c2 + + +prtext 13, 6, LBLUE, txt_c3, txt_c3e-txt_c3 + +prtext 15, 6, WHITE, txt_c4, txt_c4e-txt_c4 + +prtext 17, 10, WHITE, txt_c5, txt_c5e-txt_c5 + + +prtext 20, 5, LGREY, txt_c6, txt_c6e-txt_c6 + + +setupd controls_update + rts + +controls_update: + ; blink the return prompt + lda framectr + and #32 + bne co_hide + +prtext 22, 13, YELLOW, txt_return, 13 + jmp co_input +co_hide: + +prtext 22, 13, YELLOW, txt_blank, 13 +co_input: + jsr start_edge + beq co_stay + jmp menu_init +co_stay: + rts + +; ---- controls texts ---- +txt_controls: !scr "controls" +txt_c1: !scr "player 1 - the runner" +txt_c1e: +txt_c2: !scr "move: joystick 2 or w a s d" +txt_c2e: +txt_c3: !scr "player 2 - the wall spinner" +txt_c3e: +txt_c4: !scr "move: joystick 1 or i j k l" +txt_c4e: +txt_c5: !scr "spin: fire or return" +txt_c5e: +txt_c6: !scr "menus: fire, space or return" +txt_c6e: diff --git a/credits.asm b/credits.asm new file mode 100644 index 0000000..d31b57a --- /dev/null +++ b/credits.asm @@ -0,0 +1,164 @@ +; ----------------------------------------------------------- +; credits.asm - the same crew as rabbitc64, but a new look: +; rainbow bars scrolling along the top and bottom edges in +; opposite directions, a rainbow sweep across the big +; CREDITS, each crew line in its own fixed color and the +; ai warning pulsing grey-white (no scroller, no parade) +; ----------------------------------------------------------- + +credits_init: + jsr clear_screen + + ; rainbow bars: solid blocks across rows 0 and 24 + ldx #39 +ci_bars: + lda #$a0 + sta SCREEN,x + sta SCREEN+24*40,x + dex + bpl ci_bars + + +prbig 2, 6, WHITE, txt_credits, 7 + + +prtext 9, 13, WHITE, txt_ttgname, 14 + + +prtext 12, 8, YELLOW, txt_crew0, txt_crew0e-txt_crew0 + +prtext 13, 10, LGREEN, txt_crew1, txt_crew1e-txt_crew1 + +prtext 14, 9, CYAN, txt_crew2, txt_crew2e-txt_crew2 + +prtext 15, 10, LRED, txt_crew3, txt_crew3e-txt_crew3 + + +prtext 17, 9, LGREY, txt_ai1, txt_ai1e-txt_ai1 + +prtext 18, 12, LGREY, txt_ai2, txt_ai2e-txt_ai2 + + lda #0 + sta barphase + +setupd credits_update + rts + +credits_update: + ; the edge bars crawl in opposite directions + lda framectr + and #3 + bne cu_barsdone + inc barphase +cu_barsdone: + ldx #39 +cu_bars: + txa + clc + adc barphase + and #7 + tay + lda raintbl,y + sta COLRAM,x ; top bar: colors drift left + lda barphase + stx rtmp + sec + sbc rtmp + and #7 + tay + lda raintbl,y + sta COLRAM+24*40,x ; bottom bar: colors drift right + dex + bpl cu_bars + + ; rainbow sweep across the big CREDITS (rows 2-6) + lda #0 + sta rtmp2 +cu_sweep: + lda #2 + clc + adc rtmp2 + tax + jsr row_ptr + lda framectr + lsr + lsr + clc + adc rtmp2 + and #7 + tax + lda raintbl,x + sta rtmp + ldy #32 ; the letters span columns 6-32 +cu_scol: + lda (scrlo),y + cmp #$a0 ; only touch the letter blocks + bne cu_sskip + lda rtmp + sta (collo),y +cu_sskip: + dey + cpy #5 + bne cu_scol + inc rtmp2 + lda rtmp2 + cmp #5 + bne cu_sweep + + ; the ai warning pulses grey-white + lda framectr + lsr + lsr + lsr + and #7 + tax + lda pulsetbl,x + sta rtmp + ldx #17 + jsr row_ptr + ldy #30 +cu_p1: + lda rtmp + sta (collo),y + dey + cpy #8 + bne cu_p1 + ldx #18 + jsr row_ptr + ldy #27 +cu_p2: + lda rtmp + sta (collo),y + dey + cpy #11 + bne cu_p2 + + ; blink the return prompt + lda framectr + and #32 + bne cu_hide + +prtext 21, 13, YELLOW, txt_return, 13 + jmp cu_input +cu_hide: + +prtext 21, 13, YELLOW, txt_blank, 13 +cu_input: + jsr start_edge + beq cu_stay + jmp menu_init ; back to the menu +cu_stay: + rts + +; ---- tables ---- +pulsetbl: ; grey-white pulse (8 entries, cyclic) + !byte $0b, $0c, $0f, $01, $01, $0f, $0c, $0b + +raintbl: ; rainbow ramp + !byte $02, $08, $07, $0d, $03, $0e, $04, $0a + +; ---- the crew (content ported from rabbitc64) ---- +txt_crew0: !scr "mr.zero - tasnadi zsolt" +txt_crew0e: +txt_crew1: !scr "mr.one - tari balazs" +txt_crew1e: +txt_crew2: !scr "mr.two - timar zoltan" +txt_crew2e: +txt_crew3: !scr "mr.three - mezo bela" +txt_crew3e: +txt_ai1: !scr "warning: this game was" +txt_ai1e: +txt_ai2: !scr "written by an ai" +txt_ai2e: + +; ---- credits variables ---- +barphase: !byte 0 ; edge bar scroll offset diff --git a/defs.asm b/defs.asm new file mode 100644 index 0000000..2a7d452 --- /dev/null +++ b/defs.asm @@ -0,0 +1,137 @@ +; ----------------------------------------------------------- +; 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 +SPR_EN = $d015 ; sprite enable bits (parked at 0) +CIA1_PA = $dc00 ; CIA1 port A: keyboard columns / joystick 2 +CIA1_PB = $dc01 ; CIA1 port B: keyboard rows / joystick 1 +CIA1_TA = $dc04 ; CIA1 timer A low byte: free-running entropy + +; ---- SID registers (single-voice SFX driver) ---- +SID_V1FL = $d400 ; voice 1 frequency low +SID_V1FH = $d401 ; voice 1 frequency high +SID_V1PL = $d402 ; voice 1 pulse width low +SID_V1PH = $d403 ; voice 1 pulse width high +SID_V1CR = $d404 ; voice 1 control (waveform + gate) +SID_V1AD = $d405 ; voice 1 attack/decay +SID_V1SR = $d406 ; voice 1 sustain/release +SID_VOL = $d418 ; filter mode + master volume + +; ---- SFX IDs ---- +SFX_CURSOR = 0 ; corner cursor step: click +SFX_SELECT = 1 ; menu fire: rising tone +SFX_ROTATE = 2 ; wall spin: noise whoosh +SFX_DENY = 3 ; blocked spin: low buzz +SFX_WIN = 4 ; escape: rising jingle +SFX_LOSE = 5 ; caught: descending sweep +NSFX = 6 + +; ---- colors ---- +BLACK = 0 +WHITE = 1 +RED = 2 +CYAN = 3 +PURPLE = 4 +GREEN = 5 +BLUE = 6 +YELLOW = 7 +ORANGE = 8 +BROWN = 9 +LRED = 10 +DGREY = 11 +MGREY = 12 +LGREEN = 13 +LBLUE = 14 +LGREY = 15 + +; ---- joystick bits (after read_joy / read_joy2, active high) ---- +JOY_UP = 1 +JOY_DOWN = 2 +JOY_LEFT = 4 +JOY_RIGHT = 8 +JOY_FIRE = 16 + +; ---- the maze ---- +; 19x11 cells on a 39x23 char lattice (screen rows 1-23): +; cell = odd column, even row (walkable) +; post = even column, odd row (pivot, always solid) +; slot = the rest: wall pieces that spin around the posts +; wall thickness therefore equals corridor width: one cell +MAZE_CW = 19 ; cells across +MAZE_CH = 11 ; cells down +NCELLS = MAZE_CW * MAZE_CH +NENEMIES = 4 + +CH_WALL = $a0 ; inverse space: solid wall block +CH_FLOOR = $20 ; space: corridor +CH_EXIT = $5a ; diamond: the goal +CH_PLAYER = $51 ; filled ball: the runner +CH_ENEMY = $57 ; open circle: an enemy + +COL_WALL = MGREY ; spinnable wall pieces +COL_POST = DGREY ; fixed pivot posts +COL_EXIT = LGREEN ; the exit diamond + +; ---- game timing ---- +REP_HOLD = 10 ; runner: frames before a held dir repeats +REP_PERIOD = 5 ; runner: frames between repeat steps +CREP_HOLD = 12 ; cursor: same, a touch slower +CREP_PERIOD = 7 +RAS_SYNC = 251 ; raster line polled for the frame sync + +; ---- zero page pointers (same slots as rabbitc64) ---- +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/intro.asm b/intro.asm new file mode 100644 index 0000000..2a4486f --- /dev/null +++ b/intro.asm @@ -0,0 +1,146 @@ +; ----------------------------------------------------------- +; intro.asm - boot screen ported from rabbitc64: giant TTG +; letters built from 8x8 bitmaps with a gold color wash +; running down them, TELETYPE GAMES / PRESENTS below; +; advances to the menu after a few seconds or on fire +; ----------------------------------------------------------- + +INTRO_LROW = 3 ; top cell row of the giant letters +INTRO_TIME = 250 ; frames before it advances on its own + +intro_init: + jsr clear_screen + + ; T T G at columns 6 / 16 / 26 + lda #glyph_t + sta txthi + lda #6 + sta g8col + jsr draw_glyph8 + lda #16 + sta g8col + jsr draw_glyph8 + lda #glyph_g + sta txthi + lda #26 + sta g8col + jsr draw_glyph8 + + +prtext 13, 13, WHITE, txt_ttgname, 14 + +prtext 15, 16, LGREY, txt_presents, 8 + + lda #INTRO_TIME + sta introt + lda #0 + sta washphase + +setupd intro_update + rts + +intro_update: + ; gold wash: recolor the letter rows from the cyclic ramp + inc washphase + lda #0 + sta g8row +iw_row: + lda #INTRO_LROW + clc + adc g8row + tax + jsr row_ptr + lda washphase + lsr ; half speed keeps the shimmer gentle + clc + adc g8row + and #15 + tax + lda washtbl,x + sta rtmp + ldy #33 ; the letters span columns 6-33 +iw_col: + lda (scrlo),y + cmp #$a0 ; only touch the letter blocks + bne iw_skip + lda rtmp + sta (collo),y +iw_skip: + dey + cpy #5 + bne iw_col + inc g8row + lda g8row + cmp #8 + bne iw_row + + ; fire skips, otherwise wait out the timer + jsr start_edge + beq iw_tick + jmp menu_init +iw_tick: + dec introt + bne iw_done + jmp menu_init +iw_done: + rts + +; ---- draw an 8x8-cell giant letter: txtlo/hi = 8-byte +; bitmap, g8col = left cell column ---- +draw_glyph8: + lda #0 + sta g8row +g8_row: + lda #INTRO_LROW + clc + adc g8row + tax + jsr row_ptr + ldy g8row + lda (txtlo),y + sta curbits + ldy g8col + lda #8 + sta bitcnt +g8_bit: + asl curbits ; leftmost pixel first + bcc g8_next + lda #$a0 ; inverse space = solid block + sta (scrlo),y + lda #YELLOW + sta (collo),y +g8_next: + iny + dec bitcnt + bne g8_bit + inc g8row + lda g8row + cmp #8 + bne g8_row + rts + +; ---- 8x8 letter bitmaps ---- +glyph_t: + !byte %11111111 + !byte %11111111 + !byte %00111100 + !byte %00111100 + !byte %00111100 + !byte %00111100 + !byte %00111100 + !byte %00111100 +glyph_g: + !byte %00111100 + !byte %01111110 + !byte %11000011 + !byte %11000000 + !byte %11001111 + !byte %11000011 + !byte %01111110 + !byte %00111100 + +; ---- intro variables ---- +introt: !byte 0 ; frames left before auto-advancing +g8row: !byte 0 ; glyph renderer / wash internals +g8col: !byte 0 diff --git a/main.asm b/main.asm new file mode 100644 index 0000000..c69c4c5 --- /dev/null +++ b/main.asm @@ -0,0 +1,116 @@ +; ----------------------------------------------------------- +; main.asm - LABYRINTH for the C64 +; two-player maze duel: player 1 runs the corridors toward +; the green diamond exit while dodging the roaming colored +; enemies; player 2 rides the wall corners with a cursor +; and fire spins the four wall pieces around it by 90 +; degrees, reshaping the maze under the runner's feet +; +; Files: +; defs.asm - constants, zero page, macros +; intro.asm - TTG boot logo (ported from rabbitc64) +; menu.asm - main menu: play / rules / credits +; rules.asm - how to play +; controls.asm- joystick + keyboard mappings +; credits.asm - the crew (content from rabbitc64, new look) +; play.asm - maze generation + the game itself +; win.asm - escape screen, returns to the menu +; over.asm - game over screen with retry +; common.asm - shared routines, input, RNG, texts +; sound.asm - single-voice SFX driver +; +; Build: acme -f cbm -o labyrinth.prg main.asm +; ----------------------------------------------------------- + +* = $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 + lda #BLACK + sta BORDER + sta BG + lda #0 + sta SPR_EN ; character-only game: sprites stay off + sta framectr + sta fheld + jsr sfx_init + +!ifdef AUTOPLAY { ; test builds boot straight into one state + jsr play_init +} else { +!ifdef AUTORULES { + jsr rules_init +} else { +!ifdef AUTOCONTROLS { + jsr controls_init +} else { +!ifdef AUTOCREDITS { + jsr credits_init +} else { +!ifdef AUTOWIN { + jsr win_init +} else { +!ifdef AUTOLOSE { + jsr lose_init +} else { + jsr intro_init ; TTG logo, then the menu +} +} +} +} +} +} + +; ============================================================ +; 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 sfx_update ; advance the sound effect + 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 + +; ============================================================ +; the states and everything they share +; ============================================================ +!src "intro.asm" +!src "menu.asm" +!src "rules.asm" +!src "controls.asm" +!src "credits.asm" +!src "play.asm" +!src "win.asm" +!src "over.asm" +!src "common.asm" +!src "sound.asm" diff --git a/menu.asm b/menu.asm new file mode 100644 index 0000000..b453e31 --- /dev/null +++ b/menu.asm @@ -0,0 +1,194 @@ +; ----------------------------------------------------------- +; menu.asm - full-screen menu card: big LABYRINTH title with +; the gold wash, and a four-item menu: play / rules / +; controls / credits (joystick or w/s up/down + fire, space +; or return) +; ----------------------------------------------------------- + +MENU_ITEMS = 4 + +menu_init: + jsr clear_screen + + +prbig 2, 2, YELLOW, txt_title, 9 + +prtext 9, 13, LGREY, txt_ttgname, 14 + + lda #0 + sta menusel + sta washphase + + +setupd menu_update + rts + +menu_update: + ; gold wash rolling down the big title (rows 2-6) + inc washphase + lda #0 + sta g8row +mw_row: + lda #2 + clc + adc g8row + tax + jsr row_ptr + lda washphase + lsr + clc + adc g8row + and #15 + tax + lda washtbl,x + sta rtmp + ldy #37 ; the title spans columns 2-36 +mw_col: + lda (scrlo),y + cmp #$a0 ; only touch the letter blocks + bne mw_skip + lda rtmp + sta (collo),y +mw_skip: + dey + cpy #1 + bne mw_col + inc g8row + lda g8row + cmp #5 + bne mw_row + + ; joystick up/down steps the cursor (edge detected) + jsr read_joy + lda joyb + and #3 + beq mu_udrel + lda udheld + bne mu_udend + lda #1 + sta udheld + lda joyb + and #1 ; up + beq mu_down + lda menusel + beq mu_udend + dec menusel + lda #SFX_CURSOR + jsr sfx_play + jmp mu_udend +mu_down: + lda menusel + cmp #MENU_ITEMS-1 + beq mu_udend + inc menusel + lda #SFX_CURSOR + jsr sfx_play + jmp mu_udend +mu_udrel: + lda #0 + sta udheld +mu_udend: + + ; footer: both control lines always visible + +prtext 21, 8, LGREY, txt_info, txt_infoe-txt_info + +prtext 22, 5, LGREY, txt_info2, txt_info2e-txt_info2 + + ; the menu items: the picked line is yellow + ldx #0 +mm_items: + txa + pha + jsr draw_mitem + pla + tax + inx + cpx #MENU_ITEMS + bne mm_items + + ; blinking '>' cursor in front of the picked line + lda #$20 + sta SCREEN+12*40+15 + sta SCREEN+14*40+15 + sta SCREEN+16*40+15 + sta SCREEN+18*40+15 + lda framectr + and #16 + bne mu_start + ldx menusel + lda mcur_lo,x + sta scrlo + lda mcur_hi,x + sta scrhi + ldy #0 + lda #62 ; '>' + sta (scrlo),y + lda scrhi + clc + adc #$d4 ; same cell in color RAM + sta scrhi + lda #YELLOW + sta (scrlo),y + +mu_start: + ; fire or space opens the picked line + jsr start_edge + beq mu_done + lda #SFX_SELECT + jsr sfx_play + lda menusel + beq mg_play + cmp #1 + beq mg_rules + cmp #2 + beq mg_controls + jmp credits_init ; each init's rts returns to the main loop +mg_play: + jmp play_init +mg_rules: + jmp rules_init +mg_controls: + jmp controls_init +mu_done: + rts + +; ---- draw menu item X, yellow when it is the picked one ---- +draw_mitem: + lda mitem_lo,x + sta txtlo + lda mitem_hi,x + sta txthi + lda mitem_row,x + sta prow + lda #17 + sta pcol + lda mitem_len,x + sta plen + lda #WHITE + cpx menusel + bne dm_store + lda #YELLOW +dm_store: + sta pcolor + jmp print_text + +; ---- menu tables ---- +mitem_lo: !byte txt_play, >txt_rules, >txt_controls, >txt_credits +mitem_len: !byte 4, 5, 8, 7 +mitem_row: !byte 12, 14, 16, 18 +mcur_lo: ; cursor cells: column 15 of those rows + !byte <(SCREEN+12*40+15), <(SCREEN+14*40+15) + !byte <(SCREEN+16*40+15), <(SCREEN+18*40+15) +mcur_hi: + !byte >(SCREEN+12*40+15), >(SCREEN+14*40+15) + !byte >(SCREEN+16*40+15), >(SCREEN+18*40+15) + +; ---- menu texts ---- +txt_play: !scr "play" +txt_rules: !scr "rules" +txt_credits: !scr "credits" +txt_info: !scr "p1: joy2 / wasd - runner" +txt_infoe: +txt_info2: !scr "p2: joy1 / ijkl - wall spinner" +txt_info2e: + +; ---- menu variables ---- +menusel: !byte 0 ; picked line: 0-2 +udheld: !byte 0 ; joystick up/down edge detection state diff --git a/metadata.json b/metadata.json new file mode 100644 index 0000000..49943ee --- /dev/null +++ b/metadata.json @@ -0,0 +1,9 @@ +{ + "name": "labyrinth", + "title": "Labyrinth", + "author": "Teletype Games", + "desc": "Two-player maze duel: one player runs for the exit while the other spins the walls of the labyrinth around their corners. Dodge the enemies, find the diamond.", + "site": "https://git.teletype.hu/games/labyrinth", + "license": "MIT", + "version": "1.0.0" +} diff --git a/over.asm b/over.asm new file mode 100644 index 0000000..cabf2c5 --- /dev/null +++ b/over.asm @@ -0,0 +1,56 @@ +; ----------------------------------------------------------- +; over.asm - game over screen: big red GAME OVER, fire +; restarts with a fresh maze, the m key backs out to the menu +; ----------------------------------------------------------- + +lose_init: + jsr clear_screen + + +prbig 3, 2, RED, txt_gameover, 9 + + +prtext 11, 10, WHITE, txt_caught, txt_caughte-txt_caught + + +prtext 21, 13, LGREY, txt_mmenu, txt_mmenue-txt_mmenu + + +setupd lose_update + rts + +lose_update: + ; blink the retry prompt + lda framectr + and #32 + bne lu_hide + +prtext 19, 12, YELLOW, txt_retry, txt_retrye-txt_retry + jmp lu_input +lu_hide: + +prtext 19, 12, YELLOW, txt_blank, txt_retrye-txt_retry +lu_input: + ; fire or space: straight back in with a fresh maze + jsr start_edge + beq lu_nofire + jmp play_init +lu_nofire: + ; the m key: keyboard column 4, row bit 4 (start_edge has + ; already returned zero here, so joystick fire is not + ; ghosting this row line) + lda #$ef + sta CIA1_PA + lda CIA1_PB + and #$10 + bne lu_done + lda #$ff + sta CIA1_PA + jmp menu_init +lu_done: + lda #$ff + sta CIA1_PA + rts + +; ---- game over texts ---- +txt_gameover: !scr "game over" +txt_caught: !scr "the enemies got you" +txt_caughte: +txt_retry: !scr "fire = try again" +txt_retrye: +txt_mmenu: !scr "m = main menu" +txt_mmenue: diff --git a/play.asm b/play.asm new file mode 100644 index 0000000..add0360 --- /dev/null +++ b/play.asm @@ -0,0 +1,783 @@ +; ----------------------------------------------------------- +; play.asm - the game: a fresh maze is carved with a +; recursive backtracker every round, player 1 (joystick 2) +; runs the corridors toward the exit diamond, player 2 +; (joystick 1) rides the wall corners and fire spins the +; four wall pieces around the cursor by 90 degrees; the +; roaming colored enemies end the round on touch +; +; screen layout: status on row 0, the 39x23 maze lattice on +; rows 1-23 (cells = odd col / even row, posts = even col / +; odd row, wall slots = the rest), a hint line on row 24 +; ----------------------------------------------------------- + +play_init: + jsr clear_screen + +prtext 0, 0, WHITE, txt_title, 9 + +prtext 0, 28, LGREY, txt_goal, txt_goale-txt_goal + +prtext 24, 6, DGREY, txt_hint, txt_hinte-txt_hint + + ; fill the whole lattice with wall blocks + ldx #1 +pi_fillrow: + stx ptmp + jsr row_ptr + ldy #38 +pi_fillcol: + lda #CH_WALL + sta (scrlo),y + lda #COL_WALL + sta (collo),y + dey + bpl pi_fillcol + ldx ptmp + inx + cpx #24 + bne pi_fillrow + + ; the pivot posts get their own darker shade + ldx #1 ; post rows 1, 3, ... 23 +pi_postrow: + stx ptmp + jsr row_ptr + ldy #38 ; post columns 38, 36, ... 0 +pi_postcol: + lda #COL_POST + sta (collo),y + dey + dey + bpl pi_postcol + ldx ptmp + inx + inx + cpx #25 + bcc pi_postrow + + ; carve the maze + ldx #0 + lda #0 +pi_clrv: + sta visited,x + inx + cpx #NCELLS + bne pi_clrv + jsr gen_maze + + ; the exit: a green diamond in the right border wall +pi_exit: + jsr rand8 + and #$0f + cmp #MAZE_CH + bcs pi_exit + asl ; cell row -> screen row + clc + adc #2 + sta exrow + tax + jsr row_ptr + ldy #38 + lda #CH_EXIT + sta (scrlo),y + lda #COL_EXIT + sta (collo),y + + ; the runner starts in the top-left cell + lda #1 + sta px + lda #2 + sta py + jsr draw_player + + ; the corner cursor starts on the center post + lda #18 + sta kx + lda #11 + sta ky + + jsr spawn_enemies + + ; input state: a fire still held from the previous screen + ; must be released before it can spin walls + lda #0 + sta rheld + sta cheld + sta endflag + lda #1 + sta cfheld + +setupd play_update + rts + +; ============================================================ +; maze generation - recursive backtracker over the 19x11 +; cells; the screen itself is the wall data structure +; ============================================================ +gen_maze: + lda #0 + sta gcx + sta gcy + sta gsp + jsr gv_mark + jsr carve_cell +gm_loop: + ; collect the unvisited neighbor directions + lda #0 + sta gn + ldx #0 +gm_scan: + stx gtd + lda gcx + clc + adc dxtab,x + sta gnx + cmp #MAZE_CW ; unsigned: -1 wraps to $ff and fails too + bcs gm_next + ldx gtd + lda gcy + clc + adc dytab,x + sta gny + cmp #MAZE_CH + bcs gm_next + ldx gny + lda mul19,x + clc + adc gnx + tax + lda visited,x + bne gm_next + ldx gn + lda gtd + sta gdlist,x + inc gn +gm_next: + ldx gtd + inx + cpx #4 + bne gm_scan + lda gn + beq gm_pop + ; pick a random one of them +gm_pick: + jsr rand8 + and #3 + cmp gn + bcs gm_pick + tax + lda gdlist,x + sta gtd + ; push the current cell, open the wall, step through it + ldx gsp + lda gcx + sta stackx,x + lda gcy + sta stacky,x + inc gsp + jsr carve_wall + ldx gtd + lda gcx + clc + adc dxtab,x + sta gcx + ldx gtd + lda gcy + clc + adc dytab,x + sta gcy + jsr gv_mark + jsr carve_cell + jmp gm_loop +gm_pop: + ; dead end: back up; empty stack = the maze is done + lda gsp + beq gm_done + dec gsp + ldx gsp + lda stackx,x + sta gcx + lda stacky,x + sta gcy + jmp gm_loop +gm_done: + rts + +gv_mark: ; visited[gcy * 19 + gcx] = 1 + ldx gcy + lda mul19,x + clc + adc gcx + tax + lda #1 + sta visited,x + rts + +carve_cell: ; open the cell (2*gcx+1, 2*gcy+2) + lda gcy + asl + clc + adc #2 + tax + jsr row_ptr + lda gcx + asl + clc + adc #1 + tay + lda #CH_FLOOR + sta (scrlo),y + lda #BLACK + sta (collo),y + rts + +carve_wall: ; open the wall slot toward direction gtd + ldx gtd + lda gcy + asl + clc + adc #2 + clc + adc dytab,x + tax + jsr row_ptr + ldx gtd + lda gcx + asl + clc + adc #1 + clc + adc dxtab,x + tay + lda #CH_FLOOR + sta (scrlo),y + lda #BLACK + sta (collo),y + rts + +dxtab: !byte 0, 0, $ff, 1 ; up, down, left, right +dytab: !byte $ff, 1, 0, 0 +mul19: !byte 0, 19, 38, 57, 76, 95, 114, 133, 152, 171, 190 + +; ---- enemies: random colors, random right-half cells ---- +spawn_enemies: + lda #0 + sta ei +sn_loop: +sn_rx: + jsr rand8 + and #$0f + cmp #MAZE_CH + bcs sn_rx + clc + adc #MAZE_CW-MAZE_CH ; cell column 8-18: away from the runner + asl + clc + adc #1 + sta gnx ; candidate screen column +sn_ry: + jsr rand8 + and #$0f + cmp #MAZE_CH + bcs sn_ry + asl + clc + adc #2 + sta gny ; candidate screen row + ldx gny + jsr row_ptr + ldy gnx + lda (scrlo),y + cmp #CH_FLOOR ; only an empty cell will do + bne sn_rx + lda #CH_ENEMY + sta (scrlo),y + jsr rand8 + and #7 + tax + lda ecoltab,x + sta (collo),y + ldx ei + sta ecol,x + lda gnx + sta ex,x + lda gny + sta ey,x + jsr rand8 + and #3 + sta edir,x + inc ei + lda ei + cmp #NENEMIES + bne sn_loop + rts + +ecoltab: ; anything but the white runner / grey walls + !byte RED, CYAN, PURPLE, YELLOW, ORANGE, LRED, LBLUE, BLUE + +; ============================================================ +; one frame of play +; ============================================================ +play_update: + jsr upd_runner + lda endflag + bne pu_end + jsr upd_enemies + lda endflag + bne pu_end + jsr upd_cursor + jsr upd_flash + rts +pu_end: + cmp #1 + beq pu_win + lda #SFX_LOSE + jsr sfx_play + jmp lose_init ; its rts returns to the main loop +pu_win: + lda #SFX_WIN + jsr sfx_play + jmp win_init + +; ---- player 1: walk the corridors, one char per step ---- +upd_runner: + jsr read_joy + and #$0f + sta rdir + bne ur_held + lda #0 + sta rheld + rts +ur_held: + lda rheld + beq ur_first + dec rtimer + beq ur_rep + rts +ur_rep: + lda #REP_PERIOD + sta rtimer + jmp ur_move +ur_first: + lda #1 + sta rheld + lda #REP_HOLD + sta rtimer +ur_move: + ldx px + ldy py + lda rdir + lsr + bcs ur_up + lsr + bcs ur_down + lsr + bcs ur_left + inx ; right + jmp ur_try +ur_up: + dey + jmp ur_try +ur_down: + iny + jmp ur_try +ur_left: + dex +ur_try: + stx ttx + sty tty + ldx tty + jsr row_ptr + ldy ttx + lda (scrlo),y + cmp #CH_FLOOR + beq ur_go + cmp #CH_EXIT + beq ur_win + cmp #CH_ENEMY + beq ur_dead + rts ; a wall: blocked +ur_go: + ldx py + jsr row_ptr + ldy px + lda #CH_FLOOR + sta (scrlo),y + lda #BLACK + sta (collo),y + lda ttx + sta px + lda tty + sta py + jmp draw_player +ur_win: + lda #1 + sta endflag + rts +ur_dead: + lda #2 + sta endflag + rts + +draw_player: + ldx py + jsr row_ptr + ldy px + lda #CH_PLAYER + sta (scrlo),y + lda #WHITE + sta (collo),y + rts + +; ---- player 2: the corner cursor + the wall spin ---- +upd_cursor: + jsr read_joy2 + sta j2bits + and #$0f + sta cdir + bne uc_held + lda #0 + sta cheld + jmp uc_fire +uc_held: + lda cheld + beq uc_first + dec ctimer + beq uc_rep + jmp uc_fire +uc_rep: + lda #CREP_PERIOD + sta ctimer + jmp uc_move +uc_first: + lda #1 + sta cheld + lda #CREP_HOLD + sta ctimer +uc_move: + ldx kx + ldy ky + lda cdir + lsr + bcs uc_up + lsr + bcs uc_down + lsr + bcs uc_left + cpx #36 ; right: interior posts end at column 36 + bcs uc_fire + inx + inx + jmp uc_go +uc_up: + cpy #4 ; top interior post row is 3 + bcc uc_fire + dey + dey + jmp uc_go +uc_down: + cpy #21 ; bottom interior post row is 21 + bcs uc_fire + iny + iny + jmp uc_go +uc_left: + cpx #3 ; left interior post column is 2 + bcc uc_fire + dex + dex +uc_go: + stx ttx + sty tty + ldx ky ; the old post gets its color back + jsr row_ptr + ldy kx + lda #COL_POST + sta (collo),y + lda ttx + sta kx + lda tty + sta ky + lda #SFX_CURSOR + jsr sfx_play +uc_fire: + lda j2bits + and #JOY_FIRE + beq uc_frel + lda cfheld + bne uc_done + lda #1 + sta cfheld + jmp do_rotate ; its rts returns to play_update's caller +uc_frel: + lda #0 + sta cfheld +uc_done: + rts + +; ---- spin the four wall slots around post (kx, ky) by 90 +; degrees clockwise; denied while anyone stands in them ---- +do_rotate: + ldx ky ; gather the four slot chars + dex + jsr row_ptr + ldy kx + lda (scrlo),y + sta nch + ldx ky + inx + jsr row_ptr + ldy kx + lda (scrlo),y + sta sch + ldx ky + jsr row_ptr + ldy kx + dey + lda (scrlo),y + sta wch + iny + iny + lda (scrlo),y + sta ech + ; every slot must be plain wall or floor + lda nch + jsr rot_ok + bcs rot_deny + lda sch + jsr rot_ok + bcs rot_deny + lda wch + jsr rot_ok + bcs rot_deny + lda ech + jsr rot_ok + bcs rot_deny + ; clockwise: north<-west, east<-north, south<-east, west<-south + ldx ky + dex + jsr row_ptr + ldy kx + lda wch + jsr put_slot + ldx ky + inx + jsr row_ptr + ldy kx + lda ech + jsr put_slot + ldx ky + jsr row_ptr + ldy kx + dey + lda sch + jsr put_slot + iny + iny + lda nch + jsr put_slot + lda #SFX_ROTATE + jmp sfx_play +rot_deny: + lda #SFX_DENY + jmp sfx_play + +rot_ok: ; C clear when A is spinnable (wall or floor) + cmp #CH_WALL + beq ro_yes + cmp #CH_FLOOR + beq ro_yes + sec + rts +ro_yes: + clc + rts + +put_slot: ; write char A at (scrlo),y + matching color + sta (scrlo),y + cmp #CH_WALL + beq ps_wall + lda #BLACK + sta (collo),y + rts +ps_wall: + lda #COL_WALL + sta (collo),y + rts + +; ---- the enemies: staggered random walkers ---- +upd_enemies: + lda #0 + sta ei +ue_loop: + ldx ei + lda framectr + clc + adc estag,x + and #7 ; each enemy steps every 8th frame + bne ue_next + jsr enemy_step + lda endflag + bne ue_out +ue_next: + inc ei + lda ei + cmp #NENEMIES + bne ue_loop +ue_out: + rts + +estag: !byte 0, 2, 4, 6 + +enemy_step: + jsr rand8 ; 1 in 4: wander off in a new direction + and #3 + beq es_newdir + ldx ei + lda edir,x + jsr etry_dir + bcc es_done +es_newdir: + lda #8 + sta etries +es_pick: + jsr rand8 + and #3 + jsr etry_dir + bcc es_done + dec etries ; walled in on all sides: sit this one out + bne es_pick +es_done: + rts + +etry_dir: ; A = direction; C clear when it moved (or won) + sta etd + tax + ldy ei + lda ex,y + clc + adc dxtab,x + sta ttx + lda ey,y + clc + adc dytab,x + sta tty + ldx tty + jsr row_ptr + ldy ttx + lda (scrlo),y + cmp #CH_FLOOR + beq ed_move + cmp #CH_PLAYER + beq ed_kill + sec ; wall, exit or another enemy: blocked + rts +ed_kill: + lda #2 + sta endflag + clc + rts +ed_move: + ldx ei + lda ey,x + sta gny ; old position + lda ex,x + sta gnx + lda ttx + sta ex,x + lda tty + sta ey,x + lda etd + sta edir,x + ldx gny ; erase the old cell + jsr row_ptr + ldy gnx + lda #CH_FLOOR + sta (scrlo),y + lda #BLACK + sta (collo),y + ldx tty ; draw at the new one + jsr row_ptr + ldy ttx + lda #CH_ENEMY + sta (scrlo),y + ldx ei + lda ecol,x + sta (collo),y + clc + rts + +; ---- per-frame flashes: the exit blinks, the cursor pulses ---- +upd_flash: + ldx exrow + jsr row_ptr + ldy #38 + lda framectr + and #8 + bne uf_exw + lda #COL_EXIT + jmp uf_exset +uf_exw: + lda #WHITE +uf_exset: + sta (collo),y + ldx ky + jsr row_ptr + ldy kx + lda framectr + and #4 + bne uf_cred + lda #YELLOW + jmp uf_cset +uf_cred: + lda #RED +uf_cset: + sta (collo),y + rts + +; ---- play texts ---- +txt_goal: !scr "reach the " + !byte CH_EXIT +txt_goale: +txt_hint: !scr "p1: joy2/wasd p2: joy1/ijkl" +txt_hinte: + +; ---- play variables ---- +px: !byte 0 ; runner position (screen col / row) +py: !byte 0 +kx: !byte 0 ; corner cursor position (post col / row) +ky: !byte 0 +exrow: !byte 0 ; screen row of the exit diamond +endflag: !byte 0 ; 0 = playing, 1 = escaped, 2 = caught +rheld: !byte 0 ; runner direction repeat state +rtimer: !byte 0 +rdir: !byte 0 +cheld: !byte 0 ; cursor direction repeat state +ctimer: !byte 0 +cdir: !byte 0 +cfheld: !byte 0 ; cursor fire edge detection state +j2bits: !byte 0 +ptmp: !byte 0 ; scratch +ttx: !byte 0 ; move target (col / row) +tty: !byte 0 +nch: !byte 0 ; the four slot chars around the cursor +ech: !byte 0 +sch: !byte 0 +wch: !byte 0 +ei: !byte 0 ; enemy loop index +etries: !byte 0 +etd: !byte 0 +gcx: !byte 0 ; maze generator state +gcy: !byte 0 +gsp: !byte 0 +gn: !byte 0 +gtd: !byte 0 +gnx: !byte 0 +gny: !byte 0 +gdlist: !fill 4, 0 +ex: !fill NENEMIES, 0 ; enemy positions, headings, colors +ey: !fill NENEMIES, 0 +edir: !fill NENEMIES, 0 +ecol: !fill NENEMIES, 0 + +; ---- maze generator buffers ---- +visited: !fill NCELLS, 0 +stackx: !fill NCELLS, 0 +stacky: !fill NCELLS, 0 diff --git a/rules.asm b/rules.asm new file mode 100644 index 0000000..e6809d4 --- /dev/null +++ b/rules.asm @@ -0,0 +1,56 @@ +; ----------------------------------------------------------- +; rules.asm - how to play, one static card; fire returns +; ----------------------------------------------------------- + +rules_init: + jsr clear_screen + + +prbig 1, 10, WHITE, txt_rules, 5 + + +prtext 8, 9, LGREEN, txt_r1, txt_r1e-txt_r1 + +prtext 9, 8, WHITE, txt_r2, txt_r2e-txt_r2 + +prtext 10, 7, WHITE, txt_r3, txt_r3e-txt_r3 + + +prtext 12, 9, LBLUE, txt_r4, txt_r4e-txt_r4 + +prtext 13, 6, WHITE, txt_r5, txt_r5e-txt_r5 + +prtext 14, 6, WHITE, txt_r6, txt_r6e-txt_r6 + +prtext 15, 7, WHITE, txt_r7, txt_r7e-txt_r7 + + +prtext 17, 7, LRED, txt_r8, txt_r8e-txt_r8 + + +setupd rules_update + rts + +rules_update: + ; blink the return prompt + lda framectr + and #32 + bne ru_hide + +prtext 21, 13, YELLOW, txt_return, 13 + jmp ru_input +ru_hide: + +prtext 21, 13, YELLOW, txt_blank, 13 +ru_input: + jsr start_edge + beq ru_stay + jmp menu_init +ru_stay: + rts + +; ---- rules texts ---- +txt_r1: !scr "player 1 - joystick 2" +txt_r1e: +txt_r2: !scr "run the maze and escape" +txt_r2e: +txt_r3: !scr "through the green diamond." +txt_r3e: +txt_r4: !scr "player 2 - joystick 1" +txt_r4e: +txt_r5: !scr "ride the corners of the walls;" +txt_r5e: +txt_r6: !scr "fire spins the four wall pieces" +txt_r6e: +txt_r7: !scr "around the cursor by 90 degrees." +txt_r7e: +txt_r8: !scr "avoid the colored enemies!" +txt_r8e: diff --git a/sound.asm b/sound.asm new file mode 100644 index 0000000..f9513fb --- /dev/null +++ b/sound.asm @@ -0,0 +1,71 @@ +; ----------------------------------------------------------- +; sound.asm - single-voice SFX driver: each effect is a +; waveform + start pitch + per-frame pitch slide + duration, +; played on SID voice 1 (sustain 0, the decay shapes it) +; ----------------------------------------------------------- + +sfx_init: + lda #$0f ; full volume, no filter + sta SID_VOL + lda #0 + sta SID_V1CR + sta sfx_timer + rts + +; ---- start effect A ---- +sfx_play: + tax + lda #0 + sta SID_V1CR ; retrigger the envelope cleanly + lda sfxad,x + sta SID_V1AD + lda #0 + sta SID_V1SR + sta SID_V1FL + lda sfxfreq,x + sta SID_V1FH + sta sfx_fh + lda #$08 ; 50% pulse width, if a pulse wave is used + sta SID_V1PH + lda #0 + sta SID_V1PL + lda sfxctrl,x + sta SID_V1CR + sta sfx_ctrl + lda sfxslide,x + sta sfx_slide + lda sfxdur,x + sta sfx_timer + rts + +; ---- called once per frame by the main loop ---- +sfx_update: + lda sfx_timer + beq su_idle + dec sfx_timer + bne su_slide + lda sfx_ctrl ; time is up: gate off + and #$fe + sta SID_V1CR + rts +su_slide: + lda sfx_fh ; bend the pitch by the slide value + clc + adc sfx_slide + sta sfx_fh + sta SID_V1FH +su_idle: + rts + +; ---- effect tables: cursor, select, rotate, deny, win, lose ---- +sfxfreq: !byte $50, $20, $12, $06, $20, $38 +sfxslide: !byte $00, $04, $fe, $00, $02, $ff +sfxctrl: !byte $11, $21, $81, $21, $11, $21 +sfxad: !byte $06, $08, $08, $08, $0a, $0a +sfxdur: !byte $04, $0a, $08, $0c, $28, $30 + +; ---- driver variables ---- +sfx_timer: !byte 0 ; frames left of the current effect +sfx_fh: !byte 0 ; frequency high byte shadow +sfx_slide: !byte 0 ; signed per-frame pitch step +sfx_ctrl: !byte 0 ; control register shadow diff --git a/win.asm b/win.asm new file mode 100644 index 0000000..51a1441 --- /dev/null +++ b/win.asm @@ -0,0 +1,76 @@ +; ----------------------------------------------------------- +; win.asm - the escape screen: big ESCAPED with the gold +; wash; fire returns to the main menu +; ----------------------------------------------------------- + +win_init: + jsr clear_screen + + +prbig 3, 6, YELLOW, txt_escaped, 7 + + +prtext 11, 15, WHITE, txt_welldone, txt_welldonee-txt_welldone + +prtext 13, 9, LGREY, txt_beaten, txt_beatene-txt_beaten + + lda #0 + sta washphase + +setupd win_update + rts + +win_update: + ; the gold wash rolls down the big letters (rows 3-7) + inc washphase + lda #0 + sta rtmp2 +wu_row: + lda #3 + clc + adc rtmp2 + tax + jsr row_ptr + lda washphase + lsr + clc + adc rtmp2 + and #15 + tax + lda washtbl,x + sta rtmp + ldy #32 ; the letters span columns 6-32 +wu_col: + lda (scrlo),y + cmp #$a0 + bne wu_skip + lda rtmp + sta (collo),y +wu_skip: + dey + cpy #5 + bne wu_col + inc rtmp2 + lda rtmp2 + cmp #5 + bne wu_row + + ; blink the prompt + lda framectr + and #32 + bne wu_hide + +prtext 20, 14, YELLOW, txt_firemenu, txt_firemenue-txt_firemenu + jmp wu_input +wu_hide: + +prtext 20, 14, YELLOW, txt_blank, txt_firemenue-txt_firemenu +wu_input: + jsr start_edge + beq wu_stay + jmp menu_init ; a win leads back to the main screen +wu_stay: + rts + +; ---- win texts ---- +txt_escaped: !scr "escaped" +txt_welldone: !scr "well done!" +txt_welldonee: +txt_beaten: !scr "you beat the labyrinth" +txt_beatene: +txt_firemenu: !scr "fire = menu" +txt_firemenue: