diff --git a/Makefile b/Makefile index 2329766..2a13332 100644 --- a/Makefile +++ b/Makefile @@ -26,6 +26,7 @@ asm: $(ACME) -r build/million.lst src/million.a 2>build/log cp res/work.po "$(BUILDDISK)".po >>build/log cp res/_FileInformation.txt build/ >>build/log + $(CADIUS) ADDFILE "${BUILDDISK}".po "/MILLION/" "res/PROGRESS" >>build/log $(CADIUS) ADDFILE "${BUILDDISK}".po "/MILLION/" "build/MILLION.SYSTEM" >>build/log for f in res/levels/*; do $(CADIUS) ADDFILE "${BUILDDISK}".po "/MILLION/" "$$f" >>build/log; done @@ -36,3 +37,5 @@ mount: open "$(BUILDDISK)".po all: clean asm mount + +al: all diff --git a/res/PROGRESS b/res/PROGRESS new file mode 100644 index 0000000..85cab73 Binary files /dev/null and b/res/PROGRESS differ diff --git a/res/_FileInformation.txt b/res/_FileInformation.txt index 1673bc0..a78f989 100644 --- a/res/_FileInformation.txt +++ b/res/_FileInformation.txt @@ -1 +1,2 @@ MILLION.SYSTEM=Type(FF),AuxType(2000),Access(C3) +PROGRESS=Type(06),AuxType(8E10),Access(C3) diff --git a/src/constants.a b/src/constants.a index 1317cd4..d01c7ab 100644 --- a/src/constants.a +++ b/src/constants.a @@ -2,6 +2,24 @@ ;(c) 2020 by 4am ; +;------------------------------------------------------------------------------ +; YE OLDE GRAND UNIFIED MEMORY MAP +; +; MAIN MEMORY +; 0300..03CF - data structures for current puzzle +; 0400..07FF - text page (kept black for switching) +; 0800..19FF - scroll routines (generated at startup) +; 1A00..1BFF - HGR lookup tables +; 1C00..1FFF - ProDOS file buffer for puzzle files +; 2000..3FFF - hi-res page +; 4000+ - program code (relocated to here at startup) +; ...unused... +; 8A00..8DFF - ProDOS file buffer for PROGRESS file +; 8E10..8F37 - progress of puzzles in current world +; 8F38..8FFF - address lookup table for puzzles within following world data +; 9000..BEFF - world data (read from disk) +;------------------------------------------------------------------------------ + ; soft switches KBD = $C000 ; last key pressed (if any) STOREOFF = $C000 ; STA then use the following 4 flags: @@ -37,23 +55,25 @@ ROM_IN0 = $FE89 ; SETKBD ROM_PR0 = $FE93 ; SETVID ; zero page -PARAM = $00 ; word (used by PARAMS_ON_STACK macro, so basically everywhere) -;PTR = $02 ; word -;SRC = $04 ; word -;DEST = $06 ; word -;SAVE = $08 ; word +PARAM = $00 ; word (used by PARAMS_ON_STACK macro) HTAB = $24 ; byte VTAB = $25 ; byte -;RNDSEED = $4E ; word ; application constants +; zero page original_char = $ED ; byte char = $EE ; byte charrow = $EF ; byte + GENSCROLLDOWN = $0800 ; 0x0900 bytes GENSCROLLUP = $1100 ; 0x0900 bytes -HGRLO = $1A00 ; 0xC0 bytes -HGRHI = $1B00 ; 0xC0 bytes -PRODOSFILEBUFFER = $1C00 ; 0x0400 bytes -LEVELLO = $8F38 ; 0x64 bytes -LEVELHI = $8F9C ; 0x64 bytes +HGRLO = $1A00 ; 0x00C0 bytes +HGRHI = $1B00 ; 0x00C0 bytes +WORLDFILEBUFFER = $1C00 ; 0x0400 bytes + +PROGRESSFILEBUFFER = $8A00; 0x0400 bytes +PACKEDPROGRESS = $8E10 ; 0x00C0 bytes +PROGRESS = $8ED0 ; 0x0068 bytes +PUZZLELO = $8F38 ; 0x0064 bytes +PUZZLEHI = $8F9C ; 0x0064 bytes +WORLDDATA = $9000 ; 0x2F00 bytes max diff --git a/src/glue.mli.a b/src/glue.mli.a index 09dc497..722dc1b 100644 --- a/src/glue.mli.a +++ b/src/glue.mli.a @@ -21,6 +21,7 @@ CMD_NEWLINE = $03C9 ; set line-by-line read mode CMD_READ = $04CA ; read an open file CMD_WRITE = $04CB ; write to an open file CMD_CLOSE = $01CC ; close an open file +CMD_FLUSH = $01CD ; flush an open, written file to disk CMD_SETMARK = $02CE ; change position in an open file CMD_SETEOF = $02D0 ; set file size @@ -28,14 +29,16 @@ CMD_SETEOF = $02D0 ; set file size ; OpenFile ; open file via ProDOS MLI ; -; in: A/Y address of pathname +; in: stack contains 4 bytes of parameters: +; +1 [word] address of filename +; +3 [word] ProDOS file reference number ; out: if C set, open failed and A contains error code ; if C clear, open succeeded and A contains file reference number ;------------------------------------------------------------------------------ OpenFile - +ST16 mliparam+1 - +LDADDR PRODOSFILEBUFFER - +ST16 mliparam+3 + +PARAMS_ON_STACK 4 + +LDPARAMPTR 1, mliparam+1 ; store filename + +LDPARAMPTR 3, mliparam+3 ; store address of ProDOS file buffer +LDADDR CMD_OPEN jsr mli bcs + @@ -55,6 +58,7 @@ OpenFile ; out: if C set, read failed and A contains error code ; if C clear, read succeeded and A contains the same ; file reference number that was passed in +; stack set to next instruction after parameters ;------------------------------------------------------------------------------ ReadFile +PARAMS_ON_STACK 5 @@ -69,9 +73,64 @@ ReadFile lda mliparam+1 ; if no error, return file reference number + rts +;------------------------------------------------------------------------------ +; WriteFile +; write to an open file via ProDOS MLI +; +; in: A = file reference number +; stack contains 4 bytes of parameters: +; +1 [word] address of data buffer +; +3 [word] length of data buffer +; out: if C set, save failed and A contains error code +; if C clear, write succeeded and A contains the same file reference +; number that was passed in +;------------------------------------------------------------------------------ +WriteFile + sta mliparam+1 ; store file reference number + +PARAMS_ON_STACK 4 + +LDPARAMPTR 1, mliparam+2 ; store data buffer address + +LDPARAMPTR 3, mliparam+4 ; store data buffer length + +LDADDR CMD_WRITE + jsr mli + bcs + ; error, A contains MLI error code + lda mliparam+1 ; no error, return file reference number ++ rts + +;------------------------------------------------------------------------------ +; SetMarkTo0 +; set file position within an open file to the beginning of the file +; +; in: A = file reference number +; out: if C set, save failed and A contains error code +; if C clear, write succeeded and A contains the same file reference +; number that was passed in +;------------------------------------------------------------------------------ +SetMarkTo0 + sta mliparam+1 ; store file reference number + lda #0 + sta mliparam+2 + sta mliparam+3 + sta mliparam+4 + +LDADDR CMD_SETMARK + bne mli ; always branches + +;------------------------------------------------------------------------------ +; FlushFile +; flush an open file (actually writes it to disk, but without closing it) +; +; in: A = file reference number +; out: if error, C set and A contains error code +; if success, C clear +;------------------------------------------------------------------------------ +FlushFile + sta mliparam+1 ; store file reference number + +LDADDR CMD_FLUSH + bne mli ; always branches + ;------------------------------------------------------------------------------ ; CloseFile ; close an open file +; ; in: A = file reference number ; out: if error, C set and A contains error code ; if success, C clear @@ -88,6 +147,8 @@ CloseFile ; out: does not return ;------------------------------------------------------------------------------ Quit + lda #0 + jsr CloseFile +LDADDR CMD_QUIT ; /!\ execution falls through here ;------------------------------------------------------------------------------ @@ -106,12 +167,12 @@ mli sta mlicmd ; store command code mlicmd !byte 00 ; command number !word mliparam ; address of parameter table rts -mliparam !byte $FE,$FE,$FE,$FE -filetype !byte $FE ; file type (set by MLI get_file_info) +mliparam !byte $FD,$FD,$FD,$FD +filetype !byte $FD ; file type (set by MLI get_file_info) auxtype ; auxiliary file type (2 bytes, set by MLI get_file_info) -refnum !byte $FE ; file refnum (set by MLI open) -mlilen !byte $FE,$FE ; file length (set by MLI read) -blocks !byte $FE,$FE ; blocks used (set by getvolumeinfo) +refnum !byte $FD ; file refnum (set by MLI open) +mlilen !byte $FD,$FD ; file length (set by MLI read) +blocks !byte $FD,$FD ; blocks used (set by getvolumeinfo) ; member is also used by createfile - !byte $FE,$FE,$FE,$FE,$FE,$FE,$FE,$FE + !byte $FD,$FD,$FD,$FD,$FD,$FD,$FD,$FD ; used by get_file_info diff --git a/src/hw.storage.a b/src/hw.storage.a index 65d2315..c80f1c9 100644 --- a/src/hw.storage.a +++ b/src/hw.storage.a @@ -1,39 +1,72 @@ ;license:MIT ;(c) 2020 by 4am ; +; disk functions to load puzzles, and load & save puzzle completion status +; +; Public functions: +; - MaybeLoadWorldFromDisk +; - LoadProgressFromDisk +; - LoadProgressFromMemory +; - LoadPuzzleFromMemory +; - HasPuzzleBeenCompleted +; - MarkPuzzleCompleted +; - FindPackedProgressAddr +; +; Public variables: +; - gWorldID +; - gPuzzleID +; +; Public constants: +; - kPuzzleWidths +; -gLevelID +gWorldID ; a world is a group of 100 similar puzzles (12 worlds in total, 0-based) !byte $FD ; 0x00..0x0B -gPuzzleID +gPuzzleID ; ID of current puzzle within the current world (100 puzzles per world, 0-based) !byte $FD ; 0x00..0x63 -kLevelWidths - !byte 4,5,6,7 +kPuzzleWidths ; width in letters of the puzzles in each world + !byte 4,5,6,7 ; (every puzzle in a world is the same width) !byte 4,5,6,7 !byte 4,5,6,7 -MaybeLoadLevelsFromDisk -; in: A = level ID +progressRefNum ; [byte][private] + !byte $FF ; 0xFF means 'no progress file open' +checksum = $FB +packedvalue = $FC +bitcount = $FD + +;------------------------------------------------------------------------------ +; MaybeLoadWorldFromDisk +; load a world (100 puzzles) from a file on disk +; caches the last world loaded to reduce unnecessary disk access +; +; in: A = world ID ; out: C clear if file loaded and parsed successfully (or it was already in memory) ; C set if file could not be loaded and/or parsed ; other registers/flags clobbered +;------------------------------------------------------------------------------ +MaybeLoadWorldFromDisk clc adc #$41 cmp @fileLoaded - beq @exit - sta @filename+1 - +LDADDR @filename + bne + + clc + rts ++ sta @filename+1 jsr OpenFile + !word @filename + !word WORLDFILEBUFFER bcs @exit sta @refnum jsr ReadFile @refnum !byte $FD ; SMC - !word $9000 ; load address + !word WORLDDATA ; load address !word $2F00 ; maximum length to read php bcs @closeAndPopStatus plp - jsr ParseLevelData + jsr @PreParseWorldData cpx #100 bne + clc @@ -53,9 +86,9 @@ MaybeLoadLevelsFromDisk !byte 1 ; length !raw $FD ; SMC -ParseLevelData -; find the starting address of each of the 100 puzzles in this file -; in: $9000+ contains raw level data from disk +@PreParseWorldData +; find the starting address of each of the 100 puzzles in this world +; in: $9000+ contains raw world data from disk lda #$FF sta $FE lda #$8F @@ -76,54 +109,294 @@ ParseLevelData cmp #$5B ; '[' ends the parsing beq @exit lda $FE - sta LEVELLO, x + sta PUZZLELO, x lda $FF - sta LEVELHI, x + sta PUZZLEHI, x inx bne @skipLine ; always branches @IncAndGetChar - inc $FE - bne + - inc $FF -+ lda ($FE),y +; in: Y = 0 + +INCADDR $FE + lda ($FE),y cmp #$0D ; CR - hide it beq @IncAndGetChar -@exit rts + rts -ParseOnePuzzle - ldx gPuzzleID - lda LEVELLO, x +;------------------------------------------------------------------------------ +; LoadProgressFromDisk +; +; see comments in MarkPuzzleCompleted for file format +; +; in: none +; out: C clear if file was read successfully, and +; progressRefNum contains ProDOS file reference number (file is left +; open) +; C set if error occurred, and +; progressRefNum = 0xFF +;------------------------------------------------------------------------------ +LoadProgressFromDisk + jsr OpenFile + !word @progressFile + !word PROGRESSFILEBUFFER + bcs + + sta @refnum + sta progressRefNum + jsr ReadFile +@refnum !byte $FD ; SMC + !word PACKEDPROGRESS ; data address + !word $00C0 ; data length ++ rts +@progressFile + !byte 8 + !raw "PROGRESS" + +;------------------------------------------------------------------------------ +; LoadProgressFromMemory +; unpacks one world's progress into a 1-byte-per-puzzle array for easier access +; +; in: progress file must be in memory (call MaybeLoadProgressFromDisk and +; check C) +; out: A = next puzzle ID +;------------------------------------------------------------------------------ +LoadProgressFromMemory + lda gWorldID + jsr FindPackedProgressAddr + ; ($FE) -> start of the 16-byte structure for this world + ; zero destination array + ldx #103 + lda #0 +- sta PROGRESS, x + dex + bpl - + ; unpack values + ldy #0 ; index into ($FE) (source data) + ldx #0 ; index into PROGRESS (destination data) +-- lda #8 + sta bitcount + lda ($FE), y +- lsr + rol PROGRESS, x ; note: PROGRESS structure is 104 bytes (a multiple of 8), + inx ; even though only the first 100 bytes are used + dec bitcount + bne - + iny + cpy #$0D + bne -- + lda ($FE), y ; A = next puzzle ID + rts + +;------------------------------------------------------------------------------ +; LoadPuzzleFromMemory +; loads one puzzle from the in-memory cache of the world file, into the +; 'now playing this puzzle' data structures +; +; in: X = puzzle ID +; world file must be in memory (call MaybeLoadWorldFromDisk and check C) +; out: all registers and flags clobbered +;------------------------------------------------------------------------------ +LoadPuzzleFromMemory + lda PUZZLELO, x sta $FE - lda LEVELHI, x + lda PUZZLEHI, x sta $FF bne @addLine ; always branches @lineLoop - jsr @inc + +INCADDR $FE @addLine jsr AddLineToPuzzle - ldx gLevelID - ldy kLevelWidths, x -- jsr @inc - dey - bne - + ldx gWorldID + lda kPuzzleWidths, x + clc + adc $FE + sta $FE + bcc + + inc $FF ++ + ldy #0 lda ($FE), y cmp #$2C ; , beq @lineLoop @targetWordLoop - jsr @inc + +INCADDR $FE jsr AddTargetWordToPuzzle - ldx gLevelID - ldy kLevelWidths, x -- jsr @inc - dey - bne - + ldx gWorldID + lda kPuzzleWidths, x + clc + adc $FE + sta $FE + bcc + + inc $FF ++ + ldy #0 lda ($FE), y cmp #$2C ; , beq @targetWordLoop rts -@inc - inc $FE - bne + + +;------------------------------------------------------------------------------ +; HasPuzzleBeenCompleted +; +; in: X = puzzle ID +; out: C clear if puzzle had previously been marked as completed +; C set if puzzle has never been marked as completed +; X/Y preserved +; A clobbered +; other flags clobbered +;------------------------------------------------------------------------------ +HasPuzzleBeenCompleted + lda PROGRESS, x + beq + + clc + rts ++ sec + rts + +;------------------------------------------------------------------------------ +; MarkPuzzleCompleted +; +; in: X = puzzle ID +; out: all registers and flags clobbered +;------------------------------------------------------------------------------ +MarkPuzzleCompleted + ; Completion status for the current world (100 puzzles) is stored unpacked in + ; memory for easy access + ; Mark this puzzle as completed in the unpacked table + lda #1 + sta PROGRESS, x + ; Find the next uncompleted puzzle + clc + +HIDE_NEXT_BYTE +-- sec +- lda PROGRESS, x + beq + + inx + cpx #100 + bne - + ldx #0 ; start over at the puzzle 0 + bcc -- + ; If we fall through here, it means all puzzles in this world + ; have been completed (yay!), and we'll leave X at 0. + ; If we branch here, we found the next uncompleted puzzle + ; and X contains that puzzle ID. ++ txa + pha ; push next puzzle ID +; +; Completion status for all puzzles across all worlds is stored in packed +; bitfields, 1 bit per puzzle, so that the file on disk fits in a single +; block (actually a single sector). +; +; This is the file format (0xC0 bytes): +; +0x00 bit 0 - completion status for world 0x00, puzzle 0x00 +; bit 1 - completion status for world 0x00, puzzle 0x01 +; bit 2 - completion status for world 0x00, puzzle 0x02 +; bit 3 - completion status for world 0x00, puzzle 0x03 +; bit 4 - completion status for world 0x00, puzzle 0x04 +; bit 5 - completion status for world 0x00, puzzle 0x05 +; bit 6 - completion status for world 0x00, puzzle 0x06 +; bit 7 - completion status for world 0x00, puzzle 0x07 +; ... +; +0x0C bit 0 - completion status for world 0x00, puzzle 0x60 +; bit 1 - completion status for world 0x00, puzzle 0x61 +; bit 2 - completion status for world 0x00, puzzle 0x62 +; bit 3 - completion status for world 0x00, puzzle 0x63 +; bit 4-7 always 0 +; +0x0D next puzzle to play in world 0x00 (0x00..0x63) +; +0x0E percent complete (0x00..0x64) +; +0x0F XOR of bytes +0x00..+0x0E +; +0x10..+0x1F same format but for world 0x01 +; +0x20..+0x2F same format but for world 0x02 +; +0x30..+0x3F same format but for world 0x03 +; +0x40..+0x4F same format but for world 0x04 +; +0x50..+0x5F same format but for world 0x05 +; +0x60..+0x6F same format but for world 0x06 +; +0x70..+0x7F same format but for world 0x07 +; +0x80..+0x8F same format but for world 0x08 +; +0x90..+0x9F same format but for world 0x09 +; +0xA0..+0xAF same format but for world 0x0A +; +0xB0..+0xBF same format but for world 0x0B +; +; Among other things, this means that a file filled with zeroes is valid +; and means that no puzzles have been completed in any world and you should +; start at the beginning. So that's nice. +; + lda gWorldID + jsr FindPackedProgressAddr + ; ($FE) -> start of the 16-byte structure for this world + ; zero out all 16 bytes + ldy #$0F + lda #$00 +- sta ($FE), y + dey + bpl - + sta checksum + ; build packed values + ldy #0 + ldx #0 +-- lda #8 + sta bitcount + lda #0 + sta packedvalue +- lda PROGRESS, x + ror + ror packedvalue + inx + dec bitcount + bne - + lda packedvalue + sta ($FE), y + eor checksum + sta checksum + iny + cpy #$0D + bne -- + pla ; A = next puzzle ID (pushed earlier) + sta ($FE), y + eor checksum + sta checksum + ; count number of completed puzzles + ldy #0 + ldx #99 +- lda PROGRESS, x + beq + + iny ++ dex + bpl - + tya ; A = number of puzzles completed (out of 100, so also the percent complete) + ldy #$0E + sta ($FE), y + eor checksum + iny + sta ($FE), y + lda progressRefNum + jsr SetMarkTo0 + lda progressRefNum + jsr WriteFile + !word PACKEDPROGRESS + !word $00C0 + lda progressRefNum + jsr FlushFile + ldy #$0D + lda ($FE), y + rts + +FindPackedProgressAddr +; in: A = world ID +; out: ($FE) points to first byte of this world's packed progress (16-byte structure) +; A/Y clobbered +; X preserved +; all flags clobbered + pha + +LDADDR PACKEDPROGRESS + +ST16 $FE + pla + asl + asl + asl + asl + clc + adc $FE + sta $FE + bcc + inc $FF -+ rts + rts diff --git a/src/macros.a b/src/macros.a index 0ce9ce7..878a94e 100644 --- a/src/macros.a +++ b/src/macros.a @@ -112,6 +112,14 @@ + } +; increment a 16-bit value stored at an address +!macro INCADDR .addr { + inc .addr + bne + + inc .addr+1 ++ +} + ; compare a 16-bit value in A (low) and Y (high) to an absolute address !macro CMP16ADDR .addr { cmp .addr diff --git a/src/million.a b/src/million.a index 0a64ac9..d3a11d6 100644 --- a/src/million.a +++ b/src/million.a @@ -10,40 +10,57 @@ row = $FD !source "src/constants.a" ; no code !source "src/macros.a" ; no code - !source "src/million.init.a" ; one-time initialization code - !source "src/hw.vbl.init.a" + !source "src/million.init.a" ; one-time initialization code, exits via Start + !source "src/hw.vbl.init.a" ; TODO hook this up FirstMover !pseudopc $4000 { Start + jsr LoadProgressFromDisk jsr TitlePage - ; C set if title sequence ended prematurely (i.e. main menu requires full redraw) -- jsr MainMenu + ; C set if main menu requires full redraw (e.g. title sequence ended prematurely) +@GoToMainMenu + jsr MainMenu bcc + jmp Quit -+ sta gLevelID - jsr MaybeLoadLevelsFromDisk ; A = level ID from 'select level' page - bcc + ++ sta gWorldID + jsr MaybeLoadWorldFromDisk ; A = world ID from selection page + bcc @PlayNext jmp $FF59 ; TODO -+ ldx gLevelID - lda kLevelLeftMargins, x - sta GlobalLeftMargin - lda kLevelWidths, x - jsr InitPuzzleStorage - lda #0 +@PlayNext + jsr LoadProgressFromMemory + ; A = next puzzle ID +@Play sta gPuzzleID - ;jsr FindNextUnsolvedPuzzle ; TODO - - jsr ParseOnePuzzle - + ldx gWorldID + lda kWorldLeftMargins, x + sta GlobalLeftMargin + lda kPuzzleWidths, x + jsr InitPuzzleStorage + ldx gPuzzleID + jsr LoadPuzzleFromMemory lda #0 - sta selected_logical_column - jsr RedrawPuzzle + sta gSelectedLogicalColumn + jsr DrawPuzzleChrome + jsr ClearAndDrawPuzzle jsr AnimatePuzzleIntoPlace jsr DrawColumnSelectionIndicator jsr PlayEventLoop + cmp #kCompletedPuzzle + bne + + jsr AnimatePuzzleCompleted + ldx gPuzzleID + jsr MarkPuzzleCompleted + ; A - next puzzle ID + jmp @Play ++ cmp #kRequestedRestart + bne + + jsr Home + lda gPuzzleID + jmp @Play ++ sec - bcs - ; always branches + bcs @GoToMainMenu !source "src/puzzle.a" !source "src/hw.vbl.a" diff --git a/src/puzzle.a b/src/puzzle.a index dc5a8d0..11534f1 100644 --- a/src/puzzle.a +++ b/src/puzzle.a @@ -3,11 +3,28 @@ ; ; data storage routines for puzzles ; +; Public functions: +; - InitPuzzleStorage +; - AddLineToPuzzle +; - AddTargetWordToPuzzle +; - CheckForTargetWord +; - FindLetterInColumn +; - ScrollPuzzleDown +; - ScrollPuzzleUp +; - IsPuzzleComplete + puzzle_logical_width = $0300 ; [0x01 byte ] number of letters per word (4..7) puzzle_logical_height = $0301 ; [0x01 byte ] number of rows with letters (1..5) puzzle_offsets = $0302 ; [0x08 bytes] how far each column has been scrolled down from top of grid -puzzle_data = $030A ; [0x08 bytes] character data + +; 9 rows of data for current puzzle +; each byte is one of +; - letter with high bit off (unmatched letter) +; - letter with high bit on (matched letter, displayed in grey) +; - 0x00 (unused space) +; if letters per word is less than 7, extra bytes at end of each row are guaranteed to be 0x00 +puzzle_data0 = $030A ; [0x08 bytes] character data puzzle_data1 = $0312 ; [0x08 bytes] puzzle_data2 = $031A ; [0x08 bytes] puzzle_data3 = $0322 ; [0x08 bytes] @@ -24,17 +41,11 @@ puzzle_words = $0353 ; [0x80 bytes] InitPuzzleStorage ; in: A = logical puzzle width (number of letters in each word, 4..7) sta puzzle_logical_width + ldx #$CF lda #0 - sta puzzle_logical_height - sta puzzle_word_count - ldx #8 -- sta puzzle_offsets-1, x +- sta puzzle_logical_width, x dex bne - - ldx #$7F -- sta puzzle_words, x - dex - bpl - rts AddLineToPuzzle @@ -47,7 +58,7 @@ AddLineToPuzzle tax ldy #0 - lda ($FE), y - sta puzzle_data, x + sta puzzle_data0, x inx iny cpy puzzle_logical_width @@ -152,11 +163,11 @@ ScrollPuzzleDown lda puzzle_data1, y sta puzzle_data2, y - lda puzzle_data, y + lda puzzle_data0, y sta puzzle_data1, y lda #0 - sta puzzle_data, y + sta puzzle_data0, y lda puzzle_offsets, y clc @@ -179,7 +190,7 @@ ScrollPuzzleUp beq @fail lda puzzle_data1, y - sta puzzle_data, y + sta puzzle_data0, y lda puzzle_data2, y sta puzzle_data1, y @@ -213,3 +224,18 @@ ScrollPuzzleUp rts @fail sec rts + +IsPuzzleComplete +; out: C clear if puzzle is complete (all letters have been matched) +; C set if puzzle is not yet complete + ldx #$47 +- lda puzzle_data0, x + beq @keepChecking + bmi @keepChecking + sec + rts +@keepChecking + dex + bpl - + clc + rts diff --git a/src/ui.common.a b/src/ui.common.a index 5c1bfb5..cc64d3d 100644 --- a/src/ui.common.a +++ b/src/ui.common.a @@ -113,3 +113,56 @@ SoftBell dex bne - rts + +nonZeroDigits = $EE +paddingCharacter = $EF +ToASCIIString +; convert byte value to length-prefixed 3-digit decimal number as ASCII string with given padding character +; in: X = any number (0..255 obviously) +; A = padding character (e.g. '0' or ' ') +; out: $F1 = 0x03 +; $F2..$F4 = ASCII digits of decimal representation +; clobbers $EE,$EF,$F0 +; all flags & registers clobbered + sta paddingCharacter + stx $F0 + ldx #0 + stx nonZeroDigits + stx $F1 +@outer lda #0 + pha +@inner lda $F0 + cmp @kPowersOfTen, x + bcc @digitDone + sbc @kPowersOfTen, x + sta $F0 + lda $F1 + sbc #0 + sta $F1 + pla + adc #0 + pha + jmp @inner +@digitDone + pla + beq @maybeUsePaddingChar + inc nonZeroDigits +- ora #$30 + bne + ; always branches +@maybeUsePaddingChar + cpx #2 + beq - + ldy nonZeroDigits + bne - + lda paddingCharacter ++ sta $F2, x + inx + cpx #$03 + bcc @outer + lda #3 ; length byte + sta $F1 + rts +@kPowersOfTen + !byte 100 + !byte 10 + !byte 1 diff --git a/src/ui.main.menu.a b/src/ui.main.menu.a index 54e65b8..b673c2d 100644 --- a/src/ui.main.menu.a +++ b/src/ui.main.menu.a @@ -13,8 +13,8 @@ MainMenu ; C set if full screen clear & redraw is required ; (will happen if key is pressed during title screen, or ; if user returns to main menu from play or any other screen) -; out: C clear if user selected a level to play, and -; A = level ID +; out: C clear if user selected a world to play, and +; A = world ID ; C set if user selected quit bcc @noredraw jsr Home @@ -22,13 +22,13 @@ MainMenu sta GlobalLeftMargin ldy #3 - ldx #1 - lda titleline1-2, y + lda kTitleLine1-2, y jsr DrawLargeCharacter inx - lda titleline2-2, y + lda kTitleLine2-2, y jsr DrawLargeCharacter inx - lda titleline3-2, y + lda kTitleLine3-2, y jsr DrawLargeCharacter iny cpy #$0A @@ -53,9 +53,9 @@ MainMenu jsr SoftBell jmp - @eventReturn - jsr SelectLevel + jsr SelectWorld bcs @noselection - rts ; return to caller with A = level ID + rts ; return to caller with A = world ID @noselection jmp MainMenu ; C is already set so this will do a full redraw @eventEsc @@ -63,27 +63,27 @@ MainMenu rts counter = $F2 -selectedlevel = $F3 +selectedworld = $F3 -SelectLevel +SelectWorld ; in: none -; out: C clear if level was selected, and -; A = 0-based level ID -; C set if no level was selected and A is undefined +; out: C clear if world was selected, and +; A = 0-based world ID +; C set if no world was selected and A is undefined jsr Home - - +LDADDR leveldescriptions ; TODO update level descriptions with % complete + jsr UpdateWorldPercents + +LDADDR worlddescriptions +ST16 $FE lda #0 sta counter - sta selectedlevel ; TODO set selected level from prefs + sta selectedworld ; TODO set selected world from prefs lda #6 sta VTAB - lda #10 sta HTAB +LD16 $FE ldx counter - cpx selectedlevel + cpx selectedworld beq + ldx #20 jsr DrawHeavySilkBuffer @@ -102,12 +102,12 @@ SelectLevel cmp #12 bcc - - +PRINT_AT levelhelp, 23, 0 + +PRINT_AT worldhelp, 23, 0 bit CLEARKBD -@selectLevelLoop +@selectWorldLoop lda KBD - bpl @selectLevelLoop + bpl @selectWorldLoop bit CLEARKBD and #$7F cmp #$0B ; up arrow @@ -125,7 +125,7 @@ SelectLevel rts @eventReturn jsr Home - lda selectedlevel + lda selectedworld clc rts @eventEsc @@ -133,56 +133,56 @@ SelectLevel rts @eventLeftArrow @eventUpArrow - jsr RedrawPreviouslySelectedLevel - ldx selectedlevel + jsr RedrawPreviouslySelectedWorld + ldx selectedworld bne + ldx #12 + dex - stx selectedlevel - jsr DrawNewlySelectedLevel - jmp @selectLevelLoop + stx selectedworld + jsr DrawNewlySelectedWorld + jmp @selectWorldLoop @eventRightArrow @eventDownArrow - jsr RedrawPreviouslySelectedLevel - ldx selectedlevel + jsr RedrawPreviouslySelectedWorld + ldx selectedworld cpx #11 bne + ldx #$FF + inx - stx selectedlevel - jsr DrawNewlySelectedLevel - jmp @selectLevelLoop + stx selectedworld + jsr DrawNewlySelectedWorld + jmp @selectWorldLoop -RedrawPreviouslySelectedLevel - lda selectedlevel +RedrawPreviouslySelectedWorld + lda selectedworld tax clc adc #6 sta VTAB lda #10 sta HTAB - jsr GetLevelDescription + jsr GetWorldDescription +LD16 $FE ldx #20 jmp DrawHeavySilkBuffer -DrawNewlySelectedLevel - lda selectedlevel +DrawNewlySelectedWorld + lda selectedworld tax clc adc #6 sta VTAB lda #10 sta HTAB - jsr GetLevelDescription + jsr GetWorldDescription +LD16 $FE ldx #20 jmp DrawHeavySilkBufferInverse -GetLevelDescription -; in: X = 0-based level ID -; out: $FE/$FF points to level description buffer - +LDADDR leveldescriptions +GetWorldDescription +; in: X = 0-based world ID +; out: $FE/$FF points to world description buffer + +LDADDR worlddescriptions +ST16 $FE jmp + - lda $FE @@ -195,6 +195,37 @@ GetLevelDescription bpl - rts +worldindex=$FB +UpdateWorldPercents + +LDADDR worlddescriptions + +ST16 $FC + lda #0 + sta worldindex +-- jsr FindPackedProgressAddr + ldy #$0E + lda ($FE), y + tax + lda #$20 + jsr ToASCIIString + ldx #2 + ldy #17 +- lda $F2, x + sta ($FC), y + dey + dex + bpl - + lda $FC + clc + adc #20 + sta $FC + bcc + + inc $FD ++ inc worldindex + lda worldindex + cmp #12 + bne -- + rts + asterisk !byte 1 !byte "*" @@ -214,7 +245,7 @@ disclaimer !byte 40 !raw "* NOT GUARANTEED, ACTUAL COUNT MAY VARY." -leveldescriptions +worlddescriptions !raw " 4X3, EASY 0% " !raw " 5X3, BASIC 0% " !raw " 6X3, SIMPLE 0% " @@ -228,6 +259,36 @@ leveldescriptions !raw " 6X5, TRICKY 0% " !raw " 7X5, COMPLEX 0% " -levelhelp +kWorldShortNames + !byte 3 + !raw "4X3" + !byte 3 + !raw "5X3" + !byte 3 + !raw "6X3" + !byte 3 + !raw "7X3" + !byte 3 + !raw "4X4" + !byte 3 + !raw "5X4" + !byte 3 + !raw "6X4" + !byte 3 + !raw "7X4" + !byte 3 + !raw "4X5" + !byte 3 + !raw "5X5" + !byte 3 + !raw "6X5" + !byte 3 + !raw "7X5" + +kDash + !byte 1 + !raw "-" + +worldhelp !byte 40 !raw "ARROWS TO SELECT, RETURN TO PLAY, OR ESC" diff --git a/src/ui.play.a b/src/ui.play.a index cadea5c..0cd6741 100644 --- a/src/ui.play.a +++ b/src/ui.play.a @@ -5,17 +5,26 @@ ; ; Public functions: ; - PlayEventLoop +; - ClearAndDrawPuzzle +; - DrawPuzzleChrome +; - AnimatePuzzleIntoPlace +; - AnimatePuzzleCompleted ; -selected_logical_column +; codes returned from PlayEventLoop to explain why the event loop ended +kCompletedPuzzle = 1 +kRequestedRestart = 2 +kPressedEsc = 3 + +gSelectedLogicalColumn !byte 0 -kLevelLeftMargins +kWorldLeftMargins !byte 15,13,12,10 !byte 15,13,12,10 !byte 15,13,12,10 -kLevelRightMargins +kWorldRightMargins !byte 27,28,30,31 !byte 27,28,30,31 !byte 27,28,30,31 @@ -23,6 +32,15 @@ kLevelRightMargins kStartingColor !byte $D5,$AA +;------------------------------------------------------------------------------ +; PlayEventLoop +; main event loop for playing a puzzle +; +; in: puzzle has been loaded into memory, drawn on screen, animated, &c. +; and is ready to play +; out: A = reason why event loop ended (see list above) +; all other registers and flags clobbered +;------------------------------------------------------------------------------ PlayEventLoop bit CLEARKBD - lda KBD @@ -39,6 +57,8 @@ PlayEventLoop beq @eventRightArrow cmp #$1B ; Esc beq @eventEsc + cmp #$12 ; Ctrl-R + beq @eventCtrlR cmp #$61 bcc + and #$DF @@ -46,54 +66,65 @@ PlayEventLoop bcc + cmp #$5B bcs + - jsr @eventLetter + jmp @eventLetter + jmp PlayEventLoop + @eventLeftArrow - ldy selected_logical_column + ldy gSelectedLogicalColumn jsr EraseColumnSelectionIndicator bne + ldy puzzle_logical_width + dey - sty selected_logical_column + sty gSelectedLogicalColumn jsr DrawColumnSelectionIndicator - jmp @done + jmp PlayEventLoop + @eventRightArrow - ldy selected_logical_column + ldy gSelectedLogicalColumn jsr EraseColumnSelectionIndicator iny cpy puzzle_logical_width bcc + ldy #0 -+ sty selected_logical_column ++ sty gSelectedLogicalColumn jsr DrawColumnSelectionIndicator - jmp @done + jmp PlayEventLoop + @eventLetter - ldy selected_logical_column + ldy gSelectedLogicalColumn jsr FindLetterInColumn - bcs + ; TODO + jmp PlayEventLoop + @eventEsc -+ rts + lda #kPressedEsc + rts + +@eventCtrlR + lda #kRequestedRestart + rts + @eventUpArrow - ldy selected_logical_column + ldy gSelectedLogicalColumn jsr ScrollPuzzleUp bcs @fail jsr ScrollUp jsr CheckForTargetWord - bcc FoundTargetWord + bcc @foundTargetWord bcs @done @fail jsr SoftBell @done jmp PlayEventLoop + @eventDownArrow - ldy selected_logical_column + ldy gSelectedLogicalColumn jsr ScrollPuzzleDown bcs @fail jsr ScrollDown jsr CheckForTargetWord - bcc FoundTargetWord - bcs @done ; always branches + bcc @foundTargetWord + bcs @done -FoundTargetWord +@foundTargetWord ldx #4 ldy #0 - lda puzzle_data4, y @@ -101,90 +132,24 @@ FoundTargetWord iny cpy puzzle_logical_width bne - - ; TODO check whether puzzle is complete - jmp PlayEventLoop - -DrawThinLines - ldx #$55 - jsr DrawThinLine - ldx #$6B - ; /!\ execution falls through here -DrawThinLine -; in: X = HGR row (0x00..0xBF) - ldy HGRLO, x - sty $FE - ldy HGRHI, x - sty $FF - ldx gLevelID - ldy kLevelRightMargins, x - sty @right+1 - ldy GlobalLeftMargin - dey - tya - and #1 - tax - lda kStartingColor, x -- sta ($FE), y - eor #$7F - iny -@right cpy #$FD ; SMC - bcc - + jsr IsPuzzleComplete + bcs @done + lda #kCompletedPuzzle rts -EraseColumnSelectionIndicator -; out: preserves X/Y - stx @x+1 - sty @y+1 +;------------------------------------------------------------------------------ +; ClearAndDrawPuzzle +; clears screen and draws current puzzle (but not column selection indicator or +; other UI elements) +; +; in: none +; out: all registers and flags clobbered +;------------------------------------------------------------------------------ +ClearAndDrawPuzzle +; jsr Home +; bit TEXTMODE jsr DrawThinLines -@x ldx #$FD ; SMC -@y ldy #$FD ; SMC - rts - -DrawColumnSelectionIndicator -; out: preserves X/Y - stx @x+1 - sty @y+1 - ldx #$55 - jsr DrawOneSelectionIndicator - ldx #$6B - jsr DrawOneSelectionIndicator -@x ldx #$FD ; SMC -@y ldy #$FD ; SMC - rts - -DrawOneSelectionIndicator - lda HGRLO, x - sta $FE - lda HGRHI, x - sta $FF - ldy GlobalLeftMargin - dey - tya - and #1 - eor #1 - tax - lda kStartingColor, x - ldx selected_logical_column - beq + -- iny - iny - iny - eor #$7F - dex - bne - -+ ldx #4 -- sta ($FE), y - eor #$7F - iny - dex - bne - - rts - -RedrawPuzzle - jsr Home - bit TEXTMODE - jsr DrawThinLines - +LDADDR puzzle_data + +LDADDR puzzle_data0 +ST16 $FE ldx #0 ; logical row -- ldy #0 ; logical column @@ -206,9 +171,15 @@ RedrawPuzzle bit GFXMODE rts +;------------------------------------------------------------------------------ +; AnimatePuzzleIntoPlace +; +; in: none +; out: all registers and flags clobbered +;------------------------------------------------------------------------------ AnimatePuzzleIntoPlace - ldx gLevelID - ldy kLevelWidths, x + ldx gWorldID + ldy kPuzzleWidths, x sty @max+1 ldy #0 -- ldx #4 @@ -220,3 +191,164 @@ AnimatePuzzleIntoPlace @max cpy #$FD ; SMC bne -- rts + +;------------------------------------------------------------------------------ +; DrawPuzzleChrome +; draw all elements on puzzle screen that are not the actual puzzle +; (e.g. column selection indicator, game title, help text) +; +; in: none +; out: all registers and flags clobbered +;------------------------------------------------------------------------------ +DrawPuzzleChrome + +PRINT_AT kTitleLine1, 0, 0 + +PRINT_AT kTitleLine2, 1, 0 + +PRINT_AT kTitleLine3, 2, 0 + +LDADDR kWorldShortNames + +ST16 $FE + lda gWorldID + asl + asl + clc + adc $FE + sta $FE + bcc + + inc $FF ++ + lda #4 + sta VTAB + lda #0 + sta HTAB + +LD16 $FE + jsr DrawHeavySilkString + +LDADDR kDash + jsr DrawHeavySilkString + ldx gPuzzleID + inx ; visible puzzle number is 1-based + lda #$30 ; padding character ('0') + jsr ToASCIIString + +LDADDR $00F1 + jsr DrawHeavySilkString + jmp DrawColumnSelectionIndicator + +;------------------------------------------------------------------------------ +; AnimatePuzzleCompleted +; +; in: none +; out: all registers and flags clobbered +;------------------------------------------------------------------------------ +AnimatePuzzleCompleted + jsr DrawThinLines + ldx gWorldID + lda kPuzzleWidths, x + sta @max+1 + ldy #0 + ldx #0 +- jsr ScrollDown + inc puzzle_offsets, x + lda puzzle_offsets, x + cmp #9 + bne - + inx + iny +@max cpy #$FD ; SMC + bne - + rts + +;------------------------------------------------------------------------------ +; DrawThinLines [private] +; +; in: none +; out: all registers and flags clobbered +;------------------------------------------------------------------------------ +DrawThinLines + ldx #$55 + jsr DrawThinLine + ldx #$6B + ; /!\ execution falls through here +DrawThinLine +; in: X = HGR row (0x00..0xBF) + ldy HGRLO, x + sty $FE + ldy HGRHI, x + sty $FF + ldx gWorldID + ldy kWorldRightMargins, x + sty @right+1 + ldy GlobalLeftMargin + dey + tya + and #1 + tax + lda kStartingColor, x +- sta ($FE), y + eor #$7F + iny +@right cpy #$FD ; SMC + bcc - + rts + +;------------------------------------------------------------------------------ +; EraseColumnSelectionIndicator [private] +; +; in: none +; out: preserves X/Y +;------------------------------------------------------------------------------ +EraseColumnSelectionIndicator + stx @x+1 + sty @y+1 + jsr DrawThinLines +@x ldx #$FD ; SMC +@y ldy #$FD ; SMC + rts + +;------------------------------------------------------------------------------ +; DrawColumnSelectionIndicator [private] +; +; in: none +; out: preserves X/Y +;------------------------------------------------------------------------------ +DrawColumnSelectionIndicator + stx @x+1 + sty @y+1 + ldx #$55 + jsr DrawOneSelectionIndicator + ldx #$6B + jsr DrawOneSelectionIndicator +@x ldx #$FD ; SMC +@y ldy #$FD ; SMC + rts + +;------------------------------------------------------------------------------ +; DrawOneSelectionIndicator [private] +; +; in: none +; out: all registers and flags clobbered +;------------------------------------------------------------------------------ +DrawOneSelectionIndicator + lda HGRLO, x + sta $FE + lda HGRHI, x + sta $FF + ldy GlobalLeftMargin + dey + tya + and #1 + eor #1 + tax + lda kStartingColor, x + ldx gSelectedLogicalColumn + beq + +- iny + iny + iny + eor #$7F + dex + bne - ++ ldx #4 +- sta ($FE), y + eor #$7F + iny + dex + bne - + rts diff --git a/src/ui.title.a b/src/ui.title.a index 5c4b1f4..194b58e 100644 --- a/src/ui.title.a +++ b/src/ui.title.a @@ -7,19 +7,19 @@ ; Public functions: ; - TitlePage ; -; Public variables: -; - titleline1 -; - titleline2 -; - titleline3 +; Public constants: +; - kTitleLine1 +; - kTitleLine2 +; - kTitleLine3 titlechar = $FD -titleline1 +kTitleLine1 !byte 7 !raw "MILLION" -titleline2 +kTitleLine2 !byte 7 !raw "PERFECT" -titleline3 +kTitleLine3 !byte 7 !raw "LETTERS" @@ -79,11 +79,11 @@ TitlePage beq @line3 @random lda titlechar jmp + ; can't use a BNE because will actually be 0 the third time around -@line1 lda titleline1-2, y +@line1 lda kTitleLine1-2, y bne + -@line2 lda titleline2-2, y +@line2 lda kTitleLine2-2, y bne + -@line3 lda titleline3-2, y +@line3 lda kTitleLine3-2, y + jsr DrawLargeCharacter lda titlechar beq +