flappy-ball/flappy-ball.lisp

341 lines
11 KiB
Common Lisp
Raw Normal View History

2024-04-18 21:50:05 +02:00
(defpackage :flappy-ball
(:use :cl :sketch)
2024-04-27 09:47:19 +02:00
(:export :flappy-ball :on-click :on-key :setup
:make-ball :ball-x :ball-y :ball-velocity :ball-size :ball-move :ball-flap :ball-draw
2024-05-03 01:31:27 +02:00
:make-pipe :pipe-x :pipe-height :pipe-gap :pipe-width :pipe-draw :pipe-collides
:make-high-score :high-score-value :high-score-time :main))
2024-04-18 21:50:05 +02:00
(in-package :flappy-ball)
;; Create a new ball entity
;; (make-ball 10 20 -5.6)
(defun make-ball (x y velocity size)
(list x y velocity size))
(defun ball-x (ball)
(car ball))
(defun ball-y (ball)
(cadr ball))
(defun ball-velocity (ball)
(caddr ball))
(defun ball-size (ball)
(cadddr ball))
(defun ball-move (ball gravity max-height)
(let ((size (ball-size ball))
(velocity (ball-velocity ball)))
(make-ball
(ball-x ball)
(alexandria:clamp (+ (ball-y ball) velocity) size (- max-height size))
(+ velocity gravity)
size)))
(defun ball-flap (ball flap-speed)
(list
(ball-x ball)
(ball-y ball)
flap-speed
(ball-size ball)))
(defun ball-draw (ball pen)
(with-pen pen
(circle (ball-x ball) (ball-y ball) (ball-size ball))))
;; -------------------------------------------------------------
;; Create a new pipe entity
;; (setf pipe (make-pipe 10 200 100 20))
(defun make-pipe (x height gap width)
(list x height gap width))
;; Accessors
;; (pipe-x pipe)
(defun pipe-x (pipe)
(car pipe))
;; (pipe-height pipe)
(defun pipe-height (pipe)
(cadr pipe))
;; (pipe-gap pipe)
(defun pipe-gap (pipe)
(caddr pipe))
;; (pipe-width pipe)
(defun pipe-width (pipe)
(cadddr pipe))
(defun pipe-draw (pipe pen scroll-x height)
(let ((x (- (pipe-x pipe) scroll-x))
(h (pipe-height pipe))
(gap (pipe-gap pipe))
(w (pipe-width pipe)))
(with-pen pen
(rect x 0 w h)
(rect x (+ h gap) w (- height h gap)))))
(defun pipe-collides (pipe ball scroll-x)
(let ((x (- (pipe-x pipe) scroll-x))
(h (pipe-height pipe))
(gap (pipe-gap pipe))
(w (pipe-width pipe))
(bx (ball-x ball))
(by (ball-y ball))
(bs (ball-size ball)))
(and (<= (- x bs) bx (+ x w bs))
(not (<= (+ h bs) by (- (+ h gap) bs))))))
(defun random-pipes (n spacing max-height gap width)
(let ((padding 25))
(loop repeat n
for x from spacing by (+ spacing )
collect (make-pipe x (+ padding (random (- max-height gap padding padding))) gap width)))
)
2024-05-01 11:17:12 +02:00
;; -------------------------------------------------------------
2024-05-03 01:31:27 +02:00
(defun make-high-score (score time)
(cons score time))
(defun high-score-value (high-score)
(car high-score))
(defun high-score-time (high-score)
(cdr high-score))
(defun high-scores-path (fname)
(let* ((folder (or (uiop:getenv "XDG_DATA_HOME")
(merge-pathnames (make-pathname :directory '(:relative ".local" "share" "flappy-ball")) (user-homedir-pathname))))
(path (merge-pathnames (make-pathname :name fname :type "txt") folder)))
(ensure-directories-exist path)
path))
(defun load-high-scores (fname)
(let ((path (high-scores-path fname)))
(if (probe-file path)
(with-open-file (stream path :direction :input)
(read stream))
nil)))
(defun save-high-scores (fname high-scores)
(let ((path (high-scores-path fname)))
(with-open-file (stream path :direction :output :if-exists :supersede)
(print high-scores stream))))
(defun high-score-betterp (a b)
(or (> (high-score-value a) (high-score-value b))
(and (= (high-score-value a) (high-score-value b))
(< (high-score-time a) (high-score-time b)))))
(defun high-scores-add (high-scores score)
(let* ((hs (make-high-score (floor score) (get-universal-time)))
(new-scores (sort (append high-scores (list hs)) #'high-score-betterp)))
(if (< (length new-scores) 10)
new-scores
(subseq new-scores 0 10))))
;; -------------------------------------------------------------
(defparameter *resources* (serapeum:dict)
"Resources for our game
Hash-table with level name => file name => resource.")
(defparameter *asset-folder* (merge-pathnames "assets/"
(asdf:system-source-directory :flappy-ball)))
(defun read-level-assets (level)
(let ((level-dict (serapeum:dict))
(folder (merge-pathnames (format nil "~a~a" level "/") *asset-folder*)))
(loop for file in (list "sky.png" "clouds.png" "far.png" "middle.png" "ground.png" "prize.png")
for content = (load-resource (namestring (merge-pathnames file folder)))
do (setf (gethash file level-dict) content)
finally (setf (gethash level *resources*) level-dict))))
(defun %read-static-files-in-memory ()
"load all resources for all levels in memory"
(setf *resources* (serapeum:dict))
2024-05-09 23:02:40 +02:00
(loop for level in (list "level1" "level2" "level3" "level4")
do (read-level-assets level)
finally (return *resources*)))
(defun draw-image-layer(level resource scroll-x)
(let ((pic (serapeum:href *resources* level resource))
(offset (mod scroll-x 960)))
(draw (crop pic offset 0 960 540)
:x 0 :y 0 :width 960 :height 540)))
(defun draw-background (level scroll-x)
(draw-image-layer level "sky.png" 0)
(draw-image-layer level "clouds.png" (* -0.15 scroll-x))
(draw-image-layer level "far.png" (* 0.25 scroll-x))
(draw-image-layer level "middle.png" (* 0.6 scroll-x)))
(defun draw-foreground (level pipes scroll-x pipe-pen height)
(dolist (pipe pipes)
(pipe-draw pipe pipe-pen scroll-x height))
(draw-image-layer level "ground.png" scroll-x))
(defun draw-prize (level scroll-x target-x width)
(if (>= scroll-x target-x)
(let ((pic (serapeum:href *resources* level "prize.png")))
(draw pic :x (- (+ 60 width target-x) scroll-x) :y 0 ))))
(defun draw-hud (state score high-scores)
(when (eq state 'new)
(text "Click to Start" 50 250 200 40)
(text "High Scores" 300 30 280 40)
(loop for (score . time) in high-scores
for i from 0
do (text (format nil "~3,'0d" (floor score)) 300 (+ 70 (* i 40)) 80 30)
do (text (local-time:format-timestring nil (local-time:universal-to-timestamp time) :format '(:year "-" :month "-" :day)) 400 (+ 70 (* i 40)) 200 30)
))
(when (eq state 'won)
(text "You Won" 380 100 160 60)
(text "Return to Restart" 300 150 320 40)
)
(when (eq state 'died)
(text "You Died" 380 100 160 60)
(text "Return to Restart" 300 150 320 40))
(text (format nil "~3,'0d" (floor score)) 820 20 100 40)
)
;; -------------------------------------------------------------
2024-04-28 14:18:49 +02:00
2024-04-18 21:50:05 +02:00
(defsketch flappy-ball
((title "Flappy Ball")
(width 960)
(height 540)
(ground-level 440)
2024-04-18 21:50:05 +02:00
(copy-pixels nil)
(ball (make-ball (/ width 10) (/ height 3) 0 10))
(gravity 0.1)
(flap-speed -3)
2024-04-18 21:50:05 +02:00
(ball-pen (make-pen :stroke (gray 0.5) :fill sketch:+yellow+ :weight 1))
(collision-pen (make-pen :stroke (gray 0.5) :fill sketch:+red+ :weight 1))
2024-04-18 21:50:05 +02:00
(pipe-pen (make-pen :stroke (gray 0.5) :fill sketch:+green+ :weight 1))
2024-05-01 15:39:34 +02:00
(pipes-amount 10)
2024-04-28 15:11:20 +02:00
(pipes-spacing 200)
2024-05-01 11:17:12 +02:00
(pipes-width 20)
(pipes (random-pipes pipes-amount pipes-spacing ground-level 100 pipes-width))
2024-04-18 21:50:05 +02:00
(scroll-x 0.0)
(scroll-speed 0.8)
2024-05-01 11:17:12 +02:00
(target-x (- (* pipes-amount pipes-spacing) 720))
2024-05-01 11:57:14 +02:00
(state 'new)
2024-05-03 01:31:27 +02:00
(score 0)
(high-scores (load-high-scores "highscores"))
(on-exit (lambda () ()))
2024-05-09 23:02:40 +02:00
(play-level 1)
2024-05-03 01:31:27 +02:00
)
(let ((ball-collides (some (lambda (pipe) (pipe-collides pipe ball scroll-x)) pipes))
2024-05-09 23:02:40 +02:00
(level (format nil "level~a" play-level)))
(draw-background level scroll-x)
(draw-foreground level pipes scroll-x pipe-pen height)
(if ball-collides
(ball-draw ball collision-pen)
(ball-draw ball ball-pen))
2024-04-28 15:11:20 +02:00
(draw-prize level scroll-x target-x width)
2024-05-01 15:39:34 +02:00
(draw-hud state score high-scores)
(when (eq state 'new)
(setf scroll-x 0.0))
(when (eq state 'running)
(while-running sketch::*sketch*))
;; has to come after checking for the new state or stuck in death
(when ball-collides
(on-death sketch::*sketch*))
2024-05-01 15:39:34 +02:00
)
)
2024-05-01 11:17:12 +02:00
2024-05-01 11:57:14 +02:00
(defmethod while-running ((instance flappy-ball))
(with-slots (score state
width ground-level
scroll-x scroll-speed
2024-05-09 23:02:40 +02:00
pipes pipes-width pipes-spacing pipes-amount
ball gravity
2024-05-09 23:02:40 +02:00
play-level
high-scores) instance
2024-05-01 11:57:14 +02:00
(setf score (1+ (/ (- scroll-x pipes-width (/ width 10)) pipes-spacing)))
2024-05-01 11:17:12 +02:00
(setf scroll-x (+ scroll-x scroll-speed))
(setf ball (ball-move ball gravity ground-level))
2024-05-01 11:57:14 +02:00
(when (> (round score) pipes-amount)
2024-05-09 23:02:40 +02:00
(if (eq play-level 4)
(progn
(setf state 'won)
(setf high-scores (high-scores-add high-scores score))
(save-high-scores "highscores" high-scores))
(progn
(setf play-level (1+ play-level))
(setf pipes (random-pipes pipes-amount pipes-spacing ground-level 100 pipes-width))
(setf scroll-x 0.0)
(setf scroll-speed (* 0.8 scroll-speed)))))))
(defmethod on-death ((instance flappy-ball))
(with-slots (state high-scores score) instance
(setf high-scores (high-scores-add high-scores score))
(save-high-scores "highscores" high-scores)
(setf state 'died)))
2024-04-18 21:50:05 +02:00
(defmethod on-click ((instance flappy-ball) x y)
2024-05-01 11:17:12 +02:00
(with-slots (ball flap-speed state) instance
(setf ball (ball-flap ball flap-speed))
(when (eq state 'new)
(setf state 'running))))
2024-04-18 21:50:05 +02:00
(defmethod on-key ((instance flappy-ball) key state)
2024-05-01 11:17:12 +02:00
(with-slots (ball flap-speed state) instance
(setf ball (ball-flap ball flap-speed))
2024-05-03 01:31:27 +02:00
(when (and (eq state 'new) (not (eq key :RETURN)))
(setf state 'running))
2024-05-01 15:39:34 +02:00
(when (and (eq state 'died) (eq key :RETURN))
2024-05-01 11:17:12 +02:00
(setf state 'new))
2024-05-01 15:39:34 +02:00
(when (and (eq state 'won) (eq key :RETURN))
2024-05-01 11:17:12 +02:00
(setf state 'new))
))
2024-05-03 01:31:27 +02:00
(defmethod setup ((instance flappy-ball) &key &allow-other-keys)
(%read-static-files-in-memory)
2024-05-03 01:31:27 +02:00
(background (gray 1)))
2024-04-18 21:50:05 +02:00
;; override underlying sdl2kit method to intercept closing the window
(defmethod kit.sdl2:close-window :after ((instance flappy-ball))
(with-slots (on-exit) instance
(print "close window called")
(apply on-exit ())))
;; ---------------------------------------------------
(defun main ()
(let ((is_running t))
(print "Starting...")
(make-instance 'flappy-ball :on-exit (lambda () (progn
(print "exiting...")
(setf is_running nil))))
(loop
while is_running
do (sleep 1))))
;; --------------- deploy instructions ---------------------
;; add static resources to the deploy folder
(deploy:define-resource-directory assets (make-pathname :directory '(:relative "assets")))