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.
1495 lines
61 KiB
1495 lines
61 KiB
(comptime-define-symbol 'Unix)
|
|
(comptime-cond ('No-Kitty-Main)
|
|
(true
|
|
(comptime-define-symbol 'Kitty-Main)))
|
|
|
|
;; 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")
|
|
|
|
;;
|
|
;; 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)
|
|
|
|
(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))
|
|
|
|
;; 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)
|
|
|
|
;; 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)))
|
|
|
|
|
|
;;
|
|
;; 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))
|
|
|
|
(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
|
|
|
|
is-wall bool
|
|
is-primary-piece bool
|
|
|
|
label char)
|
|
|
|
;; 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))
|
|
|
|
;; 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)
|
|
(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))))
|
|
(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)))
|
|
|
|
(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)
|
|
;; Zero out to make overlap validation easy
|
|
(memset g-game-board-spatial-state g-empty-cell
|
|
(sizeof g-game-board-spatial-state))
|
|
(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))
|
|
|
|
;; 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)))))
|
|
|
|
;;
|
|
;; 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)
|
|
(sdl-print-error)
|
|
(return false))
|
|
(return true))
|
|
|
|
(defun-local sdl-texture-from-bmp (filename (* (const char)) renderer (* SDL_Renderer)
|
|
&return (* SDL_Texture))
|
|
(var surface (* SDL_Surface) (SDL_LoadBMP filename))
|
|
(unless surface
|
|
(SDL_Log "Failed to load surface from BMP %s\n" filename)
|
|
(sdl-print-error)
|
|
(return null))
|
|
|
|
(var texture (* SDL_Texture)
|
|
(SDL_CreateTextureFromSurface renderer surface))
|
|
;; No need to hold on to surface after texture has been created
|
|
(SDL_FreeSurface surface)
|
|
(unless texture (sdl-print-error))
|
|
(return texture))
|
|
|
|
(defun-local sdl-texture-from-bmp-color-to-transparent
|
|
(filename (* (const char)) renderer (* SDL_Renderer) r char g char b char
|
|
&return (* SDL_Texture))
|
|
(var surface (* SDL_Surface) (SDL_LoadBMP filename))
|
|
(unless surface
|
|
(SDL_Log "Failed to load surface from BMP %s\n" filename)
|
|
(sdl-print-error)
|
|
(return null))
|
|
|
|
(SDL_SetColorKey surface SDL_TRUE (SDL_MapRGB (path surface > format) r g b))
|
|
|
|
(var texture (* SDL_Texture)
|
|
(SDL_CreateTextureFromSurface renderer surface))
|
|
;; No need to hold on to surface after texture has been created
|
|
(SDL_FreeSurface surface)
|
|
(unless texture (sdl-print-error))
|
|
(return texture))
|
|
|
|
(defun-local sdl-print-time-delta (start-num-perf-ticks Uint64 label (* (const char)))
|
|
(var performance-num-ticks-per-second (const Uint64) (SDL_GetPerformanceFrequency))
|
|
|
|
(var current-counter-ticks Uint64 (SDL_GetPerformanceCounter))
|
|
(var frame-diff-ticks Uint64 (- current-counter-ticks start-num-perf-ticks))
|
|
(var delta-time float (/ frame-diff-ticks
|
|
(to-float performance-num-ticks-per-second)))
|
|
|
|
(SDL_Log "--- %s at %f seconds\n" label delta-time))
|
|
|
|
;; Factor 0 to 1
|
|
(defun-local vec2-interpolate (factor float from vec2 to vec2 &return vec2)
|
|
(return (array
|
|
(interpolate-range (vec-x from) (vec-x to)
|
|
0.f 1.f factor)
|
|
(interpolate-range (vec-y from) (vec-y to)
|
|
0.f 1.f factor))))
|
|
|
|
(defun-local vec2-is-zero (vec vec2 &return bool)
|
|
(return (and (= 0.f (vec-x vec))
|
|
(= 0.f (vec-y vec)))))
|
|
|
|
;;
|
|
;; Main
|
|
;;
|
|
|
|
;; On return null, play the game, else, exit for returned reason
|
|
(defun-local do-main-menu (renderer (* SDL_Renderer)
|
|
main-menu-texture (* SDL_Texture)
|
|
&return (* (const char)))
|
|
(var exit-reason (* (const char)) null)
|
|
(var in-state input-state (array 0))
|
|
(while (not exit-reason)
|
|
(var event SDL_Event)
|
|
(while (SDL_PollEvent (addr event))
|
|
(when (= (field event type) SDL_QUIT)
|
|
(set exit-reason "Window event")))
|
|
(var currentKeyStates (* (const Uint8)) (SDL_GetKeyboardState null))
|
|
(when (at SDL_SCANCODE_ESCAPE currentKeyStates)
|
|
(set exit-reason "Escape pressed"))
|
|
|
|
(unless (= 0 (SDL_RenderCopy renderer main-menu-texture
|
|
null null))
|
|
(sdl-print-error)
|
|
(set exit-reason "Render error"))
|
|
|
|
(var mouse-x int 0)
|
|
(var mouse-y int 0)
|
|
(var mouse-button-state Uint32 (SDL_GetMouseState (addr mouse-x) (addr mouse-y)))
|
|
(when (bit-and mouse-button-state SDL_BUTTON_LMASK)
|
|
;; Play the game!
|
|
(return null))
|
|
|
|
(SDL_RenderPresent renderer)
|
|
(SDL_Delay todo-arbitrary-delay-ms))
|
|
(return exit-reason))
|
|
|
|
(comptime-cond
|
|
('Kitty-Main
|
|
(defun main (num-args int args ([] (* char)) &return int)
|
|
(SDL_Log "Kitty Gridlock\n\n
|
|
Created by Macoy Madson <macoy@macoy.me>.\n
|
|
https://macoy.me/code/macoy/kitty-gridlock\n
|
|
Copyright (c) 2021 Macoy Madson.\n
|
|
Licensed under GPL-3.0-or-later.\n
|
|
Rush Hour database from Michael Fogleman.\n\n")
|
|
|
|
;; Useful reference for creating assets
|
|
(debug-log "Window and board dimensions:\n
|
|
\twindow width: %d\n
|
|
\twindow height: %d\n
|
|
\twindow safe area margin px: %d\n
|
|
\tgame board outer size px: %d\n
|
|
\tgame board outer top left px: %.2f %.2f\n
|
|
\tgame board grid size: %d\n
|
|
\tgame board margin px: %d\n
|
|
\tgame board cell size px: %d\n
|
|
\tgame board inner top left px: %.2f %.2f\n\n"
|
|
g-window-width
|
|
g-window-height
|
|
g-window-safe-area-margin-px
|
|
g-game-board-outer-size-px
|
|
(vec-xy g-game-board-outer-top-left-px)
|
|
g-game-board-grid-size
|
|
g-game-board-margin-px
|
|
g-game-board-cell-size-px
|
|
(vec-xy g-game-board-inner-top-left-px))
|
|
|
|
;;
|
|
;; Initialization
|
|
;;
|
|
|
|
(var window (* SDL_Window) null)
|
|
(unless (sdl-initialize-for-2d (addr window) "Kitty Gridlock"
|
|
g-window-width g-window-height)
|
|
(return 1))
|
|
|
|
(var performance-num-ticks-per-second (const Uint64) (SDL_GetPerformanceFrequency))
|
|
(var start-load-ticks Uint64 (SDL_GetPerformanceCounter))
|
|
|
|
(var renderer (* SDL_Renderer) null)
|
|
(unless (sdl-intialize-2d-renderer (addr renderer) window) (return 1))
|
|
|
|
;; Scale the render to fit the window
|
|
(SDL_RenderSetLogicalSize renderer g-window-width g-window-height)
|
|
|
|
(scope ;; Show loading screen
|
|
(var loading-texture (* SDL_Texture) (sdl-texture-from-bmp (in-data-dir "Loading.bmp")
|
|
renderer))
|
|
(unless loading-texture (return 1))
|
|
(SDL_RenderClear renderer)
|
|
(var loading-width int (* 2 200)) ;; Scale it up a bit
|
|
(var loading-height int (* 2 80))
|
|
(var loading-rect SDL_Rect (array (/ (- g-window-width loading-width) 2)
|
|
(/ (- g-window-height loading-height) 2)
|
|
loading-width loading-height))
|
|
(unless (= 0 (SDL_RenderCopy renderer loading-texture null (addr loading-rect)))
|
|
(sdl-print-error)
|
|
(return 1))
|
|
(SDL_RenderPresent renderer)
|
|
(SDL_DestroyTexture loading-texture))
|
|
|
|
(sdl-print-time-delta start-load-ticks "Loading screen displayed")
|
|
|
|
(unless (read-puzzles-binary (in-data-dir "puzzles.bin")) (return 1))
|
|
(sdl-print-time-delta start-load-ticks "Puzzles loaded")
|
|
|
|
(read-progression-data)
|
|
(sdl-print-time-delta start-load-ticks "Progression loaded")
|
|
|
|
(var main-menu-texture (* SDL_Texture)
|
|
(sdl-texture-from-bmp (in-data-dir "MainMenu.bmp") renderer))
|
|
(unless main-menu-texture (return 1))
|
|
|
|
(var background-texture (* SDL_Texture) (sdl-texture-from-bmp (in-data-dir "Board.bmp")
|
|
renderer))
|
|
(unless background-texture (return 1))
|
|
(var background-night-texture (* SDL_Texture) (sdl-texture-from-bmp (in-data-dir "Board_Night.bmp")
|
|
renderer))
|
|
(unless background-night-texture (return 1))
|
|
|
|
(var pieces-texture (* SDL_Texture)
|
|
(sdl-texture-from-bmp-color-to-transparent
|
|
(in-data-dir "Pieces.bmp")
|
|
renderer
|
|
0xff 0x00 0xd3))
|
|
(unless pieces-texture (return 1))
|
|
;; Texture atlas
|
|
(var piece-primary-origin (const vec2) (array 334.f 525.f))
|
|
(var piece-2-1-origin (const vec2) (array 534.f 686.f))
|
|
(var piece-3-1-origin (const vec2) (array 516.f 841.f))
|
|
(var piece-1-2-origin (const vec2) (array 853.f 519.f))
|
|
(var piece-1-3-origin (const vec2) (array 843.f 9.f))
|
|
(var wall-a-origin (const vec2) (array 674.f 507.f))
|
|
(var wall-b-origin (const vec2) (array 342.f 682.f))
|
|
|
|
;; Why doesn't this work?
|
|
;; (var win-texture (* SDL_Texture)
|
|
;; (sdl-texture-from-bmp-color-to-transparent
|
|
;; (in-data-dir "Win.bmp")
|
|
;; renderer
|
|
;; 0xff 0x00 0xd3))
|
|
(var win-texture (* SDL_Texture)
|
|
(sdl-texture-from-bmp
|
|
(in-data-dir "Win.bmp") renderer))
|
|
(unless win-texture (return 1))
|
|
|
|
(var theme-button-texture (* SDL_Texture)
|
|
(sdl-texture-from-bmp
|
|
(in-data-dir "Theme_Button.bmp") renderer))
|
|
(unless theme-button-texture (return 1))
|
|
(var undo-button-texture (* SDL_Texture)
|
|
(sdl-texture-from-bmp
|
|
(in-data-dir "Undo_Button.bmp") renderer))
|
|
(unless undo-button-texture (return 1))
|
|
(var next-button-texture (* SDL_Texture)
|
|
(sdl-texture-from-bmp
|
|
(in-data-dir "Next_Button.bmp") renderer))
|
|
(unless next-button-texture (return 1))
|
|
|
|
(var font-texture (* SDL_Texture)
|
|
(sdl-texture-from-bmp
|
|
(in-data-dir "Font.bmp") renderer))
|
|
(unless font-texture (return 1))
|
|
|
|
(var textures-to-destroy ([] (* SDL_Texture))
|
|
(array main-menu-texture
|
|
background-texture
|
|
pieces-texture
|
|
win-texture
|
|
theme-button-texture
|
|
undo-button-texture
|
|
next-button-texture
|
|
font-texture))
|
|
|
|
(sdl-print-time-delta start-load-ticks "Textures loaded")
|
|
|
|
(srand (type-cast (SDL_GetPerformanceCounter) int))
|
|
|
|
;; (game-board-load "IBBxooIooLDDJAALooJoKEEMFFKooMGGHHHM")
|
|
(game-board-load-next-puzzle)
|
|
|
|
;;
|
|
;; Game loop
|
|
;;
|
|
(var delta-time float 0.016f) ;; Made up but reasonable frame time
|
|
(var last-frame-perf-count Uint64 (* delta-time performance-num-ticks-per-second))
|
|
(var recent-n-perf-counts ([] 10 Uint64) (array 0))
|
|
(var recent-n-perf-counts-write-head int 0)
|
|
|
|
(var is-day-mode bool false)
|
|
(var selected-piece (* board-piece) null)
|
|
(var selection-start-position vec2 (array 0))
|
|
|
|
(var in-state input-state (array 0))
|
|
|
|
(sdl-print-time-delta start-load-ticks "Game loop starting")
|
|
|
|
(var exit-reason (* (const char)) null)
|
|
|
|
(set exit-reason (do-main-menu renderer main-menu-texture))
|
|
|
|
(while (not exit-reason)
|
|
(var event SDL_Event)
|
|
(while (SDL_PollEvent (addr event))
|
|
(when (= (field event type) SDL_QUIT)
|
|
(set exit-reason "Window event")))
|
|
(var currentKeyStates (* (const Uint8)) (SDL_GetKeyboardState null))
|
|
(when (at SDL_SCANCODE_ESCAPE currentKeyStates)
|
|
(set exit-reason "Escape pressed"))
|
|
|
|
(var mouse-x int 0)
|
|
(var mouse-y int 0)
|
|
(var mouse-button-state Uint32 (SDL_GetMouseState (addr mouse-x) (addr mouse-y)))
|
|
(var mouse-position vec2 (array (to-float mouse-x) (to-float mouse-y)))
|
|
(scope ;; Scale mouse position to match logical rendering positions
|
|
(var window-width int 1)
|
|
(var window-height int 1)
|
|
(SDL_GetWindowSize window (addr window-width) (addr window-height))
|
|
(var logical-width int 1)
|
|
(var logical-height int 1)
|
|
(SDL_RenderGetLogicalSize renderer (addr logical-width) (addr logical-height))
|
|
(when (or (= 0 window-width)
|
|
(= 0 window-height)
|
|
(= 0 logical-width)
|
|
(= 0 logical-height))
|
|
(set exit-reason "error: window or logical dimension will cause divide by zero")
|
|
(break))
|
|
(var to-logical-scale vec2
|
|
(array
|
|
(/ (to-float logical-width) window-width)
|
|
(/ (to-float logical-height) window-height)))
|
|
;; (SDL_Log "Window scale %f %f\n" (vec-xy to-logical-scale))
|
|
(var logical-aspect-ratio float (/ logical-width (to-float logical-height)))
|
|
(var window-aspect-ratio float (/ window-width (to-float window-height)))
|
|
(if (> window-aspect-ratio logical-aspect-ratio) ;; Remove centering for correct scaling
|
|
(block
|
|
(var total-margin float
|
|
(- window-width (* window-width (/ logical-aspect-ratio window-aspect-ratio))))
|
|
(set (vec-x to-logical-scale) (/ logical-width (- window-width total-margin)))
|
|
(var decentered float
|
|
(/ total-margin
|
|
2.f))
|
|
(set (vec-x mouse-position)
|
|
(- (vec-x mouse-position)
|
|
decentered)))
|
|
(block
|
|
(var total-margin float
|
|
(- window-height (* window-height (/ window-aspect-ratio logical-aspect-ratio))))
|
|
(set (vec-y to-logical-scale) (/ logical-height (- window-height total-margin)))
|
|
(var decentered float
|
|
(/ total-margin
|
|
2.f))
|
|
(set (vec-y mouse-position)
|
|
(- (vec-y mouse-position)
|
|
decentered))))
|
|
(set mouse-position (vec2-multiply mouse-position to-logical-scale)))
|
|
|
|
(update-input-state (addr in-state) mouse-position
|
|
(bit-and mouse-button-state SDL_BUTTON_LMASK))
|
|
|
|
(if (bit-and mouse-button-state SDL_BUTTON_LMASK)
|
|
(block
|
|
(unless selected-piece ;; If holding, allow dragging even when not exactly on piece
|
|
(set selection-start-position mouse-position)
|
|
(set selected-piece (pick-game-piece-from-screen-position mouse-position))))
|
|
(when selected-piece
|
|
;; Selection released; let block finish move and update grid
|
|
(var delta-grid-movement grid-vec2
|
|
(array
|
|
(type-cast
|
|
(round
|
|
(/ (vec-x (path selected-piece > moving-position))
|
|
g-game-board-cell-size-px)) int)
|
|
(type-cast
|
|
(round
|
|
(/ (vec-y (path selected-piece > moving-position))
|
|
g-game-board-cell-size-px)) int)))
|
|
(debug-log "%f %f becomes %d %d\n"
|
|
(vec-x (path selected-piece > moving-position))
|
|
(vec-y (path selected-piece > moving-position))
|
|
(vec-xy delta-grid-movement))
|
|
(var new-position grid-vec2
|
|
(grid-vec2-add (path selected-piece > grid-position) delta-grid-movement))
|
|
(unless (is-within-grid new-position)
|
|
(set exit-reason "Piece movement constraints failed to keep piece on board")
|
|
(break))
|
|
(when (or (!= 0 (vec-x delta-grid-movement))
|
|
(!= 0 (vec-y delta-grid-movement)))
|
|
(make-action selected-piece delta-grid-movement)
|
|
(set (path selected-piece > grid-position) new-position)
|
|
;; Remove the moving position difference from changing grid position, b/c moving is relative
|
|
(set (vec-x (path selected-piece > moving-position))
|
|
(- (vec-x (path selected-piece > moving-position))
|
|
(* g-game-board-cell-size-px (vec-x delta-grid-movement))))
|
|
(set (vec-y (path selected-piece > moving-position))
|
|
(- (vec-y (path selected-piece > moving-position))
|
|
(* g-game-board-cell-size-px (vec-y delta-grid-movement))))
|
|
(game-board-sync-occupied-state))
|
|
(set selected-piece nullptr)))
|
|
|
|
(SDL_RenderClear renderer)
|
|
|
|
(unless (= 0 (SDL_RenderCopy renderer (? is-day-mode background-texture background-night-texture)
|
|
null null))
|
|
(sdl-print-error)
|
|
(set exit-reason "Render error"))
|
|
|
|
(scope ;; UI
|
|
(when (do-button (addr in-state) (array 10.f 10.f) (array 166.f 166.f)
|
|
renderer theme-button-texture)
|
|
(set is-day-mode (not is-day-mode)))
|
|
(when (do-button (addr in-state) (array 190.f 1900.f) (array 166.f 166.f)
|
|
renderer undo-button-texture)
|
|
(unless selected-piece ;; Don't allow undo while also moving another piece
|
|
(undo-action)))
|
|
(when (do-button (addr in-state) (array 724.f 1900.f) (array 166.f 166.f)
|
|
renderer next-button-texture)
|
|
(game-board-load-next-puzzle)))
|
|
|
|
(on-each-existing-board-piece ;; Draw pieces
|
|
piece piece-index
|
|
(if (= selected-piece piece)
|
|
(scope
|
|
(var piece-position vec2 (game-piece-grid-position-to-screen-position piece))
|
|
(var mouse-delta-pos vec2 (vec2-subtract mouse-position selection-start-position))
|
|
(set (path piece > moving-position)
|
|
(constrain-piece-moving-position piece mouse-delta-pos)))
|
|
;; Reset positions on release
|
|
(when (not (vec2-is-zero (path piece > moving-position)))
|
|
(var ease-position vec2 (vec2-interpolate
|
|
(/ delta-time 0.1f)
|
|
(path piece > moving-position) (array 0.f 0.f)))
|
|
;; Make sure it's not always approaching smaller and smaller values
|
|
(when (< (fabs (vec-x ease-position)) 0.001f)
|
|
(set (vec-x ease-position) 0.f))
|
|
(when (< (fabs (vec-y ease-position)) 0.001f)
|
|
(set (vec-y ease-position) 0.f))
|
|
(set (path piece > moving-position) ease-position)))
|
|
|
|
(var piece-position vec2 (game-piece-position-to-screen-position piece))
|
|
(var piece-size vec2 (game-piece-get-screen-size piece))
|
|
|
|
(var selected-piece-origin vec2 piece-2-1-origin)
|
|
(cond ((path piece > is-primary-piece) ;; Pick atlas texture coords for piece
|
|
(set selected-piece-origin piece-primary-origin))
|
|
((path piece > is-wall)
|
|
(set selected-piece-origin wall-a-origin))
|
|
((and (path piece > is-vertical) (= 3 (path piece > num-cells)))
|
|
(set selected-piece-origin piece-1-3-origin))
|
|
((and (path piece > is-vertical) (= 2 (path piece > num-cells)))
|
|
(set selected-piece-origin piece-1-2-origin))
|
|
((and (not (path piece > is-vertical)) (= 2 (path piece > num-cells)))
|
|
(set selected-piece-origin piece-2-1-origin))
|
|
((and (not (path piece > is-vertical)) (= 3 (path piece > num-cells)))
|
|
(set selected-piece-origin piece-3-1-origin)))
|
|
|
|
(var dest-rect SDL_Rect (array (vec-xy-to-int piece-position)
|
|
(vec-xy-to-int piece-size)))
|
|
(var src-rect SDL_Rect (array (vec-xy-to-int selected-piece-origin)
|
|
(vec-xy-to-int piece-size)))
|
|
(unless (= 0 (SDL_RenderCopy renderer pieces-texture
|
|
(addr src-rect) (addr dest-rect)))
|
|
(sdl-print-error)
|
|
(set exit-reason "Render error")))
|
|
|
|
;; Turn count
|
|
(draw-formatted-string (array 570.f 15.f) "%d/%d" g-current-move-count
|
|
(? g-current-puzzle (path g-current-puzzle > num-moves) 0))
|
|
|
|
(draw-formatted-string (array 190.f 15.f) "%d" g-num-puzzles-won)
|
|
|
|
(when g-show-fps ;; Frame rate
|
|
(draw-formatted-string (array 700.f 2100.f) "%d" (type-cast (/ 1.f delta-time) int)))
|
|
|
|
(when (is-in-win-state)
|
|
(unless g-has-won-puzzle
|
|
(set g-has-won-puzzle true)
|
|
(incr g-num-puzzles-won)
|
|
(when g-current-progression-puzzle
|
|
(set (path g-current-progression-puzzle > is-solved) true))
|
|
(write-progression-data))
|
|
(var dest-rect SDL_Rect (array 60 140 960 500))
|
|
(unless (= 0 (SDL_RenderCopy renderer win-texture null (addr dest-rect)))
|
|
(sdl-print-error)
|
|
(set exit-reason "Render error")))
|
|
|
|
(SDL_RenderPresent renderer)
|
|
|
|
(var current-counter-ticks Uint64 (SDL_GetPerformanceCounter))
|
|
(var frame-diff-ticks Uint64 (- current-counter-ticks last-frame-perf-count))
|
|
(set last-frame-perf-count current-counter-ticks)
|
|
|
|
(set (at recent-n-perf-counts-write-head recent-n-perf-counts)
|
|
frame-diff-ticks)
|
|
(incr recent-n-perf-counts-write-head)
|
|
(when (>= recent-n-perf-counts-write-head (array-size recent-n-perf-counts))
|
|
(set recent-n-perf-counts-write-head 0))
|
|
|
|
(set delta-time (/ frame-diff-ticks
|
|
(to-float performance-num-ticks-per-second)))
|
|
;; (SDL_Log "%lu %f %fhz\n" frame-diff-ticks delta-time (/ 1.f delta-time))
|
|
(SDL_Delay todo-arbitrary-delay-ms))
|
|
|
|
(scope ;; Frame time
|
|
(var num-timings int (array-size recent-n-perf-counts))
|
|
(SDL_Log "Recent %d frame timings (fixed sleep of %d ms):\n" num-timings todo-arbitrary-delay-ms)
|
|
(each-in-array
|
|
recent-n-perf-counts i
|
|
(var delta-time float (/ (at i recent-n-perf-counts)
|
|
(to-float performance-num-ticks-per-second)))
|
|
(unless delta-time (continue))
|
|
(SDL_Log "\t%f %fhz\n" delta-time (/ 1.f delta-time))))
|
|
|
|
;;
|
|
;; Shutdown
|
|
;;
|
|
(when exit-reason
|
|
(SDL_Log "Exiting. Reason: %s\n" exit-reason))
|
|
|
|
(write-progression-data)
|
|
|
|
(when g-puzzle-list
|
|
(free (type-cast g-puzzle-list (* void)))
|
|
(set g-puzzle-list null))
|
|
|
|
(when g-progression-puzzles
|
|
(free (type-cast g-progression-puzzles (* void)))
|
|
(set g-progression-puzzles null))
|
|
|
|
(each-in-array
|
|
textures-to-destroy i
|
|
(SDL_DestroyTexture (at i textures-to-destroy)))
|
|
|
|
(SDL_DestroyRenderer renderer)
|
|
|
|
(sdl-shutdown window)
|
|
|
|
(return 0))
|
|
))
|
|
;;
|
|
;; Macros and generators
|
|
;;
|
|
|
|
(defmacro debug-log (&rest arguments any)
|
|
(tokenize-push
|
|
output
|
|
(when debug-log-enabled
|
|
(SDL_Log (token-splice-rest arguments tokens))))
|
|
(return true))
|
|
|
|
(defgenerator define-constant (define-name symbol value any)
|
|
(var define-statement (const ([] CStatementOperation))
|
|
(array
|
|
(array Keyword "#define" -1)
|
|
(array Expression null 1)
|
|
(array Keyword " " -1)
|
|
(array Expression null 2)
|
|
(array KeywordNoSpace "\n" -1)))
|
|
(return (c-statement-out define-statement)))
|
|
|
|
(defgenerator undefine-constant (define-name symbol)
|
|
(var define-statement (const ([] CStatementOperation))
|
|
(array
|
|
(array Keyword "#undef" -1)
|
|
(array Expression null 1)
|
|
(array KeywordNoSpace "\n" -1)))
|
|
(return (c-statement-out define-statement)))
|
|
|
|
;; Necessary to create e.g. in C PREFIX "_my_thing"
|
|
(defgenerator static-string-combine (string-A any string-B any)
|
|
(var statement (const ([] CStatementOperation))
|
|
(array
|
|
(array Expression null 1)
|
|
(array Keyword " " -1)
|
|
(array Expression null 2)))
|
|
(return (c-statement-out statement)))
|
|
|
|
;; cakelisp's tokenizer doesn't properly parse ' '
|
|
(defgenerator space-hack ()
|
|
(var statement (const ([] CStatementOperation))
|
|
(array
|
|
(array Keyword "' '" -1)))
|
|
(return (c-statement-out statement)))
|
|
|
|
(defgenerator if-c-preprocessor-defined (preprocessor-symbol symbol
|
|
true-block (index any) false-block (index any))
|
|
(var statement (const ([] CStatementOperation))
|
|
(array
|
|
(array Keyword "#ifdef" -1)
|
|
(array Expression null 1)
|
|
(array KeywordNoSpace "\n" -1)
|
|
(array Statement null 2)
|
|
(array KeywordNoSpace "#else" -1)
|
|
(array KeywordNoSpace "\n" -1)
|
|
(array Statement null 3)
|
|
(array KeywordNoSpace "#endif" -1)
|
|
(array KeywordNoSpace "\n" -1)))
|
|
(return (c-statement-out statement)))
|
|
|
|
(defgenerator c-for (initializer any conditional any update any &rest &optional body any)
|
|
(var statement (const ([] CStatementOperation))
|
|
(array
|
|
(array Keyword "for" -1)
|
|
(array OpenParen null -1)
|
|
(array Statement null 1)
|
|
(array Expression null 2)
|
|
(array Keyword ";" -1)
|
|
(array Expression null 3)
|
|
(array CloseParen null -1)
|
|
(array OpenBlock null -1)
|
|
(array Body null 4)
|
|
(array CloseBlock null -1)))
|
|
(return (c-statement-out statement)))
|
|
|
|
(defmacro c-statement-out (statement-operation symbol)
|
|
(tokenize-push
|
|
output
|
|
(CStatementOutput environment context tokens startTokenIndex
|
|
(token-splice statement-operation)
|
|
(array-size (token-splice statement-operation))
|
|
output))
|
|
(return true))
|
|
|
|
(defmacro repeat (thing any)
|
|
(tokenize-push
|
|
output
|
|
(token-splice thing) (token-splice thing))
|
|
(return true))
|
|
|
|
(defmacro vec-xy-to-int (vec symbol)
|
|
(tokenize-push
|
|
output
|
|
(type-cast (vec-x (token-splice vec)) int)
|
|
(type-cast (vec-y (token-splice vec)) int))
|
|
(return true))
|
|
|
|
(defmacro to-float (expression any)
|
|
(tokenize-push
|
|
output
|
|
(type-cast (token-splice expression) float))
|
|
(return true))
|
|
|
|
;; This only works for arrays where the size is known at compile-time
|
|
(defmacro each-in-array (array-name symbol iterator-name symbol &rest body any)
|
|
(tokenize-push
|
|
output
|
|
(scope
|
|
(each-in-range (array-size (token-splice array-name)) (token-splice iterator-name)
|
|
(token-splice-rest body tokens))))
|
|
(return true))
|
|
|
|
;; Note: Will reevaluate the range expression each iteration
|
|
(defmacro each-in-range (range any iterator-name symbol &rest body any)
|
|
(tokenize-push
|
|
output
|
|
(c-for (var (token-splice iterator-name) int 0)
|
|
(< (token-splice iterator-name) (token-splice range))
|
|
(incr (token-splice iterator-name))
|
|
(token-splice-rest body tokens)))
|
|
(return true))
|
|
|
|
(defmacro each-char-in-string (start-char any iterator-name symbol &rest body any)
|
|
(tokenize-push
|
|
output
|
|
(c-for (var (token-splice iterator-name) (* char) (token-splice start-char))
|
|
(deref (token-splice iterator-name))
|
|
(incr (token-splice iterator-name))
|
|
(token-splice-rest body tokens)))
|
|
(return true))
|
|
|
|
(defmacro each-char-in-string-const (start-char any iterator-name symbol &rest body any)
|
|
(tokenize-push
|
|
output
|
|
(c-for (var (token-splice iterator-name) (* (const char)) (token-splice start-char))
|
|
(deref (token-splice iterator-name))
|
|
(incr (token-splice iterator-name))
|
|
(token-splice-rest body tokens)))
|
|
(return true))
|
|
|
|
(defmacro draw-formatted-string (position any format-string string &rest arguments any)
|
|
(tokenize-push
|
|
output
|
|
(scope
|
|
(var format-buffer ([] 64 char) (array 0))
|
|
(var num-printed int
|
|
(snprintf format-buffer (array-size format-buffer)
|
|
(token-splice format-string)
|
|
(token-splice-rest arguments tokens)))
|
|
(set (at num-printed format-buffer) 0)
|
|
(draw-string renderer font-texture format-buffer (token-splice position))))
|
|
(return true))
|
|
|
|
;;
|
|
;; Building
|
|
;;
|
|
(defun-comptime generate-puzzles-list (manager (& ModuleManager) module (* Module) &return bool)
|
|
(var puzzles-text (* (const char)) "../../data/puzzles.txt")
|
|
(var puzzles-binary (* (const char)) "../../data/puzzles.bin")
|
|
|
|
;; Already built?
|
|
(unless (fileIsMoreRecentlyModified puzzles-text puzzles-binary)
|
|
(Log "generate-puzzles-list: Puzzles list already built\n")
|
|
(return true))
|
|
|
|
(unless (fileExists puzzles-text)
|
|
(Log "generate-puzzles-list: Building puzzles list from database\n")
|
|
(run-process-sequential-or
|
|
;; Note that we're still relative to Dependencies/gamelib
|
|
("./Dependencies/cakelisp/bin/cakelisp" "--execute" "../../src/Decompression.cake")
|
|
(Log "generate-puzzles-list: Failed to run Decompression for puzzle database reading\n")
|
|
(return false))
|
|
(unless (fileExists puzzles-text)
|
|
(Logf "generate-puzzles-list: Successfully executed Decompression, but didn't find
|
|
%s. Are paths incorrect?\n" puzzles-text)
|
|
(return false)))
|
|
|
|
(run-process-sequential-or
|
|
;; Note that we're still relative to Dependencies/gamelib
|
|
("./Dependencies/cakelisp/bin/cakelisp" "--execute" "../../src/PuzzleIO.cake")
|
|
(Log "generate-puzzles-list: Failed to run PuzzleIO for puzzle database reading\n")
|
|
(return false))
|
|
|
|
(unless (fileExists puzzles-binary)
|
|
(Log "generate-puzzles-list: Successfully executed PuzzleIO, but didn't find
|
|
data/puzzles.bin. Are paths incorrect?\n")
|
|
(return false))
|
|
(return true))
|
|
|
|
;; Because we have the list of modules from manager, we could actually generate the Android.mk file
|
|
;; in ../../Android/app/jni/src/ so that it automatically adds modules we import. For now I won't
|
|
;; do that, but a much bigger project would be well served by such a feature.
|
|
(defun-comptime copy-src-files-to-android (manager (& ModuleManager) module (* Module) &return bool)
|
|
(Log "copy-src-files-to-android: Copying files to SDL Android project\n")
|
|
|
|
(unless (fileExists "../../data/puzzles.bin")
|
|
(Log "copy-src-files-to-android: Didn't find data/puzzles.bin. Has generate-puzzles-list not
|
|
been executed yet?\n")
|
|
(return false))
|
|
|
|
(run-process-sequential-or
|
|
;; Note that we're still relative to Dependencies/gamelib
|
|
("rsync" "--verbose" "--recursive" "--update"
|
|
"--exclude=puzzles.txt"
|
|
"../../data"
|
|
"../../Android/app/src/main/assets/")
|
|
(Log "copy-src-files-to-android: failed to sync data/ to Android assets folder\n
|
|
This tool requires rsync to be installed.\n")
|
|
(return false))
|
|
(run-process-sequential-or
|
|
;; Note that we're still relative to Dependencies/gamelib
|
|
("rsync" "--verbose" "--recursive" "--update"
|
|
"--exclude=*.o" "--exclude=kitty-gridlock" "--exclude=*Cache.cake"
|
|
"cakelisp_cache/HandmadeMath-Kitty/"
|
|
"../../Android/app/jni/src/")
|
|
(Log "copy-src-files-to-android: failed to sync src/ to Android assets folder\n
|
|
This tool requires rsync to be installed.\n")
|
|
(return false))
|
|
(return true))
|
|
|
|
(comptime-cond
|
|
('Kitty-Main
|
|
(add-compile-time-hook-module pre-build generate-puzzles-list)
|
|
;; Order matters here, because we want to copy the generated puzzles list to android
|
|
(add-compile-time-hook-module pre-build copy-src-files-to-android :priority-decrease 1)
|
|
))
|
|
|
|
(module-use-sdl-build-options)
|
|
|
|
(comptime-cond
|
|
('Kitty-Main
|
|
;; Note that this executable still pulls .so files from Dependencies
|
|
(set-cakelisp-option executable-output "../../kitty-gridlock")))
|
|
|
|
;; This ensures we still find SDL even after we've relocated the executable
|
|
(add-library-runtime-search-directory "Dependencies/gamelib/Dependencies/SDL/buildSDLBuild/lib")
|
|
|
|
;; Use build label to make it easier to find in cakelisp_cache
|
|
(add-build-config-label "Kitty")
|
|
|