First commit in fresh repo

This commit is contained in:
Mattias Hansson 2026-01-02 13:21:20 +01:00
parent f3e483ca0b
commit 9c723cce1d
43 changed files with 6757 additions and 2 deletions

60
.gitignore vendored Normal file
View file

@ -0,0 +1,60 @@
# Python bytecode / caches
__pycache__/
*.py[cod]
*$py.class
*.pyo
*.pyd
*.so
# Virtual environments
.venv/
venv/
ENV/
.music/.venv/ # if your venv lives specifically under /music
.python-version
.env
.env.*
# Packaging / wheels / builds
build/
dist/
.eggs/
*.egg-info/
pip-wheel-metadata/
# Tests / coverage / benchmarks
.pytest_cache/
.tox/
.nox/
.coverage*
htmlcov/
.hypothesis/
.benchmark/
# Type checkers / linters
.mypy_cache/
.pytype/
.pyre/
.ruff_cache/
# Jupyter
.ipynb_checkpoints/
# Logs / databases / local data
*.log
*.sqlite3
# OS cruft
.DS_Store
Thumbs.db
# IDEs / editors
.idea/
*.iml
.vscode/
*.s
*.sym
*.prg
.claude/
.npm/

6
Dockerfile Normal file
View file

@ -0,0 +1,6 @@
FROM node:18-alpine
WORKDIR /app
RUN apk add --no-cache bash
ENV SHELL=/bin/bash
RUN npm install -g @anthropic-ai/claude-code
CMD ["claude"]

View file

@ -1,3 +1,44 @@
# solitaire-c64
# Solitaire C64
A solitaire style game for the Commodore 64
A classic Klondike solitaire card game for the Commodore 64, written in c65gm.
## 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
## Building
Requires the c65gm compiler and ACME assembler:
```bash
./cm.sh
```
Outputs `main.prg` ready to load on C64 or emulator.
## Project Structure
- `cardgame.c65` - Main entry point and initialization
- `gameloop.c65` - Game loop, interaction, and pile detection
- `cardmoves.c65` - Move validation and execution logic
- `cardrender.c65` - Card and pile rendering routines
- `carddeck.c65` - Deck shuffling and dealing
- `mouse.c65` / `joystick.c65` - Input handling
- `pointer.c65` - Sprite cursor management
- `cardsprites.c65` - Sprite data for cursor
## License
See LICENSE file for details.

31
cardconsts.c65 Normal file
View file

@ -0,0 +1,31 @@
#IFNDEF __lib_cardconsts
#DEFINE __lib_cardconsts 1
// Card suits
BYTE CONST CARD_SUIT_HEARTS = 0
BYTE CONST CARD_SUIT_DIAMONDS = 1
BYTE CONST CARD_SUIT_SPADES = 2
BYTE CONST CARD_SUIT_CLUBS = 3
// Card ranks
BYTE CONST CARD_ACE = 0
BYTE CONST CARD_2 = 1
BYTE CONST CARD_3 = 2
BYTE CONST CARD_4 = 3
BYTE CONST CARD_5 = 4
BYTE CONST CARD_6 = 5
BYTE CONST CARD_7 = 6
BYTE CONST CARD_8 = 7
BYTE CONST CARD_9 = 8
BYTE CONST CARD_10 = 9
BYTE CONST CARD_JACK = 10
BYTE CONST CARD_QUEEN = 11
BYTE CONST CARD_KING = 12
// Card representation flags
BYTE CONST CARD_FACEDOWN = $80
BYTE CONST CARD_MASK = $7F
BYTE CONST PILE_END = $FF
#IFEND

289
carddeck.c65 Normal file
View file

@ -0,0 +1,289 @@
#IFNDEF __lib_carddeck
#DEFINE __lib_carddeck 1
#INCLUDE "random.c65"
GOTO __skip_lib_carddeck
// Validation error codes
BYTE CONST VALIDATE_OK = 0
BYTE CONST VALIDATE_DUPLICATE = 1
BYTE CONST VALIDATE_MISSING = 2
BYTE CONST VALIDATE_INVALID = 3
// Temporary array to track seen cards (52 bytes)
LABEL validate_seen
ASM
!fill 52, 0
ENDASM
// Initialize stock with all 52 cards (face down)
// Cards stored as $80-$B3 (card value OR'd with CARD_FACEDOWN)
FUNC stock_init
WORD ptr @ $fa
POINTER ptr -> pile_stock
BYTE card
// Set count to 52
POKE ptr[0] , 52
// Fill with cards 0-51, all face down ($80 = CARD_FACEDOWN)
ptr++
FOR card = $80 TO $80+51
POKE ptr , card
ptr++
NEXT
FEND
// Shuffle stock pile using Fisher-Yates algorithm
FUNC stock_shuffle
WORD ptr @ $fa
POINTER ptr -> pile_stock
BYTE i
BYTE j
BYTE temp_a
BYTE temp_b
// Fisher-Yates: swap each card with a random card from remaining deck
i = 52
WHILE i >= 2
rand_max(i, j)
j++
// Swap cards at positions i and j (1-indexed in pile)
IF i <> j
temp_a = PEEK ptr[i]
temp_b = PEEK ptr[j]
POKE ptr[i] , temp_b
POKE ptr[j] , temp_a
ENDIF
i--
WEND
FEND
// Deal cards from stock to a single tableau pile
// num_cards: how many cards to deal
// Top card is flipped face up
FUNC deal_to_tableau({WORD tab_ptr @ $fc} {BYTE num_cards})
WORD stock_ptr @ $fa
POINTER stock_ptr -> pile_stock
BYTE stock_count
BYTE card
BYTE i
// Set tableau count
POKE tab_ptr[0] , num_cards
// Deal cards from stock to tableau
stock_count = PEEK stock_ptr[0]
FOR i = 1 TO num_cards
card = PEEK stock_ptr[stock_count]
POKE tab_ptr[i] , card
stock_count--
NEXT
// Update stock count
POKE stock_ptr[0] , stock_count
// Flip top card face up (clear CARD_FACEDOWN bit)
card = PEEK tab_ptr[num_cards]
card = card & CARD_MASK
POKE tab_ptr[num_cards] , card
FEND
// Deal cards to all 7 tableau piles (Klondike layout)
// 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 @ $fc
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)
FEND
// Clear the seen array
FUNC validate_clear_seen
WORD ptr @ $fa
POINTER ptr -> validate_seen
BYTE i
FOR i = 0 TO 51
POKE ptr[i] , 0
NEXT
FEND
// Check cards in a pile, mark as seen
// Returns 0 if OK, 1 if duplicate found
FUNC validate_pile({WORD pile_ptr @ $fa} out:{BYTE result})
WORD seen_ptr @ $fc
POINTER seen_ptr -> validate_seen
BYTE count
BYTE i
BYTE card
BYTE card_val
BYTE already_seen
result = VALIDATE_OK
count = PEEK pile_ptr[0]
FOR i = 1 TO count
card = PEEK pile_ptr[i]
card_val = card & CARD_MASK
// Check if valid card (0-51)
IF card_val >= 52
result = VALIDATE_INVALID
EXIT
ENDIF
already_seen = PEEK seen_ptr[card_val]
IF already_seen
result = VALIDATE_DUPLICATE
EXIT
ENDIF
POKE seen_ptr[card_val] , 1
NEXT
FEND
// Validate entire deck across all piles
// Returns: 0=OK, 1=duplicate, 2=missing
FUNC validate_deck(out:{BYTE result})
WORD ptr @ $fa
WORD seen_ptr @ $fc
BYTE pile_result
BYTE i
BYTE seen
// Clear seen array
validate_clear_seen()
result = VALIDATE_OK
// Check stock
POINTER ptr -> pile_stock
validate_pile(ptr, pile_result)
IF pile_result
result = pile_result
EXIT
ENDIF
// Check waste
POINTER ptr -> pile_waste
validate_pile(ptr, pile_result)
IF pile_result
result = pile_result
EXIT
ENDIF
// Check tableau piles
POINTER ptr -> pile_tab0
validate_pile(ptr, pile_result)
IF pile_result
result = pile_result
EXIT
ENDIF
POINTER ptr -> pile_tab1
validate_pile(ptr, pile_result)
IF pile_result
result = pile_result
EXIT
ENDIF
POINTER ptr -> pile_tab2
validate_pile(ptr, pile_result)
IF pile_result
result = pile_result
EXIT
ENDIF
POINTER ptr -> pile_tab3
validate_pile(ptr, pile_result)
IF pile_result
result = pile_result
EXIT
ENDIF
POINTER ptr -> pile_tab4
validate_pile(ptr, pile_result)
IF pile_result
result = pile_result
EXIT
ENDIF
POINTER ptr -> pile_tab5
validate_pile(ptr, pile_result)
IF pile_result
result = pile_result
EXIT
ENDIF
POINTER ptr -> pile_tab6
validate_pile(ptr, pile_result)
IF pile_result
result = pile_result
EXIT
ENDIF
// Check foundation piles
POINTER ptr -> pile_found0
validate_pile(ptr, pile_result)
IF pile_result
result = pile_result
EXIT
ENDIF
POINTER ptr -> pile_found1
validate_pile(ptr, pile_result)
IF pile_result
result = pile_result
EXIT
ENDIF
POINTER ptr -> pile_found2
validate_pile(ptr, pile_result)
IF pile_result
result = pile_result
EXIT
ENDIF
POINTER ptr -> pile_found3
validate_pile(ptr, pile_result)
IF pile_result
result = pile_result
EXIT
ENDIF
// Now check all 52 cards were seen
POINTER seen_ptr -> validate_seen
FOR i = 0 TO 51
seen = PEEK seen_ptr[i]
IF seen == 0
result = VALIDATE_MISSING
EXIT
ENDIF
NEXT
FEND
LABEL __skip_lib_carddeck
#IFEND

152
cardgame.c65 Normal file
View file

@ -0,0 +1,152 @@
#INCLUDE <c64start.c65>
#INCLUDE <c64defs.c65>
//#DEFINE MOUSE_NO_SMOOTHING 1
// Enable test game setups (comment out for release build)
//#DEFINE TEST_GAMES 1
GOTO start
ASM
*=$3000
ENDASM
#INCLUDE "utils.c65"
#INCLUDE "cardconsts.c65"
#INCLUDE "piles.c65"
#INCLUDE "random.c65"
#INCLUDE "carddeck.c65"
#INCLUDE "cardrender.c65"
//#INCLUDE "cardtests.c65"
#INCLUDE "joystick.c65"
#INCLUDE "mouse.c65"
#INCLUDE "pointer.c65"
//#INCLUDE "joysticktests.c65"
#IFDEF TEST_GAMES
#INCLUDE "testgames.c65"
#IFEND
#INCLUDE "gameloop.c65"
FUNC main
ASM
sei
ENDASM
// Initialize game state
#IFNDEF TEST_GAMES
// Normal game: random shuffle and deal
// Seed RNG with multiple entropy sources for better randomness
WORD timer_seed @ $fa
BYTE raster @ $d012
BYTE extra_entropy
BYTE warmup
// Combine CIA timer with raster position
timer_seed = PEEKW $DC04
extra_entropy = raster
timer_seed = timer_seed ^ extra_entropy
rand_seed(timer_seed)
// Warm up RNG by advancing it based on timer low byte
extra_entropy = PEEK $DC04
FOR warmup = 0 TO extra_entropy
rand(extra_entropy)
NEXT
stock_init()
stock_shuffle()
deal_tableaus()
#IFEND
BYTE validation_result
validate_deck(validation_result)
IF validation_result
POKE $d020 , color_red
ENDIF
set_vic_bank(0) // $0000 - $3fff
set_vic_screenmem(1) // $400
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)
fill_mem($d800, $d800+999, color_white) // Fill color mem
POKE $d020 , color_white //color_grey
POKE $d021 , color_black
POKE $d022 , color_red
POKE $d023 , color_light_grey
POKE $d024 , color_dark_grey
//show_charset()
fill_mem($0400, $0400+999, 0)
//render_card($0400, $30, 0) // Ace of Hearts
//render_card($0400, $35, 27) // 2 of Spades
//render_card($0400, $3a, 15) // 3 of Diamonds
//render_all_cards_test()
//render_facedown_test()
//render_faceup_stack_test()
//render_tableaus_test()
//render_midgame_test()
//render_card_back($0400, 35) // Test card back at column 35
//render_empty_pile($0400, 8*40+35) // Test empty pile below card back
//render_foundation_test()
//render_waste_test()
//render_all_piles_test()
// Move tests - runs multiple tests, press key between each
//test_stock_to_waste_draw1() // Draw 1 card x3 from stock
//wait_key()
//fill_mem($0400, $0400+999, 0)
//test_waste_to_tab_valid() // Move card from waste to tableau
//wait_key()
//fill_mem($0400, $0400+999, 0)
//test_tab_to_found() // Tableau to foundation + auto-flip
//wait_key()
//fill_mem($0400, $0400+999, 0)
//test_tab_to_tab_stack() // Move stack of 3 cards + auto-flip
// Joystick tests
//test_joystick_read() // Test joystick input reading - suit symbols
//fill_mem($0400, $0400+999, 0)
//test_pointer_sprite() // Test sprite pointer with joystick
//fill_mem($0400, $0400+999, 0)
//test_pointer_manual() // Test sprite pointer manual movement
// Mouse test
//test_pointer_mouse() // Test sprite pointer with 1351 mouse
// Start the game!
game_init()
game_loop() // Never returns
FEND
LABEL start
main()
LABEL card_charset
ASM
!binary "charpad_cards/charpad_cards7.bin"
ENDASM
LABEL card_charset_end

385
cardmoves.c65 Normal file
View file

@ -0,0 +1,385 @@
#IFNDEF __lib_cardmoves
#DEFINE __lib_cardmoves 1
#INCLUDE "cardconsts.c65"
#INCLUDE "piles.c65"
#INCLUDE "cardrender.c65"
GOTO __skip_lib_cardmoves
// Card colors
BYTE CONST COLOR_RED = 0
BYTE CONST COLOR_BLACK = 1
// Move result codes
BYTE CONST MOVE_OK = 1
BYTE CONST MOVE_INVALID = 0
// ============================================================================
// Helper Functions
// ============================================================================
// Get card color (0=red, 1=black)
// Cards 0-25 are red (Hearts, Diamonds), 26-51 are black (Spades, Clubs)
FUNC card_color({BYTE card_id} out:{BYTE color})
IF card_id < 26
color = COLOR_RED
ELSE
color = COLOR_BLACK
ENDIF
FEND
// Get top card from pile (returns card with facedown bit, or PILE_END if empty)
// Uses $fa - for source pile operations
FUNC pile_top_card({WORD pile_ptr @ $fa} out:{BYTE card})
BYTE count
count = PEEK pile_ptr[0]
IF count == 0
card = PILE_END
ELSE
card = PEEK pile_ptr[count]
ENDIF
FEND
// Remove top card from pile (returns the card, updates count)
// Uses $fa - for source pile operations
FUNC pile_pop({WORD pile_ptr @ $fa} out:{BYTE card})
BYTE count
count = PEEK pile_ptr[0]
card = PEEK pile_ptr[count]
count--
POKE pile_ptr[0] , count
FEND
// Add card to top of pile (updates count)
// Uses $fc - for destination pile operations
FUNC pile_push({WORD pile_ptr @ $fc} {BYTE card})
BYTE count
count = PEEK pile_ptr[0]
count++
POKE pile_ptr[count] , card
POKE pile_ptr[0] , count
FEND
// Flip top card face-up if it's face-down
// Uses $fa - for source pile operations
FUNC pile_flip_top({WORD pile_ptr @ $fa})
BYTE count
BYTE card
BYTE is_facedown
count = PEEK pile_ptr[0]
IF count > 0
card = PEEK pile_ptr[count]
is_facedown = card & CARD_FACEDOWN
IF is_facedown
card = card & CARD_MASK
POKE pile_ptr[count] , card
ENDIF
ENDIF
FEND
// ============================================================================
// Validation Functions
// ============================================================================
// Can card go on foundation? (Ace if empty, else same suit +1 rank)
// Uses $fc - destination pile
FUNC can_place_on_foundation({BYTE card_id} {WORD found_ptr @ $fc} out:{BYTE valid})
BYTE card_suit
BYTE card_rank
BYTE top_card
BYTE top_suit
BYTE top_rank
BYTE count
BYTE expected_rank
card_id_to_suit_rank(card_id, card_suit, card_rank)
count = PEEK found_ptr[0]
IF count == 0
// Empty foundation: only Ace allowed
IF card_rank == CARD_ACE
valid = 1
ELSE
valid = 0
ENDIF
ELSE
// Must be same suit, one rank higher
top_card = PEEK found_ptr[count]
top_card = top_card & CARD_MASK
card_id_to_suit_rank(top_card, top_suit, top_rank)
expected_rank = top_rank + 1
valid = 0
IF card_suit == top_suit
IF card_rank == expected_rank
valid = 1
ENDIF
ENDIF
ENDIF
FEND
// Can card go on tableau? (King if empty, else opposite color -1 rank)
// Uses $fc - destination pile
FUNC can_place_on_tableau({BYTE card_id} {WORD tab_ptr @ $fc} out:{BYTE valid})
BYTE card_rank
BYTE card_col
BYTE top_card
BYTE top_rank
BYTE top_col
BYTE count
BYTE card_suit
BYTE top_suit
BYTE expected_rank
card_id_to_suit_rank(card_id, card_suit, card_rank)
card_color(card_id, card_col)
count = PEEK tab_ptr[0]
IF count == 0
// Empty tableau: only King allowed
IF card_rank == CARD_KING
valid = 1
ELSE
valid = 0
ENDIF
ELSE
// Must be opposite color, one rank lower
top_card = PEEK tab_ptr[count]
top_card = top_card & CARD_MASK
card_id_to_suit_rank(top_card, top_suit, top_rank)
card_color(top_card, top_col)
expected_rank = card_rank + 1
valid = 0
IF card_col <> top_col
IF expected_rank == top_rank
valid = 1
ENDIF
ENDIF
ENDIF
FEND
// ============================================================================
// Move Functions
// ============================================================================
// Stock to Waste: Draw cards from stock to waste
// draw_count = 1 or 3 depending on game variant
// Stock is source @ $fa, Waste is destination @ $fc
FUNC move_stock_to_waste({BYTE draw_count} out:{BYTE success})
WORD stock_ptr @ $fa
WORD waste_ptr @ $fc
POINTER stock_ptr -> pile_stock
POINTER waste_ptr -> pile_waste
BYTE stock_count
BYTE card
BYTE i
stock_count = PEEK stock_ptr[0]
IF stock_count == 0
success = MOVE_INVALID
EXIT
ENDIF
// Limit draw_count to available cards
IF draw_count > stock_count
draw_count = stock_count
ENDIF
FOR i = 1 TO draw_count
pile_pop(stock_ptr, card)
card = card & CARD_MASK // Flip face-up
pile_push(waste_ptr, card)
NEXT
success = MOVE_OK
FEND
// Reset Stock: Flip waste back to stock (all face-down)
// Waste is source @ $fa, Stock is destination @ $fc
FUNC move_reset_stock(out:{BYTE success})
WORD waste_ptr @ $fa
WORD stock_ptr @ $fc
POINTER waste_ptr -> pile_waste
POINTER stock_ptr -> pile_stock
BYTE waste_count
BYTE card
BYTE i
waste_count = PEEK waste_ptr[0]
IF waste_count == 0
success = MOVE_INVALID
EXIT
ENDIF
// Move all waste cards to stock (reversed, face-down)
FOR i = 1 TO waste_count
pile_pop(waste_ptr, card)
card = card | CARD_FACEDOWN
pile_push(stock_ptr, card)
NEXT
success = MOVE_OK
FEND
// Waste to Tableau
FUNC move_waste_to_tab({WORD tab_ptr @ $fc} out:{BYTE success})
WORD waste_ptr @ $fa
POINTER waste_ptr -> pile_waste
BYTE card
BYTE valid
pile_top_card(waste_ptr, card)
IF card == PILE_END
success = MOVE_INVALID
EXIT
ENDIF
card = card & CARD_MASK
can_place_on_tableau(card, tab_ptr, valid)
IF valid == 0
success = MOVE_INVALID
EXIT
ENDIF
pile_pop(waste_ptr, card)
pile_push(tab_ptr, card)
success = MOVE_OK
FEND
// Waste to Foundation
FUNC move_waste_to_found({WORD found_ptr @ $fc} out:{BYTE success})
WORD waste_ptr @ $fa
POINTER waste_ptr -> pile_waste
BYTE card
BYTE valid
pile_top_card(waste_ptr, card)
IF card == PILE_END
success = MOVE_INVALID
EXIT
ENDIF
card = card & CARD_MASK
can_place_on_foundation(card, found_ptr, valid)
IF valid == 0
success = MOVE_INVALID
EXIT
ENDIF
pile_pop(waste_ptr, card)
pile_push(found_ptr, card)
success = MOVE_OK
FEND
// Tableau to Foundation (top card only)
FUNC move_tab_to_found({WORD tab_ptr @ $fa} {WORD found_ptr @ $fc} out:{BYTE success})
BYTE card
BYTE valid
BYTE is_facedown
pile_top_card(tab_ptr, card)
IF card == PILE_END
success = MOVE_INVALID
EXIT
ENDIF
// Must be face-up
is_facedown = card & CARD_FACEDOWN
IF is_facedown
success = MOVE_INVALID
EXIT
ENDIF
card = card & CARD_MASK
can_place_on_foundation(card, found_ptr, valid)
IF valid == 0
success = MOVE_INVALID
EXIT
ENDIF
pile_pop(tab_ptr, card)
pile_push(found_ptr, card)
pile_flip_top(tab_ptr) // Reveal next card
success = MOVE_OK
FEND
// Tableau to Tableau: Move card_count cards from src to dst
FUNC move_tab_to_tab({WORD src_ptr @ $fa} {WORD dst_ptr @ $fc} {BYTE card_count} out:{BYTE success})
BYTE src_count
BYTE start_idx
BYTE bottom_card
BYTE valid
BYTE i
BYTE card
BYTE is_facedown
src_count = PEEK src_ptr[0]
IF card_count > src_count
success = MOVE_INVALID
EXIT
ENDIF
IF card_count == 0
success = MOVE_INVALID
EXIT
ENDIF
// Find the bottom card of the stack to move
start_idx = src_count - card_count
start_idx = start_idx + 1
bottom_card = PEEK src_ptr[start_idx]
// Bottom card must be face-up
is_facedown = bottom_card & CARD_FACEDOWN
IF is_facedown
success = MOVE_INVALID
EXIT
ENDIF
bottom_card = bottom_card & CARD_MASK
can_place_on_tableau(bottom_card, dst_ptr, valid)
IF valid == 0
success = MOVE_INVALID
EXIT
ENDIF
// Move cards (preserve order)
FOR i = start_idx TO src_count
card = PEEK src_ptr[i]
pile_push(dst_ptr, card)
NEXT
// Update source count
src_count = start_idx - 1
POKE src_ptr[0] , src_count
pile_flip_top(src_ptr) // Reveal next card
success = MOVE_OK
FEND
// Foundation to Tableau (optional rule - some variants allow this)
FUNC move_found_to_tab({WORD found_ptr @ $fa} {WORD tab_ptr @ $fc} out:{BYTE success})
BYTE card
BYTE valid
pile_top_card(found_ptr, card)
IF card == PILE_END
success = MOVE_INVALID
EXIT
ENDIF
card = card & CARD_MASK
can_place_on_tableau(card, tab_ptr, valid)
IF valid == 0
success = MOVE_INVALID
EXIT
ENDIF
pile_pop(found_ptr, card)
pile_push(tab_ptr, card)
success = MOVE_OK
FEND
LABEL __skip_lib_cardmoves
#IFEND

