Added win screen

This commit is contained in:
Mattias Hansson 2026-01-18 17:30:16 +01:00
parent 1151b2d2c4
commit 6ae8587fed
6 changed files with 302 additions and 6 deletions

151
CLAUDE.md Normal file
View file

@ -0,0 +1,151 @@
# CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
## Development Workflow
This is a collaborative pair programming effort. Claude runs in a Docker environment with access to the codebase but cannot build or run the project. The user handles all builds and testing.
## Project Overview
Solitaire C64 is a Klondike solitaire card game for the Commodore 64, written in c65gm (a high-level language that compiles to 6502 assembly). Language documentation is available in `language_docs/`.
## Build Commands
```bash
./cm.sh # Build the project (outputs main.prg)
./start_in_vice.sh # Launch in VICE emulator
./exomizer_compress_prg.sh # Compress binary with Exomizer
```
Build chain: C65 source → c65gm compiler → 6502 assembly → ACME assembler → main.prg
Requirements:
- [c65gm compiler](https://git.techserio.com/mattiashz/c65gm)
- ACME 6502 assembler
## Testing
Enable test modes by uncommenting `#DEFINE TEST_GAMES 1` in `cardgame.c65`. Test functions in `cardtests.c65` and `joysticktests.c65` can be called from main() for debugging specific scenarios.
## Architecture
### Module Organization
**Core Game Logic:**
- `cardgame.c65` - Main entry point, initialization, RNG seeding
- `gameloop.c65` - Main loop, input processing, coordinate-to-pile mapping
- `gamestate.c65` - Global state (draw mode, selected piles, interaction flags)
- `cardmoves.c65` - Move validation and execution
- `carddeck.c65` - Fisher-Yates shuffle, dealing
- `cardrender.c65` - Card and pile rendering
**Input System:**
- `joystick.c65` - CIA Port 2 joystick driver
- `mouse.c65` - 1351 mouse driver with smoothing
- `keyboard.c65` - Non-interfering keyboard scan
- `pointer.c65` - VIC-II sprite cursor control
**Data & Constants:**
- `piles.c65` - Pile data structures (13 piles: stock, waste, 4 foundations, 7 tableaus)
- `pileconsts.c65` - Pile ID constants
- `cardconsts.c65` - Card suits, ranks, flags
### Game Data Model
- 52 cards represented as values 0-51 (0-12 Hearts, 13-25 Diamonds, 26-38 Spades, 39-51 Clubs)
- Face-down cards have high bit set ($80)
- Each pile: 1 count byte + card slots (tableaus: 53 bytes, foundations: 14 bytes)
- PILE_END marker ($FF) indicates empty pile
### Memory Layout
- `$0400` - Screen memory
- `$2000` - Custom character set
- `$3000` - Code start
- `$C000` - Screen backup (menu)
- `$D000` - VIC-II registers
- `$DC00` - CIA ports (joystick/keyboard)
### Custom Character Set (ECM Mode)
The game uses a 64-character ECM (Extended Color Mode) font stored at `$2000`. This is NOT a standard ASCII font - it contains only card-specific graphics:
- **Char 0**: Solid filled block (`$FF` bytes)
- **Chars 1-12**: Rank characters (2-10, J, Q, K) - note: Ace uses char 13
- **Char 13**: Ace character
- **Chars 14-15**: Suit symbols (spades, clubs in one color set)
- **Chars 19 ($13)**: Empty/blank character (`$00` bytes)
- **Chars $15-$39**: Suit graphics (hearts, diamonds, spades, clubs as 3x3 grids)
- **Chars $50-$51**: Suit symbols (hearts, diamonds)
- **Various**: Card borders, corners, card back pattern pieces
Character data is in `charpad_cards/`. The map PNG shows the visual layout. ECM mode uses 2 bits from color RAM to select between 4 color sets, effectively giving 64 unique characters × 4 color variations.
Since there are no alphabet characters, any text display (like "YOU WIN!") must be constructed from available shapes (solid blocks, empty spaces, card graphics).
## c65gm Language Notes
See `language_docs/` for full reference. Key points:
**Arithmetic expressions have two contexts:**
*Compile-time* (no spaces) - evaluated by compiler, supports `+ - * /` and logic operators:
```c65
value = 5+6*2 // Computed at compile time, supports * /
offset = 40*5+SCREEN // Fifth row (40 cols * 5 rows) + screen base address
// NOTE: SCREEN+40*5 would mean (SCREEN+40)*5 due to left-to-right eval!
```
*Runtime* (with spaces) - generates 6502 code, only `+ -` and logic operators:
```c65
result = a + b // Runtime addition
result = count - 1 // Runtime subtraction
// No runtime multiplication - split complex expressions into multiple statements
```
**Critical: No operator precedence** in either context. Evaluation is strictly left to right:
```c65
result = 2+3*4 // = 20, NOT 14 (evaluates as (2+3)*4)
```
**Types:**
- `BYTE` (8-bit), `WORD` (16-bit)
- `BYTE CONST` / `WORD CONST` for constants (preferred over `#DEFINE`)
- Memory-mapped: `BYTE borderColor @ $D020`
**Control flow:** `IF`/`ENDIF`, `WHILE`/`WEND`, `FOR`/`NEXT`, `SWITCH`/`CASE`/`ENDSWITCH`, `BREAK`
**Functions:**
```c65
FUNC add(in:a, in:b, out:result)
result = a + b
FEND
```
Parameter modifiers: `in:` (read-only, default), `out:` (write-only), `io:` (read-write, both in and out).
Call with `@<label>` to pass an address: `myFunc(@dataTable)` sends the address of `dataTable` to that WORD parameter.
**Memory access:**
```c65
value = PEEK $D020 // Read byte
POKE $D020 WITH 0 // Write byte
value = PEEK screenPtr[index] // Indexed access (pointer must be WORD in zero page)
POINTER screenPtr TO $0400 // Set pointer
```
Also `PEEKW`/`POKEW` for 16-bit values.
**Pointers:** Not special like in C - just WORD variables. Whether it's a "pointer" depends on usage. Normal WORD variables work fine to hold addresses. Only use zero-page (`@ $xx`) for pointers that need indexed PEEK/POKE access.
**Inline assembly:** Use `ASM`...`ENDASM`. Reference local variables with `|varname|` syntax.
**Include guards pattern:**
```c65
#IFNDEF __MY_LIBRARY
#DEFINE __MY_LIBRARY = 1
GOTO lib_skip
// ... library code ...
LABEL lib_skip
#IFEND
```
**Number formats:** Decimal `123`, hex `$FF`, binary `%11110000`

View file

@ -32,6 +32,7 @@ ENDASM
#IFDEF TEST_GAMES #IFDEF TEST_GAMES
#INCLUDE "testgames.c65" #INCLUDE "testgames.c65"
#IFEND #IFEND
#INCLUDE "winscreen.c65"
#INCLUDE "gameloop.c65" #INCLUDE "gameloop.c65"

View file

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

View file

@ -840,6 +840,7 @@ FUNC restart_game
game_selected_pile = PILE_ID_NONE game_selected_pile = PILE_ID_NONE
game_selected_card_count = 0 game_selected_card_count = 0
game_prev_button_state = 0 game_prev_button_state = 0
game_win_shown = 0
FEND FEND
@ -901,8 +902,8 @@ FUNC game_loop
#IFDEF TEST_GAMES #IFDEF TEST_GAMES
// Test game options (comment/uncomment one): // Test game options (comment/uncomment one):
//setup_test_game_tall_tableau() // K->3 in tab0, red 2 in waste //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_one_move_to_win() // 1 move from victory
setup_test_game_overflow() // K->A in tab3 to test screen overflow //setup_test_game_overflow() // K->A in tab3 to test screen overflow
#IFEND #IFEND
BYTE menu_key BYTE menu_key
@ -1022,10 +1023,11 @@ FUNC game_loop
// Check win condition // Check win condition
check_win_condition(is_won) check_win_condition(is_won)
IF is_won IF is_won
// Flash border or show message IF game_win_shown = 0
show_win_screen()
game_win_shown = 1
ENDIF
POKE $d020 , color_green POKE $d020 , color_green
// Could add "YOU WIN" message here
// For now, just keep running to allow admiring the win
ENDIF ENDIF
// Small delay to avoid reading mouse too fast // Small delay to avoid reading mouse too fast

View file

@ -19,6 +19,9 @@ BYTE game_selected_pile // Currently selected pile ID (PILE_ID_NON
BYTE game_selected_card_count // Number of cards selected from tableau (1+ for tableau stacks) 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_prev_button_state // Previous mouse button state for click detection
// Win state
BYTE game_win_shown = 0 // 1 if win screen has been displayed
LABEL __skip_lib_gamestate LABEL __skip_lib_gamestate

139
winscreen.c65 Normal file
View file

@ -0,0 +1,139 @@
#IFNDEF __lib_winscreen
#DEFINE __lib_winscreen 1
GOTO __skip_lib_winscreen
// ============================================================================
// WIN SCREEN
// Displays "YOU WIN!" celebration when game is won
// ============================================================================
BYTE CONST WIN_LOGO_CHAR = $50 // Hearts suit character
BYTE CONST WIN_CLEAR_CHAR = 0 // Solid block (shows color RAM color)
// Logo dimensions and position
// Logo is 26 chars wide x 5 chars tall, centered on 40x25 screen
BYTE CONST WIN_LOGO_WIDTH = 26
BYTE CONST WIN_LOGO_HEIGHT = 5
BYTE CONST WIN_LOGO_START_COL = 7 // (40-26)/2 = 7
BYTE CONST WIN_LOGO_START_ROW = 10 // (25-5)/2 = 10
// Clear area with 1 char padding on sides
BYTE CONST WIN_CLEAR_WIDTH = 28
BYTE CONST WIN_CLEAR_START_COL = 6
// Screen base
WORD CONST SCREEN_BASE = $0400
// ----------------------------------------------------------------------------
// Logo offset data
// Each byte is an offset from logo top-left where hearts char goes
// Arranged visually to show letter shapes in source
// ----------------------------------------------------------------------------
LABEL win_logo_offsets
ASM
; "YOU WIN!" - 5 rows x 26 cols
; Offsets where hearts char ($50) is placed
;
; Y O U W I N !
;
; Row 0:
; █ █ ███ █ █ █ █ █ █ █ █
!8 0,2, 4,5,6, 8,10, 13,17, 19, 21,23, 25
;
; Row 1:
; █ █ █ █ █ █ █ █ ███ █
!8 41, 44,46, 48,50, 53,57, 59, 61,62,63,65
;
; Row 2:
; █ █ █ █ █ █ █ █ █ █ █ █
!8 81, 84,86, 88,90, 93,95,97, 99, 101,103,105
;
; Row 3:
; █ █ █ █ █ █ █ █ █ █ █
!8 121, 124,126,128,130, 133,135,137,139,141,143
;
; Row 4:
; █ ███ ███ █ █ █ █ █ █
!8 161, 164,165,166,168,169,170,174,176,179,181,183,185
;
; Terminator
!8 255
ENDASM
// ============================================================================
// FUNC clear_win_area
// Clears a rectangular area in the center of screen for the logo
// Fills with WIN_CLEAR_CHAR (solid block showing white)
// ============================================================================
FUNC clear_win_area
WORD screen_ptr @ $a0
BYTE row
BYTE col
WORD row_start
// Calculate starting position: row 10, col 6
// Offset = 10 * 40 + 6 = 406
row_start = SCREEN_BASE + 406
FOR row = 0 TO WIN_LOGO_HEIGHT-1
POINTER screen_ptr -> row_start
FOR col = 0 TO WIN_CLEAR_WIDTH-1
POKE screen_ptr[col] , WIN_CLEAR_CHAR
NEXT
// Move to next row (add 40)
row_start = row_start + 40
NEXT
FEND
// ============================================================================
// FUNC draw_win_logo
// Draws "YOU WIN!" using hearts characters at the calculated offsets
// Must call clear_win_area first
// ============================================================================
FUNC draw_win_logo
WORD screen_ptr @ $a0
WORD offset_ptr @ $a2
WORD logo_base
BYTE offset
// Logo base position: row 10, col 7
// Offset = 10 * 40 + 7 = 407
logo_base = SCREEN_BASE + 407
POINTER offset_ptr -> win_logo_offsets
BYTE index
index = 0
// Loop through all offsets until terminator (255)
offset = PEEK offset_ptr[index]
WHILE offset != 255
// Calculate screen address: logo_base + offset
screen_ptr = logo_base + offset
// Poke hearts character
POKE screen_ptr[0] , WIN_LOGO_CHAR
// Next offset
index = index + 1
offset = PEEK offset_ptr[index]
WEND
FEND
// ============================================================================
// FUNC show_win_screen
// Main entry point - clears area and draws the win logo
// ============================================================================
FUNC show_win_screen
clear_win_area()
draw_win_logo()
FEND
LABEL __skip_lib_winscreen
#IFEND