Switch to combinator-driven NPC AI API

This commit is contained in:
Joshua Barretto 2023-01-04 11:25:39 +00:00
parent acecc62d40
commit b2f92e4a6c
5 changed files with 376 additions and 200 deletions

View File

@ -1,4 +1,4 @@
use crate::rule::npc_ai;
use crate::rule::npc_ai::NpcCtx;
pub use common::rtsim::{NpcId, Profession};
use common::{
comp,
@ -50,221 +50,295 @@ pub struct Controller {
pub goto: Option<(Vec3<f32>, f32)>,
}
#[derive(Default)]
pub struct TaskState {
state: Option<Box<dyn Any + Send + Sync>>,
impl Controller {
pub fn idle() -> Self { Self { goto: None } }
}
pub const CONTINUE: ControlFlow<()> = ControlFlow::Break(());
pub const FINISH: ControlFlow<()> = ControlFlow::Continue(());
pub trait Action<R = ()>: Any + Send + Sync {
/// Returns `true` if the action should be considered the 'same' (i.e:
/// achieving the same objective) as another. In general, the AI system
/// will try to avoid switching (and therefore restarting) tasks when the
/// new task is the 'same' as the old one.
// TODO: Figure out a way to compare actions based on their 'intention': i.e:
// two pathing actions should be considered equivalent if their destination
// is the same regardless of the progress they've each made.
fn is_same(&self, other: &Self) -> bool
where
Self: Sized;
fn dyn_is_same_sized(&self, other: &dyn Action<R>) -> bool
where
Self: Sized,
{
match (other as &dyn Any).downcast_ref::<Self>() {
Some(other) => self.is_same(other),
None => false,
}
}
fn dyn_is_same(&self, other: &dyn Action<R>) -> bool;
// Reset the action to its initial state so it can be restarted
fn reset(&mut self);
pub trait Task: PartialEq + Clone + Send + Sync + 'static {
type State: Send + Sync;
type Ctx<'a>;
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R>;
fn begin<'a>(&self, ctx: &Self::Ctx<'a>) -> Self::State;
fn run<'a>(
&self,
state: &mut Self::State,
ctx: &Self::Ctx<'a>,
controller: &mut Controller,
) -> ControlFlow<()>;
fn then<B: Task>(self, other: B) -> Then<Self, B> { Then(self, other) }
fn repeat(self) -> Repeat<Self> { Repeat(self) }
fn then<A1: Action<R1>, R1>(self, other: A1) -> Then<Self, A1, R>
where
Self: Sized,
{
Then {
a0: self,
a0_finished: false,
a1: other,
phantom: PhantomData,
}
}
fn repeat<R1>(self) -> Repeat<Self, R1>
where
Self: Sized,
{
Repeat(self, PhantomData)
}
fn stop_if<F: FnMut(&mut NpcCtx) -> bool>(self, f: F) -> StopIf<Self, F>
where
Self: Sized,
{
StopIf(self, f)
}
fn map<F: FnMut(R) -> R1, R1>(self, f: F) -> Map<Self, F, R>
where
Self: Sized,
{
Map(self, f, PhantomData)
}
}
#[derive(Clone, PartialEq)]
pub struct Then<A, B>(A, B);
// Now
impl<A: Task, B> Task for Then<A, B>
where
B: for<'a> Task<Ctx<'a> = A::Ctx<'a>>,
#[derive(Copy, Clone)]
pub struct Now<F, A>(F, Option<A>);
impl<R: Send + Sync + 'static, F: FnMut(&mut NpcCtx) -> A + Send + Sync + 'static, A: Action<R>>
Action<R> for Now<F, A>
{
// TODO: Use `Either` instead
type Ctx<'a> = A::Ctx<'a>;
type State = Result<A::State, B::State>;
// TODO: This doesn't compare?!
fn is_same(&self, other: &Self) -> bool { true }
fn begin<'a>(&self, ctx: &Self::Ctx<'a>) -> Self::State { Ok(self.0.begin(ctx)) }
fn dyn_is_same(&self, other: &dyn Action<R>) -> bool { self.dyn_is_same_sized(other) }
fn run<'a>(
&self,
state: &mut Self::State,
ctx: &Self::Ctx<'a>,
controller: &mut Controller,
) -> ControlFlow<()> {
match state {
Ok(a_state) => {
self.0.run(a_state, ctx, controller)?;
*state = Err(self.1.begin(ctx));
CONTINUE
},
Err(b_state) => self.1.run(b_state, ctx, controller),
}
fn reset(&mut self) { self.1 = None; }
// TODO: Reset closure state?
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R> {
(self.1.get_or_insert_with(|| (self.0)(ctx))).tick(ctx)
}
}
#[derive(Clone, PartialEq)]
pub struct Repeat<A>(A);
impl<A: Task> Task for Repeat<A> {
type Ctx<'a> = A::Ctx<'a>;
type State = A::State;
fn begin<'a>(&self, ctx: &Self::Ctx<'a>) -> Self::State { self.0.begin(ctx) }
fn run<'a>(
&self,
state: &mut Self::State,
ctx: &Self::Ctx<'a>,
controller: &mut Controller,
) -> ControlFlow<()> {
self.0.run(state, ctx, controller)?;
*state = self.0.begin(ctx);
CONTINUE
}
pub fn now<F, A>(f: F) -> Now<F, A>
where
F: FnMut(&mut NpcCtx) -> A,
{
Now(f, None)
}
impl TaskState {
pub fn perform<'a, T: Task>(
&mut self,
task: T,
ctx: &T::Ctx<'a>,
controller: &mut Controller,
) -> ControlFlow<()> {
type StateOf<T> = (T, <T as Task>::State);
// Just
let mut state = if let Some(state) = self.state.take().and_then(|state| {
state
.downcast::<StateOf<T>>()
.ok()
.filter(|state| state.0 == task)
}) {
state
} else {
let mut state = task.begin(ctx);
Box::new((task, state))
};
#[derive(Copy, Clone)]
pub struct Just<F, R = ()>(F, PhantomData<R>);
let res = state.0.run(&mut state.1, ctx, controller);
impl<R: Send + Sync + 'static, F: FnMut(&mut NpcCtx) -> R + Send + Sync + 'static> Action<R>
for Just<F, R>
{
fn is_same(&self, other: &Self) -> bool { true }
self.state = if matches!(res, ControlFlow::Break(())) {
Some(state)
} else {
None
};
fn dyn_is_same(&self, other: &dyn Action<R>) -> bool { self.dyn_is_same_sized(other) }
res
}
fn reset(&mut self) {}
// TODO: Reset closure state?
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R> { ControlFlow::Break((self.0)(ctx)) }
}
pub unsafe trait Context {
// TODO: Somehow we need to enforce this bound, I think?
// Hence, this trait is unsafe for now.
type Ty<'a>; // where for<'a> Self::Ty<'a>: 'a;
pub fn just<F, R: Send + Sync + 'static>(mut f: F) -> Just<F, R>
where
F: FnMut(&mut NpcCtx) -> R + Send + Sync + 'static,
{
Just(f, PhantomData)
}
pub struct Data<C: Context>(Arc<AtomicPtr<()>>, PhantomData<C>);
impl<C: Context> Clone for Data<C> {
fn clone(&self) -> Self { Self(self.0.clone(), PhantomData) }
}
impl<C: Context> Data<C> {
pub fn with<R>(&mut self, f: impl FnOnce(&mut C::Ty<'_>) -> R) -> R {
let ptr = self.0.swap(std::ptr::null_mut(), Ordering::Acquire);
if ptr.is_null() {
panic!("Data pointer was null, you probably tried to access data recursively")
} else {
// Safety: We have exclusive access to the pointer within this scope.
// TODO: Do we need a panic guard here?
let r = f(unsafe { &mut *(ptr as *mut C::Ty<'_>) });
self.0.store(ptr, Ordering::Release);
r
}
}
}
// Tree
pub type Priority = usize;
pub struct TaskBox<C: Context, A = ()> {
task: Option<(
TypeId,
Box<dyn Generator<Data<C>, Yield = A, Return = ()> + Unpin + Send + Sync>,
Priority,
)>,
data: Data<C>,
const URGENT: Priority = 0;
const CASUAL: Priority = 1;
pub struct Node<R>(Box<dyn Action<R>>, Priority);
pub fn urgent<A: Action<R>, R>(a: A) -> Node<R> { Node(Box::new(a), URGENT) }
pub fn casual<A: Action<R>, R>(a: A) -> Node<R> { Node(Box::new(a), CASUAL) }
pub struct Tree<F, R> {
next: F,
prev: Option<Node<R>>,
interrupt: bool,
}
impl<C: Context, A> TaskBox<C, A> {
pub fn new(data: Data<C>) -> Self { Self { task: None, data } }
impl<F: FnMut(&mut NpcCtx) -> Node<R> + Send + Sync + 'static, R: 'static> Action<R>
for Tree<F, R>
{
fn is_same(&self, other: &Self) -> bool { true }
#[must_use]
pub fn finish(&mut self, prio: Priority) -> ControlFlow<A> {
if let Some((_, task, _)) = &mut self.task.as_mut().filter(|(_, _, p)| *p <= prio) {
match Pin::new(task).resume(self.data.clone()) {
GeneratorState::Yielded(action) => ControlFlow::Break(action),
GeneratorState::Complete(_) => {
self.task = None;
ControlFlow::Continue(())
},
}
} else {
ControlFlow::Continue(())
}
}
fn dyn_is_same(&self, other: &dyn Action<R>) -> bool { self.dyn_is_same_sized(other) }
#[must_use]
pub fn perform<T: Generator<Data<C>, Yield = A, Return = ()> + Unpin + Any + Send + Sync>(
&mut self,
prio: Priority,
task: T,
) -> ControlFlow<A> {
let ty = TypeId::of::<T>();
if self
.task
.as_mut()
.filter(|(ty1, _, _)| *ty1 == ty)
.is_none()
{
self.task = Some((ty, Box::new(task), prio));
fn reset(&mut self) { self.prev = None; }
// TODO: Reset `next` too?
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R> {
let new = (self.next)(ctx);
let prev = match &mut self.prev {
Some(prev) if prev.1 <= new.1 && (prev.0.dyn_is_same(&*new.0) || !self.interrupt) => {
prev
},
_ => self.prev.insert(new),
};
self.finish(prio)
}
}
pub struct Brain<C: Context, A = ()> {
task: Box<dyn Generator<Data<C>, Yield = A, Return = !> + Unpin + Send + Sync>,
data: Data<C>,
}
impl<C: Context, A> Brain<C, A> {
pub fn new<T: Generator<Data<C>, Yield = A, Return = !> + Unpin + Any + Send + Sync>(
task: T,
) -> Self {
Self {
task: Box::new(task),
data: Data(Arc::new(AtomicPtr::new(std::ptr::null_mut())), PhantomData),
}
}
pub fn tick(&mut self, ctx_ref: &mut C::Ty<'_>) -> A {
self.data
.0
.store(ctx_ref as *mut C::Ty<'_> as *mut (), Ordering::SeqCst);
match Pin::new(&mut self.task).resume(self.data.clone()) {
GeneratorState::Yielded(action) => {
self.data.0.store(std::ptr::null_mut(), Ordering::Release);
action
match prev.0.tick(ctx) {
ControlFlow::Continue(()) => return ControlFlow::Continue(()),
ControlFlow::Break(r) => {
self.prev = None;
ControlFlow::Break(r)
},
GeneratorState::Complete(ret) => match ret {},
}
}
}
pub fn choose<R: 'static, F>(f: F) -> impl Action<R>
where
F: FnMut(&mut NpcCtx) -> Node<R> + Send + Sync + 'static,
{
Tree {
next: f,
prev: None,
interrupt: false,
}
}
pub fn watch<R: 'static, F>(f: F) -> impl Action<R>
where
F: FnMut(&mut NpcCtx) -> Node<R> + Send + Sync + 'static,
{
Tree {
next: f,
prev: None,
interrupt: true,
}
}
// Then
#[derive(Copy, Clone)]
pub struct Then<A0, A1, R0> {
a0: A0,
a0_finished: bool,
a1: A1,
phantom: PhantomData<R0>,
}
impl<A0: Action<R0>, A1: Action<R1>, R0: Send + Sync + 'static, R1: Send + Sync + 'static>
Action<R1> for Then<A0, A1, R0>
{
fn is_same(&self, other: &Self) -> bool {
self.a0.is_same(&other.a0) && self.a1.is_same(&other.a1)
}
fn dyn_is_same(&self, other: &dyn Action<R1>) -> bool { self.dyn_is_same_sized(other) }
fn reset(&mut self) {
self.a0.reset();
self.a0_finished = false;
self.a1.reset();
}
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R1> {
if !self.a0_finished {
match self.a0.tick(ctx) {
ControlFlow::Continue(()) => return ControlFlow::Continue(()),
ControlFlow::Break(_) => self.a0_finished = true,
}
}
self.a1.tick(ctx)
}
}
// Repeat
#[derive(Copy, Clone)]
pub struct Repeat<A, R = ()>(A, PhantomData<R>);
impl<R: Send + Sync + 'static, A: Action<R>> Action<!> for Repeat<A, R> {
fn is_same(&self, other: &Self) -> bool { self.0.is_same(&other.0) }
fn dyn_is_same(&self, other: &dyn Action<!>) -> bool { self.dyn_is_same_sized(other) }
fn reset(&mut self) { self.0.reset(); }
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<!> {
match self.0.tick(ctx) {
ControlFlow::Continue(()) => ControlFlow::Continue(()),
ControlFlow::Break(_) => {
self.0.reset();
ControlFlow::Continue(())
},
}
}
}
// StopIf
#[derive(Copy, Clone)]
pub struct StopIf<A, F>(A, F);
impl<A: Action<R>, F: FnMut(&mut NpcCtx) -> bool + Send + Sync + 'static, R> Action<Option<R>>
for StopIf<A, F>
{
fn is_same(&self, other: &Self) -> bool { self.0.is_same(&other.0) }
fn dyn_is_same(&self, other: &dyn Action<Option<R>>) -> bool { self.dyn_is_same_sized(other) }
fn reset(&mut self) { self.0.reset(); }
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<Option<R>> {
if (self.1)(ctx) {
ControlFlow::Break(None)
} else {
self.0.tick(ctx).map_break(Some)
}
}
}
// Map
#[derive(Copy, Clone)]
pub struct Map<A, F, R>(A, F, PhantomData<R>);
impl<A: Action<R>, F: FnMut(R) -> R1 + Send + Sync + 'static, R: Send + Sync + 'static, R1>
Action<R1> for Map<A, F, R>
{
fn is_same(&self, other: &Self) -> bool { self.0.is_same(&other.0) }
fn dyn_is_same(&self, other: &dyn Action<R1>) -> bool { self.dyn_is_same_sized(other) }
fn reset(&mut self) { self.0.reset(); }
fn tick(&mut self, ctx: &mut NpcCtx) -> ControlFlow<R1> {
self.0.tick(ctx).map_break(&mut self.1)
}
}
pub struct Brain {
pub(crate) action: Box<dyn Action<!>>,
}
#[derive(Serialize, Deserialize)]
pub struct Npc {
// Persisted state
@ -292,10 +366,7 @@ pub struct Npc {
pub mode: NpcMode,
#[serde(skip_serializing, skip_deserializing)]
pub task_state: Option<TaskState>,
#[serde(skip_serializing, skip_deserializing)]
pub brain: Option<Brain<npc_ai::NpcData<'static>>>,
pub brain: Option<Brain>,
}
impl Clone for Npc {
@ -310,7 +381,6 @@ impl Clone for Npc {
current_site: Default::default(),
goto: Default::default(),
mode: Default::default(),
task_state: Default::default(),
brain: Default::default(),
}
}
@ -330,8 +400,7 @@ impl Npc {
current_site: None,
goto: None,
mode: NpcMode::Simulated,
task_state: Default::default(),
brain: Some(npc_ai::brain()),
brain: None,
}
}

View File

@ -72,12 +72,12 @@ impl Data {
.map(|e| e as f32 + 0.5)
.with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0))
};
for _ in 0..1 {
for _ in 0..10 {
this.npcs.create(
Npc::new(rng.gen(), rand_wpos(&mut rng))
.with_faction(site.faction)
.with_home(site_id)
.with_profession(match 1/*rng.gen_range(0..20)*/ {
.with_profession(match rng.gen_range(0..20) {
0 => Profession::Hunter,
1 => Profession::Blacksmith,
2 => Profession::Chef,

View File

@ -4,7 +4,9 @@
try_blocks,
generator_trait,
generators,
trait_alias
trait_alias,
trait_upcasting,
control_flow_enum
)]
pub mod data;

View File

@ -3,8 +3,8 @@ use std::{collections::VecDeque, hash::BuildHasherDefault};
use crate::{
data::{
npc::{
Brain, Context, Controller, Data, Npc, NpcId, PathData, PathingMemory, Task, TaskBox,
TaskState, CONTINUE, FINISH,
casual, choose, just, now, urgent, Action, Brain, Controller, Npc, NpcId, PathData,
PathingMemory,
},
Sites,
},
@ -240,21 +240,27 @@ impl Rule for NpcAi {
let npc_ids = ctx.state.data().npcs.keys().collect::<Vec<_>>();
for npc_id in npc_ids {
let mut task_state = ctx.state.data_mut().npcs[npc_id]
.task_state
.take()
.unwrap_or_default();
let mut brain = ctx.state.data_mut().npcs[npc_id]
.brain
.take()
.unwrap_or_else(brain);
.unwrap_or_else(|| Brain {
action: Box::new(think().repeat()),
});
let (controller, task_state) = {
let controller = {
let data = &*ctx.state.data();
let npc = &data.npcs[npc_id];
let mut controller = Controller { goto: npc.goto };
brain.action.tick(&mut NpcCtx {
ctx: &ctx,
npc,
npc_id,
controller: &mut controller,
});
/*
let action: ControlFlow<()> = try {
if matches!(npc.profession, Some(Profession::Adventurer(_))) {
if let Some(home) = npc.home {
@ -344,12 +350,12 @@ impl Rule for NpcAi {
*/
}
};
*/
(controller, task_state)
controller
};
ctx.state.data_mut().npcs[npc_id].goto = controller.goto;
ctx.state.data_mut().npcs[npc_id].task_state = Some(task_state);
ctx.state.data_mut().npcs[npc_id].brain = Some(brain);
}
});
@ -358,6 +364,99 @@ impl Rule for NpcAi {
}
}
pub struct NpcCtx<'a> {
ctx: &'a EventCtx<'a, NpcAi, OnTick>,
npc_id: NpcId,
npc: &'a Npc,
controller: &'a mut Controller,
}
fn idle() -> impl Action + Clone { just(|ctx| *ctx.controller = Controller::idle()) }
fn move_toward(wpos: Vec3<f32>, speed_factor: f32) -> impl Action + Clone {
const STEP_DIST: f32 = 10.0;
just(move |ctx| {
let rpos = wpos - ctx.npc.wpos;
let len = rpos.magnitude();
ctx.controller.goto = Some((
ctx.npc.wpos + (rpos / len) * len.min(STEP_DIST),
speed_factor,
));
})
}
fn goto(wpos: Vec3<f32>, speed_factor: f32) -> impl Action + Clone {
const MIN_DIST: f32 = 1.0;
move_toward(wpos, speed_factor)
.repeat()
.stop_if(move |ctx| ctx.npc.wpos.xy().distance_squared(wpos.xy()) < MIN_DIST.powi(2))
.map(|_| {})
}
// Seconds
fn timeout(ctx: &NpcCtx, time: f64) -> impl FnMut(&mut NpcCtx) -> bool + Clone + Send + Sync {
let end = ctx.ctx.event.time.0 + time;
move |ctx| ctx.ctx.event.time.0 > end
}
fn think() -> impl Action {
choose(|ctx| {
if matches!(ctx.npc.profession, Some(Profession::Adventurer(_))) {
// Choose a random site that's fairly close by
let site_wpos2d = ctx
.ctx
.state
.data()
.sites
.iter()
.filter(|(site_id, site)| {
site.faction.is_some()
&& ctx.npc.current_site.map_or(true, |cs| *site_id != cs)
&& thread_rng().gen_bool(0.25)
})
.min_by_key(|(_, site)| site.wpos.as_().distance(ctx.npc.wpos.xy()) as i32)
.map(|(site_id, _)| site_id)
.and_then(|tgt_site| {
ctx.ctx
.state
.data()
.sites
.get(tgt_site)
.map(|site| site.wpos)
});
if let Some(site_wpos2d) = site_wpos2d {
// Walk toward the site
casual(goto(
site_wpos2d.map(|e| e as f32 + 0.5).with_z(
ctx.ctx
.world
.sim()
.get_alt_approx(site_wpos2d.as_())
.unwrap_or(0.0),
),
1.0,
))
} else {
casual(idle())
}
} else if matches!(ctx.npc.profession, Some(Profession::Blacksmith)) {
casual(idle())
} else {
casual(
now(|ctx| goto(ctx.npc.wpos + Vec3::unit_x() * 10.0, 1.0))
.then(now(|ctx| goto(ctx.npc.wpos - Vec3::unit_x() * 10.0, 1.0)))
.repeat()
.stop_if(timeout(ctx, 10.0))
.then(now(|ctx| idle().repeat().stop_if(timeout(ctx, 5.0))))
.map(|_| {}),
)
}
})
}
/*
#[derive(Clone)]
pub struct Generate<F, T>(F, PhantomData<T>);
@ -763,3 +862,4 @@ fn walk_path(site: Id<WorldSite>, path: Path<Vec2<i32>>) -> impl IsTask {
println!("Waited.");
}
}
*/

View File

@ -229,11 +229,16 @@ impl<'a> AgentData<'a> {
controller.push_cancel_input(InputKind::Fly)
}
let chase_tgt = *travel_to/*read_data.terrain
.try_find_space(travel_to.as_())
.map(|pos| pos.as_())
.unwrap_or(*travel_to)*/;
if let Some((bearing, speed)) = agent.chaser.chase(
&*read_data.terrain,
self.pos.0,
self.vel.0,
*travel_to,
chase_tgt,
TraversalConfig {
min_tgt_dist: 1.25,
..self.traversal_config