963
cardrender.c65 Normal file
View file

@ -0,0 +1,963 @@
#IFNDEF __lib_cardrender
#DEFINE __lib_cardrender 1
#INCLUDE "cardconsts.c65"
GOTO __skip_lib_cardrender
LABEL card_charcode_map
ASM
!8 13, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12
ENDASM
LABEL card_suit_charcode_map
ASM
!8 $50, $51, $0e, $0f
ENDASM
LABEL suit_graphic_hearts
ASM
!8 $1d+64, $1e+64, $1f+64, $2d+64, $2e+64, $2f+64, 0+64, $3e+64, 0+64
ENDASM
LABEL suit_graphic_diamonds
ASM
!8 0+64, $1b+64, 0+64, $2a+64, $2b+64, $2c+64, 0+64, $3b+64, 0+64
ENDASM
LABEL suit_graphic_spades
ASM
!8 0, $18, $19, $27, $28, $29, $37, $38, $39
ENDASM
LABEL suit_graphic_clubs
ASM
!8 0, $15, 0, $24, $25, $26, 0, $35, $36
ENDASM
// Convert card ID (0-51) to suit and rank
// 0-12: Hearts, 13-25: Diamonds, 26-38: Spades, 39-51: Clubs
FUNC card_id_to_suit_rank({BYTE card_id} out:{BYTE card_suit} out:{BYTE card_rank})
card_rank = card_id
card_suit = 0
IF card_rank >= 13
card_rank = card_rank - 13
card_suit = 1
ENDIF
IF card_rank >= 13
card_rank = card_rank - 13
card_suit = 2
ENDIF
IF card_rank >= 13
card_rank = card_rank - 13
card_suit = 3
ENDIF
FEND
// ZP usage $fa-$fd
// Render full card using card_id (0-51)
FUNC render_card({WORD screen_address} {WORD offset} {BYTE card_id})
BYTE card_suit
BYTE card_rank
card_id_to_suit_rank(card_id, card_suit, card_rank)
// Get rank charcode
WORD card_charcode_map_ptr @ $fa
POINTER card_charcode_map_ptr -> card_charcode_map
BYTE card_rank_charcode
card_rank_charcode = PEEK card_charcode_map_ptr[card_rank]
// Adjust color for red suit
IF card_suit < CARD_SUIT_SPADES
card_rank_charcode = card_rank_charcode + 64 //red color for hearts and diamonds
ENDIF
// Get suit charcode
WORD card_suit_charcode_map_ptr @ $fa
POINTER card_suit_charcode_map_ptr -> card_suit_charcode_map
BYTE suit_charcode
suit_charcode = PEEK card_suit_charcode_map_ptr[card_suit]
WORD p2 @ $fa
SWITCH card_suit
CASE CARD_SUIT_HEARTS
POINTER p2 -> suit_graphic_hearts
CASE CARD_SUIT_DIAMONDS
POINTER p2 -> suit_graphic_diamonds
CASE CARD_SUIT_SPADES
POINTER p2 -> suit_graphic_spades
CASE CARD_SUIT_CLUBS
POINTER p2 -> suit_graphic_clubs
ENDSWITCH
WORD p @ $fc
p = screen_address + offset
POKE p[0] , 224 // top left corner
POKE p[1] , 225
POKE p[2] , 225
POKE p[3] , 225
POKE p[4] , 226
p = p + 40
POKE p[0] , 240
POKE p[1] , card_rank_charcode
POKE p[2] , 0
POKE p[3] , suit_charcode
POKE p[4] , 243
p = p + 40
// Here we draw the bigger suit graphic in the middle of the card
BYTE g
POKE p[0] , 240
g = PEEK p2[0]
POKE p[1] , g
g = PEEK p2[1]
POKE p[2] , g
g = PEEK p2[2]
POKE p[3] , g
POKE p[4] , 243
p = p + 40
POKE p[0] , 240
g = PEEK p2[3]
POKE p[1] , g
g = PEEK p2[4]
POKE p[2] , g
g = PEEK p2[5]
POKE p[3] , g
POKE p[4] , 243
p = p + 40
POKE p[0] , 240
g = PEEK p2[6]
POKE p[1] , g
g = PEEK p2[7]
POKE p[2] , g
g = PEEK p2[8]
POKE p[3] , g
POKE p[4] , 243
p = p + 40
POKE p[0] , 240
POKE p[1] , suit_charcode
POKE p[2] , 0
POKE p[3] , card_rank_charcode
POKE p[4] , 243
p = p + 40
POKE p[0] , 241
POKE p[1] , 227
POKE p[2] , 227
POKE p[3] , 227
POKE p[4] , 242
FEND
// Render face-down card stack and top border of visible card
// Returns number of rows drawn (for offset calculation)
// n=0: just draws normal top border ($e0)
// n=1-2: draws modified top border showing stacked cards
// n=3+: draws multiple rows ending with top border
FUNC render_facedown_stack({WORD screen_address} {WORD offset} {BYTE num_facedown} out:{BYTE rows_drawn})
WORD p @ $fc
p = screen_address + offset
BYTE middle_rows
BYTE top_type
IF num_facedown == 0
// Normal top border
POKE p[0] , $e0
POKE p[1] , $e1
POKE p[2] , $e1
POKE p[3] , $e1
POKE p[4] , $e2
rows_drawn = 1
EXIT
ENDIF
IF num_facedown == 1
// 1 face-down: $fa row
POKE p[0] , $fa
POKE p[1] , $da
POKE p[2] , $da
POKE p[3] , $da
POKE p[4] , $fc
rows_drawn = 1
EXIT
ENDIF
IF num_facedown == 2
// 2 face-down: $fd row
POKE p[0] , $fd
POKE p[1] , $dc
POKE p[2] , $dc
POKE p[3] , $dc
POKE p[4] , $ff
rows_drawn = 1
EXIT
ENDIF
// 3+ face-down cards
// Top row based on (num_facedown mod 3)
top_type = num_facedown
WHILE top_type >= 3
top_type = top_type - 3
WEND
// Draw top row
IF top_type == 0
POKE p[0] , $e0
POKE p[1] , $e1
POKE p[2] , $e1
POKE p[3] , $e1
POKE p[4] , $e2
ENDIF
IF top_type == 1
POKE p[0] , $fa
POKE p[1] , $da
POKE p[2] , $da
POKE p[3] , $da
POKE p[4] , $fc
ENDIF
IF top_type == 2
POKE p[0] , $fd
POKE p[1] , $dc
POKE p[2] , $dc
POKE p[3] , $dc
POKE p[4] , $ff
ENDIF
p = p + 40
rows_drawn = 1
// Calculate middle $d7 rows: (num_facedown - 3) / 3
middle_rows = num_facedown - 3
WHILE middle_rows >= 3
middle_rows = middle_rows - 3
// Draw $d7 row
POKE p[0] , $d7
POKE p[1] , $dc
POKE p[2] , $dc
POKE p[3] , $dc
POKE p[4] , $f4
p = p + 40
rows_drawn++
WEND
// Draw card top row: $d7 for all n>=3
POKE p[0] , $d7
POKE p[1] , $dc
POKE p[2] , $dc
POKE p[3] , $dc
POKE p[4] , $f4
rows_drawn++
FEND
// Draw connecting border between stacked face-up cards
// Used before each face-up card after the first
FUNC render_connecting_border({WORD screen_address} {WORD offset})
WORD p @ $fc
p = screen_address + offset
POKE p[0] , $d4
POKE p[1] , $e1
POKE p[2] , $e1
POKE p[3] , $e1
POKE p[4] , $d6
FEND
// Render just the rank/suit row (no top border)
// Used for face-up cards in a stack where top border is already drawn
FUNC render_card_body_partial({WORD screen_address} {WORD offset} {BYTE card_id})
BYTE card_suit
BYTE card_rank
card_id_to_suit_rank(card_id, card_suit, card_rank)
// Get rank charcode
WORD card_charcode_map_ptr @ $fa
POINTER card_charcode_map_ptr -> card_charcode_map
BYTE card_rank_charcode
card_rank_charcode = PEEK card_charcode_map_ptr[card_rank]
// Adjust color for red suit
IF card_suit < CARD_SUIT_SPADES
card_rank_charcode = card_rank_charcode + 64
ENDIF
// Get suit charcode
WORD card_suit_charcode_map_ptr @ $fa
POINTER card_suit_charcode_map_ptr -> card_suit_charcode_map
BYTE suit_charcode
suit_charcode = PEEK card_suit_charcode_map_ptr[card_suit]
WORD p @ $fc
p = screen_address + offset
// Rank/suit row
POKE p[0] , $f0
POKE p[1] , card_rank_charcode
POKE p[2] , 0
POKE p[3] , suit_charcode
POKE p[4] , $f3
FEND
// Render card body without top border (rank/suit + middle + bottom)
// Used for the last face-up card in a stack
FUNC render_card_body_full({WORD screen_address} {WORD offset} {BYTE card_id})
BYTE card_suit
BYTE card_rank
card_id_to_suit_rank(card_id, card_suit, card_rank)
// Get rank charcode
WORD card_charcode_map_ptr @ $fa
POINTER card_charcode_map_ptr -> card_charcode_map
BYTE card_rank_charcode
card_rank_charcode = PEEK card_charcode_map_ptr[card_rank]
// Adjust color for red suit
IF card_suit < CARD_SUIT_SPADES
card_rank_charcode = card_rank_charcode + 64
ENDIF
// Get suit charcode
WORD card_suit_charcode_map_ptr @ $fa
POINTER card_suit_charcode_map_ptr -> card_suit_charcode_map
BYTE suit_charcode
suit_charcode = PEEK card_suit_charcode_map_ptr[card_suit]
// Get suit graphic pointer
WORD p2 @ $fa
SWITCH card_suit
CASE CARD_SUIT_HEARTS
POINTER p2 -> suit_graphic_hearts
CASE CARD_SUIT_DIAMONDS
POINTER p2 -> suit_graphic_diamonds
CASE CARD_SUIT_SPADES
POINTER p2 -> suit_graphic_spades
CASE CARD_SUIT_CLUBS
POINTER p2 -> suit_graphic_clubs
ENDSWITCH
WORD p @ $fc
p = screen_address + offset
WORD CONST SCREEN_MEM_END = $0400+999
// Bounds check: skip last 2 rows if they'd overflow screen (compact mode)
IF p > SCREEN_MEM_END-4
EXIT
ENDIF
// Row 0: Rank/suit row
POKE p[0] , $f0
POKE p[1] , card_rank_charcode
POKE p[2] , 0
POKE p[3] , suit_charcode
POKE p[4] , $f3
p = p + 40
// Bounds check: skip last 2 rows if they'd overflow screen (compact mode)
IF p > SCREEN_MEM_END-4
EXIT
ENDIF
// Rows 1-3: Suit graphic
BYTE g
POKE p[0] , $f0
g = PEEK p2[0]
POKE p[1] , g
g = PEEK p2[1]
POKE p[2] , g
g = PEEK p2[2]
POKE p[3] , g
POKE p[4] , $f3
p = p + 40
// Bounds check: skip last 2 rows if they'd overflow screen (compact mode)
IF p > SCREEN_MEM_END-4
EXIT
ENDIF
POKE p[0] , $f0
g = PEEK p2[3]
POKE p[1] , g
g = PEEK p2[4]
POKE p[2] , g
g = PEEK p2[5]
POKE p[3] , g
POKE p[4] , $f3
p = p + 40
// Bounds check: skip last 2 rows if they'd overflow screen (compact mode)
IF p > SCREEN_MEM_END-4
EXIT
ENDIF
POKE p[0] , $f0
g = PEEK p2[6]
POKE p[1] , g
g = PEEK p2[7]
POKE p[2] , g
g = PEEK p2[8]
POKE p[3] , g
POKE p[4] , $f3
p = p + 40
// Bounds check: skip last 2 rows if they'd overflow screen (compact mode)
IF p > SCREEN_MEM_END-4
EXIT
ENDIF
// Row 4: Bottom rank/suit
POKE p[0] , $f0
POKE p[1] , suit_charcode
POKE p[2] , 0
POKE p[3] , card_rank_charcode
POKE p[4] , $f3
p = p + 40
// Bounds check: skip bottom border if it'd overflow (K-2 case)
IF p > SCREEN_MEM_END-4
EXIT
ENDIF
// Row 5: Bottom border
POKE p[0] , $f1
POKE p[1] , $e3
POKE p[2] , $e3
POKE p[3] , $e3
POKE p[4] , $f2
FEND
// Render a complete tableau pile from pile data
// Handles face-down stack and all face-up cards
FUNC render_tableau_pile({WORD screen_address} {WORD offset} {WORD pile_ptr @ $fe})
BYTE count
BYTE facedown_count
BYTE faceup_count
BYTE faceup_index
BYTE card
BYTE card_id
BYTE i
BYTE rows
WORD pos
BYTE use_compact
count = PEEK pile_ptr[0]
// Empty pile - nothing to render
IF count == 0
EXIT
ENDIF
// Count face-down cards
facedown_count = 0
BYTE is_facedown
FOR i = 1 TO count
card = PEEK pile_ptr[i]
is_facedown = card & CARD_FACEDOWN
IF is_facedown
facedown_count++
ENDIF
NEXT
faceup_count = count - facedown_count
// Calculate if we need compact mode (total rows > 17)
BYTE facedown_rows
BYTE faceup_rows_normal
BYTE total_rows
BYTE temp_count
// Calculate facedown rows
IF facedown_count == 0
facedown_rows = 0
ELSE
IF facedown_count <= 2
facedown_rows = 1
ELSE
facedown_rows = 2
temp_count = facedown_count - 3
WHILE temp_count >= 3
temp_count = temp_count - 3
facedown_rows++
WEND
ENDIF
ENDIF
// Calculate faceup rows (normal mode)
IF faceup_count == 0
faceup_rows_normal = 0
ELSE
IF faceup_count == 1
faceup_rows_normal = 6
ELSE
// Normal mode: 4 + 2*N rows
faceup_rows_normal = faceup_count + faceup_count
faceup_rows_normal = faceup_rows_normal + 4
ENDIF
ENDIF
// Check if we need compact mode (screen rows 8-24 = 17 rows available)
total_rows = facedown_rows + faceup_rows_normal
use_compact = 0
IF total_rows > 17
use_compact = 1
ENDIF
// Draw face-down stack + first face-up top border
pos = offset
render_facedown_stack(screen_address, pos, facedown_count, rows)
FOR i = 1 TO rows
pos = pos + 40
NEXT
// If no face-up cards, we're done (shouldn't happen in valid game)
IF faceup_count == 0
EXIT
ENDIF
// Render face-up cards
faceup_index = 0
#PRAGMA _P_USE_LONG_JUMP 1
FOR i = 1 TO count
#PRAGMA _P_USE_LONG_JUMP 0
card = PEEK pile_ptr[i]
// Skip face-down cards
is_facedown = card & CARD_FACEDOWN
IF is_facedown
// do nothing
ELSE
faceup_index++
card_id = card & CARD_MASK
IF faceup_index == 1
// First face-up: top border already drawn, just body
IF faceup_count == 1
// Only one face-up card - render full
render_card_body_full(screen_address, pos, card_id)
ELSE
// More cards follow - render partial
render_card_body_partial(screen_address, pos, card_id)
pos = pos + 40
ENDIF
ELSE
// Subsequent face-up cards
IF use_compact == 0
// Normal mode: draw connecting border
render_connecting_border(screen_address, pos)
pos = pos + 40
ENDIF
IF faceup_index == faceup_count
// Last face-up card - render full
render_card_body_full(screen_address, pos, card_id)
ELSE
// More cards follow - render partial
render_card_body_partial(screen_address, pos, card_id)
pos = pos + 40
ENDIF
ENDIF
ENDIF
NEXT
FEND
// Render partial card (top 2 rows only) for stacked display
// Used for face-up cards underneath the top card
FUNC render_card_partial({WORD screen_address} {WORD offset} {BYTE card_id})
BYTE card_suit
BYTE card_rank
card_id_to_suit_rank(card_id, card_suit, card_rank)
// Get rank charcode
WORD card_charcode_map_ptr @ $fa
POINTER card_charcode_map_ptr -> card_charcode_map
BYTE card_rank_charcode
card_rank_charcode = PEEK card_charcode_map_ptr[card_rank]
// Adjust color for red suit
IF card_suit < CARD_SUIT_SPADES
card_rank_charcode = card_rank_charcode + 64
ENDIF
// Get suit charcode
WORD card_suit_charcode_map_ptr @ $fa
POINTER card_suit_charcode_map_ptr -> card_suit_charcode_map
BYTE suit_charcode
suit_charcode = PEEK card_suit_charcode_map_ptr[card_suit]
WORD p @ $fc
p = screen_address + offset
// Row 0: top border
POKE p[0] , 224
POKE p[1] , 225
POKE p[2] , 225
POKE p[3] , 225
POKE p[4] , 226
p = p + 40
// Row 1: rank and suit
POKE p[0] , 240
POKE p[1] , card_rank_charcode
POKE p[2] , 0
POKE p[3] , suit_charcode
POKE p[4] , 243
FEND
// Render back side of a card (face-down full card)
// Same border as face card, middle filled with $52
FUNC render_card_back({WORD screen_address} {WORD offset})
WORD p @ $fc
p = screen_address + offset
// Row 0: top border
POKE p[0] , $e0
POKE p[1] , $e1
POKE p[2] , $e1
POKE p[3] , $e1
POKE p[4] , $e2
p = p + 40
// Row 1: side borders + $52 fill
POKE p[0] , $f0
POKE p[1] , $52
POKE p[2] , $52
POKE p[3] , $52
POKE p[4] , $f3
p = p + 40
// Row 2: side borders + $52 fill
POKE p[0] , $f0
POKE p[1] , $52
POKE p[2] , $52
POKE p[3] , $52
POKE p[4] , $f3
p = p + 40
// Row 3: side borders + $52 fill
POKE p[0] , $f0
POKE p[1] , $52
POKE p[2] , $52
POKE p[3] , $52
POKE p[4] , $f3
p = p + 40
// Row 4: side borders + $52 fill
POKE p[0] , $f0
POKE p[1] , $52
POKE p[2] , $52
POKE p[3] , $52
POKE p[4] , $f3
p = p + 40
// Row 5: side borders + $52 fill
POKE p[0] , $f0
POKE p[1] , $52
POKE p[2] , $52
POKE p[3] , $52
POKE p[4] , $f3
p = p + 40
// Row 6: bottom border
POKE p[0] , $f1
POKE p[1] , $e3
POKE p[2] , $e3
POKE p[3] , $e3
POKE p[4] , $f2
FEND
// Render empty pile placeholder
// Same border shape but -$80 for light pink color, interior filled with 0
FUNC render_empty_pile({WORD screen_address} {WORD offset})
WORD p @ $fc
p = screen_address + offset
// Row 0: top border (pink)
POKE p[0] , $60
POKE p[1] , $61
POKE p[2] , $61
POKE p[3] , $61
POKE p[4] , $62
p = p + 40
// Row 1: side borders + empty fill
POKE p[0] , $70
POKE p[1] , 0
POKE p[2] , 0
POKE p[3] , 0
POKE p[4] , $73
p = p + 40
// Row 2: side borders + empty fill
POKE p[0] , $70
POKE p[1] , 0
POKE p[2] , 0
POKE p[3] , 0
POKE p[4] , $73
p = p + 40
// Row 3: side borders + empty fill
POKE p[0] , $70
POKE p[1] , 0
POKE p[2] , 0
POKE p[3] , 0
POKE p[4] , $73
p = p + 40
// Row 4: side borders + empty fill
POKE p[0] , $70
POKE p[1] , 0
POKE p[2] , 0
POKE p[3] , 0
POKE p[4] , $73
p = p + 40
// Row 5: side borders + empty fill
POKE p[0] , $70
POKE p[1] , 0
POKE p[2] , 0
POKE p[3] , 0
POKE p[4] , $73
p = p + 40
// Row 6: bottom border (pink)
POKE p[0] , $71
POKE p[1] , $63
POKE p[2] , $63
POKE p[3] , $63
POKE p[4] , $72
FEND
// Render foundation pile - shows top card or empty placeholder
FUNC render_foundation_pile({WORD screen_address} {WORD offset} {WORD pile_ptr @ $fe})
BYTE count
BYTE card
BYTE card_id
count = PEEK pile_ptr[0]
IF count == 0
render_empty_pile(screen_address, offset)
ELSE
card = PEEK pile_ptr[count]
card_id = card & CARD_MASK
render_card(screen_address, offset, card_id)
ENDIF
FEND
// Render stock pile - shows card back or empty placeholder
FUNC render_stock_pile({WORD screen_address} {WORD offset} {WORD pile_ptr @ $fe})
BYTE count
count = PEEK pile_ptr[0]
IF count == 0
render_empty_pile(screen_address, offset)
ELSE
render_card_back(screen_address, offset)
ENDIF
FEND
// Render left edge of card (2 columns) for fanned display
FUNC render_card_left_edge({WORD screen_address} {WORD offset} {BYTE card_id})
BYTE card_suit
BYTE card_rank
card_id_to_suit_rank(card_id, card_suit, card_rank)
// Get rank charcode
WORD card_charcode_map_ptr @ $fa
POINTER card_charcode_map_ptr -> card_charcode_map
BYTE card_rank_charcode
card_rank_charcode = PEEK card_charcode_map_ptr[card_rank]
// Adjust color for red suit
IF card_suit < CARD_SUIT_SPADES
card_rank_charcode = card_rank_charcode + 64
ENDIF
// Get suit charcode
WORD card_suit_charcode_map_ptr @ $fa
POINTER card_suit_charcode_map_ptr -> card_suit_charcode_map
BYTE suit_charcode
suit_charcode = PEEK card_suit_charcode_map_ptr[card_suit]
// Get suit graphic pointer
WORD p2 @ $fa
SWITCH card_suit
CASE CARD_SUIT_HEARTS
POINTER p2 -> suit_graphic_hearts
CASE CARD_SUIT_DIAMONDS
POINTER p2 -> suit_graphic_diamonds
CASE CARD_SUIT_SPADES
POINTER p2 -> suit_graphic_spades
CASE CARD_SUIT_CLUBS
POINTER p2 -> suit_graphic_clubs
ENDSWITCH
WORD p @ $fc
p = screen_address + offset
BYTE g
// Row 0: top border (2 cols)
POKE p[0] , $e0
POKE p[1] , $e1
p = p + 40
// Row 1: left border + rank
POKE p[0] , $f0
POKE p[1] , card_rank_charcode
p = p + 40
// Row 2: left border + suit graphic
POKE p[0] , $f0
g = PEEK p2[0]
POKE p[1] , g
p = p + 40
// Row 3: left border + suit graphic
POKE p[0] , $f0
g = PEEK p2[3]
POKE p[1] , g
p = p + 40
// Row 4: left border + suit graphic
POKE p[0] , $f0
g = PEEK p2[6]
POKE p[1] , g
p = p + 40
// Row 5: left border + suit
POKE p[0] , $f0
POKE p[1] , suit_charcode
p = p + 40
// Row 6: bottom border (2 cols)
POKE p[0] , $f1
POKE p[1] , $e3
FEND
// Render waste pile - draw-3 style fanned display
// Shows up to 3 cards fanned horizontally (2 col offset per card)
FUNC render_waste_pile({WORD screen_address} {WORD offset} {WORD pile_ptr @ $fe} {BYTE draw_mode})
BYTE count
BYTE card
BYTE card_id
WORD off2
WORD off4
count = PEEK pile_ptr[0]
// Draw-1 mode: only show top card
IF draw_mode == 1
IF count == 0
render_empty_pile(screen_address, offset)
ELSE
card = PEEK pile_ptr[count]
card_id = card & CARD_MASK
render_card(screen_address, offset, card_id)
ENDIF
EXIT
ENDIF
// Draw-3 mode: fan out top 3 cards
off2 = offset + 2
off4 = offset + 4
SWITCH count
CASE 0
render_empty_pile(screen_address, offset)
CASE 1
card = PEEK pile_ptr[1]
card_id = card & CARD_MASK
render_card(screen_address, offset, card_id)
CASE 2
// 2nd from top (left edge)
card = PEEK pile_ptr[1]
card_id = card & CARD_MASK
render_card_left_edge(screen_address, offset, card_id)
// Top card (full)
card = PEEK pile_ptr[2]
card_id = card & CARD_MASK
render_card(screen_address, off2, card_id)
DEFAULT
// 3+ cards: show top 3 fanned
BYTE idx
// 3rd from top (left edge)
idx = count - 2
card = PEEK pile_ptr[idx]
card_id = card & CARD_MASK
render_card_left_edge(screen_address, offset, card_id)
// 2nd from top (left edge)
idx = count - 1
card = PEEK pile_ptr[idx]
card_id = card & CARD_MASK
render_card_left_edge(screen_address, off2, card_id)
// Top card (full)
card = PEEK pile_ptr[count]
card_id = card & CARD_MASK
render_card(screen_address, off4, card_id)
ENDSWITCH
FEND
LABEL __skip_lib_cardrender
#IFEND

