From e4d8e42821998d676be857ad1d9f1c1ad0b665be Mon Sep 17 00:00:00 2001 From: Mattias Hansson Date: Sat, 10 Jan 2026 15:38:46 +0100 Subject: [PATCH 1/6] Added a game menu to the system with possibility to restart the game and to choose game options. --- cardgame.c65 | 4 + gameloop.c65 | 312 +++++++++++++++++++++++++++++------------------- gamemenu.c65 | 313 +++++++++++++++++++++++++++++++++++++++++++++++++ gamestate.c65 | 25 ++++ keyboard.c65 | 156 ++++++++++++++++++++++++ pileconsts.c65 | 31 +++++ piles.c65 | 41 +++++++ utils.c65 | 8 ++ 8 files changed, 767 insertions(+), 123 deletions(-) create mode 100644 gamemenu.c65 create mode 100644 gamestate.c65 create mode 100644 keyboard.c65 create mode 100644 pileconsts.c65 diff --git a/cardgame.c65 b/cardgame.c65 index 980dfa7..bd9a178 100644 --- a/cardgame.c65 +++ b/cardgame.c65 @@ -14,7 +14,9 @@ ENDASM #INCLUDE "utils.c65" #INCLUDE "cardconsts.c65" +#INCLUDE "pileconsts.c65" #INCLUDE "piles.c65" +#INCLUDE "gamestate.c65" #INCLUDE "random.c65" #INCLUDE "carddeck.c65" #INCLUDE "cardrender.c65" @@ -22,6 +24,8 @@ ENDASM #INCLUDE "joystick.c65" #INCLUDE "mouse.c65" #INCLUDE "pointer.c65" +#INCLUDE "keyboard.c65" +#INCLUDE "gamemenu.c65" //#INCLUDE "joysticktests.c65" #IFDEF TEST_GAMES #INCLUDE "testgames.c65" diff --git a/gameloop.c65 b/gameloop.c65 index cc0baf5..2857265 100644 --- a/gameloop.c65 +++ b/gameloop.c65 @@ -3,12 +3,16 @@ #DEFINE __lib_gameloop 1 #INCLUDE "cardconsts.c65" +#INCLUDE "pileconsts.c65" #INCLUDE "piles.c65" +#INCLUDE "gamestate.c65" #INCLUDE "cardmoves.c65" #INCLUDE "cardrender.c65" #INCLUDE "mouse.c65" #INCLUDE "pointer.c65" #INCLUDE "cardsprites.c65" +#INCLUDE "keyboard.c65" +#INCLUDE "gamemenu.c65" GOTO __skip_lib_gameloop @@ -24,27 +28,8 @@ GOTO __skip_lib_gameloop // game_loop() // Never returns // ============================================================================ -// Pile identifiers -BYTE CONST PILE_ID_NONE = 0 -BYTE CONST PILE_ID_STOCK = 1 -BYTE CONST PILE_ID_WASTE = 2 -BYTE CONST PILE_ID_FOUND0 = 3 -BYTE CONST PILE_ID_FOUND1 = 4 -BYTE CONST PILE_ID_FOUND2 = 5 -BYTE CONST PILE_ID_FOUND3 = 6 -BYTE CONST PILE_ID_TAB0 = 7 -BYTE CONST PILE_ID_TAB1 = 8 -BYTE CONST PILE_ID_TAB2 = 9 -BYTE CONST PILE_ID_TAB3 = 10 -BYTE CONST PILE_ID_TAB4 = 11 -BYTE CONST PILE_ID_TAB5 = 12 -BYTE CONST PILE_ID_TAB6 = 13 - -// Game state variables -BYTE game_selected_pile // Currently selected pile ID (PILE_ID_NONE if none) -BYTE game_selected_card_count // Number of cards selected from tableau (1+ for tableau stacks) -BYTE game_prev_button_state // Previous mouse button state for click detection -BYTE game_draw_mode = 1 // Stock draw mode: 1 or 3 cards per draw +// Pile identifiers now in pileconsts.c65 +// Game state variables now in gamestate.c65 // Layout constants (in character coordinates) BYTE CONST LAYOUT_STOCK_COL = 0 @@ -786,11 +771,54 @@ FUNC handle_click_on_pile({BYTE clicked_pile} {BYTE click_row}) ENDIF ENDIF + // Foundation to Tableau (if enabled) + IF game_allow_found_to_tab + BYTE selected_is_foundation + selected_is_foundation = 0 + IF game_selected_pile >= PILE_ID_FOUND0 + IF game_selected_pile <= PILE_ID_FOUND3 + selected_is_foundation = 1 + ENDIF + ENDIF + + IF selected_is_foundation + IF is_tableau + move_found_to_tab(src_ptr, dst_ptr, success) + IF success + render_pile_by_id(game_selected_pile) + render_pile_by_id(clicked_pile) + ENDIF + game_selected_pile = PILE_ID_NONE + EXIT + ENDIF + ENDIF + ENDIF + // Click on same pile or invalid destination: deselect game_selected_pile = PILE_ID_NONE FEND +// ============================================================================ +// FUNC restart_game +// Restart the game with new shuffle and deal +// ============================================================================ +FUNC restart_game + // Clear all piles (waste, foundations, tableaus) + clear_all_piles() + + // Re-initialize deck and shuffle (RNG continues from current state) + stock_init() + stock_shuffle() + deal_tableaus() + + // Reset game state + game_selected_pile = PILE_ID_NONE + game_selected_card_count = 0 + game_prev_button_state = 0 +FEND + + // ============================================================================ // FUNC render_all_piles_initial // Render all piles for initial game display @@ -836,128 +864,166 @@ FUNC game_loop POINTER dst_ptr -> $2240 mem_copy_range(src_ptr, dst_ptr, 64*25) - // Initialize mouse and pointer - mouse_init() - pointer_init(160, 100, color_red, 136) // Sprite block 136 = $2200, bright red color - // Enable sprite - pointer_enable(1) - - // Initialize card display sprites - card_display_init() - -#IFDEF TEST_GAMES - // Test game options (comment/uncomment one): - //setup_test_game_tall_tableau() // K->3 in tab0, red 2 in waste - //setup_test_game_one_move_to_win() // 1 move from victory - setup_test_game_overflow() // K->A in tab3 to test screen overflow -#IFEND - - // Initial render - fill_mem($0400, $0400+999, 0) // Clear screen - render_all_piles_initial() - - BYTE raster @ $d012 + // Outer loop for restart handling WHILE 1 - // Wait for raster to avoid tearing - WHILE raster != 250 - WEND - // Read mouse input (Port 1) - mouse_read() - pointer_update_mouse(mouse_delta_x, mouse_delta_y) - // Read joystick input (Port 2) - joy_read_port2() - pointer_update_joystick(joy_state) + // Initialize mouse and pointer + mouse_init() + pointer_init(160, 100, color_red, 136) // Sprite block 136 = $2200, bright red color - // Get pointer position - pointer_get_x(sprite_x) - pointer_get_y(sprite_y) + // Enable sprite + pointer_enable(1) - // Convert to character coordinates - pointer_to_char_coords(sprite_x, sprite_y, char_col, char_row) - - // Get pile under cursor - get_pile_at_coords(char_col, char_row, pile_at_cursor) + // Initialize card display sprites + card_display_init() #IFDEF TEST_GAMES - // Visual feedback - show selection state and hover - IF game_selected_pile != PILE_ID_NONE - // Something selected - show with cyan border - POKE $d020 , color_cyan - ELSE - // Nothing selected - grey when hovering over pile, white otherwise - IF pile_at_cursor != PILE_ID_NONE - POKE $d020 , color_light_grey - ELSE - POKE $d020 , color_white - ENDIF - ENDIF + // Test game options (comment/uncomment one): + //setup_test_game_tall_tableau() // K->3 in tab0, red 2 in waste + //setup_test_game_one_move_to_win() // 1 move from victory + setup_test_game_overflow() // K->A in tab3 to test screen overflow #IFEND - // Detect click from both mouse button (bit 4) and joystick fire (bit 4) - // Mouse buttons: bit 4 = left button (active low, 0=pressed) - // Joystick state: bit 4 = fire button (active high, 1=pressed after inversion) - // Combine both: if either has bit 4 clear (mouse) or set (joy), trigger click - BYTE combined_button - combined_button = mouse_buttons & %00010000 // Mouse: 0=pressed - IF combined_button != 0 - // Mouse not pressed, check joystick - combined_button = joy_state & %00010000 // Joy: 1=pressed - IF combined_button - combined_button = 0 // Make it 0 (pressed) like mouse - ELSE - combined_button = %00010000 // Not pressed + + BYTE raster @ $d012 + BYTE menu_key + BYTE menu_action + + + // Initial render + fill_mem($0400, $0400+999, 0) // Clear screen + + render_all_piles_initial() + + // Inner game loop + WHILE 1 + // Wait for raster to avoid tearing + WHILE raster != 250 + WEND + + // Check for menu key (Return or Left Arrow) + key_scan(menu_key) + IF menu_key == KEY_RETURN + menu_show(menu_action) + IF menu_action == MENU_ACTION_RESTART + // Restart requested - break out to restart + BREAK + ENDIF + // Clear screen and re-render after resume + fill_mem($0400, $0400+999, 0) + render_all_piles_initial() + ENDIF + IF menu_key == KEY_LEFT_ARROW + menu_show(menu_action) + IF menu_action == MENU_ACTION_RESTART + // Restart requested - break out to restart + BREAK + ENDIF + // Clear screen and re-render after resume + fill_mem($0400, $0400+999, 0) + render_all_piles_initial() ENDIF - ENDIF - button_state = combined_button - detect_click(button_state, clicked) + // Read mouse input (Port 1) + mouse_read() + pointer_update_mouse(mouse_delta_x, mouse_delta_y) + + // Read joystick input (Port 2) + joy_read_port2() + pointer_update_joystick(joy_state) + + // Get pointer position + pointer_get_x(sprite_x) + pointer_get_y(sprite_y) + + // Convert to character coordinates + pointer_to_char_coords(sprite_x, sprite_y, char_col, char_row) + + // Get pile under cursor + get_pile_at_coords(char_col, char_row, pile_at_cursor) - // Handle click if occurred - IF clicked #IFDEF TEST_GAMES - // Visual feedback - flash border on click - POKE $d020 , color_yellow - #IFEND - - IF pile_at_cursor != PILE_ID_NONE - handle_click_on_pile(pile_at_cursor, char_row) + // Visual feedback - show selection state and hover + IF game_selected_pile != PILE_ID_NONE + // Something selected - show with cyan border + POKE $d020 , color_cyan ELSE - // Click on empty area: deselect - game_selected_pile = PILE_ID_NONE + // Nothing selected - grey when hovering over pile, white otherwise + IF pile_at_cursor != PILE_ID_NONE + POKE $d020 , color_light_grey + ELSE + POKE $d020 , color_white + ENDIF + ENDIF + #IFEND + // Detect click from both mouse button (bit 4) and joystick fire (bit 4) + // Mouse buttons: bit 4 = left button (active low, 0=pressed) + // Joystick state: bit 4 = fire button (active high, 1=pressed after inversion) + // Combine both: if either has bit 4 clear (mouse) or set (joy), trigger click + BYTE combined_button + combined_button = mouse_buttons & %00010000 // Mouse: 0=pressed + IF combined_button != 0 + // Mouse not pressed, check joystick + combined_button = joy_state & %00010000 // Joy: 1=pressed + IF combined_button + combined_button = 0 // Make it 0 (pressed) like mouse + ELSE + combined_button = %00010000 // Not pressed + ENDIF ENDIF - ENDIF - // Update selected card display (upper right corner using sprites) - BYTE display_card_id - BYTE display_valid - BYTE display_rank - BYTE display_suit + button_state = combined_button + detect_click(button_state, clicked) - get_selected_card_info(display_card_id, display_valid) - IF display_valid - // Show selected card rank and suit using sprites - card_id_to_suit_rank(display_card_id, display_suit, display_rank) - card_display_show(display_rank, display_suit) - ELSE - // Hide sprites when nothing selected - card_display_hide() - ENDIF + // Handle click if occurred + IF clicked + #IFDEF TEST_GAMES + // Visual feedback - flash border on click + POKE $d020 , color_yellow + #IFEND - // Check win condition - check_win_condition(is_won) - IF is_won - // Flash border or show message - POKE $d020 , color_green - // Could add "YOU WIN" message here - // For now, just keep running to allow admiring the win - ENDIF + IF pile_at_cursor != PILE_ID_NONE + handle_click_on_pile(pile_at_cursor, char_row) + ELSE + // Click on empty area: deselect + game_selected_pile = PILE_ID_NONE + ENDIF + ENDIF - // Small delay to avoid reading mouse too fast - // Could sync to raster if needed for smoother experience - WEND + // Update selected card display (upper right corner using sprites) + BYTE display_card_id + BYTE display_valid + BYTE display_rank + BYTE display_suit + + get_selected_card_info(display_card_id, display_valid) + IF display_valid + // Show selected card rank and suit using sprites + card_id_to_suit_rank(display_card_id, display_suit, display_rank) + card_display_show(display_rank, display_suit) + ELSE + // Hide sprites when nothing selected + card_display_hide() + ENDIF + + // Check win condition + check_win_condition(is_won) + IF is_won + // Flash border or show message + POKE $d020 , color_green + // Could add "YOU WIN" message here + // For now, just keep running to allow admiring the win + ENDIF + + // Small delay to avoid reading mouse too fast + // Could sync to raster if needed for smoother experience + WEND // End inner game loop + + // If we get here, restart was requested + restart_game() + WEND // End outer restart loop FEND diff --git a/gamemenu.c65 b/gamemenu.c65 new file mode 100644 index 0000000..db9be21 --- /dev/null +++ b/gamemenu.c65 @@ -0,0 +1,313 @@ + +#IFNDEF __lib_gamemenu +#DEFINE __lib_gamemenu 1 + +#INCLUDE "pileconsts.c65" +#INCLUDE "keyboard.c65" +#INCLUDE "utils.c65" + +GOTO __skip_lib_gamemenu + +// ============================================================================ +// GAME MENU SYSTEM +// ============================================================================ +// Pause game and display menu for configuration and restart +// +// Usage: +// #INCLUDE "gamemenu.c65" +// BYTE action +// menu_show(action) // Returns MENU_ACTION_* constant +// ============================================================================ + +// Menu action constants +BYTE CONST MENU_ACTION_RESUME = 0 +BYTE CONST MENU_ACTION_RESTART = 1 + +// Screen backup buffer (1000 bytes for screen, 1000 for color) +// Located at $C000-$C7CF (2000 bytes) +WORD CONST SCREEN_BACKUP = $C000 +WORD CONST COLOR_BACKUP = $C3E8 + +// ============================================================================ +// FUNC menu_save_screen +// Save current screen and color memory to backup buffer +// ============================================================================ +FUNC menu_save_screen + mem_copy_range($0400, SCREEN_BACKUP, 1000) + mem_copy_range($D800, COLOR_BACKUP, 1000) +FEND + + +// ============================================================================ +// FUNC menu_restore_screen +// Restore screen and color memory from backup buffer +// ============================================================================ +FUNC menu_restore_screen + mem_copy_range(SCREEN_BACKUP, $0400, 1000) + mem_copy_range(COLOR_BACKUP, $D800, 1000) +FEND + + +// ============================================================================ +// FUNC menu_switch_to_rom_charset +// Switch VIC-II to use ROM charset at $1800 (uppercase/lowercase) +// ============================================================================ +FUNC menu_switch_to_rom_charset + set_vic_charmem(3) // $1800 / $0800 = 3 +FEND + + +// ============================================================================ +// FUNC menu_switch_to_card_charset +// Switch VIC-II back to custom card charset at $2000 +// ============================================================================ +FUNC menu_switch_to_card_charset + set_vic_charmem(4) // $2000 / $0800 = 4 +FEND + + +// ============================================================================ +// FUNC menu_print_string +// Print a string at a specific screen position +// ============================================================================ +FUNC menu_print_string({WORD screen_pos @ $e0} {WORD str_ptr @ $e2}) + BYTE char + BYTE str_offset + + str_offset = 0 + char = PEEK str_ptr[str_offset] + WHILE char != 0 + POKE screen_pos[0] , char + screen_pos++ + str_offset++ + char = PEEK str_ptr[str_offset] + WEND +FEND + + +// ============================================================================ +// FUNC menu_render +// Render the menu screen with current settings +// ============================================================================ +FUNC menu_render + WORD screen_pos @ $f0 + WORD str_ptr @ $f2 + BYTE draw_indicator + BYTE found_to_tab_indicator + + // Clear screen + fill_mem($0400, $0400+999, 32) // 32 = space character + fill_mem($D800, $D800+999, 14) // Light blue text + + // Title (centered at row 2) + screen_pos = 2*40+$0400+8 + POINTER str_ptr -> str_title + menu_print_string(screen_pos, str_ptr) + + // Draw mode option (row 6) + screen_pos = 6*40+$0400+4 + POINTER str_ptr -> str_draw_mode + menu_print_string(screen_pos, str_ptr) + + // Draw mode value + screen_pos = 6*40+$0400+24 + IF game_draw_mode == 1 + POKE screen_pos[0] , 49 // '1' + ELSE + POKE screen_pos[0] , 51 // '3' + ENDIF + + // Draw mode indicator + IF game_draw_mode == 1 + draw_indicator = 91 // '[' + screen_pos = 6*40+$0400+23 + POKE screen_pos[0] , draw_indicator + screen_pos = 6*40+$0400+25 + POKE screen_pos[0] , 93 // ']' + ELSE + draw_indicator = 91 // '[' + screen_pos = 6*40+$0400+26 + POKE screen_pos[0] , draw_indicator + screen_pos = 6*40+$0400+28 + POKE screen_pos[0] , 93 // ']' + ENDIF + + // Foundation to Tableau option (row 9) + screen_pos = 9*40+$0400+4 + POINTER str_ptr -> str_found_to_tab + menu_print_string(screen_pos, str_ptr) + + // Foundation to Tableau value + screen_pos = 9*40+$0400+28 + IF game_allow_found_to_tab == 0 + POINTER str_ptr -> str_off + ELSE + POINTER str_ptr -> str_on + ENDIF + menu_print_string(screen_pos, str_ptr) + + // Instructions for resume/restart (row 14+) + screen_pos = 14*40+$0400+4 + POINTER str_ptr -> str_inst_f7 + menu_print_string(screen_pos, str_ptr) + + screen_pos = 15*40+$0400+4 + POINTER str_ptr -> str_inst_f8 + menu_print_string(screen_pos, str_ptr) + + // Set border color to menu color (purple) + POKE $d020 , 4 +FEND + + +// ============================================================================ +// FUNC menu_show +// Display menu and handle input, return action to take +// ============================================================================ +FUNC menu_show(out:{BYTE action}) + BYTE key + BYTE prev_key + + action = MENU_ACTION_RESUME + prev_key = KEY_NONE + + // Clear any card selection before entering menu + game_selected_pile = PILE_ID_NONE + game_selected_card_count = 0 + + // Save current screen state + menu_save_screen() + + // Disable all sprites + POKE $D015 , 0 // Sprite enable register - disable all + + // Switch to ROM charset and normal character mode + menu_switch_to_rom_charset() + clear_vic_ecm() + + // Render menu + menu_render() + + // Wait for key release first (debounce) + key_scan(key) + WHILE key != KEY_NONE + key_scan(key) + WEND + + // Menu loop + WHILE 1 + key_scan(key) + + // Only process on key press (transition from NONE to key) + IF key != KEY_NONE + IF prev_key == KEY_NONE + SWITCH key + CASE KEY_F1 + // Toggle draw mode + IF game_draw_mode == 1 + game_draw_mode = 3 + ELSE + game_draw_mode = 1 + ENDIF + menu_render() + + CASE KEY_F3 + // Toggle foundation to tableau + IF game_allow_found_to_tab == 0 + game_allow_found_to_tab = 1 + ELSE + game_allow_found_to_tab = 0 + ENDIF + menu_render() + + CASE KEY_F7 + // Resume game + action = MENU_ACTION_RESUME + BREAK + + CASE KEY_F8 + // Restart game (SHIFT+F7) + action = MENU_ACTION_RESTART + BREAK + ENDSWITCH + ENDIF + ENDIF + + prev_key = key + WEND + + // Wait for key release before returning + WHILE key != KEY_NONE + key_scan(key) + WEND + + // Restore screen or clear for restart + IF action == MENU_ACTION_RESUME + menu_restore_screen() + ELSE + // Restarting - clear screen and reset colors + fill_mem($0400, $0400+999, 0) + fill_mem($d800, $d800+999, 1) // White color + ENDIF + + // Switch back to card charset and ECM mode + menu_switch_to_card_charset() + set_vic_ecm() + + // Re-enable pointer sprite (game loop will handle card display sprites) + pointer_enable(1) + + // Restore border color + POKE $d020 , 1 // White +FEND + +// Menu strings (null-terminated) - Using !scr for screen codes +LABEL str_title +ASM + !scr "Klondike Solitaire Menu", 0 +ENDASM + +LABEL str_draw_mode +ASM + !scr "F1: Draw mode:", 0 +ENDASM + +LABEL str_found_to_tab +ASM + !scr "F3: Foundation>Tableau:", 0 +ENDASM + +LABEL str_on +ASM + !scr "On", 0 +ENDASM + +LABEL str_off +ASM + !scr "Off", 0 +ENDASM + +LABEL str_inst_f1 +ASM + !scr "F1: Toggle draw mode (1/3)", 0 +ENDASM + +LABEL str_inst_f3 +ASM + !scr "F3: Toggle foundation moves", 0 +ENDASM + +LABEL str_inst_f7 +ASM + !scr "F7: Resume game", 0 +ENDASM + +LABEL str_inst_f8 +ASM + !scr "F8: Restart game", 0 +ENDASM + + +LABEL __skip_lib_gamemenu + +#IFEND diff --git a/gamestate.c65 b/gamestate.c65 new file mode 100644 index 0000000..2f33195 --- /dev/null +++ b/gamestate.c65 @@ -0,0 +1,25 @@ + +#IFNDEF __lib_gamestate +#DEFINE __lib_gamestate 1 + +GOTO __skip_lib_gamestate + +// ============================================================================ +// GAME STATE VARIABLES +// ============================================================================ +// Global game state that needs to be accessible from multiple modules +// ============================================================================ + +// Game configuration +BYTE game_draw_mode = 1 // Stock draw mode: 1 or 3 cards per draw +BYTE game_allow_found_to_tab = 0 // Allow foundation to tableau moves: 0=disallow, 1=allow + +// Game interaction state +BYTE game_selected_pile // Currently selected pile ID (PILE_ID_NONE if none) +BYTE game_selected_card_count // Number of cards selected from tableau (1+ for tableau stacks) +BYTE game_prev_button_state // Previous mouse button state for click detection + + +LABEL __skip_lib_gamestate + +#IFEND diff --git a/keyboard.c65 b/keyboard.c65 new file mode 100644 index 0000000..b28cdc3 --- /dev/null +++ b/keyboard.c65 @@ -0,0 +1,156 @@ + +#IFNDEF __lib_keyboard +#DEFINE __lib_keyboard 1 + +GOTO __skip_lib_keyboard + +// ============================================================================ +// KEYBOARD INPUT DRIVER FOR COMMODORE 64 +// ============================================================================ +// Direct CIA keyboard matrix reading (no IRQ required) +// +// C64 Keyboard Matrix: +// $DC00 (Port A): Column select (write, active-low) +// $DC01 (Port B): Row read (read, active-low) +// +// Usage: +// #INCLUDE "keyboard.c65" +// BYTE key +// key_scan(key) +// SWITCH key +// CASE KEY_F1 +// // handle F1 +// CASE KEY_F3 +// // handle F3 +// ENDSWITCH +// +// // Debouncing: wait for KEY_NONE +// WHILE key != KEY_NONE +// key_scan(key) +// WEND +// ============================================================================ + +// CIA Port addresses +WORD CONST CIA_PORTA = $DC00 // Port A - ROW selection (write) +WORD CONST CIA_PORTB = $DC01 // Port B - COLUMN reading (read) +WORD CONST CIA_DDRA = $DC02 // Port A Data Direction Register +WORD CONST CIA_DDRB = $DC03 // Port B Data Direction Register + +// Key constants +BYTE CONST KEY_NONE = 0 +BYTE CONST KEY_F1 = 1 +BYTE CONST KEY_F3 = 2 +BYTE CONST KEY_F7 = 3 +BYTE CONST KEY_F8 = 4 // F7 + SHIFT +BYTE CONST KEY_RETURN = 5 +BYTE CONST KEY_LEFT_ARROW = 6 + +// ============================================================================ +// FUNC key_scan +// Scan keyboard and return which key is pressed +// Returns KEY_* constant (KEY_NONE if no relevant key pressed) +// C64 Keyboard Matrix: +// - Write to $DC00 (Port A) to select ROW +// - Read from $DC01 (Port B) to detect COLUMN +// ============================================================================ +FUNC key_scan(out:{BYTE key_pressed}) + BYTE col_data + BYTE temp + BYTE shift_held + BYTE saved_ddra + BYTE saved_ddrb + BYTE saved_porta + BYTE row0_data + BYTE row1_data + BYTE row6_data + + key_pressed = KEY_NONE + shift_held = 0 + + // Save current DDR state + saved_ddra = PEEK CIA_DDRA + saved_ddrb = PEEK CIA_DDRB + saved_porta = PEEK CIA_PORTA + + // Set up DDR: Port A = output (rows), Port B = input (columns) + POKE CIA_DDRA , $FF + POKE CIA_DDRB , $00 + + // Check F8 FIRST with two-read atomic check + // Read 1: Select rows 0+1+6 simultaneously ($BC = 10111100) + POKE CIA_PORTA , $BC + BYTE multi_row_data + multi_row_data = PEEK CIA_PORTB + + // Read 2: Select ONLY row 0 ($FE = 11111110) + POKE CIA_PORTA , $FE + col_data = PEEK CIA_PORTB + + // F8 detection: F7 pressed AND SHIFT pressed + // F7 at Row 0, Col 3 - check in row0 data + temp = col_data & $08 + IF temp == 0 + // F7 is pressed, now check if real SHIFT is pressed + // Compare multi-row read vs row-0-only read for cols 4 and 7 + // If col 4 or 7 was low in multi-row but NOT in row-0-only, it's SHIFT + BYTE shift_cols + shift_cols = multi_row_data & $90 // Cols 4 and 7 in multi-row read + temp = col_data & $90 // Cols 4 and 7 in row 0 only read + // If multi-row has a low bit that row-0-only doesn't, it's from rows 1 or 6 (SHIFT) + IF shift_cols != temp + key_pressed = KEY_F8 + ENDIF + ENDIF + + // If not F8, check other Row 0 keys (col_data already has row 0 from above) + IF key_pressed == KEY_NONE + + // Check F1 (Row 0, Column 4) + temp = col_data & $10 + IF temp == 0 + key_pressed = KEY_F1 + ENDIF + + // Check F3 (Row 0, Column 5) + IF key_pressed == KEY_NONE + temp = col_data & $20 + IF temp == 0 + key_pressed = KEY_F3 + ENDIF + ENDIF + + // Check F7 (Row 0, Column 3) + IF key_pressed == KEY_NONE + temp = col_data & $08 + IF temp == 0 + key_pressed = KEY_F7 + ENDIF + ENDIF + + // Check RETURN (Row 0, Column 1) + IF key_pressed == KEY_NONE + temp = col_data & $02 + IF temp == 0 + key_pressed = KEY_RETURN + ENDIF + ENDIF + + // Check Left Arrow (Row 0, Column 7) + IF key_pressed == KEY_NONE + temp = col_data & $80 + IF temp == 0 + key_pressed = KEY_LEFT_ARROW + ENDIF + ENDIF + ENDIF + + // Always restore saved DDR and port state + POKE CIA_PORTA , saved_porta + POKE CIA_DDRA , saved_ddra + POKE CIA_DDRB , saved_ddrb +FEND + + +LABEL __skip_lib_keyboard + +#IFEND diff --git a/pileconsts.c65 b/pileconsts.c65 new file mode 100644 index 0000000..c0f8c83 --- /dev/null +++ b/pileconsts.c65 @@ -0,0 +1,31 @@ + +#IFNDEF __lib_pileconsts +#DEFINE __lib_pileconsts 1 + +GOTO __skip_lib_pileconsts + +// ============================================================================ +// PILE ID CONSTANTS +// ============================================================================ +// Pile identifiers used throughout the game +// ============================================================================ + +BYTE CONST PILE_ID_NONE = 0 +BYTE CONST PILE_ID_STOCK = 1 +BYTE CONST PILE_ID_WASTE = 2 +BYTE CONST PILE_ID_FOUND0 = 3 +BYTE CONST PILE_ID_FOUND1 = 4 +BYTE CONST PILE_ID_FOUND2 = 5 +BYTE CONST PILE_ID_FOUND3 = 6 +BYTE CONST PILE_ID_TAB0 = 7 +BYTE CONST PILE_ID_TAB1 = 8 +BYTE CONST PILE_ID_TAB2 = 9 +BYTE CONST PILE_ID_TAB3 = 10 +BYTE CONST PILE_ID_TAB4 = 11 +BYTE CONST PILE_ID_TAB5 = 12 +BYTE CONST PILE_ID_TAB6 = 13 + + +LABEL __skip_lib_pileconsts + +#IFEND diff --git a/piles.c65 b/piles.c65 index 929082e..cf10014 100644 --- a/piles.c65 +++ b/piles.c65 @@ -73,6 +73,47 @@ ASM !fill 14, 0 ENDASM + +// ============================================================================ +// FUNC clear_all_piles +// Clear all piles (waste, foundations, tableaus) by setting their counts to 0 +// Stock is not cleared as it's managed by stock_init() +// ============================================================================ +FUNC clear_all_piles + WORD ptr @ $f0 + + // Clear waste pile + POINTER ptr -> pile_waste + POKE ptr[0] , 0 + + // Clear all foundation piles + POINTER ptr -> pile_found0 + POKE ptr[0] , 0 + POINTER ptr -> pile_found1 + POKE ptr[0] , 0 + POINTER ptr -> pile_found2 + POKE ptr[0] , 0 + POINTER ptr -> pile_found3 + POKE ptr[0] , 0 + + // Clear all tableau piles + POINTER ptr -> pile_tab0 + POKE ptr[0] , 0 + POINTER ptr -> pile_tab1 + POKE ptr[0] , 0 + POINTER ptr -> pile_tab2 + POKE ptr[0] , 0 + POINTER ptr -> pile_tab3 + POKE ptr[0] , 0 + POINTER ptr -> pile_tab4 + POKE ptr[0] , 0 + POINTER ptr -> pile_tab5 + POKE ptr[0] , 0 + POINTER ptr -> pile_tab6 + POKE ptr[0] , 0 +FEND + + LABEL __skip_lib_piles #IFEND diff --git a/utils.c65 b/utils.c65 index 9935784..ed82796 100644 --- a/utils.c65 +++ b/utils.c65 @@ -117,6 +117,14 @@ FUNC set_vic_ecm ENDASM FEND +FUNC clear_vic_ecm + ASM + lda $d011 + and #$1f ; leave DEN bit + all scroll stuff, clear ECM bit + sta $d011 + ENDASM +FEND + // Wait for a keypress using direct CIA keyboard scan // No interrupts needed FUNC wait_key From 263e019669bda31be27f69244866eae2f1ae73b6 Mon Sep 17 00:00:00 2001 From: Mattias Hansson Date: Sun, 11 Jan 2026 09:57:04 +0100 Subject: [PATCH 2/6] Game menu working. Move from foundation working. --- gameloop.c65 | 28 +++++++++++++-- gamemenu.c65 | 99 ++++++++++++++++++++++++++++++++++------------------ 2 files changed, 91 insertions(+), 36 deletions(-) diff --git a/gameloop.c65 b/gameloop.c65 index 2857265..f434e27 100644 --- a/gameloop.c65 +++ b/gameloop.c65 @@ -403,6 +403,16 @@ FUNC get_selected_card_info(out:{BYTE card_id} out:{BYTE valid}) EXIT ENDIF + // Foundation: top card + IF game_selected_pile >= PILE_ID_FOUND0 + IF game_selected_pile <= PILE_ID_FOUND3 + card = PEEK pile_ptr[pile_count] + card_id = card & CARD_MASK + valid = 1 + EXIT + ENDIF + ENDIF + // Tableau: bottom card of selection (the clicked card) IF game_selected_pile >= PILE_ID_TAB0 IF game_selected_pile <= PILE_ID_TAB6 @@ -661,7 +671,7 @@ FUNC handle_click_on_pile({BYTE clicked_pile} {BYTE click_row}) IF game_selected_pile == PILE_ID_NONE #PRAGMA _P_USE_LONG_JUMP 0 - // Can only select waste or tableau + // Can only select waste, tableau, or foundation (if enabled) IF clicked_pile == PILE_ID_WASTE game_selected_pile = PILE_ID_WASTE game_selected_card_count = 1 @@ -702,7 +712,21 @@ FUNC handle_click_on_pile({BYTE clicked_pile} {BYTE click_row}) IF game_selected_card_count > 0 game_selected_pile = clicked_pile ENDIF - EXIT + ENDIF + + // Foundation selection (if enabled) + IF game_allow_found_to_tab + IF is_foundation + // Check if foundation has cards before allowing selection + WORD found_ptr @ $e8 + pile_id_to_pointer(clicked_pile, found_ptr) + BYTE found_count + found_count = PEEK found_ptr[0] + IF found_count > 0 + game_selected_pile = clicked_pile + game_selected_card_count = 1 + ENDIF + ENDIF ENDIF EXIT diff --git a/gamemenu.c65 b/gamemenu.c65 index db9be21..35cce13 100644 --- a/gamemenu.c65 +++ b/gamemenu.c65 @@ -85,15 +85,72 @@ FUNC menu_print_string({WORD screen_pos @ $e0} {WORD str_ptr @ $e2}) FEND +// ============================================================================ +// FUNC menu_update_draw_mode +// Update only the draw mode display (row 6) - no screen clear +// ============================================================================ +FUNC menu_update_draw_mode + WORD screen_pos @ $e0 + + // Clear the value area (cols 19-23) + screen_pos = 6*40+$0400+19 + POKE screen_pos[0] , 32 // space + POKE screen_pos[1] , 32 + POKE screen_pos[2] , 32 + POKE screen_pos[3] , 32 + POKE screen_pos[4] , 32 + + // Draw mode value - both values at same position (col 19) + // Show both [1] [3] with brackets around the selected one + screen_pos = 6*40+$0400+19 + IF game_draw_mode == 1 + POKE screen_pos[0] , 27 // '[' screen code + POKE screen_pos[1] , 49 // '1' + POKE screen_pos[2] , 29 // ']' screen code + POKE screen_pos[3] , 32 // space + POKE screen_pos[4] , 51 // '3' + ELSE + POKE screen_pos[0] , 49 // '1' + POKE screen_pos[1] , 32 // space + POKE screen_pos[2] , 27 // '[' screen code + POKE screen_pos[3] , 51 // '3' + POKE screen_pos[4] , 29 // ']' screen code + ENDIF +FEND + + +// ============================================================================ +// FUNC menu_update_found_to_tab +// Update only the foundation to tableau display (row 9) - no screen clear +// ============================================================================ +FUNC menu_update_found_to_tab + WORD screen_pos @ $e4 + WORD str_ptr @ $e6 + + // Clear the value area (cols 28-30) + screen_pos = 9*40+$0400+28 + POKE screen_pos[0] , 32 // space + POKE screen_pos[1] , 32 + POKE screen_pos[2] , 32 + + // Foundation to Tableau value + screen_pos = 9*40+$0400+28 + IF game_allow_found_to_tab == 0 + POINTER str_ptr -> str_off + ELSE + POINTER str_ptr -> str_on + ENDIF + menu_print_string(screen_pos, str_ptr) +FEND + + // ============================================================================ // FUNC menu_render -// Render the menu screen with current settings +// Render the full menu screen with current settings // ============================================================================ FUNC menu_render WORD screen_pos @ $f0 WORD str_ptr @ $f2 - BYTE draw_indicator - BYTE found_to_tab_indicator // Clear screen fill_mem($0400, $0400+999, 32) // 32 = space character @@ -109,28 +166,8 @@ FUNC menu_render POINTER str_ptr -> str_draw_mode menu_print_string(screen_pos, str_ptr) - // Draw mode value - screen_pos = 6*40+$0400+24 - IF game_draw_mode == 1 - POKE screen_pos[0] , 49 // '1' - ELSE - POKE screen_pos[0] , 51 // '3' - ENDIF - - // Draw mode indicator - IF game_draw_mode == 1 - draw_indicator = 91 // '[' - screen_pos = 6*40+$0400+23 - POKE screen_pos[0] , draw_indicator - screen_pos = 6*40+$0400+25 - POKE screen_pos[0] , 93 // ']' - ELSE - draw_indicator = 91 // '[' - screen_pos = 6*40+$0400+26 - POKE screen_pos[0] , draw_indicator - screen_pos = 6*40+$0400+28 - POKE screen_pos[0] , 93 // ']' - ENDIF + // Draw mode value and indicators + menu_update_draw_mode() // Foundation to Tableau option (row 9) screen_pos = 9*40+$0400+4 @@ -138,13 +175,7 @@ FUNC menu_render menu_print_string(screen_pos, str_ptr) // Foundation to Tableau value - screen_pos = 9*40+$0400+28 - IF game_allow_found_to_tab == 0 - POINTER str_ptr -> str_off - ELSE - POINTER str_ptr -> str_on - ENDIF - menu_print_string(screen_pos, str_ptr) + menu_update_found_to_tab() // Instructions for resume/restart (row 14+) screen_pos = 14*40+$0400+4 @@ -209,7 +240,7 @@ FUNC menu_show(out:{BYTE action}) ELSE game_draw_mode = 1 ENDIF - menu_render() + menu_update_draw_mode() CASE KEY_F3 // Toggle foundation to tableau @@ -218,7 +249,7 @@ FUNC menu_show(out:{BYTE action}) ELSE game_allow_found_to_tab = 0 ENDIF - menu_render() + menu_update_found_to_tab() CASE KEY_F7 // Resume game From a799d7695cfe304483591de94088f9233addbba9 Mon Sep 17 00:00:00 2001 From: Mattias Hansson Date: Sun, 11 Jan 2026 18:38:24 +0100 Subject: [PATCH 3/6] Game menu working. Issue where reading keys cause interference with JOY/Mouse atm. --- cardgame.c65 | 8 +- cardsprites.c65 | 87 +++++++++- .../charpad_rank_sprites - Projectv2.asm | 149 ++++++++++++++++++ charpad_rank_sprites/charpad_rank_sprites.spd | Bin 980 -> 1044 bytes gameloop.c65 | 23 +-- gamemenu.c65 | 48 +++++- utils.c65 | 12 ++ 7 files changed, 305 insertions(+), 22 deletions(-) create mode 100644 charpad_rank_sprites/charpad_rank_sprites - Projectv2.asm diff --git a/cardgame.c65 b/cardgame.c65 index bd9a178..e6ce36c 100644 --- a/cardgame.c65 +++ b/cardgame.c65 @@ -12,6 +12,7 @@ ASM *=$3000 ENDASM +#INCLUDE #INCLUDE "utils.c65" #INCLUDE "cardconsts.c65" #INCLUDE "pileconsts.c65" @@ -24,6 +25,7 @@ ENDASM #INCLUDE "joystick.c65" #INCLUDE "mouse.c65" #INCLUDE "pointer.c65" +#INCLUDE "cardsprites.c65" #INCLUDE "keyboard.c65" #INCLUDE "gamemenu.c65" //#INCLUDE "joysticktests.c65" @@ -40,6 +42,9 @@ FUNC main sei ENDASM + lib_c64scr_blank() // screen later enabled just before gameloop starts. + + // Save zero page for potential kernal operations later save_zeropage() @@ -48,13 +53,12 @@ FUNC main // Normal game: random shuffle and deal // Seed RNG with multiple entropy sources for better randomness WORD timer_seed - BYTE raster @ $d012 BYTE extra_entropy BYTE warmup // Combine CIA timer with raster position timer_seed = PEEKW $DC04 - extra_entropy = raster + extra_entropy = vic_raster timer_seed = timer_seed ^ extra_entropy rand_seed(timer_seed) diff --git a/cardsprites.c65 b/cardsprites.c65 index 58558ea..58a258a 100644 --- a/cardsprites.c65 +++ b/cardsprites.c65 @@ -709,6 +709,33 @@ ASM !8 $00 ENDASM +// Sprite 25: "RETURN FOR MENU" text (XORed with $ff) +LABEL sprite_text_return_menu +ASM + !8 $ee, $ea, $e9 + !8 $a8, $4a, $ad + !8 $cc, $4a, $cb + !8 $a8, $4a, $a9 + !8 $ae, $4e, $a9 + !8 $00, $00, $00 + !8 $03, $bb, $80 + !8 $02, $2a, $80 + !8 $03, $2b, $00 + !8 $02, $2a, $80 + !8 $02, $3a, $80 + !8 $00, $00, $00 + !8 $0a, $e9, $50 + !8 $0e, $8d, $50 + !8 $0a, $cb, $50 + !8 $0a, $89, $50 + !8 $0a, $e9, $70 + !8 $00, $00, $00 + !8 $00, $00, $00 + !8 $00, $00, $00 + !8 $00, $00, $00 + !8 $fe +ENDASM + // ============================================================================ // CARD DISPLAY SPRITE MANAGEMENT // ============================================================================ @@ -716,7 +743,7 @@ ENDASM // Uses sprites 1-5 (sprite 0 is reserved for pointer) // ============================================================================ -// VIC-II Sprite registers for card display (sprites 1-5) +// VIC-II Sprite registers for card display (sprites 1-5) and menu hint (sprite 6) // Note: sprite_x_msb and sprite_enable already declared in pointer.c65 BYTE sprite_x1 @ $d002 // Sprite 1 X BYTE sprite_y1 @ $d003 // Sprite 1 Y @@ -728,16 +755,20 @@ BYTE sprite_x4 @ $d008 // Sprite 4 X BYTE sprite_y4 @ $d009 // Sprite 4 Y BYTE sprite_x5 @ $d00a // Sprite 5 X BYTE sprite_y5 @ $d00b // Sprite 5 Y +BYTE sprite_x6 @ $d00c // Sprite 6 X (menu hint) +BYTE sprite_y6 @ $d00d // Sprite 6 Y (menu hint) BYTE sprite_color1 @ $d028 // Sprite 1 color BYTE sprite_color2 @ $d029 // Sprite 2 color BYTE sprite_color3 @ $d02a // Sprite 3 color BYTE sprite_color4 @ $d02b // Sprite 4 color BYTE sprite_color5 @ $d02c // Sprite 5 color +BYTE sprite_color6 @ $d02d // Sprite 6 color (menu hint) BYTE sprite_pointer1 @ $07f9 // Sprite 1 pointer BYTE sprite_pointer2 @ $07fa // Sprite 2 pointer BYTE sprite_pointer3 @ $07fb // Sprite 3 pointer BYTE sprite_pointer4 @ $07fc // Sprite 4 pointer BYTE sprite_pointer5 @ $07fd // Sprite 5 pointer +BYTE sprite_pointer6 @ $07fe // Sprite 6 pointer (menu hint) // Card display sprite positions (upper right corner) // Screen starts at (24,50), size 320x200 @@ -888,12 +919,64 @@ FEND FUNC card_display_hide BYTE temp - // Disable sprites 1-5, keep sprite 0 (pointer) + // Disable sprites 1-5, keep sprite 0 (pointer) and sprite 6 (menu hint) temp = sprite_enable temp = temp & %11000001 sprite_enable = temp FEND + +// ============================================================================ +// MENU HINT SPRITE MANAGEMENT +// ============================================================================ +// "RETURN FOR MENU" text displayed at bottom of screen during gameplay +// Uses sprite 6 +// ============================================================================ + +// Menu hint sprite position (bottom right of screen) +WORD CONST MENU_HINT_X = 320 // Right edge (beyond 255, needs X MSB) +BYTE CONST MENU_HINT_Y = 230 // Bottom of screen (50 + 200 - 21 sprite height) + +// Sprite block for "RETURN FOR MENU" text (sprite 25) +BYTE CONST SPRITE_BLOCK_MENU_HINT = 162 // 137 + 25 = 162 + +// ============================================================================ +// FUNC menu_hint_show +// Show "RETURN FOR MENU" sprite at bottom of screen +// Call this when entering gameplay +// ============================================================================ +FUNC menu_hint_show + // Set sprite 6 pointer to "RETURN FOR MENU" sprite + sprite_pointer6 = SPRITE_BLOCK_MENU_HINT + + // Position at bottom right + sprite_x6 = MENU_HINT_X + sprite_y6 = MENU_HINT_Y + + // Set X MSB for sprite 6 (bit 6) if X > 255 + IF MENU_HINT_X > 255 + sprite_x_msb = sprite_x_msb | %01000000 + ENDIF + + // Set color to blue (6) + sprite_color6 = 6 + + // Enable sprite 6 + sprite_enable = sprite_enable | %01000000 +FEND + + +// ============================================================================ +// FUNC menu_hint_hide +// Hide "RETURN FOR MENU" sprite +// Call this when entering menu +// ============================================================================ +FUNC menu_hint_hide + // Disable sprite 6 + sprite_enable = sprite_enable & %10111111 +FEND + + LABEL __skip_lib_cardsprites #IFEND diff --git a/charpad_rank_sprites/charpad_rank_sprites - Projectv2.asm b/charpad_rank_sprites/charpad_rank_sprites - Projectv2.asm new file mode 100644 index 0000000..6eb75e8 --- /dev/null +++ b/charpad_rank_sprites/charpad_rank_sprites - Projectv2.asm @@ -0,0 +1,149 @@ + +; Generated by SpritePad C64 - Subchrist Software, 2003-2025. +; Assemble with 64TASS or similar. + + +; Colour values... + +colr_vic_bg0 = 0 +colr_vic_sprite_mc1 = 0 +colr_vic_sprite_mc2 = 1 + + +; Quantities and dimensions... + +sprite_count = 16 + + +; Data block addresses (dummy values) and sizes... + +addr_spriteset_data = $1000 +size_spriteset_data = $400 ; (1024 bytes) + +addr_spriteset_attrib_data = $1000 +size_spriteset_attrib_data = $10 ; (16 bytes) + + + + + +; * INSERT EXAMPLE PROGRAM HERE! * (or just include this file in your project). + + + + +; SpriteSet Data... +; 16 images, 64 bytes per image, total size is 1024 ($400) bytes. + +* = addr_spriteset_data +spriteset_data + +sprite_image_0 +.byte $ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ef,$ff,$ff +.byte $ef,$ff,$ff,$c7,$ff,$ff,$c7,$ff,$ff,$93,$ff,$ff,$93,$ff,$ff,$39 +.byte $ff,$ff,$01,$ff,$fe,$00,$ff,$fe,$7c,$ff,$fc,$38,$7f,$fc,$38,$7f +.byte $ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$01 + +sprite_image_1 +.byte $ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$83,$ff,$ff +.byte $01,$ff,$ff,$39,$ff,$ff,$f9,$ff,$ff,$f1,$ff,$ff,$e3,$ff,$ff,$c7 +.byte $ff,$ff,$8f,$ff,$ff,$19,$ff,$ff,$39,$ff,$ff,$01,$ff,$ff,$01,$ff +.byte $ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$01 + +sprite_image_2 +.byte $ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$01,$ff,$ff +.byte $01,$ff,$ff,$33,$ff,$ff,$e7,$ff,$ff,$cf,$ff,$ff,$83,$ff,$ff,$81 +.byte $ff,$ff,$f9,$ff,$ff,$f9,$ff,$ff,$39,$ff,$ff,$01,$ff,$ff,$83,$ff +.byte $ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$01 + +sprite_image_3 +.byte $ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$f3,$ff,$ff +.byte $e3,$ff,$ff,$c3,$ff,$ff,$83,$ff,$ff,$13,$ff,$fe,$33,$ff,$fe,$00 +.byte $ff,$fe,$00,$ff,$ff,$f3,$ff,$ff,$f3,$ff,$ff,$e1,$ff,$ff,$e1,$ff +.byte $ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$01 + +sprite_image_4 +.byte $ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$01,$ff,$ff +.byte $01,$ff,$ff,$3f,$ff,$ff,$3f,$ff,$ff,$03,$ff,$ff,$01,$ff,$ff,$f9 +.byte $ff,$ff,$f9,$ff,$ff,$39,$ff,$ff,$39,$ff,$ff,$01,$ff,$ff,$83,$ff +.byte $ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$01 + +sprite_image_5 +.byte $ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$c3,$ff,$ff +.byte $83,$ff,$ff,$1f,$ff,$ff,$3f,$ff,$ff,$03,$ff,$ff,$01,$ff,$ff,$39 +.byte $ff,$ff,$39,$ff,$ff,$39,$ff,$ff,$39,$ff,$ff,$01,$ff,$ff,$83,$ff +.byte $ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$01 + +sprite_image_6 +.byte $ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$01,$ff,$ff +.byte $01,$ff,$ff,$39,$ff,$ff,$f3,$ff,$ff,$f3,$ff,$ff,$e7,$ff,$ff,$e7 +.byte $ff,$ff,$e7,$ff,$ff,$cf,$ff,$ff,$cf,$ff,$ff,$cf,$ff,$ff,$cf,$ff +.byte $ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$01 + +sprite_image_7 +.byte $ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$83,$ff,$ff +.byte $01,$ff,$ff,$39,$ff,$ff,$39,$ff,$ff,$39,$ff,$ff,$83,$ff,$ff,$01 +.byte $ff,$ff,$39,$ff,$ff,$39,$ff,$ff,$39,$ff,$ff,$01,$ff,$ff,$83,$ff +.byte $ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$01 + +sprite_image_8 +.byte $ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$83,$ff,$ff +.byte $01,$ff,$ff,$39,$ff,$ff,$39,$ff,$ff,$39,$ff,$ff,$39,$ff,$ff,$01 +.byte $ff,$ff,$81,$ff,$ff,$f9,$ff,$ff,$f1,$ff,$ff,$83,$ff,$ff,$87,$ff +.byte $ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$01 + +sprite_image_9 +.byte $ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$fe,$61,$ff,$fe +.byte $40,$ff,$fe,$4c,$ff,$fe,$4c,$ff,$fe,$4c,$ff,$fe,$4c,$ff,$fe,$4c +.byte $ff,$fe,$4c,$ff,$fe,$4c,$ff,$fe,$4c,$ff,$fe,$40,$ff,$fe,$61,$ff +.byte $ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$01 + +sprite_image_10 +.byte $ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$e1,$ff,$ff +.byte $e1,$ff,$ff,$f3,$ff,$ff,$f3,$ff,$ff,$f3,$ff,$ff,$f3,$ff,$ff,$f3 +.byte $ff,$ff,$f3,$ff,$ff,$33,$ff,$ff,$33,$ff,$ff,$03,$ff,$ff,$87,$ff +.byte $ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$01 + +sprite_image_11 +.byte $ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$83,$ff,$ff +.byte $01,$ff,$ff,$39,$ff,$ff,$39,$ff,$ff,$39,$ff,$ff,$39,$ff,$ff,$39 +.byte $ff,$ff,$39,$ff,$ff,$39,$ff,$ff,$39,$ff,$ff,$01,$ff,$ff,$83,$ff +.byte $ff,$f1,$ff,$ff,$f9,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$01 + +sprite_image_12 +.byte $ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$fe,$10,$ff,$fe +.byte $10,$ff,$ff,$33,$ff,$ff,$27,$ff,$ff,$0f,$ff,$ff,$1f,$ff,$ff,$0f +.byte $ff,$ff,$27,$ff,$ff,$33,$ff,$ff,$39,$ff,$fe,$10,$ff,$fe,$10,$ff +.byte $ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$01 + +sprite_image_13 +.byte $51,$15,$11,$55,$75,$d5,$11,$73,$b5,$55,$75,$75,$55,$15,$11,$ff +.byte $ff,$ff,$f1,$b3,$11,$f7,$b5,$75,$f1,$b5,$31,$fd,$b5,$73,$f1,$b3 +.byte $15,$ff,$ff,$ff,$ff,$11,$11,$ff,$d5,$d7,$ff,$15,$11,$ff,$75,$75 +.byte $ff,$11,$11,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$01 + +sprite_image_14 +.byte $b3,$ff,$ff,$b5,$ff,$ff,$b5,$ff,$ff,$b5,$ff,$ff,$b3,$ff,$ff,$ff +.byte $ff,$ff,$1f,$ff,$ff,$7f,$ff,$ff,$1f,$ff,$ff,$df,$ff,$ff,$1f,$ff +.byte $ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff +.byte $ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$01 + +sprite_image_15 +.byte $11,$15,$16,$57,$b5,$52,$33,$b5,$34,$57,$b5,$56,$51,$b1,$56,$ff +.byte $ff,$ff,$fc,$44,$7f,$fd,$d5,$7f,$fc,$d4,$ff,$fd,$d5,$7f,$fd,$c5 +.byte $7f,$ff,$ff,$ff,$f5,$16,$af,$f1,$72,$af,$f5,$34,$af,$f5,$76,$af +.byte $f5,$16,$8f,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$01 + + + +; SpriteSet Attribute Data... +; 16 attributes, 1 per image, 8 bits each, total size is 16 ($10) bytes. +; nb. Upper nybbles = MYXV, lower nybbles = colour (0-15). + +* = addr_spriteset_attrib_data +spriteset_attrib_data + +.byte $01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01 + + + diff --git a/charpad_rank_sprites/charpad_rank_sprites.spd b/charpad_rank_sprites/charpad_rank_sprites.spd index d48f8d9524bfc6a4ffaae6503fc3e1aedb3b909f..83343e2a29d975ab3dabd37cbe5143030e0cb40c 100644 GIT binary patch delta 80 zcmcb@K81rVIKYLKL0}`>Ic5hzQL*r?LB?B6!ncM6ZVdbX|NkGC`oCA}|6KVGB>x_* b2a0?ZTmP|W{a2IqU(42i73&9zzyTuwETS-K delta 15 WcmbQjafO{NIKYLKfqx_0Ic5MMmIMI+ diff --git a/gameloop.c65 b/gameloop.c65 index f434e27..6d0e985 100644 --- a/gameloop.c65 +++ b/gameloop.c65 @@ -883,17 +883,16 @@ FUNC game_loop mem_copy(src_ptr, src_end_ptr, dst_ptr) // Copy card sprite data to $2240 (sprite blocks 137+) - // 25 sprites × 64 bytes = 1600 bytes + // 26 sprites × 64 bytes = 1664 bytes (includes "RETURN FOR MENU" sprite) POINTER src_ptr -> sprite_rank_ace POINTER dst_ptr -> $2240 - mem_copy_range(src_ptr, dst_ptr, 64*25) + mem_copy_range(src_ptr, dst_ptr, 64*26) // Outer loop for restart handling WHILE 1 - // Initialize mouse and pointer mouse_init() pointer_init(160, 100, color_red, 136) // Sprite block 136 = $2200, bright red color @@ -904,6 +903,9 @@ FUNC game_loop // Initialize card display sprites card_display_init() + // Show menu hint sprite + menu_hint_show() + #IFDEF TEST_GAMES // Test game options (comment/uncomment one): //setup_test_game_tall_tableau() // K->3 in tab0, red 2 in waste @@ -911,7 +913,6 @@ FUNC game_loop setup_test_game_overflow() // K->A in tab3 to test screen overflow #IFEND - BYTE raster @ $d012 BYTE menu_key BYTE menu_action @@ -921,10 +922,12 @@ FUNC game_loop render_all_piles_initial() + lib_c64scr_show() + // Inner game loop WHILE 1 // Wait for raster to avoid tearing - WHILE raster != 250 + WHILE vic_raster != 250 WEND // Check for menu key (Return or Left Arrow) @@ -939,16 +942,6 @@ FUNC game_loop fill_mem($0400, $0400+999, 0) render_all_piles_initial() ENDIF - IF menu_key == KEY_LEFT_ARROW - menu_show(menu_action) - IF menu_action == MENU_ACTION_RESTART - // Restart requested - break out to restart - BREAK - ENDIF - // Clear screen and re-render after resume - fill_mem($0400, $0400+999, 0) - render_all_piles_initial() - ENDIF // Read mouse input (Port 1) mouse_read() diff --git a/gamemenu.c65 b/gamemenu.c65 index 35cce13..9a92eba 100644 --- a/gamemenu.c65 +++ b/gamemenu.c65 @@ -149,8 +149,8 @@ FEND // Render the full menu screen with current settings // ============================================================================ FUNC menu_render - WORD screen_pos @ $f0 - WORD str_ptr @ $f2 + WORD screen_pos + WORD str_ptr // Clear screen fill_mem($0400, $0400+999, 32) // 32 = space character @@ -178,7 +178,7 @@ FUNC menu_render menu_update_found_to_tab() // Instructions for resume/restart (row 14+) - screen_pos = 14*40+$0400+4 + screen_pos = 12*40+$0400+4 POINTER str_ptr -> str_inst_f7 menu_print_string(screen_pos, str_ptr) @@ -186,6 +186,17 @@ FUNC menu_render POINTER str_ptr -> str_inst_f8 menu_print_string(screen_pos, str_ptr) + // License (row 23, centered) + screen_pos = 20*40+$0400+2 + POINTER str_ptr -> str_controls + menu_print_string(screen_pos, str_ptr) + + + // License (row 23, centered) + screen_pos = 23*40+$0400+1 + POINTER str_ptr -> str_license + menu_print_string(screen_pos, str_ptr) + // Set border color to menu color (purple) POKE $d020 , 4 FEND @@ -196,6 +207,9 @@ FEND // Display menu and handle input, return action to take // ============================================================================ FUNC menu_show(out:{BYTE action}) + + lib_c64scr_blank() + BYTE key BYTE prev_key @@ -206,6 +220,9 @@ FUNC menu_show(out:{BYTE action}) game_selected_pile = PILE_ID_NONE game_selected_card_count = 0 + // Hide menu hint sprite + menu_hint_hide() + // Save current screen state menu_save_screen() @@ -219,6 +236,8 @@ FUNC menu_show(out:{BYTE action}) // Render menu menu_render() + lib_c64scr_show() + // Wait for key release first (debounce) key_scan(key) WHILE key != KEY_NONE @@ -256,6 +275,11 @@ FUNC menu_show(out:{BYTE action}) action = MENU_ACTION_RESUME BREAK + CASE KEY_RETURN + // Resume game + action = MENU_ACTION_RESUME + BREAK + CASE KEY_F8 // Restart game (SHIFT+F7) action = MENU_ACTION_RESTART @@ -272,6 +296,8 @@ FUNC menu_show(out:{BYTE action}) key_scan(key) WEND + lib_c64scr_blank() + // Restore screen or clear for restart IF action == MENU_ACTION_RESUME menu_restore_screen() @@ -288,8 +314,13 @@ FUNC menu_show(out:{BYTE action}) // Re-enable pointer sprite (game loop will handle card display sprites) pointer_enable(1) + // Show menu hint sprite again + menu_hint_show() + // Restore border color POKE $d020 , 1 // White + + lib_c64scr_show() FEND // Menu strings (null-terminated) - Using !scr for screen codes @@ -338,6 +369,17 @@ ASM !scr "F8: Restart game", 0 ENDASM +LABEL str_controls +ASM + !scr "1351 Mouse Port 1 / Joystick Port 2", 0 +ENDASM + +LABEL str_license +ASM + !scr "by Hackz0id/Siders 2026 (GNU GPL v2.0)", 0 +ENDASM + + LABEL __skip_lib_gamemenu diff --git a/utils.c65 b/utils.c65 index ed82796..c39346a 100644 --- a/utils.c65 +++ b/utils.c65 @@ -4,6 +4,18 @@ GOTO __skip_lib_utils +// ============================================================================ +// HARDWARE REGISTER DEFINITIONS +// ============================================================================ +// Shared hardware register references to avoid duplicate declarations +// ============================================================================ + +BYTE vic_raster @ $d012 // VIC-II raster line register + +// ============================================================================ +// UTILITY FUNCTIONS +// ============================================================================ + FUNC fill_mem({WORD start_addr @ $fb} {WORD end_addr} {BYTE value}) FOR start_addr = start_addr TO end_addr From f4bff18f29a52f16aaf5a333e17ccfc095ea45f0 Mon Sep 17 00:00:00 2001 From: Mattias Hansson Date: Sun, 11 Jan 2026 21:00:08 +0100 Subject: [PATCH 4/6] Fixed game menu. Fixed jerky mouse movement while scanning keyboard (cause was scanning keyboard right before reading mouse). Menu code tighter. --- gameloop.c65 | 12 +-- gamemenu.c65 | 47 ++++------- keyboard.c65 | 228 +++++++++++++++++++++++++++++---------------------- 3 files changed, 152 insertions(+), 135 deletions(-) diff --git a/gameloop.c65 b/gameloop.c65 index 6d0e985..f30fad4 100644 --- a/gameloop.c65 +++ b/gameloop.c65 @@ -930,7 +930,13 @@ FUNC game_loop WHILE vic_raster != 250 WEND - // Check for menu key (Return or Left Arrow) + // Read mouse input FIRST (before keyboard scan) + // Per Commodore 1351 manual: keyboard scan lines affect POT registers + // so mouse must be read before keyboard to avoid interference + mouse_read() + pointer_update_mouse(mouse_delta_x, mouse_delta_y) + + // Check for menu key (Return or Left Arrow) AFTER mouse key_scan(menu_key) IF menu_key == KEY_RETURN menu_show(menu_action) @@ -943,10 +949,6 @@ FUNC game_loop render_all_piles_initial() ENDIF - // Read mouse input (Port 1) - mouse_read() - pointer_update_mouse(mouse_delta_x, mouse_delta_y) - // Read joystick input (Port 2) joy_read_port2() pointer_update_joystick(joy_state) diff --git a/gamemenu.c65 b/gamemenu.c65 index 9a92eba..799befd 100644 --- a/gamemenu.c65 +++ b/gamemenu.c65 @@ -149,53 +149,34 @@ FEND // Render the full menu screen with current settings // ============================================================================ FUNC menu_render - WORD screen_pos - WORD str_ptr - // Clear screen fill_mem($0400, $0400+999, 32) // 32 = space character fill_mem($D800, $D800+999, 14) // Light blue text // Title (centered at row 2) - screen_pos = 2*40+$0400+8 - POINTER str_ptr -> str_title - menu_print_string(screen_pos, str_ptr) + menu_print_string(2*40+$0400+8, @str_title) // Draw mode option (row 6) - screen_pos = 6*40+$0400+4 - POINTER str_ptr -> str_draw_mode - menu_print_string(screen_pos, str_ptr) + menu_print_string(6*40+$0400+4, @str_draw_mode) // Draw mode value and indicators menu_update_draw_mode() // Foundation to Tableau option (row 9) - screen_pos = 9*40+$0400+4 - POINTER str_ptr -> str_found_to_tab - menu_print_string(screen_pos, str_ptr) + menu_print_string(9*40+$0400+4, @str_found_to_tab) // Foundation to Tableau value menu_update_found_to_tab() - // Instructions for resume/restart (row 14+) - screen_pos = 12*40+$0400+4 - POINTER str_ptr -> str_inst_f7 - menu_print_string(screen_pos, str_ptr) + // Instructions for resume/restart (row 12+) + menu_print_string(12*40+$0400+4, @str_inst_f7) + menu_print_string(15*40+$0400+4, @str_inst_runstop) - screen_pos = 15*40+$0400+4 - POINTER str_ptr -> str_inst_f8 - menu_print_string(screen_pos, str_ptr) + // Controls info (row 20) + menu_print_string(20*40+$0400+2, @str_controls) - // License (row 23, centered) - screen_pos = 20*40+$0400+2 - POINTER str_ptr -> str_controls - menu_print_string(screen_pos, str_ptr) - - - // License (row 23, centered) - screen_pos = 23*40+$0400+1 - POINTER str_ptr -> str_license - menu_print_string(screen_pos, str_ptr) + // License (row 23) + menu_print_string(23*40+$0400+1, @str_license) // Set border color to menu color (purple) POKE $d020 , 4 @@ -280,8 +261,8 @@ FUNC menu_show(out:{BYTE action}) action = MENU_ACTION_RESUME BREAK - CASE KEY_F8 - // Restart game (SHIFT+F7) + CASE KEY_RUNSTOP + // Restart game action = MENU_ACTION_RESTART BREAK ENDSWITCH @@ -364,9 +345,9 @@ ASM !scr "F7: Resume game", 0 ENDASM -LABEL str_inst_f8 +LABEL str_inst_runstop ASM - !scr "F8: Restart game", 0 + !scr "Run/Stop: Restart game", 0 ENDASM LABEL str_controls diff --git a/keyboard.c65 b/keyboard.c65 index b28cdc3..273c389 100644 --- a/keyboard.c65 +++ b/keyboard.c65 @@ -8,10 +8,14 @@ GOTO __skip_lib_keyboard // KEYBOARD INPUT DRIVER FOR COMMODORE 64 // ============================================================================ // Direct CIA keyboard matrix reading (no IRQ required) +// Uses non-interfering keyboard scan technique to avoid conflicts with +// joysticks on ports 1 and 2. // -// C64 Keyboard Matrix: -// $DC00 (Port A): Column select (write, active-low) -// $DC01 (Port B): Row read (read, active-low) +// Based on code from: +// https://codebase64.c64.org/doku.php?id=base:scanning_the_keyboard_without_joysticks_interfere +// +// This method converts keycodes to row/column values and performs double-check +// scanning to ensure joystick inputs don't interfere with keyboard detection. // // Usage: // #INCLUDE "keyboard.c65" @@ -23,11 +27,6 @@ GOTO __skip_lib_keyboard // CASE KEY_F3 // // handle F3 // ENDSWITCH -// -// // Debouncing: wait for KEY_NONE -// WHILE key != KEY_NONE -// key_scan(key) -// WEND // ============================================================================ // CIA Port addresses @@ -36,118 +35,153 @@ WORD CONST CIA_PORTB = $DC01 // Port B - COLUMN reading (read) WORD CONST CIA_DDRA = $DC02 // Port A Data Direction Register WORD CONST CIA_DDRB = $DC03 // Port B Data Direction Register -// Key constants +// Key constants (returned by key_scan) BYTE CONST KEY_NONE = 0 BYTE CONST KEY_F1 = 1 BYTE CONST KEY_F3 = 2 BYTE CONST KEY_F7 = 3 -BYTE CONST KEY_F8 = 4 // F7 + SHIFT +BYTE CONST KEY_RUNSTOP = 4 BYTE CONST KEY_RETURN = 5 -BYTE CONST KEY_LEFT_ARROW = 6 + +// C64 Keycode constants (for internal use) +// Based on: https://www.c64-wiki.com/wiki/Keyboard +BYTE CONST KEYCODE_RETURN = $01 +BYTE CONST KEYCODE_F7F8 = $03 +BYTE CONST KEYCODE_F1F2 = $04 +BYTE CONST KEYCODE_F3F4 = $05 +BYTE CONST KEYCODE_RUNSTOP = $3F + +// Row and column lookup tables for keyboard scanning +LABEL key_row_table +ASM + !8 $FE, $FD, $FB, $F7, $EF, $DF, $BF, $7F +ENDASM + +LABEL key_column_table +ASM + !8 $01, $02, $04, $08, $10, $20, $40, $80 +ENDASM + +// Key history for debouncing (one byte per tracked key) +BYTE key_history_f1 +BYTE key_history_f3 +BYTE key_history_f7 +BYTE key_history_return +BYTE key_history_runstop + +// ============================================================================ +// FUNC key_check_raw +// Low-level keyboard check for a specific keycode +// Uses non-interfering scan method with double-check to avoid joystick conflicts +// Returns: result = 1 if pressed, 0 if not pressed +// This comes from here: +// https://codebase64.c64.org/doku.php?id=base:scanning_the_keyboard_without_joysticks_interfere +// ============================================================================ +FUNC key_check_raw({BYTE keycode} io:{BYTE histv} out:{BYTE result}) + ASM + lda |keycode| + pha + lsr + lsr + lsr + tay + lda key_row_table,y + sta $dc00 + pla + and #$07 + tay + lda $dc01 + and key_column_table,y + bne _nokey + lda #$ff ; key is checked 2nd time, to be sure + sta $dc00 ; joy #1 and #2 are not interfering + lda $dc01 + and key_column_table,y + beq _nokey + + cmp |histv| ; history value? + bne _newkey + clc ; case #3, key is held down. keep history & set C=0 + bcc _backy + +_nokey + asl ; case #1, no keypress, update history value 2x & set C=0 + clc + !byte $24 ; BIT zero page - skips next 2-byte instruction +_newkey + sec ; case #2, key pressed&released. update history & set C=1 + sta |histv| + +_backy + lda #$ff + sta $dc00 ; set default value + lda #$7f + sta $dc01 ; set default value + + ; Convert carry flag to result: C=1 -> pressed (1), C=0 -> not pressed (0) + lda #0 + rol ; shift carry into bit 0 + sta |result| + ENDASM +FEND + // ============================================================================ // FUNC key_scan // Scan keyboard and return which key is pressed // Returns KEY_* constant (KEY_NONE if no relevant key pressed) -// C64 Keyboard Matrix: -// - Write to $DC00 (Port A) to select ROW -// - Read from $DC01 (Port B) to detect COLUMN +// Uses non-interfering keyboard scan to avoid joystick conflicts // ============================================================================ FUNC key_scan(out:{BYTE key_pressed}) - BYTE col_data - BYTE temp - BYTE shift_held - BYTE saved_ddra - BYTE saved_ddrb - BYTE saved_porta - BYTE row0_data - BYTE row1_data - BYTE row6_data + BYTE check_result key_pressed = KEY_NONE - shift_held = 0 - - // Save current DDR state - saved_ddra = PEEK CIA_DDRA - saved_ddrb = PEEK CIA_DDRB - saved_porta = PEEK CIA_PORTA // Set up DDR: Port A = output (rows), Port B = input (columns) - POKE CIA_DDRA , $FF - POKE CIA_DDRB , $00 + ASM + lda #$ff + sta $dc02 // CIA_DDRA + lda #$00 + sta $dc03 // CIA_DDRB + ENDASM - // Check F8 FIRST with two-read atomic check - // Read 1: Select rows 0+1+6 simultaneously ($BC = 10111100) - POKE CIA_PORTA , $BC - BYTE multi_row_data - multi_row_data = PEEK CIA_PORTB - - // Read 2: Select ONLY row 0 ($FE = 11111110) - POKE CIA_PORTA , $FE - col_data = PEEK CIA_PORTB - - // F8 detection: F7 pressed AND SHIFT pressed - // F7 at Row 0, Col 3 - check in row0 data - temp = col_data & $08 - IF temp == 0 - // F7 is pressed, now check if real SHIFT is pressed - // Compare multi-row read vs row-0-only read for cols 4 and 7 - // If col 4 or 7 was low in multi-row but NOT in row-0-only, it's SHIFT - BYTE shift_cols - shift_cols = multi_row_data & $90 // Cols 4 and 7 in multi-row read - temp = col_data & $90 // Cols 4 and 7 in row 0 only read - // If multi-row has a low bit that row-0-only doesn't, it's from rows 1 or 6 (SHIFT) - IF shift_cols != temp - key_pressed = KEY_F8 - ENDIF + // Check F1 + key_check_raw(KEYCODE_F1F2, key_history_f1, check_result) + IF check_result > 0 + key_pressed = KEY_F1 ENDIF - // If not F8, check other Row 0 keys (col_data already has row 0 from above) + // Check F3 IF key_pressed == KEY_NONE - - // Check F1 (Row 0, Column 4) - temp = col_data & $10 - IF temp == 0 - key_pressed = KEY_F1 - ENDIF - - // Check F3 (Row 0, Column 5) - IF key_pressed == KEY_NONE - temp = col_data & $20 - IF temp == 0 - key_pressed = KEY_F3 - ENDIF - ENDIF - - // Check F7 (Row 0, Column 3) - IF key_pressed == KEY_NONE - temp = col_data & $08 - IF temp == 0 - key_pressed = KEY_F7 - ENDIF - ENDIF - - // Check RETURN (Row 0, Column 1) - IF key_pressed == KEY_NONE - temp = col_data & $02 - IF temp == 0 - key_pressed = KEY_RETURN - ENDIF - ENDIF - - // Check Left Arrow (Row 0, Column 7) - IF key_pressed == KEY_NONE - temp = col_data & $80 - IF temp == 0 - key_pressed = KEY_LEFT_ARROW - ENDIF + key_check_raw(KEYCODE_F3F4, key_history_f3, check_result) + IF check_result > 0 + key_pressed = KEY_F3 ENDIF ENDIF - // Always restore saved DDR and port state - POKE CIA_PORTA , saved_porta - POKE CIA_DDRA , saved_ddra - POKE CIA_DDRB , saved_ddrb + // Check F7 + IF key_pressed == KEY_NONE + key_check_raw(KEYCODE_F7F8, key_history_f7, check_result) + IF check_result > 0 + key_pressed = KEY_F7 + ENDIF + ENDIF + + // Check RUN/STOP + IF key_pressed == KEY_NONE + key_check_raw(KEYCODE_RUNSTOP, key_history_runstop, check_result) + IF check_result > 0 + key_pressed = KEY_RUNSTOP + ENDIF + ENDIF + + // Check RETURN + IF key_pressed == KEY_NONE + key_check_raw(KEYCODE_RETURN, key_history_return, check_result) + IF check_result > 0 + key_pressed = KEY_RETURN + ENDIF + ENDIF FEND From 788c6ed60ddde8407949d31877db89eb6bd69a6c Mon Sep 17 00:00:00 2001 From: Mattias Hansson Date: Mon, 12 Jan 2026 13:39:27 +0100 Subject: [PATCH 5/6] Tightened up the code with better label and pointer handling. README improved. --- README.md | 21 ++++++--------------- carddeck.c65 | 29 +++++++---------------------- cardgame.c65 | 8 +------- cardmoves.c65 | 8 ++++---- gameloop.c65 | 12 ++---------- 5 files changed, 20 insertions(+), 58 deletions(-) diff --git a/README.md b/README.md index 01fbd87..f047d3c 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,17 @@ # Solitaire C64 -A classic Klondike solitaire card game for the Commodore 64, written in c65gm. +A classic Klondike solitaire card game for the Commodore 64. ## Features -- **Full Klondike Solitaire**: Stock, waste, 7 tableaus, and 4 foundation piles -- **Dual Input Support**: Play with joystick or 1351 mouse -- **Draw Modes**: Toggle between draw-1 and draw-3 gameplay -- **Custom Graphics**: Character-based card rendering using extended color mode -- **Sprite Cursor**: Visual pointer for card selection and movement -- **Smart Shuffling**: Hardware-seeded RNG for true randomness - -## Technical Details - -**Language**: c65gm (C-like language for 6502/C64) -**Memory Layout**: Code at $3000, custom charset at $2000 -**Graphics**: Extended Color Mode (ECM) with custom character set -**Input**: CIA joystick ports + 1351 proportional mouse support +- Full Klondike with draw-1 and draw-3 modes +- Joystick and 1351 mouse support +- Extended color mode graphics with sprite cursor +- Hardware-seeded RNG for shuffling ## Building -Requires the c65gm compiler and ACME assembler: +Requires the [c65gm compiler](https://git.techserio.com/mattiashz/c65gm) and ACME assembler: ```bash ./cm.sh diff --git a/carddeck.c65 b/carddeck.c65 index 53d26ca..eeb3038 100644 --- a/carddeck.c65 +++ b/carddeck.c65 @@ -95,28 +95,13 @@ FEND // Tab0=1, Tab1=2, Tab2=3, Tab3=4, Tab4=5, Tab5=6, Tab6=7 cards // Top card of each tableau is face up FUNC deal_tableaus - WORD ptr - - POINTER ptr -> pile_tab0 - deal_to_tableau(ptr, 1) - - POINTER ptr -> pile_tab1 - deal_to_tableau(ptr, 2) - - POINTER ptr -> pile_tab2 - deal_to_tableau(ptr, 3) - - POINTER ptr -> pile_tab3 - deal_to_tableau(ptr, 4) - - POINTER ptr -> pile_tab4 - deal_to_tableau(ptr, 5) - - POINTER ptr -> pile_tab5 - deal_to_tableau(ptr, 6) - - POINTER ptr -> pile_tab6 - deal_to_tableau(ptr, 7) + deal_to_tableau(@pile_tab0, 1) + deal_to_tableau(@pile_tab1, 2) + deal_to_tableau(@pile_tab2, 3) + deal_to_tableau(@pile_tab3, 4) + deal_to_tableau(@pile_tab4, 5) + deal_to_tableau(@pile_tab5, 6) + deal_to_tableau(@pile_tab6, 7) FEND // Clear the seen array diff --git a/cardgame.c65 b/cardgame.c65 index e6ce36c..52ee9e0 100644 --- a/cardgame.c65 +++ b/cardgame.c65 @@ -84,13 +84,7 @@ FUNC main set_vic_charmem(4) // $2000 set_vic_ecm() - - WORD charset_ptr - WORD charset_end_ptr - POINTER charset_ptr -> card_charset - POINTER charset_end_ptr -> card_charset_end - mem_copy(charset_ptr, charset_end_ptr, $2000) - //mem_copy_range(charset_ptr, $2000, 512) + mem_copy(@card_charset, @card_charset_end, $2000) fill_mem($d800, $d800+999, color_white) // Fill color mem POKE $d020 , color_white //color_grey diff --git a/cardmoves.c65 b/cardmoves.c65 index 091ca78..aa93729 100644 --- a/cardmoves.c65 +++ b/cardmoves.c65 @@ -224,7 +224,7 @@ FEND // Waste to Tableau FUNC move_waste_to_tab({WORD tab_ptr} out:{BYTE success}) - WORD waste_ptr @ $8c + WORD waste_ptr POINTER waste_ptr -> pile_waste BYTE card BYTE valid @@ -249,7 +249,7 @@ FEND // Waste to Foundation FUNC move_waste_to_found({WORD found_ptr} out:{BYTE success}) - WORD waste_ptr @ $aa + WORD waste_ptr POINTER waste_ptr -> pile_waste BYTE card BYTE valid @@ -273,7 +273,7 @@ FUNC move_waste_to_found({WORD found_ptr} out:{BYTE success}) FEND // Tableau to Foundation (top card only) -FUNC move_tab_to_found({WORD tab_ptr @ $8e} {WORD found_ptr} out:{BYTE success}) +FUNC move_tab_to_found({WORD tab_ptr} {WORD found_ptr} out:{BYTE success}) BYTE card BYTE valid BYTE is_facedown @@ -358,7 +358,7 @@ FUNC move_tab_to_tab({WORD src_ptr @ $b0} {WORD dst_ptr} {BYTE card_count} out:{ FEND // Foundation to Tableau (optional rule - some variants allow this) -FUNC move_found_to_tab({WORD found_ptr @ $84} {WORD tab_ptr} out:{BYTE success}) +FUNC move_found_to_tab({WORD found_ptr} {WORD tab_ptr} out:{BYTE success}) BYTE card BYTE valid diff --git a/gameloop.c65 b/gameloop.c65 index f30fad4..8b85c7b 100644 --- a/gameloop.c65 +++ b/gameloop.c65 @@ -871,22 +871,14 @@ FUNC game_loop BYTE button_state BYTE clicked BYTE is_won - WORD src_ptr @ $8a - WORD src_end_ptr - WORD dst_ptr // Copy sprite data to $2200 (sprite block 136) // $2000-$21FF reserved for charset - POINTER src_ptr -> pointer_sprite_data - POINTER src_end_ptr -> pointer_sprite_data_end - POINTER dst_ptr -> $2200 - mem_copy(src_ptr, src_end_ptr, dst_ptr) + mem_copy(@pointer_sprite_data, @pointer_sprite_data_end, $2200) // Copy card sprite data to $2240 (sprite blocks 137+) // 26 sprites × 64 bytes = 1664 bytes (includes "RETURN FOR MENU" sprite) - POINTER src_ptr -> sprite_rank_ace - POINTER dst_ptr -> $2240 - mem_copy_range(src_ptr, dst_ptr, 64*26) + mem_copy_range(@sprite_rank_ace, $2240, 64*26) From b27cadc740a595c8d4e3b6e311af921f2e5a5630 Mon Sep 17 00:00:00 2001 From: Mattias Hansson Date: Mon, 12 Jan 2026 13:48:28 +0100 Subject: [PATCH 6/6] Tightened up the code with better label and pointer handling. --- carddeck.c65 | 40 +++++++++++++--------------------------- 1 file changed, 13 insertions(+), 27 deletions(-) diff --git a/carddeck.c65 b/carddeck.c65 index eeb3038..94f4ea6 100644 --- a/carddeck.c65 +++ b/carddeck.c65 @@ -151,7 +151,6 @@ FEND // Validate entire deck across all piles // Returns: 0=OK, 1=duplicate, 2=missing FUNC validate_deck(out:{BYTE result}) - WORD ptr @ $98 WORD seen_ptr @ $fc BYTE pile_result BYTE i @@ -163,95 +162,82 @@ FUNC validate_deck(out:{BYTE result}) result = VALIDATE_OK // Check stock - POINTER ptr -> pile_stock - validate_pile(ptr, pile_result) + validate_pile(@pile_stock, pile_result) IF pile_result result = pile_result EXIT ENDIF // Check waste - POINTER ptr -> pile_waste - validate_pile(ptr, pile_result) + validate_pile(@pile_waste, pile_result) IF pile_result result = pile_result EXIT ENDIF // Check tableau piles - POINTER ptr -> pile_tab0 - validate_pile(ptr, pile_result) + validate_pile(@pile_tab0, pile_result) IF pile_result result = pile_result EXIT ENDIF - POINTER ptr -> pile_tab1 - validate_pile(ptr, pile_result) + validate_pile(@pile_tab1, pile_result) IF pile_result result = pile_result EXIT ENDIF - POINTER ptr -> pile_tab2 - validate_pile(ptr, pile_result) + validate_pile(@pile_tab2, pile_result) IF pile_result result = pile_result EXIT ENDIF - POINTER ptr -> pile_tab3 - validate_pile(ptr, pile_result) + validate_pile(@pile_tab3, pile_result) IF pile_result result = pile_result EXIT ENDIF - POINTER ptr -> pile_tab4 - validate_pile(ptr, pile_result) + validate_pile(@pile_tab4, pile_result) IF pile_result result = pile_result EXIT ENDIF - POINTER ptr -> pile_tab5 - validate_pile(ptr, pile_result) + validate_pile(@pile_tab5, pile_result) IF pile_result result = pile_result EXIT ENDIF - POINTER ptr -> pile_tab6 - validate_pile(ptr, pile_result) + validate_pile(@pile_tab6, pile_result) IF pile_result result = pile_result EXIT ENDIF // Check foundation piles - POINTER ptr -> pile_found0 - validate_pile(ptr, pile_result) + validate_pile(@pile_found0, pile_result) IF pile_result result = pile_result EXIT ENDIF - POINTER ptr -> pile_found1 - validate_pile(ptr, pile_result) + validate_pile(@pile_found1, pile_result) IF pile_result result = pile_result EXIT ENDIF - POINTER ptr -> pile_found2 - validate_pile(ptr, pile_result) + validate_pile(@pile_found2, pile_result) IF pile_result result = pile_result EXIT ENDIF - POINTER ptr -> pile_found3 - validate_pile(ptr, pile_result) + validate_pile(@pile_found3, pile_result) IF pile_result result = pile_result EXIT