solitaire-c64/gameloop.c65

966 lines
25 KiB
Text
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#IFNDEF __lib_gameloop
#DEFINE __lib_gameloop 1
#INCLUDE "cardconsts.c65"
#INCLUDE "piles.c65"
#INCLUDE "cardmoves.c65"
#INCLUDE "cardrender.c65"
#INCLUDE "mouse.c65"
#INCLUDE "pointer.c65"
#INCLUDE "cardsprites.c65"
GOTO __skip_lib_gameloop
// ============================================================================
// SOLITAIRE GAME LOOP AND INTERACTION SYSTEM
// ============================================================================
// This library provides the main game loop, coordinate-to-pile mapping,
// click detection, selection state, and selective rendering.
//
// Usage:
// #INCLUDE "gameloop.c65"
// game_init()
// 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
// Layout constants (in character coordinates)
BYTE CONST LAYOUT_STOCK_COL = 0
BYTE CONST LAYOUT_STOCK_ROW = 0
BYTE CONST LAYOUT_WASTE_COL = 6
BYTE CONST LAYOUT_WASTE_ROW = 0
BYTE CONST LAYOUT_FOUND_COL = 16 // First foundation column
BYTE CONST LAYOUT_FOUND_ROW = 0
BYTE CONST LAYOUT_TAB_COL = 0 // First tableau column
BYTE CONST LAYOUT_TAB_ROW = 8
BYTE CONST PILE_WIDTH = 5 // Cards are 5 chars wide
// ============================================================================
// FUNC pointer_to_char_coords
// Convert sprite pixel coordinates to character coordinates
// C64 sprite coords start at (24,50) for top-left of visible screen
// Sprite coords: X (0-511), Y (0-250)
// Char coords: col (0-39), row (0-24)
// ============================================================================
FUNC pointer_to_char_coords({WORD sprite_x} {BYTE sprite_y} out:{BYTE char_col} out:{BYTE char_row})
WORD temp_x
BYTE temp_y
// Subtract sprite offset (24 pixels X, 50 pixels Y)
temp_x = sprite_x - 24
temp_y = sprite_y - 50
// Divide X by 8 (shift right 3 times)
// For WORD: need to shift high byte into low byte
ASM
lda |temp_x|+1 // High byte
lsr // Shift right 1
sta |temp_x|+1
lda |temp_x| // Low byte
ror // Rotate right (carry from high byte)
lsr // Shift right 1
lsr // Shift right 1
sta |char_col|
ENDASM
// Divide Y by 8 (shift right 3 times)
ASM
lda |temp_y|
lsr
lsr
lsr
sta |char_row|
ENDASM
// Bounds checking (handle underflow as well)
IF char_col > 39
char_col = 0
ENDIF
IF char_row > 24
char_row = 0
ENDIF
FEND
// ============================================================================
// FUNC get_pile_at_coords
// Determine which pile (if any) is at the given character coordinates
// Returns PILE_ID_* constant
// ============================================================================
FUNC get_pile_at_coords({BYTE char_col} {BYTE char_row} out:{BYTE pile_id})
pile_id = PILE_ID_NONE
// Check top row (stock, waste, foundations)
IF char_row < 7 // Cards are 7 rows tall
// Stock: cols 0-4
IF char_col >= LAYOUT_STOCK_COL
IF char_col < LAYOUT_STOCK_COL+PILE_WIDTH
pile_id = PILE_ID_STOCK
EXIT
ENDIF
ENDIF
// Waste: cols 6-14 (fanned display is 9 chars wide)
IF char_col >= LAYOUT_WASTE_COL
IF char_col < LAYOUT_WASTE_COL+9
pile_id = PILE_ID_WASTE
EXIT
ENDIF
ENDIF
// Foundations: cols 16-35 (4 foundations x 5 chars, with spacing)
IF char_col >= LAYOUT_FOUND_COL
// Foundation 0: 16-20
IF char_col < LAYOUT_FOUND_COL+PILE_WIDTH
pile_id = PILE_ID_FOUND0
EXIT
ENDIF
// Foundation 1: 21-25
IF char_col >= 21
IF char_col < 21+PILE_WIDTH
pile_id = PILE_ID_FOUND1
EXIT
ENDIF
ENDIF
// Foundation 2: 26-30
IF char_col >= 26
IF char_col < 26+PILE_WIDTH
pile_id = PILE_ID_FOUND2
EXIT
ENDIF
ENDIF
// Foundation 3: 31-35
IF char_col >= 31
IF char_col < 31+PILE_WIDTH
pile_id = PILE_ID_FOUND3
EXIT
ENDIF
ENDIF
ENDIF
ENDIF
// Check tableau row (row 8+)
#PRAGMA _P_USE_LONG_JUMP 1
IF char_row >= LAYOUT_TAB_ROW
#PRAGMA _P_USE_LONG_JUMP 0
// Tableaus can extend many rows down
// Each tableau: 5 chars wide + 1 char gap (except last)
// Tableau 0: cols 0-4
IF char_col < 5
pile_id = PILE_ID_TAB0
EXIT
ENDIF
// Tableau 1: cols 5-9
IF char_col >= 5
IF char_col < 10
pile_id = PILE_ID_TAB1
EXIT
ENDIF
ENDIF
// Tableau 2: cols 10-14
IF char_col >= 10
IF char_col < 15
pile_id = PILE_ID_TAB2
EXIT
ENDIF
ENDIF
// Tableau 3: cols 15-19
IF char_col >= 15
IF char_col < 20
pile_id = PILE_ID_TAB3
EXIT
ENDIF
ENDIF
// Tableau 4: cols 20-24
IF char_col >= 20
IF char_col < 25
pile_id = PILE_ID_TAB4
EXIT
ENDIF
ENDIF
// Tableau 5: cols 25-29
IF char_col >= 25
IF char_col < 30
pile_id = PILE_ID_TAB5
EXIT
ENDIF
ENDIF
// Tableau 6: cols 30-34
IF char_col >= 30
IF char_col < 35
pile_id = PILE_ID_TAB6
EXIT
ENDIF
ENDIF
ENDIF
FEND
// ============================================================================
// FUNC pile_id_to_pointer
// Convert pile ID to pile data pointer
// ============================================================================
FUNC pile_id_to_pointer({BYTE pile_id} out:{WORD pile_ptr})
WORD ptr
SWITCH pile_id
CASE PILE_ID_STOCK
POINTER ptr -> pile_stock
CASE PILE_ID_WASTE
POINTER ptr -> pile_waste
CASE PILE_ID_FOUND0
POINTER ptr -> pile_found0
CASE PILE_ID_FOUND1
POINTER ptr -> pile_found1
CASE PILE_ID_FOUND2
POINTER ptr -> pile_found2
CASE PILE_ID_FOUND3
POINTER ptr -> pile_found3
CASE PILE_ID_TAB0
POINTER ptr -> pile_tab0
CASE PILE_ID_TAB1
POINTER ptr -> pile_tab1
CASE PILE_ID_TAB2
POINTER ptr -> pile_tab2
CASE PILE_ID_TAB3
POINTER ptr -> pile_tab3
CASE PILE_ID_TAB4
POINTER ptr -> pile_tab4
CASE PILE_ID_TAB5
POINTER ptr -> pile_tab5
CASE PILE_ID_TAB6
POINTER ptr -> pile_tab6
DEFAULT
ptr = 0
ENDSWITCH
pile_ptr = ptr
FEND
// ============================================================================
// FUNC get_tableau_card_count
// Determine how many cards to select from tableau based on click position
// Scans screen memory to find which card was clicked, then counts from there
// Returns the number of cards from the clicked position to the top
// ============================================================================
FUNC get_tableau_card_count({WORD pile_ptr @ $f8} {BYTE click_row} {BYTE tableau_col} out:{BYTE card_count})
BYTE count
BYTE faceup_count
BYTE card
BYTE is_facedown
BYTE i
BYTE screen_char
BYTE card_rank
BYTE card_suit
BYTE card_id
BYTE clicked_card_id
BYTE found_match
BYTE match_index
BYTE rank_char
WORD rank_map_ptr @ $f4
WORD screen_pos @ $f6
WORD row_offset
count = PEEK pile_ptr[0]
IF count == 0
card_count = 0
EXIT
ENDIF
// Count face-up cards and find top card
faceup_count = 0
FOR i = 1 TO count
card = PEEK pile_ptr[i]
is_facedown = card & CARD_FACEDOWN
IF is_facedown == 0
faceup_count++
ENDIF
NEXT
IF faceup_count == 0
card_count = 0
EXIT
ENDIF
// Read screen character at click position to identify clicked card
// Tableau columns: 0,5,10,15,20,25,30 - rank is at column +1
// Calculate screen position for clicked row
// row_offset = click_row * 40
row_offset = 0
FOR i = 1 TO click_row
row_offset = row_offset + 40
NEXT
// Rank character is at tableau_col + 1 (see cardrender.c65:113)
screen_pos = $0400 + row_offset
screen_pos = screen_pos + tableau_col
screen_pos = screen_pos + 1 // Rank is at offset +1
screen_char = PEEK screen_pos[0]
// Debug: show screen character we read
//POKE $0400+35 , screen_char
// Try to match screen character to a rank
POINTER rank_map_ptr -> card_charcode_map
clicked_card_id = $FF
found_match = 0
// Scan face-up cards in pile to find match (bottom to top)
FOR i = 1 TO count
card = PEEK pile_ptr[i]
is_facedown = card & CARD_FACEDOWN
IF is_facedown == 0
card_id = card & CARD_MASK
card_id_to_suit_rank(card_id, card_suit, card_rank)
// Get rank character and compare
rank_char = PEEK rank_map_ptr[card_rank]
// Adjust for color (red cards have +64)
IF card_suit < 2
rank_char = rank_char + 64
ENDIF
IF rank_char == screen_char
// Found match - use this card
clicked_card_id = card_id
match_index = i
found_match = 1
BREAK // Exit loop early
ENDIF
ENDIF
NEXT
// If no match found, select top card
IF found_match == 0
card_count = 1
EXIT
ENDIF
// Count cards from matched position to top
card_count = count - match_index
card_count = card_count + 1
FEND
// ============================================================================
// FUNC get_selected_card_info
// Returns info about currently selected card (if any)
// Returns card_id (0-51) and valid flag (1=valid, 0=no selection)
// ============================================================================
FUNC get_selected_card_info(out:{BYTE card_id} out:{BYTE valid})
WORD pile_ptr @ $80
BYTE pile_count
BYTE card_index
BYTE card
valid = 0
card_id = 0
// No selection
IF game_selected_pile == PILE_ID_NONE
EXIT
ENDIF
// Get pile pointer
pile_id_to_pointer(game_selected_pile, pile_ptr)
IF pile_ptr == 0
EXIT
ENDIF
pile_count = PEEK pile_ptr[0]
IF pile_count == 0
EXIT
ENDIF
// Waste: top card
IF game_selected_pile == PILE_ID_WASTE
card = PEEK pile_ptr[pile_count]
card_id = card & CARD_MASK
valid = 1
EXIT
ENDIF
// Tableau: bottom card of selection (the clicked card)
IF game_selected_pile >= PILE_ID_TAB0
IF game_selected_pile <= PILE_ID_TAB6
// Calculate index of clicked card (bottom of selection)
card_index = pile_count - game_selected_card_count
card_index = card_index + 1
card = PEEK pile_ptr[card_index]
card_id = card & CARD_MASK
valid = 1
EXIT
ENDIF
ENDIF
FEND
// ============================================================================
// FUNC game_init
// Initialize game state and input devices
// ============================================================================
FUNC game_init
game_selected_pile = PILE_ID_NONE
game_selected_card_count = 0
game_prev_button_state = 0
// Initialize joystick (Port 2)
joy_state = 0
FEND
// ============================================================================
// FUNC detect_click
// Detect button click (press and release)
// Returns 1 if clicked, 0 if not
// ============================================================================
FUNC detect_click({BYTE current_button_state} out:{BYTE clicked})
clicked = 0
// Check for left mouse button (bit 4, as shown in test)
// Button is active-low, so 0 = pressed
BYTE current_pressed
BYTE prev_pressed
BYTE temp
// Check if current button is pressed (bit 4 = 0)
temp = current_button_state & $10
current_pressed = 0
IF temp == 0
current_pressed = 1
ENDIF
// Check if previous button was pressed
temp = game_prev_button_state & $10
prev_pressed = 0
IF temp == 0
prev_pressed = 1
ENDIF
// Click = was pressed, now released
IF prev_pressed == 1
IF current_pressed == 0
clicked = 1
ENDIF
ENDIF
game_prev_button_state = current_button_state
FEND
// ============================================================================
// FUNC clear_tableau_column
// Clear a tableau column from row 8 to bottom of screen
// ============================================================================
FUNC clear_tableau_column({BYTE col_offset})
WORD pos @ $f6
BYTE row
BYTE i
// Clear from row 8 to row 24 (17 rows)
// 8*40+$0400 is constant expression, then add variable col_offset
pos = 8*40+$0400 + col_offset
FOR row = 8 TO 24
// Clear 5 characters wide (card width)
FOR i = 0 TO 4
POKE pos[i] , 0
NEXT
pos = pos + 40
NEXT
FEND
// ============================================================================
// FUNC clear_waste_area
// Clear the waste pile area (9 chars wide for fanned display)
// ============================================================================
FUNC clear_waste_area
WORD pos @ $f6
BYTE row
BYTE i
// Clear waste area: row 0, cols 6-14 (9 chars), 7 rows tall
pos = 6+$0400
FOR row = 0 TO 6
FOR i = 0 TO 8
POKE pos[i] , 0
NEXT
pos = pos + 40
NEXT
FEND
// ============================================================================
// FUNC render_pile_by_id
// Render a specific pile by ID to its screen location
// Clears the area first to remove artifacts from previous render
// ============================================================================
FUNC render_pile_by_id({BYTE pile_id})
WORD pile_ptr @ $92
WORD screen_offset
pile_id_to_pointer(pile_id, pile_ptr)
IF pile_ptr == 0
EXIT
ENDIF
SWITCH pile_id
CASE PILE_ID_STOCK
render_stock_pile($0400, 0, pile_ptr)
CASE PILE_ID_WASTE
clear_waste_area()
render_waste_pile($0400, 6, pile_ptr, game_draw_mode)
CASE PILE_ID_FOUND0
render_foundation_pile($0400, 16, pile_ptr)
CASE PILE_ID_FOUND1
render_foundation_pile($0400, 21, pile_ptr)
CASE PILE_ID_FOUND2
render_foundation_pile($0400, 26, pile_ptr)
CASE PILE_ID_FOUND3
render_foundation_pile($0400, 31, pile_ptr)
CASE PILE_ID_TAB0
clear_tableau_column(0)
render_tableau_pile($0400, 8*40, pile_ptr)
CASE PILE_ID_TAB1
clear_tableau_column(5)
render_tableau_pile($0400, 8*40+5, pile_ptr)
CASE PILE_ID_TAB2
clear_tableau_column(10)
render_tableau_pile($0400, 8*40+10, pile_ptr)
CASE PILE_ID_TAB3
clear_tableau_column(15)
render_tableau_pile($0400, 8*40+15, pile_ptr)
CASE PILE_ID_TAB4
clear_tableau_column(20)
render_tableau_pile($0400, 8*40+20, pile_ptr)
CASE PILE_ID_TAB5
clear_tableau_column(25)
render_tableau_pile($0400, 8*40+25, pile_ptr)
CASE PILE_ID_TAB6
clear_tableau_column(30)
render_tableau_pile($0400, 8*40+30, pile_ptr)
ENDSWITCH
FEND
// ============================================================================
// FUNC check_win_condition
// Check if all 4 foundations are complete (13 cards each = King on top)
// Returns 1 if won, 0 otherwise
// ============================================================================
FUNC check_win_condition(out:{BYTE is_won})
WORD ptr @ $a4
BYTE count
is_won = 1 // Assume won, set to 0 if any foundation incomplete
// Check foundation 0
POINTER ptr -> pile_found0
count = PEEK ptr[0]
IF count != 13
is_won = 0
EXIT
ENDIF
// Check foundation 1
POINTER ptr -> pile_found1
count = PEEK ptr[0]
IF count != 13
is_won = 0
EXIT
ENDIF
// Check foundation 2
POINTER ptr -> pile_found2
count = PEEK ptr[0]
IF count != 13
is_won = 0
EXIT
ENDIF
// Check foundation 3
POINTER ptr -> pile_found3
count = PEEK ptr[0]
IF count != 13
is_won = 0
EXIT
ENDIF
FEND
// ============================================================================
// FUNC handle_click_on_pile
// Handle a click on a specific pile
// Implements game logic for selection and move execution
// ============================================================================
FUNC handle_click_on_pile({BYTE clicked_pile} {BYTE click_row})
BYTE is_foundation
BYTE is_tableau
BYTE selected_is_tableau
BYTE success
WORD src_ptr
WORD dst_ptr
//WORD tab_ptr @ $f8
WORD tab_ptr
BYTE tab_col
BYTE tab_index
BYTE j
// Determine pile type
is_foundation = 0
IF clicked_pile >= PILE_ID_FOUND0
IF clicked_pile <= PILE_ID_FOUND3
is_foundation = 1
ENDIF
ENDIF
is_tableau = 0
IF clicked_pile >= PILE_ID_TAB0
IF clicked_pile <= PILE_ID_TAB6
is_tableau = 1
ENDIF
ENDIF
// If nothing selected, select this pile
#PRAGMA _P_USE_LONG_JUMP 1
IF game_selected_pile == PILE_ID_NONE
#PRAGMA _P_USE_LONG_JUMP 0
// Can only select waste or tableau
IF clicked_pile == PILE_ID_WASTE
game_selected_pile = PILE_ID_WASTE
game_selected_card_count = 1
EXIT
ENDIF
IF clicked_pile == PILE_ID_STOCK
// Stock: draw cards
move_stock_to_waste(game_draw_mode, success)
IF success
render_pile_by_id(PILE_ID_STOCK)
render_pile_by_id(PILE_ID_WASTE)
ELSE
// Stock empty, reset from waste
move_reset_stock(success)
IF success
render_pile_by_id(PILE_ID_STOCK)
render_pile_by_id(PILE_ID_WASTE)
ENDIF
ENDIF
EXIT
ENDIF
#PRAGMA _P_USE_LONG_JUMP 1
IF is_tableau
#PRAGMA _P_USE_LONG_JUMP 0
// Select tableau - calculate how many cards based on click position
pile_id_to_pointer(clicked_pile, tab_ptr)
// Calculate tableau column: 0,5,10,15,20,25,30
tab_index = clicked_pile - PILE_ID_TAB0
tab_col = 0
FOR j = 1 TO tab_index
tab_col = tab_col + 5
NEXT
get_tableau_card_count(tab_ptr, click_row, tab_col, game_selected_card_count)
IF game_selected_card_count > 0
game_selected_pile = clicked_pile
ENDIF
EXIT
ENDIF
EXIT
ENDIF
// Something is selected, try to move to destination
pile_id_to_pointer(game_selected_pile, src_ptr)
pile_id_to_pointer(clicked_pile, dst_ptr)
// Waste to Foundation
IF game_selected_pile == PILE_ID_WASTE
IF is_foundation
move_waste_to_found(dst_ptr, success)
IF success
render_pile_by_id(PILE_ID_WASTE)
render_pile_by_id(clicked_pile)
ENDIF
game_selected_pile = PILE_ID_NONE
EXIT
ENDIF
ENDIF
// Waste to Tableau
IF game_selected_pile == PILE_ID_WASTE
IF is_tableau
move_waste_to_tab(dst_ptr, success)
IF success
render_pile_by_id(PILE_ID_WASTE)
render_pile_by_id(clicked_pile)
ENDIF
game_selected_pile = PILE_ID_NONE
EXIT
ENDIF
ENDIF
// Tableau to Foundation/Tableau - check if selected pile is tableau
selected_is_tableau = 0
IF game_selected_pile >= PILE_ID_TAB0
IF game_selected_pile <= PILE_ID_TAB6
selected_is_tableau = 1
ENDIF
ENDIF
IF selected_is_tableau
IF is_foundation
move_tab_to_found(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
// Tableau to Tableau
IF selected_is_tableau
IF is_tableau
move_tab_to_tab(src_ptr, dst_ptr, game_selected_card_count, 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
// Click on same pile or invalid destination: deselect
game_selected_pile = PILE_ID_NONE
FEND
// ============================================================================
// FUNC render_all_piles_initial
// Render all piles for initial game display
// ============================================================================
FUNC render_all_piles_initial
BYTE pile_id
pile_id = PILE_ID_STOCK
WHILE pile_id <= PILE_ID_TAB6
render_pile_by_id(pile_id)
pile_id++
WEND
FEND
// ============================================================================
// FUNC game_loop
// Main game loop - never returns
// ============================================================================
FUNC game_loop
WORD sprite_x
BYTE sprite_y
BYTE char_col
BYTE char_row
BYTE pile_at_cursor
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)
// Copy card sprite data to $2240 (sprite blocks 137+)
// 25 sprites × 64 bytes = 1600 bytes
POINTER src_ptr -> sprite_rank_ace
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
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)
// 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)
#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
#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
button_state = combined_button
detect_click(button_state, clicked)
// 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)
ELSE
// Click on empty area: deselect
game_selected_pile = PILE_ID_NONE
ENDIF
ENDIF
// 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
FEND
LABEL __skip_lib_gameloop
#IFEND