commit 9f263276fe96bc34530e7c45252be2cd0b87a6eb Author: Simon Owen Date: Tue Nov 8 23:49:42 2011 +0000 pacemuzx v1.0 diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..b023f67 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +ReadMe.txt -crlf +make.bat -crlf +make.bat-dist -crlf diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cb2ee98 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +dist/ +sprites.bin +tiles.bin +pacemuzx.tap diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..4533a82 --- /dev/null +++ b/Makefile @@ -0,0 +1,30 @@ +TAPE=pacemuzx.tap +ROMS=pacman.6e pacman.6f pacman.6h pacman.6j + +.PHONY: dist clean + +$(TAPE): loader.tap pacemuzx.o + cat loader.tap pacemuzx.o > $@ + +tiles.bin: tiles.png + ./png2bin.pl $< 6 + +sprites.bin: sprites.png + ./png2bin.pl $< 12 + +pacemuzx.o: pacemuzx.asm tiles.bin sprites.bin $(ROMS) + pasmo --tap pacemuzx.asm pacemuzx.o pacman.sym + +dist: $(TAPE) + rm -rf dist + mkdir dist + cp ReadMe.txt dist/ + cp Makefile-dist dist/Makefile + cp make.bat-dist dist/make.bat + ./remove_rom.pl $(TAPE) + mv start.part end.part dist/ + +clean: + rm -f $(TAPE) pacemuzx.sym pacemuzx.o + rm -f tiles.bin sprites.bin start.part end.part + rm -rf dist diff --git a/Makefile-dist b/Makefile-dist new file mode 100644 index 0000000..b8f1c9d --- /dev/null +++ b/Makefile-dist @@ -0,0 +1,8 @@ +TAPE=pacemuzx.tap +ROMS=pacman.6e pacman.6f pacman.6h pacman.6j + +$(TAPE): start.part $(ROMS) end.part + cat start.part $(ROMS) end.part > $(TAPE) + +clean: + rm -f pacemuzx.tap diff --git a/ReadMe.txt b/ReadMe.txt new file mode 100644 index 0000000..1bcfb2d --- /dev/null +++ b/ReadMe.txt @@ -0,0 +1,19 @@ +Pac-Man Emulator for Sinclair Spectrum +-------------------------------------- + +This program requires a Spectrum +2A/+3, and will not work on earlier models. + +The Pac-Man ROMs cannot be supplied with this program, so you must supply your +own copies of the following files: + + pacman.6e pacman.6f pacman.6h pacman.6j + +Copy them to the same directory as this file, then run make.bat (Windows) or +make (Mac/Linux/Un*x) to build the final pacemuzx.tap image file. + +Enjoy! + +--- + +Simon Owen +http://simonowen.com/spectrum/pacemuzx/ diff --git a/loader.tap b/loader.tap new file mode 100644 index 0000000..0e39318 Binary files /dev/null and b/loader.tap differ diff --git a/make.bat b/make.bat new file mode 100644 index 0000000..a1451b9 --- /dev/null +++ b/make.bat @@ -0,0 +1,29 @@ +@echo off + +if "%1"=="clean" goto clean + +png2bin.pl tiles.png 6 +png2bin.pl sprites.png 12 + +pasmo --tap pacemuzx.asm pacemuzx.o + +copy /b loader.tap+pacemuzx.o pacemuzx.tap + +if not "%1"=="dist" goto end + +if not exist dist mkdir dist +copy ReadMe.txt dist\ +copy Makefile-dist dist\Makefile +copy make.bat-dist dist\make.bat +remove_rom.pl +move start.part dist\ +move end.part dist\ +goto end + +:clean +if exist pacemuzx.tap del pacemuzx.tap pacemuzx.sym pacemuzx.o +if exist tiles.bin del tiles.bin sprites.bin +if exist dist\ del dist\Makefile dist\make.bat dist\start.part dist\end.part dist\pacemuzx.tap +if exist dist\ rmdir dist + +:end diff --git a/make.bat-dist b/make.bat-dist new file mode 100644 index 0000000..802cbeb --- /dev/null +++ b/make.bat-dist @@ -0,0 +1,30 @@ +@echo off + +if not exist pacman.6e goto missing +if not exist pacman.6f goto missing +if not exist pacman.6h goto missing +if not exist pacman.6j goto missing +goto got_roms + +:missing +echo. +echo **************************************************************** +echo. +echo The Pac-Man ROMs can't be distributed with this program, so you +echo must provide your own copies of: +echo. +echo pacman.6e pacman.6f pacman.6h pacman.6j +echo. +echo Copy them to this directory and re-run to generate: pacemuzx.tap +echo. +echo **************************************************************** +echo. +pause +goto end + +:got_roms +copy /b start.part+pacman.6e+pacman.6f+pacman.6h+pacman.6j+end.part pacemuzx.tap + +start pacemuzx.tap + +:end diff --git a/pacemuzx.asm b/pacemuzx.asm new file mode 100644 index 0000000..ea8d997 --- /dev/null +++ b/pacemuzx.asm @@ -0,0 +1,2503 @@ +; Pac-Man hardware emulation for the Sinclair ZX Spectrum +; +; http://simonowen.com/spectrum/pacemuzx/ + +debug: equ 0 + +; Memory maps +; +; Loading (normal paging R/5/2/0): +; a000-afff - emulation code +; b000-bfff - unshifted tiles+sprites +; c000-ffff - 16K Pac-Man ROM +; +; Emulation (special paging 0/1/2/3): +; 0000-3fff - 16K Pac-Man ROM +; 4000-50ff - Pac-Man display, I/O and working RAM +; 5100-7fff - unused +; 8000-9fff - 2nd half of sprite data +; a000-afff - emulation code +; b000-bfff - look-up tables +; c000-dfff - 8K sound table +; e000-ffff - first 8K of Pac-Man ROM (unpatched) +; +; Graphics (normal paging R/5/2/7): +; 0000-3fff - Spectrum ROM +; 4000-5aff - Spectrum display (normal) +; 5b00-5be3 - screen data behind sprites (normal) +; 5be4-9fff - pre-rotated sprite graphics +; a000-afff - emulation code +; b000-bfff - look-up tables +; c000-daff - Spectrum display (alt) +; db00-dbe3 - screen data behind sprites (alt) +; dbe4-ffff - pre-rotated tile graphics + +attr_colour: equ &07 ; default = white on black +flash_colour: equ &41 ; maze flash colour = bright blue on black + +kempston: equ 31 ; Kempston joystick in bits 4-0 +border: equ 254 ; Border colour in bits 2-0 +keyboard: equ 254 ; Keyboard matrix in bits 4-0 + +pac_footer: equ &4000 ; credit and fruit display +pac_chars: equ &4040 ; start of main Pac-Man display (skipping the score rows) +pac_header: equ &43c0 ; 64 bytes containing the score + +; address of saved sprite block followed by the data itself +spr_save_2: equ &5b00 +spr_save_3: equ spr_save_2+2+(3*12) +spr_save_4: equ spr_save_3+2+(3*12) +spr_save_5: equ spr_save_4+2+(3*12) +spr_save_6: equ spr_save_5+2+(3*12) +spr_save_7: equ spr_save_6+2+(3*12) +spr_save_end: equ spr_save_7+2+(3*12) + +; pre-shifted sprite graphics +spr_data_0: equ spr_save_end +spr_data_1: equ spr_data_0 + (76*2*12) ; 11111111 11110000 +spr_data_2: equ spr_data_1 + (76*2*12) ; 01111111 11111000 +spr_data_3: equ spr_data_2 + (76*2*12) ; 00111111 11111100 +spr_data_4: equ spr_data_3 + (76*2*12) ; 00011111 11111110 +spr_data_5: equ spr_data_4 + (76*2*12) ; 00001111 11111111 +spr_data_6: equ spr_data_5 + (76*3*12) ; 00000111 11111111 10000000 +spr_data_7: equ spr_data_6 + (76*3*12) ; 00000011 11111111 11000000 +spr_data_end: equ spr_data_7 + (76*3*12) ; 00000001 11111111 11100000 + +; pre-shifted tile graphics +tile_data_0: equ &8000 + spr_data_0 +tile_data_6: equ tile_data_0 + (192*1*6) ; 11111100 +tile_data_4: equ tile_data_6 + (192*2*6) ; 00000011 11110000 +tile_data_2: equ tile_data_4 + (192*2*6) ; 00001111 11000000 +end_tile_data: equ tile_data_2 + (192*1*6) ; 00111111 + +; sound look-up table +sound_table: equ &c000 + + +MACRO set_border, colour +IF debug + ld a,colour + out (border),a +ENDIF +ENDM + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + org &a000 +start: jr start2 + +; Dips at fixed address for easy poking +dip_5000: defb %11111111 ; c21tdrlu c=credit, 2=coin2, 1=coin1, t=rack test on/off ; down, right, left, up + ; default: rack test off, nothing else signalled +dip_5040: defb %11111111 ; c--sdrlu c=cocktail/upright ; s=service mode on/off ; down, right, left, up (player 2) + ; default: upright mode, service mode off, no controls pressed +dip_5080: defb %11001001 ; -dbbllcc d=hard/normal bb=bonus life at 10K/15K/20K/none ; ll=1/2/3/5 lives ; cc=freeplay/1coin1credit/1coin2credits/2coins1credit + ; default: normal, bonus life at 10K, 3 lives, 1 coin 1 credit + +; First, check that +2A/+3 paging is available +start2: di + ld a,(&5b5c) ; sysvar holding 128K paging + ex af,af' ; keep safe + ld a,%00000001 ; special paging mode (0/1/2/3) + ld bc,&1ffd ; +3 paging port + out (c),a ; attempt paging + ld a,(3) ; peek value in Pac-Man ROM + cp &ed ; as expected? + jr z,start3 ; if so, start up + + ex af,af' + out (c),a ; restore 128K paging (if any) + call &0d6b ; CLS + ld a,2 ; main screen + call &1601 ; CHAN-OPEN + ei + + ld hl,fail_msg +msg_lp: ld a,(hl) + and a + ret z + rst 16 ; PRINT-A + inc l + jr msg_lp + +fail_msg: defm "This program requires a +2A/+3" + defb 0 + +; Next, move any data from load position to final location +start3: ld sp,new_stack + + ld hl,&0000 + ld de,&e000 + ld bc,&2000 + ldir ; copy 8K of ROM to &e000 + + xor a ; normal paging mode + ld bc,&1ffd + out (c),a ; set paging to R/5/2/X + ld a,%00000111 ; normal display, page 7 + ld b,&7f + out (c),a ; page 7 at &c000 + ld ixh,&80 ; write to alt screen + + ld hl,&b000 + ld de,tile_data_0 + ld bc,&0480 + ldir ; copy unshifted tile data + + ld hl,&b480 + ld de,spr_data_0 + ld bc,&0720 + ldir ; copy unshifted sprite data + + +; Clear both screens and set the default attribute colour + ld b,2 ; 2 screens to prepare +scrinit_lp: push bc + + ld bc,&1800 + ld l,c + ld e,&01 + ld (hl),l ; clear display data + ld a,&40 + or ixh + ld h,a + ld d,a + ldir + + ld bc,&0300 + ld (hl),attr_colour ; set display attrs + ldir + + ld bc,&00e4 + ld (hl),l ; clear sprite restore data + ldir + + ld hl,&5800+(10*32)+4 ; left column of tunnel trim + ld a,h + or ixh + ld h,a + ld bc,23 ; right column to left on next row + ld de,32-24 ; left column to right column + ld a,5 ; 5 blocks per column +attr_lp: ld (hl),b ; hide left column (6 pixels needed) + add hl,bc + ld (hl),b ; hide right column 0 + inc l + ld (hl),b ; hide right column 1 (2 pixels needed) + add hl,de + dec a + jr nz,attr_lp + + call do_flip ; switch to other display + pop bc + djnz scrinit_lp ; finish both screens + + + call mk_lookups ; create all the look-up tables + + call page_rom + call sound_init ; enable sound chip + call patch_rom ; patch DIP fixes and hook us into the interrupt handling + + + ld hl,&5000 ; Pac-Man I/O area + xor a +clear_io: ld (hl),a ; zero it + inc l + jr nz,clear_io + + ld a,(dip_5000) ; set hardware dips to our defaults + ld (&5000),a + ld a,(dip_5040) + ld (&5040),a + ld a,(dip_5080) + ld (&5080),a + + xor a + out (border),a ; black border + + ld sp,&4c00 ; stack in spare RAM + jp 0 ; start the ROM! + + +page_rom: push af + push bc + ld a,%00000001 ; special +2A/+3 paging, banks 0/1/2/3 + ld bc,&1ffd + out (c),a + pop bc + pop af + ret + +page_screen: push af + push bc + xor a ; normal paging, banks R/5/2/7 + ld bc,&1ffd + out (c),a + pop bc + pop af + ret + + +patch_rom: ld a,&56 ; ED *56* + ld (&233c),a ; change IM 2 to IM 1 + + ld hl,&47ed + ld (&233f),hl ; change OUT (&00),A to LD I,A + ld (&3183),hl + + ld a,&c3 ; JP nn + ld (&0038),a + ld hl,do_int_hook ; interrupt hook + ld (&0039),hl + + ld hl,&04d6 ; SUB 4 + ld (&3181),hl ; restore original instruction in patched bootleg ROMs + + ld a,&01 ; to change &5000 writes to &5001, which is unused + ld (&0093),a + ld (&01d7),a + ld (&2347),a + ld (&238a),a + ld (&3194),a + ld (&3248),a + + ld a,1 ; start clearing at &5001, to avoid DIP overwrite + ld (&2353),a + ld (&230c),a + ld (&2353),a + ld a,7 ; shorten block clear after start adjustment above + ld (&230f),a + ld (&2357),a + + ld a,&41 ; start clearing at &5041, to avoid DIP overwrite + ld (&2363),a + ld a,&3f ; shorten block clear after start adjustment above + ld (&2366),a + + ld a,&b0 ; LSB of address in look-up table + ld (&3ffa),a ; skip memory test (actual code starts at &3000) + + ld hl,&e0f6 ; change AND &1F to OR &E0 so ROM peeks are from unmodified copy of the ROM + ld (&2a2d),hl ; (used as random number source for blue ghost movements) + + ld a,&cd ; CALL nn + ld (&2c62),a + ld hl,text_fix ; fix bit 7 being set in horizontal text screen writes + ld (&2c63),hl + + ld a,&dc ; CALL C,nn + ld (&0353),a ; disable 1UP/2UP flashing to save cycles + ld (&035e),a + + ret + +; The ROM uses bit 7 of the display address to indicate text to the top/bottom lines, which +; is rotated compared to the main screen. It doesn't clear the bit before using the address, +; but due to RAM mirroring the arcade version still works. Our version requires a fix. +text_fix: ld e,(hl) + inc hl + ld d,(hl) + res 7,d + ret + + +; Do everything we need to update video/sound/input +; +do_int_hook: ld (old_stack+1),sp + ld sp,new_stack + + push af + push bc + push de + push hl + ex af,af' + push af + exx + push bc + push de + push hl + push ix + + call do_flip ; show last frame, page in new one + call flash_maze ; flash the end of level maze + + ld hl,&5062 ; sprite 1 x + inc (hl) ; offset 1 pixel left (mirrored) + ld hl,&5064 ; sprite 2 x + inc (hl) + +set_border 1 + call do_restore ; restore under the old sprites +set_border 2 + call do_tiles ; update a portion of the background tiles +set_border 3 + call flash_pills ; flash the power pills +set_border 4 + call do_save ; save under the new sprite positions +set_border 5 + call do_sprites ; draw the 6 new masked sprites +set_border 6 + call do_trim ; trim sprites at screen edge +set_border 7 + call do_input ; scan the joystick and DIP switches + call do_sound ; convert the sound to the AY chip +set_border 0 + + ld hl,&5062 ; sprite 1 x + dec (hl) ; reverse change from above + ld hl,&5064 ; sprite 2 x + dec (hl) + + call set_int_chain ; prepare interrupt handler chain + + pop ix + pop hl + pop de + pop bc + exx + pop af + ex af,af' + pop hl + pop de + pop bc + pop af + +old_stack: ld sp,0 ; self-modified by code above +int_chain: jp 0 ; address completed by set_int_chain + + +; Prepare the Pac-Man interrupt handler address for our return - does an IM2-style +; lookup to determine the address for normal Pac-Man interrupt processing +; +set_int_chain: ld a,i ; bus value originally written to port &00 + ld l,a + ld h,&3f ; normal I value + ld a,(hl) ; handler low + inc hl + ld h,(hl) ; handler high + ld l,a + ld (int_chain+1),hl ; write into JP in interrupt handler + ret + + +; Flip to show the screen prepared during the last frame, and prepare to draw the next +; +do_flip: push af + push bc + + ld a,(scr_page) ; current screen + xor %00001000 ; toggle active screen bit + ld (scr_page),a + ld bc,&7ffd + out (c),a ; activate + + sub %00001000 ; set carry if we're viewing the normal screen + ld a,0 + rra + ld ixh,a ; b7 holds b15 of drawing screen address + + pop bc + pop af + ret + +scr_page: defb %00000111 ; normal screen (page 5), page 7 at &c000 + + +; Set the maze palette colour by detecting the attribute used for the maze white +; We also need to remove the ghost box door, as the real attribute wipe does. +; +flash_maze: ld a,(&4440) ; attribute of maze top-right + cp &1f + ld a,attr_colour ; default = white + jr nz,maze_blue + + ld a,&40 ; blank tile + ld (&420d),a ; clear left of ghost box door + ld (&41ed),a ; clear right of ghost box door + + ld a,flash_colour ; default = bright blue +maze_blue: + ; fall through... + +; Set attributes to value in A +set_attrs: call page_screen + + ex af,af' + ld a,2 ; 2 screens +attr_scr_lp: ld hl,&5800+5 + ld a,h + or ixh + ld h,a + + ex af,af' + cp (hl) ; check current colour + jr z,attr_same ; skip fill if it's the correct colour + + ld de,32-22 + ld b,&18 ; 24 lines to fill +attr_fill_lp: ld c,22 +attr_fill_lp2: ld (hl),a + inc l + dec c + jr nz,attr_fill_lp2 + add hl,de + djnz attr_fill_lp + + call do_flip ; switch to other screen + + ex af,af' + dec a + jr nz,attr_scr_lp + +attr_same: jp page_rom + + +; Set the power pill palette colour to the correct state by reading the 6 known +; pill locations, and reacting to changes in the attribute setting. +; +flash_pills: ld a,($4278) ; bottom attract screen pill location + cp &14 ; power pill? + jr z,attract_mode ; if so, we're in attract mode + + ld a,(&4384) ; top-left + cp &14 + ld a,(&4784) + ld hl,&4066 ; x=2, y=4 + ld bc,&878f ; pre-ORed left byte data + ld de,&80c0 ; original right byte data + call z,pill_2 + + ld a,(&4064) ; top right + cp &14 + ld a,(&4464) + ld hl,&4079 ; x=27, y=4 + call z,pill_1 + + ld a,(&4398) ; bottom-left + cp &14 + ld a,(&4798) + ld hl,&5046 ; x=2, y=24 + ld bc,&878f + ld de,&80c0 + call z,pill_2 + + ld a,(&4078) ; bottom-right + cp &14 + ld a,(&4478) + ld hl,&5059 ; x=27, y=24 + call z,pill_1 + + ret + +attract_mode: ld a,($4678) ; bottom attract + ld hl,&504d ; x=11, y=24 + call pill_1 + + ld a,(&4332) ; top attract + cp &14 + ld a,(&4732) + ld bc,&0103 + ld de,&e0f0 + ld hl,&4ca8 ; x=5, y=18 + call z,pill_2 + + ret + +; Pill tile in 1 byte +pill_1: cp &9f + jr z,pill_1_on + cp &10 + jr nz,pill_clear_1 +pill_1_on: + xor a ; normal paging, banks R/5/2/7 + ld bc,&1ffd + out (c),a + + ld a,h + or ixh + ld h,a + + ld de,&1e3f ; pill data for ends and middle + + ld (hl),d + inc h + ld (hl),e + inc h + ld (hl),e + inc h + ld (hl),e + inc h + ld (hl),e + inc h + ld (hl),d + + ld a,1 + ld bc,&1ffd + out (c),a ; special paging (ROM) + ret +; clear pill +pill_clear_1: xor a ; normal paging, banks R/5/2/7 + ld bc,&1ffd + out (c),a + + ld a,h + or ixh + ld h,a + + xor a + ld (hl),a + inc h + ld (hl),a + inc h + ld (hl),a + inc h + ld (hl),a + inc h + ld (hl),a + inc h + ld (hl),a + + ld a,1 + ld bc,&1ffd + out (c),a ; special paging (ROM) + ret + + +; Pill tile spanning 2 bytes +pill_2: cp &10 + jr nz,pill_clear_2 + + call page_screen + + ld a,h + or ixh + ld h,a + + ld (hl),b + inc l + ld a,(hl) + or d + ld (hl),a + dec l + inc h + + ld (hl),c + inc l + ld a,(hl) + or e + ld (hl),a + dec l + inc h + + ld (hl),c + inc l + ld a,(hl) + or e + ld (hl),a + dec l + inc h + + ld (hl),c + inc l + ld a,(hl) + or e + ld (hl),a + dec l + inc h + + ld a,h + and %00000111 + call z,blockdown_hl + + ld (hl),c + inc l + ld a,(hl) + or e + ld (hl),a + dec l + inc h + + ld (hl),b + inc l + ld a,(hl) + or d + ld (hl),a + dec l + inc h + + ld a,1 + ld bc,&1ffd + out (c),a ; special paging (ROM) + ret + +; clear pill +pill_clear_2: call page_screen + + ld a,h + or ixh + ld h,a + + ld de,&c00f ; mask + + ld a,(hl) + and d + ld (hl),a + inc l + ld a,(hl) + and e + ld (hl),a + dec l + inc h + + ld a,(hl) + and d + ld (hl),a + inc l + ld a,(hl) + and e + ld (hl),a + dec l + inc h + + ld a,(hl) + and d + ld (hl),a + inc l + ld a,(hl) + and e + ld (hl),a + dec l + inc h + + ld a,(hl) + and d + ld (hl),a + inc l + ld a,(hl) + and e + ld (hl),a + dec l + inc h + + ld a,h + and %00000111 + call z,blockdown_hl + + ld a,(hl) + and d + ld (hl),a + inc l + ld a,(hl) + and e + ld (hl),a + dec l + inc h + + ld a,(hl) + and d + ld (hl),a + inc l + ld a,(hl) + and e + ld (hl),a + + ld a,1 + ld bc,&1ffd + out (c),a ; special paging (ROM) + ret + + +; Scan the input DIP switches for joystick movement and button presses +; +do_input: ld de,&ffff ; nothing pressed + + ld a,&f7 + in a,(keyboard) + cpl + and %00000111 + jr z,not_123 + rra + jr nc,not_1 + res 5,e ; 1 = start 1 +not_1: rra + jr nc,not_2 + res 6,e ; 2 = start 2 +not_2: rra + jr nc,not_123 + res 5,d ; 3 = coin 1 +not_123: + + ld a,&fe + in a,(keyboard) + rra + jr c,no_shift + + ld a,&fb + in a,(keyboard) + bit 4,a + jr nz,not_shift_t + res 4,d ; shift-t = rack test +not_shift_t: + +; Shifted for Cursor keys + ld a,&f7 + in a,(keyboard) + bit 4,a + jr nz,not_shift_5 + res 1,d ; Shift-5 = left +not_shift_5: + ld a,&ef + in a,(keyboard) + cpl + and %00011100 + jr z,read_joy + rra + rra + rra + jr nc,not_shift_8 + res 2,d ; Shift-8 = right +not_shift_8: rra + jr nc,not_shift_7 + res 0,d ; Shift-7 = up +not_shift_7: rra + jr nc,read_qaop + res 3,d ; Shift-6 = down + jr read_qaop + +; Unshifted for Sinclair joystick +no_shift: ld a,&ef + in a,(keyboard) + cpl + and %00011111 + jr z,not_67890 + rra + jr nc,not_0 + res 5,d ; 0 = coin 1 + res 5,e ; 0 = start 1 +not_0: rra + jr nc,not_9 + res 0,d ; 9 = up +not_9: rra + jr nc,not_8 + res 3,d ; 8 = down +not_8: rra + jr nc,not_7 + res 2,d ; 7 = right +not_7: rra + jr nc,not_67890 + res 1,d ; 6 = left +not_67890: + +read_qaop: ld a,&fb + in a,(keyboard) + rra + jr c,not_q + res 0,d ; Q = up +not_q: + ld a,&fd + in a,(keyboard) + rra + jr c,not_a + res 3,d ; A = down +not_a: + ld a,&df + in a,(keyboard) + rra + jr c,not_p + res 2,d ; P = right +not_p: rra + jr c,not_o + res 1,d ; O = left +not_o: + +; Kempston joystick +read_joy: in a,(kempston) ; read Kempston joystick + inc a + cp 2 + jr c,not_fire ; ignore blank or invalid inputs + dec a + rra + jr nc,not_right + res 2,d ; right +not_right: rra + jr nc,not_left + res 1,d ; left +not_left: rra + jr nc,not_down + res 3,d ; down +not_down: rra + jr nc,not_up + res 0,d ; up +not_up: rra + jr nc,not_fire + res 5,d ; Fire = coin 1 + res 5,e ; Fire = start 1 +not_fire: + ld a,d + ld (&5000),a + ld a,e + ld (&5040),a + ret + + +; Check sprite visibility, returns carry if any visible, no-carry if all hidden +is_visible: ld a,&10 ; minimum x/y position to be visible + ld b,7 ; 7 sprites to check + ld hl,&5062 +vis_lp: cp (hl) + ret c + inc l + cp (hl) + ret c + inc l + inc l + inc l + djnz vis_lp + ret + + +; Draw the background tile changes, in 1-5 steps over the 2 double-buffered screens +do_tiles: call is_visible ; set carry state for below + +tile_state: ld a,ixh + bit 7,a ; alt screen? (don't disturb carry!) + + jr c,tile_strips ; if any sprites are visible we'll draw in strips + jr nz,fulldraw_alt ; full screen draw (alt) + +fulldraw_norm: ld b,28 + ld de,pac_chars + ld hl,bak_chars1-pac_footer + add hl,de + call tile_comp + ld hl,bak_chars1 + call do_fruit + ld hl,bak_chars1 + call do_lives + ld hl,bak_chars1 + call do_score1 + ld hl,bak_chars1 + jp do_score2 + +fulldraw_alt: ld b,28 + ld de,pac_chars + ld hl,bak_chars2-pac_footer + add hl,de + call tile_comp + ld hl,bak_chars2 + call do_fruit + ld hl,bak_chars2 + call do_lives + ld hl,bak_chars2 + call do_score1 + ld hl,bak_chars2 + jp do_score2 + + +tile_strips: jp nz,strip_odd +strip_even: jp strip_0 + +strip_0: ld b,6 + ld de,pac_chars + ld hl,bak_chars1-pac_footer + add hl,de + call tile_comp + ld hl,strip_1 + ld (strip_even+1),hl + ret + +strip_1: ld b,5 + ld de,pac_chars+(32*(6)) + ld hl,bak_chars1-pac_footer + call tile_comp + ld hl,bak_chars1 + call do_score1 + ld hl,bak_chars1 + call do_score2 + ld hl,strip_2 + ld (strip_even+1),hl + ret + +strip_2: ld b,6 + ld de,pac_chars+(32*(6+5)) + ld hl,bak_chars1-pac_footer + add hl,de + call tile_comp + ld hl,strip_3 + ld (strip_even+1),hl + ret + +strip_3: ld b,5 + ld de,pac_chars+(32*(6+5+6)) + ld hl,bak_chars1-pac_footer + add hl,de + call tile_comp + ld hl,bak_chars1 + call do_fruit + ld hl,bak_chars1 + call do_lives + ld hl,strip_4 + ld (strip_even+1),hl + ret + +strip_4: ld b,6 + ld de,pac_chars+(32*(6+5+6+5)) + ld hl,bak_chars1-pac_footer + add hl,de + call tile_comp + ld hl,strip_0 + ld (strip_even+1),hl + ret + +strip_odd: jp strip_0_alt + +strip_0_alt: ld b,6 + ld de,pac_chars + ld hl,bak_chars2-pac_footer + add hl,de + call tile_comp + ld hl,strip_1_alt + ld (strip_odd+1),hl + ret + +strip_1_alt: ld b,5 + ld de,pac_chars+(32*(6)) + ld hl,bak_chars2-pac_footer + add hl,de + call tile_comp + ld hl,bak_chars2 + call do_score1 + ld hl,bak_chars2 + call do_score2 + ld hl,strip_2_alt + ld (strip_odd+1),hl + ret + +strip_2_alt: ld b,6 + ld de,pac_chars+(32*(6+5)) + ld hl,bak_chars2-pac_footer + add hl,de + call tile_comp + ld hl,strip_3_alt + ld (strip_odd+1),hl + ret + +strip_3_alt: ld b,5 + ld de,pac_chars+(32*(6+5+6)) + ld hl,bak_chars2-pac_footer + add hl,de + call tile_comp + ld hl,bak_chars2 + call do_fruit + ld hl,bak_chars2 + call do_lives + ld hl,strip_4_alt + ld (strip_odd+1),hl + ret + +strip_4_alt: ld b,6 + ld de,pac_chars+(32*(6+5+6+5)) + ld hl,bak_chars2-pac_footer + add hl,de + call tile_comp + ld hl,strip_0_alt + ld (strip_odd+1),hl + ret + + +tile_comp: call find_change ; scan block for display changes + dec sp ; restore the same return address to here + dec sp + + ld (hl),a ; update with new tile value + + cp 144 ; before fruit tiles? + jr c,tile_mapped + sub 63 ; relocate to account for removed fruits + cp 176-63 ; first ghost tile + jr c,tile_mapped + cp 176-63+6 ; after last ghost tile? + jr nc,tile_mapped + + ex af,af' ; save tile + set 2,d ; switch to attributes + ld a,(de) ; fetch tile attribute + res 2,d ; switch back to data + ld c,a + ex af,af' ; restore tile + dec c ; red ghost? + jr nz,tile_mapped + add a,6 ; offset to Blinky tiles +tile_mapped: + ex af,af' ; save tile for later + push de + exx ; save to resume find + pop hl ; Pac-Man screen address of changed tile + + ld a,l + and %00011111 ; column is in bits 0-4 + ld b,a ; tile y + + add a,a ; *2 + add a,a ; *4 + add a,b ; *5 (code size to check each byte) + add a,3 ; skip ld+cp+ret, so we advance pointers + ld e,a + ld d,find_change/256 + push de ; return address to resume find + + add hl,hl ; *2 + add hl,hl ; *4 + add hl,hl ; *8 (ignore overflow), H is now mirrored column number + ld a,28+2 ; 28 columns wide, offset 2 by additional rows + sub h ; unmirror the column + ld c,a ; tile x + +draw_tile: ld ixl,5 ; offset to centre maze on Speccy display +draw_tile_x: ld a,b + add a,a ; *2 + ld b,a + add a,a ; *4 + add a,b ; *6 + ld l,a + ld h,scradtab/256 + ld e,(hl) + inc h + ld a,(hl) + or ixh + ld d,a ; DE holds base addr for screen line + + ld b,conv_8_6/256 + ld a,(bc) ; 4 tiles to 3 byte conversion for tile x + add a,e ; add screen LSB + add a,ixl ; centre maze on Speccy display + ld e,a ; DE holds addr for tile + + ld a,c + ex af,af' ; save tile x, restore tile number + + ld l,a + ld h,0 + add hl,hl ; *2 + ld b,h + ld c,l + add hl,hl ; *4 + add hl,bc ; *6 + + xor a ; normal paging, banks R/5/2/7 + ld bc,&1ffd + out (c),a + + ex af,af' + rra + jr c,tile_62 + rra + jr c,tile_4 + +; 11111100 +tile_0: ld bc,tile_data_0 + add hl,bc + ld bc,&0503 ; 5 lines, mask of 00000011 +tile_0_lp: ld a,(de) + and c + or (hl) + ld (de),a + inc hl + inc d + ld a,d + and %00000111 + call z,blockdown_de + djnz tile_0_lp + ld a,(de) + and c + or (hl) + ld (de),a + jr tile_exit + +; 00001111 11000000 +tile_4: ld bc,tile_data_4 + add hl,hl + add hl,bc + ld bc,&05f0 ; 5 lines, mask of 11110000 +tile_4_lp: ld a,(de) + and c + or (hl) + ld (de),a + inc e + inc hl + ld a,(de) + and %00111111 + or (hl) + ld (de),a + dec e + inc hl + inc d + ld a,d + and %00000111 + call z,blockdown_de + djnz tile_4_lp + ld a,(de) + and c + or (hl) + ld (de),a + inc e + inc hl + ld a,(de) + and %00111111 + or (hl) + ld (de),a + jr tile_exit + +tile_62: rra + jr c,tile_2 + +; 00000011 11110000 +tile_6: ld bc,tile_data_6 + add hl,hl + add hl,bc + ld bc,&05fc ; 5 lines, mask of 11111100 +tile_6_lp: ld a,(de) + and c + or (hl) + ld (de),a + inc e + inc hl + ld a,(de) + and %00001111 + or (hl) + ld (de),a + dec e + inc hl + inc d + ld a,d + and %00000111 + call z,blockdown_de + djnz tile_6_lp + ld a,(de) + and c + or (hl) + ld (de),a + inc e + inc hl + ld a,(de) + and %00001111 + or (hl) + ld (de),a + jr tile_exit + +; 00111111 +tile_2: ld bc,tile_data_2 + add hl,bc + ld bc,&05c0 ; 5 lines, mask of 11000000 +tile_2_lp: ld a,(de) + and c + or (hl) + ld (de),a + inc hl + inc d + ld a,d + and %00000111 + call z,blockdown_de + djnz tile_2_lp + ld a,(de) + and c + or (hl) + ld (de),a + jr tile_exit + +tile_exit: + ld a,1 ; special paging, banks 0/1/2/3 + ld bc,&1ffd + out (c),a + + exx + ret + + +; Draw a 12x12 sprite (H=x, L=y, D=attr) +; +draw_spr: ld a,h + cp &10 ; off bottom of screen? + ret c + ld a,l + cp &10 ; off right of screen? + ret c + + ld a,d + and a ; sprite palette all black? + ret z + + call page_screen + call xy_to_addr + ld a,c + and %00000111 ; shift position + + ex af,af' + call map_spr ; map sprites to the correct orientation/colour + +draw_spr2: + ex de,hl + add a,a ; *2 + ld l,a + ld h,0 + add hl,hl ; *4 + ld b,h + ld c,l + add hl,hl ; *8 + add hl,bc ; *12 + + ex af,af' + rra + jr c,rot_odd +rot_even: rra + jr c,rot_2_6 +rot_0_4: rra + ld bc,spr_data_4 ; rot_4 + jr c,spr_2 + ld bc,spr_data_0 ; rot_0 + jp spr_2 + +rot_2_6: rra + ld bc,spr_data_6 ; rot_6 + jr c,spr_3 + ld bc,spr_data_2 ; rot_2 + jp spr_2 + +rot_odd: rra + jr c,rot_3_7 +rot_1_5: rra + ld bc,spr_data_5 ; rot_5 + jr c,spr_3 + ld bc,spr_data_1 ; rot_1 + jp spr_2 + +rot_3_7: rra + ld bc,spr_data_7 ; rot_7 + jr c,spr_3 + ld bc,spr_data_3 ; rot_3 + jp spr_2 + +; draw a sprite using 2-byte source data (shifts 0-4) +spr_2: add hl,hl + add hl,bc + ld b,12 +rot_0_lp: ld a,(de) + or (hl) + ld (de),a + inc e + inc hl + ld a,(de) + or (hl) + ld (de),a + dec e + inc hl + inc d + ld a,d + and %00000111 + call z,blockdown_de + djnz rot_0_lp + jp page_rom + +; draw a sprite using 3-byte source data (shifts 5-7) +spr_3: push bc + ld b,h + ld c,l + add hl,hl ; *24 + add hl,bc ; *36 + pop bc + add hl,bc + ld b,12 +spr_3_lp: ld a,(de) + or (hl) + ld (de),a + inc e + inc hl + ld a,(de) + or (hl) + ld (de),a + inc e + inc hl + ld a,(de) + or (hl) + ld (de),a + dec e + dec e + inc hl + inc d + ld a,d + and %00000111 + call z,blockdown_de + djnz spr_3_lp + jp page_rom + + +; Map an arcade tile number to our tile number, allowing for attribute differences +; and any tiles we've mapped to different locations (we have no fruit tiles) +map_spr: ld b,0 + ld a,e + srl a + rl b ; b0=flip-y + rra + rl b ; b1=flip-y, b0=flip-x + cp 16 ; big pac-man + ret c ; anything before is unchanged + cp 28 ; scared ghost + jr c,map_big + cp 32 ; first ghost + jr c,map_scared + cp 40 ; last ghost + 1 + jr c,map_ghost + cp 44 ; first Pac-Man + ret c + cp 48 ; last Pac-Man + 1 + ret nc + inc b + dec b ; mirrored? + ret z ; no, so right/down + add a,28 ; offset to left/up + ret + +map_big: cp 24 ; closed mouth + ret nc + add a,48 + bit 0,a ; mouth segment? + ret nz + and %00000010 ; up/down back + or %00011000 ; re-use back segments + ret + +map_ghost: ;jr $ ;01=red 03=pink 05=cyan 07=orange + sub 16 + dec d ; red? + ret z + add a,16 + bit 3,d ; transparent colour? + ret z ; return if not + add a,32 ; eyes offset + and %11111110 ; use only even positions + ret + +map_scared: bit 1,d ; check colour + ret z ; return if normal colour + add a,2 ; white flashing offset + ret + + +; Trim sprites that overlap the maze edges, as the real hardware does automatically +; Here we trim partial bytes, as full blocks are hidden behind black on black attrs +; +do_trim: ld de,&5062 ; start of sprite data + ld b,&07 ; 7 sprites to check + +trim_lp: ld a,(de) ; sprite x + inc e + cp &10 ; hidden sprite? + jr c,no_trim + cp &20 ; clipped at right edge? + jr c,may_trim + cp &f0 ; clipped at left edge? + jr nc,may_trim +no_trim: inc e + djnz trim_lp + ret + +may_trim: ld a,(de) ; sprite y + cp &10 ; hidden sprite? + jr c,no_trim + + ld e,12 ; height of sprite + ld hl,&4f45 ; left of tunnel + cp &8c ; sprite y for tunnel? + jr z,trim_edge + ld e,24 ; height of 2 sprites + ld hl,&4d65 ; left of intermission row +trim_edge: + ld a,h + or ixh ; offset to current screen + ld h,a + + xor a + ld bc,&1ffd + out (c),a ; normal paging (screen) + + ld b,e ; line count + ld de,&03fc ; masks for below + +trim_line_lp: ld a,(hl) + and d ; clear 6 pixels + ld (hl),a + ld c,l + ld a,l + add a,&15 ; advance to right edge + ld l,a + ld a,(hl) + and e ; clear 2 pixels + ld (hl),a + ld l,c ; restore left position + inc h ; next line + ld a,h + and %00000111 + call z,blockdown_hl + djnz trim_line_lp + + ld a,1 + ld bc,&1ffd + out (c),a ; special paging (ROM) + + ret + + +; Clear a sprite-sized hole, used for blank tiles in our fruit and lives display +; +blank_sprite: call page_screen + + ld bc,&0c00 ; 12 lines, zero fill +blank_lp: ld (hl),c + inc l + ld (hl),c + dec l + inc h + ld a,h + and %00000111 + call z,blockdown_hl + djnz blank_lp + + jp page_rom + + +; Save the background screen behind locations we're about to draw active sprites +; +do_save: ld hl,(&5062) ; pre-fetch position data as we page it out + push hl + ld hl,(&5064) + push hl + ld hl,(&5066) + push hl + ld hl,(&5068) + push hl + ld hl,(&506a) + push hl + ld hl,(&506c) + + call page_screen + + ld de,spr_save_7 + call spr_save + + pop hl + ld de,spr_save_6 + call spr_save + + pop hl + ld de,spr_save_5 + call spr_save + + pop hl + ld de,spr_save_4 + call spr_save + + pop hl + ld de,spr_save_3 + call spr_save + + pop hl + ld de,spr_save_2 + call spr_save + + call page_rom + ret + +; Save a single sprite-sized block, if visible +spr_save: ld a,h + cp 16 + ret c ; off bottom of screen + ld a,l + cp 16 + ret c ; off right of screen + + ld a,d + or ixh + ld d,a + + call xy_to_addr ; convert to Speccy display address + + ex de,hl + ld (hl),e ; save address low + inc l + ld (hl),d ; save address high + inc l + ex de,hl + + ld bc,3*12 ; 3 bytes and 12 lines + +save_lp: ld a,l + ldi + ldi + ldi + ret po ; return if done + ld l,a + inc h + ld a,h + and %00000111 + jp nz,save_lp + call blockdown_hl + jp save_lp + +; +; Remove the previous sprites by restoring the image that was underneath them +; +do_restore: + call page_screen + + ld hl,spr_save_2 + call spr_restore + ld hl,spr_save_3 + call spr_restore + ld hl,spr_save_4 + call spr_restore + ld hl,spr_save_5 + call spr_restore + ld hl,spr_save_6 + call spr_restore + ld hl,spr_save_7 + call spr_restore + + jp page_rom + + +; Restore a single sprite-sized block, if data was saved +spr_restore: ld a,h + or ixh + ld h,a + ld a,(hl) + and a + ret z ; no data saved + + ld (hl),0 ; flag 'no restore data' + + ld e,a ; restore address low + inc l + ld d,(hl) ; restore address high + inc l + + ld bc,3*12 ; 3 bytes of 12 lines + +restore_lp: ld a,e + ldi + ldi + ldi + ret po + ld e,a + inc d + ld a,d + and %00000111 + jp nz,restore_lp + call blockdown_de + jp restore_lp + + +; Draw the currently visible sprites, in the correct order for overlaps +; Note: sprite order changes depending on mode, so not always as listed! +; +do_sprites: + ld hl,(&506c) + ld de,(&4ffc) + call draw_spr ; fruit + + ld hl,(&506a) + ld de,(&4ffa) + call draw_spr ; pacman + + ld hl,(&5068) + ld de,(&4ff8) + call draw_spr ; orange ghost + + ld hl,(&5066) + ld de,(&4ff6) + call draw_spr ; cyan ghost + + ld hl,(&5064) + ld de,(&4ff4) + call draw_spr ; pink ghost + + ld hl,(&5062) + ld de,(&4ff2) + call draw_spr ; red ghost + + ret + + +do_score1: inc h + inc h + inc h ; advance to header area containing score + + ld ixl,0 ; screen offset for left edge + + ld l,&da + ld bc,&0003 + call chk_digit ; 1 + + ld l,&d9 + ld bc,&0004 + call chk_digit ; U + + ld l,&d8 + ld bc,&0005 + call chk_digit ; P + + + ld l,&fc + ld bc,&0100 + call chk_digit ; 100,000s + + ld l,&fb + ld bc,&0101 + call chk_digit ; 10,000s + + ld l,&fa + ld bc,&0102 + call chk_digit ; 1,000s + + ld l,&f9 + ld bc,&0103 + call chk_digit ; 100s + + ld l,&f8 + ld bc,&0104 + call chk_digit ; 10s + + ld l,&f7 + ld bc,&0105 + call chk_digit ; 1s + + ret + +; +do_score2: inc h + inc h + inc h ; advance to header area containing score + + ld ixl,26 ; screen offset for left edge + + ld l,&c7 + ld bc,&0003 + call chk_digit ; 2 + + ld l,&c6 + ld bc,&0004 + call chk_digit ; U + + ld l,&c5 + ld bc,&0005 + call chk_digit ; P + + + ld l,&e9 + ld bc,&0100 + call chk_digit ; 100,000s + + ld l,&e8 + ld bc,&0101 + call chk_digit ; 10,000s + + ld l,&e7 + ld bc,&0102 + call chk_digit ; 1,000s + + ld l,&e6 + ld bc,&0103 + call chk_digit ; 100s + + ld l,&e5 + ld bc,&0104 + call chk_digit ; 10s + + ld l,&e4 + ld bc,&0105 + call chk_digit ; 1s + + ret + +chk_digit: ld d,&43 + ld e,l + ld a,(de) + cp (hl) + ret z + ld (hl),a + + ex af,af' + push hl + call draw_tile_x + pop hl + ret + +; +; Draw changes to the fruit display, which is remapped to a vertical layout +; We use the sprite versions of the tiles, for easier drawing +; +do_fruit: ld l,5 ; offset to first fruit in display and comparison buffer + ld c,&b4 + push hl + call chk_fruit + pop hl + + ld l,7 + ld c,&a8 + push hl + call chk_fruit + pop hl + + ld l,9 + ld c,&9c + push hl + call chk_fruit + pop hl + + ld l,11 + ld c,&90 + push hl + call chk_fruit + pop hl + + ld l,13 + ld c,&84 + push hl + call chk_fruit + pop hl + + ld l,15 + ld c,&78 + push hl + call chk_fruit + pop hl + + ld l,17 + ld c,&6c + push hl + call chk_fruit + pop hl + + ret + +chk_fruit: ld b,scradtab/256 + ld a,(bc) + add a,&1d ; Speccy line offset for fruit column + ld e,a + inc b + ld a,(bc) + or ixh + ld d,a + + ld b,pac_footer/256 ; tile MSB for fruit + ld c,l + ld a,(bc) ; live fruit + cp (hl) ; has it changed? + ret z ; return if not + + ld (hl),a ; update buffered copy + ex de,hl + ex af,af' + + push hl + call blank_sprite + pop hl + + xor a ; no rotation, for sprite drawing later + ex af,af' + cp &40 ; blank? + ret z + + sub &91 ; subtract cherry tile number + srl a ; /2 + srl a ; /4 tiles per fruit, to give fruit sprite number (cherry=0) + + call page_screen + jp draw_spr2 + +; +; Draw changes to the number of remaining lives +; +do_lives: ld l,&1b + ld c,&b4 + push hl + call chk_life + pop hl + + ld l,&19 + ld c,&a8 + push hl + call chk_life + pop hl + + ld l,&17 + ld c,&9c + push hl + call chk_life + pop hl + + ld l,&15 + ld c,&90 + push hl + call chk_life + pop hl + + ld l,&13 + ld c,&84 + push hl + call chk_life + pop hl + + ret + +; Draw either a blank or a left-facing Pac-Man sprite +chk_life: ld b,scradtab/256 + ld a,(bc) + add a,&02 ; Speccy line offset for lives column + ld e,a + inc b + ld a,(bc) + or ixh + ld d,a + + ld b,&40 + ld c,l + ld a,(bc) + cp (hl) + ret z + + ld (hl),a + ex de,hl + ex af,af' + + push hl + call blank_sprite + pop hl + + xor a ; no rotation, for sprite drawing later + ex af,af' + cp &40 ; blank? + ret z + + call page_screen + ld a,72 ; sprite for left-facing Pac-Man + jp draw_spr2 + + +; Build the sound table and initialise the AY-3-8912 chip +; +sound_init: ld bc,sound_table + +sound_lp: ld a,b ; map entry address to freq + and &3f + rra + ld d,a + ld a,c + rra + ld e,a ; freq (divisor) now in DE + + ld hl,0 + exx + ld de,&da7a ; dividend in DEHL + ld hl,&8000 ; 111861 << 15 = 0xda7a8000 + ld b,16 + and a +div_lp: adc hl,hl ; shift up for next division + rl e + rl d + exx + adc hl,hl ; include new bit + sbc hl,de ; does it divide? + jr nc,div_ok + add hl,de ; add back if not, setting carry +div_ok: exx + ccf ; set carry if it divided + djnz div_lp + adc hl,hl ; include final bit + + ld a,h + ex af,af' + ld a,l + exx + ld (bc),a ; note LSB + inc c + ex af,af' + ld (bc),a ; note MSB + inc bc ; freq++ + ex af,af' + + xor c + and %00000111 + out (border),a ; flash the border to show we're busy + + bit 5,b + jr z,sound_lp + + ld hl,sinit_data + ld de,&ffbf + ld c,&fd +sinit_lp: ld a,(hl) + and a + ret m + ld b,d + out (c),a + inc hl + ld a,(hl) + inc hl + ld b,e + out (c),a + jr sinit_lp + +; Sound init: set volumes to zero, enable tones A+B+C, end +sinit_data: defb &08,0, &09,0, &0a,0, &07,%00111000, &ff + + +; Map the current sound chip frequencies to the AY +; +do_sound: ld hl,&5051 ; voice 0 freq and volume + ld a,(&5045) ; voice 0 waveform + call map_sound + xor a + call play_sound + + ld hl,&5051+5 ; voice 1 freq and volume + ld a,(&504a) ; voice 1 waveform + call map_sound + ld a,1 + call play_sound + + ld hl,&5051+5+5 ; voice 2 freq and volume + ld a,(&504f) ; voice 2 waveform + call map_sound + ld a,2 + call play_sound + + ret + +map_sound: ld b,a ; save waveform + + ld a,(hl) + and %00001111 + add a,a + add a,a + add a,a + add a,a + ld e,a + inc hl + ld a,(hl) + and %00001111 + ld d,a + inc hl + ld a,(hl) + add a,a + add a,a + add a,a + add a,a + or d + ld d,a + or e ; check for zero frequency + inc hl + inc hl + ld a,(hl) ; volume + ex de,hl + + jr nz,not_silent + xor a ; zero frequency gives silence +not_silent: ex af,af' ; save volume for caller + + ld a,b + cp 5 ; waveform used when eating ghost? + jr z,eat_sound ; if so, don't divide freq by 8 + srl h + rr l + srl h + rr l + srl h + rr l +eat_sound: + ld a,h + or &c0 ; MSB of sound table + ld h,a + res 0,l + + ld a,(hl) ; pick up LSB + inc hl + ld h,(hl) ; pick up MSB + ld l,a + + ret + +; Update a single voice, setting the note number and volume +play_sound: ld de,&ffbf ; sound register port MSB + ld c,&fd ; LSB + + add a,a ; 2 registers per tone + ld b,d + out (c),a ; tone low + ld b,e + out (c),l + + inc a + ld b,d + out (c),a ; tone high + ld b,e + out (c),h + + rra + or %00001000 + ld b,d + out (c),a ; volume + ex af,af' + ld b,e + out (c),a ; volume data + + ret + + +; Create the look-up tables used to speed up various calculations +; +mk_lookups: ld hl,conv_8_6 + xor a +conv_86_lp: ld (hl),a ; 0 + inc l + ld (hl),a ; 0 + inc a + inc l + ld (hl),a ; 1 + inc a + inc l + ld (hl),a ; 2, etc. (repeating pattern) + inc a + inc l + jr nz,conv_86_lp + + ; note: HL re-used from above + ld de,conv_y + ld bc,conv_x +mirror_lp: xor a + sub c ; mirror y-axis + ld l,a + ld a,(hl) ; map to Speccy coords + ld (de),a + xor a + sub e ; mirror x-axis + ld l,a + ld a,(hl) ; map to Speccy coords + add a,34 ; centre on display + ld (bc),a + inc e + inc c + jr nz,mirror_lp + + + ld hl,tile_data_4 + ld de,tile_data_6 + exx + ld hl,tile_data_0 + ld de,tile_data_2 + ld c,192 ; 192 tiles +tilerot_lp: ld b,6 ; 6 lines per tile +tilerot_lp2: ld a,(hl) + inc hl + srl a + rra + ld (de),a ; >> 6 + inc de + exx + ld c,0 + rra + rr c + rra + rr c + ld (hl),a ; >> 4 + inc l + ld (hl),c + inc hl + ex de,hl + rra + rr c + rra + rr c + ld (hl),a ; >> 2 + inc l + ld (hl),c + inc hl + ex de,hl + exx + djnz tilerot_lp2 + dec c + jr nz,tilerot_lp + + + ld hl,spr_data_5 + ld de,76*3*12 + exx + ld hl,spr_data_0 + ld de,76*2*12 + + ld c,76 ; 76 sprites +spr_rot_lp: push bc + ld b,12 ; 12 lines per sprite +spr_rot_lp2: push bc + + ld c,(hl) ; take a line from spr_data_0 + inc hl + ld a,(hl) + dec hl + + push hl ; save + ld b,4 ; four more 2-byte shifted versions +spr_rot_lp3: add hl,de ; next shifted copy + srl c ; >> 1 + rra + ld (hl),c ; spr_data_1 to spr_data_4 + inc hl + ld (hl),a + dec hl + djnz spr_rot_lp3 + + pop hl ; restore spr_data_0 position + inc hl ; advance to next line + inc hl + + ex af,af' ; preserve A and carry from final rra above + ld a,c ; copy for exx + exx + ld c,a ; restore C + ex af,af' ; restore A and carry + ld b,0 ; extra shift register + rr b ; recover carry + ex af,af' + + ld a,3 ; three 3-byte shifted versions + push hl ; save +spr_rot_lp4: ex af,af' + + srl c ; >> 5 to 7 + rra + rr b + + ld (hl),c ; spr_data_5 to spr_data_7 + inc hl + ld (hl),a + inc hl + ld (hl),b + dec hl + dec hl + add hl,de ; next shifted copy + + ex af,af' + dec a + jr nz,spr_rot_lp4 + + pop hl ; restore spr_data_5 position + inc hl ; advance to next line + inc hl + inc hl + exx ; back to spr_data_0 + + pop bc + djnz spr_rot_lp2 ; complete lines + + pop bc + dec c + jr nz,spr_rot_lp ; complete sprites + + + ld hl,scradtab + ld de,&4000 ; Speccy screen base + ld b,&c0 ; 192 lines +scrtab_lp: ld (hl),e + inc h + ld (hl),d + dec h + inc l + inc d + ld a,d + and %00000111 + call z,blockdown_de + djnz scrtab_lp + + ret + +; Map a Pac-Man screen coordinate to a Speccy display address, scaling down from 8x8 to 6x6 as we go +; +xy_to_addr: ld b,conv_y/256 + ld c,h + ld a,(bc) ; look up y coord + ld h,conv_x/256 + ld c,(hl) ; look up x coord + + ld h,scradtab/256 + ld l,a + ld b,(hl) + inc h + ld a,(hl) + or ixh + ld h,a + ld l,b + ld a,c + and %11111000 + rra + rra + rra + add a,l + ld l,a + ret + +blockdown_hl: ld a,l + add a,32 + ld l,a + ret c + + ld a,h + sub 8 + ld h,a + ret + +blockdown_de: ld a,e + add a,32 + ld e,a + ret c + + ld a,d + sub 8 + ld d,a + ret + + defs (-$)%256 ; align to next 256-byte boundary +; +; Scan a 32-byte block for changes, used for fast scanning of the Pac-Man display +; Aligned on a 256-byte boundary for easy resuming of the scanning +find_change: ld a,(de) ; 0 + cp (hl) + ret nz + inc e + inc l + + ld a,(de) ; 1 + cp (hl) + ret nz + inc e + inc l + + ld a,(de) ; 2 + cp (hl) + ret nz + inc e + inc l + + ld a,(de) ; 3 + cp (hl) + ret nz + inc e + inc l + + ld a,(de) ; 4 + cp (hl) + ret nz + inc e + inc l + + ld a,(de) ; 5 + cp (hl) + ret nz + inc e + inc l + + ld a,(de) ; 6 + cp (hl) + ret nz + inc e + inc l + + ld a,(de) ; 7 + cp (hl) + ret nz + inc e + inc l + + ld a,(de) ; 8 + cp (hl) + ret nz + inc e + inc l + + ld a,(de) ; 9 + cp (hl) + ret nz + inc e + inc l + + ld a,(de) ; 10 + cp (hl) + ret nz + inc e + inc l + + ld a,(de) ; 11 + cp (hl) + ret nz + inc e + inc l + + ld a,(de) ; 12 + cp (hl) + ret nz + inc e + inc l + + ld a,(de) ; 13 + cp (hl) + ret nz + inc e + inc l + + ld a,(de) ; 14 + cp (hl) + ret nz + inc e + inc l + + ld a,(de) ; 15 + cp (hl) + ret nz + inc e + inc l + + ld a,(de) ; 16 + cp (hl) + ret nz + inc e + inc l + + ld a,(de) ; 17 + cp (hl) + ret nz + inc e + inc l + + ld a,(de) ; 18 + cp (hl) + ret nz + inc e + inc l + + ld a,(de) ; 19 + cp (hl) + ret nz + inc e + inc l + + ld a,(de) ; 20 + cp (hl) + ret nz + inc e + inc l + + ld a,(de) ; 21 + cp (hl) + ret nz + inc e + inc l + + ld a,(de) ; 22 + cp (hl) + ret nz + inc e + inc l + + ld a,(de) ; 23 + cp (hl) + ret nz + inc e + inc l + + ld a,(de) ; 24 + cp (hl) + ret nz + inc e + inc l + + ld a,(de) ; 25 + cp (hl) + ret nz + inc e + inc l + + ld a,(de) ; 26 + cp (hl) + ret nz + inc e + inc l + + ld a,(de) ; 27 + cp (hl) + ret nz + inc e + inc l + + ld a,(de) ; 28 + cp (hl) + ret nz + inc e + inc l + + ld a,(de) ; 29 + cp (hl) + ret nz + inc e + inc l + + ld a,(de) ; 30 + cp (hl) + ret nz + inc e + inc l + + ld a,(de) ; 31 + cp (hl) + ret nz + inc de ; 16-bit increment as we may be at 256-byte boundary + inc hl + + dec b + jp nz,find_change ; jump too big for DJNZ + + pop hl ; junk return to update + ret + +end_a000: equ $-1 + +new_stack: equ &b000 ; hangs back into &Axxx + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + org &b000 + +; Graphics here at load time + incbin "tiles.bin" ; 192 tiles * 6 lines * 1 byte per line = 1152 bytes + incbin "sprites.bin" ; 76 sprites * 12 lines * 2 byte per line = 1824 bytes + +; Tables here at run time +conv_8_6: equ &b000 +conv_x: equ conv_8_6 + &100 +conv_y: equ conv_x + &100 +scradtab: equ conv_y + &100 +bak_chars1: equ scradtab + &200 ; copy of Pac-Man display for normal screen +bak_chars2: equ bak_chars1 + &400 ; copy of Pac-Man display for alt screen + +end_b000: equ bak_chars2 + &400 + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + + org &c000 + + incbin "pacman.6e" + incbin "pacman.6f" + incbin "pacman.6h" + incbin "pacman.6j" + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +end start ; auto-run address diff --git a/pacman.sym b/pacman.sym new file mode 100644 index 0000000..41ff5cd --- /dev/null +++ b/pacman.sym @@ -0,0 +1,196 @@ +attr_colour EQU 00007H +attr_fill_lp EQU 0A21EH +attr_fill_lp2 EQU 0A220H +attr_lp EQU 0A0AEH +attr_same EQU 0A22FH +attr_scr_lp EQU 0A20EH +attract_mode EQU 0A27EH +bak_chars1 EQU 0B500H +bak_chars2 EQU 0B900H +blank_lp EQU 0A7CAH +blank_sprite EQU 0A7C4H +blockdown_de EQU 0AC12H +blockdown_hl EQU 0AC08H +border EQU 000FEH +chk_digit EQU 0A97AH +chk_fruit EQU 0A9C8H +chk_life EQU 0AA20H +clear_io EQU 0A0CDH +conv_86_lp EQU 0AB2BH +conv_8_6 EQU 0B000H +conv_x EQU 0B100H +conv_y EQU 0B200H +debug EQU 00000H +dip_5000 EQU 0A002H +dip_5040 EQU 0A003H +dip_5080 EQU 0A004H +div_lp EQU 0AA5EH +div_ok EQU 0AA6CH +do_flip EQU 0A1DBH +do_fruit EQU 0A988H +do_input EQU 0A371H +do_int_hook EQU 0A178H +do_lives EQU 0A9F2H +do_restore EQU 0A84FH +do_save EQU 0A7DAH +do_score1 EQU 0A8DCH +do_score2 EQU 0A92BH +do_sound EQU 0AAA4H +do_sprites EQU 0A89FH +do_tiles EQU 0A445H +do_trim EQU 0A76FH +draw_spr EQU 0A68EH +draw_spr2 EQU 0A6A6H +draw_tile EQU 0A5C7H +draw_tile_x EQU 0A5CAH +eat_sound EQU 0AAFEH +end_a000 EQU 0ADA5H +end_b000 EQU 0BD00H +end_tile_data EQU 0F6E4H +fail_msg EQU 0A02EH +find_change EQU 0AD00H +flash_colour EQU 00041H +flash_maze EQU 0A1F5H +flash_pills EQU 0A232H +fulldraw_alt EQU 0A474H +fulldraw_norm EQU 0A450H +int_chain EQU 0A1CBH +is_visible EQU 0A433H +kempston EQU 0001FH +keyboard EQU 000FEH +map_big EQU 0A74EH +map_ghost EQU 0A75BH +map_scared EQU 0A769H +map_sound EQU 0AACEH +map_spr EQU 0A729H +may_trim EQU 0A786H +maze_blue EQU 0A208H +mirror_lp EQU 0AB3EH +mk_lookups EQU 0AB27H +msg_lp EQU 0A027H +new_stack EQU 0B000H +no_shift EQU 0A3C3H +no_trim EQU 0A782H +not_0 EQU 0A3D3H +not_1 EQU 0A382H +not_123 EQU 0A38CH +not_2 EQU 0A387H +not_67890 EQU 0A3E7H +not_7 EQU 0A3E2H +not_8 EQU 0A3DDH +not_9 EQU 0A3D8H +not_a EQU 0A3F9H +not_down EQU 0A41EH +not_fire EQU 0A42AH +not_left EQU 0A419H +not_o EQU 0A407H +not_p EQU 0A402H +not_q EQU 0A3F0H +not_right EQU 0A414H +not_shift_5 EQU 0A3A7H +not_shift_7 EQU 0A3BCH +not_shift_8 EQU 0A3B7H +not_shift_t EQU 0A39DH +not_silent EQU 0AAECH +not_up EQU 0A423H +old_stack EQU 0A1C8H +pac_chars EQU 04040H +pac_footer EQU 04000H +pac_header EQU 043C0H +page_rom EQU 0A0ECH +page_screen EQU 0A0F8H +patch_rom EQU 0A103H +pill_1 EQU 0A29CH +pill_1_on EQU 0A2A4H +pill_2 EQU 0A2E2H +pill_clear_1 EQU 0A2C4H +pill_clear_2 EQU 0A325H +play_sound EQU 0AB09H +read_joy EQU 0A407H +read_qaop EQU 0A3E7H +restore_lp EQU 0A889H +rot_0_4 EQU 0A6B7H +rot_0_lp EQU 0A6EEH +rot_1_5 EQU 0A6D2H +rot_2_6 EQU 0A6C3H +rot_3_7 EQU 0A6DEH +rot_even EQU 0A6B4H +rot_odd EQU 0A6CFH +save_lp EQU 0A839H +scr_page EQU 0A1F4H +scradtab EQU 0B300H +scrinit_lp EQU 0A082H +scrtab_lp EQU 0ABDFH +set_attrs EQU 0A208H +set_int_chain EQU 0A1CEH +sinit_data EQU 0AA9BH +sinit_lp EQU 0AA8DH +sound_init EQU 0AA46H +sound_lp EQU 0AA49H +sound_table EQU 0C000H +spr_2 EQU 0A6EAH +spr_3 EQU 0A704H +spr_3_lp EQU 0A70DH +spr_data_0 EQU 05BE4H +spr_data_1 EQU 06304H +spr_data_2 EQU 06A24H +spr_data_3 EQU 07144H +spr_data_4 EQU 07864H +spr_data_5 EQU 07F84H +spr_data_6 EQU 08A34H +spr_data_7 EQU 094E4H +spr_data_end EQU 09F94H +spr_restore EQU 0A879H +spr_rot_lp EQU 0AB94H +spr_rot_lp2 EQU 0AB97H +spr_rot_lp3 EQU 0AB9FH +spr_rot_lp4 EQU 0ABB9H +spr_save EQU 0A821H +spr_save_2 EQU 05B00H +spr_save_3 EQU 05B26H +spr_save_4 EQU 05B4CH +spr_save_5 EQU 05B72H +spr_save_6 EQU 05B98H +spr_save_7 EQU 05BBEH +spr_save_end EQU 05BE4H +start EQU 0A000H +start2 EQU 0A005H +start3 EQU 0A04DH +strip_0 EQU 0A49EH +strip_0_alt EQU 0A517H +strip_1 EQU 0A4B1H +strip_1_alt EQU 0A52AH +strip_2 EQU 0A4CFH +strip_2_alt EQU 0A549H +strip_3 EQU 0A4E2H +strip_3_alt EQU 0A55CH +strip_4 EQU 0A501H +strip_4_alt EQU 0A57BH +strip_even EQU 0A49BH +strip_odd EQU 0A514H +text_fix EQU 0A172H +tile_0 EQU 0A5F6H +tile_0_lp EQU 0A5FDH +tile_2 EQU 0A66AH +tile_2_lp EQU 0A671H +tile_4 EQU 0A611H +tile_4_lp EQU 0A619H +tile_6 EQU 0A63FH +tile_62 EQU 0A63CH +tile_6_lp EQU 0A647H +tile_comp EQU 0A58EH +tile_data_0 EQU 0DBE4H +tile_data_2 EQU 0F264H +tile_data_4 EQU 0E964H +tile_data_6 EQU 0E064H +tile_exit EQU 0A685H +tile_mapped EQU 0A5AFH +tile_state EQU 0A448H +tile_strips EQU 0A498H +tilerot_lp EQU 0AB5DH +tilerot_lp2 EQU 0AB5FH +trim_edge EQU 0A799H +trim_line_lp EQU 0A7A7H +trim_lp EQU 0A774H +vis_lp EQU 0A43AH +xy_to_addr EQU 0ABEEH diff --git a/png2bin.pl b/png2bin.pl new file mode 100755 index 0000000..4a08798 --- /dev/null +++ b/png2bin.pl @@ -0,0 +1,89 @@ +#!/usr/bin/perl -w +# +# Convert PNG image to Spectrum format sprite image data +# +# Input image should be palettised 1-bit with no transparency +# +# Simon Owen + +use Compress::Zlib; +use Getopt::Std; + +# Allow -v option for verbose output +getopt('v'); + +# Strip path from input filename, and check +$0 =~ s/.*\///; +die "Usage: $0 []\n" unless @ARGV == 2 or @ARGV == 3; + +# Input image and output data file +($file,$w,$h) = (@ARGV,$ARGV[1]); # height defaults to width if not provided +die "Input image must be in PNG format\n" unless $file =~ /.png$/i; +die "Sprite width should be 2-16 pixels\n" unless $w >= 2 && $w <= 16; +die "Sprite height should be at least 1 pixel\n" unless $h > 0; + +# Slurp the entire PNG image +open INPUT, "<$file" and binmode INPUT or die "$file: $!\n"; +read INPUT, $data='', -s $file; +close INPUT; + +# Extract and check the image dimensions +$data =~ /IHDR(.{8})/s; +($iw,$ih) = unpack "N2", $1; +die "Input image (${iw}x${ih}) must be an exact multiple of sprite size (${w}x${h})\n" if ($iw%$w) or ($ih%$h); + +# Extract and expand the compressed image data +($data) = $data =~ /IDAT(.*).{8}IEND/s; +$data = Compress::Zlib::uncompress($data); + +# Remove the type byte from the start of each line, leaving just 0+1 pixel values +if (length($data) == (($iw+1)*$ih)) { # 8bpp? + $data =~ s/.(.{$iw})/$1/sg; + @data = map { $_&1 } unpack("C*", $data); +} elsif (length($data) == (($iw/2+1)*$ih)) { # 4bpp? + my $iw_2 = $iw/2; + $data =~ s/.(.{$iw_2})/$1/sg; + foreach (unpack("C*", $data)) { + push @data, ($_>>4)&1, $_&1; + } +} else { + die "Image data format is unsupported (size = ".@data." bytes)\n"; +} + +# Calculate rows+columns and the expected size of the output data +($c,$r) = ($iw/$w,$ih/$h); +$size = int(($w+7)/8)*$h*$r*$c; +print "Input image ($file) = ${iw}x${ih}, sprite = ${w}x${h}\n" if $opt_v; + +foreach $y (0..$r-1) +{ + foreach $x (0..$c-1) + { + foreach $l (0..$h-1) # line + { + # Calculate the offset of the required bit sequence + my $o = ((($y*$h+$l)*$c) + $x) * $w; + + # Convert the individual bits to a binary string then a decimal value + my $n = unpack("N", pack("B32", substr("0"x32 . join('', @data[$o..$o+$w-1]), -32))); + + # Pack into 1 or 2 bytes, with the bits left-aligned (MSB) + $output .= pack(($w<=8)?"C":"n", $n << (8-($w & 7))); + } + } +} + +# Sanity check the output data size +die "Output data ", length($output), " is the wrong size! (should be $size)\n" unless length($output) == $size; + +# The output file is the input filename with a .bin extension instead of .png +$file =~ s/png$/bin/i; +open OUTPUT, ">$file" and binmode OUTPUT or die "$file: $!\n"; + +# Determine the size of a single sprite, and strip blank sprites from the end of the data +$ss = length($output) / ($r*$c); +$output =~ s/(\0{$ss})*$//; +print "Output data ($file) = ", length($output), " bytes = ", length($output)/$ss, " sprites\n" if $opt_v; + +print OUTPUT $output; +close OUTPUT; diff --git a/remove_rom.pl b/remove_rom.pl new file mode 100755 index 0000000..71cb24f --- /dev/null +++ b/remove_rom.pl @@ -0,0 +1,26 @@ +#!/usr/bin/perl -w + +# First 4K of Pac-Man ROM +$file = 'pacman.6e'; +open FILE, "<$file" and binmode FILE or die "$file: $!\n"; +read FILE, $rom='', -s $file; +close FILE; + +# Full TAP image for emulator, including ROM +$file = 'pacemuzx.tap'; +open FILE, "<$file" and binmode FILE or die "$file: $!\n"; +read FILE, $data='', -s $file; +close FILE; + +$index = index($data, $rom); +die "ROM image not found in TAP file!\n" if $index < 0; + +$file = 'start.part'; +open FILE, ">$file" and binmode FILE or die "$file: $!\n"; +print FILE substr $data, 0, $index; +close FILE; + +$file = 'end.part'; +open FILE, ">$file" and binmode FILE or die "$file: $!\n"; +print FILE substr $data, $index+16384; +close FILE; diff --git a/sprites.png b/sprites.png new file mode 100644 index 0000000..b04995b Binary files /dev/null and b/sprites.png differ diff --git a/tiles.png b/tiles.png new file mode 100644 index 0000000..3cb2ecf Binary files /dev/null and b/tiles.png differ