A Rush Hour game made with Cakelisp
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1496 lines
61 KiB

(comptime-define-symbol 'Unix)
(comptime-cond ('No-Kitty-Main)
(true
(comptime-define-symbol 'Kitty-Main)))
3 years ago
;; Until GameLib is relocatable, we will build from it; All comptime paths in this file are relative
;; to Dependencies/gamelib!
;; (add-cakelisp-search-directory "Dependencies/gamelib/src")
;; (set-cakelisp-option cakelisp-src-dir "Dependencies/gamelib/Dependencies/cakelisp/src")
;; (add-cakelisp-search-directory "Dependencies/gamelib/Dependencies/cakelisp/runtime")
(add-cakelisp-search-directory "src" "Dependencies/cakelisp/runtime"
;; kitty-gridlock src
"../../src")
(import "SDL.cake" "Math.cake" ;; GameLib
"PuzzleIO.cake" ;; kitty-gridlock
;; "Decompression.cake" ;; kitty-gridlock ;; Not used for runtime game
&comptime-only "Macros.cake" "BuildTools.cake") ;; cakelisp/runtime
(c-import "<stdio.h>" "<stdlib.h>"
"SDL.h" "SDL_syswm.h" "SDL_timer.h" "SDL_render.h" "SDL_rwops.h"
;; For round()
"<math.h>")
;; This doesn't work for some reason. I had to override SDLActivity::setOrientationBis() instead
;; (undefine-constant SDL_HINT_ORIENTATIONS) ;; Silence warning
;; (define-constant SDL_HINT_ORIENTATIONS "Portrait")
3 years ago
;;
;; Constants
;;
;; (var debug-log-enabled bool true)
(var debug-log-enabled bool false)
;; (var g-show-fps bool true)
(var g-show-fps bool false)
;; TODO: Actually look at frame rate and adjust accordingly
;; This fixed GPU "coil whine" which I was getting running at 5400hz (completely unnecessary)
(var todo-arbitrary-delay-ms int 10)
3 years ago
(define-constant DATA_DIR "data/")
(defmacro in-data-dir (path-in-data string)
(tokenize-push output
(static-string-combine DATA_DIR (token-splice path-in-data)))
(return true))
3 years ago
;; Made for OnePlus 6T screen
(var g-window-width (const int) 1080)
(var g-window-height (const int) 2340)
(var g-window-safe-area-margin-px (const int) 40)
(defstruct-local grid-vec2
X int
Y int)
3 years ago
;; Game board constants
;; Note that these assume portrait aspect ratio by basing the size on the smaller dimension
(var g-game-board-outer-size-px (const int) (- g-window-width (* 2 g-window-safe-area-margin-px)))
(var g-game-board-outer-top-left-px (const vec2)
(array g-window-safe-area-margin-px (/ (- g-window-height g-game-board-outer-size-px) 2)))
(var g-game-board-grid-size (const int) 6)
(var g-game-board-win-cell (const grid-vec2) (array 5 2))
;; Use the nearest even pixel for pixel-perfect game board
(var g-game-board-margin-px (const int)
(/ (% g-game-board-outer-size-px g-game-board-grid-size) 2))
;; With margins of 2px, that becomes 996, or 166 pixels per column
(var g-game-board-cell-size-px (const int)
(/ (- g-game-board-outer-size-px g-game-board-margin-px) g-game-board-grid-size))
(var g-game-board-inner-top-left-px (const vec2)
(array (+ (vec-x g-game-board-outer-top-left-px) g-game-board-margin-px)
(+ (vec-y g-game-board-outer-top-left-px) g-game-board-margin-px)))
3 years ago
;;
;; Game board state
;;
(defun-local grid-vec2-add (a grid-vec2 b grid-vec2 &return grid-vec2)
(var result grid-vec2)
(set (vec-x result) (+ (vec-x a) (vec-x b)))
(set (vec-y result) (+ (vec-y a) (vec-y b)))
(return result))
3 years ago
(defstruct-local board-piece
grid-position grid-vec2 ;; Always from the top left
moving-position vec2 ;; Floating point for animation, smooth dragging
num-cells int ;; If 0, piece is empty
is-vertical bool
3 years ago
is-wall bool
is-primary-piece bool
label char)
3 years ago
;; Pieces are stored separately, then their positions inform occupied state, which is used to check
;; movement constraints. g-empty-cell = empty cell. Values are indices into g-game-board-pieces. Used to
;; easily pick from tap/click
(def-type-alias BoardPieceIndex char)
;; Previously, I used -1 as empty cell. On Android, it did not like that, and I'm not sure why
(var g-empty-cell char 127)
(var g-game-board-spatial-state ([] 6 ([] 6 BoardPieceIndex)) (array g-empty-cell))
3 years ago
;; Max pieces is determined as follows:
;; 6x6 grid = 36 squares
;; Minimum moveable piece size = 2 squares. Wall size = 1 square; max 2 walls
;; 36 squares / 2 (min piece size) = 13; if two walls, need 14
(var g-game-board-pieces ([] 14 board-piece) (array 0))
(defun-local game-board-reset-pieces ()
(memset g-game-board-pieces 0 (sizeof g-game-board-pieces)))
(defmacro on-each-board-piece (piece-pointer-name symbol piece-index-name symbol
&rest body any)
3 years ago
(tokenize-push
output
(var (token-splice piece-index-name) int 0)
(each-in-array
g-game-board-pieces (token-splice piece-index-name)
(var (token-splice piece-pointer-name) (* board-piece)
(addr (at (token-splice piece-index-name) g-game-board-pieces)))
(token-splice-rest body tokens)))
(return true))
(defmacro on-each-existing-board-piece (piece-pointer-name symbol piece-index-name symbol
&rest body any)
(tokenize-push
output
(on-each-board-piece
(token-splice piece-pointer-name) (token-splice piece-index-name)
(when (path (token-splice piece-pointer-name) > num-cells)
(token-splice-rest body tokens))))
3 years ago
(return true))
(defun-local game-board-print ()
(var row int 0)
(each-in-range
g-game-board-grid-size row
(each-in-range
g-game-board-grid-size column
(var index BoardPieceIndex (at row column g-game-board-spatial-state))
(if (and (>= index 0) (field (at index g-game-board-pieces) num-cells))
(scope
(var piece-in-cell (* board-piece) (addr (at index g-game-board-pieces)))
(var label char (path piece-in-cell > label))
(fprintf stderr "%c " (? label label '#')))
(fprintf stderr ". ")))
(fprintf stderr "\n")))
(defun-local print-board-piece (piece (* (const board-piece)) index int)
(SDL_Log "Piece %c ([%d] %p )
\n\tgrid-position %d %d
\n\tmoving-position %f %f
\n\tnum-cells %d
\n\tis-vertical %d
\n\tis-wall %d
\n\tis-primary-piece %d\n\n"
(path piece > label) index piece
(field (path piece > grid-position) X) (field (path piece > grid-position) Y)
(field (path piece > moving-position) X) (field (path piece > moving-position) Y)
(path piece > num-cells)
(path piece > is-vertical)
(path piece > is-wall)
(path piece > is-primary-piece)))
3 years ago
(defun-local is-within-grid (coordinate grid-vec2 &return bool)
(return (and (>= (vec-x coordinate) 0)
(>= (vec-y coordinate) 0)
(< (vec-x coordinate) g-game-board-grid-size)
(< (vec-y coordinate) g-game-board-grid-size))))
(defun-local game-board-sync-occupied-state (&return bool)
3 years ago
;; Zero out to make overlap validation easy
(memset g-game-board-spatial-state g-empty-cell
(sizeof g-game-board-spatial-state))
3 years ago
(on-each-existing-board-piece
piece piece-index
(when debug-log-enabled
(print-board-piece piece piece-index))
(var num-cells int (path piece > num-cells))
(scope ;; Validate that piece does not go off board
(unless (is-within-grid (path piece > grid-position))
(SDL_Log "error: Piece %p origin is off board! Must abort occupied state sync\n" piece)
(return false))
(if (path piece > is-vertical)
(block ;; Vertical
(when (> (+ num-cells (vec-y (path piece > grid-position))) g-game-board-grid-size)
(SDL_Log "error: Piece %p vertical is off board! Must abort occupied state sync\n" piece)
(return false)))
(block ;; Horizontal
(when (> (+ num-cells (vec-x (path piece > grid-position))) g-game-board-grid-size)
(SDL_Log "error: Piece %p horizontal is off board! Must abort occupied state sync\n" piece)
(return false)))))
(each-in-range
num-cells cell-offset
(var cell-to-set grid-vec2 (array 0))
(if (path piece > is-vertical)
(block ;; Vertical
(set (vec-x cell-to-set) (vec-x (path piece > grid-position)))
(set (vec-y cell-to-set) (+ cell-offset (vec-y (path piece > grid-position)))))
(block ;; Horizontal
(set (vec-x cell-to-set) (+ cell-offset (vec-x (path piece > grid-position))))
(set (vec-y cell-to-set) (vec-y (path piece > grid-position)))))
(var occupy-space-pointer (* BoardPieceIndex)
(addr (at (vec-y cell-to-set) (vec-x cell-to-set) g-game-board-spatial-state)))
(when (!= g-empty-cell (deref occupy-space-pointer))
(SDL_Log "error: Piece %d overlapping %d at (%d, %d)! Aborting\n"
piece-index (deref occupy-space-pointer)
(vec-xy cell-to-set))
(return false))
(set (deref occupy-space-pointer) piece-index)))
(when debug-log-enabled
(game-board-print))
(return true))
(defun-local game-board-piece-from-char (board-char char piece-cell-position grid-vec2)
(when (= board-char 'x') ;; Handle walls specially
(on-each-board-piece
piece piece-index
(unless (= 0 (field (deref piece) label))
(continue))
(set (field (deref piece) label) board-char)
(set (field (deref piece) grid-position) piece-cell-position)
(set (field (deref piece) is-wall) true)
(incr (field (deref piece) num-cells))
(break))
(return))
(on-each-board-piece
piece piece-index
(cond
((= board-char (field (deref piece) label)) ;; Existing piece
(cond ;; Determine direction
((> (vec-x piece-cell-position) (vec-x (field (deref piece) grid-position)))
(set (field (deref piece) is-vertical) false))
((> (vec-y piece-cell-position) (vec-y (field (deref piece) grid-position)))
(set (field (deref piece) is-vertical) true)))
(incr (field (deref piece) num-cells))
(break))
((= 0 (field (deref piece) label)) ;; New piece
(set (field (deref piece) label) board-char)
(set (field (deref piece) grid-position) piece-cell-position)
(set (field (deref piece) is-primary-piece) (= board-char 'A'))
(incr (field (deref piece) num-cells))
(break)))))
;; (The following text is copied from [[https://www.michaelfogleman.com/rush/][Michael Fogleman's Rush Hour database]]
;; ([[https://web.archive.org/web/20201101044241/https://www.michaelfogleman.com/rush/][archive.org]]),
;; for easier reference)
;; The database is a simple text file with just a few columns. There is one row for every valid
;; (solvable, minimal) cluster. The columns are: number of moves, board description, and cluster
;; size (number of reachable states).
;; The board description is a 36-character string representing the state of the unsolved board. It
;; is a 6x6 2D array in row-major order. The characters in the description follow these simple rules:
;; - o empty cell
;; - x wall (fixed obstacle)
;; - A primary piece (red car)
;; - B - Z all other pieces
;; I used lowercase ~o~ instead of periods ~.~ for the empty cells in the database so that the
;; entire board description can be selected with a double-click.
;; Example format:
;; 60 IBBxooIooLDDJAALooJoKEEMFFKooMGGHHHM 2332
(defun-local game-board-load (board-string (* (const char)) &return bool)
(game-board-reset-pieces)
(var board-char (* (const char)) board-string)
(while (< (- board-char board-string) 36)
(cond
((= (deref board-char) 'o')
(debug-log "empty\n")) ;; Empty space
(true ;; Wall or piece
(var char-index int (- board-char board-string))
(var piece-cell-position grid-vec2 (array (% char-index g-game-board-grid-size)
(/ char-index g-game-board-grid-size)))
(debug-log "[%d] '%c' = (%d %d)\n" char-index (deref board-char) (vec-xy piece-cell-position))
(game-board-piece-from-char (deref board-char) piece-cell-position)))
(incr board-char))
(game-board-sync-occupied-state)
(return true))
3 years ago
;; Loaded by PuzzleIO.cake
(defstruct puzzle-data
num-moves char
num-states int
board ([] 36 char))
(var-global g-puzzle-list (* puzzle-data) null)
(var-global g-num-puzzles int 0)
(var g-current-puzzle (* puzzle-data) null)
(defun-local game-piece-grid-position-to-screen-position (piece (* (const board-piece)) &return vec2)
(var piece-position vec2 (array (to-float (vec-x (path piece > grid-position)))
(to-float (vec-y (path piece > grid-position)))))
;; Transfer to pixel space
(set piece-position (vec2-multiply (array (repeat g-game-board-cell-size-px)) piece-position))
;; Add board offset
(set piece-position (vec2-add piece-position g-game-board-inner-top-left-px))
(return piece-position))
(defun-local game-piece-position-to-screen-position (piece (* (const board-piece)) &return vec2)
(var piece-position vec2 (game-piece-grid-position-to-screen-position piece))
;; Add in moving position
(set piece-position (vec2-add piece-position (path piece > moving-position)))
(return piece-position))
(defun-local game-piece-get-screen-size (piece (* (const board-piece)) &return vec2)
(var piece-grid-size vec2
(array
(? (path piece > is-vertical) 1.f (to-float (path piece > num-cells)))
(? (path piece > is-vertical) (to-float (path piece > num-cells)) 1.f)))
(var piece-size vec2 (array (repeat g-game-board-cell-size-px)))
(set piece-size (vec2-multiply piece-size piece-grid-size))
(return piece-size))
(defun-local is-within-game-piece (piece (* (const board-piece)) screen-point-to-test vec2 &return bool)
(var piece-top-left-px vec2 (game-piece-position-to-screen-position piece))
(var piece-bottom-right-px vec2 (game-piece-get-screen-size piece))
(set piece-bottom-right-px (vec2-add piece-top-left-px piece-bottom-right-px))
;; (SDL_Log "%f %f -> %f %f vs. (%f %f)\n" (vec-xy piece-top-left-px) (vec-xy piece-bottom-right-px)
;; (vec-xy screen-point-to-test))
(return
(and
(>= (vec-x screen-point-to-test) (vec-x piece-top-left-px))
(>= (vec-y screen-point-to-test) (vec-y piece-top-left-px))
(< (vec-x screen-point-to-test) (vec-x piece-bottom-right-px))
(< (vec-y screen-point-to-test) (vec-y piece-bottom-right-px)))))
(defun-local pick-game-piece-from-screen-position (position vec2 &return (* board-piece))
(var board-position vec2 (vec2-subtract position g-game-board-inner-top-left-px))
(var grid-position grid-vec2
(array (/ (type-cast (vec-x board-position) int) g-game-board-cell-size-px)
(/ (type-cast (vec-y board-position) int) g-game-board-cell-size-px)))
(unless (is-within-grid grid-position)
(return null))
(var piece-index char
(at (vec-y grid-position) (vec-x grid-position) g-game-board-spatial-state))
(unless (!= g-empty-cell piece-index)
(return null))
(var piece (* board-piece) (addr (at piece-index g-game-board-pieces)))
(unless (is-within-game-piece piece position)
(return null))
(return piece))
(defun-local clamp-within (moving float minimum float maximum float &return float)
(when (< moving minimum)
(return minimum))
(when (> moving maximum)
(return maximum))
(return moving))
(defun-local constrain-piece-moving-position (piece (* (const board-piece))
delta-position vec2 &return vec2)
(when (path piece > is-wall)
(return (array 0.f 0.f)))
(var constrained-position vec2 delta-position)
(if (path piece > is-vertical) ;; Constrain on axes
(set (vec-x constrained-position) 0.f)
(set (vec-y constrained-position) 0.f))
;; Constrain based on empty spaces
(var min-position grid-vec2 (array 0))
(var max-position grid-vec2 (array 0))
(var position-delta-increase grid-vec2 (array 0))
(var position-delta-decrease grid-vec2 (array 0))
(var nearest-adjacent-cell grid-vec2 (path piece > grid-position))
(var furthest-adjacent-cell grid-vec2 (path piece > grid-position))
(if (path piece > is-vertical)
(block
(set (vec-y position-delta-increase) 1)
(set (vec-y position-delta-decrease) -1)
(set (vec-y nearest-adjacent-cell) (- (vec-y nearest-adjacent-cell) 1))
(set (vec-y furthest-adjacent-cell)
(+ (vec-y (path piece > grid-position)) (path piece > num-cells))))
(block
(set (vec-x position-delta-increase) 1)
(set (vec-x position-delta-decrease) -1)
(set (vec-x nearest-adjacent-cell) (- (vec-x nearest-adjacent-cell) 1))
(set (vec-x furthest-adjacent-cell)
(+ (vec-x (path piece > grid-position)) (path piece > num-cells)))))
(defstruct scan-direction
start-position grid-vec2
delta grid-vec2
is-increasing bool)
(var directions ([] 2 scan-direction)
(array
(array furthest-adjacent-cell position-delta-increase true)
(array nearest-adjacent-cell position-delta-decrease false)))
(each-in-array
directions i
(var direction (* scan-direction) (addr (at i directions)))
(var scan-position grid-vec2 (path direction > start-position))
(while (is-within-grid scan-position)
(var space-index int (at (vec-y scan-position) (vec-x scan-position)
g-game-board-spatial-state))
(if (= g-empty-cell space-index)
(block
(if (path direction > is-increasing)
(set max-position (grid-vec2-add max-position (path direction > delta)))
(set min-position (grid-vec2-add min-position (path direction > delta))))
(set scan-position (grid-vec2-add scan-position (path direction > delta))))
;; Anything other than empty space can immediately break us out
(break))))
(var min-delta-px vec2 (array (* g-game-board-cell-size-px (to-float (vec-x min-position)))
(* g-game-board-cell-size-px (to-float (vec-y min-position)))))
(var max-delta-px vec2 (array (* g-game-board-cell-size-px (to-float (vec-x max-position)))
(* g-game-board-cell-size-px (to-float (vec-y max-position)))))
(if (path piece > is-vertical)
(set (vec-y constrained-position)
(clamp-within (vec-y constrained-position) (vec-y min-delta-px) (vec-y max-delta-px)))
(set (vec-x constrained-position)
(clamp-within (vec-x constrained-position) (vec-x min-delta-px) (vec-x max-delta-px))))
(return constrained-position))
(defun-local is-in-win-state (&return bool)
(var win-square-index BoardPieceIndex (at (vec-y g-game-board-win-cell)
(vec-x g-game-board-win-cell)
g-game-board-spatial-state))
(when (!= g-empty-cell win-square-index)
(var piece (* board-piece) (addr (at win-square-index g-game-board-pieces)))
(return (path piece > is-primary-piece)))
(return false))
(defstruct-local move-action
piece (* board-piece)
delta-position grid-vec2)
;; Circular buffer
(var g-action-buffer ([] 128 move-action) (array 0))
(var g-action-buffer-write-head (* move-action) g-action-buffer)
(defun-local reset-action-buffer ()
(memset g-action-buffer 0 (sizeof g-action-buffer)))
(var g-current-move-count int 0)
(var g-has-won-puzzle bool false)
(var g-num-puzzles-won int 0)
(defun-local make-action (piece (* board-piece) delta-position grid-vec2)
(set (path g-action-buffer-write-head > piece) piece)
(set (path g-action-buffer-write-head > delta-position) delta-position)
(incr g-action-buffer-write-head)
(when (>= (- g-action-buffer-write-head g-action-buffer) (array-size g-action-buffer))
(set g-action-buffer-write-head g-action-buffer))
(incr g-current-move-count))
(defun-local undo-action ()
(var action-buffer-read-head (* move-action) (- g-action-buffer-write-head 1))
(when (< action-buffer-read-head g-action-buffer)
(set action-buffer-read-head (+ g-action-buffer
(- (array-size g-action-buffer) 1))))
;; Nothing to do, the undo buffer is empty
(var piece (* board-piece) (path action-buffer-read-head > piece))
(when (= piece null)
(return))
;; Undo the action
(scope
(var invert-movement (const grid-vec2) (array -1 -1))
(var undo-movement grid-vec2 (path action-buffer-read-head > delta-position))
(set (vec-x undo-movement) (* (vec-x undo-movement) (vec-x invert-movement)))
(set (vec-y undo-movement) (* (vec-y undo-movement) (vec-y invert-movement)))
(set (path piece > grid-position) (grid-vec2-add (path piece > grid-position) undo-movement))
;; Because pieces tween back to their positions, we will add back in the undid position to the
;; move position for a nice transition
(var relative-movement vec2
(array
(* g-game-board-cell-size-px
(to-float (vec-x (path action-buffer-read-head > delta-position))))
(* g-game-board-cell-size-px
(to-float (vec-y (path action-buffer-read-head > delta-position))))))
(set (path piece > moving-position) (vec2-add (path piece > moving-position) relative-movement))
(decr g-current-move-count)
(game-board-sync-occupied-state))
(set (path action-buffer-read-head > piece) null)
(set (path action-buffer-read-head > delta-position) (array 0 0))
(set g-action-buffer-write-head action-buffer-read-head))
;; Future work: A puzzle list ID would facilitate expansion puzzle databases not ruining progression
(defstruct-local progression-puzzle
index int ;; into g-puzzle-list
is-solved bool)
(var g-progression-puzzles (* progression-puzzle) null)
(var g-num-progression-puzzles int 0)
(var g-current-progression-puzzle (* progression-puzzle) null)
(defun-local pick-random-progression-puzzle ()
(set g-current-progression-puzzle (addr (at (mod (rand) g-num-progression-puzzles)
g-progression-puzzles))))
(defun-local game-board-load-next-puzzle ()
(when g-num-progression-puzzles
(var g-previous-progression-puzzle (* progression-puzzle) g-current-progression-puzzle)
(pick-random-progression-puzzle)
(var num-attempts int 1)
;; Reasonable given how many puzzles someone might do in one sitting vs. total num puzzles
(var max-num-attempts int 200)
;; Don't try too hard when the data set is small
(when (< g-num-progression-puzzles max-num-attempts)
(set max-num-attempts (* 2 g-num-progression-puzzles)))
(while (or (= g-previous-progression-puzzle g-current-progression-puzzle) ;; Try to ensure change
(path g-current-progression-puzzle > is-solved)) ;; Only pick non-solved
(pick-random-progression-puzzle)
(incr num-attempts)
(when (= num-attempts max-num-attempts)
(SDL_Log "Max attempts reached for random puzzle! Has the app not been restarted in a long time?\n")
(break)))
(unless (< (path g-current-progression-puzzle > index) g-num-puzzles)
(SDL_Log "error: Progression puzzle index out of range!\n")
(return))
(set g-current-puzzle (addr (at (path g-current-progression-puzzle > index)
g-puzzle-list)))
(game-board-load (path g-current-puzzle > board)))
(set g-current-move-count 0)
(set g-has-won-puzzle false)
(reset-action-buffer))
(var g-progression-file-version (const int) 2)
(var g-progression-file-name ([] 512 char) (array 0))
(defun-local create-progression-puzzles ()
(when g-progression-puzzles (free (type-cast g-progression-puzzles (* void))))
(unless g-num-progression-puzzles
(SDL_Log "error: expected g-num-progression-puzzles to be set before create-progression-puzzles\n")
(return))
(set g-progression-puzzles
(type-cast (calloc g-num-progression-puzzles
(sizeof (type progression-puzzle))) (* progression-puzzle))))
;; If no previous progression is stored, we need to create the initial state from the puzzle
;; database. All puzzles are added and marked as unsolved
(defun-local initialize-progression-puzzles-from-database ()
(SDL_Log "Creating puzzle progression state from database\n")
(unless g-num-puzzles
(SDL_Log "error: no puzzles in database")
(return))
(set g-num-progression-puzzles g-num-puzzles)
(create-progression-puzzles)
(each-in-range g-num-progression-puzzles i
(set (field (at i g-progression-puzzles) index) i)
(set (field (at i g-progression-puzzles) is-solved) false)))
(defun-local set-progression-file-name ()
(if-c-preprocessor-defined
__ANDROID__
;; Must save in this directory unless you're okay with the file being nuked on app update
;; See e.g. http://www.dinomage.com/2013/05/howto-sdl-on-android-part-2-platform-details/
(snprintf g-progression-file-name (array-size g-progression-file-name)
"%s/progression.bin" (SDL_AndroidGetInternalStoragePath))
(snprintf g-progression-file-name (array-size g-progression-file-name)
"progression.bin")))
;; TODO: All of these reads and writes should check their error codes...
(defun-local write-progression-data ()
(set-progression-file-name)
(var progression-file (* SDL_RWops) (SDL_RWFromFile g-progression-file-name "w"))
(if progression-file
(scope
(SDL_WriteLE32 progression-file g-progression-file-version)
(SDL_WriteLE32 progression-file g-num-puzzles-won)
(SDL_WriteLE32 progression-file g-num-progression-puzzles)
;; Write all of them, even solved ones; read-progression-data will read them one-by-one and
;; remove solved puzzles
(SDL_RWwrite progression-file g-progression-puzzles (sizeof (type progression-puzzle))
g-num-progression-puzzles)
(SDL_RWclose progression-file))
(SDL_Log "warning: failed to save progression file\n")))
(defun-local read-progression-data ()
(set-progression-file-name)
(var progression-file (* SDL_RWops) (SDL_RWFromFile g-progression-file-name "r"))
(if progression-file
(scope
(var version int (SDL_ReadLE32 progression-file))
(unless (= version g-progression-file-version)
(SDL_Log "warning: failed to load progression file (version mismatch). Progress will be lost\n")
(initialize-progression-puzzles-from-database)
(SDL_RWclose progression-file)
(return))
(set g-num-puzzles-won (SDL_ReadLE32 progression-file))
;; Read the puzzles in, filtering out completed ones
(set g-num-progression-puzzles (SDL_ReadLE32 progression-file))
(create-progression-puzzles)
(var num-unsolved-puzzles int 0)
(each-in-range
g-num-progression-puzzles i
(var current-puzzle progression-puzzle)
(SDL_RWread progression-file (addr current-puzzle) (sizeof current-puzzle) 1)
(when (field current-puzzle is-solved)
(continue))
(set (at num-unsolved-puzzles g-progression-puzzles) current-puzzle)
(incr num-unsolved-puzzles))
;; Free empty space from all the solved puzzles last time the game was played
(when (and g-num-progression-puzzles
(!= g-num-progression-puzzles num-unsolved-puzzles))
(set g-progression-puzzles
(type-cast (realloc g-progression-puzzles
(* (sizeof (type progression-puzzle)) num-unsolved-puzzles))
(* progression-puzzle)))
(set g-num-progression-puzzles num-unsolved-puzzles))
(SDL_Log "%d unsolved puzzles remaining\n" g-num-progression-puzzles)
(SDL_RWclose progression-file))
(scope ;; File didn't exist or something
(SDL_Log "warning: failed to load progression file. This is fine if it's the first run\n")
(initialize-progression-puzzles-from-database))))
;;
;; UI (immediate-mode)
;;
(defstruct-local input-state
pointer-position vec2
is-pointer-pressed bool
start-pressed-position vec2
was-clicked bool) ;; Click on pointer release
(defun-local update-input-state (in-state (* input-state) mouse-position vec2 is-pressed bool)
(set (path in-state > pointer-position) mouse-position)
(var is-entering-press bool (and (not (path in-state > is-pointer-pressed))
is-pressed))
(var is-exiting-press bool (and (path in-state > is-pointer-pressed)
(not is-pressed)))
;; Clicks only count on release so you can move off a button mid-press to not do action
(set (path in-state > was-clicked) is-exiting-press)
(when is-entering-press
(set (path in-state > start-pressed-position) mouse-position))
(set (path in-state > is-pointer-pressed) is-pressed))
(defun-local is-within-aabb (point-to-test vec2 upper-left vec2 size vec2 &return bool)
(var bottom-right vec2 (vec2-add upper-left size))
(return
(and
(>= (vec-x point-to-test) (vec-x upper-left))
(>= (vec-y point-to-test) (vec-y upper-left))
(< (vec-x point-to-test) (vec-x bottom-right))
(< (vec-y point-to-test) (vec-y bottom-right)))))
(defun-local do-button (in-state (* input-state)
position vec2 size vec2
renderer (* SDL_Renderer)
texture (* SDL_Texture) ;; Assumes whole texture drawn
&return bool) ;; Returns whether clicked
(when (and renderer texture)
(var dest-rect SDL_Rect (array (vec-xy-to-int position)
(vec-xy-to-int size)))
(unless (= 0 (SDL_RenderCopy renderer texture null (addr dest-rect)))
(sdl-print-error)))
(return (and (path in-state > was-clicked)
(is-within-aabb (path in-state > pointer-position) position size)
;; So releasing on a button you didn't start pressing won't do action
(is-within-aabb (path in-state > start-pressed-position) position size))))
(defstruct-local font-glyph
symbol char
position SDL_Rect)
(var g-font-atlas ([] font-glyph)
(array
(array '1' (array 3 13 88 130))
(array '2' (array 108 13 97 129))
(array '3' (array 221 13 92 124))
(array '4' (array 329 21 79 125))
(array '5' (array 425 15 79 125))
(array '6' (array 513 16 82 118))
(array '7' (array 601 18 91 111))
(array '8' (array 692 13 92 115))
(array '9' (array 788 13 88 126))
(array '0' (array 894 17 83 116))
(array '/' (array 981 14 80 139))))
(defun-local pick-font-glyph-from-character (symbol char &return (* font-glyph))
(each-in-array
g-font-atlas i
(when (= symbol (field (at i g-font-atlas) symbol))
(return (addr (at i g-font-atlas)))))
(return null))
(defun-local draw-string (renderer (* SDL_Renderer) font-texture (* SDL_Texture)
text (* (const char)) position vec2)
(var current-position vec2 position)
(each-char-in-string-const
text current-char
(var glyph (* font-glyph) (pick-font-glyph-from-character (deref current-char)))
(unless glyph ;; Missing character
(continue))
(var dest-rect SDL_Rect (path glyph > position))
(var src-rect SDL_Rect (path glyph > position))
(set (field dest-rect x) (to-float (vec-x current-position)))
(set (field dest-rect y) (to-float (vec-y current-position)))
(unless (= 0 (SDL_RenderCopy renderer font-texture
(addr src-rect) (addr dest-rect)))
(sdl-print-error))
(set (vec-x current-position) (+ (vec-x current-position) (field src-rect w)))))
3 years ago
;;
;; Helpers
;;
(defun-local sdl-intialize-2d-renderer (renderer-out (* (* SDL_Renderer)) window (* SDL_Window)
&return bool)
(var num-render-drivers int (SDL_GetNumRenderDrivers))
(unless num-render-drivers
(return false))
(var i int 0)
(each-in-range
num-render-drivers i
(var driver-info SDL_RendererInfo (array 0))
(unless (= 0 (SDL_GetRenderDriverInfo i (addr driver-info)))
(return false))
(SDL_Log "Renderer [%d]: %s\n
\tHardware accelerated: %s\n
\tRender to texture: %s\n
\tMax texture width: %d\n
\tMax texture height: %d\n
\n"
i (field driver-info name)
(? (bit-and (field driver-info flags) SDL_RENDERER_ACCELERATED) "yes" "no")
(? (bit-and (field driver-info flags) SDL_RENDERER_TARGETTEXTURE) "yes" "no")
(field driver-info max_texture_width)
(field driver-info max_texture_height)))
(var macoy-beast-driver (const int) 0)
(var selected-renderer int macoy-beast-driver)
(SDL_Log "Using renderer %d\n" selected-renderer)
(set (deref renderer-out) (SDL_CreateRenderer window selected-renderer SDL_RENDERER_ACCELERATED))
(unless (deref renderer-out)