966 lines
25 KiB
Text
966 lines
25 KiB
Text
|
||
#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
|