;;; Starling Game Engine ;;; Copyright © 2019 David Thompson ;;; ;;; This program is free software: you can redistribute it and/or ;;; modify it under the terms of the GNU General Public License as ;;; published by the Free Software Foundation, either version 3 of the ;;; License, or (at your option) any later version. ;;; ;;; This program is distributed in the hope that it will be useful, ;;; but WITHOUT ANY WARRANTY; without even the implied warranty of ;;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU ;;; General Public License for more details. ;;; ;;; You should have received a copy of the GNU General Public License ;;; along with Starling. If not, see . ;;; Commentary: ;; ;; Tetris clone. ;; ;;; Code: (use-modules (chickadee math easings) (chickadee math matrix) (chickadee math rect) (chickadee math vector) (chickadee graphics color) (chickadee graphics font) (chickadee graphics particles) (chickadee graphics texture) (chickadee graphics sprite) (chickadee scripting) (ice-9 match) (oop goops) (srfi srfi-1) (srfi srfi-43) (starling asset) (starling kernel) (starling node) (starling node-2d) (starling scene)) ;;; ;;; Constants ;;; (define window-width 600) (define window-height 800) (define board-width 10) (define board-height 20) (define tile-width 32) (define tile-height 32) ;;; ;;; Assets ;;; (define (load-block-atlas file-name) (let ((texture (load-image file-name))) (split-texture texture tile-width tile-height))) (define-asset atlas (load-block-atlas "images/blocks.png")) (define-asset star (load-image "images/star.png")) (define-asset click-font (load-bitmap-font "fonts/click.xml")) ;;; ;;; Convenience Procedures/Macros ;;; (define (transparent color) "Make a fully transparent version of COLOR." (make-color (color-r color) (color-g color) (color-b color) 0.0)) (define (centered width) "Return the X coordinate needed to center an object occupying WIDTH pixels in the right-hand side UI area." ;; We're not centering it with the full screen, just the portion of ;; the screen available to the right of the game board. (let ((board-width-pixels (+ (* board-width tile-width) (* 16.0 2.0)))) (+ board-width-pixels (/ (- window-width board-width-pixels width) 2.0)))) (define (centered-text text) "Return the X coordinate needed to center the string TEXT in the UI area next to the board." (centered (* (font-line-width (asset-ref click-font) text) 2.0))) (define-syntax for-each-permutation (syntax-rules () ((_ () body ...) (begin body ...)) ((_ ((var start end inc) rest ...) body ...) (let loop ((i start)) (when (< i end) (let ((var i)) (for-each-permutation (rest ...) body ...)) (loop (+ i inc))))) ((_ ((var start end) rest ...) body ...) (for-each-permutation ((var start end 1) rest ...) body ...)))) (define-syntax map-permutation (syntax-rules () ((_ ((var start end inc)) body ...) (let loop ((i start)) (if (< i end) (let ((var i)) (cons (begin body ...) (loop (+ i 1)))) '()))) ((_ ((var start end inc) rest ...) body ...) (let loop ((i start)) (if (< i end) (let ((var i)) (append (map-permutation (rest ...) body ...) (loop (+ i 1)))) '()))) ((_ ((var start end) rest ...) body ...) (map-permutation ((var start end 1) rest ...) body ...)))) ;;; ;;; Tiles ;;; ;; A single block on a tetris board. (define-class () (type #:accessor type #:init-form #f) (area #:getter area #:init-keyword #:area)) (define (for-each-coord proc) (for-each-permutation ((x 0 board-width) (y 0 board-height)) (proc x y))) ;;; ;;; Pieces ;;; ;; Moveable tetromino. (define-class () (x #:accessor x #:init-keyword #:x #:init-form 0) (y #:accessor y #:init-keyword #:y #:init-form 0) (type #:accessor type #:init-keyword #:type) (shape #:accessor shape #:init-keyword #:shape)) (define shapes '((i (0 1) (1 1) (2 1) (3 1)) (j (0 0) (1 0) (2 0) (0 1)) (l (0 0) (1 0) (2 0) (2 1)) (o (0 0) (0 1) (1 0) (1 1)) (s (0 0) (1 0) (1 1) (2 1)) (t (0 0) (1 0) (2 0) (1 1)) (z (0 1) (1 1) (1 0) (2 0)))) (define colors '((i . yellow) (j . blue) (l . red) (o . purple) (s . green) (t . gray) (z . red))) (define (type->atlas-index type) (match type ('yellow 0) ('blue 0) ('green 0) ('purple 0) ('red 0) ('gray 0) (_ #f))) ;; A convenient cache of all the possible sprite locations with a ;; tetrnomino to avoid recalculating them all the time. (define rects (map-permutation ((x 0 4) (y 0 4)) `((,x ,y) . ,(make-rect (* x tile-width) (* y tile-height) tile-width tile-height)))) (define-method (piece-width (piece )) (let loop ((shape (shape piece)) (minx #f) (maxx #f)) (match shape (() (+ (- maxx minx) 1)) (((x _) . rest) (loop rest (if minx (min minx x) x) (if maxx (max maxx x) x)))))) (define-method (piece-height (piece )) (let loop ((shape (shape piece)) (miny #f) (maxy #f)) (match shape (() (+ (- maxy miny) 1)) (((_ y) . rest) (loop rest (if miny (min miny y) y) (if maxy (max maxy y) y)))))) (define-method (piece-y-offset (piece )) (fold (lambda (p memo) (match p ((_ y) (if memo (min y memo) y)))) #f (shape piece))) (define-method (max-x (piece )) (- board-width 1 (fold (lambda (p memo) (match p ((x _) (max x memo)))) 0 (shape piece)))) (define-method (move-piece (piece ) new-x new-y init?) (let ((old-x (x piece)) (old-y (y piece)) (px (* new-x tile-width)) (py (* new-y tile-height))) (set! (x piece) new-x) (set! (y piece) new-y) (if init? (teleport piece px py) (run-script piece (move-to piece px py 8))))) (define-method (move-piece (piece ) new-x new-y) (move-piece piece new-x new-y #f)) (define-method (render (piece ) alpha) (for-each (match-lambda (pos (draw-sprite* (texture-atlas-ref (asset-ref atlas) (type->atlas-index (type piece))) (assoc-ref rects pos) (world-matrix piece)))) (shape piece))) (define (make-random-piece) (let ((type (list-ref '(i o t j l s z) (random 7)))) (make #:type (assq-ref colors type) #:shape (assq-ref shapes type) #:rank 999))) ;;; ;;; Rows ;;; ;; A horizontal line of tiles on the tetris board. (define-class () (y #:accessor y #:init-keyword #:y) (tiles #:accessor tiles #:init-keyword #:tiles)) (define-method (on-boot (row )) (teleport row 0.0 (* (y row) tile-height))) (define-method (render (row ) alpha) (let ((tiles (tiles row)) (batch (batch (parent row))) (atlas (asset-ref atlas))) (let loop ((x 0)) (when (< x board-width) (let* ((tile (vector-ref tiles x)) (i (type->atlas-index (type tile)))) (when i (sprite-batch-add* batch (area tile) (world-matrix row) #:texture-region (texture-atlas-ref atlas i)))) (loop (+ x 1)))))) (define-method (on-clear (row ) particles) (run-script row (scale-to row 0.0 0.0 10) (detach row)) ;; Emit some particles! woooooo (let* ((pos (position row)) (x (vec2-y pos)) (y (vec2-y pos))) (vector-for-each (lambda (i tile) (let* ((area (make-rect (* i tile-width) y tile-width tile-height)) (emitter (make-particle-emitter area 2 10))) (add-particle-emitter particles emitter))) (tiles row)))) (define (make-row y) (define (make-tile x) (make #:area (make-rect (* x tile-width) 0.0 tile-width tile-height))) (let ((row (make-vector board-width))) (vector-for-each (lambda (x e) (vector-set! row x (make-tile x))) row) (make #:y y #:tiles row))) ;;; ;;; Boards ;;; ;; A collection of rows forming the complete tetris game board. (define-class () (batch #:accessor batch #:init-keyword #:batch) (rows #:accessor rows #:init-form (make-vector board-height #f))) (define-method (on-boot (board )) (set! (batch board) (make-sprite-batch #f)) (attach-to board (make #:name 'background #:region (make-rect 0.0 0.0 320.0 640.0) #:color tango-aluminium-6) (make #:name 'batch #:batch (batch board)) (make #:name 'particles #:particles (make-particles 2000 #:texture (asset-ref star) #:start-color tango-butter #:end-color (transparent tango-butter) #:lifetime 10)))) (define-method (get-tile (board ) x y) (vector-ref (tiles (vector-ref (rows board) y)) x)) (define-method (board-ref (board ) x y) (type (get-tile board x y))) (define-method (board-set! (board ) x y new-type) (set! (type (get-tile board x y)) new-type)) (define-method (add-to-board (board ) (piece )) (let ((type (type piece)) (x (x piece)) (y (y piece))) (for-each (match-lambda ((sx sy) (let ((bx (+ x sx)) (by (+ y sy))) (when (< by board-height) (board-set! board bx by type))))) (shape piece)))) (define-method (overlaps-board? (piece ) (board )) (let ((px (x piece)) (py (y piece))) (any (match-lambda ((x y) (board-ref board (+ x px) (+ y py)))) (shape piece)))) (define-method (out-of-bounds? (piece )) (let ((px (x piece)) (py (y piece))) (any (match-lambda ((x y) (let ((sx (+ x px)) (sy (+ y py))) (or (< sx 0) (>= sx board-width) (< sy 0) (>= sy board-height))))) (shape piece)))) (define-method (rotate-piece (piece ) (board )) (let* ((max-y (fold (lambda (pos memo) (match pos ((x y) (max y memo)))) 0 (shape piece))) (old-shape (shape piece)) (new-shape (map (match-lambda ((x y) (list (- max-y y) x))) old-shape))) (set! (shape piece) new-shape) (when (or (out-of-bounds? piece) (overlaps-board? piece board)) (set! (shape piece) old-shape)))) (define-method (filled-rows (board )) (define (row-full? y) (let loop ((x 0)) (cond ((= x board-width) #t) ((board-ref board x y) (loop (+ x 1))) (else #f)))) (let loop ((y 19)) (if (< y 0) '() (if (row-full? y) (cons y (loop (- y 1))) (loop (- y 1)))))) (define-method (remove-filled-rows (board )) (let* ((rows (rows board)) (rows-to-remove (filled-rows board)) (anim-duration 10)) (let loop ((dead-rows rows-to-remove) (count 0)) (match dead-rows (() (length rows-to-remove)) ((dead-y . rest) ;; Remove the cleared row. (let ((dead-row (vector-ref rows dead-y)) (particles (particles (& board particles)))) (on-clear dead-row particles)) ;; Move everything above the cleared row down. (let y-loop ((old-y (+ dead-y 1))) (when (< old-y board-height) (let ((row (vector-ref rows old-y)) (new-y (- old-y 1))) (vector-set! rows new-y row) (set! (y row) new-y) ;; Smoothly animate the drop down. (run-script row (unless (zero? count) (sleep (* count anim-duration))) (move-to row 0.0 (* new-y tile-height) anim-duration (if (zero? count) smoothstep linear)))) (y-loop (+ old-y 1)))) ;; Add a new blank row to the top. (let ((new-row (make-row (- board-height 1)))) (attach-to board new-row) (vector-set! rows (- board-height 1) new-row)) (loop rest (+ count 1))))))) (define-method (clear-board (board )) (let ((rows (rows board))) (when rows (let loop ((y 0)) (when (< y board-height) (let ((old-row (vector-ref rows y))) (and old-row (detach old-row))) (let ((new-row (make-row y))) (vector-set! rows y new-row) (attach-to board new-row)) (loop (+ y 1))))))) (define-method (touching-next-row? (piece ) (board )) (any (match-lambda ((sx sy) (let ((bx (+ (x piece) sx)) (by (- (+ (y piece) sy) 1))) (or (= by -1) (and (< by board-height) (board-ref board bx by)))))) (shape piece))) (define-method (render-tree (board ) alpha) (set-sprite-batch-texture! (batch board) (texture-atlas-texture (asset-ref atlas))) (next-method)) ;;; ;;; Tetra ;;; ;; The main game scene. (define-class () (state #:accessor state #:init-form 'play) (board #:accessor board) (piece #:accessor piece #:init-form #f) (next-piece #:accessor next-piece #:init-form #f) (timer #:accessor timer #:init-form 0) (down-interval #:accessor down-interval #:init-form 30) (score #:accessor score #:init-form -1)) (define-method (game-over (tetra )) (set! (state tetra) 'game-over) (let* ((message "GAME OVER") (instructions "press ENTER to play again") (font (asset-ref click-font)) (message-width (* (font-line-width font message) 2.0)) (instructions-width (* (font-line-width font instructions) 2.0)) (line-height (* (font-line-height font) 2.0)) (padding 16.0) (border 2.0) (container-width (+ (max message-width instructions-width) (* padding 2.0))) (container-height (+ (* line-height 2) (* padding 3.0))) (container (make #:name 'game-over-container #:rank 9999 #:position (vec2 (- (+ (/ (* board-width tile-width) 2.0) 16.0) (/ container-width 2.0)) (/ (- window-height container-height) 2.0))))) (attach-to container (make #:name 'background #:region (make-rect 0.0 0.0 container-width container-height) #:color tango-aluminium-3) (make #:name 'background #:region (make-rect border border (- container-width (* border 2.0)) (- container-height (* border 2.0))) #:color tango-aluminium-6) (make