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 <noreply@anthropic.com>
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-27 21:51:35 +03:00
parent 537d3a0ab3
commit 4add35b4ca
9 changed files with 168 additions and 10 deletions

View file

@ -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
| | |

View file

@ -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

View file

@ -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 {

View file

@ -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"

View file

@ -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

View file

@ -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

View file

@ -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)
}
}

View file

@ -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

View file

@ -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()
}
}