diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 88bfc0a..bac6a2a 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -3,7 +3,20 @@ "allow": [ "Bash(build.bat)", "PowerShell(Copy-Item \"c:\\\\Development\\\\Commodore\\\\os Experiments\\\\c64os\\\\samples\\\\cc65-mouse-driver-demo\\\\mousedemo.c\" \"c:\\\\Development\\\\Commodore\\\\os Experiments\\\\c64os\\\\samples\\\\cc65-mouse-driver-demo\\\\mouse_driver_demo.c\" -Force)", - "Bash(mkdir -p \"C:\\\\Users\\\\danielc.NTP\\\\.claude\\\\projects\\\\d--Development-Commodore-C64-dev\\\\memory\")" + "Bash(mkdir -p \"C:\\\\Users\\\\danielc.NTP\\\\.claude\\\\projects\\\\d--Development-Commodore-C64-dev\\\\memory\")", + "PowerShell($f = \"c:\\\\Development\\\\Commodore\\\\os Experiments\\\\c64os\\\\samples\\\\kernal-os-skeleton\\\\kernal_os.prg\"; \\(Get-Item $f\\).Length)", + "PowerShell($output = & \"C:\\\\Program Files\\\\GTK3VICE-3.10-win64\\\\bin\\\\x64sc.exe\" -help 2>&1; $output | Where-Object { $_ -match \"device|fs|drive|disk\" } | Select-Object -First 40)", + "PowerShell(cmd /c \"cd /d `\"c:\\\\Development\\\\Commodore\\\\os Experiments\\\\c64os\\\\samples\\\\kernal-os-skeleton`\" && build_all.bat\" 2>&1)", + "Bash(cmd.exe /c build_all.bat)", + "Bash(cmd.exe /c \"build_all.bat\")", + "Bash(echo \"exit: $?\")", + "Bash(cmd.exe /c \"ca65 kernal_os.asm -o kernal_os.o && ld65 -C kernal_os.cfg kernal_os.o -o kernal_os.prg && echo kernal_os OK\")", + "Bash(ca65 \"c:/Development/Commodore/os Experiments/c64os/samples/kernal-os-skeleton/kernal_os.asm\" -o \"c:/Development/Commodore/os Experiments/c64os/samples/kernal-os-skeleton/kernal_os.o\")", + "Bash(cmd /c build_all.bat)", + "Bash(cmd /c \"build_all.bat && echo BUILD_OK || echo BUILD_FAILED\")", + "Bash(echo \"EXIT: $?\")", + "PowerShell(Get-ChildItem -Path \"c:\\\\Development\\\\Commodore\\\\os Experiments\\\\c64os\\\\skirmish-game\\\\\" -Recurse -File | Select-Object FullName | ForEach-Object { $_.FullName })", + "PowerShell(Test-Path \"c:\\\\Development\\\\Commodore\\\\os Experiments\\\\c64os\\\\skirmish-game\\\\skirmish.prg\" && \\(Get-Item \"c:\\\\Development\\\\Commodore\\\\os Experiments\\\\c64os\\\\skirmish-game\\\\skirmish.prg\"\\).Length)" ], "additionalDirectories": [ "C:\\Users\\danielc.NTP\\.claude\\projects\\d--Development-Commodore-C64-dev" diff --git a/samples/kernal-os-skeleton/build_all.bat b/samples/kernal-os-skeleton/build_all.bat new file mode 100644 index 0000000..7580c71 --- /dev/null +++ b/samples/kernal-os-skeleton/build_all.bat @@ -0,0 +1,40 @@ +@echo off +setlocal + +set "C1541=C:\Program Files\GTK3VICE-3.10-win64\bin\c1541.exe" +set "DISK=kernal_os.d64" + +rem ---- build kernal_os.prg ---- +call build.bat +if errorlevel 1 goto error + +rem ---- build vim.prg ---- +call build_vim.bat +if errorlevel 1 goto error + +rem ---- verify c1541 ---- +if not exist "%C1541%" ( + echo c1541.exe not found at: + echo %C1541% + exit /b 1 +) + +rem ---- create fresh D64 and write both PRGs ---- +"%C1541%" -format "kernal os,01" d64 "%DISK%" +if errorlevel 1 goto error + +"%C1541%" "%DISK%" -write "kernal_os.prg" "kernal_os" +if errorlevel 1 goto error + +"%C1541%" "%DISK%" -write "vim.prg" "vim" +if errorlevel 1 goto error + +echo. +echo built %DISK% (kernal_os + vim) +goto end + +:error +echo build failed. +exit /b 1 + +:end diff --git a/samples/kernal-os-skeleton/build_vim.bat b/samples/kernal-os-skeleton/build_vim.bat new file mode 100644 index 0000000..4adf7a2 --- /dev/null +++ b/samples/kernal-os-skeleton/build_vim.bat @@ -0,0 +1,15 @@ +@echo off + +ca65 vim.asm -o vim.o +if errorlevel 1 goto error + +ld65 -C vim.cfg vim.o -o vim.prg +if errorlevel 1 goto error + +echo built vim.prg successfully. +goto end + +:error +echo build failed. + +:end diff --git a/samples/kernal-os-skeleton/kernal_os.asm b/samples/kernal-os-skeleton/kernal_os.asm index 956da72..712b0ea 100644 --- a/samples/kernal-os-skeleton/kernal_os.asm +++ b/samples/kernal-os-skeleton/kernal_os.asm @@ -34,6 +34,7 @@ CHKOUT = $FFC9 CLRCH = $FFCC BASIN = $FFCF CLALL = $FFE7 +LOAD = $FFD5 ; ------------------------------------------------------------ ; system locations @@ -55,6 +56,7 @@ INPUT_MAX = 64 DISK_DEV = 8 LFN_DIR = 2 LFN_CMD = 15 +EXEC_AREA = $2000 SYS_ENTRY = 2061 ; ------------------------------------------------------------ @@ -1171,12 +1173,137 @@ check_exit: jmp request_exit command_unknown: - lda #msg_unknown + jmp do_exec + +command_empty: + rts + +; ------------------------------------------------------------ +; external program loader +; ------------------------------------------------------------ +; When the typed word does not match any built-in command, +; search the current disk device for a PRG file with that name. +; If found, load it to EXEC_AREA ($2000) and JSR there. +; The external program should end with RTS to return to the shell. +; All external programs must be assembled for address $2000. +; ------------------------------------------------------------ + +do_exec: + jsr build_exec_filename + bcc exec_try_load + jmp exec_not_found + +exec_try_load: + jsr reset_file_state + + lda #LFN_DIR + sta active_lfn + ldx current_device + ldy #1 + jsr SETLFS + + lda filename_len + ldx #filename_spec + jsr SETNAM + + lda #0 + ldx #EXEC_AREA + jsr LOAD + bcc exec_loaded + + jsr reset_file_state + jmp exec_not_found + +exec_loaded: + jsr reset_file_state + jsr set_os_memory + + lda #msg_exec_run jsr print_string + + ldy #0 +exec_print_name: + lda input_buffer,y + beq exec_print_done + cmp #' ' + beq exec_print_done + jsr CHROUT + iny + bne exec_print_name + +exec_print_done: + lda #CR + jsr CHROUT + + lda #EXEC_AREA + sta PTR1_HI + jsr call_indirect + jsr set_os_memory rts -command_empty: +exec_not_found: + lda #msg_exec_nf + jsr print_string + + ldy #0 +exec_nf_name: + lda input_buffer,y + beq exec_nf_done + cmp #' ' + beq exec_nf_done + jsr CHROUT + iny + bne exec_nf_name + +exec_nf_done: + lda #CR + jsr CHROUT + rts + +; JMP (PTR1) — turns a JSR into an indirect call. +; Caller does: JSR call_indirect +; The called code's RTS returns to call_indirect's caller. +call_indirect: + jmp (PTR1_LO) + +; build filename_spec = "{drive}:{cmdname}" from input_buffer word 0 +build_exec_filename: + ldx #0 + lda current_drive + clc + adc #'0' + sta filename_spec,x + inx + lda #':' + sta filename_spec,x + inx + + ldy #0 + +exec_copy_name: + lda input_buffer,y + beq exec_name_done + cmp #' ' + beq exec_name_done + sta filename_spec,x + inx + iny + cpx #35 + bcc exec_copy_name + sec + rts + +exec_name_done: + stx filename_len + lda #0 + sta filename_spec,x + clc rts ; ------------------------------------------------------------ @@ -1958,9 +2085,6 @@ msg_banner: msg_prompt: .byte "OS> ", 0 -msg_unknown: - .byte "UNKNOWN. HELP", CR, 0 - msg_help: .byte CR .byte "HELP MEM INC DUMP ZERO SIGN", CR @@ -1969,6 +2093,7 @@ msg_help: .byte "HEX RUN RENAME COPY", CR .byte "DEVICE DEV DRIVE DEL ERASE", CR .byte "ABOUT EXIT", CR + .byte "OTHER: LOAD+RUN PRG AT $2000", CR .byte CR .byte 0 @@ -2003,6 +2128,8 @@ msg_about: .byte "KERNAL STAYS MAPPED IN.", CR .byte "SHELL CODE STARTS AT $080D.", CR .byte "USE $A000-$BFFF AS WORK RAM.", CR + .byte "EXTERNAL CMDS LOAD AT $2000.", CR + .byte "ASSEMBLE CMDS WITH *=$2000.", CR .byte CR .byte 0 @@ -2178,6 +2305,12 @@ msg_copy: msg_copy_usage: .byte "USAGE: COPY srcname dstname", CR, 0 +msg_exec_run: + .byte "EXEC: ", 0 + +msg_exec_nf: + .byte "? ", 0 + .segment "BSS" workspace_counter: diff --git a/samples/kernal-os-skeleton/kernal_os.d64 b/samples/kernal-os-skeleton/kernal_os.d64 new file mode 100644 index 0000000..1a1425a Binary files /dev/null and b/samples/kernal-os-skeleton/kernal_os.d64 differ diff --git a/samples/kernal-os-skeleton/kernal_os.o b/samples/kernal-os-skeleton/kernal_os.o index 0c664a2..11d0623 100644 Binary files a/samples/kernal-os-skeleton/kernal_os.o and b/samples/kernal-os-skeleton/kernal_os.o differ diff --git a/samples/kernal-os-skeleton/kernal_os.prg b/samples/kernal-os-skeleton/kernal_os.prg index 5b3d2d4..2c09c37 100644 Binary files a/samples/kernal-os-skeleton/kernal_os.prg and b/samples/kernal-os-skeleton/kernal_os.prg differ diff --git a/samples/kernal-os-skeleton/run.bat b/samples/kernal-os-skeleton/run.bat new file mode 100644 index 0000000..e771423 --- /dev/null +++ b/samples/kernal-os-skeleton/run.bat @@ -0,0 +1,16 @@ +@echo off +setlocal + +set "VICE_EXE=C:\Program Files\GTK3VICE-3.10-win64\bin\x64sc.exe" +set "DISK=kernal_os.d64" + +if not exist "%VICE_EXE%" ( + echo VICE x64sc was not found at: + echo %VICE_EXE% + exit /b 1 +) + +call build_all.bat +if errorlevel 1 exit /b 1 + +"%VICE_EXE%" -autostart "%CD%\%DISK%" diff --git a/samples/kernal-os-skeleton/vim.asm b/samples/kernal-os-skeleton/vim.asm new file mode 100644 index 0000000..c74a9bc --- /dev/null +++ b/samples/kernal-os-skeleton/vim.asm @@ -0,0 +1,907 @@ +; +; vim.asm - minimal modal text editor for the kernal-os-skeleton shell +; +; Build: +; ca65 vim.asm -o vim.o +; ld65 -C vim.cfg vim.o -o vim.prg +; +; From the OS shell type: VIM +; All external commands load at $2000 and end with RTS. +; +; Modes +; NORMAL (default) h j k l or cursor keys = move +; i = insert before cursor +; a = append after cursor +; o = open line below +; x = delete char under cursor +; : = enter command mode +; INSERT type text, DEL = backspace, RUN/STOP = back to NORMAL +; COMMAND :q quit :w NAME save to disk +; RUN/STOP = cancel +; +; Text model: the 24x40 text area of screen RAM IS the buffer. +; Row 24 (bottom line) is the status bar. +; + +.setcpu "6502" + +; ---- KERNAL ---- +GETIN = $FFE4 +STOPKEY = $FFE1 +CHROUT = $FFD2 +SETLFS = $FFBA +SETNAM = $FFBD +OPEN = $FFC0 +CLOSE = $FFC3 +CHKOUT = $FFC9 +CLRCH = $FFCC +CLALL = $FFE7 + +; ---- hardware ---- +SCRRAM = $0400 + +; ---- layout ---- +SCR_COLS = 40 +SCR_ROWS = 24 ; rows 0-23 = text, row 24 = status bar + +; ---- modes ---- +MODE_NORMAL = 0 +MODE_INSERT = 1 +MODE_CMD = 2 + +; ---- PETSCII ---- +CR_CH = $0D +DEL_CH = $14 + +; ---- command buffer ---- +CMD_MAX = 20 + +; ---- save LFN ---- +SAVE_LFN = 5 +SAVE_DEV = 8 + +; ---- zero page (user area, safe under OS banking) ---- +ZP_LO = $FB ; general destination pointer lo +ZP_HI = $FC ; general destination pointer hi +STR_LO = $FD ; string source pointer lo +STR_HI = $FE ; string source pointer hi +ZP_TMP = $02 ; scratch byte + +; ================================================================ +; PRG load address header +; ================================================================ +.segment "LOADADDR" + .word $2000 + +; ================================================================ +; CODE +; ================================================================ +.segment "CODE" + +; ---- entry ---- +vim_main: + jsr CLALL ; close any open channels from shell + jsr vim_init + jsr full_redraw + +main_loop: + lda quit_flag + beq ml_no_quit + jmp vim_do_exit +ml_no_quit: + + jsr get_key ; waits for keypress; $FF = RUN/STOP + cmp #$FF + bne ml_key_ok + ; RUN/STOP pressed + lda cur_mode + beq ml_stop_quit ; normal mode: quit + lda #MODE_NORMAL ; other modes: back to normal + sta cur_mode + jsr full_redraw + jmp main_loop +ml_stop_quit: + lda #1 + sta quit_flag + jmp main_loop +ml_key_ok: + sta last_key + lda cur_mode + cmp #MODE_INSERT + bne ml_not_insert + jmp handle_insert +ml_not_insert: + cmp #MODE_CMD + bne ml_not_cmd + jmp handle_cmd +ml_not_cmd: + ; fall through = normal mode + +; ================================================================ +; NORMAL MODE +; ================================================================ +handle_normal: + lda last_key + cmp #'H' + beq nm_left + cmp #$9D ; cursor-left hardware key + beq nm_left + cmp #'L' + beq nm_right + cmp #$1D ; cursor-right hardware key + beq nm_right + cmp #'K' + beq nm_up + cmp #$91 ; cursor-up hardware key + beq nm_up + cmp #'J' + beq nm_down + cmp #$11 ; cursor-down hardware key + beq nm_down + cmp #'I' + beq nm_ins + cmp #'A' + beq nm_app + cmp #'X' + beq nm_del + cmp #'O' + beq nm_open + cmp #':' + beq nm_cmd + jmp nm_done +nm_left: + jsr cursor_left + jmp nm_done +nm_right: + jsr cursor_right + jmp nm_done +nm_up: + jsr cursor_up + jmp nm_done +nm_down: + jsr cursor_down + jmp nm_done +nm_ins: + lda #MODE_INSERT + sta cur_mode + jmp nm_done +nm_app: + jsr cursor_right + lda #MODE_INSERT + sta cur_mode + jmp nm_done +nm_del: + jsr delete_char + jmp nm_done +nm_open: + jsr open_line_below + lda #MODE_INSERT + sta cur_mode + jmp nm_done +nm_cmd: + lda #MODE_CMD + sta cur_mode + lda #0 + sta cmd_len +nm_done: + jsr full_redraw + jmp main_loop + +; ================================================================ +; INSERT MODE +; ================================================================ +handle_insert: + lda last_key + cmp #CR_CH + beq ins_nl + cmp #DEL_CH + beq ins_bs + cmp #$20 ; ignore control chars below $20 + bcc ins_done + cmp #$7F + bcs ins_done + jsr insert_char ; A = PETSCII char to insert + jsr cursor_right + jmp ins_done +ins_nl: + jsr do_newline + jmp ins_done +ins_bs: + jsr backspace_char +ins_done: + jsr full_redraw + jmp main_loop + +; ================================================================ +; COMMAND MODE +; ================================================================ +handle_cmd: + lda last_key + cmp #CR_CH + beq cmd_exec + cmp #DEL_CH + beq cmd_bs + cmp #$20 + bcc cmd_done + cmp #$7F + bcs cmd_done + ldx cmd_len + cpx #CMD_MAX + bcs cmd_done + sta cmd_buf,x + inc cmd_len + jmp cmd_done +cmd_bs: + lda cmd_len + beq cmd_done + dec cmd_len + jmp cmd_done +cmd_exec: + jsr run_cmd +cmd_done: + jsr full_redraw + jmp main_loop + +; ================================================================ +; EXIT +; ================================================================ +vim_do_exit: + jsr restore_cursor ; de-invert cursor char before leaving + rts ; return to kernal-os-skeleton shell + +; ================================================================ +; COMMAND EXECUTION +; ================================================================ +run_cmd: + lda cmd_len + beq rc_cancel + + ; --- :Q quit --- + cmp #1 + bne rc_not_q + lda cmd_buf + cmp #'Q' + bne rc_not_q + lda #1 + sta quit_flag + lda #MODE_NORMAL + sta cur_mode + rts + +rc_not_q: + ; --- :W [NAME] save --- + lda cmd_buf + cmp #'W' + bne rc_unknown + jsr do_save + lda #MODE_NORMAL + sta cur_mode + rts + +rc_unknown: + ; show "?" in status then return to normal + lda #msg_cmd_err + jsr show_status_text +rc_cancel: + lda #MODE_NORMAL + sta cur_mode + rts + +; ================================================================ +; FILE SAVE +; do_save: reads cmd_buf "W NAME" and writes text to disk +; ================================================================ +do_save: + ; need at least "W N" (3 chars: W, space, one char of name) + lda cmd_len + cmp #3 + bcs ds_len_ok + jmp ds_no_name +ds_len_ok: + lda cmd_buf + 1 + cmp #' ' + beq ds_space_ok + jmp ds_no_name +ds_space_ok: + + ; copy filename from cmd_buf[2..] to save_fn + ldy #2 + ldx #0 +ds_copy_name: + lda cmd_buf,y + beq ds_name_done + cmp #' ' + beq ds_name_done + sta save_fn,x + inx + iny + cpx #15 + bcc ds_copy_name +ds_name_done: + stx save_fn_len + lda #0 + sta save_fn,x + + ; build "0:NAME,S,W" in full_fn + jsr build_save_fname + + ; SETLFS + lda #SAVE_LFN + ldx #SAVE_DEV + ldy #2 + jsr SETLFS + + ; SETNAM + lda full_fn_len + ldx #full_fn + jsr SETNAM + + jsr OPEN + bcc ds_open_ok + ; open error + lda #msg_disk_err + jsr show_status_text + rts + +ds_open_ok: + lda #SAVE_LFN + jsr CHKOUT + bcc ds_write_rows + jsr CLRCH + lda #SAVE_LFN + jsr CLOSE + lda #msg_disk_err + jsr show_status_text + rts + +ds_write_rows: + lda #0 + sta save_row + +ds_row_loop: + ldx save_row + lda row_lo,x + sta ZP_LO + lda row_hi,x + sta ZP_HI + + ; find rightmost non-space column + ldy #SCR_COLS - 1 +ds_find_end: + lda (ZP_LO),y + cmp #$20 + bne ds_row_found + tya + beq ds_row_empty + dey + jmp ds_find_end + +ds_row_empty: + ; empty row: just write CR + lda #CR_CH + jsr CHROUT + jmp ds_row_done + +ds_row_found: + ; write cols 0..Y then CR + sty save_col_end + ldy #0 +ds_write_col: + lda (ZP_LO),y + jsr screen_to_petscii + jsr CHROUT + cpy save_col_end + beq ds_write_cr + iny + jmp ds_write_col + +ds_write_cr: + lda #CR_CH + jsr CHROUT + +ds_row_done: + inc save_row + lda save_row + cmp #SCR_ROWS + bne ds_row_loop + + ; close + jsr CLRCH + lda #SAVE_LFN + jsr CLOSE + + lda #msg_saved + jsr show_status_text + rts + +ds_no_name: + lda #msg_need_name + jsr show_status_text + rts + +; builds "0:save_fn,S,W" into full_fn, length into full_fn_len +build_save_fname: + ldx #0 + lda #'0' + sta full_fn,x + inx + lda #':' + sta full_fn,x + inx + ldy #0 +bsf_loop: + lda save_fn,y + beq bsf_suffix + sta full_fn,x + inx + iny + bne bsf_loop +bsf_suffix: + lda #',' + sta full_fn,x + inx + lda #'S' + sta full_fn,x + inx + lda #',' + sta full_fn,x + inx + lda #'W' + sta full_fn,x + inx + stx full_fn_len + lda #0 + sta full_fn,x + rts + +; ================================================================ +; CURSOR MOVEMENT +; ================================================================ +cursor_left: + lda cur_col + beq cl_wrap + dec cur_col + rts +cl_wrap: + lda cur_row + beq cl_top + dec cur_row + lda #SCR_COLS - 1 + sta cur_col +cl_top: rts + +cursor_right: + lda cur_col + cmp #SCR_COLS - 1 + bcs cr_wrap + inc cur_col + rts +cr_wrap: + lda cur_row + cmp #SCR_ROWS - 1 + bcs cr_bot + inc cur_row + lda #0 + sta cur_col +cr_bot: rts + +cursor_up: + lda cur_row + beq cu_top + dec cur_row +cu_top: rts + +cursor_down: + lda cur_row + cmp #SCR_ROWS - 1 + bcs cd_bot + inc cur_row +cd_bot: rts + +; ================================================================ +; TEXT EDITING +; ================================================================ + +; Set ZP_LO/ZP_HI to the screen RAM base for cur_row +row_ptr: + ldx cur_row + lda row_lo,x + sta ZP_LO + lda row_hi,x + sta ZP_HI + rts + +; Insert PETSCII char (in A) at cursor; shift rest of line right (last char lost) +insert_char: + jsr petscii_to_screen + sta ZP_TMP + jsr row_ptr + ldy #SCR_COLS - 1 +ic_shift: + cpy cur_col + beq ic_write + dey + lda (ZP_LO),y + iny + sta (ZP_LO),y + dey + jmp ic_shift +ic_write: + lda ZP_TMP + ldy cur_col + sta (ZP_LO),y + rts + +; Delete char at cursor; shift rest of line left, fill end with space +delete_char: + jsr row_ptr + ldy cur_col +dc_shift: + cpy #SCR_COLS - 1 + beq dc_space + iny + lda (ZP_LO),y + dey + sta (ZP_LO),y + iny + jmp dc_shift +dc_space: + lda #$20 + sta (ZP_LO),y + rts + +; Backspace: move cursor left then delete +backspace_char: + lda cur_col + beq bs_done + jsr cursor_left + jsr delete_char +bs_done:rts + +; RETURN in insert mode: move to col 0 of next row +do_newline: + lda cur_row + cmp #SCR_ROWS - 1 + bcs nl_done + inc cur_row + lda #0 + sta cur_col +nl_done:rts + +; Open new line below: advance row, clear it, col = 0 +open_line_below: + lda cur_row + cmp #SCR_ROWS - 1 + bcs ol_done + inc cur_row + lda #0 + sta cur_col + jsr clear_current_row +ol_done:rts + +clear_current_row: + jsr row_ptr + ldy #0 + lda #$20 +ccr_lp: sta (ZP_LO),y + iny + cpy #SCR_COLS + bne ccr_lp + rts + +; Clear all 24 text rows to spaces +clear_text: + lda #0 + sta ZP_TMP +ct_loop: + ldx ZP_TMP + lda row_lo,x + sta ZP_LO + lda row_hi,x + sta ZP_HI + ldy #0 + lda #$20 +ct_row: sta (ZP_LO),y + iny + cpy #SCR_COLS + bne ct_row + inc ZP_TMP + lda ZP_TMP + cmp #SCR_ROWS + bne ct_loop + rts + +; ================================================================ +; SCREEN RENDERING +; ================================================================ + +; Restore the saved char at (prev_row, prev_col) +restore_cursor: + ldx prev_row + lda row_lo,x + clc + adc prev_col + sta ZP_LO + lda row_hi,x + adc #0 + sta ZP_HI + lda prev_char + ldy #0 + sta (ZP_LO),y + rts + +; Show cursor by inverting char at (cur_row, cur_col); saves old char +show_cursor: + ldx cur_row + lda row_lo,x + clc + adc cur_col + sta ZP_LO + lda row_hi,x + adc #0 + sta ZP_HI + ldy #0 + lda (ZP_LO),y + sta prev_char + ora #$80 ; set bit 7 = reversed character + sta (ZP_LO),y + lda cur_row + sta prev_row + lda cur_col + sta prev_col + rts + +; Draw status bar at row 24 +draw_status: + ; clear status row + lda row_lo + 24 + sta ZP_LO + lda row_hi + 24 + sta ZP_HI + ldy #0 + lda #$20 +dstat_clr: + sta (ZP_LO),y + iny + cpy #SCR_COLS + bne dstat_clr + + lda cur_mode + cmp #MODE_INSERT + beq dstat_ins + cmp #MODE_CMD + beq dstat_cmd + + ; NORMAL + lda #str_normal + jsr write_str_to_status + rts + +dstat_ins: + lda #str_insert + jsr write_str_to_status + rts + +dstat_cmd: + ; write ":" then cmd_buf + lda row_lo + 24 + sta ZP_LO + lda row_hi + 24 + sta ZP_HI + lda #$3A ; screen code for ':' + ldy #0 + sta (ZP_LO),y + iny + ldx #0 +dstat_cmd_lp: + cpx cmd_len + beq dstat_done + lda cmd_buf,x + jsr petscii_to_screen + sta (ZP_LO),y + iny + inx + cpy #SCR_COLS + bcc dstat_cmd_lp +dstat_done: + rts + +; Write null-terminated PETSCII string (A=lo, Y=hi) to status row +write_str_to_status: + sta STR_LO + sty STR_HI + lda row_lo + 24 + sta ZP_LO + lda row_hi + 24 + sta ZP_HI + ldy #0 +wss_lp: + lda (STR_LO),y + beq wss_done + jsr petscii_to_screen + sta (ZP_LO),y + iny + cpy #SCR_COLS + bcc wss_lp +wss_done: + rts + +; Copy a PETSCII string directly to status (no screen-code conversion) +; Used for messages that already have screen codes (msg_saved etc.) +; Actually we just reuse write_str_to_status since it converts +show_status_text: + jsr write_str_to_status + rts + +; Restore cursor, redraw cursor, redraw status bar +full_redraw: + jsr restore_cursor + jsr show_cursor + jsr draw_status + rts + +; ================================================================ +; INIT +; ================================================================ +vim_init: + lda #0 + sta cur_row + sta cur_col + sta prev_row + sta prev_col + sta cur_mode + sta cmd_len + sta quit_flag + sta save_row + sta save_col_end + lda #$20 ; cursor starts on a space + sta prev_char + jsr clear_text + rts + +; ================================================================ +; KEY INPUT +; ================================================================ +get_key: +gk_loop: + jsr STOPKEY + bne gk_getin + lda #$FF ; RUN/STOP = special exit code + rts +gk_getin: + jsr GETIN + beq gk_loop ; no key yet, keep polling + rts + +; ================================================================ +; CHARACTER CONVERSION +; ================================================================ + +; PETSCII → screen code +; $41-$5A (A-Z unshifted) → $01-$1A (subtract $40) +; $61-$7A (a-z lowercase) → $01-$1A (subtract $60) +; $20-$3F (space,nums,punct) → same +; others: pass through +petscii_to_screen: + cmp #$41 + bcc pts_pass + cmp #$5B + bcc pts_upper + cmp #$61 + bcc pts_pass + cmp #$7B + bcc pts_lower +pts_pass: + rts +pts_upper: + sec + sbc #$40 + rts +pts_lower: + sec + sbc #$60 + rts + +; screen code → PETSCII (inverse of above) +; $01-$1A → $41-$5A (add $40) +; $20-$3F → same +; $00 → '@' +; others: return space $20 +screen_to_petscii: + cmp #$01 + bcc stp_at + cmp #$1B + bcc stp_letter + cmp #$20 + bcc stp_space + cmp #$40 + bcc stp_pass +stp_space: + lda #$20 + rts +stp_at: + lda #$40 ; '@' + rts +stp_letter: + clc + adc #$40 + rts +stp_pass: + rts + +; ================================================================ +; RODATA +; ================================================================ +.segment "RODATA" + +str_normal: + .byte "NORMAL (hjkl=move i=ins x=del :=cmd)", 0 + +str_insert: + .byte "INSERT (type text, DEL=bksp, STOP=normal)", 0 + +msg_cmd_err: + .byte "? unknown command", 0 + +msg_saved: + .byte "saved.", 0 + +msg_disk_err: + .byte "disk error.", 0 + +msg_need_name: + .byte "usage: :w filename", 0 + +; Row-base lookup tables (25 entries: rows 0-23 = text, row 24 = status) +row_lo: + .byte <(SCRRAM + 0*40), <(SCRRAM + 1*40), <(SCRRAM + 2*40) + .byte <(SCRRAM + 3*40), <(SCRRAM + 4*40), <(SCRRAM + 5*40) + .byte <(SCRRAM + 6*40), <(SCRRAM + 7*40), <(SCRRAM + 8*40) + .byte <(SCRRAM + 9*40), <(SCRRAM + 10*40), <(SCRRAM + 11*40) + .byte <(SCRRAM + 12*40), <(SCRRAM + 13*40), <(SCRRAM + 14*40) + .byte <(SCRRAM + 15*40), <(SCRRAM + 16*40), <(SCRRAM + 17*40) + .byte <(SCRRAM + 18*40), <(SCRRAM + 19*40), <(SCRRAM + 20*40) + .byte <(SCRRAM + 21*40), <(SCRRAM + 22*40), <(SCRRAM + 23*40) + .byte <(SCRRAM + 24*40) + +row_hi: + .byte >(SCRRAM + 0*40), >(SCRRAM + 1*40), >(SCRRAM + 2*40) + .byte >(SCRRAM + 3*40), >(SCRRAM + 4*40), >(SCRRAM + 5*40) + .byte >(SCRRAM + 6*40), >(SCRRAM + 7*40), >(SCRRAM + 8*40) + .byte >(SCRRAM + 9*40), >(SCRRAM + 10*40), >(SCRRAM + 11*40) + .byte >(SCRRAM + 12*40), >(SCRRAM + 13*40), >(SCRRAM + 14*40) + .byte >(SCRRAM + 15*40), >(SCRRAM + 16*40), >(SCRRAM + 17*40) + .byte >(SCRRAM + 18*40), >(SCRRAM + 19*40), >(SCRRAM + 20*40) + .byte >(SCRRAM + 21*40), >(SCRRAM + 22*40), >(SCRRAM + 23*40) + .byte >(SCRRAM + 24*40) + +; ================================================================ +; BSS (zero-initialised by the OS / KERNAL CINT on cold start; +; vim_init zeros what it cares about explicitly) +; ================================================================ +.segment "BSS" + +cur_row: .res 1 +cur_col: .res 1 +prev_row: .res 1 +prev_col: .res 1 +prev_char: .res 1 ; screen code saved before cursor inversion +cur_mode: .res 1 +cmd_len: .res 1 +cmd_buf: .res CMD_MAX + 1 +last_key: .res 1 +quit_flag: .res 1 +save_row: .res 1 +save_col_end: .res 1 +save_fn: .res 16 ; extracted filename (no path) +save_fn_len: .res 1 +full_fn: .res 24 ; "0:filename,S,W" +full_fn_len: .res 1 diff --git a/samples/kernal-os-skeleton/vim.cfg b/samples/kernal-os-skeleton/vim.cfg new file mode 100644 index 0000000..93b7bfc --- /dev/null +++ b/samples/kernal-os-skeleton/vim.cfg @@ -0,0 +1,11 @@ +MEMORY { + LOADADDR: start = $1FFE, size = $0002, type = ro, file = %O; + MAIN: start = $2000, size = $7FFF, type = rw, file = %O; +} + +SEGMENTS { + LOADADDR: load = LOADADDR, type = ro; + CODE: load = MAIN, type = ro; + RODATA: load = MAIN, type = ro; + BSS: load = MAIN, type = bss, define = yes; +} diff --git a/samples/kernal-os-skeleton/vim.o b/samples/kernal-os-skeleton/vim.o new file mode 100644 index 0000000..8719f20 Binary files /dev/null and b/samples/kernal-os-skeleton/vim.o differ diff --git a/samples/kernal-os-skeleton/vim.prg b/samples/kernal-os-skeleton/vim.prg new file mode 100644 index 0000000..a5ee53c Binary files /dev/null and b/samples/kernal-os-skeleton/vim.prg differ diff --git a/skirmish-game/IMPLEMENTATION.md b/skirmish-game/IMPLEMENTATION.md new file mode 100644 index 0000000..41606a0 --- /dev/null +++ b/skirmish-game/IMPLEMENTATION.md @@ -0,0 +1,118 @@ +# Implementation Progress — Skirmish (C64) + +## Status: BUILDABLE MVP BASELINE + +Last updated: 2026-05-28 + +--- + +## What Exists Now + +### Project structure + +``` +skirmish-game/ +├── src/ +│ ├── skirmish.asm # Entry point, raster sync, shared state +│ ├── input.asm # Keyboard + joystick polling +│ ├── game-logic.asm # Turn flow, movement, combat, occupancy +│ ├── enemy-ai.asm # Closest-target AI and movement +│ ├── render.asm # Sprites, HUD, board markers +│ ├── data.asm # Tables, text, sprite bitmaps +│ ├── skirmish.inc # Shared constants +│ └── c64.cfg # ld65 memory layout +├── docs/ +│ ├── GDD.md +│ ├── TECHNICAL-PLAN.md +│ └── C64-CHEATSHEET.md +├── build.bat +├── run-vice.bat +├── debug-vice.bat +├── Makefile +├── README.md +└── skirmish.prg +``` + +### Core gameplay implemented + +- 8x8 tactical board with coordinate markers +- 3 player units and 3 enemy units +- Single-step turn flow: + - player selects a unit + - player moves or attacks + - enemy team takes its turn + - win/lose is checked +- Adjacent melee combat for both teams +- Unit HP tracking and death removal +- Greedy enemy AI using Manhattan distance +- Selected-unit highlight via sprite color +- Restart flow after win or loss + +### Controls implemented + +- `Q` or joystick fire: cycle selected player unit +- `W`, `A`, `S`, `D` or joystick directions: move or attack +- `E` or space: end turn +- `R`: restart after game over + +### Engine and rendering implemented + +- BASIC stub starts game with `RUN` -> `SYS 2061` +- Raster-driven frame sync installed through IRQ vector +- Main loop updates once per IRQ tick +- 6 hardware sprites used for units +- HUD shows current phase, turn count, selected unit, and all unit HP +- Sprite bitmaps and lookup tables are linked into C64-visible RAM +- Occupancy grid rebuilt from unit state to avoid stale collision state + +--- + +## Build Verification + +### Confirmed + +- `build.bat` assembles and links successfully with local `ca65` and `ld65` +- Output artifact currently builds as `skirmish.prg` + +### Current artifact + +- `skirmish.prg` size: 2617 bytes +- Most recent successful local build: 2026-05-28 + +### Not yet verified + +- Full gameplay smoke test inside VICE +- Sprite alignment and board readability in the live emulator +- Joystick port 2 behavior in VICE input mapping +- End-to-end win/lose flow by playtesting + +--- + +## Known Gaps + +### High priority + +1. Run the build in `x64sc` and verify the boot path, controls, and turn flow. +2. Confirm the raster-driven loop behaves correctly on a real VICE session. +3. Tune board readability if the current dot-grid is too sparse under sprites. + +### Medium priority + +1. Add attack and death feedback such as flash or simple SID beeps. +2. Add a stronger title/opening screen. +3. Add a clearer wait indicator and maybe a per-unit action prompt. + +### Low priority + +1. Add obstacles and richer pathing. +2. Add ranged attacks or special abilities. +3. Add difficulty scaling or multiple waves. + +--- + +## Notes + +- The previous tracker text described files that did not actually exist yet. +- The source tree and build scripts now exist and match this document. +- The PRG boot path was corrected so the BASIC stub jumps to the real contiguous load address. +- The next milestone should be emulator validation and gameplay polish, not project scaffolding. diff --git a/skirmish-game/Makefile b/skirmish-game/Makefile new file mode 100644 index 0000000..4f355a7 --- /dev/null +++ b/skirmish-game/Makefile @@ -0,0 +1,30 @@ +CA65 ?= ca65 +LD65 ?= ld65 + +OBJS = skirmish.o input.o game-logic.o enemy-ai.o render.o data.o + +all: skirmish.prg + +skirmish.prg: $(OBJS) + $(LD65) $(OBJS) -C src/c64.cfg -o $@ + +skirmish.o: src/skirmish.asm src/skirmish.inc + $(CA65) src/skirmish.asm -o $@ + +input.o: src/input.asm src/skirmish.inc + $(CA65) src/input.asm -o $@ + +game-logic.o: src/game-logic.asm src/skirmish.inc + $(CA65) src/game-logic.asm -o $@ + +enemy-ai.o: src/enemy-ai.asm src/skirmish.inc + $(CA65) src/enemy-ai.asm -o $@ + +render.o: src/render.asm src/skirmish.inc + $(CA65) src/render.asm -o $@ + +data.o: src/data.asm src/skirmish.inc + $(CA65) src/data.asm -o $@ + +clean: + rm -f $(OBJS) skirmish.prg diff --git a/skirmish-game/README.md b/skirmish-game/README.md new file mode 100644 index 0000000..84ab389 --- /dev/null +++ b/skirmish-game/README.md @@ -0,0 +1,48 @@ +# Skirmish + +`Skirmish` is a single-screen C64 tactical prototype built in 6510 assembly with `ca65`/`ld65`. + +## Controls + +- `Q` or joystick fire: cycle the selected player unit +- `W`, `A`, `S`, `D` or joystick directions: move or attack +- `E` or space: end your turn without moving +- `R`: restart after a win or loss + +## Build + +### Windows + +```bat +build.bat +``` + +### Cross-platform + +```bash +make +``` + +## Run + +### Normal + +```bat +run-vice.bat +``` + +If you load it manually in BASIC, use `RUN`. The built PRG contains a BASIC stub that executes `SYS 2061`. + +### With monitor + +```bat +debug-vice.bat +``` + +## Current MVP + +- 3 player units and 3 enemy units on an 8x8 board +- Keyboard and joystick input +- Single-step movement or adjacent melee attacks +- Greedy enemy AI using Manhattan distance +- Win/lose flow with restart diff --git a/skirmish-game/build.bat b/skirmish-game/build.bat new file mode 100644 index 0000000..108f196 --- /dev/null +++ b/skirmish-game/build.bat @@ -0,0 +1,13 @@ +@echo off +setlocal +cd /d "%~dp0" + +ca65 src\skirmish.asm -o skirmish.o || exit /b 1 +ca65 src\input.asm -o input.o || exit /b 1 +ca65 src\game-logic.asm -o game-logic.o || exit /b 1 +ca65 src\enemy-ai.asm -o enemy-ai.o || exit /b 1 +ca65 src\render.asm -o render.o || exit /b 1 +ca65 src\data.asm -o data.o || exit /b 1 +ld65 skirmish.o input.o game-logic.o enemy-ai.o render.o data.o -C src\c64.cfg -o skirmish.prg || exit /b 1 + +echo Built skirmish.prg diff --git a/skirmish-game/data.o b/skirmish-game/data.o new file mode 100644 index 0000000..b60b0af Binary files /dev/null and b/skirmish-game/data.o differ diff --git a/skirmish-game/debug-vice.bat b/skirmish-game/debug-vice.bat new file mode 100644 index 0000000..3650a15 --- /dev/null +++ b/skirmish-game/debug-vice.bat @@ -0,0 +1,6 @@ +@echo off +setlocal +cd /d "%~dp0" + +call build.bat || exit /b 1 +"C:\Program Files\GTK3VICE-3.10-win64\bin\x64sc.exe" -autostart skirmish.prg -autostart-warp -nativemonitor -keepmonopen -refreshonbreak diff --git a/skirmish-game/docs/C64-CHEATSHEET.md b/skirmish-game/docs/C64-CHEATSHEET.md new file mode 100644 index 0000000..01ff47a --- /dev/null +++ b/skirmish-game/docs/C64-CHEATSHEET.md @@ -0,0 +1,333 @@ +# C64 6502 Assembly Cheat Sheet + +--- + +## 1. Memory Map + +``` +$0000–$00FF Zero page (fast access, 2-byte instructions) +$0100–$01FF Stack (grows downward from $01FF) +$0200–$1FFF System/free area +$2000–$3FFF User RAM (40 KB available) +$D000–$DFFF I/O ports (VIC-II, SID, CIA1, CIA2) +$E000–$FFFF Kernal ROM (can be banked out for more RAM) +``` + +--- + +## 2. Key VIC-II Registers + +All addresses relative to $D000 (e.g., $D010 = $D000 + $10). + +### Sprite Coordinates + +``` +$D000–$D00F Sprite X positions (16 bytes, 2 per sprite) + $D000 = Sprite 0 X (low byte) + $D010 = X high bits (bit 0 = Sprite 0 X bit 8, etc.) + +$D001–$D00F Sprite Y positions (16 bytes, 2 per sprite) + $D001 = Sprite 0 Y + $D003 = Sprite 1 Y + ... (every other byte) +``` + +### Sprite Control + +``` +$D015 Sprite enable ($00 = all off, $3F = all 6 on) +$D017 Sprite Y expand (1 = double height) +$D01D Sprite X expand (1 = double width) +$D025 Sprite 0 color +$D026 Sprite 1 color +... (one byte per sprite) + +$D027 Sprite 0 multicolor (0–3 for multicolor mode) +$D028 Sprite 1 multicolor +... +$D01C Multicolor mode enable (bit = sprite index) +``` + +### Screen Control + +``` +$D011 VIC control register 1 + Bit 7: Raster interrupt request + Bit 6: Extended color text mode + Bit 5: Bitmap mode + Bit 4: Blank screen (1 = blank) + Bits 3–0: Scroll Y + +$D012 Raster line (read for current, write for interrupt trigger) + +$D016 VIC control register 2 (scroll X, multicolor, etc.) + +$D018 Memory control (screen addr, bitmap addr) + Default: $14 (screen at $0400, bitmap at $2000) +``` + +### Colors + +``` +$D020 Border color +$D021 Background color 0 +$D022 Background color 1 (multicolor mode) +$D023 Background color 2 (multicolor mode) +$D024 Background color 3 (multicolor mode) +``` + +--- + +## 3. CIA (Complex Interface Adapter) — Input + +### CIA1 ($DC00–$DC0F) + +``` +$DC00 Data direction register A (keyboard columns) +$DC01 Keyboard matrix input register + Bit 0 = Key Row + Bit 1 = Key Row, etc. + +$DC02 Data direction register B +$DC03 Joystick / paddle input + +$DC04–$DC05 Timer A (low/high) +$DC06–$DC07 Timer B (low/high) +$DC0E Control register A (timer, interrupt) +$DC0F Interrupt enable register +``` + +### Simple Joystick Read + +``` +LDA #$00 ; Set port B for input +STA $DC02 +LDA $DC01 ; Read joystick (port 2) or keyboard +; Bit pattern: 7=up, 6=down, 5=left, 4=right, 3=fire +``` + +--- + +## 4. Common 6502 Instructions + +### Load/Store + +```asm +LDA #$42 ; Load immediate +LDA $42 ; Load from zero page +LDA $4200 ; Load from absolute address +LDA $4200,X ; Indexed +LDA ($42,X) ; Indirect indexed +STA $42 ; Store +``` + +### Arithmetic + +```asm +ADC #$10 ; Add with carry +SBC #$10 ; Subtract with carry +INC $42 ; Increment memory +DEC $42 ; Decrement memory +INX, INY, DEX, DEY ; Increment/decrement registers +``` + +### Branching + +```asm +BNE $4000 ; Branch if not equal (after CMP, etc.) +BEQ $4000 ; Branch if equal +BCS $4000 ; Branch if carry set +BCC $4000 ; Branch if carry clear +BPL $4000 ; Branch if plus (MSB = 0) +BMI $4000 ; Branch if minus (MSB = 1) +JMP $4000 ; Unconditional jump +JSR $4000 ; Jump to subroutine (push return address) +RTS ; Return from subroutine +``` + +### Comparisons + +```asm +CMP #$10 ; Compare A with immediate (sets flags) +CMP $42 ; Compare A with memory +CPX #$10 ; Compare X +CPY #$10 ; Compare Y +; Flags: Z=zero (equal), C=carry (unsigned <), N=minus +``` + +### Bit Operations + +```asm +ASL ; Arithmetic shift left (A or M) +LSR ; Logical shift right +ROL ; Rotate left +ROR ; Rotate right +AND #$0F ; Bitwise AND +ORA #$F0 ; Bitwise OR +EOR #$FF ; Bitwise XOR +BIT $42 ; Test bits (sets N, V, Z) +``` + +### Stack + +```asm +PHA ; Push A +PLA ; Pull A +PHP ; Push status +PLP ; Pull status +``` + +--- + +## 5. Interrupt Handling + +### Setup IRQ Handler + +```asm +; In initialization: +LDA #IRQ_HANDLER ; High byte +STA $0315 + +CLI ; Clear interrupt disable flag +``` + +### IRQ Handler Structure + +```asm +IRQ_HANDLER: + PHA ; Save registers + TXA + PHA + TYA + PHA + + ; Check if it's our interrupt (raster or timer) + LDA $D019 ; VIC interrupt status + BIT #$01 ; Test raster interrupt + BEQ NOT_VIC + + ; Handle VIC/raster interrupt + ; (your game logic here) + + LDA #$01 ; Acknowledge interrupt + STA $D019 + +NOT_VIC: + PLA ; Restore registers + TAY + PLA + TAX + PLA + RTI +``` + +--- + +## 6. Sprite Data Format + +C64 sprites are 24 pixels wide × 21 pixels high, stored as 63 bytes (3 bytes/line). + +**Location:** Default at $3000–$3FFF (can be changed via $D018) + +**Format:** +``` +Bytes 0–2: Line 0 (pixel data) +Bytes 3–5: Line 1 +... +Bytes 60–62: Line 20 +``` + +Each 3-byte line = 24 pixels (bit pattern). + +**Example: Simple square** +``` +$38 $38 $00 (11000000 11000000 00000000 = two vertical bars) +$38 $38 $00 +... +``` + +--- + +## 7. Useful Zero-Page Patterns + +```asm +; Temporary registers (safe to overwrite) +TEMP0 = $F0 +TEMP1 = $F1 +TEMP2 = $F2 +TEMP3 = $F3 + +; Loop counters +LOOP_X = $F4 +LOOP_Y = $F5 +LOOP_C = $F6 + +; Pointers (16-bit address) +PTR0 = $F8 ; PTR0_L = $F8, PTR0_H = $F9 +PTR1 = $FA +``` + +--- + +## 8. Quick Patterns + +### Wait for Raster (Sync to VIC) + +```asm +WAIT_RASTER: + LDA # Player input → select/move/attack + 1 => Animate move, resolve collision/damage + 2 => Enemy AI, animate enemy moves + 3 => Check win/lose, advance state + + ; 3. Render sprites & text + JSR SPRITE_UPDATE + JSR TEXT_UPDATE + + Restore registers + RTI +``` + +--- + +## 6. Subroutine Checklist + +### Input & Selection + +| Name | In | Out | Cost | +|------|----|----|------| +| `INPUT_READ` | — | `$30` (key code) | 10 cyc | +| `UNIT_SELECT` | `$30` (key) | `$32` (unit) | 20 cyc | +| `UNIT_MOVE` | `$32`, `$58`, `$59` | `$5A` (valid) | 40 cyc | +| `UNIT_ATTACK` | `$32`, target unit | HP update | 30 cyc | + +### Game Logic + +| Name | In | Out | Cost | +|------|----|----|------| +| `FIND_CLOSEST_ENEMY` | `$32` (player unit) | `$F0` (enemy idx) | 50 cyc | +| `ENEMY_MOVE` | `$F0` (enemy idx) | Update unit pos | 80 cyc | +| `ENEMY_AI` | — | Execute all enemies | 300 cyc | +| `CHECK_WIN` | — | Set `GAME_STATE` | 20 cyc | + +### Rendering + +| Name | In | Out | Cost | +|------|----|----|------| +| `SPRITE_UPDATE` | Unit state | VIC-II regs | 100 cyc | +| `TEXT_UPDATE` | Game state | Screen RAM | 50 cyc | +| `REBUILD_OCCUPANCY` | Unit state | Occupancy array | 60 cyc | + +--- + +## 7. Key Algorithms + +### Manhattan Distance + +```asm +; Input: X in $F0, Y in $F1 (target); A (own X), $F2 (own Y) +; Output: A (distance) +MANHATTAN: + SEC + SBC $F0 ; A = own_x - target_x + BPL + ; If positive, skip + EOR #$FF + ADC #$01 ; Negate (two's complement) ++ STA $F3 ; Save |dx| + + LDA $F2 + SEC + SBC $F1 ; A = own_y - target_y + BPL + ; If positive, skip + EOR #$FF + ADC #$01 ; Negate ++ CLC + ADC $F3 ; A = |dx| + |dy| + RTS +``` + +### Grid Occupancy Check + +```asm +; Input: X in $F0, Y in $F1 +; Output: A = unit index (or $FF if empty) +GRID_OCCUPIED: + LDA $F1 + ASL + ASL + ASL ; A = Y * 8 + CLC + ADC $F0 ; A = Y*8 + X + TAX + LDA $6000,X ; Lookup in occupancy array + RTS +``` + +--- + +## 8. State Transitions + +``` +WAITING_FOR_INPUT + ↓ [Player selects unit] + ↓ [Player moves/attacks] +EXECUTING_MOVE + ↓ [Move resolves, damage applied] +ENEMY_TURN + ↓ [Each enemy acts] + ↓ [Wait for animations] +CHECK_WIN + ↓ [If enemy count = 0 or player count = 0] +GAME_OVER (win/lose) + ↓ [Display result, wait for restart key] +WAITING_FOR_INPUT [loop] +``` + +--- + +## 9. Build & Run + +**Development tools:** +- **cc65** (C-to-6502 compiler, includes assembler `ca65` and linker `ld65`) +- **VICE** (Commodore 64 emulator, with debugger) +- **Makefile** or shell script to assemble & link + +**Command sequence:** +```bash +ca65 main.asm -o main.o # Assemble +ld65 main.o -C c64.cfg -o game.prg # Link +x64sc game.prg # Run in VICE +``` + +--- + +## 10. Implementation Order (MVP Priority) + +1. **Kernel & Interrupt Handler** — Get IRQ handler ticking +2. **Sprite Rendering** — Display 6 units on screen +3. **Grid & Occupancy** — Manage unit positions +4. **Input & Unit Selection** — Player can select a unit +5. **Movement & Collision** — Player can move units (no combat yet) +6. **Combat & Damage** — Attack resolves HP +7. **Enemy AI** — Simple greedy pathfinding +8. **Win/Lose Detection** — Game ends correctly +9. **Polish** — Text output, feedback, cleanup + +--- + +## 11. Known Pitfalls & Mitigations + +| Risk | Mitigation | +|------|-----------| +| Interrupt handler exceeds raster line | Use raster-stable jitter removal; keep subroutines cycle-predictable | +| Sprite overlap (>8 on screen) | Multiplex later; for now, 6 sprites fit without overlap | +| Occupancy array corruption | Rebuild every frame from unit state; don't write directly | +| AI hangs/slowdown | Limit enemy AI to 1 move each; use lookup tables for distance | +| Collision bugs | Test grid occupancy before allowing move; confirm unit dies at 0 HP | + +--- + +## 12. File Structure + +``` +skirmish-game/ + src/ + main.asm # Entry point, IRQ handler, main loop + input.asm # Input polling, key handling + game-logic.asm # Unit selection, movement, combat + enemy-ai.asm # Pathfinding, enemy turn + render.asm # Sprite updates, text output + data.asm # Lookup tables, sprite/tile data + assets/ + sprites.chr # C64 sprite design (binary) + tiles.chr # Tile data (if used) + docs/ + GDD.md # Game design (this file) + TECHNICAL-PLAN.md # Technical details (this file) + C64-CHEATSHEET.md # Quick register/memory reference + Makefile # Build script + c64.cfg # cc65 linker config (if using cc65) + README.md # Build & run instructions +``` + +--- + +## 13. Next Steps + +1. **Choose assembler:** cc65 + ca65/ld65 (recommended) or ACME +2. **Set up build environment** on your machine +3. **Write skeleton main.asm** (entry point + IRQ stub) +4. **Create sprite test** (render 6 colored squares) +5. **Implement zero-page variables** +6. **Add input handler** (keyboard or joystick) +7. **Iterate gameplay logic** + diff --git a/skirmish-game/enemy-ai.o b/skirmish-game/enemy-ai.o new file mode 100644 index 0000000..923c31a Binary files /dev/null and b/skirmish-game/enemy-ai.o differ diff --git a/skirmish-game/game-logic.o b/skirmish-game/game-logic.o new file mode 100644 index 0000000..7933a93 Binary files /dev/null and b/skirmish-game/game-logic.o differ diff --git a/skirmish-game/input.o b/skirmish-game/input.o new file mode 100644 index 0000000..0251555 Binary files /dev/null and b/skirmish-game/input.o differ diff --git a/skirmish-game/render.o b/skirmish-game/render.o new file mode 100644 index 0000000..be89bbd Binary files /dev/null and b/skirmish-game/render.o differ diff --git a/skirmish-game/run-vice.bat b/skirmish-game/run-vice.bat new file mode 100644 index 0000000..a7a91c4 --- /dev/null +++ b/skirmish-game/run-vice.bat @@ -0,0 +1,6 @@ +@echo off +setlocal +cd /d "%~dp0" + +call build.bat || exit /b 1 +"C:\Program Files\GTK3VICE-3.10-win64\bin\x64sc.exe" -autostart skirmish.prg -autostart-warp diff --git a/skirmish-game/skirmish.o b/skirmish-game/skirmish.o new file mode 100644 index 0000000..b0642ec Binary files /dev/null and b/skirmish-game/skirmish.o differ diff --git a/skirmish-game/skirmish.prg b/skirmish-game/skirmish.prg new file mode 100644 index 0000000..98a3c95 Binary files /dev/null and b/skirmish-game/skirmish.prg differ diff --git a/skirmish-game/src/c64.cfg b/skirmish-game/src/c64.cfg new file mode 100644 index 0000000..23059cd --- /dev/null +++ b/skirmish-game/src/c64.cfg @@ -0,0 +1,19 @@ +MEMORY { + ZP: start = $0030, size = $00D0, type = rw, define = yes; + LOADADDR: start = $07FF, size = $0002, type = rw, file = %O; + RAM: start = $0801, size = $17FF, type = rw, file = %O; + OCCUPY: start = $2000, size = $0040, type = rw, define = yes; + BSSRAM: start = $2040, size = $0200, type = rw, define = yes; +} + +SEGMENTS { + ZEROPAGE: load = ZP, type = zp; + LOADADDR: load = LOADADDR, type = ro; + EXEHDR: load = RAM, type = ro; + CODE: load = RAM, type = ro; + RODATA: load = RAM, type = ro; + DATA: load = RAM, type = rw; + SPRITES: load = RAM, type = ro, align = $40; + OCCUPANCY: load = OCCUPY, type = bss, define = yes; + BSS: load = BSSRAM, type = bss, define = yes; +} diff --git a/skirmish-game/src/data.asm b/skirmish-game/src/data.asm new file mode 100644 index 0000000..90795e2 --- /dev/null +++ b/skirmish-game/src/data.asm @@ -0,0 +1,132 @@ +.include "skirmish.inc" + +.export grid_x_low +.export grid_x_msb +.export grid_y_pos +.export sprite_bits +.export screen_row_lo +.export screen_row_hi +.export color_row_lo +.export color_row_hi + +.export title_text +.export player_turn_text +.export enemy_turn_text +.export win_text +.export lose_text +.export controls_text +.export turn_text +.export select_text +.export player_label_text +.export enemy_label_text + +.export player_sprite +.export enemy_sprite + +.segment "RODATA" +grid_x_low: + .byte 60, 92, 124, 156, 188, 220, 252, 28 +grid_x_msb: + .byte 0, 0, 0, 0, 0, 0, 0, 1 +grid_y_pos: + .byte 60, 81, 102, 123, 144, 165, 186, 207 + +sprite_bits: + .byte $01, $02, $04, $08, $10, $20 + +screen_row_lo: + .byte <(SCREEN_RAM + 40 * 0), <(SCREEN_RAM + 40 * 1), <(SCREEN_RAM + 40 * 2), <(SCREEN_RAM + 40 * 3), <(SCREEN_RAM + 40 * 4) + .byte <(SCREEN_RAM + 40 * 5), <(SCREEN_RAM + 40 * 6), <(SCREEN_RAM + 40 * 7), <(SCREEN_RAM + 40 * 8), <(SCREEN_RAM + 40 * 9) + .byte <(SCREEN_RAM + 40 * 10), <(SCREEN_RAM + 40 * 11), <(SCREEN_RAM + 40 * 12), <(SCREEN_RAM + 40 * 13), <(SCREEN_RAM + 40 * 14) + .byte <(SCREEN_RAM + 40 * 15), <(SCREEN_RAM + 40 * 16), <(SCREEN_RAM + 40 * 17), <(SCREEN_RAM + 40 * 18), <(SCREEN_RAM + 40 * 19) + .byte <(SCREEN_RAM + 40 * 20), <(SCREEN_RAM + 40 * 21), <(SCREEN_RAM + 40 * 22), <(SCREEN_RAM + 40 * 23), <(SCREEN_RAM + 40 * 24) +screen_row_hi: + .byte >(SCREEN_RAM + 40 * 0), >(SCREEN_RAM + 40 * 1), >(SCREEN_RAM + 40 * 2), >(SCREEN_RAM + 40 * 3), >(SCREEN_RAM + 40 * 4) + .byte >(SCREEN_RAM + 40 * 5), >(SCREEN_RAM + 40 * 6), >(SCREEN_RAM + 40 * 7), >(SCREEN_RAM + 40 * 8), >(SCREEN_RAM + 40 * 9) + .byte >(SCREEN_RAM + 40 * 10), >(SCREEN_RAM + 40 * 11), >(SCREEN_RAM + 40 * 12), >(SCREEN_RAM + 40 * 13), >(SCREEN_RAM + 40 * 14) + .byte >(SCREEN_RAM + 40 * 15), >(SCREEN_RAM + 40 * 16), >(SCREEN_RAM + 40 * 17), >(SCREEN_RAM + 40 * 18), >(SCREEN_RAM + 40 * 19) + .byte >(SCREEN_RAM + 40 * 20), >(SCREEN_RAM + 40 * 21), >(SCREEN_RAM + 40 * 22), >(SCREEN_RAM + 40 * 23), >(SCREEN_RAM + 40 * 24) + +color_row_lo: + .byte <(COLOR_RAM + 40 * 0), <(COLOR_RAM + 40 * 1), <(COLOR_RAM + 40 * 2), <(COLOR_RAM + 40 * 3), <(COLOR_RAM + 40 * 4) + .byte <(COLOR_RAM + 40 * 5), <(COLOR_RAM + 40 * 6), <(COLOR_RAM + 40 * 7), <(COLOR_RAM + 40 * 8), <(COLOR_RAM + 40 * 9) + .byte <(COLOR_RAM + 40 * 10), <(COLOR_RAM + 40 * 11), <(COLOR_RAM + 40 * 12), <(COLOR_RAM + 40 * 13), <(COLOR_RAM + 40 * 14) + .byte <(COLOR_RAM + 40 * 15), <(COLOR_RAM + 40 * 16), <(COLOR_RAM + 40 * 17), <(COLOR_RAM + 40 * 18), <(COLOR_RAM + 40 * 19) + .byte <(COLOR_RAM + 40 * 20), <(COLOR_RAM + 40 * 21), <(COLOR_RAM + 40 * 22), <(COLOR_RAM + 40 * 23), <(COLOR_RAM + 40 * 24) +color_row_hi: + .byte >(COLOR_RAM + 40 * 0), >(COLOR_RAM + 40 * 1), >(COLOR_RAM + 40 * 2), >(COLOR_RAM + 40 * 3), >(COLOR_RAM + 40 * 4) + .byte >(COLOR_RAM + 40 * 5), >(COLOR_RAM + 40 * 6), >(COLOR_RAM + 40 * 7), >(COLOR_RAM + 40 * 8), >(COLOR_RAM + 40 * 9) + .byte >(COLOR_RAM + 40 * 10), >(COLOR_RAM + 40 * 11), >(COLOR_RAM + 40 * 12), >(COLOR_RAM + 40 * 13), >(COLOR_RAM + 40 * 14) + .byte >(COLOR_RAM + 40 * 15), >(COLOR_RAM + 40 * 16), >(COLOR_RAM + 40 * 17), >(COLOR_RAM + 40 * 18), >(COLOR_RAM + 40 * 19) + .byte >(COLOR_RAM + 40 * 20), >(COLOR_RAM + 40 * 21), >(COLOR_RAM + 40 * 22), >(COLOR_RAM + 40 * 23), >(COLOR_RAM + 40 * 24) + +title_text: + .byte 19, 11, 9, 18, 13, 9, 19, 8, 0 +player_turn_text: + .byte 16, 12, 1, 25, 5, 18, 32, 20, 21, 18, 14, 0 +enemy_turn_text: + .byte 5, 14, 5, 13, 25, 32, 20, 8, 9, 14, 11, 9, 14, 7, 0 +win_text: + .byte 25, 15, 21, 32, 23, 9, 14, 0 +lose_text: + .byte 7, 1, 13, 5, 32, 15, 22, 5, 18, 0 +controls_text: + .byte 17, 32, 19, 5, 12, 32, 23, 1, 19, 4, 32, 13, 15, 22, 5, 32, 5, 32, 23, 1, 9, 20, 32, 18, 32, 18, 5, 19, 20, 1, 18, 20, 0 +turn_text: + .byte 20, 21, 18, 14, 32, 0 +select_text: + .byte 19, 5, 12, 32, 0 +player_label_text: + .byte 16, 0 +enemy_label_text: + .byte 5, 0 + +.segment "SPRITES" +.align 64 +player_sprite: + .byte $00, $18, $00 + .byte $00, $3C, $00 + .byte $00, $7E, $00 + .byte $00, $FF, $00 + .byte $01, $FF, $80 + .byte $03, $FF, $C0 + .byte $07, $FF, $E0 + .byte $0F, $FF, $F0 + .byte $1F, $FF, $F8 + .byte $3F, $FF, $FC + .byte $3F, $7E, $FC + .byte $1E, $3C, $78 + .byte $0C, $18, $30 + .byte $0C, $18, $30 + .byte $1E, $3C, $78 + .byte $3F, $24, $FC + .byte $33, $24, $CC + .byte $21, $24, $84 + .byte $01, $C3, $80 + .byte $00, $C3, $00 + .byte $00, $42, $00 + .byte $00 + +enemy_sprite: + .byte $00, $00, $00 + .byte $03, $C3, $C0 + .byte $07, $E7, $E0 + .byte $0F, $FF, $F0 + .byte $1E, $7E, $78 + .byte $3C, $3C, $3C + .byte $78, $18, $1E + .byte $F0, $00, $0F + .byte $E0, $00, $07 + .byte $C3, $81, $C3 + .byte $C7, $FF, $E3 + .byte $EF, $FF, $F7 + .byte $7F, $FF, $FE + .byte $3F, $FF, $FC + .byte $1F, $FF, $F8 + .byte $0F, $7E, $F0 + .byte $06, $24, $60 + .byte $0C, $3C, $30 + .byte $18, $24, $18 + .byte $30, $00, $0C + .byte $00, $00, $00 + .byte $00 diff --git a/skirmish-game/src/enemy-ai.asm b/skirmish-game/src/enemy-ai.asm new file mode 100644 index 0000000..4ec3f5a --- /dev/null +++ b/skirmish-game/src/enemy-ai.asm @@ -0,0 +1,303 @@ +.include "skirmish.inc" + +.export ENEMY_AI_TURN + +.import REBUILD_OCCUPANCY +.import UNIT_ATTACK +.import ENSURE_SELECTED_UNIT + +.importzp temp0 +.importzp temp1 +.importzp temp2 +.importzp temp3 +.importzp temp4 +.importzp temp5 +.importzp temp6 +.importzp temp7 + +.import unit_x +.import unit_y +.import unit_hp +.import unit_team +.import grid_occupancy + +.segment "CODE" +ENEMY_AI_TURN: + ldx #PLAYER_COUNT +enemy_loop: + cpx #UNIT_COUNT + beq enemy_done + lda unit_hp,x + beq next_enemy + + stx temp7 + jsr FIND_CLOSEST_PLAYER + cpy #$FF + beq restore_enemy + jsr ENEMY_ACT +restore_enemy: + ldx temp7 +next_enemy: + inx + jmp enemy_loop + +enemy_done: + jsr ENSURE_SELECTED_UNIT + rts + +FIND_CLOSEST_PLAYER: + ldx temp7 + lda unit_x,x + sta temp0 + lda unit_y,x + sta temp1 + + lda #$FF + sta temp2 + sta temp3 + ldy #$FF + + ldx #0 +find_target_loop: + lda unit_hp,x + beq next_target + + lda temp0 + cmp unit_x,x + bcs :+ + lda unit_x,x + sec + sbc temp0 + jmp :++ +: sec + sbc unit_x,x +: sta temp4 + + lda temp1 + cmp unit_y,x + bcs :+ + lda unit_y,x + sec + sbc temp1 + jmp :++ +: sec + sbc unit_y,x +: clc + adc temp4 + sta temp4 + + lda unit_x,x + cmp #3 + bcs :+ + sec + lda #3 + sbc unit_x,x + jmp :++ +: sec + sbc #3 +: sta temp5 + + lda unit_y,x + cmp #3 + bcs :+ + sec + lda #3 + sbc unit_y,x + jmp :++ +: sec + sbc #3 +: clc + adc temp5 + sta temp5 + + lda temp4 + cmp temp2 + bcc better_target + bne next_target + lda temp5 + cmp temp3 + bcs next_target + +better_target: + lda temp4 + sta temp2 + lda temp5 + sta temp3 + txa + tay + +next_target: + inx + cpx #PLAYER_COUNT + bne find_target_loop + rts + +ENEMY_ACT: + sty temp6 + ldx temp7 + + lda unit_x,x + sta temp2 + lda unit_y,x + sta temp3 + + ldy temp6 + lda unit_x,y + sta temp4 + lda unit_y,y + sta temp5 + + lda temp2 + cmp temp4 + bcs :+ + lda temp4 + sec + sbc temp2 + jmp :++ +: sec + sbc temp4 +: sta temp0 + + lda temp3 + cmp temp5 + bcs :+ + lda temp5 + sec + sbc temp3 + jmp :++ +: sec + sbc temp5 +: sta temp1 + + lda temp0 + clc + adc temp1 + cmp #1 + bne move_enemy + + ldx temp7 + ldy temp6 + jsr UNIT_ATTACK + jsr REBUILD_OCCUPANCY + rts + +move_enemy: + jsr CALC_X_STEP + sta temp2 + jsr CALC_Y_STEP + sta temp3 + + lda temp0 + cmp temp1 + bcc try_y_first + +try_x_first: + lda temp2 + beq try_y_after_x + jsr TRY_X_MOVE + bcs enemy_move_done +try_y_after_x: + lda temp3 + beq enemy_move_fail + jsr TRY_Y_MOVE + bcs enemy_move_done + jmp enemy_move_fail + +try_y_first: + lda temp3 + beq try_x_after_y + jsr TRY_Y_MOVE + bcs enemy_move_done +try_x_after_y: + lda temp2 + beq enemy_move_fail + jsr TRY_X_MOVE + bcs enemy_move_done + +enemy_move_fail: + clc + rts + +enemy_move_done: + jsr REBUILD_OCCUPANCY + sec + rts + +CALC_X_STEP: + ldx temp7 + ldy temp6 + lda unit_x,y + cmp unit_x,x + beq x_step_zero + bcc x_step_left + lda #1 + rts +x_step_left: + lda #$FF + rts +x_step_zero: + lda #0 + rts + +CALC_Y_STEP: + ldx temp7 + ldy temp6 + lda unit_y,y + cmp unit_y,x + beq y_step_zero + bcc y_step_up + lda #1 + rts +y_step_up: + lda #$FF + rts +y_step_zero: + lda #0 + rts + +TRY_X_MOVE: + ldx temp7 + lda unit_x,x + clc + adc temp2 + sta temp4 + cmp #8 + bcs move_fail + lda unit_y,x + sta temp5 + jmp TRY_MOVE_COMMON + +TRY_Y_MOVE: + ldx temp7 + lda unit_x,x + sta temp4 + lda unit_y,x + clc + adc temp3 + sta temp5 + cmp #8 + bcs move_fail + +TRY_MOVE_COMMON: + lda temp5 + asl + asl + asl + clc + adc temp4 + tay + lda grid_occupancy,y + cmp #EMPTY_CELL + bne move_fail + + ldx temp7 + lda temp4 + sta unit_x,x + lda temp5 + sta unit_y,x + sec + rts + +move_fail: + clc + rts diff --git a/skirmish-game/src/game-logic.asm b/skirmish-game/src/game-logic.asm new file mode 100644 index 0000000..178908b --- /dev/null +++ b/skirmish-game/src/game-logic.asm @@ -0,0 +1,391 @@ +.include "skirmish.inc" + +.export INIT_GAME +.export UPDATE_GAME +.export REBUILD_OCCUPANCY +.export UNIT_ATTACK +.export ENSURE_SELECTED_UNIT + +.import ENEMY_AI_TURN + +.importzp input_flags +.importzp game_state +.importzp selected_unit +.importzp turn_counter +.importzp player_units +.importzp enemy_units +.importzp move_dx +.importzp move_dy +.importzp action_result +.importzp temp0 +.importzp temp1 +.importzp temp2 +.importzp temp3 +.importzp temp4 +.importzp temp5 + +.import unit_x +.import unit_y +.import unit_hp +.import unit_team +.import grid_occupancy + +.segment "CODE" +INIT_GAME: + lda #STATE_PLAYER + sta game_state + lda #0 + sta turn_counter + sta action_result + lda #0 + sta selected_unit + + lda #PLAYER_COUNT + sta player_units + lda #ENEMY_COUNT + sta enemy_units + + ldx #0 +clear_units: + lda #0 + sta unit_x,x + sta unit_y,x + sta unit_hp,x + sta unit_team,x + inx + cpx #UNIT_COUNT + bne clear_units + + lda #0 + sta unit_x+0 + sta unit_x+1 + sta unit_x+2 + lda #7 + sta unit_x+3 + sta unit_x+4 + sta unit_x+5 + + lda #1 + sta unit_y+0 + lda #3 + sta unit_y+1 + lda #5 + sta unit_y+2 + lda #1 + sta unit_y+3 + lda #3 + sta unit_y+4 + lda #5 + sta unit_y+5 + + lda #3 + ldx #0 +hp_loop: + sta unit_hp,x + inx + cpx #UNIT_COUNT + bne hp_loop + + lda #TEAM_PLAYER + sta unit_team+0 + sta unit_team+1 + sta unit_team+2 + lda #TEAM_ENEMY + sta unit_team+3 + sta unit_team+4 + sta unit_team+5 + + jsr REBUILD_OCCUPANCY + rts + +UPDATE_GAME: + jsr REBUILD_OCCUPANCY + + lda game_state + cmp #STATE_PLAYER + beq update_player + cmp #STATE_ENEMY + beq update_enemy + cmp #STATE_CHECK + beq update_check + jmp update_end + +update_player: + jsr ENSURE_SELECTED_UNIT + + lda input_flags + and #INPUT_NEXT + beq :+ + jsr CYCLE_SELECTED_UNIT + rts +: + lda input_flags + and #INPUT_FIRE + beq :+ + jsr CYCLE_SELECTED_UNIT + rts +: + lda input_flags + and #INPUT_WAIT + beq :+ + lda #STATE_ENEMY + sta game_state + rts +: + lda #0 + sta move_dx + sta move_dy + + lda input_flags + and #INPUT_UP + beq :+ + lda #$FF + sta move_dy + jmp do_player_action +: + lda input_flags + and #INPUT_DOWN + beq :+ + lda #1 + sta move_dy + jmp do_player_action +: + lda input_flags + and #INPUT_LEFT + beq :+ + lda #$FF + sta move_dx + jmp do_player_action +: + lda input_flags + and #INPUT_RIGHT + beq :+ + lda #1 + sta move_dx + jmp do_player_action +: + rts + +do_player_action: + jsr UNIT_TRY_ACTION + lda action_result + beq :+ + lda #STATE_ENEMY + sta game_state +: + rts + +update_enemy: + jsr ENEMY_AI_TURN + inc turn_counter + lda #STATE_CHECK + sta game_state + rts + +update_check: + jsr CHECK_WIN_LOSE + lda game_state + cmp #STATE_CHECK + bne :+ + lda #STATE_PLAYER + sta game_state +: + rts + +update_end: + lda input_flags + and #INPUT_RESTART + beq :+ + jsr INIT_GAME +: + rts + +UNIT_TRY_ACTION: + lda #0 + sta action_result + + ldx selected_unit + cpx #PLAYER_COUNT + bcs unit_try_done + lda unit_hp,x + beq unit_try_done + + lda unit_x,x + clc + adc move_dx + sta temp0 + cmp #8 + bcs unit_try_done + + lda unit_y,x + clc + adc move_dy + sta temp1 + cmp #8 + bcs unit_try_done + + lda temp1 + asl + asl + asl + clc + adc temp0 + tay + lda grid_occupancy,y + cmp #EMPTY_CELL + beq move_player + + tay + lda unit_team,y + cmp #TEAM_ENEMY + bne unit_try_done + + ldx selected_unit + jsr UNIT_ATTACK_XY + lda #1 + sta action_result + jmp unit_try_done + +move_player: + ldx selected_unit + lda temp0 + sta unit_x,x + lda temp1 + sta unit_y,x + lda #1 + sta action_result + +unit_try_done: + rts + +REBUILD_OCCUPANCY: + ldx #0 +clear_grid: + lda #EMPTY_CELL + sta grid_occupancy,x + inx + cpx #64 + bne clear_grid + + ldx #0 +grid_loop: + lda unit_hp,x + beq next_grid_unit + lda unit_y,x + asl + asl + asl + clc + adc unit_x,x + tay + txa + sta grid_occupancy,y +next_grid_unit: + inx + cpx #UNIT_COUNT + bne grid_loop + rts + +CHECK_WIN_LOSE: + lda enemy_units + bne :+ + lda #STATE_WIN + sta game_state + rts +: + lda player_units + bne :+ + lda #STATE_LOSE + sta game_state + rts +: + lda #STATE_CHECK + sta game_state + rts + +ENSURE_SELECTED_UNIT: + lda selected_unit + cmp #PLAYER_COUNT + bcs choose_first_player + tax + lda unit_hp,x + bne ensure_done + +choose_first_player: + ldx #0 +find_alive_player: + lda unit_hp,x + bne found_player + inx + cpx #PLAYER_COUNT + bne find_alive_player + lda #$FF + sta selected_unit + rts + +found_player: + stx selected_unit +ensure_done: + rts + +CYCLE_SELECTED_UNIT: + lda player_units + beq no_cycle_target + + lda selected_unit + cmp #PLAYER_COUNT + bcc :+ + lda #$FF +: + clc + adc #1 + cmp #PLAYER_COUNT + bcc :+ + lda #0 +: + sta temp0 + ldx #PLAYER_COUNT +cycle_search: + ldy temp0 + lda unit_hp,y + bne cycle_found + inc temp0 + lda temp0 + cmp #PLAYER_COUNT + bcc :+ + lda #0 + sta temp0 +: + dex + bne cycle_search + +no_cycle_target: + lda #$FF + sta selected_unit + rts + +cycle_found: + sty selected_unit + rts + +UNIT_ATTACK: + jsr UNIT_ATTACK_XY + rts + +UNIT_ATTACK_XY: + lda unit_hp,y + beq attack_done + sec + sbc #1 + sta unit_hp,y + bne attack_done + lda unit_team,y + beq dead_player + dec enemy_units + jmp attack_done + +dead_player: + dec player_units + +attack_done: + rts diff --git a/skirmish-game/src/input.asm b/skirmish-game/src/input.asm new file mode 100644 index 0000000..2121cf9 --- /dev/null +++ b/skirmish-game/src/input.asm @@ -0,0 +1,112 @@ +.include "skirmish.inc" + +.export INPUT_READ + +.importzp input_flags +.importzp last_input_flags +.importzp temp0 +.importzp temp1 + +.segment "CODE" +INPUT_READ: + lda #0 + sta temp0 + + lda $DC00 + sta temp1 + + lda temp1 + and #$01 + bne :+ + lda temp0 + ora #INPUT_UP + sta temp0 +: + lda temp1 + and #$02 + bne :+ + lda temp0 + ora #INPUT_DOWN + sta temp0 +: + lda temp1 + and #$04 + bne :+ + lda temp0 + ora #INPUT_LEFT + sta temp0 +: + lda temp1 + and #$08 + bne :+ + lda temp0 + ora #INPUT_RIGHT + sta temp0 +: + lda temp1 + and #$10 + bne :+ + lda temp0 + ora #INPUT_FIRE + sta temp0 +: + jsr $FFE4 + beq keyboard_done + + cmp #'W' + bne :+ + lda temp0 + ora #INPUT_UP + sta temp0 +: + cmp #'S' + bne :+ + lda temp0 + ora #INPUT_DOWN + sta temp0 +: + cmp #'A' + bne :+ + lda temp0 + ora #INPUT_LEFT + sta temp0 +: + cmp #'D' + bne :+ + lda temp0 + ora #INPUT_RIGHT + sta temp0 +: + cmp #'Q' + bne :+ + lda temp0 + ora #INPUT_NEXT + sta temp0 +: + cmp #' ' + bne :+ + lda temp0 + ora #INPUT_WAIT + sta temp0 +: + cmp #'E' + bne :+ + lda temp0 + ora #INPUT_WAIT + sta temp0 +: + cmp #'R' + bne keyboard_done + lda temp0 + ora #INPUT_RESTART + sta temp0 + +keyboard_done: + lda last_input_flags + eor #$FF + and temp0 + sta input_flags + + lda temp0 + sta last_input_flags + rts diff --git a/skirmish-game/src/render.asm b/skirmish-game/src/render.asm new file mode 100644 index 0000000..d6040bf --- /dev/null +++ b/skirmish-game/src/render.asm @@ -0,0 +1,613 @@ +.include "skirmish.inc" + +.export INIT_RENDER +.export SPRITE_UPDATE +.export TEXT_UPDATE + +.importzp game_state +.importzp selected_unit +.importzp turn_counter +.importzp player_units +.importzp enemy_units +.importzp temp0 +.importzp temp1 +.importzp temp2 +.importzp temp3 +.importzp temp4 +.importzp temp5 + +.import unit_x +.import unit_y +.import unit_hp + +.import grid_x_low +.import grid_x_msb +.import grid_y_pos +.import sprite_bits +.import screen_row_lo +.import screen_row_hi +.import color_row_lo +.import color_row_hi + +.import title_text +.import player_turn_text +.import enemy_turn_text +.import win_text +.import lose_text +.import controls_text +.import turn_text +.import select_text +.import player_label_text +.import enemy_label_text +.import player_sprite +.import enemy_sprite + +.segment "CODE" +INIT_RENDER: + lda #BORDER_COLOR + sta $D020 + lda #BG_COLOR + sta $D021 + + lda #0 + sta $D015 + sta $D017 + sta $D01B + sta $D01C + sta $D01D + sta $D010 + + jsr CLEAR_SCREEN_AND_COLOR + jsr DRAW_STATIC_UI + jsr SETUP_SPRITES + rts + +SPRITE_UPDATE: + lda #0 + sta temp0 + sta temp1 + + ldx #0 +sprite_loop: + lda unit_hp,x + beq hide_sprite + + lda temp0 + ora sprite_bits,x + sta temp0 + + stx temp4 + txa + asl + tay + + lda unit_x,x + tax + lda grid_x_low,x + sta $D000,y + lda grid_x_msb,x + beq :+ + ldx temp4 + lda temp1 + ora sprite_bits,x + sta temp1 +: + ldx temp4 + lda unit_y,x + tax + lda grid_y_pos,x + sta $D001,y + + ldx temp4 + cpx #PLAYER_COUNT + bcs enemy_color + cpx selected_unit + bne player_color + lda #SPRITE_SELECTED_COLOR + bne store_color +player_color: + lda #SPRITE_PLAYER_COLOR + bne store_color +enemy_color: + lda #SPRITE_ENEMY_COLOR +store_color: + sta $D027,x +hide_sprite_after_pos: + jmp next_sprite + +hide_sprite: + stx temp4 + txa + asl + tay + lda #250 + sta $D001,y + ldx temp4 + +next_sprite: + inx + cpx #UNIT_COUNT + bne sprite_loop + + lda temp0 + sta $D015 + lda temp1 + sta $D010 + rts + +TEXT_UPDATE: + ldx #1 + jsr CLEAR_ROW + lda game_state + cmp #STATE_PLAYER + beq text_player + cmp #STATE_ENEMY + beq text_enemy + cmp #STATE_WIN + beq text_win + lda #lose_text + sta temp3 + ldx #1 + lda #14 + jmp draw_state_line + +text_player: + lda #player_turn_text + sta temp3 + ldx #1 + lda #13 + jmp draw_state_line + +text_enemy: + lda #enemy_turn_text + sta temp3 + ldx #1 + lda #7 + jmp draw_state_line + +text_win: + lda #win_text + sta temp3 + ldx #1 + lda #5 + +draw_state_line: + sta temp4 + jsr SET_SCREEN_PTR + lda #14 + jsr ADD_TO_SCREEN_PTR + jsr WRITE_STRING + ldx #1 + lda temp4 + jsr COLOR_FULL_ROW + + ldx #22 + jsr CLEAR_ROW + ldx #22 + jsr SET_SCREEN_PTR + lda #2 + jsr ADD_TO_SCREEN_PTR + lda #turn_text + sta temp3 + jsr WRITE_STRING + lda turn_counter + jsr WRITE_DECIMAL2 + + lda #14 + jsr ADD_TO_SCREEN_PTR + lda #select_text + sta temp3 + jsr WRITE_STRING + lda selected_unit + cmp #$FF + beq no_selection + clc + adc #1 + clc + adc #48 + ldy #0 + sta (temp0),y + jmp status_done +no_selection: + lda #45 + ldy #0 + sta (temp0),y +status_done: + ldx #22 + lda #HUD_COLOR + jsr COLOR_FULL_ROW + + ldx #23 + jsr CLEAR_ROW + ldx #23 + jsr SET_SCREEN_PTR + lda #2 + jsr ADD_TO_SCREEN_PTR + lda #16 + ldy #0 + sta (temp0),y + iny + lda #49 + sta (temp0),y + iny + lda #58 + sta (temp0),y + iny + ldx #0 + lda unit_hp,x + clc + adc #48 + sta (temp0),y + + iny + iny + lda #16 + sta (temp0),y + iny + lda #50 + sta (temp0),y + iny + lda #58 + sta (temp0),y + iny + ldx #1 + lda unit_hp,x + clc + adc #48 + sta (temp0),y + + iny + iny + lda #16 + sta (temp0),y + iny + lda #51 + sta (temp0),y + iny + lda #58 + sta (temp0),y + iny + ldx #2 + lda unit_hp,x + clc + adc #48 + sta (temp0),y + + iny + iny + lda #5 + sta (temp0),y + iny + lda #49 + sta (temp0),y + iny + lda #58 + sta (temp0),y + iny + ldx #3 + lda unit_hp,x + clc + adc #48 + sta (temp0),y + + iny + iny + lda #5 + sta (temp0),y + iny + lda #50 + sta (temp0),y + iny + lda #58 + sta (temp0),y + iny + ldx #4 + lda unit_hp,x + clc + adc #48 + sta (temp0),y + + iny + iny + lda #5 + sta (temp0),y + iny + lda #51 + sta (temp0),y + iny + lda #58 + sta (temp0),y + iny + ldx #5 + lda unit_hp,x + clc + adc #48 + sta (temp0),y + + ldx #23 + lda #HUD_COLOR + jsr COLOR_FULL_ROW + rts + +CLEAR_SCREEN_AND_COLOR: + ldx #0 +clear_pages: + lda #32 + sta SCREEN_RAM + $0000,x + sta SCREEN_RAM + $0100,x + sta SCREEN_RAM + $0200,x + sta SCREEN_RAM + $02E8,x + + lda #HUD_COLOR + sta COLOR_RAM + $0000,x + sta COLOR_RAM + $0100,x + sta COLOR_RAM + $0200,x + sta COLOR_RAM + $02E8,x + inx + cpx #$E8 + bne clear_pages + rts + +DRAW_STATIC_UI: + ldx #0 + jsr SET_SCREEN_PTR + lda #15 + jsr ADD_TO_SCREEN_PTR + lda #title_text + sta temp3 + jsr WRITE_STRING + ldx #0 + lda #7 + jsr COLOR_FULL_ROW + + ldx #24 + jsr CLEAR_ROW + ldx #24 + jsr SET_SCREEN_PTR + lda #1 + jsr ADD_TO_SCREEN_PTR + lda #controls_text + sta temp3 + jsr WRITE_STRING + ldx #24 + lda #3 + jsr COLOR_FULL_ROW + + jsr DRAW_BOARD + rts + +DRAW_BOARD: + ldx #0 +board_top_labels: + lda screen_row_lo + (BOARD_ROW - 1) + sta temp0 + lda screen_row_hi + (BOARD_ROW - 1) + sta temp1 + txa + asl + asl + clc + adc #BOARD_COL + tay + txa + clc + adc #48 + sta (temp0),y + + lda color_row_lo + (BOARD_ROW - 1) + sta temp2 + lda color_row_hi + (BOARD_ROW - 1) + sta temp3 + lda #BOARD_COLOR + sta (temp2),y + + txa + pha + txa + asl + clc + adc #BOARD_ROW + tax + jsr DRAW_BOARD_ROW + pla + tax + inx + cpx #8 + bne board_top_labels + rts + +DRAW_BOARD_ROW: + stx temp5 + lda screen_row_lo,x + sta temp0 + lda screen_row_hi,x + sta temp1 + lda color_row_lo,x + sta temp2 + lda color_row_hi,x + sta temp3 + + ldy #BOARD_COL + lda #46 + sta (temp0),y + lda #BOARD_COLOR + sta (temp2),y + iny + iny + iny + lda #46 + sta (temp0),y + lda #BOARD_COLOR + sta (temp2),y + iny + iny + iny + lda #46 + sta (temp0),y + lda #BOARD_COLOR + sta (temp2),y + iny + iny + iny + lda #46 + sta (temp0),y + lda #BOARD_COLOR + sta (temp2),y + iny + iny + iny + lda #46 + sta (temp0),y + lda #BOARD_COLOR + sta (temp2),y + iny + iny + iny + lda #46 + sta (temp0),y + lda #BOARD_COLOR + sta (temp2),y + iny + iny + iny + lda #46 + sta (temp0),y + lda #BOARD_COLOR + sta (temp2),y + iny + iny + iny + lda #46 + sta (temp0),y + lda #BOARD_COLOR + sta (temp2),y + + lda temp5 + sec + sbc #BOARD_ROW + lsr + clc + adc #48 + ldy #2 + sta (temp0),y + lda #BOARD_COLOR + sta (temp2),y + rts + +SETUP_SPRITES: + lda #<(player_sprite / 64) + sta SPRITE_PTRS + 0 + sta SPRITE_PTRS + 1 + sta SPRITE_PTRS + 2 + lda #<(enemy_sprite / 64) + sta SPRITE_PTRS + 3 + sta SPRITE_PTRS + 4 + sta SPRITE_PTRS + 5 + rts + +CLEAR_ROW: + jsr SET_SCREEN_PTR + ldy #0 +clear_row_loop: + lda #32 + sta (temp0),y + iny + cpy #40 + bne clear_row_loop + rts + +SET_SCREEN_PTR: + lda screen_row_lo,x + sta temp0 + lda screen_row_hi,x + sta temp1 + rts + +ADD_TO_SCREEN_PTR: + clc + adc temp0 + sta temp0 + bcc :+ + inc temp1 +: rts + +WRITE_STRING: + ldy #0 +write_string_loop: + lda (temp2),y + beq write_string_done + sta (temp0),y + iny + bne write_string_loop +write_string_done: + tya + clc + adc temp0 + sta temp0 + bcc :+ + inc temp1 +: rts + +WRITE_DECIMAL2: + sta temp4 + ldx #0 +decimal_tens_loop: + lda temp4 + cmp #10 + bcc decimal_store + sec + sbc #10 + sta temp4 + inx + jmp decimal_tens_loop + +decimal_store: + ldy #0 + txa + clc + adc #48 + sta (temp0),y + iny + lda temp4 + clc + adc #48 + sta (temp0),y + iny + tya + clc + adc temp0 + sta temp0 + bcc :+ + inc temp1 +: rts + +COLOR_FULL_ROW: + sta temp4 + lda color_row_lo,x + sta temp2 + lda color_row_hi,x + sta temp3 + ldy #0 +color_loop: + lda temp4 + sta (temp2),y + iny + cpy #40 + bne color_loop + rts diff --git a/skirmish-game/src/skirmish.asm b/skirmish-game/src/skirmish.asm new file mode 100644 index 0000000..9238496 --- /dev/null +++ b/skirmish-game/src/skirmish.asm @@ -0,0 +1,136 @@ +.include "skirmish.inc" + +.exportzp input_flags +.exportzp last_input_flags +.exportzp game_state +.exportzp selected_unit +.exportzp turn_counter +.exportzp player_units +.exportzp enemy_units +.exportzp move_dx +.exportzp move_dy +.exportzp action_result +.exportzp frame_ready +.exportzp temp0 +.exportzp temp1 +.exportzp temp2 +.exportzp temp3 +.exportzp temp4 +.exportzp temp5 +.exportzp temp6 +.exportzp temp7 + +.export unit_x +.export unit_y +.export unit_hp +.export unit_team +.export old_irq +.export grid_occupancy + +.import INPUT_READ +.import INIT_GAME +.import UPDATE_GAME +.import INIT_RENDER +.import SPRITE_UPDATE +.import TEXT_UPDATE + +.segment "LOADADDR" + .word $0801 + +.segment "EXEHDR" + .byte $0B, $08, $0A, $00, $9E + .byte "2061", $00, $00, $00 + +.segment "ZEROPAGE" +input_flags: .res 1 +last_input_flags: .res 1 +game_state: .res 1 +selected_unit: .res 1 +turn_counter: .res 1 +player_units: .res 1 +enemy_units: .res 1 +move_dx: .res 1 +move_dy: .res 1 +action_result: .res 1 +frame_ready: .res 1 +temp0: .res 1 +temp1: .res 1 +temp2: .res 1 +temp3: .res 1 +temp4: .res 1 +temp5: .res 1 +temp6: .res 1 +temp7: .res 1 + +.segment "BSS" +unit_x: .res UNIT_COUNT +unit_y: .res UNIT_COUNT +unit_hp: .res UNIT_COUNT +unit_team:.res UNIT_COUNT +old_irq: .res 2 + +.segment "OCCUPANCY" +grid_occupancy: .res 64 + +.segment "CODE" +start: + sei + jsr $FF81 + + lda #0 + sta frame_ready + sta last_input_flags + + lda $0314 + sta old_irq + lda $0315 + sta old_irq+1 + + jsr INIT_RENDER + jsr INIT_GAME + jsr install_irq + cli + +main_loop: + lda frame_ready + beq main_loop + lda #0 + sta frame_ready + + jsr INPUT_READ + jsr UPDATE_GAME + jsr SPRITE_UPDATE + jsr TEXT_UPDATE + jmp main_loop + +install_irq: + lda #irq_handler + sta $0315 + + lda $D011 + and #$7F + sta $D011 + lda #$80 + sta $D012 + + lda #$01 + sta $D01A + + lda $DC0D + lda $DD0D + lda #$01 + sta $D019 + rts + +irq_handler: + lda $D019 + and #$01 + beq chain_irq + lda #$01 + sta $D019 + inc frame_ready + +chain_irq: + jmp (old_irq) diff --git a/skirmish-game/src/skirmish.inc b/skirmish-game/src/skirmish.inc new file mode 100644 index 0000000..20002ef --- /dev/null +++ b/skirmish-game/src/skirmish.inc @@ -0,0 +1,40 @@ +UNIT_COUNT = 6 +PLAYER_COUNT = 3 +ENEMY_COUNT = 3 + +EMPTY_CELL = $FF + +TEAM_PLAYER = 0 +TEAM_ENEMY = 1 + +STATE_PLAYER = 0 +STATE_ENEMY = 1 +STATE_CHECK = 2 +STATE_WIN = 3 +STATE_LOSE = 4 + +INPUT_UP = $01 +INPUT_DOWN = $02 +INPUT_LEFT = $04 +INPUT_RIGHT = $08 +INPUT_FIRE = $10 +INPUT_NEXT = $20 +INPUT_WAIT = $40 +INPUT_RESTART = $80 + +SCREEN_RAM = $0400 +COLOR_RAM = $D800 +SPRITE_PTRS = SCREEN_RAM + $03F8 + +BOARD_COL = 4 +BOARD_ROW = 4 +BOARD_PIXEL_X = 56 +BOARD_PIXEL_Y = 60 + +SPRITE_PLAYER_COLOR = 13 +SPRITE_SELECTED_COLOR = 7 +SPRITE_ENEMY_COLOR = 10 +HUD_COLOR = 1 +BOARD_COLOR = 15 +BG_COLOR = 6 +BORDER_COLOR = 14