From 4add35b4cae098c22cf491f6131cf7a492ae67a1 Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Sun, 27 Sep 2026 21:51:35 +0300 Subject: [PATCH] feat(wildlife): 17.5 - an animal walks by the world's way where it has one: WildlifeWorld.way (the next corner toward where it is going, asked every half second or on reaching it) steers wandering and going to water or forage, and goal (a spot it can reach) is a wander's target, seeded by the same two draws so the dice do not move; the defaults are the straight line and the old target. way_test: round a wall by its corners, and a wander's target where the world says Co-Authored-By: Claude Opus 5.5 --- packages/ludic.wildlife/README.md | 9 ++ packages/ludic.wildlife/animal.ludic | 3 + packages/ludic.wildlife/ground.ludic | 2 +- packages/ludic.wildlife/index.ludic | 1 + packages/ludic.wildlife/port.ludic | 9 ++ packages/ludic.wildlife/steer.ludic | 9 +- packages/ludic.wildlife/tests/way_test.ludic | 106 +++++++++++++++++++ packages/ludic.wildlife/want.ludic | 3 +- packages/ludic.wildlife/way.ludic | 36 +++++++ 9 files changed, 168 insertions(+), 10 deletions(-) create mode 100644 packages/ludic.wildlife/tests/way_test.ludic create mode 100644 packages/ludic.wildlife/way.ludic diff --git a/packages/ludic.wildlife/README.md b/packages/ludic.wildlife/README.md index c9d5b67b..67cf0306 100644 --- a/packages/ludic.wildlife/README.md +++ b/packages/ludic.wildlife/README.md @@ -46,9 +46,18 @@ export port WildlifeWorld { # every member has a default: a flat dry mead next_id: fn() -> int # an animal's id across machines born: fn(WildAnimal) -> void # draw it placed: fn(WildRange, int) -> void # a range was set out: put its water and forage down + way: fn(WildAnimal, float, float) -> bool # the next corner of its way toward (x, z), into way_x / _z + goal: fn(WildAnimal, x, z, r, seed) -> bool # a spot about r from (x, z) it can walk to, into way_x / _z } ``` +**An animal walks by the world's way where the world has one.** Wandering and going to water or +forage face the next corner `way` gives (asked again every half second, or on reaching the corner), +and a wander's target is a spot `goal` says it can reach, seeded by the same two draws the old +target took - so binding them changes where an animal walks and never the order of the dice. With +no answer (the defaults) it walks straight and turns 75 degrees off water and a climb, as before. +Maroon Lake binds both to its navmesh, walked by the species' habitat's taste. + ## API | | | diff --git a/packages/ludic.wildlife/animal.ludic b/packages/ludic.wildlife/animal.ludic index a5f3ef4c..7ebf36dd 100644 --- a/packages/ludic.wildlife/animal.ludic +++ b/packages/ludic.wildlife/animal.ludic @@ -41,6 +41,9 @@ export property WildAnimal { home_z: float = 0.0 tx: float = 0.0 tz: float = 0.0 + wx: float = 0.0 # the corner of its way it is walking to now (WildlifeWorld.way) + wz: float = 0.0 + way_t: float = 0.0 # seconds until it asks the way again; 0 asks at once thirst: float = 0.0 # 0..100: over 60 it goes looking for water hunger: float = 0.0 scat_t: float = 0.0 # game minutes until it might leave sign diff --git a/packages/ludic.wildlife/ground.ludic b/packages/ludic.wildlife/ground.ludic index ae9339c0..26313853 100644 --- a/packages/ludic.wildlife/ground.ludic +++ b/packages/ludic.wildlife/ground.ludic @@ -31,7 +31,7 @@ export function wildlife_tick_ground(wildlife_st: mut WildlifeState, a: WildAnim } return } - wl_face(a, a.tx, a.tz, 2.0, dt) + wl_head(a, a.tx, a.tz, 2.0, dt) a.speed = s.walk wl_step(wildlife_st, a, dt) if wl_at_target(a, 1.5) or a.timer < 0.0 { diff --git a/packages/ludic.wildlife/index.ludic b/packages/ludic.wildlife/index.ludic index 832aa192..f768c96b 100644 --- a/packages/ludic.wildlife/index.ludic +++ b/packages/ludic.wildlife/index.ludic @@ -16,6 +16,7 @@ import "ranges.ludic" import "migrate.ludic" import "senses.ludic" import "steer.ludic" +import "way.ludic" import "lures.ludic" import "needs.ludic" import "ground.ludic" diff --git a/packages/ludic.wildlife/port.ludic b/packages/ludic.wildlife/port.ludic index 5fb77d1c..7c599b5c 100644 --- a/packages/ludic.wildlife/port.ludic +++ b/packages/ludic.wildlife/port.ludic @@ -46,6 +46,13 @@ export port WildlifeWorld { next_id: fn() -> int = fn wl_next_id born: fn(WildAnimal) -> void = fn wl_nothing # a new animal: draw it placed: fn(WildRange, int) -> void = fn wl_range_nothing # a range was set out + # (a, x, z): the next corner of this animal's way toward (x, z) into way_x / _z; false, the + # straight line (a navmesh's path by its habitat's taste, in Maroon Lake) + way: fn(WildAnimal, float, float) -> bool = fn wl_no_way + # (a, x, z, r, seed): a spot about r from (x, z) this animal can walk to, into way_x / _z + goal: fn(WildAnimal, float, float, float, int) -> bool = fn wl_no_goal + way_x: fn() -> float = fn wl_asked_x + way_z: fn() -> float = fn wl_asked_z } function wl_zero0() -> float { return 0.0 } function wl_zero1(p: int) -> float { return 0.0 } @@ -73,6 +80,8 @@ function wl_no_spot(a: WildAnimal, water: bool, s: WildSpot) -> bool { return fa function wl_no_lure(i: int, l: WildLure) -> bool { return false } function wl_nothing(a: WildAnimal) -> void { } function wl_range_nothing(r: WildRange, i: int) -> void { } +function wl_no_way(a: WildAnimal, x: float, z: float) -> bool { return false } +function wl_no_goal(a: WildAnimal, x: float, z: float, r: float, seed: int) -> bool { return false } function wl_next_id(wildlife_st: mut WildlifeState) -> int { wildlife_st.wl_ids += 1 return wildlife_st.wl_ids diff --git a/packages/ludic.wildlife/steer.ludic b/packages/ludic.wildlife/steer.ludic index b561d5cc..4e73371e 100644 --- a/packages/ludic.wildlife/steer.ludic +++ b/packages/ludic.wildlife/steer.ludic @@ -1,5 +1,5 @@ # steer.ludic - turning toward a point at a rate, a step along the facing that turns away from water -# and a climb, and a target somewhere about home; a walker leaves a print every 2.8 m +# and a climb; a walker leaves a print every 2.8 m (the way there and a target about home: way.ludic) function wl_wrap(a: float) -> float { var r = a while r > 3.1415927 { r = r - WL_TAU } @@ -58,13 +58,6 @@ function wl_push(wildlife_st: mut WildlifeState, a: WildAnimal, s: WildSpecies, return hit } -function wl_pick_target(wildlife_st: WildlifeState, a: WildAnimal, radius: float) -> void { - let ang = wl_rnd(wildlife_st) * WL_TAU - let d = wl_rnd(wildlife_st) * radius - a.tx = a.home_x + Math.cos(ang) * d - a.tz = a.home_z + Math.sin(ang) * d -} - function wl_at_target(a: WildAnimal, r: float) -> bool { let dx = a.tx - a.x let dz = a.tz - a.z diff --git a/packages/ludic.wildlife/tests/way_test.ludic b/packages/ludic.wildlife/tests/way_test.ludic new file mode 100644 index 00000000..b9624030 --- /dev/null +++ b/packages/ludic.wildlife/tests/way_test.ludic @@ -0,0 +1,106 @@ +# way_test.ludic - an animal walks by the world's way where the world has one: round a wall the +# straight line runs into (by the corners WildlifeWorld.way gives), and to a wander target the world +# says it can reach (goal); with no answer it walks straight, as before +import "ludic.wildlife" +import "ludic.base" +import "fake" +program WayTest { + numbers float + bind WildlifeWorld { + ground: fn fk_ground + water: fn fk_water + forest: fn fk_forest + sight: fn fk_sight + push: fn fk_push + pushed_x: fn fk_pushed_x + pushed_z: fn fk_pushed_z + players: fn fk_players + player_on: fn fk_player_on + player_x: fn fk_x + player_y: fn fk_y + player_z: fn fk_z + hidden: fn fk_is_hidden + noise: fn fk_noise_of + scent: fn fk_scent_of + stillness: fn fk_still_of + wind: fn fk_wind_of + wind_dir: fn fk_wind_to + winterness: fn fk_winter_of + about: fn fk_about_of + per_range: fn fk_one + lures: fn fk_count + lure: fn fk_lure + present: fn fk_present + born: fn fk_on_born + spot: fn fk_spot + way: fn wt_way + way_x: fn wt_x + way_z: fn wt_z + goal: fn wt_goal + } + + state WayTestState { + wx: float = 0.0 + wz: float = 0.0 + asked: int = 0 + } + # a wall across z = -40 for |x| < 30: the way goes round its east end, then on to the target + function wt_way(way_test_st: mut WayTestState, a: WildAnimal, x: float, z: float) -> bool { + way_test_st.asked += 1 + way_test_st.wx = x + way_test_st.wz = z + if a.z > -38.0 and a.x < 31.0 { + way_test_st.wx = 32.0 + way_test_st.wz = -40.0 + } + return true + } + # a wander target a known step from home, whatever the seed + function wt_goal(way_test_st: mut WayTestState, a: WildAnimal, x: float, z: float, r: float, seed: int) -> bool { + way_test_st.wx = x + 7.0 + way_test_st.wz = z + 3.0 + return true + } + function wt_x(way_test_st: WayTestState) -> float { return way_test_st.wx } + function wt_z(way_test_st: WayTestState) -> float { return way_test_st.wz } + + test "it walks round the wall by the corners it is given, and asks again as it goes" (wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState, way_test_st: WayTestState) { + fk_setup(wildlife_st, wildlife_tests_fake_st) + let a = wildlife_make(wildlife_st, 0, 0.0, 0.0, false, 0.0, 0, 1.0, 1) + wildlife_tests_fake_st.fk_px[0] = 400.0 + wildlife_tests_fake_st.fk_pz[0] = 0.0 + wildlife_tests_fake_st.fk_hidden = true + a.state = WILD_WANDER + a.tx = 0.0 + a.tz = -100.0 + a.timer = 300.0 + var east = -100.0 + for k in 0 .. 3000 { + wildlife_tick(wildlife_st, 0.05, 0.0) + if a.z > -42.0 and a.z < -38.0 { east = Math.max(east, a.x) } + } + expect(east > 29.0) + expect(a.z < -60.0) + expect(way_test_st.asked > 10) + } + + test "a wander target is where the world says it can walk" (wildlife_st: mut WildlifeState, wildlife_tests_fake_st: mut WildlifeTestsFakeState, way_test_st: WayTestState) { + fk_setup(wildlife_st, wildlife_tests_fake_st) + let a = wildlife_make(wildlife_st, 0, 5.0, 5.0, false, 0.0, 0, 1.0, 1) + wildlife_tests_fake_st.fk_px[0] = 400.0 + wildlife_tests_fake_st.fk_pz[0] = 0.0 + wildlife_tests_fake_st.fk_hidden = true + a.hunger = 0.0 + a.thirst = 0.0 + a.state = WILD_IDLE + a.timer = -1.0 + var k = 0 + while a.state != WILD_WANDER and k < 2000 { + wildlife_tick(wildlife_st, 0.05, 0.0) + k += 1 + } + expect_eq(a.state, WILD_WANDER) + expect_near(a.tx, a.home_x + 7.0, 0.001) + expect_near(a.tz, a.home_z + 3.0, 0.001) + } +} diff --git a/packages/ludic.wildlife/want.ludic b/packages/ludic.wildlife/want.ludic index 696d6005..ebe42f43 100644 --- a/packages/ludic.wildlife/want.ludic +++ b/packages/ludic.wildlife/want.ludic @@ -10,7 +10,7 @@ function wl_seek(wildlife_st: mut WildlifeState, a: WildAnimal, dt: float) -> bo a.timer = 2.0 return true } - wl_face(a, a.spot_x, a.spot_z, 2.0, dt) + wl_head(a, a.spot_x, a.spot_z, 2.0, dt) a.speed = wl_sp(wildlife_st, a.sp).walk a.tx = a.spot_x a.tz = a.spot_z @@ -44,6 +44,7 @@ function wl_drink(wildlife_st: WildlifeState, a: WildAnimal, dt: float, gh: floa let water = a.thirst > a.hunger if not WildlifeWorld.spot(a, water, wildlife_st.wl_spot_ask) { return false } a.spot = wildlife_st.wl_spot_ask.uid + a.way_t = 0.0 a.spot_x = wildlife_st.wl_spot_ask.x a.spot_z = wildlife_st.wl_spot_ask.z a.spot_water = wildlife_st.wl_spot_ask.water diff --git a/packages/ludic.wildlife/way.ludic b/packages/ludic.wildlife/way.ludic new file mode 100644 index 00000000..ddc00e31 --- /dev/null +++ b/packages/ludic.wildlife/way.ludic @@ -0,0 +1,36 @@ +# way.ludic - where an animal walks: by the world's path to where it is going (WildlifeWorld.way, a +# navmesh by its habitat's taste in Maroon Lake), to a target about home it can reach (goal), and +# straight, as before, where the world has no answer + +# face the way toward (tx, tz): the next corner of the world's path for it, asked again every +# WL_WAY_EVERY seconds or on reaching the corner; without a path, the straight line as before +const WL_WAY_EVERY: float = 0.5 +function wl_head(a: WildAnimal, tx: float, tz: float, rate: float, dt: float) -> void { + a.way_t = a.way_t - dt + let cx = a.wx - a.x + let cz = a.wz - a.z + if a.way_t <= 0.0 or cx * cx + cz * cz < 1.0 { + a.way_t = WL_WAY_EVERY + a.wx = tx + a.wz = tz + if WildlifeWorld.way(a, tx, tz) { + a.wx = WildlifeWorld.way_x() + a.wz = WildlifeWorld.way_z() + } + } + wl_face(a, a.wx, a.wz, rate, dt) +} + +# somewhere about home: a spot the world says it can walk to when it knows, seeded by the same two +# draws the old target took, so the dice are the same whether it knows or not +function wl_pick_target(wildlife_st: WildlifeState, a: WildAnimal, radius: float) -> void { + let ang = wl_rnd(wildlife_st) * WL_TAU + let d = wl_rnd(wildlife_st) * radius + a.tx = a.home_x + Math.cos(ang) * d + a.tz = a.home_z + Math.sin(ang) * d + a.way_t = 0.0 + if WildlifeWorld.goal(a, a.home_x, a.home_z, radius, int(ang * 100000.0) + int(d * 1000.0)) { + a.tx = WildlifeWorld.way_x() + a.tz = WildlifeWorld.way_z() + } +}