AtlatestRepositorysigil-studio
sigil-studio / tree / examplessolitaire.sgl
1
;;; solitaire.sgl - Cyberpunk Solitaire2
;;;3
;;; A keyboard-controlled Klondike solitaire game with cyberpunk aesthetics.4
;;;5
;;; Controls:6
;;; a s d f - Select tableau columns 1-47
;;; j k l - Select tableau columns 5-78
;;; w - Flip card from stock to waste9
;;; e - Select card from waste pile10
;;; u i o p - Select foundation piles 1-411
;;; Escape - Quit12
;;; n - New game14
(import (sigil core)15
(sigil math)16
(sigil app)17
(sigil graphics)18
(sigil graphics font))20
;; ============================================================21
;; CONSTANTS22
;; ============================================================24
;; Viewport size25
(define SCREEN-WIDTH 800)26
(define SCREEN-HEIGHT 600)28
;; Card dimensions29
(define CARD-WIDTH 60)30
(define CARD-HEIGHT 80)31
(define CARD-GAP 15)32
(define STACK-OFFSET 20) ;; Vertical offset for stacked cards34
;; Colors (cyberpunk theme)35
(define BG-R 0.04)36
(define BG-G 0.04)37
(define BG-B 0.10)39
(define CARD-BACK-R 0.1)40
(define CARD-BACK-G 0.1)41
(define CARD-BACK-B 0.15)43
(define CARD-FACE-R 0.12)44
(define CARD-FACE-G 0.12)45
(define CARD-FACE-B 0.14)47
(define NEON-CYAN-R 0.0)48
(define NEON-CYAN-G 1.0)49
(define NEON-CYAN-B 1.0)51
(define NEON-PINK-R 1.0)52
(define NEON-PINK-G 0.0)53
(define NEON-PINK-B 0.4)55
(define NEON-YELLOW-R 1.0)56
(define NEON-YELLOW-G 1.0)57
(define NEON-YELLOW-B 0.0)59
(define DIM-CYAN-R 0.0)60
(define DIM-CYAN-G 0.4)61
(define DIM-CYAN-B 0.4)63
;; ============================================================64
;; CARD DATA STRUCTURES65
;; ============================================================67
;; Card is a vector: #(suit rank face-up?)68
;; Suits: 0=hearts, 1=diamonds, 2=clubs, 3=spades69
;; Ranks: 1=Ace, 2-10, 11=Jack, 12=Queen, 13=King71
(define (make-card suit rank)72
(vector suit rank #f))74
(define (card-suit card) (vector-ref card 0))75
(define (card-rank card) (vector-ref card 1))76
(define (card-face-up? card) (vector-ref card 2))78
(define (card-set-face-up! card up?)79
(vector-set! card 2 up?))81
(define (card-flip! card)82
(vector-set! card 2 (not (card-face-up? card))))84
;; Red suits: hearts (0), diamonds (1)85
;; Black suits: clubs (2), spades (3)86
(define (card-red? card)87
(< (card-suit card) 2))89
(define (card-black? card)90
(>= (card-suit card) 2))92
(define (suit-symbol suit)93
(case suit94
((0) "H")95
((1) "D")96
((2) "C")97
((3) "S")98
(else "?")))100
(define (rank-symbol rank)101
(case rank102
((1) "A")103
((11) "J")104
((12) "Q")105
((13) "K")106
(else (number->string rank))))108
;; ============================================================109
;; DECK CREATION AND SHUFFLING110
;; ============================================================112
(define (make-deck)113
(let ((deck '()))114
(let suit-loop ((suit 0))115
(when (< suit 4)116
(let rank-loop ((rank 1))117
(when (< rank 14)118
(set! deck (cons (make-card suit rank) deck))119
(rank-loop (+ rank 1))))120
(suit-loop (+ suit 1))))121
deck))123
;; Fisher-Yates shuffle using a simple LCG random124
(define *random-state* 12345)126
(define (random-next)127
(set! *random-state*128
(modulo (+ (* *random-state* 1103515245) 12345) 2147483648))129
*random-state*)131
(define (random-int n)132
(modulo (random-next) n))134
(define (shuffle-list lst)135
(let ((vec (list->vector lst))136
(n (length lst)))137
(let loop ((i (- n 1)))138
(when (>= i 1)139
(let* ((j (random-int (+ i 1)))140
(tmp (vector-ref vec i)))141
(vector-set! vec i (vector-ref vec j))142
(vector-set! vec j tmp))143
(loop (- i 1))))144
(vector->list vec)))146
;; ============================================================147
;; GAME STATE148
;; ============================================================150
(define *stock* '()) ;; Face-down draw pile151
(define *waste* '()) ;; Face-up drawn cards152
(define *foundations* #f) ;; Vector of 4 foundation piles (lists)153
(define *tableau* #f) ;; Vector of 7 tableau columns (lists)155
;; Selection state156
(define *selected-source* #f) ;; 'stock, 'waste, 'foundation, 'tableau, or #f157
(define *selected-index* #f) ;; Which pile (0-3 for foundation, 0-6 for tableau)158
(define *selected-count* 1) ;; How many cards selected from tableau160
;; Font161
(define *font* #f)162
(define *font-small* #f)164
;; Messages165
(define *message* "")166
(define *message-timer* 0.0)168
;; ============================================================169
;; GAME INITIALIZATION170
;; ============================================================172
(define (init-game)173
;; Seed random with time174
(set! *random-state* (truncate (* (time-elapsed) 1000000)))176
;; Create and shuffle deck177
(let ((deck (shuffle-list (make-deck))))178
;; Initialize foundations (empty)179
(set! *foundations* (vector '() '() '() '()))181
;; Initialize tableau - deal 1,2,3,4,5,6,7 cards182
(set! *tableau* (vector '() '() '() '() '() '() '()))183
(let deal-loop ((col 0) (deck deck))184
(if (< col 7)185
(let ((cards (take deck (+ col 1)))186
(rest (drop deck (+ col 1))))187
;; Flip the top card face-up188
(card-set-face-up! (car (reverse cards)) #t)189
(vector-set! *tableau* col (reverse cards))190
(deal-loop (+ col 1) rest))191
;; Remaining cards go to stock192
(set! *stock* deck)))194
;; Clear waste and selection195
(set! *waste* '())196
(clear-selection)197
(show-message "New game started")))199
;; Helper: take first n elements200
(define (take lst n)201
(if (or (<= n 0) (null? lst))202
'()203
(cons (car lst) (take (cdr lst) (- n 1)))))205
;; Helper: drop first n elements206
(define (drop lst n)207
(if (or (<= n 0) (null? lst))208
lst209
(drop (cdr lst) (- n 1))))211
;; ============================================================212
;; SELECTION213
;; ============================================================215
(define (clear-selection)216
(set! *selected-source* #f)217
(set! *selected-index* #f)218
(set! *selected-count* 1))220
(define (select-tableau col)221
(let ((pile (vector-ref *tableau* col)))222
(if (null? pile)223
;; Empty pile - if we have a selection, try to move there224
(if *selected-source*225
(try-move-to-tableau col)226
(show-message "Empty column"))227
;; Non-empty pile228
(if (and (eq? *selected-source* 'tableau)229
(= *selected-index* col))230
;; Already selected this column - increase selection count231
(let ((face-up-count (count-face-up pile)))232
(if (< *selected-count* face-up-count)233
(set! *selected-count* (+ *selected-count* 1))234
(show-message "Can't select more cards")))235
;; New selection or moving here236
(if *selected-source*237
;; Try to move to this tableau238
(try-move-to-tableau col)239
;; New selection240
(begin241
(set! *selected-source* 'tableau)242
(set! *selected-index* col)243
(set! *selected-count* 1)))))))245
(define (select-foundation idx)246
(let ((pile (vector-ref *foundations* idx)))247
(if *selected-source*248
;; Try to move to foundation249
(try-move-to-foundation idx)250
;; Select from foundation (rarely useful, but allowed)251
(if (null? pile)252
(show-message "Empty foundation")253
(begin254
(set! *selected-source* 'foundation)255
(set! *selected-index* idx)256
(set! *selected-count* 1))))))258
(define (select-waste)259
(if (null? *waste*)260
(show-message "No cards in waste")261
(if *selected-source*262
;; Clear selection and select waste263
(begin264
(clear-selection)265
(set! *selected-source* 'waste)266
(set! *selected-index* 0)267
(set! *selected-count* 1))268
;; New selection from waste269
(begin270
(set! *selected-source* 'waste)271
(set! *selected-index* 0)272
(set! *selected-count* 1)))))274
(define (flip-stock)275
(clear-selection)276
(if (null? *stock*)277
;; Recycle waste back to stock278
(if (null? *waste*)279
(show-message "No cards to flip")280
(begin281
(set! *stock* (map (lambda (c)282
(card-set-face-up! c #f) c)283
(reverse *waste*)))284
(set! *waste* '())285
(show-message "Stock recycled")))286
;; Flip one card from stock to waste287
(let ((card (car *stock*)))288
(set! *stock* (cdr *stock*))289
(card-set-face-up! card #t)290
(set! *waste* (cons card *waste*)))))292
;; ============================================================293
;; MOVE VALIDATION AND EXECUTION294
;; ============================================================296
(define (count-face-up pile)297
(let loop ((pile pile) (count 0))298
(if (or (null? pile) (not (card-face-up? (car pile))))299
count300
(loop (cdr pile) (+ count 1)))))302
(define (get-selected-cards)303
(case *selected-source*304
((waste) (list (car *waste*)))305
((foundation) (list (car (vector-ref *foundations* *selected-index*))))306
((tableau)307
(take (vector-ref *tableau* *selected-index*) *selected-count*))308
(else '())))310
(define (can-place-on-tableau? cards target-pile)311
(if (null? cards)312
#f313
(let ((bottom-card (car (reverse cards))))314
(if (null? target-pile)315
;; Empty pile - only King can go there316
(= (card-rank bottom-card) 13)317
;; Must alternate color and be one less in rank318
(let ((top-card (car target-pile)))319
(and (card-face-up? top-card)320
(not (eq? (card-red? bottom-card) (card-red? top-card)))321
(= (card-rank bottom-card) (- (card-rank top-card) 1))))))))323
(define (can-place-on-foundation? card foundation-pile)324
(if (null? foundation-pile)325
;; Empty foundation - only Ace326
(= (card-rank card) 1)327
;; Must be same suit and one higher328
(let ((top-card (car foundation-pile)))329
(and (= (card-suit card) (card-suit top-card))330
(= (card-rank card) (+ (card-rank top-card) 1))))))332
(define (try-move-to-tableau target-col)333
(let ((cards (get-selected-cards))334
(target-pile (vector-ref *tableau* target-col)))335
(if (can-place-on-tableau? cards target-pile)336
(begin337
;; Remove cards from source338
(remove-selected-cards)339
;; Add to target340
(vector-set! *tableau* target-col (append cards target-pile))341
(clear-selection)342
(show-message "Moved!"))343
(begin344
(clear-selection)345
(show-message "Invalid move")))))347
(define (try-move-to-foundation target-idx)348
(let ((cards (get-selected-cards)))349
;; Can only move single card to foundation350
(if (not (= (length cards) 1))351
(begin352
(show-message "Only single cards to foundation")353
(clear-selection))354
(let ((card (car cards))355
(foundation-pile (vector-ref *foundations* target-idx)))356
(if (can-place-on-foundation? card foundation-pile)357
(begin358
(remove-selected-cards)359
(vector-set! *foundations* target-idx (cons card foundation-pile))360
(clear-selection)361
(check-win)362
(show-message "To foundation!"))363
(begin364
(clear-selection)365
(show-message "Invalid foundation move")))))))367
(define (remove-selected-cards)368
(case *selected-source*369
((waste)370
(set! *waste* (cdr *waste*)))371
((foundation)372
(let ((pile (vector-ref *foundations* *selected-index*)))373
(vector-set! *foundations* *selected-index* (cdr pile))))374
((tableau)375
(let ((pile (vector-ref *tableau* *selected-index*)))376
(vector-set! *tableau* *selected-index* (drop pile *selected-count*))377
;; Flip newly exposed card378
(let ((new-pile (vector-ref *tableau* *selected-index*)))379
(when (and (not (null? new-pile))380
(not (card-face-up? (car new-pile))))381
(card-set-face-up! (car new-pile) #t)))))))383
(define (check-win)384
(let ((total (+ (length (vector-ref *foundations* 0))385
(length (vector-ref *foundations* 1))386
(length (vector-ref *foundations* 2))387
(length (vector-ref *foundations* 3)))))388
(when (= total 52)389
(show-message "YOU WIN!"))))391
;; ============================================================392
;; MESSAGES393
;; ============================================================395
(define (show-message msg)396
(set! *message* msg)397
(set! *message-timer* 2.0))399
(define (update-message dt)400
(when (> *message-timer* 0)401
(set! *message-timer* (- *message-timer* dt))))403
;; ============================================================404
;; RENDERING405
;; ============================================================407
(define (draw-card-back x y selected?)408
;; Card background409
(set-color CARD-BACK-R CARD-BACK-G CARD-BACK-B)410
(draw-filled-rect x y CARD-WIDTH CARD-HEIGHT)411
;; Border412
(if selected?413
(set-color NEON-YELLOW-R NEON-YELLOW-G NEON-YELLOW-B)414
(set-color DIM-CYAN-R DIM-CYAN-G DIM-CYAN-B))415
(draw-rect x y CARD-WIDTH CARD-HEIGHT))417
(define (draw-card-face card x y selected?)418
;; Card background419
(set-color CARD-FACE-R CARD-FACE-G CARD-FACE-B)420
(draw-filled-rect x y CARD-WIDTH CARD-HEIGHT)422
;; Border color based on suit423
(if selected?424
(set-color NEON-YELLOW-R NEON-YELLOW-G NEON-YELLOW-B)425
(if (card-red? card)426
(set-color NEON-PINK-R NEON-PINK-G NEON-PINK-B)427
(set-color NEON-CYAN-R NEON-CYAN-G NEON-CYAN-B)))428
(draw-rect x y CARD-WIDTH CARD-HEIGHT)430
;; Draw rank and suit431
(when *font*432
(let ((rank-str (rank-symbol (card-rank card)))433
(suit-str (suit-symbol (card-suit card))))434
;; Suit color for text435
(if (card-red? card)436
(set-color NEON-PINK-R NEON-PINK-G NEON-PINK-B)437
(set-color NEON-CYAN-R NEON-CYAN-G NEON-CYAN-B))438
(draw-text *font* rank-str (+ x 5) (+ y 25))439
(draw-text *font* suit-str (+ x 5) (+ y 55)))))441
(define (draw-empty-slot x y label)442
(set-color DIM-CYAN-R DIM-CYAN-G DIM-CYAN-B 0.3)443
(draw-rect x y CARD-WIDTH CARD-HEIGHT)444
(when *font-small*445
(set-color DIM-CYAN-R DIM-CYAN-G DIM-CYAN-B)446
(draw-text *font-small* label (+ x 20) (+ y 45))))448
(define (draw-key-hint x y key)449
(when *font-small*450
(set-color DIM-CYAN-R DIM-CYAN-G DIM-CYAN-B)451
(draw-text *font-small* key x y)))453
(define (draw-stock)454
(let ((x 30) (y 30))455
;; Draw stock pile456
(if (null? *stock*)457
(draw-empty-slot x y "W")458
(draw-card-back x y #f))459
(draw-key-hint (+ x 25) (+ y CARD-HEIGHT 20) "W")))461
(define (draw-waste)462
(let ((x 30) (y (+ 30 CARD-HEIGHT CARD-GAP 30)))463
(if (null? *waste*)464
(draw-empty-slot x y "E")465
(draw-card-face (car *waste*) x y466
(eq? *selected-source* 'waste)))467
(draw-key-hint (+ x 25) (+ y CARD-HEIGHT 20) "E")))469
(define (draw-foundations)470
(let ((start-x 250)471
(y 30)472
(keys '("U" "I" "O" "P")))473
(let loop ((i 0))474
(when (< i 4)475
(let ((x (+ start-x (* i (+ CARD-WIDTH CARD-GAP))))476
(pile (vector-ref *foundations* i))477
(key (list-ref keys i)))478
(if (null? pile)479
(draw-empty-slot x y key)480
(draw-card-face (car pile) x y481
(and (eq? *selected-source* 'foundation)482
(= *selected-index* i))))483
(draw-key-hint (+ x 25) (+ y CARD-HEIGHT 20) key))484
(loop (+ i 1))))))486
(define (draw-tableau)487
(let ((start-x 50)488
(start-y 200)489
(keys '("A" "S" "D" "F" #f "J" "K" "L")))490
(let col-loop ((col 0))491
(when (< col 7)492
(let* ((x (+ start-x (* col (+ CARD-WIDTH CARD-GAP))))493
(pile (vector-ref *tableau* col))494
(key (list-ref keys (if (< col 4) col (+ col 1))))495
(is-selected (and (eq? *selected-source* 'tableau)496
(= *selected-index* col))))497
;; Draw empty slot or cards498
(if (null? pile)499
(draw-empty-slot x start-y (or key ""))500
;; Draw stack of cards501
(let ((pile-rev (reverse pile))502
(count (length pile)))503
(let card-loop ((i 0) (cards pile-rev))504
(when (not (null? cards))505
(let* ((card (car cards))506
(card-y (+ start-y (* i STACK-OFFSET)))507
(card-idx (- count i 1))508
(selected? (and is-selected509
(< card-idx *selected-count*))))510
(if (card-face-up? card)511
(draw-card-face card x card-y selected?)512
(draw-card-back x card-y #f)))513
(card-loop (+ i 1) (cdr cards))))))514
;; Key hint515
(when key516
(draw-key-hint (+ x 25) (+ start-y (* 8 STACK-OFFSET) 30) key)))517
(col-loop (+ col 1))))))519
(define (draw-message)520
(when (and *font* (> *message-timer* 0))521
(let ((alpha (min 1.0 *message-timer*)))522
(set-color 1.0 1.0 1.0 alpha)523
(draw-text *font* *message* 300 560))))525
(define (draw-game)526
(draw-stock)527
(draw-waste)528
(draw-foundations)529
(draw-tableau)530
(draw-message))532
;; ============================================================533
;; INPUT HANDLING534
;; ============================================================536
(define (handle-input)537
;; Quit538
(when (key-pressed? 'escape)539
(request-quit))541
;; New game542
(when (key-pressed? 'n)543
(init-game))545
;; Stock546
(when (key-pressed? 'w)547
(flip-stock))549
;; Waste550
(when (key-pressed? 'e)551
(select-waste))553
;; Foundations554
(when (key-pressed? 'u) (select-foundation 0))555
(when (key-pressed? 'i) (select-foundation 1))556
(when (key-pressed? 'o) (select-foundation 2))557
(when (key-pressed? 'p) (select-foundation 3))559
;; Tableau560
(when (key-pressed? 'a) (select-tableau 0))561
(when (key-pressed? 's) (select-tableau 1))562
(when (key-pressed? 'd) (select-tableau 2))563
(when (key-pressed? 'f) (select-tableau 3))564
(when (key-pressed? 'j) (select-tableau 4))565
(when (key-pressed? 'k) (select-tableau 5))566
(when (key-pressed? 'l) (select-tableau 6)))568
;; ============================================================569
;; MAIN570
;; ============================================================572
(define (on-init)573
(gfx-setup)574
(set-viewport SCREEN-WIDTH SCREEN-HEIGHT)575
(set-letterbox-color 0.02 0.02 0.05)577
;; Load font (use test font for now)578
(set! *font* (load-font "packages/sigil-studio/test/Saucer.ttf" 24))579
(set! *font-small* (load-font "packages/sigil-studio/test/Saucer.ttf" 16))581
(init-game))583
(define (on-frame)584
(let ((dt (frame-time)))585
(update-message dt)586
(handle-input)588
(begin-frame)589
(clear-screen BG-R BG-G BG-B)590
(draw-game)591
(end-frame)))593
(define (on-cleanup)594
(display "Thanks for playing!\n"))596
(app-run on-init on-frame on-cleanup "Cyberpunk Solitaire" 800 600)