899
cardsprites.c65 Normal file
View file

@ -0,0 +1,899 @@
#IFNDEF __lib_cardsprites
#DEFINE __lib_cardsprites 1
GOTO __skip_lib_cardsprites
// ============================================================================
// CARD RANK SPRITES (A-K)
// ============================================================================
// Sprites for displaying card ranks in multiplexed border display
// Each sprite is 64 bytes (21 rows of 3 bytes + 1 padding byte)
// ============================================================================
// Sprite 0: Ace
LABEL sprite_rank_ace
ASM
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $10, $00
!8 $00, $10, $00
!8 $00, $38, $00
!8 $00, $38, $00
!8 $00, $6c, $00
!8 $00, $6c, $00
!8 $00, $c6, $00
!8 $00, $fe, $00
!8 $01, $ff, $00
!8 $01, $83, $00
!8 $03, $c7, $80
!8 $03, $c7, $80
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $fe
ENDASM
// Sprite 1: 2
LABEL sprite_rank_2
ASM
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $7c, $00
!8 $00, $fe, $00
!8 $00, $c6, $00
!8 $00, $06, $00
!8 $00, $0e, $00
!8 $00, $1c, $00
!8 $00, $38, $00
!8 $00, $70, $00
!8 $00, $e6, $00
!8 $00, $c6, $00
!8 $00, $fe, $00
!8 $00, $fe, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $fe
ENDASM
// Sprite 2: 3
LABEL sprite_rank_3
ASM
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $fe, $00
!8 $00, $fe, $00
!8 $00, $cc, $00
!8 $00, $18, $00
!8 $00, $30, $00
!8 $00, $7c, $00
!8 $00, $7e, $00
!8 $00, $06, $00
!8 $00, $06, $00
!8 $00, $c6, $00
!8 $00, $fe, $00
!8 $00, $7c, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $fe
ENDASM
// Sprite 3: 4
LABEL sprite_rank_4
ASM
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $0c, $00
!8 $00, $1c, $00
!8 $00, $3c, $00
!8 $00, $7c, $00
!8 $00, $ec, $00
!8 $01, $cc, $00
!8 $01, $ff, $00
!8 $01, $ff, $00
!8 $00, $0c, $00
!8 $00, $0c, $00
!8 $00, $1e, $00
!8 $00, $1e, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $fe
ENDASM
// Sprite 4: 5
LABEL sprite_rank_5
ASM
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $fe, $00
!8 $00, $fe, $00
!8 $00, $c0, $00
!8 $00, $c0, $00
!8 $00, $fc, $00
!8 $00, $fe, $00
!8 $00, $06, $00
!8 $00, $06, $00
!8 $00, $c6, $00
!8 $00, $c6, $00
!8 $00, $fe, $00
!8 $00, $7c, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $fe
ENDASM
// Sprite 5: 6
LABEL sprite_rank_6
ASM
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $3c, $00
!8 $00, $7c, $00
!8 $00, $e0, $00
!8 $00, $c0, $00
!8 $00, $fc, $00
!8 $00, $fe, $00
!8 $00, $c6, $00
!8 $00, $c6, $00
!8 $00, $c6, $00
!8 $00, $c6, $00
!8 $00, $fe, $00
!8 $00, $7c, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $fe
ENDASM
// Sprite 6: 7
LABEL sprite_rank_7
ASM
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $fe, $00
!8 $00, $fe, $00
!8 $00, $c6, $00
!8 $00, $0c, $00
!8 $00, $0c, $00
!8 $00, $18, $00
!8 $00, $18, $00
!8 $00, $18, $00
!8 $00, $30, $00
!8 $00, $30, $00
!8 $00, $30, $00
!8 $00, $30, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $fe
ENDASM
// Sprite 7: 8
LABEL sprite_rank_8
ASM
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $7c, $00
!8 $00, $fe, $00
!8 $00, $c6, $00
!8 $00, $c6, $00
!8 $00, $c6, $00
!8 $00, $7c, $00
!8 $00, $fe, $00
!8 $00, $c6, $00
!8 $00, $c6, $00
!8 $00, $c6, $00
!8 $00, $fe, $00
!8 $00, $7c, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $fe
ENDASM
// Sprite 8: 9
LABEL sprite_rank_9
ASM
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $7c, $00
!8 $00, $fe, $00
!8 $00, $c6, $00
!8 $00, $c6, $00
!8 $00, $c6, $00
!8 $00, $c6, $00
!8 $00, $fe, $00
!8 $00, $7e, $00
!8 $00, $06, $00
!8 $00, $0e, $00
!8 $00, $7c, $00
!8 $00, $78, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $fe
ENDASM
// Sprite 9: 10
LABEL sprite_rank_10
ASM
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $01, $9e, $00
!8 $01, $bf, $00
!8 $01, $b3, $00
!8 $01, $b3, $00
!8 $01, $b3, $00
!8 $01, $b3, $00
!8 $01, $b3, $00
!8 $01, $b3, $00
!8 $01, $b3, $00
!8 $01, $b3, $00
!8 $01, $bf, $00
!8 $01, $9e, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $fe
ENDASM
// Sprite 10: Jack
LABEL sprite_rank_jack
ASM
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $1e, $00
!8 $00, $1e, $00
!8 $00, $0c, $00
!8 $00, $0c, $00
!8 $00, $0c, $00
!8 $00, $0c, $00
!8 $00, $0c, $00
!8 $00, $0c, $00
!8 $00, $cc, $00
!8 $00, $cc, $00
!8 $00, $fc, $00
!8 $00, $78, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $fe
ENDASM
// Sprite 11: Queen
LABEL sprite_rank_queen
ASM
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $7c, $00
!8 $00, $fe, $00
!8 $00, $c6, $00
!8 $00, $c6, $00
!8 $00, $c6, $00
!8 $00, $c6, $00
!8 $00, $c6, $00
!8 $00, $c6, $00
!8 $00, $c6, $00
!8 $00, $c6, $00
!8 $00, $fe, $00
!8 $00, $7c, $00
!8 $00, $0e, $00
!8 $00, $06, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $fe
ENDASM
// Sprite 12: King
LABEL sprite_rank_king
ASM
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $01, $ef, $00
!8 $01, $ef, $00
!8 $00, $cc, $00
!8 $00, $d8, $00
!8 $00, $f0, $00
!8 $00, $e0, $00
!8 $00, $f0, $00
!8 $00, $d8, $00
!8 $00, $cc, $00
!8 $00, $c6, $00
!8 $01, $ef, $00
!8 $01, $ef, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $fe
ENDASM
// ============================================================================
// CREDITS SPRITES
// ============================================================================
// Two sprites that go together horizontally to display credits
// ============================================================================
// Sprite 13: Credits (left part)
LABEL sprite_credits_left
ASM
!8 $ae, $ea, $ee
!8 $aa, $8a, $2a
!8 $ee, $8c, $4a
!8 $aa, $8a, $8a
!8 $aa, $ea, $ee
!8 $00, $00, $00
!8 $0e, $4c, $ee
!8 $08, $4a, $8a
!8 $0e, $4a, $ce
!8 $02, $4a, $8c
!8 $0e, $4c, $ea
!8 $00, $00, $00
!8 $00, $ee, $ee
!8 $00, $2a, $28
!8 $00, $ea, $ee
!8 $00, $8a, $8a
!8 $00, $ee, $ee
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $fe
ENDASM
// Sprite 14: Credits (right part)
LABEL sprite_credits_right
ASM
!8 $4c, $00, $00
!8 $4a, $00, $00
!8 $4a, $00, $00
!8 $4a, $00, $00
!8 $4c, $00, $00
!8 $00, $00, $00
!8 $e0, $00, $00
!8 $80, $00, $00
!8 $e0, $00, $00
!8 $20, $00, $00
!8 $e0, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $fe
ENDASM
// ============================================================================
// SUIT COLOR SPRITES (Overlay pairs for multi-color display)
// ============================================================================
// Each suit uses 2 sprites overlaid to allow for two screen colors
// Hearts (sprites 15-16), Diamonds (17-18), Clubs (19-20), Spades (21-22)
// ============================================================================
// Sprite 15: Hearts (layer 1)
LABEL sprite_suit_hearts_1
ASM
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $01, $c7, $00
!8 $03, $ef, $80
!8 $07, $ff, $c0
!8 $07, $ff, $c0
!8 $07, $ff, $c0
!8 $03, $ff, $80
!8 $01, $ff, $00
!8 $00, $fe, $00
!8 $00, $7c, $00
!8 $00, $38, $00
!8 $00, $10, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $12
ENDASM
// Sprite 16: Hearts (layer 2)
LABEL sprite_suit_hearts_2
ASM
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $41, $00
!8 $00, $20, $80
!8 $00, $00, $80
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $0a
ENDASM
// Sprite 17: Diamonds (layer 1)
LABEL sprite_suit_diamonds_1
ASM
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $10, $00
!8 $00, $38, $00
!8 $00, $7c, $00
!8 $00, $fe, $00
!8 $01, $ff, $00
!8 $03, $ff, $80
!8 $03, $ff, $80
!8 $01, $ff, $00
!8 $00, $fe, $00
!8 $00, $7c, $00
!8 $00, $38, $00
!8 $00, $10, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $12
ENDASM
// Sprite 18: Diamonds (layer 2)
LABEL sprite_suit_diamonds_2
ASM
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $08, $00
!8 $00, $04, $00
!8 $00, $02, $00
!8 $00, $01, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $0a
ENDASM
// Sprite 19: Clubs (layer 1)
LABEL sprite_suit_clubs_1
ASM
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $38, $00
!8 $00, $7c, $00
!8 $00, $7c, $00
!8 $00, $7c, $00
!8 $03, $bb, $80
!8 $07, $d7, $c0
!8 $07, $ff, $c0
!8 $07, $d7, $c0
!8 $03, $93, $80
!8 $00, $10, $00
!8 $00, $38, $00
!8 $00, $7c, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $10
ENDASM
// Sprite 20: Clubs (layer 2)
LABEL sprite_suit_clubs_2
ASM
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $38, $00
!8 $00, $0c, $00
!8 $00, $04, $00
!8 $00, $04, $00
!8 $01, $83, $80
!8 $00, $c0, $c0
!8 $00, $00, $40
!8 $00, $00, $40
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $08, $00
!8 $00, $04, $00
;!8 $08, $00, $00
;!8 $04, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $0b
ENDASM
// Sprite 21: Spades (layer 1)
LABEL sprite_suit_spades_1
ASM
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $10, $00
!8 $00, $38, $00
!8 $00, $7c, $00
!8 $00, $fe, $00
!8 $01, $ff, $00
!8 $03, $ff, $80
!8 $03, $ff, $80
!8 $03, $ff, $80
!8 $01, $d7, $00
!8 $00, $10, $00
!8 $00, $38, $00
!8 $00, $7c, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $10
ENDASM
// Sprite 22: Spades (layer 2)
LABEL sprite_suit_spades_2
ASM
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $10, $00
!8 $00, $18, $00
!8 $00, $0c, $00
!8 $00, $06, $00
!8 $00, $03, $00
!8 $00, $01, $80
!8 $00, $00, $80
!8 $00, $00, $80
!8 $00, $00, $00
!8 $00, $00, $00
;!8 $08, $00, $00
!8 $00, $08, $00
;!8 $04, $00, $00
!8 $00, $04, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $0b
ENDASM
// ============================================================================
// TEXT SPRITES - "SELECTED CARD"
// ============================================================================
// Two sprites that display "SELECTED CARD" text
// ============================================================================
// Sprite 23: "SELECTED CARD" (left part)
LABEL sprite_text_selected_card_1
ASM
!8 $77, $47, $77
!8 $44, $44, $42
!8 $76, $46, $42
!8 $14, $44, $42
!8 $77, $77, $72
!8 $00, $00, $00
!8 $00, $00, $00
!8 $77, $76, $00
!8 $45, $55, $00
!8 $47, $75, $00
!8 $45, $65, $00
!8 $75, $56, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00
ENDASM
// Sprite 24: "SELECTED CARD" (right part)
LABEL sprite_text_selected_card_2
ASM
!8 $76, $00, $00
!8 $45, $00, $00
!8 $65, $00, $00
!8 $45, $00, $00
!8 $76, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00
ENDASM
// ============================================================================
// CARD DISPLAY SPRITE MANAGEMENT
// ============================================================================
// Functions to display selected card using sprites in upper right corner
// Uses sprites 1-5 (sprite 0 is reserved for pointer)
// ============================================================================
// VIC-II Sprite registers for card display (sprites 1-5)
// 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
BYTE sprite_x2 @ $d004 // Sprite 2 X
BYTE sprite_y2 @ $d005 // Sprite 2 Y
BYTE sprite_x3 @ $d006 // Sprite 3 X
BYTE sprite_y3 @ $d007 // Sprite 3 Y
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_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_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
// Card display sprite positions (upper right corner)
// Screen starts at (24,50), size 320x200
// Sprites are 24 pixels wide
WORD CONST CARD_TEXT_X1 = 312 // "SELECTED CARD" left part
WORD CONST CARD_TEXT_X2 = CARD_TEXT_X1+24 // "SELECTED CARD" right part
BYTE CONST CARD_TEXT_Y = 50 // Top of screen
WORD CONST CARD_RANK_X = 312 // Rank sprite X
WORD CONST CARD_SUIT_X = CARD_RANK_X+14 // Suit sprites X (both layers) - side by side with rank
BYTE CONST CARD_RANK_Y = 64 // Below text (50 + 21 pixel sprite height)
// Sprite data block numbers (offset from base at $2240 / 64 = 137)
BYTE CONST SPRITE_BLOCK_TEXT_LEFT = 160 // sprite_text_selected_card_1 (sprite 23)
BYTE CONST SPRITE_BLOCK_TEXT_RIGHT = 161 // sprite_text_selected_card_2 (sprite 24)
// Rank blocks: 137-149 (ace=137, 2=138, ... king=149)
// Suit blocks: hearts1=152, hearts2=153, diamonds1=154, diamonds2=155,
// clubs1=156, clubs2=157, spades1=158, spades2=159
// ============================================================================
// FUNC card_display_init
// Initialize card display sprites (position and "SELECTED CARD" text)
// Call this once during game initialization
// ============================================================================
FUNC card_display_init
BYTE temp
// Set "SELECTED CARD" text sprites (sprites 1 & 2)
sprite_pointer1 = SPRITE_BLOCK_TEXT_LEFT
sprite_pointer2 = SPRITE_BLOCK_TEXT_RIGHT
// Position text sprites
sprite_x1 = CARD_TEXT_X1
sprite_y1 = CARD_TEXT_Y
sprite_x2 = CARD_TEXT_X2
sprite_y2 = CARD_TEXT_Y
// Set X MSB for sprites > 255
temp = sprite_x_msb
IF CARD_TEXT_X1 > 255
temp = temp | %00000010 // Sprite 1 X MSB
ENDIF
IF CARD_TEXT_X2 > 255
temp = temp | %00000100 // Sprite 2 X MSB
ENDIF
sprite_x_msb = temp
// Set text sprite colors (black)
sprite_color1 = color_black
sprite_color2 = color_black
// Initially hide all card display sprites (1-5)
temp = sprite_enable
temp = temp & %11000001 // Keep sprite 0 (pointer), clear 1-5
sprite_enable = temp
FEND
// ============================================================================
// FUNC card_display_show
// Show selected card using sprites
// Parameters:
// card_rank: Card rank (0-12: A,2-10,J,Q,K)
// card_suit: Card suit (0-3: hearts, diamonds, clubs, spades)
// ============================================================================
FUNC card_display_show({BYTE card_rank} {BYTE card_suit})
BYTE rank_block
BYTE suit_block1
BYTE suit_block2
BYTE suit_color
BYTE rank_color
BYTE suit_color2
// Calculate sprite block for rank (137 = ace, 138 = 2, etc.)
rank_block = 137
rank_block = rank_block + card_rank
// Calculate sprite blocks for suit (2 layers each)
// Sprite order: Hearts: 152-153, Diamonds: 154-155, Clubs: 156-157, Spades: 158-159
// Card suit order: 0=Hearts, 1=Diamonds, 2=Spades, 3=Clubs
// Need to swap spades(2) and clubs(3)
BYTE suit_index
IF card_suit == 2
suit_index = 3 // Spades (card suit 2) -> sprite index 3
ELSE
IF card_suit == 3
suit_index = 2 // Clubs (card suit 3) -> sprite index 2
ELSE
suit_index = card_suit // Hearts and Diamonds map directly
ENDIF
ENDIF
ASM
lda |suit_index|
asl // Multiply by 2 (shift left)
clc
adc #152
sta |suit_block1|
ENDASM
suit_block2 = suit_block1 + 1
//suit_block2 = suit_block2 + 1
// Determine suit colors
// Rank and suit layer 1: red for hearts/diamonds, black for clubs/spades
// Suit layer 2: pink for red suits, dark grey for black suits
IF card_suit < 2
rank_color = color_red
suit_color = color_red
suit_color2 = color_pink
ELSE
rank_color = color_black
suit_color = color_black
suit_color2 = color_dark_grey
ENDIF
// Set sprite 3: Rank
sprite_pointer3 = rank_block
sprite_x3 = CARD_RANK_X
sprite_y3 = CARD_RANK_Y
sprite_color3 = rank_color
// Set sprites 4 & 5: Suit (2 layers at same position)
// Sprite 4 has higher priority (drawn on top), so it gets the overlay layer
// Sprite 5 has lower priority (drawn behind), so it gets the base layer
sprite_pointer4 = suit_block2 // Overlay layer (drawn on top)
sprite_pointer5 = suit_block1 // Base layer (drawn behind)
sprite_x4 = CARD_SUIT_X
sprite_y4 = CARD_RANK_Y // Same Y as rank
sprite_x5 = CARD_SUIT_X // Exactly same X/Y as sprite 4
sprite_y5 = CARD_RANK_Y
sprite_color4 = suit_color2 // Overlay color (pink/dark grey)
sprite_color5 = suit_color // Base color (red/black)
// Set X MSB for rank and suit sprites
IF CARD_RANK_X > 255
sprite_x_msb = sprite_x_msb | %00111110
ENDIF
// Enable all card display sprites (1-5)
sprite_enable = sprite_enable | %00111110
FEND
// ============================================================================
// FUNC card_display_hide
// Hide all card display sprites (when no card selected)
// ============================================================================
FUNC card_display_hide
BYTE temp
// Disable sprites 1-5, keep sprite 0 (pointer)
temp = sprite_enable
temp = temp & %11000001
sprite_enable = temp
FEND
LABEL __skip_lib_cardsprites
#IFEND

878
cardtests.c65 Normal file
View file

@ -0,0 +1,878 @@
#IFNDEF __lib_cardtests
#DEFINE __lib_cardtests 1
#INCLUDE "cardrender.c65"
#INCLUDE "piles.c65"
#INCLUDE "carddeck.c65"
#INCLUDE "cardmoves.c65"
GOTO __skip_lib_cardtests
FUNC show_charset
WORD i
BYTE b
b = 0
FOR i = $0400 TO $0400+999
POKE i , b
b++
NEXT
FEND
// Demo: fill $c000-$c1ff with 512 random bytes
FUNC rand_demo
WORD ptr @ $fa
WORD count
BYTE r
ptr = $c000
FOR count = 0 TO 511
//rand(r)
rand_max(52, r)
POKE ptr , r
ptr++
NEXT
FEND
FUNC render_all_cards_test
// Test render_card - render all cards by ID
BYTE i
WORD wi
wi = 0
FOR i = 0 TO 12
render_card($0404, wi, i)
wi = wi + 2
NEXT
wi = 0
FOR i = 0+13 TO 12+13
render_card(7*40+$0404, wi, i)
wi = wi + 2
NEXT
wi = 0
FOR i = 13*2+0 TO 13*2+12
render_card(2*7*40+$0404, wi, i)
wi = wi + 2
NEXT
wi = 0
FOR i = 13*3+0 TO 13*3+12
render_card(3*7*40+$0404, wi, i)
wi = wi + 2
NEXT
FEND
// Test face-down stack rendering (1-6 cards)
FUNC render_facedown_test
BYTE rows
// Column 0: 1 face-down
render_facedown_stack($0400, 0, 1, rows)
// Column 1: 2 face-down
render_facedown_stack($0400, 6, 2, rows)
// Column 2: 3 face-down
render_facedown_stack($0400, 12, 3, rows)
// Column 3: 4 face-down
render_facedown_stack($0400, 18, 4, rows)
// Column 4: 5 face-down
render_facedown_stack($0400, 24, 0, rows)
// Column 5: 6 face-down
render_facedown_stack($0400, 30, 51, rows)
FEND
// Test stacked face-up cards
FUNC render_faceup_stack_test
BYTE rows
BYTE i
WORD pos
// Column 0: 1 face-down, 1 face-up (full card)
pos = 0
render_facedown_stack($0400, pos, 1, rows)
FOR i = 1 TO rows
pos = pos + 40
NEXT
render_card_body_full($0400, pos, 0) // Ace of Hearts
// Column 1: 2 face-down, 2 face-up
pos = 6
render_facedown_stack($0400, pos, 2, rows)
FOR i = 1 TO rows
pos = pos + 40
NEXT
render_card_body_partial($0400, pos, 13) // Ace of Diamonds
pos = pos + 40
render_connecting_border($0400, pos)
pos = pos + 40
render_card_body_full($0400, pos, 26) // Ace of Spades
// Column 2: 1 face-down, 3 face-up
pos = 12
render_facedown_stack($0400, pos, 1, rows)
FOR i = 1 TO rows
pos = pos + 40
NEXT
render_card_body_partial($0400, pos, 1) // 2 of Hearts
pos = pos + 40
render_connecting_border($0400, pos)
pos = pos + 40
render_card_body_partial($0400, pos, 14) // 2 of Diamonds
pos = pos + 40
render_connecting_border($0400, pos)
pos = pos + 40
render_card_body_full($0400, pos, 27) // 2 of Spades
// Column 3: 0 face-down, 2 face-up (no face-down cards)
pos = 18
render_facedown_stack($0400, pos, 0, rows)
FOR i = 1 TO rows
pos = pos + 40
NEXT
render_card_body_partial($0400, pos, 39) // Ace of Clubs
pos = pos + 40
render_connecting_border($0400, pos)
pos = pos + 40
render_card_body_full($0400, pos, 40) // 2 of Clubs
// Column 4: 3 face-down, 1 face-up
pos = 24
render_facedown_stack($0400, pos, 3, rows)
FOR i = 1 TO rows
pos = pos + 40
NEXT
render_card_body_full($0400, pos, 12) // King of Hearts
// Column 5: Standalone card (foundation style)
render_card($0400, 30, 51) // King of Clubs
FEND
// Test rendering actual dealt tableau piles
FUNC render_tableaus_test
WORD ptr @ $f8
// Render all 7 tableau piles
// Each pile is 6 chars wide (5 card + 1 space)
POINTER ptr -> pile_tab0
render_tableau_pile($0400, 0, ptr)
POINTER ptr -> pile_tab1
render_tableau_pile($0400, 5, ptr)
POINTER ptr -> pile_tab2
render_tableau_pile($0400, 10, ptr)
POINTER ptr -> pile_tab3
render_tableau_pile($0400, 15, ptr)
POINTER ptr -> pile_tab4
render_tableau_pile($0400, 20, ptr)
POINTER ptr -> pile_tab5
render_tableau_pile($0400, 25, ptr)
POINTER ptr -> pile_tab6
render_tableau_pile($0400, 30, ptr)
FEND
// Setup mid-game tableau piles for demo (Klondike rules)
// Card IDs: Hearts 0-12 (red), Diamonds 13-25 (red), Spades 26-38 (black), Clubs 39-51 (black)
// Rank: A=0, 2=1, 3=2, 4=3, 5=4, 6=5, 7=6, 8=7, 9=8, 10=9, J=10, Q=11, K=12
// Klondike: alternating colors, descending rank
FUNC setup_midgame_piles
WORD ptr @ $f8
// Tab0: K♥(red) → Q♠(black) → J♦(red) → 10♣(black)
POINTER ptr -> pile_tab0
POKE ptr[0] , 4
POKE ptr[1] , 12 // King of Hearts (red)
POKE ptr[2] , 37 // Queen of Spades (black)
POKE ptr[3] , 23 // Jack of Diamonds (red)
POKE ptr[4] , 48 // 10 of Clubs (black)
// Tab1: 1 face-down, Q♣(black) → J♥(red) → 10♠(black)
POINTER ptr -> pile_tab1
POKE ptr[0] , 4
POKE ptr[1] , 0|CARD_FACEDOWN
POKE ptr[2] , 50 // Queen of Clubs (black)
POKE ptr[3] , 10 // Jack of Hearts (red)
POKE ptr[4] , 35 // 10 of Spades (black)
// Tab2: 2 face-down, 7♦(red)
POINTER ptr -> pile_tab2
POKE ptr[0] , 3
POKE ptr[1] , 1|CARD_FACEDOWN
POKE ptr[2] , 2|CARD_FACEDOWN
POKE ptr[3] , 19 // 7 of Diamonds (red)
// Tab3: K♠(black) → Q♥(red) → J♣(black) → 10♦(red) → 9♠(black)
POINTER ptr -> pile_tab3
POKE ptr[0] , 5
POKE ptr[1] , 38 // King of Spades (black)
POKE ptr[2] , 11 // Queen of Hearts (red)
POKE ptr[3] , 49 // Jack of Clubs (black)
POKE ptr[4] , 22 // 10 of Diamonds (red)
POKE ptr[5] , 34 // 9 of Spades (black)
// Tab4: 3 face-down, K♦(red) → Q♣(black)
POINTER ptr -> pile_tab4
POKE ptr[0] , 5
POKE ptr[1] , 3|CARD_FACEDOWN
POKE ptr[2] , 4|CARD_FACEDOWN
POKE ptr[3] , 5|CARD_FACEDOWN
POKE ptr[4] , 25 // King of Diamonds (red)
POKE ptr[5] , 50 // Queen of Clubs (black)
// Tab5: 4 face-down, 5♥(red)
POINTER ptr -> pile_tab5
POKE ptr[0] , 5
POKE ptr[1] , 6|CARD_FACEDOWN
POKE ptr[2] , 7|CARD_FACEDOWN
POKE ptr[3] , 8|CARD_FACEDOWN
POKE ptr[4] , 9|CARD_FACEDOWN
POKE ptr[5] , 4 // 5 of Hearts (red)
// Tab6: 5 face-down, 8♣(black) → 7♥(red)
POINTER ptr -> pile_tab6
POKE ptr[0] , 7
POKE ptr[1] , 10|CARD_FACEDOWN
POKE ptr[2] , 11|CARD_FACEDOWN
POKE ptr[3] , 12|CARD_FACEDOWN
POKE ptr[4] , 13|CARD_FACEDOWN
POKE ptr[5] , 14|CARD_FACEDOWN
POKE ptr[6] , 46 // 8 of Clubs (black)
POKE ptr[7] , 6 // 7 of Hearts (red)
FEND
// Test mid-game tableau rendering
FUNC render_midgame_test
setup_midgame_piles()
render_tableaus_test()
FEND
// Setup foundation piles in different states for testing
FUNC setup_foundation_test_piles
WORD ptr @ $f8
// Foundation 0: empty (Hearts)
POINTER ptr -> pile_found0
POKE ptr[0] , 0
// Foundation 1: Ace only (Diamonds)
POINTER ptr -> pile_found1
POKE ptr[0] , 1
POKE ptr[1] , 13 // A♦
// Foundation 2: A-2-3 (Spades)
POINTER ptr -> pile_found2
POKE ptr[0] , 3
POKE ptr[1] , 26 // A♠
POKE ptr[2] , 27 // 2♠
POKE ptr[3] , 28 // 3♠
// Foundation 3: full A-K (Clubs)
POINTER ptr -> pile_found3
POKE ptr[0] , 13
BYTE i
BYTE card
card = 39
ptr++
FOR i = 0 TO 12
POKE ptr , card
ptr++
card++
NEXT
FEND
// Test foundation pile rendering
FUNC render_foundation_test
WORD ptr @ $f8
setup_foundation_test_piles()
// Render 4 foundations side by side
POINTER ptr -> pile_found0
render_foundation_pile($0400, 0, ptr)
POINTER ptr -> pile_found1
render_foundation_pile($0400, 6, ptr)
POINTER ptr -> pile_found2
render_foundation_pile($0400, 12, ptr)
POINTER ptr -> pile_found3
render_foundation_pile($0400, 18, ptr)
FEND
// Test waste pile rendering with different card counts
FUNC render_waste_test
WORD ptr @ $f8 // Use $f8 to avoid conflict with $fa used by sub-functions
// Test 0 cards
POINTER ptr -> pile_waste
POKE ptr[0] , 0
render_waste_pile($0400, 0, ptr, 3)
// Test 1 card
POKE ptr[0] , 1
POKE ptr[1] , 0 // A♥
render_waste_pile($0400, 10, ptr, 3)
// Test 2 cards
POKE ptr[0] , 2
POKE ptr[1] , 13 // A♦
POKE ptr[2] , 26 // A♠
render_waste_pile($0400, 20, ptr, 3)
// Test 3 cards
POKE ptr[0] , 3
POKE ptr[1] , 39 // A♣
POKE ptr[2] , 1 // 2♥
POKE ptr[3] , 14 // 2♦
render_waste_pile($0400, 8*40, ptr, 3)
// Test 5 cards (should show only top 3)
POKE ptr[0] , 5
POKE ptr[1] , 10 // J♥
POKE ptr[2] , 11 // Q♥
POKE ptr[3] , 12 // K♥
POKE ptr[4] , 25 // K♦
POKE ptr[5] , 38 // K♠
render_waste_pile($0400, 8*40+15, ptr, 3)
FEND
// Test all pile renderers together - Klondike layout
FUNC render_all_piles_test
WORD ptr @ $f8
// Setup stock with some cards
POINTER ptr -> pile_stock
POKE ptr[0] , 10
// Setup waste with 3 cards
POINTER ptr -> pile_waste
POKE ptr[0] , 3
POKE ptr[1] , 5 // 6♥
POKE ptr[2] , 18 // 6♦
POKE ptr[3] , 31 // 6♠
// Setup foundations
POINTER ptr -> pile_found0
POKE ptr[0] , 0 // empty
POINTER ptr -> pile_found1
POKE ptr[0] , 1
POKE ptr[1] , 13 // A♦
POINTER ptr -> pile_found2
POKE ptr[0] , 2
POKE ptr[1] , 26 // A♠
POKE ptr[2] , 27 // 2♠
POINTER ptr -> pile_found3
POKE ptr[0] , 3
POKE ptr[1] , 39 // A♣
POKE ptr[2] , 40 // 2♣
POKE ptr[3] , 41 // 3♣
// Setup tableaus using midgame piles
setup_midgame_piles()
// Row 0: Stock, Waste, gap, 4 Foundations
// Stock at col 0
POINTER ptr -> pile_stock
render_stock_pile($0400, 0, ptr)
// Waste at col 6 (fanned takes 9 cols)
POINTER ptr -> pile_waste
render_waste_pile($0400, 6, ptr, 3)
// Foundations at cols 16, 21, 26, 31
POINTER ptr -> pile_found0
render_foundation_pile($0400, 16, ptr)
POINTER ptr -> pile_found1
render_foundation_pile($0400, 21, ptr)
POINTER ptr -> pile_found2
render_foundation_pile($0400, 26, ptr)
POINTER ptr -> pile_found3
render_foundation_pile($0400, 31, ptr)
// Row 8: 7 Tableaus
POINTER ptr -> pile_tab0
render_tableau_pile($0400, 8*40, ptr)
POINTER ptr -> pile_tab1
render_tableau_pile($0400, 8*40+5, ptr)
POINTER ptr -> pile_tab2
render_tableau_pile($0400, 8*40+10, ptr)
POINTER ptr -> pile_tab3
render_tableau_pile($0400, 8*40+15, ptr)
POINTER ptr -> pile_tab4
render_tableau_pile($0400, 8*40+20, ptr)
POINTER ptr -> pile_tab5
render_tableau_pile($0400, 8*40+25, ptr)
POINTER ptr -> pile_tab6
render_tableau_pile($0400, 8*40+30, ptr)
FEND
// ============================================================================
// Move Function Tests
// ============================================================================
// Clear all piles to known empty state
FUNC clear_all_piles
WORD ptr @ $f8
POINTER ptr -> pile_stock
POKE ptr[0] , 0
POINTER ptr -> pile_waste
POKE ptr[0] , 0
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
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
FEND
// Helper: Render stock and waste side by side for move tests
FUNC render_stock_waste_test
WORD ptr @ $f8
POINTER ptr -> pile_stock
render_stock_pile($0400, 0, ptr)
POINTER ptr -> pile_waste
render_waste_pile($0400, 6, ptr, 3)
FEND
// Test move_stock_to_waste with draw-1
FUNC test_stock_to_waste_draw1
WORD ptr @ $f8
BYTE success
clear_all_piles()
// Setup stock with 5 cards (face-down)
POINTER ptr -> pile_stock
POKE ptr[0] , 5
POKE ptr[1] , 0|CARD_FACEDOWN // A♥
POKE ptr[2] , 13|CARD_FACEDOWN // A♦
POKE ptr[3] , 26|CARD_FACEDOWN // A♠
POKE ptr[4] , 39|CARD_FACEDOWN // A♣
POKE ptr[5] , 12|CARD_FACEDOWN // K♥
// Render BEFORE
render_stock_waste_test()
wait_key()
// Draw 1 card three times
move_stock_to_waste(1, success) // K♥ to waste
move_stock_to_waste(1, success) // A♣ to waste
move_stock_to_waste(1, success) // A♠ to waste
// Render AFTER: stock should have 2, waste should have 3
fill_mem($0400, $0400+999, 0)
render_stock_waste_test()
FEND
// Test move_stock_to_waste with draw-3
FUNC test_stock_to_waste_draw3
WORD ptr @ $f8
BYTE success
clear_all_piles()
// Setup stock with 7 cards (face-down)
POINTER ptr -> pile_stock
POKE ptr[0] , 7
POKE ptr[1] , 0|CARD_FACEDOWN // A♥
POKE ptr[2] , 1|CARD_FACEDOWN // 2♥
POKE ptr[3] , 2|CARD_FACEDOWN // 3♥
POKE ptr[4] , 3|CARD_FACEDOWN // 4♥
POKE ptr[5] , 4|CARD_FACEDOWN // 5♥
POKE ptr[6] , 5|CARD_FACEDOWN // 6♥
POKE ptr[7] , 6|CARD_FACEDOWN // 7♥
// Draw 3 cards
move_stock_to_waste(3, success)
// Render result: stock should have 4, waste should have 3
render_stock_waste_test()
FEND
// Test move_reset_stock
FUNC test_reset_stock
WORD ptr @ $f8
BYTE success
clear_all_piles()
// Setup waste with 4 cards (face-up)
POINTER ptr -> pile_waste
POKE ptr[0] , 4
POKE ptr[1] , 0 // A♥
POKE ptr[2] , 13 // A♦
POKE ptr[3] , 26 // A♠
POKE ptr[4] , 39 // A♣
// Reset stock
move_reset_stock(success)
// Render result: stock should have 4 (face-down), waste empty
render_stock_waste_test()
FEND
// Test move_waste_to_tab - valid move
FUNC test_waste_to_tab_valid
WORD ptr @ $f8
WORD tab_ptr @ $f6
BYTE success
clear_all_piles()
// Setup waste with 8♥ (red, rank 7)
POINTER ptr -> pile_waste
POKE ptr[0] , 1
POKE ptr[1] , 7 // 8♥
// Setup tab0 with 9♠ (black, rank 8) - valid target for red 8
POINTER tab_ptr -> pile_tab0
POKE tab_ptr[0] , 1
POKE tab_ptr[1] , 34 // 9♠
// Render BEFORE
render_tableau_pile($0400, 0, tab_ptr)
render_waste_pile($0400, 10, ptr, 3)
wait_key()
// Move waste to tab
move_waste_to_tab(tab_ptr, success)
// Render AFTER
fill_mem($0400, $0400+999, 0)
POINTER tab_ptr -> pile_tab0
render_tableau_pile($0400, 0, tab_ptr)
POINTER ptr -> pile_waste
render_waste_pile($0400, 10, ptr, 3)
FEND
// Test move_waste_to_tab - King to empty tableau
FUNC test_waste_to_tab_king_empty
WORD ptr @ $f8
WORD tab_ptr @ $fc
BYTE success
clear_all_piles()
// Setup waste with K♠ (black, rank 12)
POINTER ptr -> pile_waste
POKE ptr[0] , 1
POKE ptr[1] , 38 // K♠
// Tab0 is empty
POINTER tab_ptr -> pile_tab0
// Move King to empty tableau
move_waste_to_tab(tab_ptr, success)
// Render tableau - should show K♠
render_tableau_pile($0400, 0, tab_ptr)
FEND
// Test move_waste_to_tab - invalid move (same color)
FUNC test_waste_to_tab_invalid
WORD ptr @ $f8
WORD tab_ptr @ $fc
BYTE success
clear_all_piles()
// Setup waste with 8♥ (red)
POINTER ptr -> pile_waste
POKE ptr[0] , 1
POKE ptr[1] , 7 // 8♥
// Setup tab0 with 9♦ (red) - invalid target (same color)
POINTER tab_ptr -> pile_tab0
POKE tab_ptr[0] , 1
POKE tab_ptr[1] , 21 // 9♦
// Try move - should fail
move_waste_to_tab(tab_ptr, success)
// Render - waste should still have card, tableau unchanged
render_tableau_pile($0400, 0, tab_ptr)
POINTER ptr -> pile_waste
render_waste_pile($0400, 10, ptr, 3)
FEND
// Test move_waste_to_found - build Ace to foundation
FUNC test_waste_to_found_ace
WORD ptr @ $f8
WORD found_ptr @ $fc
BYTE success
clear_all_piles()
// Setup waste with A♥
POINTER ptr -> pile_waste
POKE ptr[0] , 1
POKE ptr[1] , 0 // A♥
// Foundation 0 is empty
POINTER found_ptr -> pile_found0
// Move Ace to foundation
move_waste_to_found(found_ptr, success)
// Render foundation - should show A♥
render_foundation_pile($0400, 0, found_ptr)
FEND
// Test move_waste_to_found - build sequence
FUNC test_waste_to_found_sequence
WORD ptr @ $f8
WORD found_ptr @ $fc
BYTE success
clear_all_piles()
// Setup foundation with A♠, 2♠
POINTER found_ptr -> pile_found2
POKE found_ptr[0] , 2
POKE found_ptr[1] , 26 // A♠
POKE found_ptr[2] , 27 // 2♠
// Setup waste with 3♠
POINTER ptr -> pile_waste
POKE ptr[0] , 1
POKE ptr[1] , 28 // 3♠
// Move 3♠ to foundation
move_waste_to_found(found_ptr, success)
// Render foundation - should show 3♠
render_foundation_pile($0400, 0, found_ptr)
FEND
// Test move_tab_to_found
FUNC test_tab_to_found
WORD tab_ptr @ $f6
WORD found_ptr @ $f8
BYTE success
clear_all_piles()
// Setup tab0 with face-down card + A♦
POINTER tab_ptr -> pile_tab0
POKE tab_ptr[0] , 2
POKE tab_ptr[1] , 5|CARD_FACEDOWN // 6♥ face-down
POKE tab_ptr[2] , 13 // A♦ face-up
// Foundation 1 empty
POINTER found_ptr -> pile_found1
// Render BEFORE
render_foundation_pile($0400, 0, found_ptr)
render_tableau_pile($0400, 10, tab_ptr)
wait_key()
// Move A♦ to foundation
move_tab_to_found(tab_ptr, found_ptr, success)
// Render AFTER - foundation should have A♦, tableau should show flipped 6♥
fill_mem($0400, $0400+999, 0)
POINTER found_ptr -> pile_found1
POINTER tab_ptr -> pile_tab0
render_foundation_pile($0400, 0, found_ptr)
render_tableau_pile($0400, 10, tab_ptr)
FEND
// Test move_tab_to_tab - move single card
FUNC test_tab_to_tab_single
WORD src_ptr @ $f6
WORD dst_ptr @ $f8
BYTE success
clear_all_piles()
// Setup src with 8♦ (red)
POINTER src_ptr -> pile_tab0
POKE src_ptr[0] , 1
POKE src_ptr[1] , 20 // 8♦
// Setup dst with 9♠ (black)
POINTER dst_ptr -> pile_tab1
POKE dst_ptr[0] , 1
POKE dst_ptr[1] , 34 // 9♠
// Move 1 card from tab0 to tab1
move_tab_to_tab(src_ptr, dst_ptr, 1, success)
// Render both tableaus
render_tableau_pile($0400, 0, src_ptr)
render_tableau_pile($0400, 10, dst_ptr)
FEND
// Test move_tab_to_tab - move stack of 3 cards
FUNC test_tab_to_tab_stack
WORD src_ptr @ $f6
WORD dst_ptr @ $f8
BYTE success
clear_all_piles()
// Setup src with: face-down, Q♠(black), J♦(red), 10♣(black)
POINTER src_ptr -> pile_tab0
POKE src_ptr[0] , 4
POKE src_ptr[1] , 0|CARD_FACEDOWN // hidden card
POKE src_ptr[2] , 37 // Q♠ (black)
POKE src_ptr[3] , 23 // J♦ (red)
POKE src_ptr[4] , 48 // 10♣ (black)
// Setup dst with K♥ (red) - valid for Q♠
POINTER dst_ptr -> pile_tab1
POKE dst_ptr[0] , 1
POKE dst_ptr[1] , 12 // K♥
// Render BEFORE state
render_tableau_pile($0400, 0, src_ptr)
render_tableau_pile($0400, 10, dst_ptr)
// Wait for keypress
wait_key()
// Move 3 cards (Q♠, J♦, 10♣) from tab0 to tab1
move_tab_to_tab(src_ptr, dst_ptr, 3, success)
// Clear screen before redraw
fill_mem($0400, $0400+999, 0)
// Render AFTER state - src should show flipped card, dst should have 4 cards
render_tableau_pile($0400, 0, src_ptr)
render_tableau_pile($0400, 10, dst_ptr)
FEND
// Test move_tab_to_tab - King to empty tableau
FUNC test_tab_to_tab_king_empty
WORD src_ptr @ $f6
WORD dst_ptr @ $f8
BYTE success
clear_all_piles()
// Setup src with K♣ (black)
POINTER src_ptr -> pile_tab0
POKE src_ptr[0] , 1
POKE src_ptr[1] , 51 // K♣
// dst is empty
POINTER dst_ptr -> pile_tab1
// Move King to empty tableau
move_tab_to_tab(src_ptr, dst_ptr, 1, success)
// Render
render_tableau_pile($0400, 0, src_ptr)
render_tableau_pile($0400, 10, dst_ptr)
FEND
// Test move_found_to_tab - undo move from foundation
FUNC test_found_to_tab
WORD found_ptr @ $f6
WORD tab_ptr @ $f8
BYTE success
clear_all_piles()
// Setup foundation with A♥, 2♥, 3♥
POINTER found_ptr -> pile_found0
POKE found_ptr[0] , 3
POKE found_ptr[1] , 0 // A♥
POKE found_ptr[2] , 1 // 2♥
POKE found_ptr[3] , 2 // 3♥
// Setup tableau with 4♠ (black) - valid for 3♥ (red)
POINTER tab_ptr -> pile_tab0
POKE tab_ptr[0] , 1
POKE tab_ptr[1] , 29 // 4♠
// Move 3♥ from foundation to tableau
move_found_to_tab(found_ptr, tab_ptr, success)
// Render
render_foundation_pile($0400, 0, found_ptr)
render_tableau_pile($0400, 10, tab_ptr)
FEND
// Run all move tests sequentially (for visual inspection)
// Each test clears screen area and renders result
FUNC run_all_move_tests
BYTE dummy
// Test 1: Stock to Waste draw-1
test_stock_to_waste_draw1()
// Wait for keypress or delay here if needed
// To run individual tests, call them directly:
// test_stock_to_waste_draw3()
// test_reset_stock()
// test_waste_to_tab_valid()
// test_waste_to_tab_king_empty()
// test_waste_to_tab_invalid()
// test_waste_to_found_ace()
// test_waste_to_found_sequence()
// test_tab_to_found()
// test_tab_to_tab_single()
// test_tab_to_tab_stack()
// test_tab_to_tab_king_empty()
// test_found_to_tab()
FEND
LABEL __skip_lib_cardtests
#IFEND

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,163 @@
; CharSet Data...
; 256 images, 8 bytes per image, total size is 2048 ($800) bytes.
* = addr_charset_data
charset_data
.byte $ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$c7,$bb,$fb,$c7,$bf,$bf,$c3,$ff
.byte $c7,$bb,$fb,$c7,$fb,$bb,$c7,$ff,$bb,$bb,$bb,$c3,$fb,$fb,$fb,$ff
.byte $83,$bf,$bf,$c7,$fb,$bb,$c7,$ff,$c7,$bb,$bf,$87,$bb,$bb,$c7,$ff
.byte $87,$fb,$fb,$f7,$ef,$df,$df,$ff,$c7,$bb,$bb,$c7,$bb,$bb,$c7,$ff
.byte $c7,$bb,$bb,$c3,$fb,$bb,$c7,$ff,$db,$95,$d5,$d5,$d5,$d5,$db,$ff
.byte $f3,$fb,$fb,$fb,$fb,$bb,$c7,$ff,$c7,$bb,$bb,$bb,$bb,$b3,$c3,$fd
.byte $bb,$b7,$af,$9f,$af,$b7,$bb,$ff,$c7,$bb,$bb,$bb,$83,$bb,$bb,$ff
.byte $ef,$c7,$83,$83,$83,$ef,$c7,$ff,$e7,$c3,$a5,$00,$00,$a5,$e7,$c3
.byte $ff,$93,$01,$01,$83,$c7,$ef,$ff,$ef,$c7,$83,$01,$83,$c7,$ef,$ff
.byte $a2,$55,$2a,$14,$2a,$55,$a2,$41,$00,$00,$00,$00,$00,$00,$00,$00
.byte $fb,$fb,$fb,$fb,$fb,$fa,$f9,$fb,$ff,$ff,$c3,$81,$00,$00,$00,$00
.byte $df,$df,$df,$df,$df,$5f,$9f,$df,$fb,$fa,$f9,$fa,$f9,$fa,$f9,$fb
.byte $ff,$ff,$ff,$f7,$e3,$c1,$80,$00,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$7f
.byte $ff,$ff,$ff,$00,$ff,$00,$ff,$ff,$ff,$ff,$ff,$ff,$e7,$c3,$81,$00
.byte $ff,$00,$ff,$00,$ff,$00,$ff,$ff,$ff,$ff,$ff,$ff,$fe,$fc,$f8,$f0
.byte $ff,$ff,$ff,$ff,$38,$10,$00,$00,$ff,$ff,$ff,$ff,$ff,$7f,$3f,$1f
.byte $ff,$ff,$ff,$ff,$ff,$fe,$fd,$fb,$ff,$ff,$ff,$ff,$ff,$00,$ff,$ff
.byte $ff,$ff,$ff,$ff,$ff,$7f,$bf,$df,$ff,$ff,$00,$ff,$ff,$ff,$ff,$ff
.byte $f1,$e0,$c0,$c0,$c0,$c0,$e0,$f1,$00,$81,$42,$00,$00,$00,$c3,$e7
.byte $8f,$07,$03,$03,$03,$03,$07,$8f,$fe,$fc,$f8,$f8,$f0,$f0,$f0,$f0
.byte $00,$00,$00,$00,$00,$00,$00,$00,$3f,$1f,$0f,$0f,$07,$07,$07,$0f
.byte $fe,$fc,$f8,$f0,$f0,$f8,$fc,$fe,$00,$00,$00,$00,$00,$00,$00,$00
.byte $7f,$3f,$1f,$0f,$0f,$1f,$3f,$7f,$f0,$f0,$f0,$f8,$fc,$fe,$ff,$ff
.byte $00,$00,$00,$00,$00,$00,$01,$83,$1f,$1f,$1f,$3f,$7f,$ff,$ff,$ff
.byte $fb,$fb,$fb,$fb,$fb,$fb,$fb,$fb,$fb,$fd,$fe,$ff,$ff,$ff,$ff,$ff
.byte $df,$bf,$7f,$ff,$ff,$ff,$ff,$ff,$df,$df,$df,$df,$df,$df,$df,$df
.byte $df,$5f,$9f,$5f,$9f,$5f,$9f,$df,$e7,$e7,$e7,$e7,$c3,$81,$ff,$ff
.byte $ff,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$f8,$fc,$ff,$ff,$ff,$ff,$ff,$ff
.byte $14,$36,$f7,$f7,$e3,$c1,$ff,$ff,$0f,$1f,$ff,$ff,$ff,$ff,$ff,$ff
.byte $ff,$ff,$ff,$fe,$fd,$fa,$f9,$fb,$00,$81,$c3,$e7,$ff,$ff,$ff,$ff
.byte $ff,$ff,$ff,$7f,$bf,$5f,$9f,$df,$ff,$fe,$fd,$fa,$f9,$fa,$f9,$fb
.byte $c7,$ef,$ff,$ff,$ff,$ff,$ff,$ff,$ff,$7f,$bf,$5f,$9f,$5f,$9f,$df
.byte $00,$00,$00,$00,$00,$00,$00,$00,$c7,$bb,$fb,$c7,$fb,$bb,$c7,$ff
.byte $c7,$bb,$fb,$c7,$fb,$bb,$c7,$ff,$c7,$bb,$fb,$c7,$fb,$bb,$c7,$ff
.byte $c7,$bb,$fb,$c7,$fb,$bb,$c7,$ff,$c7,$bb,$fb,$c7,$fb,$bb,$c7,$ff
.byte $c7,$bb,$fb,$c7,$fb,$bb,$c7,$ff,$c7,$bb,$fb,$c7,$fb,$bb,$c7,$ff
.byte $c7,$bb,$fb,$c7,$fb,$bb,$c7,$ff,$c7,$bb,$bb,$c7,$fb,$bb,$c7,$ff
.byte $db,$95,$d5,$d5,$d5,$d5,$db,$ff,$f3,$fb,$fb,$fb,$fb,$bb,$c7,$ff
.byte $c7,$bb,$bb,$bb,$bb,$b3,$c3,$fd,$bb,$b7,$af,$9f,$af,$b7,$bb,$ff
.byte $00,$05,$0a,$14,$2a,$55,$22,$41,$00,$50,$28,$14,$2a,$54,$a2,$40
.byte $22,$55,$2a,$14,$0a,$05,$02,$00,$a2,$54,$2a,$14,$28,$50,$a0,$00
.byte $a2,$55,$2a,$14,$2a,$55,$a2,$41,$00,$00,$00,$00,$00,$00,$00,$00
.byte $ff,$ff,$ff,$ff,$ff,$fe,$fd,$fb,$00,$00,$00,$00,$00,$00,$00,$00
.byte $ff,$ff,$ff,$ff,$ff,$7f,$bf,$df,$ff,$fe,$fd,$fa,$f9,$fa,$f9,$fb
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
.byte $ff,$ff,$ff,$ff,$ff,$00,$ff,$ff,$00,$00,$00,$00,$00,$00,$00,$00
.byte $ff,$ff,$ff,$ff,$ff,$00,$ff,$ff,$fe,$fd,$fb,$f7,$f7,$f7,$fb,$fb
.byte $38,$d7,$ef,$ff,$ff,$ff,$ff,$ff,$ff,$7f,$bf,$df,$df,$df,$bf,$bf
.byte $ff,$ff,$ff,$ff,$ff,$fe,$fd,$fb,$ff,$ff,$ff,$ff,$ff,$00,$ff,$ff
.byte $ff,$ff,$ff,$ff,$ff,$7f,$bf,$df,$ff,$ff,$00,$ff,$ff,$ff,$ff,$ff
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
.byte $f1,$e0,$c0,$c0,$c0,$c0,$e0,$f1,$00,$00,$00,$00,$00,$00,$00,$00
.byte $00,$00,$00,$00,$00,$00,$00,$00,$fe,$fc,$f8,$f8,$f0,$f0,$f0,$f8
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
.byte $fb,$fb,$fb,$fb,$fb,$fb,$fb,$fb,$fb,$fd,$fe,$ff,$ff,$ff,$ff,$ff
.byte $df,$bf,$7f,$ff,$ff,$ff,$ff,$ff,$df,$df,$df,$df,$df,$df,$df,$df
.byte $ff,$7f,$bf,$5f,$9f,$5f,$9f,$df,$00,$00,$00,$00,$00,$00,$00,$00
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
.byte $00,$00,$00,$00,$00,$00,$00,$00,$f8,$fc,$ff,$ff,$ff,$ff,$ff,$ff
.byte $ff,$ff,$ff,$ff,$ff,$fe,$fd,$fb,$00,$00,$00,$00,$00,$00,$00,$00
.byte $ff,$ff,$ff,$ff,$ff,$7f,$bf,$df,$ff,$ff,$ff,$fe,$fd,$fa,$f9,$fb
.byte $00,$00,$00,$00,$00,$00,$00,$00,$ff,$ff,$ff,$7f,$bf,$5f,$9f,$df
.byte $00,$00,$00,$00,$00,$00,$00,$00,$c7,$bb,$fb,$c7,$fb,$bb,$c7,$ff
.byte $c7,$bb,$fb,$c7,$fb,$bb,$c7,$ff,$c7,$bb,$fb,$c7,$fb,$bb,$c7,$ff
.byte $c7,$bb,$fb,$c7,$fb,$bb,$c7,$ff,$c7,$bb,$fb,$c7,$fb,$bb,$c7,$ff
.byte $c7,$bb,$fb,$c7,$fb,$bb,$c7,$ff,$c7,$bb,$fb,$c7,$fb,$bb,$c7,$ff
.byte $c7,$bb,$fb,$c7,$fb,$bb,$c7,$ff,$c7,$bb,$bb,$c7,$fb,$bb,$c7,$ff
.byte $db,$95,$d5,$d5,$d5,$d5,$db,$ff,$f3,$fb,$fb,$fb,$fb,$bb,$c7,$ff
.byte $c7,$bb,$bb,$bb,$bb,$b3,$c3,$fd,$bb,$b7,$af,$9f,$af,$b7,$bb,$ff
.byte $00,$05,$0a,$14,$2a,$55,$22,$41,$00,$50,$28,$14,$2a,$54,$a2,$40
.byte $22,$55,$2a,$14,$0a,$05,$02,$00,$a2,$54,$2a,$14,$28,$50,$a0,$00
.byte $a2,$55,$2a,$14,$2a,$55,$a2,$41,$00,$00,$00,$00,$00,$00,$00,$00
.byte $ff,$ff,$ff,$ff,$ff,$fe,$fd,$fb,$00,$00,$00,$00,$00,$00,$00,$00
.byte $ff,$ff,$ff,$ff,$ff,$7f,$bf,$df,$ff,$fe,$fd,$fa,$f9,$fa,$f9,$fb
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
.byte $ff,$ff,$ff,$ff,$ff,$00,$ff,$ff,$00,$00,$00,$00,$00,$00,$00,$00
.byte $ff,$ff,$ff,$ff,$ff,$00,$ff,$ff,$fe,$fd,$fb,$f7,$f7,$f7,$fb,$fb
.byte $38,$d7,$ef,$ff,$ff,$ff,$ff,$ff,$ff,$7f,$bf,$df,$df,$df,$bf,$bf
.byte $ff,$ff,$ff,$ff,$ff,$fe,$fd,$fb,$ff,$ff,$ff,$ff,$ff,$00,$ff,$ff
.byte $ff,$ff,$ff,$ff,$ff,$7f,$bf,$df,$ff,$ff,$00,$ff,$ff,$ff,$ff,$ff
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
.byte $f1,$e0,$c0,$c0,$c0,$c0,$e0,$f1,$00,$00,$00,$00,$00,$00,$00,$00
.byte $00,$00,$00,$00,$00,$00,$00,$00,$fe,$fc,$f8,$f8,$f0,$f0,$f0,$f8
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
.byte $fb,$fb,$fb,$fb,$fb,$fb,$fb,$fb,$fb,$fd,$fe,$ff,$ff,$ff,$ff,$ff
.byte $df,$bf,$7f,$ff,$ff,$ff,$ff,$ff,$df,$df,$df,$df,$df,$df,$df,$df
.byte $ff,$7f,$bf,$5f,$9f,$5f,$9f,$df,$00,$00,$00,$00,$00,$00,$00,$00
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
.byte $00,$00,$00,$00,$00,$00,$00,$00,$f8,$fc,$ff,$ff,$ff,$ff,$ff,$ff
.byte $ff,$ff,$ff,$ff,$ff,$fe,$fd,$fb,$00,$00,$00,$00,$00,$00,$00,$00
.byte $ff,$ff,$ff,$ff,$ff,$7f,$bf,$df,$ff,$ff,$ff,$fe,$fd,$fa,$f9,$fb
.byte $00,$00,$00,$00,$00,$00,$00,$00,$ff,$ff,$ff,$7f,$bf,$5f,$9f,$df
.byte $00,$00,$00,$00,$00,$00,$00,$00,$c7,$bb,$fb,$c7,$fb,$bb,$c7,$ff
.byte $c7,$bb,$fb,$c7,$fb,$bb,$c7,$ff,$c7,$bb,$fb,$c7,$fb,$bb,$c7,$ff
.byte $c7,$bb,$fb,$c7,$fb,$bb,$c7,$ff,$c7,$bb,$fb,$c7,$fb,$bb,$c7,$ff
.byte $c7,$bb,$fb,$c7,$fb,$bb,$c7,$ff,$c7,$bb,$fb,$c7,$fb,$bb,$c7,$ff
.byte $c7,$bb,$fb,$c7,$fb,$bb,$c7,$ff,$c7,$bb,$bb,$c7,$fb,$bb,$c7,$ff
.byte $db,$95,$d5,$d5,$d5,$d5,$db,$ff,$f3,$fb,$fb,$fb,$fb,$bb,$c7,$ff
.byte $c7,$bb,$bb,$bb,$bb,$b3,$c3,$fd,$bb,$b7,$af,$9f,$af,$b7,$bb,$ff
.byte $00,$05,$0a,$14,$2a,$55,$22,$41,$00,$50,$28,$14,$2a,$54,$a2,$40
.byte $22,$55,$2a,$14,$0a,$05,$02,$00,$a2,$54,$2a,$14,$28,$50,$a0,$00
.byte $a2,$55,$2a,$14,$2a,$55,$a2,$41,$00,$00,$00,$00,$00,$00,$00,$00
.byte $ff,$ff,$ff,$ff,$ff,$fe,$fd,$fb,$00,$00,$00,$00,$00,$00,$00,$00
.byte $ff,$ff,$ff,$ff,$ff,$7f,$bf,$df,$ff,$fe,$fd,$fa,$f9,$fa,$f9,$fb
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
.byte $ff,$ff,$ff,$ff,$ff,$00,$ff,$ff,$00,$00,$00,$00,$00,$00,$00,$00
.byte $ff,$ff,$ff,$ff,$ff,$00,$ff,$ff,$fe,$fd,$fb,$f7,$f7,$f7,$fb,$fb
.byte $38,$d7,$ef,$ff,$ff,$ff,$ff,$ff,$ff,$7f,$bf,$df,$df,$df,$bf,$bf
.byte $ff,$ff,$ff,$ff,$ff,$fe,$fd,$fb,$ff,$ff,$ff,$ff,$ff,$00,$ff,$ff
.byte $ff,$ff,$ff,$ff,$ff,$7f,$bf,$df,$ff,$ff,$00,$ff,$ff,$ff,$ff,$ff
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
.byte $f1,$e0,$c0,$c0,$c0,$c0,$e0,$f1,$00,$00,$00,$00,$00,$00,$00,$00
.byte $00,$00,$00,$00,$00,$00,$00,$00,$fe,$fc,$f8,$f8,$f0,$f0,$f0,$f8
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
.byte $fb,$fb,$fb,$fb,$fb,$fb,$fb,$fb,$fb,$fd,$fe,$ff,$ff,$ff,$ff,$ff
.byte $df,$bf,$7f,$ff,$ff,$ff,$ff,$ff,$df,$df,$df,$df,$df,$df,$df,$df
.byte $ff,$7f,$bf,$5f,$9f,$5f,$9f,$df,$00,$00,$00,$00,$00,$00,$00,$00
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
.byte $00,$00,$00,$00,$00,$00,$00,$00,$f8,$fc,$ff,$ff,$ff,$ff,$ff,$ff
.byte $ff,$ff,$ff,$ff,$ff,$fe,$fd,$fb,$00,$00,$00,$00,$00,$00,$00,$00
.byte $ff,$ff,$ff,$ff,$ff,$7f,$bf,$df,$ff,$ff,$ff,$fe,$fd,$fa,$f9,$fb
.byte $00,$00,$00,$00,$00,$00,$00,$00,$ff,$ff,$ff,$7f,$bf,$5f,$9f,$df
; CharSet Attribute (L1) Data...
; 256 attributes, 1 attribute per image, 8 bits per attribute, total size is 256 ($100) bytes.
; nb. Upper nybbles = material, lower nybbles = colour (colour matrix low).
* = addr_charset_attrib_L1_data
charset_attrib_L1_data
.byte $01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01
.byte $02,$02,$02,$02,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01
.byte $01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01
.byte $01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01
.byte $0a,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$02,$02
.byte $02,$02,$02,$02,$01,$02,$01,$01,$02,$02,$01,$02,$01,$01,$01,$01
.byte $02,$02,$02,$02,$02,$02,$01,$02,$02,$01,$02,$02,$02,$02,$02,$02
.byte $02,$02,$02,$02,$01,$02,$02,$02,$02,$01,$01,$02,$01,$01,$02,$01
.byte $01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$02,$02
.byte $01,$01,$02,$02,$01,$02,$01,$01,$02,$02,$01,$01,$01,$01,$01,$01
.byte $02,$02,$02,$02,$02,$02,$01,$02,$02,$01,$01,$01,$01,$01,$01,$01
.byte $02,$02,$02,$02,$01,$02,$02,$02,$02,$01,$01,$01,$01,$01,$01,$01
.byte $01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$02,$02
.byte $02,$02,$02,$02,$01,$02,$01,$01,$02,$02,$01,$02,$01,$01,$01,$01
.byte $01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01,$01
.byte $01,$01,$01,$01,$01,$02,$02,$02,$02,$01,$01,$02,$01,$01,$02,$01

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 3 KiB

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1 @@
xxd -b -c1 charpad_cards7.bin | cut -d' ' -f2 | tr '01' '.#'

View file

@ -0,0 +1,113 @@
; Generated by SpritePad C64 - Subchrist Software, 2003-2025.
; Assemble with 64TASS or similar.
; Colour values...
colr_vic_bg0 = 1
colr_vic_sprite_mc1 = 0
colr_vic_sprite_mc2 = 1
; Quantities and dimensions...
sprite_count = 10
; Data block addresses (dummy values) and sizes...
addr_spriteset_data = $1000
size_spriteset_data = $280 ; (640 bytes)
addr_spriteset_attrib_data = $1000
size_spriteset_attrib_data = $a ; (10 bytes)
; * INSERT EXAMPLE PROGRAM HERE! * (or just include this file in your project).
; SpriteSet Data...
; 10 images, 64 bytes per image, total size is 640 ($280) bytes.
* = addr_spriteset_data
spriteset_data
sprite_image_0
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$01
.byte $c7,$00,$03,$ef,$80,$07,$ff,$c0,$07,$ff,$c0,$07,$ff,$c0,$03,$ff
.byte $80,$01,$ff,$00,$00,$fe,$00,$00,$7c,$00,$00,$38,$00,$00,$10,$00
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$12
sprite_image_1
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
.byte $00,$00,$00,$41,$00,$00,$20,$80,$00,$00,$80,$00,$00,$00,$00,$00
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$0a
sprite_image_2
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$10,$00,$00
.byte $38,$00,$00,$7c,$00,$00,$fe,$00,$01,$ff,$00,$03,$ff,$80,$03,$ff
.byte $80,$01,$ff,$00,$00,$fe,$00,$00,$7c,$00,$00,$38,$00,$00,$10,$00
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$12
sprite_image_3
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
.byte $00,$00,$00,$08,$00,$00,$04,$00,$00,$02,$00,$00,$01,$00,$00,$00
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$0a
sprite_image_4
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$38,$00,$00
.byte $7c,$00,$00,$7c,$00,$00,$7c,$00,$03,$bb,$80,$07,$d7,$c0,$07,$ff
.byte $c0,$07,$d7,$c0,$03,$93,$80,$00,$10,$00,$00,$38,$00,$00,$7c,$00
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$10
sprite_image_5
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$38,$00,$00
.byte $0c,$00,$00,$04,$00,$00,$04,$00,$01,$83,$80,$00,$c0,$c0,$00,$00
.byte $40,$00,$00,$40,$00,$00,$00,$00,$00,$00,$00,$08,$00,$00,$04,$00
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$0b
sprite_image_6
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$10,$00,$00
.byte $38,$00,$00,$7c,$00,$00,$fe,$00,$01,$ff,$00,$03,$ff,$80,$03,$ff
.byte $80,$03,$ff,$80,$01,$d7,$00,$00,$10,$00,$00,$38,$00,$00,$7c,$00
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$10
sprite_image_7
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$10,$00,$00
.byte $18,$00,$00,$0c,$00,$00,$06,$00,$00,$03,$00,$00,$01,$80,$00,$00
.byte $80,$00,$00,$80,$00,$00,$00,$00,$00,$00,$00,$08,$00,$00,$04,$00
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$0b
sprite_image_8
.byte $77,$47,$77,$44,$44,$42,$76,$46,$42,$14,$44,$42,$77,$77,$72,$00
.byte $00,$00,$00,$00,$00,$77,$76,$00,$45,$55,$00,$47,$75,$00,$45,$65
.byte $00,$75,$56,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
sprite_image_9
.byte $76,$00,$00,$45,$00,$00,$65,$00,$00,$45,$00,$00,$76,$00,$00,$00
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
.byte $00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00,$00
; SpriteSet Attribute Data...
; 10 attributes, 1 per image, 8 bits each, total size is 10 ($a) bytes.
; nb. Upper nybbles = MYXV, lower nybbles = colour (0-15).
* = addr_spriteset_attrib_data
spriteset_attrib_data
.byte $12,$0a,$12,$0a,$10,$0b,$10,$0b,$00,$00

Binary file not shown.

View file

@ -0,0 +1,143 @@
; 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 = 15
; Data block addresses (dummy values) and sizes...
addr_spriteset_data = $1000
size_spriteset_data = $3c0 ; (960 bytes)
addr_spriteset_attrib_data = $1000
size_spriteset_attrib_data = $f ; (15 bytes)
; * INSERT EXAMPLE PROGRAM HERE! * (or just include this file in your project).
; SpriteSet Data...
; 15 images, 64 bytes per image, total size is 960 ($3c0) 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
; SpriteSet Attribute Data...
; 15 attributes, 1 per image, 8 bits each, total size is 15 ($f) 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1 KiB

Binary file not shown.

Binary file not shown.

4
claude_code_docker.sh Normal file
View file

@ -0,0 +1,4 @@
#!/bin/bash
export DOCKER_UID=$(id -u)
export DOCKER_GID=$(id -g)
docker compose run --rm claude-solitaire

21
cm.sh Executable file
View file

@ -0,0 +1,21 @@
#!/bin/sh
# Define filename as variable
PROGNAME="cardgame"
# Only set C65LIBPATH if not already defined
if [ -z "$C65LIBPATH" ]; then
export C65LIBPATH=$(readlink -f "../../lib")
fi
# Compile
c65gm -in ${PROGNAME}.c65 -out ${PROGNAME}.s
#c65cm in:${PROGNAME}.c65 out:${PROGNAME}.s hidelicense
echo assemble.
acme ${PROGNAME}.s
# Only remove if exists
if [ -f ${PROGNAME}.prg ]; then
rm ${PROGNAME}.prg
fi
#mv main.bin ${PROGNAME}.prg
mv main.bin main.prg

16
docker-compose.yml Normal file
View file

@ -0,0 +1,16 @@
services:
claude-solitaire:
build: .
user: "${DOCKER_UID}:${DOCKER_GID}"
volumes:
- .:/app
environment:
#- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
- CLAUDE_CONFIG_DIR=/app/.claude
- HOME=/app
- DISABLE_AUTOUPDATER=1
- CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
- DISABLE_TELEMETRY=1
- DISABLE_ERROR_REPORTING=1
stdin_open: true
tty: true

1
exomizer_compress_prg.sh Normal file
View file

@ -0,0 +1 @@
exomizer sfx basic main.prg -o main2.prg

967
gameloop.c65 Normal file
View file

@ -0,0 +1,967 @@
#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 @ $fa
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 @ $fa
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 @ $fa
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 @ $fa
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
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
WORD stock_ptr @ $fa
POINTER stock_ptr -> pile_stock
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 @ $fa
WORD src_end_ptr
WORD dst_ptr @ $fc
// 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

72
joystick.c65 Normal file
View file

@ -0,0 +1,72 @@
#IFNDEF __lib_joystick
#DEFINE __lib_joystick 1
GOTO __skip_lib_joystick
// ============================================================================
// JOYSTICK INPUT DRIVER FOR COMMODORE 64
// ============================================================================
// This library provides joystick input reading for the Commodore 64.
//
// Joystick reading uses CIA port 2 ($DC00) which is preferred for games
// Port 1 ($DC01) conflicts with keyboard matrix scanning
//
// Usage:
// #INCLUDE "joystick.c65"
// WHILE 1
// joy_read()
// BYTE test_bit
// test_bit = joy_state & JOY_UP_MASK
// IF test_bit
// // handle up
// ENDIF
// WEND
// ============================================================================
// CIA Port addresses
WORD CONST JOY_PORT1 = $DC01 // Port 1 (conflicts with keyboard)
WORD CONST JOY_PORT2 = $DC00 // Port 2 (recommended for games)
// Joystick bit masks (active low - 0 = pressed)
BYTE CONST JOY_UP_MASK = %00000001 // Bit 0
BYTE CONST JOY_DOWN_MASK = %00000010 // Bit 1
BYTE CONST JOY_LEFT_MASK = %00000100 // Bit 2
BYTE CONST JOY_RIGHT_MASK = %00001000 // Bit 3
BYTE CONST JOY_FIRE_MASK = %00010000 // Bit 4
// Joystick state variable
BYTE joy_state // Inverted joystick state (1=pressed, 0=not pressed)
// ============================================================================
// FUNC joy_read_port1
// Read joystick port 1 (alternative port, may conflict with keyboard)
// Updates the same joy_state variable as joy_read()
// ============================================================================
FUNC joy_read_port1
BYTE joy_raw
joy_raw = PEEK JOY_PORT1
joy_state = joy_raw ^ $FF // Invert bits: 1=pressed, 0=not pressed
FEND
// ============================================================================
// FUNC joy_read
// Read joystick port 2 and update state variable
// Call this every frame to update joystick state
// Inverts active-low CIA bits to active-high (1=pressed, 0=not pressed)
// ============================================================================
FUNC joy_read_port2
BYTE joy_raw
joy_raw = PEEK JOY_PORT2
joy_state = joy_raw ^ $FF // Invert bits: 1=pressed, 0=not pressed
FEND
LABEL __skip_lib_joystick
#IFEND

272
joysticktests.c65 Normal file
View file

@ -0,0 +1,272 @@
#IFNDEF __lib_joysticktests
#DEFINE __lib_joysticktests 1
#INCLUDE "joystick.c65"
#INCLUDE "mouse.c65"
#INCLUDE "pointer.c65"
GOTO __skip_lib_joysticktests
// ============================================================================
// FUNC test_joystick_read
// Test basic joystick reading using card suit symbols
// Displays suits in 4 directions, lights up when joystick pressed
// Runs forever - reset to exit
// ============================================================================
//FUNC test_joystick_read
// WORD screen_ptr @ $fb
// BYTE raster @ $d012
//
// // Initialize joystick
// joy_init()
//
// // Draw suit symbols in cross pattern (center of screen)
// // Hearts (red) = UP, Diamonds (red) = DOWN, Spades (black) = LEFT, Clubs (black) = RIGHT
//
// // UP position - Hearts suit symbol
// POINTER screen_ptr -> 9*40+$0400+18
// POKE screen_ptr[0] , $50
//
// // DOWN position - Diamonds suit symbol
// POINTER screen_ptr -> 11*40+$0400+18
// POKE screen_ptr[0] , $51
//
// // LEFT position - Spades suit symbol
// POINTER screen_ptr -> 10*40+$0400+16
// POKE screen_ptr[0] , $0e
//
// // RIGHT position - Clubs suit symbol
// POINTER screen_ptr -> 10*40+$0400+20
// POKE screen_ptr[0] , $0f
//
// // CENTER - Fire indicator (Ace symbol)
// POINTER screen_ptr -> 10*40+$0400+18
// POKE screen_ptr[0] , 0
//
// // Main test loop - runs forever
// WHILE 1
// joy_read_port2()
//
// // Wait for raster
// WHILE raster != 250
// WEND
//
// // Update suit displays based on joystick
//
// // UP - Hearts (normal or highlighted)
// POINTER screen_ptr -> 9*40+$0400+18
// IF joy_up
// POKE screen_ptr[0] , $50+64 // Red/highlighted
// ELSE
// POKE screen_ptr[0] , $50 // Normal
// ENDIF
//
// // DOWN - Diamonds
// POINTER screen_ptr -> 11*40+$0400+18
// IF joy_down
// POKE screen_ptr[0] , $51+64 // Red/highlighted
// ELSE
// POKE screen_ptr[0] , $51 // Normal
// ENDIF
//
// // LEFT - Spades
// POINTER screen_ptr -> 10*40+$0400+16
// IF joy_left
// POKE screen_ptr[0] , $0e+64 // Red/highlighted
// ELSE
// POKE screen_ptr[0] , $0e // Normal
// ENDIF
//
// // RIGHT - Clubs
// POINTER screen_ptr -> 10*40+$0400+20
// IF joy_right
// POKE screen_ptr[0] , $0f+64 // Red/highlighted
// ELSE
// POKE screen_ptr[0] , $0f // Normal
// ENDIF
//
// // FIRE - Center Ace
// POINTER screen_ptr -> 10*40+$0400+18
// IF joy_fire
// POKE screen_ptr[0] , 13+64 // Ace highlighted
// ELSE
// POKE screen_ptr[0] , 0 // Blank
// ENDIF
// WEND
//FEND
// ============================================================================
// FUNC test_pointer_sprite
// Test sprite pointer movement with joystick
// Initializes sprite, copies sprite data, and moves pointer with joystick
// Run for ~200 frames then returns
// ============================================================================
FUNC test_pointer_sprite
// Copy sprite data to $2200 (sprite block 136)
// $2000-$21FF reserved for charset
WORD src_ptr @ $fa
WORD src_end_ptr
WORD dst_ptr @ $fc
WORD frame_count
BYTE raster @ $d012
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)
// Initialize pointer
pointer_init(160, 100, color_red, 136) // Sprite block 136 = $2200, red color
pointer_set_speed(2)
// Enable sprite
pointer_enable(1)
// Movement loop - runs forever
WHILE 1
// Wait for raster
WHILE raster != 250
WEND
DEC $d020
// Read joystick and update pointer
joy_read_port2()
pointer_update_joystick(joy_state)
INC $d020
WEND
FEND
// ============================================================================
// FUNC test_pointer_manual
// Test pointer with manual position setting
// Moves pointer in a square pattern without joystick
// ============================================================================
//FUNC test_pointer_manual
// WORD x
// BYTE y
// BYTE delay
// WORD x_down
// BYTE y_down
//
// // Copy sprite data to $2200 (sprite block 136)
// WORD src_ptr @ $fa
// WORD src_end_ptr
// WORD dst_ptr @ $fc
// 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)
//
// // Initialize pointer
// pointer_init(50, 50, color_cyan, 136)
// pointer_set_speed(3)
//
// // Move in square pattern
// // Right
// FOR x = 50 TO 200
// pointer_set_x(x)
// pointer_set_y(50)
// // Small delay
// FOR delay = 0 TO 10
// NEXT
// NEXT
//
// // Down
// FOR y = 50 TO 150
// pointer_set_x(200)
// pointer_set_y(y)
// FOR delay = 0 TO 10
// NEXT
// NEXT
//
// // Left
// x_down = 200
// WHILE x_down > 50
// pointer_set_x(x_down)
// pointer_set_y(150)
// x_down--
// FOR delay = 0 TO 10
// NEXT
// WEND
//
// // Up
// y_down = 150
// WHILE y_down > 50
// pointer_set_x(50)
// pointer_set_y(y_down)
// y_down--
// FOR delay = 0 TO 10
// NEXT
// WEND
//
// // Disable sprite
// pointer_enable(0)
//FEND
// ============================================================================
// FUNC test_pointer_mouse
// Test sprite pointer movement with 1351 mouse
// Initializes sprite, copies sprite data, and moves pointer with mouse
// Run forever - press RUN/STOP+RESTORE to exit
// ============================================================================
FUNC test_pointer_mouse
// Copy sprite data to $2200 (sprite block 136)
// $2000-$21FF reserved for charset
WORD src_ptr @ $fa
WORD src_end_ptr
WORD dst_ptr @ $fc
BYTE raster @ $d012
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)
// Initialize mouse and pointer
mouse_init()
pointer_init(160, 100, color_red, 136) // Sprite block 136 = $2200, white color
// Enable sprite
pointer_enable(1)
// Movement loop - runs forever
WHILE 1
// Wait for raster
WHILE raster != 250
WEND
DEC $d020
// Read mouse and update pointer
mouse_read()
pointer_update_mouse(mouse_delta_x, mouse_delta_y)
// Test buttons (active-low: 0=pressed)
BYTE left_btn
BYTE right_btn
// Left button = bit 4
left_btn = mouse_buttons & %00010000
IF left_btn == 0
INC $0400 // Pressed
ENDIF
// Right button = bit 0
right_btn = mouse_buttons & %00000001
IF right_btn == 0
INC $0401 // Pressed
ENDIF
INC $d020
WEND
FEND
LABEL __skip_lib_joysticktests
#IFEND

307
mouse.c65 Normal file
View file

@ -0,0 +1,307 @@
#IFNDEF __lib_mouse
#DEFINE __lib_mouse 1
GOTO __skip_lib_mouse
// ============================================================================
// COMMODORE 1351 MOUSE DRIVER FOR C64
// ============================================================================
// This library provides mouse input reading for the Commodore 1351 mouse.
//
// The 1351 uses the SID chip's POT lines to report relative movement.
// The SID hardware automatically samples at ~2kHz, but software should
// read once per frame (50-60Hz) to minimize CPU usage.
//
// Mouse is read from CONTROL PORT 1 (POT lines + buttons)
// This allows simultaneous use with joystick on CONTROL PORT 2
//
// Based on implementations from C64 OS and cc65.
//
// FAST MOVEMENT SMOOTHING:
// By default, directional momentum smoothing is enabled to fix the 1351's
// known issue where fast movements can register in the opposite direction.
// To disable smoothing (saves CPU cycles), define MOUSE_NO_SMOOTHING:
// #DEFINE MOUSE_NO_SMOOTHING 1
// #INCLUDE "mouse.c65"
//
// Usage:
// #INCLUDE "mouse.c65"
// mouse_init()
// WHILE 1
// mouse_read() // Call once per frame
// pointer_update_mouse(mouse_delta_x, mouse_delta_y)
// WEND
// ============================================================================
// SID POT line addresses
WORD CONST SID_BASE = $D400
BYTE pot_x @ SID_BASE+$19 // SID+$19: POT X (analog input)
BYTE pot_y @ SID_BASE+$1A // SID+$1A: POT Y (analog input)
// CIA port for button reading (same as joystick port 1)
WORD CONST CIA1_BASE = $DC00
BYTE cia_port1 @ CIA1_BASE+1 // $DC01: Port 1 (mouse buttons)
// Mouse state variables
BYTE mouse_pot_x_old // Previous POT X reading
BYTE mouse_pot_y_old // Previous POT Y reading
BYTE mouse_delta_x // Signed X movement delta
BYTE mouse_delta_y // Signed Y movement delta
BYTE mouse_buttons // Button state (bit 4=right, bit 0=left)
#IFNDEF MOUSE_NO_SMOOTHING
// Directional momentum tracking (for fast movement disambiguation)
// 0=no momentum, 1-127=rightward, 128-255=leftward (unsigned byte as signed)
// Define MOUSE_NO_SMOOTHING before including this library to disable smoothing
BYTE mouse_momentum_x
BYTE mouse_momentum_y
// ============================================================================
// FUNC mouse_apply_momentum
// Apply directional momentum to disambiguate fast movements
// When delta magnitude is large (ambiguous), use momentum to determine
// if we should flip the interpretation (±64 in 6-bit space)
// ============================================================================
FUNC mouse_apply_momentum({BYTE delta} {BYTE momentum} out:{BYTE corrected_delta} out:{BYTE new_momentum})
BYTE abs_delta
BYTE is_negative
BYTE is_ambiguous
BYTE was_corrected
// Get absolute value and sign of delta
is_negative = delta & $80
IF is_negative
abs_delta = 0 - delta
ELSE
abs_delta = delta
ENDIF
// Check if delta is ambiguous (magnitude >= 12 after division by 2 in convert function)
// In raw 6-bit space, deltas near ±32 become ±16 after /2, which is ambiguous
is_ambiguous = 0
was_corrected = 0
#PRAGMA _P_USE_LONG_JUMP 1
IF abs_delta >= 12
#PRAGMA _P_USE_LONG_JUMP 0
is_ambiguous = 1
// Ambiguous - check if flipping (negating) would match momentum better
BYTE flipped_delta
BYTE flipped_negative
// To flip interpretation, just negate the delta
flipped_delta = abs_delta
IF is_negative
// Delta is negative, flipped would be positive
flipped_negative = 0
ELSE
// Delta is positive, flipped would be negative
flipped_negative = 1
ENDIF
// Use momentum to decide: if momentum sign matches flipped, use flipped
IF momentum != 0
BYTE momentum_is_left
BYTE signs_match
momentum_is_left = 0
IF momentum >= 128
momentum_is_left = 1
ENDIF
signs_match = 0
IF momentum_is_left == flipped_negative
signs_match = 1
ENDIF
IF signs_match == 1
// Momentum suggests flipped interpretation
IF flipped_negative
corrected_delta = 0 - flipped_delta
ELSE
corrected_delta = flipped_delta
ENDIF
was_corrected = 1
ELSE
// Keep original
corrected_delta = delta
was_corrected = 1
ENDIF
ELSE
// No momentum yet - ignore this ambiguous delta, return 0
corrected_delta = 0
ENDIF
ELSE
// Unambiguous, use as-is
corrected_delta = delta
ENDIF
// Update momentum based on corrected delta
// Only update from unambiguous deltas to avoid bootstrapping in wrong direction
#PRAGMA _P_USE_LONG_JUMP 1
IF is_ambiguous == 0
#PRAGMA _P_USE_LONG_JUMP 0
IF corrected_delta == 0
// No movement - decay momentum toward zero
IF momentum > 0
IF momentum < 128
IF momentum > 2
momentum = momentum - 2
ELSE
momentum = 0
ENDIF
ELSE
IF momentum < 253
momentum = momentum + 2
ELSE
momentum = 0
ENDIF
ENDIF
ENDIF
ELSE
// Add corrected delta to momentum (clamped)
BYTE corrected_negative
corrected_negative = corrected_delta & $80
IF corrected_negative
// Moving left - push momentum toward 128-255
IF momentum < 128
momentum = 128
ELSE
IF momentum < 240
momentum = momentum + 8
ENDIF
ENDIF
ELSE
// Moving right - push momentum toward 1-127
IF momentum >= 128
momentum = 127
ELSE
IF momentum < 120
momentum = momentum + 8
ENDIF
ENDIF
ENDIF
ENDIF
ENDIF
new_momentum = momentum
FEND
#IFEND
// ============================================================================
// FUNC mouse_convert_pot_delta
// Convert 6-bit POT delta to 8-bit signed delta
// The 1351 outputs 6-bit signed values in bits 1-6 (bits 0,7 are noise)
// This converts them to standard 8-bit signed values (-128 to +127)
// ============================================================================
FUNC mouse_convert_pot_delta({BYTE new_val} {BYTE old_val} out:{BYTE delta})
BYTE diff
BYTE sign_bit
// Calculate raw difference (wraps at 64 due to 6-bit counter)
diff = new_val - old_val
// Mask to 7 bits (ignore bit 7 noise)
diff = diff & %01111111
// Check if value is negative (bit 6 set means >= 64 in 6-bit space)
sign_bit = diff & %01000000
IF sign_bit
// Negative value: set high bits and divide by 2
diff = diff | %11000000 // Sign extend
ASM
lda |diff|
cmp #$FF
beq @zero
sec
ror // Arithmetic shift right (preserves sign)
sta |diff|
jmp @done
@zero:
lda #0
sta |diff|
@done:
ENDASM
delta = diff
ELSE
// Positive value: just divide by 2
ASM
lda |diff|
beq @zero2
lsr // Logical shift right
sta |diff|
jmp @done2
@zero2:
lda #0
sta |diff|
@done2:
ENDASM
delta = diff
ENDIF
FEND
// ============================================================================
// FUNC mouse_init
// Initialize mouse driver - call once at program start
// ============================================================================
FUNC mouse_init
// Read initial pot values
mouse_pot_x_old = pot_x
mouse_pot_y_old = pot_y
mouse_delta_x = 0
mouse_delta_y = 0
mouse_buttons = 0
#IFNDEF MOUSE_NO_SMOOTHING
mouse_momentum_x = 0
mouse_momentum_y = 0
#IFEND
FEND
// ============================================================================
// FUNC mouse_read
// Read mouse movement and button state
// Call this every frame to update mouse state
// Updates mouse_delta_x, mouse_delta_y (signed 8-bit deltas)
// Applies directional momentum to fix fast movement bugs
// ============================================================================
FUNC mouse_read
BYTE pot_x_new
BYTE pot_y_new
// Read current POT values
pot_x_new = pot_x
pot_y_new = pot_y
// Calculate X delta
mouse_convert_pot_delta(pot_x_new, mouse_pot_x_old, mouse_delta_x)
mouse_pot_x_old = pot_x_new
#IFNDEF MOUSE_NO_SMOOTHING
// Apply momentum correction to X
mouse_apply_momentum(mouse_delta_x, mouse_momentum_x, mouse_delta_x, mouse_momentum_x)
#IFEND
// Calculate Y delta (invert for correct screen movement)
mouse_convert_pot_delta(pot_y_new, mouse_pot_y_old, mouse_delta_y)
mouse_delta_y = 0 - mouse_delta_y // Negate Y
mouse_pot_y_old = pot_y_new
#IFNDEF MOUSE_NO_SMOOTHING
// Apply momentum correction to Y
mouse_apply_momentum(mouse_delta_y, mouse_momentum_y, mouse_delta_y, mouse_momentum_y)
#IFEND
// Read buttons from CIA port
mouse_buttons = cia_port1
FEND
LABEL __skip_lib_mouse
#IFEND

78
piles.c65 Normal file
View file

@ -0,0 +1,78 @@
#IFNDEF __lib_piles
#DEFINE __lib_piles 1
GOTO __skip_lib_piles
// Pile data structures
// byte 0 = count, bytes 1-52 = cards
// Cards use CARD_FACEDOWN bit for face-down flag
LABEL pile_stock
ASM
!fill 53, 0
ENDASM
LABEL pile_waste
ASM
!fill 53, 0
ENDASM
LABEL pile_tab0
ASM
!fill 53, 0
ENDASM
LABEL pile_tab1
ASM
!fill 53, 0
ENDASM
LABEL pile_tab2
ASM
!fill 53, 0
ENDASM
LABEL pile_tab3
ASM
!fill 53, 0
ENDASM
LABEL pile_tab4
ASM
!fill 53, 0
ENDASM
LABEL pile_tab5
ASM
!fill 53, 0
ENDASM
LABEL pile_tab6
ASM
!fill 53, 0
ENDASM
LABEL pile_found0
ASM
!fill 14, 0
ENDASM
LABEL pile_found1
ASM
!fill 14, 0
ENDASM
LABEL pile_found2
ASM
!fill 14, 0
ENDASM
LABEL pile_found3
ASM
!fill 14, 0
ENDASM
LABEL __skip_lib_piles
#IFEND

371
pointer.c65 Normal file
View file

