Attempted generator-driven AI

This commit is contained in:
Joshua Barretto 2022-10-30 21:11:30 +00:00
parent 0b4d3c9e20
commit 558b5f7c3a
4 changed files with 266 additions and 8 deletions

View File

@ -10,12 +10,16 @@ use rand::prelude::*;
use serde::{Deserialize, Serialize};
use slotmap::HopSlotMap;
use std::{
any::Any,
any::{Any, TypeId},
collections::VecDeque,
ops::{ControlFlow, Deref, DerefMut},
ops::{ControlFlow, Deref, DerefMut, Generator, GeneratorState},
sync::{Arc, atomic::{AtomicPtr, Ordering}},
pin::Pin,
marker::PhantomData,
};
use vek::*;
use world::{civ::Track, site::Site as WorldSite, util::RandomPerm};
use crate::rule::npc_ai;
#[derive(Copy, Clone, Default)]
pub enum NpcMode {
@ -153,6 +157,110 @@ impl TaskState {
}
}
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 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
}
}
}
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>,
}
impl<C: Context, A> TaskBox<C, A> {
pub fn new(data: Data<C>) -> Self {
Self {
task: None,
data,
}
}
#[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(())
}
}
#[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));
};
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
},
GeneratorState::Complete(ret) => match ret {},
}
}
}
#[derive(Serialize, Deserialize)]
pub struct Npc {
// Persisted state
@ -181,6 +289,9 @@ pub struct Npc {
#[serde(skip_serializing, skip_deserializing)]
pub task_state: Option<TaskState>,
#[serde(skip_serializing, skip_deserializing)]
pub brain: Option<Brain<npc_ai::NpcData<'static>>>,
}
impl Clone for Npc {
@ -196,6 +307,7 @@ impl Clone for Npc {
goto: Default::default(),
mode: Default::default(),
task_state: Default::default(),
brain: Default::default(),
}
}
}
@ -215,6 +327,7 @@ impl Npc {
goto: None,
mode: NpcMode::Simulated,
task_state: Default::default(),
brain: Some(npc_ai::brain()),
}
}

View File

