aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Makefile6
-rw-r--r--README.org8
-rw-r--r--ai.scm135
-rw-r--r--docs/guide.org4
-rw-r--r--downstroke.egg5
-rw-r--r--prefabs.scm10
-rw-r--r--tests/ai-test.scm48
-rw-r--r--tests/prefabs-test.scm32
8 files changed, 25 insertions, 223 deletions
diff --git a/Makefile b/Makefile
index 990d589..c0052b8 100644
--- a/Makefile
+++ b/Makefile
@@ -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
diff --git a/README.org b/README.org
index 45db408..ef5ecd8 100644
--- a/README.org
+++ b/README.org
@@ -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 |
diff --git a/ai.scm b/ai.scm
deleted file mode 100644
index 9240eba..0000000
--- a/ai.scm
+++ /dev/null
@@ -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"