#!/bin/sh #| -*- scheme -*- exec csi -R r7rs -ss "$0" "$@" apple --- 2023 lisp game jam (C) Case Duckworth Distributed under the terms of the LATCRIFPL, v1.0. See COPYING for details. |# (import (scheme base) (scheme time) (scheme write) (chicken io) (chicken port) (chicken random) (yolk common) (yolk xterm) (yolk erase) (yolk cursor) (yolk attrs) (matchable) (stty)) #;(define (log* . xs) (with-output-to-port (current-error-port) (lambda () (for-each (lambda (x) (display x) (display " ") x) xs) (newline)))) (define (main args) (parameterize ((world (make-world 80 25))) (parameterize ((me (make-thing 'me (world) (with-attrs '(red) "ó") 10 10 2)) (snake (make-snake (world) 5 (current-jiffy) 200 'left (make-thing 'snake-head (world) "<" 20 20 2) (list))) (fruit (make-thing 'fruit (world) (random-choice (fruits)) 10 20 2))) (game-intro) (readch) (game-exit-message (dynamic-wind game-setup game-loop game-cleanup))))) (define (game-exit-message msg) (cond ((and (pair? msg) (eq? (car msg) 'error)) (apply error "An error occurred" msg) #f) (else (cond ((eq? msg 'win) (print "Congrats!")) ((eq? msg 'lose) (print "Better luck next time."))) (print "Final score: " (score)) #t))) ;;; Parameters (define me (make-parameter #f)) (define snake (make-parameter #f)) (define fruit (make-parameter #f)) (define world (make-parameter #f)) (define game-end (make-parameter #f)) (define score (make-parameter 0)) (define fruits (make-parameter (list (with-attrs '(green) "ò") (with-attrs '(yellow) ")") (with-attrs '(magenta) "%") (with-attrs '(blue) "*")))) ;;; Main game elements (define (game-setup) ;; Prepare terminal (stty '(raw (not echo))) (set-buffering-mode! (current-input-port) #:none 1) (set-buffering-mode! (current-output-port) #:none 1) (draw alt-buffer-enable erase-screen (cursor-save) invisible-cursor) ;; Set up world (world-init (world)) (world-draw (world))) (define (game-cleanup) ;; Restore terminal (stty '(cooked echo)) (draw cursor-home (cursor-restore) alt-buffer-disable visible-cursor)) (define (game-loop) (call/cc (lambda (return) (parameterize ((game-end return)) (with-exception-handler (lambda (e) (return (cons 'error e))) (lambda () (let loop ((now (current-jiffy))) (unless (and (eq? (thing-x (fruit)) (thing-x (me))) (eq? (thing-y (fruit)) (thing-y (me)))) (thing-place! (fruit))) (handle-input) (snake-step (snake) now) (world-draw (world)) (loop (current-jiffy))))))))) (define (game-intro) (draw #<#END A P P L E! a Spring Lisp Game Jam 2023 entry by Case Duckworth Welcome to APPLE, the game of SNAKE, except you're the apple instead! Specifically, you're the RED APPLE #(thing-look (me)), running from the SNAKE #(thing-look (snake-head (snake)))·····. Try to dodge the snake and get him to eat himself --- you can distract him by going near other FRUIT #(fruits), which he'll eat instead of you. Move using HJKL or arrow keys! Quit with Q, and pause with ESC. GOOD LUCK :D Press ENTER to continue ..... END )) (define (handle-input) (if (char-ready?) (match (readch) ((or #\q #\) ((game-end) 'quit)) (#\k (up!)) (#\j (down!)) (#\l (right!)) (#\h (left!)) ;; Escape characters (#\escape (match (readch) (#\[ (match (readch) (#\A (up!)) (#\B (down!)) (#\C (right!)) (#\D (left!)) (_ #f))) (_ #f))) (_ #f)) #f)) (define (readch) (let ((c (integer->char (read-byte)))) c)) ;;; random (define random-int (case-lambda (() (pseudo-random-integer 100)) ((max) (pseudo-random-integer max)) ((min max) (let ((max (if (> max min) max min)) (min (if (< min max) min max))) (+ min (pseudo-random-integer (- max min))))))) (define (random-x world) (random-int 1 (- (world-width world) 1))) (define (random-y world) (random-int 1 (- (world-height world) 1))) (define (random-point world) (let loop ((x (random-x world)) (y (random-y world))) (if (world-get world x y) (loop (random-x world) (random-y world)) (values x y)))) (define (random-choice choices) (list-ref choices (random-int (length choices)))) ;;; Drawing stuff (define (draw . instructions) (for-each (lambda (i) (cond ((string? i) (display i)) ((list? i) (apply draw i)) (else (error "Don't know how to draw" i)))) instructions)) ;;; World (define-record-type (%make-world width height map) world? (width world-width world-width-set!) (height world-height world-height-set!) (map world-map world-map-set!)) (define (in-bounds? world x y) (or (< x (world-width world)) (> x 0) (< y (world-height world)) (> y 0))) (define (coords->index world x y) (if (in-bounds? world x y) (+ x (* (world-width world) y)) (error "Out of bounds" (list (world-width world) (world-height world)) (list x y)))) (define (world-get world x y) (vector-ref (world-map world) (coords->index world x y))) (define (world-set! world x y obj) (vector-set! (world-map world) (coords->index world x y) obj)) (define (make-world width height) (%make-world width height (make-vector (* width height) #f))) (define (for-world proc world) (do ((y 1 (+ y 1))) ((= y (world-height world)) #t) (do ((x 1 (+ x 1))) ((= x (world-width world)) #t) (proc world x y)))) (define (world-draw world) (draw cursor-home) (for-world (lambda (w x y) (cond ((thing? (world-get w x y)) (draw (cursor-move x y) (thing-look (world-get w x y)))) (else (draw (cursor-move x y) " ")))) world)) (define (world-init world) (for-world (lambda (w x y) (cond ((or (= x 1) (= y 1) (= x (- (world-width world) 1)) (= y (- (world-height world) 1))) (thing-place! (wall w x y))) (else))) world) (thing-pos-randomize! (me)) (thing-place! (me)) (thing-pos-randomize! (snake-head (snake))) (thing-place! (snake-head (snake))) (for-each (lambda (t) (thing-place! t)) (snake-tail (snake))) (thing-pos-randomize! (fruit)) (thing-place! (fruit))) (define (thing-pos-randomize! thing) (let-values (((x y) (random-point (thing-world thing)))) (thing-x-set! thing x) (thing-y-set! thing y))) ;;; Things (define-record-type (make-thing name world look x y z) thing? (name thing-name) (world thing-world thing-world-set!) (look thing-look thing-look-set!) (x thing-x thing-x-set!) (y thing-y thing-y-set!) (z thing-z thing-z-set!)) (define (wall w x y) (make-thing 'wall w "#" x y 100)) (define (thing-place! thing) (world-set! (thing-world thing) (thing-x thing) (thing-y thing) thing)) (define (thing-move! thing x y) (let ((world (thing-world thing))) (world-set! world (thing-x thing) (thing-y thing) #f) (thing-x-set! thing x) (thing-y-set! thing y) (world-set! world x y thing))) (define (thing-move-relative! thing dx dy) (let* ((world (thing-world thing)) (new-x (+ (thing-x thing) dx)) (new-y (+ (thing-y thing) dy)) (other (world-get world new-x new-y))) (cond ((and (thing? other) (<= (thing-z thing) (thing-z other))) (handle-collision thing other)) (else (cond ((< new-x 1) (set! new-x 1)) ((> new-x (- (world-width world) 1)) (set! new-x (- (world-width world) 1)))) (cond ((< new-y 1) (set! new-y 1)) ((> new-y (- (world-height world) 1)) (set! new-y (- (world-height world) 1)))) (thing-move! thing new-x new-y))))) ;;; XXX: these are hacky (define (me? x) (and (thing? x) (eq? 'me (thing-name x)))) (define (snake-head? x) (and (thing? x) (eq? 'snake-head (thing-name x)))) (define (fruit? x) (and (thing? x) (eq? 'fruit (thing-name x)))) ;;; hacky-end (define (handle-collision a b) #;(log* (thing-name a) (thing-name b)) (cond ((or (and (me? a) (snake-head? b)) (and (snake-head? a) (me? b))) (game-lose)) ((or (and (fruit? a) (snake-head? b)) (and (snake-head? a) (fruit? b))) (snake-eat-fruit)) ((or (and (snake-head? a) (thing? b)) (and (thing? a) (snake-head? b))) (game-win)) (else))) (define (snake-eat-fruit) #;(log* "snake eat fruit") (world-set! (thing-world (fruit)) (thing-x (fruit)) (thing-y (fruit)) #f) (thing-pos-randomize! (fruit)) (thing-look-set! (fruit) (random-choice (fruits))) (snake-length-set! (snake) (+ (random-int 4 7) (snake-length (snake)))) (score (+ 5 (score)))) (define (game-win) (draw (cursor-move 10 10) "YOU WIN! ") (sleep 1) (readch) ((game-end) 'win)) (define (game-lose) (draw (cursor-move 10 10) "YOU LOSE :( ") (sleep 1) (readch) ((game-end) 'lose)) (define (thing-up! thing) (thing-move-relative! thing 0 -1)) (define (thing-down! thing) (thing-move-relative! thing 0 1)) (define (thing-right! thing) (thing-move-relative! thing 1 0)) (define (thing-left! thing) (thing-move-relative! thing -1 0)) (define (up!) (thing-up! (me))) (define (down!) (thing-down! (me))) (define (left!) (thing-left! (me))) (define (right!) (thing-right! (me))) (define (thing-above thing) (if (< (thing-y thing) 1) #f (let ((t (world-get (thing-world thing) (thing-x thing) (- (thing-y thing) 1)))) (and (thing? t) (thing-name t))))) (define (thing-below thing) (if (> (thing-y thing) (world-height (thing-world thing))) #f (let ((t (world-get (thing-world thing) (thing-x thing) (+ (thing-y thing) 1)))) (and (thing? t) (thing-name t))))) (define (thing-to-left thing) (if (< (thing-x thing) 1) #f (let ((t (world-get (thing-world thing) (- (thing-x thing) 1) (thing-y thing)))) (and (thing? t) (thing-name t))))) (define (thing-to-right thing) (if (> (thing-x thing) (world-width (thing-world thing))) #f (let ((t (world-get (thing-world thing) (+ (thing-x thing) 1) (thing-y thing)))) (and (thing? t) (thing-name t))))) ;;; snake (define-record-type (make-snake world length mtime wait direction head tail) snake? (length snake-length snake-length-set!) (world snake-world snake-world-set!) (mtime snake-mtime snake-mtime-set!) (direction snake-direction snake-direction-set!) (wait snake-wait snake-wait-set!) (head snake-head snake-head-set!) (tail snake-tail snake-tail-set!)) (define (snake-x snake) (thing-x (snake-head snake))) (define (snake-y snake) (thing-y (snake-head snake))) (define (snake-z snake) (thing-z (snake-head snake))) (define (take* xs n) ;;; Like `take' from SRFI 1, but doesn't choke on too-short lists. (unless (and (integer? n) (> n 0)) (error "Must take non-negative integer" n)) (let loop ((xs xs) (n n) (acc '())) (if (or (null? xs) (zero? n)) (reverse acc) (loop (cdr xs) (- n 1) (cons (car xs) acc))))) (define (snake-step snake now) (when (> now (+ (snake-mtime snake) (snake-wait snake))) (let ((new-tail (make-thing 'snake-tail (snake-world snake) "∙" #;(if (snake-vertical? snake) "|" "-") (snake-x snake) (snake-y snake) (snake-z snake)))) (snake-update-head! snake) (snake-update-direction! snake) (apply thing-move-relative! (snake-head snake) (direction->velocity (snake-direction snake))) (for-each (lambda (t) (world-set! (snake-world snake) (thing-x t) (thing-y t) #f)) (snake-tail snake)) (snake-tail-set! snake (take* (cons new-tail (snake-tail snake)) (snake-length snake))) (for-each (lambda (t) (thing-place! t)) (snake-tail snake)) (snake-mtime-set! snake now) (score (+ 1 (score)))))) (define (direction->velocity direction) (case direction ((up) '(0 -1)) ((down) '(0 1)) ((left) '(-1 0)) ((right) '(1 0)) (else direction))) (define (snake-vertical? snake) (or (eq? (snake-direction snake) 'up) (eq? (snake-direction snake) 'down))) (define (snake-update-head! snake) (thing-look-set! (snake-head snake) (case (snake-direction snake) ((right) ">") ((down) "v") ((left) "<") ((up) "^")))) (define (distance thing-a thing-b) (sqrt (+ (expt (abs (- (thing-x thing-a) (thing-x thing-b))) 2) (expt (abs (- (thing-y thing-a) (thing-y thing-b))) 2)))) (define (snake-move-toward snake thing) (let ((tx (thing-x thing)) (ty (thing-y thing)) (sx (snake-x snake)) (sy (snake-y snake))) (cond ((< sx tx) (if (> (abs (- sx tx)) (abs (- sy ty))) 'right (if (< sy ty) 'down 'up))) ((> sx tx) (if (> (abs (- sx tx)) (abs (- sy ty))) 'left (if (< sy ty) 'down 'up))) ((= sx tx) (cond ((<= sy ty) 'down) ((> sy ty) 'up))) ;; Otherwise, keep going how we're going (else (snake-direction snake))))) (define (edible? thing-name) (or (eq? thing-name 'me) (eq? thing-name 'fruit))) (define (snake-decide-move snake) ; snake => direction (let* ((head (snake-head snake)) (direction (snake-direction snake)) (new-dir ;; Initial decision (cond ;; If `me' is right there, move toward it ((edible? (thing-above head)) 'up) ((edible? (thing-below head)) 'down) ((edible? (thing-to-right head)) 'right) ((edible? (thing-to-left head)) 'left) ;; If about to collide with something, dodge it. ((or (and (thing-above head) (eq? direction 'up)) (and (thing-below head) (eq? direction 'down))) (if (thing-to-left head) 'right 'left)) ((or (and (thing-to-right head) (eq? direction 'right)) (and (thing-to-left head) (eq? direction 'left))) (if (thing-above head) 'down 'up)) ;; If close to the fruit, get it ((> 5 (distance (snake-head snake) (fruit))) (snake-move-toward snake (fruit))) ;; Otherwise, move toward `me' (else (snake-move-toward snake (me)))))) #;(unless (eq? direction new-dir) (log* direction new-dir)) ;; Don't let the snake double back into itself (cond ((or (and (eq? direction 'right) (eq? new-dir 'left)) (and (eq? direction 'left) (eq? new-dir 'right))) (snake-direction-set! snake (if (< (snake-x snake) (thing-y (me))) 'down 'up)) (snake-decide-move snake)) ((or (and (eq? direction 'up) (eq? new-dir 'down)) (and (eq? direction 'down) (eq? new-dir 'up))) (snake-direction-set! snake (if (< (snake-x snake) (thing-y (me))) 'right 'left)) (snake-decide-move snake)) (else new-dir)))) (define (snake-update-direction! snake) (snake-direction-set! snake (snake-decide-move snake))) (define (%flush-input port) (let loop ((ready? (char-ready? port))) (if ready? (begin (read-byte port) (loop (char-ready? port))) #t))) (define flush-input (case-lambda (() (%flush-input (current-input-port))) ((port) (%flush-input port))))