commit e9c69864f124b5bec6a4a4dc49129e0d53decf4c Author: Zsolt Tasnadi Date: Sat Jul 18 16:12:06 2026 +0200 initial commit 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: