diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..915d48f --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..653f077 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md index 065d38b..01fbd87 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,44 @@ -# solitaire-c64 +# Solitaire C64 -A solitaire style game for the Commodore 64 \ No newline at end of file +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. diff --git a/cardconsts.c65 b/cardconsts.c65 new file mode 100644 index 0000000..5d6b03d --- /dev/null +++ b/cardconsts.c65 @@ -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 diff --git a/carddeck.c65 b/carddeck.c65 new file mode 100644 index 0000000..bb3794f --- /dev/null +++ b/carddeck.c65 @@ -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 diff --git a/cardgame.c65 b/cardgame.c65 new file mode 100644 index 0000000..8734b6d --- /dev/null +++ b/cardgame.c65 @@ -0,0 +1,152 @@ +#INCLUDE +#INCLUDE + +//#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 diff --git a/cardmoves.c65 b/cardmoves.c65 new file mode 100644 index 0000000..99cb002 --- /dev/null +++ b/cardmoves.c65 @@ -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 diff --git a/cardrender.c65 b/cardrender.c65 new file mode 100644 index 0000000..be9928f --- /dev/null +++ b/cardrender.c65 @@ -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 diff --git a/cardsprites.c65 b/cardsprites.c65 new file mode 100644 index 0000000..58558ea --- /dev/null +++ b/cardsprites.c65 @@ -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 diff --git a/cardtests.c65 b/cardtests.c65 new file mode 100644 index 0000000..6790521 --- /dev/null +++ b/cardtests.c65 @@ -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 diff --git a/charpad_cards/charpad_cards.ctm b/charpad_cards/charpad_cards.ctm new file mode 100644 index 0000000..18671a6 Binary files /dev/null and b/charpad_cards/charpad_cards.ctm differ diff --git a/charpad_cards/charpad_cards2.ctm b/charpad_cards/charpad_cards2.ctm new file mode 100644 index 0000000..c211ea9 Binary files /dev/null and b/charpad_cards/charpad_cards2.ctm differ diff --git a/charpad_cards/charpad_cards3.ctm b/charpad_cards/charpad_cards3.ctm new file mode 100644 index 0000000..f04da50 Binary files /dev/null and b/charpad_cards/charpad_cards3.ctm differ diff --git a/charpad_cards/charpad_cards4.ctm b/charpad_cards/charpad_cards4.ctm new file mode 100644 index 0000000..bc7262a Binary files /dev/null and b/charpad_cards/charpad_cards4.ctm differ diff --git a/charpad_cards/charpad_cards5.ctm b/charpad_cards/charpad_cards5.ctm new file mode 100644 index 0000000..f3b1d11 Binary files /dev/null and b/charpad_cards/charpad_cards5.ctm differ diff --git a/charpad_cards/charpad_cards6 - Chars.asm b/charpad_cards/charpad_cards6 - Chars.asm new file mode 100644 index 0000000..9060c54 --- /dev/null +++ b/charpad_cards/charpad_cards6 - Chars.asm @@ -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 + + + diff --git a/charpad_cards/charpad_cards6 - Chars.bin b/charpad_cards/charpad_cards6 - Chars.bin new file mode 100644 index 0000000..e74a6ea Binary files /dev/null and b/charpad_cards/charpad_cards6 - Chars.bin differ diff --git a/charpad_cards/charpad_cards6.ctm b/charpad_cards/charpad_cards6.ctm new file mode 100644 index 0000000..48eadf3 Binary files /dev/null and b/charpad_cards/charpad_cards6.ctm differ diff --git a/charpad_cards/charpad_cards7 - Chars.bin b/charpad_cards/charpad_cards7 - Chars.bin new file mode 100644 index 0000000..4115c5f Binary files /dev/null and b/charpad_cards/charpad_cards7 - Chars.bin differ diff --git a/charpad_cards/charpad_cards7 - Map.png b/charpad_cards/charpad_cards7 - Map.png new file mode 100644 index 0000000..5edc995 Binary files /dev/null and b/charpad_cards/charpad_cards7 - Map.png differ diff --git a/charpad_cards/charpad_cards7.bin b/charpad_cards/charpad_cards7.bin new file mode 100644 index 0000000..12a9c22 Binary files /dev/null and b/charpad_cards/charpad_cards7.bin differ diff --git a/charpad_cards/charpad_cards7.ctm b/charpad_cards/charpad_cards7.ctm new file mode 100644 index 0000000..4fc1c24 Binary files /dev/null and b/charpad_cards/charpad_cards7.ctm differ diff --git a/charpad_cards/charpad_cards7_ecm.bin b/charpad_cards/charpad_cards7_ecm.bin new file mode 100644 index 0000000..30329ce Binary files /dev/null and b/charpad_cards/charpad_cards7_ecm.bin differ diff --git a/charpad_cards/visualize_chars.sh b/charpad_cards/visualize_chars.sh new file mode 100644 index 0000000..d518eeb --- /dev/null +++ b/charpad_cards/visualize_chars.sh @@ -0,0 +1 @@ +xxd -b -c1 charpad_cards7.bin | cut -d' ' -f2 | tr '01' '.#' diff --git a/charpad_color_sprites/charpad_color_sprites - Project.asm b/charpad_color_sprites/charpad_color_sprites - Project.asm new file mode 100644 index 0000000..b23c425 --- /dev/null +++ b/charpad_color_sprites/charpad_color_sprites - Project.asm @@ -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 + + + diff --git a/charpad_color_sprites/charpad_color_sprites.spd b/charpad_color_sprites/charpad_color_sprites.spd new file mode 100644 index 0000000..b6cf294 Binary files /dev/null and b/charpad_color_sprites/charpad_color_sprites.spd differ diff --git a/charpad_rank_sprites/charpad_rank_sprites - Project.asm b/charpad_rank_sprites/charpad_rank_sprites - Project.asm new file mode 100644 index 0000000..3938f08 --- /dev/null +++ b/charpad_rank_sprites/charpad_rank_sprites - Project.asm @@ -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 + + + diff --git a/charpad_rank_sprites/charpad_rank_sprites.png b/charpad_rank_sprites/charpad_rank_sprites.png new file mode 100644 index 0000000..dba2e57 Binary files /dev/null and b/charpad_rank_sprites/charpad_rank_sprites.png differ diff --git a/charpad_rank_sprites/charpad_rank_sprites.spd b/charpad_rank_sprites/charpad_rank_sprites.spd new file mode 100644 index 0000000..d48f8d9 Binary files /dev/null and b/charpad_rank_sprites/charpad_rank_sprites.spd differ diff --git a/charpad_rank_sprites/charpad_rank_sprites.xcf b/charpad_rank_sprites/charpad_rank_sprites.xcf new file mode 100644 index 0000000..5521a78 Binary files /dev/null and b/charpad_rank_sprites/charpad_rank_sprites.xcf differ diff --git a/claude_code_docker.sh b/claude_code_docker.sh new file mode 100644 index 0000000..f022680 --- /dev/null +++ b/claude_code_docker.sh @@ -0,0 +1,4 @@ +#!/bin/bash +export DOCKER_UID=$(id -u) +export DOCKER_GID=$(id -g) +docker compose run --rm claude-solitaire diff --git a/cm.sh b/cm.sh new file mode 100755 index 0000000..c59b649 --- /dev/null +++ b/cm.sh @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..4d9c8a5 --- /dev/null +++ b/docker-compose.yml @@ -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 diff --git a/exomizer_compress_prg.sh b/exomizer_compress_prg.sh new file mode 100644 index 0000000..f36edeb --- /dev/null +++ b/exomizer_compress_prg.sh @@ -0,0 +1 @@ +exomizer sfx basic main.prg -o main2.prg diff --git a/gameloop.c65 b/gameloop.c65 new file mode 100644 index 0000000..b8c21ae --- /dev/null +++ b/gameloop.c65 @@ -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 diff --git a/joystick.c65 b/joystick.c65 new file mode 100644 index 0000000..70b3113 --- /dev/null +++ b/joystick.c65 @@ -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 diff --git a/joysticktests.c65 b/joysticktests.c65 new file mode 100644 index 0000000..2b4584e --- /dev/null +++ b/joysticktests.c65 @@ -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 diff --git a/mouse.c65 b/mouse.c65 new file mode 100644 index 0000000..ac4ca08 --- /dev/null +++ b/mouse.c65 @@ -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 diff --git a/piles.c65 b/piles.c65 new file mode 100644 index 0000000..929082e --- /dev/null +++ b/piles.c65 @@ -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 diff --git a/pointer.c65 b/pointer.c65 new file mode 100644 index 0000000..74f3a14 --- /dev/null +++ b/pointer.c65 @@ -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 diff --git a/random.c65 b/random.c65 new file mode 100644 index 0000000..5b33040 --- /dev/null +++ b/random.c65 @@ -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 diff --git a/testgames.c65 b/testgames.c65 new file mode 100644 index 0000000..b66047b --- /dev/null +++ b/testgames.c65 @@ -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 diff --git a/utils.c65 b/utils.c65 new file mode 100644 index 0000000..182a4d1 --- /dev/null +++ b/utils.c65 @@ -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