diff options
| -rw-r--r-- | Makefile | 6 | ||||
| -rw-r--r-- | README.org | 8 | ||||
| -rw-r--r-- | ai.scm | 135 | ||||
| -rw-r--r-- | docs/guide.org | 4 | ||||
| -rw-r--r-- | downstroke.egg | 5 | ||||
| -rw-r--r-- | prefabs.scm | 10 | ||||
| -rw-r--r-- | tests/ai-test.scm | 48 | ||||
| -rw-r--r-- | tests/prefabs-test.scm | 32 |
8 files changed, 25 insertions, 223 deletions
@@ -1,7 +1,7 @@ .DEFAULT_GOAL := engine # Modules listed in dependency order -MODULE_NAMES := entity tween tilemap world input physics renderer assets engine mixer sound animation ai prefabs scene-loader +MODULE_NAMES := entity tween tilemap world input physics renderer assets engine mixer sound animation prefabs scene-loader OBJECT_FILES := $(patsubst %,bin/%.o,$(MODULE_NAMES)) DEMO_NAMES := platformer shmup topdown audio sandbox spritefont menu tweens scaling @@ -29,8 +29,7 @@ bin/engine.o: bin/renderer.o bin/world.o bin/input.o bin/assets.o bin/mixer.o: bin/sound.o: bin/mixer.o bin/animation.o: bin/entity.o bin/world.o -bin/ai.o: bin/entity.o bin/world.o -bin/prefabs.o: bin/entity.o bin/ai.o +bin/prefabs.o: bin/entity.o bin/scene-loader.o: bin/world.o bin/tilemap.o bin/assets.o bin/engine.o bin/prefabs.o # Pattern rule: compile each module as a library unit @@ -56,7 +55,6 @@ test: @csi -s tests/assets-test.scm @csi -s tests/engine-test.scm @csi -s tests/animation-test.scm - @csi -s tests/ai-test.scm @csi -s tests/prefabs-test.scm @csi -s tests/scene-loader-test.scm @csi -s tests/tween-test.scm @@ -19,7 +19,6 @@ A 2D tile-driven game engine for Chicken Scheme, built on SDL2. Targets old-scho - Built-in update pipeline: input → acceleration → gravity → x-collision → y-collision → ground detection → entity collisions - Entities as plists — purely functional, no classes - Data-driven prefab/mixin system for composing entity types -- FSM-based enemy AI via the =states= egg - Configurable input: keyboard, joystick, and controller - Asset registry with =preload:= lifecycle hook - Scene and camera management @@ -53,10 +52,12 @@ System libraries: =SDL2=, =SDL2_image=, =SDL2_mixer=, =SDL2_ttf= Chicken eggs: #+begin_example -chicken-install sdl2 sdl2-image expat matchable defstruct states \ +chicken-install sdl2 sdl2-image expat matchable defstruct \ srfi-197 simple-logger srfi-64 #+end_example +Games that ship their own FSM logic (for example with the =states= egg) declare that egg themselves; Downstroke does not depend on it. + ** Building #+begin_src sh @@ -75,7 +76,7 @@ csi -s tests/physics-test.scm Modules live at the project root. Each compiles as a Chicken unit (=csc -c -J -unit=). Compile order follows the dependency graph: -: entity, tilemap → world → animation, physics, ai → input → prefabs → mixer → sound → engine +: entity, tilemap → world → animation, physics → input → prefabs → mixer → sound → engine | Module | Responsibility | |----------------+---------------------------------------------------| @@ -85,7 +86,6 @@ Modules live at the project root. Each compiles as a Chicken unit (=csc -c -J -u | =physics= | Gravity, velocity, AABB tile + entity collisions | | =input= | SDL2 events → action mapping, configurable binds | | =animation= | Frame/tick tracking, sprite ID mapping | -| =ai= | FSM enemy AI: idle → patrol → chase → attack | | =prefabs= | Mixin composition, entity instantiation | | =renderer= | =draw-sprite=, =draw-tilemap-layer=, =draw-text= | | =assets= | Asset registry for the =preload:= hook | @@ -1,135 +0,0 @@ -(module downstroke-ai * - (import scheme - (chicken base) - (chicken keyword) - (only srfi-1 find) - states - downstroke-entity - downstroke-world) - - ;; Patrol speed in pixels per frame - (define *patrol-speed* 1) - - ;; Chase behavior constants - (define *chase-speed* 2) - (define *detect-range-x* 80) - (define *detect-range-y* 16) - (define *chase-max-distance* 96) - (define *blind-spot-x* 16) - (define *attack-range-x* 20) - (define *attack-duration* 10) - - ;; ---- Geometry helpers ---- - - (define (entity-center-x e) - (+ (entity-ref e #:x 0) (/ (entity-ref e #:width 0) 2))) - - (define (entity-center-y e) - (+ (entity-ref e #:y 0) (/ (entity-ref e #:height 0) 2))) - - (define (direction-to entity target) - (if (>= (entity-center-x target) (entity-center-x entity)) 1 -1)) - - ;; ---- Entity update helpers ---- - - ;; Find player by #:tags list (tag-based lookup, Milestone 10) - (define (find-player entities) - (find (lambda (e) (member 'player (entity-ref e #:tags '()))) entities)) - - ;; Set both facing fields in the same direction - (define (face-toward entity dir) - (entity-set (entity-set entity #:ai-facing dir) #:facing dir)) - - ;; Face a direction and set horizontal velocity - (define (move-toward entity dir speed) - (entity-set (face-toward entity dir) #:vx (* speed dir))) - - ;; ---- Detection predicates ---- - - (define (player-in-range? entity entities) - (let ((player (find-player entities))) - (and player - (let ((dx (abs (- (entity-center-x player) (entity-center-x entity)))) - (dy (abs (- (entity-center-y player) (entity-center-y entity))))) - (and (<= dx *detect-range-x*) - (<= dy *detect-range-y*) - (not (and (< (entity-center-y player) (entity-center-y entity)) - (<= dx *blind-spot-x*)))))))) - - (define (player-in-attack-range? entity entities) - (let ((player (find-player entities))) - (and player - (<= (abs (- (entity-center-x player) (entity-center-x entity))) *attack-range-x*) - (<= (abs (- (entity-center-y player) (entity-center-y entity))) *detect-range-y*)))) - - ;; ---- State machine ---- - - (define (idle-trigger) #f) - (define (patrol-trigger) #f) - (define (chase-trigger) #f) - - (define (make-enemy-ai-machine) - (state-machine idle - (idle-trigger ! idle -> patrol) - (patrol-trigger ! patrol -> chase) - (chase-trigger ! chase -> patrol))) - - ;; ---- Per-state handlers ---- - - (define (ai-state-idle entity sm) - (%make-transition sm 'patrol) - (entity-set entity #:vx (* *patrol-speed* (entity-ref entity #:ai-facing 1)))) - - (define (patrol-wall-flip entity) - (let ((new-facing (- (entity-ref entity #:ai-facing 1)))) - (move-toward entity new-facing *patrol-speed*))) - - (define (ai-state-patrol entity entities sm) - (if (player-in-range? entity entities) - (let* ((player (find-player entities)) - (dir (direction-to entity player))) - (%make-transition sm 'chase) - (move-toward (entity-set entity #:chase-origin-x (entity-ref entity #:x 0)) - dir *chase-speed*)) - (if (zero? (entity-ref entity #:vx 0)) - (patrol-wall-flip entity) - entity))) - - (define (chase-give-up? entity entities) - (let ((distance-chased (abs (- (entity-ref entity #:x 0) - (entity-ref entity #:chase-origin-x 0))))) - (or (not (player-in-range? entity entities)) - (> distance-chased *chase-max-distance*)))) - - (define (ai-state-chase entity entities sm) - (if (player-in-attack-range? entity entities) - (let* ((player (find-player entities)) - (dir (direction-to entity player)) - (stopped (entity-set (face-toward entity dir) #:vx 0))) - (if (= (entity-ref stopped #:attack-timer 0) 0) - (entity-set stopped #:attack-timer *attack-duration*) - stopped)) - (if (chase-give-up? entity entities) - (begin - (%make-transition sm 'patrol) - (entity-set entity #:vx (* *patrol-speed* (entity-ref entity #:ai-facing 1)))) - (move-toward entity - (direction-to entity (find-player entities)) - *chase-speed*)))) - - ;; ---- Top-level dispatcher ---- - - ;; Update AI for a single enemy entity. - ;; entities: all entities in the current scene (used for player detection). - (define (update-enemy-ai entity entities) - (if (entity-ref entity #:disabled #f) - entity - (let ((sm (entity-ref entity #:ai-machine #f))) - (if (not sm) - entity - (case (machine-state sm) - ((idle) (ai-state-idle entity sm)) - ((patrol) (ai-state-patrol entity entities sm)) - ((chase) (ai-state-chase entity entities sm)) - (else entity)))))) -) diff --git a/docs/guide.org b/docs/guide.org index d98b00e..ede9180 100644 --- a/docs/guide.org +++ b/docs/guide.org @@ -34,9 +34,11 @@ brew install sdl2 sdl2_mixer sdl2_ttf sdl2_image expat Install the Downstroke egg along with its dependencies: #+begin_src bash -chicken-install downstroke sdl2 sdl2-image defstruct matchable states +chicken-install downstroke #+end_src +This pulls in the eggs declared by the Downstroke package. If your game uses the =states= egg for AI or other state machines, install it separately (=chicken-install states=). + * Hello World — Your First Game Create a file called =mygame.scm=: diff --git a/downstroke.egg b/downstroke.egg index 000328c..ab72665 100644 --- a/downstroke.egg +++ b/downstroke.egg @@ -37,12 +37,9 @@ (extension downstroke-animation (source "animation.scm") (component-dependencies downstroke-entity downstroke-world)) - (extension downstroke-ai - (source "ai.scm") - (component-dependencies downstroke-entity downstroke-world)) (extension downstroke-prefabs (source "prefabs.scm") - (component-dependencies downstroke-entity downstroke-ai)) + (component-dependencies downstroke-entity)) (extension downstroke-scene-loader (source "scene-loader.scm") (component-dependencies downstroke-world downstroke-tilemap downstroke-assets downstroke-engine downstroke-prefabs)))) diff --git a/prefabs.scm b/prefabs.scm index 798375a..5ae1255 100644 --- a/prefabs.scm +++ b/prefabs.scm @@ -4,8 +4,7 @@ (chicken keyword) (chicken port) defstruct - downstroke-entity - downstroke-ai) + downstroke-entity) ;; Registry struct to hold prefab data (defstruct prefab-registry @@ -15,8 +14,7 @@ (define (engine-mixins) '((physics-body #:vx 0 #:vy 0 #:ay 0 #:gravity? #t #:solid? #t #:on-ground? #f) (has-facing #:facing 1) - (animated #:anim-name idle #:anim-frame 0 #:anim-tick 0 #:tile-id 0) - (ai-body #:ai-facing 1 #:ai-machine #f #:chase-origin-x 0 #:disabled #f))) + (animated #:anim-name idle #:anim-frame 0 #:anim-tick 0 #:tile-id 0))) ;; Compose a prefab entry with mixin table ;; Returns (name . merged-plist) @@ -38,9 +36,7 @@ (merged (apply append inline-fields mixin-plists))) (cons name merged))) - ;; Engine-level hooks - (define *engine-hooks* - `((init-enemy-ai . ,(lambda (e) (entity-set e #:ai-machine (make-enemy-ai-machine)))))) + (define *engine-hooks* '()) ;; Lookup a hook symbol in the hook table (define (lookup-hook hook-table hook-sym) diff --git a/tests/ai-test.scm b/tests/ai-test.scm deleted file mode 100644 index 02a9806..0000000 --- a/tests/ai-test.scm +++ /dev/null @@ -1,48 +0,0 @@ -;; Mock entity module for testing -(module downstroke-entity * - (import scheme (chicken base) (chicken keyword)) - (define (entity-ref entity key #!optional default) - (get-keyword key entity (if (procedure? default) default (lambda () default)))) - (define (entity-set entity key val) - (cons key (cons val (let loop ((lst entity)) - (if (null? lst) '() - (if (eq? (car lst) key) - (cddr lst) - (cons (car lst) (cons (cadr lst) (loop (cddr lst)))))))))) - (define (entity-type e) (entity-ref e #:type #f))) - -;; Mock world module for testing -(module downstroke-world * - (import scheme (chicken base)) - (define (scene-entities s) s) - (define (scene-find-tagged scene tag) #f)) - -(import (srfi 64) - states - downstroke-entity - downstroke-world) - -(include "ai.scm") -(import downstroke-ai) - -(test-begin "ai") - -(test-group "find-player (tag-based)" - (let* ((player (list #:type 'player #:x 100 #:y 100 #:width 16 #:height 16 - #:tags '(player))) - (enemy (list #:type 'enemy #:x 200 #:y 100 #:width 16 #:height 16 - #:tags '(enemy))) - (entities (list enemy player))) - (test-equal "finds player by tags" player (find-player entities)) - (test-equal "returns #f with no player" #f (find-player (list enemy))))) - -(test-group "update-enemy-ai" - (let* ((entity (list #:type 'enemy #:x 0 #:y 0 #:width 16 #:height 16 - #:disabled #t))) - (test-equal "returns entity unchanged when disabled" entity - (update-enemy-ai entity '()))) - (let* ((entity (list #:type 'enemy #:x 0 #:y 0 #:width 16 #:height 16))) - (test-equal "returns entity unchanged when no ai-machine" entity - (update-enemy-ai entity '())))) - -(test-end "ai") diff --git a/tests/prefabs-test.scm b/tests/prefabs-test.scm index f8cec0a..e635d0a 100644 --- a/tests/prefabs-test.scm +++ b/tests/prefabs-test.scm @@ -24,12 +24,6 @@ (define (entity-type entity) (entity-ref entity #:type #f))) (import downstroke-entity) -;; Mock downstroke-ai -(module downstroke-ai * - (import scheme (chicken base)) - (define (make-enemy-ai-machine) 'mock-ai-machine)) -(import downstroke-ai) - ;; Load module under test (include "entity.scm") (import downstroke-entity) @@ -45,7 +39,6 @@ (test-assert "physics-body entry exists" (assq 'physics-body m)) (test-assert "has-facing entry exists" (assq 'has-facing m)) (test-assert "animated entry exists" (assq 'animated m)) - (test-assert "ai-body entry exists" (assq 'ai-body m)) (let ((pb (cdr (assq 'physics-body m)))) (test-equal "physics-body has #:vx 0" 0 (cadr (memq #:vx pb))) @@ -55,12 +48,7 @@ (let ((an (cdr (assq 'animated m)))) (test-equal "animated has #:anim-tick 0" 0 (cadr (memq #:anim-tick an))) (test-equal "animated has #:tile-id 0" 0 (cadr (memq #:tile-id an))) - (test-equal "animated has #:anim-name idle" 'idle (cadr (memq #:anim-name an)))) - - (let ((ai (cdr (assq 'ai-body m)))) - (test-equal "ai-body has #:ai-machine #f" #f (cadr (memq #:ai-machine ai))) - (test-equal "ai-body has #:disabled #f" #f (cadr (memq #:disabled ai))) - (test-equal "ai-body has #:chase-origin-x 0" 0 (cadr (memq #:chase-origin-x ai)))))) + (test-equal "animated has #:anim-name idle" 'idle (cadr (memq #:anim-name an)))))) (test-group "compose-prefab (via load-prefabs with temp file)" @@ -176,14 +164,18 @@ #t (entity-ref e #:initialized)))))) - (test-group "init-enemy-ai engine hook" - (with-hook-registry - "(npc ai-body has-facing #:type npc #:on-instantiate init-enemy-ai)" - '() - (lambda (reg) + (test-group "game hook via user-hooks (e.g. init-enemy-ai pattern)" + (let ((tmp "/tmp/test-prefabs-user-init.scm")) + (with-output-to-file tmp + (lambda () + (display + "((mixins (ai-body #:ai-facing 1 #:ai-machine #f #:chase-origin-x 0 #:disabled #f)) + (prefabs (npc ai-body has-facing #:type npc #:on-instantiate init-npc)))"))) + (let ((reg (load-prefabs tmp (engine-mixins) + `((init-npc . ,(lambda (e) (entity-set e #:ai-machine 'from-user-hook))))))) (let ((e (instantiate-prefab reg 'npc 0 0 16 16))) - (test-equal "engine hook sets #:ai-machine via make-enemy-ai-machine" - 'mock-ai-machine + (test-equal "user hook sets #:ai-machine" + 'from-user-hook (entity-ref e #:ai-machine)))))) (test-group "no hook: entity returned unchanged" |