@ -65,19 +65,19 @@ impl Data {
);
// Spawn some test entities at the sites
for (site_id, site) in this.sites.iter() {
for (site_id, site) in this.sites.iter().take(1) {
let rand_wpos = |rng: &mut SmallRng| {
let wpos2d = site.wpos.map(|e| e + rng.gen_range(-10..10));
wpos2d
.map(|e| e as f32 + 0.5)
.with_z(world.sim().get_alt_approx(wpos2d).unwrap_or(0.0))
};
for _ in 0..20 {
for _ in 0..1 {
this.npcs.create(
Npc::new(rng.gen(), rand_wpos(&mut rng))
.with_faction(site.faction)
.with_home(site_id)
.with_profession(match rng.gen_range(0..20) {
.with_profession(match 1/*rng.gen_range(0..20)*/ {
0 => Profession::Hunter,
1 => Profession::Blacksmith,
2 => Profession::Chef,

View File

@ -1,8 +1,10 @@
#![feature(
explicit_generic_args_with_impl_trait,
generic_associated_types,
never_type,
try_blocks
try_blocks,
generator_trait,
generators,
trait_alias
)]
pub mod data;

View File

@ -2,7 +2,7 @@ use std::{collections::VecDeque, hash::BuildHasherDefault};
use crate::{
data::{
npc::{Controller, Npc, NpcId, PathData, PathingMemory, Task, TaskState, CONTINUE, FINISH},
npc::{Controller, Npc, NpcId, PathData, PathingMemory, Task, TaskState, CONTINUE, FINISH, TaskBox, Brain, Data, Context},
Sites,
},
event::OnTick,
@ -241,6 +241,10 @@ impl Rule for NpcAi {
.task_state
.take()
.unwrap_or_default();
let mut brain = ctx.state.data_mut().npcs[npc_id]
.brain
.take()
.unwrap_or_else(brain);
let (controller, task_state) = {
let data = &*ctx.state.data();
@ -295,6 +299,13 @@ impl Rule for NpcAi {
)?;
}
} else {
brain.tick(&mut NpcData {
ctx: &ctx,
npc,
npc_id,
controller: &mut controller,
});
/*
// // Choose a random plaza in the npcs home site (which should be the
// // current here) to go to.
let task =
@ -327,6 +338,7 @@ impl Rule for NpcAi {
.repeat();
task_state.perform(task, &(npc_id, &*npc, &ctx), &mut controller)?;
*/
}
};
@ -335,6 +347,7 @@ impl Rule for NpcAi {
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);
}
});
@ -599,3 +612,133 @@ impl Task for TravelTo {
}
}
}
/*
let data = ctx.state.data();
let site2 =
npc.home.and_then(|home| data.sites.get(home)).and_then(
|home| match &ctx.index.sites.get(home.world_site?).kind
{
SiteKind::Refactor(site2)
| SiteKind::CliffTown(site2)
| SiteKind::DesertCity(site2) => Some(site2),
_ => None,
},
);
let wpos = site2
.and_then(|site2| {
let plaza = &site2.plots
[site2.plazas().choose(&mut thread_rng())?];
Some(site2.tile_center_wpos(plaza.root_tile()).as_())
})
.unwrap_or(npc.wpos.xy());
TravelTo {
wpos,
use_paths: true,
}
*/
trait IsTask = core::ops::Generator<Data<NpcData<'static>>, Yield = (), Return = ()> + Any + Send + Sync;
pub struct NpcData<'a> {
ctx: &'a EventCtx<'a, NpcAi, OnTick>,
npc_id: NpcId,
npc: &'a Npc,
controller: &'a mut Controller,
}
unsafe impl Context for NpcData<'static> {
type Ty<'a> = NpcData<'a>;
}
pub fn brain() -> Brain<NpcData<'static>> {
Brain::new(|mut data: Data<NpcData>| {
let mut task = TaskBox::<_, ()>::new(data.clone());
loop {
println!("Started");
while let ControlFlow::Break(end) = task.finish(0) {
yield end;
}
// Choose a new plaza in the NPC's home site to path towards
let path = data.with(|d| {
let data = d.ctx.state.data();
let current_site = data.sites.get(d.npc.current_site?)?;
let site2 = match &d.ctx.index.sites.get(current_site.world_site?).kind {
SiteKind::Refactor(site2)
| SiteKind::CliffTown(site2)
| SiteKind::DesertCity(site2) => Some(site2),
_ => None,
}?;
let plaza = &site2.plots[site2.plazas().choose(&mut thread_rng())?];
let end_wpos = site2.tile_center_wpos(plaza.root_tile());
if end_wpos.as_::<f32>().distance(d.npc.wpos.xy()) < 32.0 {
return None;
}
let start = site2.wpos_tile_pos(d.npc.wpos.xy().as_());
let end = site2.wpos_tile_pos(plaza.root_tile());
let path = match path_in_site(start, end, site2) {
PathResult::Path(path) => path,
_ => return None,
};
println!("CHOSE PATH, len = {}, start = {:?}, end = {:?}\nnpc = {:?}", path.len(), start, end, d.npc_id);
Some((current_site.world_site?, path))
});
if let Some((site, path)) = path {
println!("Begin path");
task.perform(0, walk_path(site, path));
} else {
println!("No path, waiting...");
for _ in 0..100 {
yield ();
}
println!("Waited.");
}
}
})
}
fn walk_path(site: Id<WorldSite>, path: Path<Vec2<i32>>) -> impl IsTask {
move |mut data: Data<NpcData>| {
for tile in path {
println!("TILE");
let wpos = data.with(|d| match &d.ctx.index.sites.get(site).kind {
SiteKind::Refactor(site2)
| SiteKind::CliffTown(site2)
| SiteKind::DesertCity(site2) => Some(site2),
_ => None,
}
.expect("intrasite path should only be started on a site2 site")
.tile_center_wpos(tile)
.as_()
+ 0.5);
println!("Walking to next tile... tile wpos = {:?} npc wpos = {:?}", wpos, data.with(|d| d.npc.wpos));
while data.with(|d| d.npc.wpos.xy().distance_squared(wpos) > 2.0) {
data.with(|d| d.controller.goto = Some((
wpos.with_z(d.ctx.world
.sim()
.get_alt_approx(wpos.map(|e| e as i32))
.unwrap_or(0.0)),
1.0,
)));
yield ();
}
}
println!("Waiting..");
for _ in 0..100 {
yield ();
}
println!("Waited.");
}
}