@ -0,0 +1,371 @@
#IFNDEF __lib_pointer
#DEFINE __lib_pointer 1
GOTO __skip_lib_pointer
// ============================================================================
// SPRITE POINTER DRIVER FOR COMMODORE 64
// ============================================================================
// This library provides sprite-based pointer/cursor control.
// Can be driven by joystick, mouse, keyboard, or other input methods.
//
// Usage:
// #INCLUDE "pointer.c65"
// pointer_init(100, 50, 1, 192) // Init at x=100, y=50, white color, sprite data block 192
// WHILE 1
// // Read your input device (joystick, mouse, etc.)
// pointer_move(delta_x, delta_y) // Move pointer
// WEND
// ============================================================================
// VIC-II Sprite registers
WORD CONST VIC2 = $D000
BYTE sprite_x0 @ VIC2+0 // Sprite 0 X position
BYTE sprite_y0 @ VIC2+1 // Sprite 0 Y position
BYTE sprite_x_msb @ VIC2+16 // Sprite X MSB (for X > 255)
BYTE sprite_enable @ VIC2+21 // Sprite enable register
BYTE sprite_color0 @ VIC2+39 // Sprite 0 color
BYTE sprite_pointer0 @ $07F8 // Sprite 0 pointer (screen + $3F8)
// Pointer position variables
WORD pointer_x // Current pointer X position (0-511)
BYTE pointer_y // Current pointer Y position
BYTE pointer_speed // Base movement speed
BYTE pointer_accel // Current acceleration counter
BYTE pointer_max_speed // Maximum speed with acceleration
// ============================================================================
// Compact arrow pointer sprite - 7 pixels tall
// Tip at (0,0) for easy pointing at coordinates
// ============================================================================
LABEL pointer_sprite_data
ASM
// Simple compact arrow
!8 $80, $00, $00 // Row 0: *
!8 $C0, $00, $00 // Row 1: **
!8 $E0, $00, $00 // Row 2: ***
!8 $F0, $00, $00 // Row 3: ****
!8 $E0, $00, $00 // Row 4: ***
!8 $C0, $00, $00 // Row 5: **
!8 $80, $00, $00 // Row 6: *
!8 $00, $00, $00 // Row 7-20: blank
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00, $00, $00
!8 $00 // Padding
ENDASM
LABEL pointer_sprite_data_end
// ============================================================================
// FUNC pointer_init
// Initialize sprite 0 as a pointer at given position
// Parameters:
// x_pos: Initial X position (0-255)
// y_pos: Initial Y position (0-250)
// color: Sprite color (0-15)
// sprite_data: Sprite data block number (0-255)
// ============================================================================
FUNC pointer_init({BYTE x_pos} {BYTE y_pos} {BYTE color} {BYTE sprite_data})
BYTE temp
// Set pointer position
pointer_x = x_pos
pointer_y = y_pos
pointer_speed = 1 // Base speed: 1 pixel
pointer_accel = 0 // No acceleration initially
pointer_max_speed = 10 // Max accelerated speed
// Set sprite X position (low byte)
sprite_x0 = x_pos
sprite_y0 = y_pos
// Clear X MSB for sprite 0 (x_pos is < 256)
temp = sprite_x_msb
temp = temp & %11111110 // Clear bit 0
sprite_x_msb = temp
// Set sprite color
sprite_color0 = color
// Set sprite data pointer
sprite_pointer0 = sprite_data
// Enable sprite 0
sprite_enable = 1
FEND
// ============================================================================
// FUNC pointer_move_left
// Move pointer left by speed amount
// ============================================================================
FUNC pointer_move_left({BYTE speed})
// Move left with bounds check
IF pointer_x >= speed
pointer_x = pointer_x - speed
ELSE
pointer_x = 0
ENDIF
IF pointer_x < 24
pointer_x = 24
ENDIF
// Update VIC registers
sprite_x0 = pointer_x
IF pointer_x > 255
sprite_x_msb = sprite_x_msb | %00000001
ELSE
sprite_x_msb = sprite_x_msb & %11111110
ENDIF
FEND
// ============================================================================
// FUNC pointer_move_right
// Move pointer right by speed amount
// ============================================================================
FUNC pointer_move_right({BYTE speed})
// Move right with bounds check
pointer_x = pointer_x + speed
IF pointer_x > 320+20
pointer_x = 320+20
ENDIF
// Update VIC registers
sprite_x0 = pointer_x
IF pointer_x > 255
sprite_x_msb = sprite_x_msb | %00000001
ELSE
sprite_x_msb = sprite_x_msb & %11111110
ENDIF
FEND
// ============================================================================
// FUNC pointer_move_up
// Move pointer up by speed amount
// ============================================================================
FUNC pointer_move_up({BYTE speed})
IF pointer_y >= speed
pointer_y = pointer_y - speed
ENDIF
IF pointer_y < 50
pointer_y = 50 // Stop at top edge, not 0
ENDIF
sprite_y0 = pointer_y
FEND
// ============================================================================
// FUNC pointer_move_down
// Move pointer down by speed amount
// ============================================================================
FUNC pointer_move_down({BYTE speed})
WORD new_y
new_y = pointer_y + speed
// Stop at bottom edge (around Y=229 for NTSC, 249 for PAL)
IF new_y > 247
pointer_y = 247
ELSE
pointer_y = new_y
ENDIF
sprite_y0 = pointer_y
FEND
// ============================================================================
// FUNC pointer_update_joystick
// Update pointer based on joystick input state with acceleration
// Expects joy_state to be set (from joystick.c65 library)
// joy_state bits: UP=bit0, DOWN=bit1, LEFT=bit2, RIGHT=bit3, FIRE=bit4
// ============================================================================
FUNC pointer_update_joystick({BYTE joy_state})
BYTE actual_speed
BYTE any_direction
BYTE test_bit
// Check if any direction is pressed (mask all direction bits)
any_direction = joy_state & %00001111
// Reset acceleration if joystick is neutral
IF any_direction == 0
pointer_accel = 0
ELSE
// Increase acceleration while moving
IF pointer_accel < 255
pointer_accel++
ENDIF
ENDIF
// Calculate actual speed with acceleration
// Divide accel by 8 (shift right 3 times)
actual_speed = pointer_accel
ASM
lda |actual_speed|
;lsr
lsr
lsr
lsr
sta |actual_speed|
ENDASM
actual_speed = pointer_speed + actual_speed
IF actual_speed > pointer_max_speed
actual_speed = pointer_max_speed
ENDIF
// Move in pressed directions
test_bit = joy_state & %00000100 // LEFT mask
IF test_bit
pointer_move_left(actual_speed)
ENDIF
test_bit = joy_state & %00001000 // RIGHT mask
IF test_bit
pointer_move_right(actual_speed)
ENDIF
test_bit = joy_state & %00000001 // UP mask
IF test_bit
pointer_move_up(actual_speed)
ENDIF
test_bit = joy_state & %00000010 // DOWN mask
IF test_bit
pointer_move_down(actual_speed)
ENDIF
FEND
// ============================================================================
// FUNC pointer_update_mouse
// Update pointer based on mouse delta movements
// Expects mouse_delta_x and mouse_delta_y to be set (from mouse.c65 library)
// Mouse deltas are signed 8-bit values
// ============================================================================
FUNC pointer_update_mouse({BYTE delta_x} {BYTE delta_y})
BYTE sign_x
BYTE sign_y
BYTE abs_x
BYTE abs_y
// Handle X movement
sign_x = delta_x & %10000000
IF sign_x
// Negative (moving left)
abs_x = 0 - delta_x // Get absolute value
IF abs_x > 0
pointer_move_left(abs_x)
ENDIF
ELSE
// Positive (moving right)
IF delta_x > 0
pointer_move_right(delta_x)
ENDIF
ENDIF
// Handle Y movement
sign_y = delta_y & %10000000
IF sign_y
// Negative (moving up)
abs_y = 0 - delta_y // Get absolute value
IF abs_y > 0
pointer_move_up(abs_y)
ENDIF
ELSE
// Positive (moving down)
IF delta_y > 0
pointer_move_down(delta_y)
ENDIF
ENDIF
FEND
// ============================================================================
// FUNC pointer_set_speed
// Set pointer movement speed
// Parameters:
// speed: Pixels to move per movement (1-8 recommended)
// ============================================================================
FUNC pointer_set_speed({BYTE speed})
pointer_speed = speed
FEND
// ============================================================================
// FUNC pointer_get_x
// Get current pointer X position
// Parameters:
// out:x_pos: X position (0-511)
// ============================================================================
FUNC pointer_get_x(out:{WORD x_pos})
x_pos = pointer_x
FEND
// ============================================================================
// FUNC pointer_get_y
// Get current pointer Y position
// Returns: Y position in out parameter
// ============================================================================
FUNC pointer_get_y(out:{BYTE y_pos})
y_pos = pointer_y
FEND
// ============================================================================
// FUNC pointer_enable
// Enable or disable pointer sprite
// Parameters:
// enable: 1 to enable, 0 to disable
// ============================================================================
FUNC pointer_enable({BYTE enable})
BYTE temp
temp = sprite_enable
IF enable
temp = temp | %00000001
ELSE
temp = temp & %11111110
ENDIF
sprite_enable = temp
FEND
// ============================================================================
// FUNC pointer_set_color
// Change pointer sprite color
// Parameters:
// color: Color value (0-15)
// ============================================================================
FUNC pointer_set_color({BYTE color})
sprite_color0 = color
FEND
LABEL __skip_lib_pointer
#IFEND

131
random.c65 Normal file
View file

@ -0,0 +1,131 @@
#IFNDEF __lib_random
#DEFINE __lib_random 1
GOTO __skip_lib_random
// LFSR Random Number Generator
// Combined 16-bit (period 65535) and 15-bit (period 32767) generators
// Total period: ~2.1 billion (65535 * 32767 = 2,147,385,345)
//
// Based on Hanno Behrens' implementation (LGPL)
// Adapted for c65 by using local variable storage
//
// Usage:
// BYTE rnd
// rand(rnd) // rnd now contains 0-255
//
// For ZP acceleration, change the variable declarations below
// from regular addresses to ZP addresses (e.g., @ $f6)
// Random state - initialized with default seeds
WORD rand_sr1 = $a55a
WORD rand_sr2 = $7653
// Initialize the random generator with default seeds
// Call once at startup (or just use rand_seed() with custom seed)
FUNC rand_init
rand_sr1 = $a55a
rand_sr2 = $7653
FEND
// Initialize with a custom seed value
// Good for seeding from user input timing
FUNC rand_seed({WORD seed})
rand_sr1 = seed ^ $a55a
rand_sr2 = seed ^ $7653
// Ensure neither seed is zero
IF rand_sr1 == 0
rand_sr1 = $a55a
ENDIF
IF rand_sr2 == 0
rand_sr2 = $7653
ENDIF
FEND
// Internal: 16-bit LFSR with period 65535
// Taps: 10, 12, 13, 15 (polynomial)
FUNC rand_lfsr64k
ASM
lda |rand_sr1|+1
asl
asl
eor |rand_sr1|+1
asl
eor |rand_sr1|+1
asl
asl
eor |rand_sr1|+1
asl
rol |rand_sr1|
rol |rand_sr1|+1
ENDASM
FEND
// Internal: 15-bit LFSR with period 32767
// Taps: 13, 14 (polynomial)
FUNC rand_lfsr32k
ASM
lda |rand_sr2|+1
asl
eor |rand_sr2|+1
asl
asl
ror |rand_sr2|
rol |rand_sr2|+1
ENDASM
FEND
// Get random byte (0-255)
// Result returned in 'result' parameter
FUNC rand(out:{BYTE result})
rand_lfsr64k()
rand_lfsr32k()
ASM
lda |rand_sr1|
eor |rand_sr2|
sta |result|
ENDASM
FEND
// Get random byte in range [0, max)
// Uses rejection sampling for unbiased results
FUNC rand_max({BYTE max} out:{BYTE result})
BYTE r
BYTE mask
// Calculate mask (smear highest bit down to get next power of 2 minus 1)
mask = max - 1
ASM
lda |mask|
lsr
ora |mask|
sta |mask|
lsr
lsr
ora |mask|
sta |mask|
lsr
lsr
lsr
lsr
ora |mask|
sta |mask|
ENDASM
// Rejection sampling loop
WHILE 1
rand(r)
r = r & mask
IF r < max
result = r
EXIT
ENDIF
WEND
FEND
LABEL __skip_lib_random
#IFEND

249
testgames.c65 Normal file
View file

@ -0,0 +1,249 @@
#IFNDEF __lib_testgames
#DEFINE __lib_testgames 1
GOTO __skip_lib_testgames
// ============================================================================
// Test game setups for debugging specific scenarios
// To use: call setup_test_game_X() in main() instead of normal deal sequence
// ============================================================================
// ============================================================================
// FUNC setup_test_game_tall_tableau
// Sets up tableau 0 with K->3 (11 cards) and red 2 in waste
// Tests compact rendering mode and edge case of moving 2 onto 3
// ============================================================================
FUNC setup_test_game_tall_tableau
WORD ptr @ $fa
// Clear all piles
POINTER ptr -> pile_stock
POKE ptr[0] , 0
POINTER ptr -> pile_waste
POKE ptr[0] , 0
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
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
// Setup tableau 0: K->3 alternating colors (all face-up)
// Spades King (38) -> Hearts Queen (11) -> Spades Jack (36) -> ...
POINTER ptr -> pile_tab0
POKE ptr[0] , 11 // Count
POKE ptr[1] , 38 // Spades King
POKE ptr[2] , 11 // Hearts Queen
POKE ptr[3] , 36 // Spades Jack
POKE ptr[4] , 9 // Hearts 10
POKE ptr[5] , 34 // Spades 9
POKE ptr[6] , 7 // Hearts 8
POKE ptr[7] , 32 // Spades 7
POKE ptr[8] , 5 // Hearts 6
POKE ptr[9] , 30 // Spades 5
POKE ptr[10] , 3 // Hearts 4
POKE ptr[11] , 28 // Spades 3
// Put red 2 (Hearts 2 = 1) in waste
POINTER ptr -> pile_waste
POKE ptr[0] , 1 // Count
POKE ptr[1] , 1 // Hearts 2
// Put some cards in stock for drawing
POINTER ptr -> pile_stock
POKE ptr[0] , 5
POKE ptr[1] , 13 // Diamonds Ace
POKE ptr[2] , 26 // Spades Ace
POKE ptr[3] , 39 // Clubs Ace
POKE ptr[4] , 0 // Hearts Ace
POKE ptr[5] , 14 // Diamonds 2
FEND
// ============================================================================
// FUNC setup_test_game_one_move_to_win
// Sets up game 1 move from victory
// 3 foundations complete, 4th has Ace->Queen, Clubs King in waste
// ============================================================================
FUNC setup_test_game_one_move_to_win
WORD ptr @ $fa
BYTE i
// Clear all piles
POINTER ptr -> pile_stock
POKE ptr[0] , 0
POINTER ptr -> pile_waste
POKE ptr[0] , 0
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
// Foundation 0: Hearts Ace->King (complete)
POINTER ptr -> pile_found0
POKE ptr[0] , 13
POKE ptr[1] , 0
POKE ptr[2] , 1
POKE ptr[3] , 2
POKE ptr[4] , 3
POKE ptr[5] , 4
POKE ptr[6] , 5
POKE ptr[7] , 6
POKE ptr[8] , 7
POKE ptr[9] , 8
POKE ptr[10] , 9
POKE ptr[11] , 10
POKE ptr[12] , 11
POKE ptr[13] , 12
// Foundation 1: Diamonds Ace->King (complete)
POINTER ptr -> pile_found1
POKE ptr[0] , 13
POKE ptr[1] , 13
POKE ptr[2] , 14
POKE ptr[3] , 15
POKE ptr[4] , 16
POKE ptr[5] , 17
POKE ptr[6] , 18
POKE ptr[7] , 19
POKE ptr[8] , 20
POKE ptr[9] , 21
POKE ptr[10] , 22
POKE ptr[11] , 23
POKE ptr[12] , 24
POKE ptr[13] , 25
// Foundation 2: Spades Ace->King (complete)
POINTER ptr -> pile_found2
POKE ptr[0] , 13
POKE ptr[1] , 26
POKE ptr[2] , 27
POKE ptr[3] , 28
POKE ptr[4] , 29
POKE ptr[5] , 30
POKE ptr[6] , 31
POKE ptr[7] , 32
POKE ptr[8] , 33
POKE ptr[9] , 34
POKE ptr[10] , 35
POKE ptr[11] , 36
POKE ptr[12] , 37
POKE ptr[13] , 38
// Foundation 3: Clubs Ace->Queen (missing King)
POINTER ptr -> pile_found3
POKE ptr[0] , 12
POKE ptr[1] , 39
POKE ptr[2] , 40
POKE ptr[3] , 41
POKE ptr[4] , 42
POKE ptr[5] , 43
POKE ptr[6] , 44
POKE ptr[7] , 45
POKE ptr[8] , 46
POKE ptr[9] , 47
POKE ptr[10] , 48
POKE ptr[11] , 49
POKE ptr[12] , 50
// Waste: Clubs King (the winning move)
POINTER ptr -> pile_waste
POKE ptr[0] , 1
POKE ptr[1] , 51
FEND
// ============================================================================
// FUNC setup_test_game_overflow
// Sets up very tall tableau to test screen overflow bounds checking
// Tableau 3 gets King->Ace (13 cards) all face-up to trigger compact mode
// ============================================================================
FUNC setup_test_game_overflow
WORD ptr @ $fa
// Clear all piles
POINTER ptr -> pile_stock
POKE ptr[0] , 0
POINTER ptr -> pile_waste
POKE ptr[0] , 0
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
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
// Setup tableau 3: King->Ace alternating colors (13 cards, all face-up)
// This will trigger compact rendering and test overflow protection
POINTER ptr -> pile_tab3
POKE ptr[0] , 13 // Count
POKE ptr[1] , 38 // Spades King
POKE ptr[2] , 11 // Hearts Queen
POKE ptr[3] , 36 // Spades Jack
POKE ptr[4] , 9 // Hearts 10
POKE ptr[5] , 34 // Spades 9
POKE ptr[6] , 7 // Hearts 8
POKE ptr[7] , 32 // Spades 7
POKE ptr[8] , 5 // Hearts 6
POKE ptr[9] , 30 // Spades 5
POKE ptr[10] , 3 // Hearts 4
POKE ptr[11] , 28 // Spades 3
POKE ptr[12] , 1 // Hearts 2
POKE ptr[13] , 26 // Spades Ace
FEND
LABEL __skip_lib_testgames
#IFEND

142
utils.c65 Normal file
View file

@ -0,0 +1,142 @@
#IFNDEF __lib_utils
#DEFINE __lib_utils 1
GOTO __skip_lib_utils
FUNC fill_mem({WORD start_addr @ $fa} {WORD end_addr} {BYTE value})
FOR start_addr = start_addr TO end_addr
POKE start_addr , value
NEXT
FEND
FUNC mem_copy({WORD start_addr @ $fa} {WORD end_addr} {WORD target_addr @ $fc})
BYTE value
FOR start_addr = start_addr TO end_addr
value = PEEK start_addr
POKE target_addr , value
target_addr++
NEXT
FEND
FUNC mem_copy_range({WORD start_addr @ $fa} {WORD target_addr @ $fc} {WORD range})
BYTE value
WHILE range
value = PEEK start_addr
POKE target_addr , value
start_addr++
target_addr++
range--
WEND
FEND
FUNC set_vic_bank({BYTE bank})
ASM
lda $dd02
ora #3
sta $dd02
; sanitize and reverse value (so bank 0 = $0000. bank 1 = $4000 etc)
lda |bank|
and #3
eor #3
sta |bank|
lda $dd00
and #%11111100
ora |bank|
sta $dd00
ENDASM
FEND
// Screen mem goes in banks of $0400
// 0=$0000
// 1=$0400
// 2=$0800
// ...
FUNC set_vic_screenmem({BYTE screenmem})
ASM
; sanitize screenmem and move to relevant $d018 bits
lda |screenmem|
and #$0f
clc
rol
rol
rol
rol
sta |screenmem|
lda $d018
and #$0f
ora |screenmem|
sta $d018
ENDASM
FEND
// Char mem goes in banks of $0800
// 0=$0000
// 1=$0800
// 2=$1000
// ...
// (Bitmaps in banks of $2000)
// 0=$0000
// 4=$2000
FUNC set_vic_charmem({BYTE charmem})
ASM
; sanitize charmem and move to relevant $d018 bits
lda |charmem|
and #%00000111
clc
rol
sta |charmem|
lda $d018
and #$f0
ora |charmem|
sta $d018
ENDASM
FEND
FUNC set_vic_ecm
ASM
lda $d011
and #$1f ; leave DEN bit + all scroll stuff
ora #64
sta $d011
ENDASM
FEND
// Wait for a keypress using direct CIA keyboard scan
// No interrupts needed
FUNC wait_key
BYTE row @ $dc01
BYTE col @ $dc00
// Wait for NO key pressed first (debounce)
col = 0
WHILE row <> $ff
WEND
// Now wait for ANY key pressed
WHILE row == $ff
WEND
// Wait for key release (debounce)
WHILE row <> $ff
WEND
FEND
LABEL __skip_lib_utils
#IFEND