ludic/examples/games/npcai_demo.ludic
Orkuncakilkaya 19fcf60599 wip(0.S3): packages and examples migrated again from their pre-0.S sources in one run
ludic migrate state packages <every example program> packages/ludic.lab/example/plate.ludic
  1804 vars into 126 states, 64 into lets; 23498 edits in 460 files

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 15:31:34 +03:00

119 lines
5.9 KiB
Text

# npcai_demo.ludic — the ludic.npcai stack (#61) exercised headlessly and
# deterministically, driving a body built from the ludic.shooter controller: the
# AI never moves the body directly, it writes the same intent fields the player
# controllers read, so the shooter's weapon/aim fire for an enemy unchanged.
#
# Proves: perception (spot a hostile), FSM chase (move toward), FSM attack (auto-aim
# + fire -> damage), utility flee (move away at low hp), and a companion follower.
#
# A full run prints: 1 1 1 1 1 1 1 1
#
# Build (from the repo root):
# LUDIC_MODULES=packages ludicc --headless examples/games/npcai_demo.ludic
program NpcAiDemo {
import "ludic.npcai/npcai.ludic"
import "ludic.shooter/shooter.ludic"
property Position { x: int = 0, y: int = 0 }
property Body {
vx: fixed = 0.0, vy: fixed = 0.0, gravity: fixed = 0.0, max_fall: fixed = 0.0,
rx: fixed = 0.0, ry: fixed = 0.0, policy: int = 1,
on_ground: int = 0, hit_wall: int = 0, hit_ceiling: int = 0
}
property Collider {
w: int = 0, h: int = 0, offx: int = 0, offy: int = 0,
is_trigger: int = 0, one_way: int = 0, layer: int = 0, mask: int = 0,
hit: int = 0, entered: int = 0, exited: int = 0
}
model Enemy { Position, Body, Collider, TopDown, Weapon, Faction, Stats, Vision, Memory, Brain }
model Ally { Position, Body, Collider, TopDown, Faction, Stats, Brain, Follower }
model Dummy { Position, Collider, Faction, Stats }
@OnSpawn(Enemy) handler EInit { }
@OnSpawn(Ally) handler AInit { }
@OnSpawn(Dummy) handler DInit { }
state NpcAiDemoState {
spotted: int = 0
}
@On(TargetSpotted) handler OnSpot(npc_ai_demo_st: mut NpcAiDemoState) { npc_ai_demo_st.spotted += 1 }
function tick_n(n: int) -> void { var i = 0; while i < n { tick_fixed(); i += 1 } }
function bi(b: bool) -> int { if b { return 1 }; return 0 }
function gx(e: int) -> int { let P = World.prop_id("Position"); return World.get(e, P, World.field_id(P, "x")) }
function gy(e: int) -> int { let P = World.prop_id("Position"); return World.get(e, P, World.field_id(P, "y")) }
function hp_of(e: int) -> int { let P = World.prop_id("Stats"); return World.get(e, P, World.field_id(P, "hp")) }
function set_pos(e: int, x: int, y: int) -> void {
let P = World.prop_id("Position")
World.set(e, P, World.field_id(P, "x"), x); World.set(e, P, World.field_id(P, "y"), y)
}
function set_hp(e: int, h: int) -> void { let P = World.prop_id("Stats"); World.set(e, P, World.field_id(P, "hp"), h) }
# the dummy target = Stats-carrying entity without a Brain.
function find_dummy() -> int {
let PS = World.prop_id("Stats")
let PBr = World.prop_id("Brain")
var e = World.query_next(PS, 0)
while e >= 0 { if World.has(e, PBr) == 0 { return e }; e = World.query_next(PS, e + 1) }
return -1
}
entry (npc_ai_demo_st: NpcAiDemoState) {
let PBr = World.prop_id("Brain")
Weapon.def("blaster", 4, 10, 10, 0, 1, 0)
# enemy (faction 2) auto-aims at the nearest enemy (aim_mode 3) and hunts with an
# FSM. dummy player (faction 1) sits 50px to the right.
spawn Enemy { Position { x: 100, y: 100 }, Collider { w: 12, h: 12 },
TopDown { move_speed: 3, aim_mode: 3 }, Weapon { def_id: 0 }, Faction { id: 2 },
Stats { hp: 40, max_hp: 40 }, Vision { range: 200, scan: 2 },
Brain { model: 0, attack_range: 60, flee_pct: 40 } }
let enemy = World.query_next(PBr, 0)
spawn Dummy { Position { x: 150, y: 100 }, Collider { w: 16, h: 16 }, Faction { id: 1 }, Stats { hp: 100, max_hp: 100 } }
let dummy = find_dummy()
# ---- A. perception: the enemy spots the hostile dummy -------------------
tick_n(4)
print(bi(npc_ai_demo_st.spotted >= 1)) # 1 — TargetSpotted fired
print(bi(Ai.target(enemy) == dummy)) # 1 — remembers the right target
# ---- B. FSM attack: in range -> auto-aim + fire -> dummy takes damage ---
let dhp0 = hp_of(dummy)
tick_n(40)
print(bi(Ai.state(enemy) == 2)) # 1 — attack state (dummy within 60px)
print(bi(hp_of(dummy) < dhp0)) # 1 — the AI's weapon hit the player
# ---- C. FSM chase: move the dummy away (still within vision) -> advance -
set_pos(dummy, 220, 100) # 120px: within range 200, beyond attack 60
tick_n(4) # re-perceive + begin chasing
print(bi(Ai.state(enemy) == 1)) # 1 — chase state (still out of attack range)
let ex0 = gx(enemy)
tick_n(30)
print(bi(gx(enemy) > ex0)) # 1 — advanced toward the target
# ---- D. utility flee: switch model + drop hp -> retreat ----------------
set_pos(enemy, 200, 100)
set_pos(dummy, 320, 100) # dummy to the right
Ai.set_model(enemy, 1) # utility
set_hp(enemy, 4) # 10% of 40 < flee_pct 40
tick_n(4)
let fx0 = gx(enemy)
tick_n(20)
print(bi(gx(enemy) < fx0)) # 1 — fled left, away from the target
# ---- E. companion follower advances toward its leader ------------------
spawn Ally { Position { x: 40, y: 40 }, Collider { w: 12, h: 12 },
TopDown { move_speed: 3 }, Faction { id: 2 }, Stats { hp: 30, max_hp: 30 },
Brain { model: 0 }, Follower { leader: -1, distance: 24 } }
# the ally is the Brain+Follower entity that isn't the enemy
var ally = World.query_next(PBr, 0)
while (ally >= 0) and (World.has(ally, World.prop_id("Follower")) == 0) { ally = World.query_next(PBr, ally + 1) }
Follower.set(ally, enemy, 24, 0) # follow the enemy as leader
set_pos(enemy, 300, 300) # leader far away
let ax0 = gx(ally)
tick_n(20)
print(bi(gx(ally) > ax0)) # 1 — companion moved toward the leader
quit()
}
}