use crate::descriptor::Descriptor;
use crate::library::Library;
use crate::map::{Interaction, Location, Map, RoleKind, RoleSlot};
use crate::rng::DetRng;
use crate::vocab::{Can, Vocabularies};
#[derive(Clone, Debug, Default, PartialEq)]
pub struct RoleMasks {
by: Vec<(String, String, Vec<u64>)>,
}
impl RoleMasks {
pub fn get(&self, location: &str, verb: &str) -> Option<&[u64]> {
self.by
.iter()
.find(|(l, v, _)| l == location && v == verb)
.map(|(_, _, m)| m.as_slice())
}
pub fn len(&self) -> usize {
self.by.len()
}
pub fn is_empty(&self) -> bool {
self.by.is_empty()
}
}
pub fn resolve_roles(map: &Map, vocab: &Vocabularies) -> Result<RoleMasks, String> {
let mut by = Vec::new();
for loc in &map.locations {
for interaction in &loc.interactions {
let site = format!("{}/{}", loc.id, interaction.verb);
let mut masks = Vec::with_capacity(interaction.roles.len());
for role in &interaction.roles {
masks.push(vocab.role_mask(role, &site)?);
}
by.push((loc.id.clone(), interaction.verb.clone(), masks));
}
}
Ok(RoleMasks { by })
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct Actor {
pub id: u64,
pub can: Can,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Seat {
pub prop: String,
pub socket: String,
pub role: Option<String>,
pub at: (f32, f32, f32),
pub yaw: f32,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Filled {
pub role: String,
pub actor: u64,
pub seat: Option<Seat>,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Cast {
pub location: String,
pub verb: String,
pub filled: Vec<Filled>,
}
impl Cast {
pub fn actors(&self) -> impl Iterator<Item = u64> + '_ {
self.filled.iter().map(|f| f.actor)
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum Unfilled {
Role {
role: String,
need: u8,
found: usize,
},
Seats {
role: String,
need: u8,
found: usize,
},
}
impl std::fmt::Display for Unfilled {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Unfilled::Role { role, need, found } => write!(
f,
"role `{role}` needs {need} and {found} of the actors offered can fill it"
),
Unfilled::Seats { role, need, found } => write!(
f,
"role `{role}` needs {need} seat(s) and the location's props offer {found}"
),
}
}
}
pub fn seats_of(
map: &Map,
library: &Library,
y: &[f32],
location: &Location,
) -> Result<Vec<Seat>, String> {
let mut out = Vec::new();
for prop_id in &location.props {
let Some(i) = map.placements.iter().position(|p| &p.id == prop_id) else {
return Err(format!(
"location `{}` governs `{prop_id}`, which this map does not place",
location.id
));
};
let p = &map.placements[i];
let d: &Descriptor = library.get(&p.descriptor).ok_or_else(|| {
format!(
"location `{}`: `{prop_id}` names descriptor `{}`, which this library does not define",
location.id, p.descriptor
)
})?;
let Some(&py) = y.get(i) else { continue };
for s in &d.offers.sockets {
let (sin, cos) = p.yaw.to_radians().sin_cos();
let (lx, ly, lz) = s.at;
out.push(Seat {
prop: prop_id.clone(),
socket: s.id.clone(),
role: s.role.clone(),
at: (
map.origin.0 + p.at.0 + lx * cos + lz * sin,
py + ly,
map.origin.2 + p.at.1 - lx * sin + lz * cos,
),
yaw: p.yaw + s.yaw,
});
}
}
out.sort_by(|a, b| (&a.prop, &a.socket).cmp(&(&b.prop, &b.socket)));
Ok(out)
}
pub fn allocate(
location: &Location,
interaction: &Interaction,
requires: &[u64],
actors: &[Actor],
seats: &[Seat],
rng: &mut impl DetRng,
) -> Result<Cast, Unfilled> {
let mut pool: Vec<Actor> = actors.to_vec();
pool.sort_unstable();
shuffle(&mut pool, rng);
let mut free_seats: Vec<&Seat> = seats.iter().collect();
let mut filled: Vec<Filled> = Vec::new();
let mut taken = vec![false; pool.len()];
for (r, role) in interaction.roles.iter().enumerate() {
let needs = requires.get(r).copied().unwrap_or(0);
let want = usize::from(role.min);
let got = take_into(
&mut filled,
&pool,
&mut taken,
&mut free_seats,
role,
needs,
want,
);
if got < want {
if role.kind != RoleKind::Main {
continue;
}
return Err(match seat_shortage(role, seats, want) {
Some(found) => Unfilled::Seats {
role: role.name.clone(),
need: role.min,
found,
},
None => Unfilled::Role {
role: role.name.clone(),
need: role.min,
found: got,
},
});
}
}
for (r, role) in interaction.roles.iter().enumerate() {
let needs = requires.get(r).copied().unwrap_or(0);
let already = filled.iter().filter(|f| f.role == role.name).count();
let room = usize::from(role.max).saturating_sub(already);
if room > 0 {
take_into(
&mut filled,
&pool,
&mut taken,
&mut free_seats,
role,
needs,
room,
);
}
}
Ok(Cast {
location: location.id.clone(),
verb: interaction.verb.clone(),
filled,
})
}
fn take_into(
filled: &mut Vec<Filled>,
pool: &[Actor],
taken: &mut [bool],
free_seats: &mut Vec<&Seat>,
role: &RoleSlot,
needs: u64,
want: usize,
) -> usize {
let mut placed = 0usize;
for (i, actor) in pool.iter().enumerate() {
if placed == want {
break;
}
if taken[i] || !actor.can.meets(needs) {
continue;
}
let seat = match &role.socket_role {
Some(want_role) => {
let Some(at) = free_seats
.iter()
.position(|s| s.role.as_deref() == Some(want_role.as_str()))
else {
break;
};
Some(free_seats.remove(at).clone())
}
None => None,
};
taken[i] = true;
filled.push(Filled {
role: role.name.clone(),
actor: actor.id,
seat,
});
placed += 1;
}
placed
}
fn seat_shortage(role: &RoleSlot, seats: &[Seat], want: usize) -> Option<usize> {
let want_role = role.socket_role.as_deref()?;
let have = seats
.iter()
.filter(|s| s.role.as_deref() == Some(want_role))
.count();
(have < want).then_some(have)
}
fn shuffle<T>(items: &mut [T], rng: &mut impl DetRng) {
if items.len() < 2 {
return;
}
for i in (1..items.len()).rev() {
items.swap(i, rng.below(i + 1));
}
}
#[derive(Clone, Debug, Default)]
pub struct Booking {
running: Vec<Cast>,
}
impl Booking {
pub fn new() -> Booking {
Booking::default()
}
pub fn is_busy(&self, location: &str) -> bool {
self.running.iter().any(|c| c.location == location)
}
pub fn is_engaged(&self, actor: u64) -> bool {
self.running.iter().any(|c| c.actors().any(|a| a == actor))
}
pub fn free(&self, offered: &[Actor]) -> Vec<Actor> {
offered
.iter()
.copied()
.filter(|a| !self.is_engaged(a.id))
.collect()
}
pub fn start(&mut self, cast: Cast) -> Result<(), String> {
if self.is_busy(&cast.location) {
return Err(format!(
"location `{}` is already running an interaction — a location owns its props, so a \
second scene would be two casts using one table",
cast.location
));
}
if let Some(a) = cast.actors().find(|a| self.is_engaged(*a)) {
return Err(format!(
"actor {a} is already in a scene — an actor takes part in at most one at a time"
));
}
self.running.push(cast);
Ok(())
}
pub fn finish(&mut self, location: &str) -> Option<Cast> {
let at = self.running.iter().position(|c| c.location == location)?;
Some(self.running.remove(at))
}
pub fn casts(&self) -> &[Cast] {
&self.running
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::descriptor::{Extent, Offers, Socket};
use crate::library::LIBRARY_VERSION;
use crate::map::{Effect, Interaction, Placed, RoleKind, RoleSlot};
use crate::rng::seeded;
const EAT: u64 = 1;
const COOK: u64 = 2;
fn table_with_seats(n: usize) -> Descriptor {
Descriptor {
id: "table".into(),
mesh: Some("table.glb".into()),
extent: Extent {
footprint: Some((1.6, 0.8)),
height: Some(0.8),
},
offers: Offers {
surfaces: vec!["worktop".into()],
faces: Vec::new(),
sockets: (0..n)
.map(|i| Socket {
id: format!("seat_{i}"),
role: Some("diner".into()),
at: (0.0, 0.45, -0.5 + i as f32),
yaw: 0.0,
})
.collect(),
},
..Descriptor::default()
}
}
fn library(n_seats: usize) -> Library {
Library {
version: LIBRARY_VERSION,
note: None,
descriptors: vec![table_with_seats(n_seats)],
}
}
fn map_with_table() -> Map {
Map {
name: "galley".into(),
placements: vec![Placed {
id: "t1".into(),
descriptor: "table".into(),
at: (2.0, 3.0),
..Placed::default()
}],
..Map::default()
}
}
fn role(name: &str, kind: RoleKind, min: u8, max: u8, socket: Option<&str>) -> RoleSlot {
RoleSlot {
name: name.to_owned(),
kind,
min,
max,
socket_role: socket.map(str::to_owned),
requires: Vec::new(),
}
}
fn dinner(roles: Vec<RoleSlot>) -> Interaction {
Interaction {
verb: "eat".into(),
roles,
guard: None,
effects: vec![Effect::Restore {
drive: "stamina".into(),
rate: 0.2,
}],
note: None,
}
}
fn location(props: &[&str], interactions: Vec<Interaction>) -> Location {
Location {
id: "galley_table_1".into(),
props: props.iter().map(|p| (*p).to_owned()).collect(),
interactions,
note: None,
}
}
fn actors(spec: &[(u64, u64)]) -> Vec<Actor> {
spec.iter()
.map(|(id, can)| Actor {
id: *id,
can: Can(*can),
})
.collect()
}
fn seats(n: usize) -> Vec<Seat> {
let map = map_with_table();
let lib = library(n);
let y = crate::stack::resolve_y(&map, &lib).unwrap_or_else(|e| panic!("{e}"));
seats_of(&map, &lib, &y, &location(&["t1"], vec![])).unwrap_or_else(|e| panic!("{e}"))
}
#[test]
fn four_agents_fill_a_four_seat_table_without_double_booking() {
let interaction = dinner(vec![role("diner", RoleKind::Main, 1, 4, Some("diner"))]);
let cast = allocate(
&location(&["t1"], vec![]),
&interaction,
&[0],
&actors(&[(1, EAT), (2, EAT), (3, EAT), (4, EAT)]),
&seats(4),
&mut seeded(7),
)
.unwrap_or_else(|e| panic!("{e}"));
assert_eq!(cast.filled.len(), 4);
let mut who: Vec<u64> = cast.actors().collect();
who.sort_unstable();
assert_eq!(who, vec![1, 2, 3, 4], "every actor exactly once");
let mut where_: Vec<&str> = cast
.filled
.iter()
.filter_map(|f| f.seat.as_ref().map(|s| s.socket.as_str()))
.collect();
where_.sort_unstable();
where_.dedup();
assert_eq!(where_.len(), 4, "every seat exactly once");
}
#[test]
fn a_table_seats_as_many_as_it_has_chairs() {
let interaction = dinner(vec![role("diner", RoleKind::Main, 1, 8, Some("diner"))]);
let cast = allocate(
&location(&["t1"], vec![]),
&interaction,
&[0],
&actors(&[(1, EAT), (2, EAT), (3, EAT), (4, EAT), (5, EAT)]),
&seats(4),
&mut seeded(7),
)
.unwrap_or_else(|e| panic!("{e}"));
assert_eq!(cast.filled.len(), 4);
}
#[test]
fn an_unfillable_main_role_stops_the_scene() {
let interaction = dinner(vec![role("diner", RoleKind::Main, 2, 4, Some("diner"))]);
let err = allocate(
&location(&["t1"], vec![]),
&interaction,
&[EAT],
&actors(&[(1, COOK)]),
&seats(4),
&mut seeded(7),
)
.err()
.unwrap_or_else(|| panic!("expected a refusal"));
assert_eq!(
err,
Unfilled::Role {
role: "diner".into(),
need: 2,
found: 0
}
);
}
#[test]
fn too_few_seats_is_a_different_failure_from_too_few_actors() {
let interaction = dinner(vec![role("diner", RoleKind::Main, 3, 4, Some("diner"))]);
let err = allocate(
&location(&["t1"], vec![]),
&interaction,
&[0],
&actors(&[(1, EAT), (2, EAT), (3, EAT)]),
&seats(2),
&mut seeded(7),
)
.err()
.unwrap_or_else(|| panic!("expected a refusal"));
assert_eq!(
err,
Unfilled::Seats {
role: "diner".into(),
need: 3,
found: 2
}
);
}
#[test]
fn a_supporting_role_that_cannot_be_filled_does_not_stop_the_scene() {
let interaction = dinner(vec![
role("diner", RoleKind::Main, 1, 4, Some("diner")),
role("server", RoleKind::Supporting, 1, 1, None),
]);
let mut server = role("server", RoleKind::Supporting, 1, 1, None);
server.requires = vec!["cook".into()];
let interaction = Interaction {
roles: vec![interaction.roles[0].clone(), server],
..interaction
};
let cast = allocate(
&location(&["t1"], vec![]),
&interaction,
&[EAT, COOK],
&actors(&[(1, EAT), (2, EAT)]),
&seats(4),
&mut seeded(7),
)
.unwrap_or_else(|e| panic!("{e}"));
assert!(cast.filled.iter().all(|f| f.role == "diner"));
assert_eq!(cast.filled.len(), 2);
}
#[test]
fn a_role_requiring_two_capabilities_wants_both() {
let mut chef = role("chef", RoleKind::Main, 1, 1, None);
chef.requires = vec!["eat".into(), "cook".into()];
let interaction = dinner(vec![chef]);
let err = allocate(
&location(&["t1"], vec![]),
&interaction,
&[EAT | COOK],
&actors(&[(1, EAT), (2, COOK)]),
&seats(0),
&mut seeded(7),
)
.err();
assert!(err.is_some(), "neither actor can do both");
let cast = allocate(
&location(&["t1"], vec![]),
&interaction,
&[EAT | COOK],
&actors(&[(1, EAT), (2, EAT | COOK)]),
&seats(0),
&mut seeded(7),
)
.unwrap_or_else(|e| panic!("{e}"));
assert_eq!(cast.filled[0].actor, 2);
}
#[test]
fn the_same_seed_allocates_the_same_way() {
let interaction = dinner(vec![role("diner", RoleKind::Main, 1, 2, Some("diner"))]);
let who = |seed: u64, order: &[(u64, u64)]| -> Vec<u64> {
allocate(
&location(&["t1"], vec![]),
&interaction,
&[0],
&actors(order),
&seats(4),
&mut seeded(seed),
)
.map(|c| c.actors().collect())
.unwrap_or_default()
};
let forwards = [(1, EAT), (2, EAT), (3, EAT), (4, EAT)];
let backwards = [(4, EAT), (3, EAT), (2, EAT), (1, EAT)];
assert_eq!(who(11, &forwards), who(11, &forwards));
assert_eq!(who(11, &forwards), who(11, &backwards));
let seeds: Vec<Vec<u64>> = (0..24).map(|s| who(s, &forwards)).collect();
assert!(
seeds.iter().any(|s| s != &seeds[0]),
"every seed produced the same cast — the shuffle is not shuffling"
);
}
#[test]
fn a_seat_is_placed_by_its_props_position_and_yaw() {
let mut map = map_with_table();
let lib = library(1);
let y = crate::stack::resolve_y(&map, &lib).unwrap_or_else(|e| panic!("{e}"));
let s = &seats_of(&map, &lib, &y, &location(&["t1"], vec![]))
.unwrap_or_else(|e| panic!("{e}"))[0];
assert!((s.at.0 - 2.0).abs() < 1e-5, "{:?}", s.at);
assert!((s.at.1 - 0.45).abs() < 1e-5, "{:?}", s.at);
assert!((s.at.2 - 2.5).abs() < 1e-5, "{:?}", s.at);
map.placements[0].yaw = 90.0;
let s = &seats_of(&map, &lib, &y, &location(&["t1"], vec![]))
.unwrap_or_else(|e| panic!("{e}"))[0];
assert!((s.at.0 - 1.5).abs() < 1e-5, "{:?}", s.at);
assert!((s.at.2 - 3.0).abs() < 1e-5, "{:?}", s.at);
assert!((s.yaw - 90.0).abs() < 1e-5);
}
#[test]
fn a_location_governing_a_missing_prop_is_refused() {
let map = map_with_table();
let lib = library(2);
let y = crate::stack::resolve_y(&map, &lib).unwrap_or_else(|e| panic!("{e}"));
let err = seats_of(&map, &lib, &y, &location(&["t1", "t9"], vec![]))
.err()
.unwrap_or_default();
assert!(err.contains("t9"), "{err}");
}
#[test]
fn a_location_runs_one_scene_and_an_actor_joins_one() {
let cast = |loc: &str, who: &[u64]| Cast {
location: loc.to_owned(),
verb: "eat".into(),
filled: who
.iter()
.map(|a| Filled {
role: "diner".into(),
actor: *a,
seat: None,
})
.collect(),
};
let mut booking = Booking::new();
booking
.start(cast("galley_table_1", &[1, 2]))
.unwrap_or_else(|e| panic!("{e}"));
let err = booking
.start(cast("galley_table_1", &[3]))
.err()
.unwrap_or_default();
assert!(err.contains("already running"), "{err}");
let err = booking
.start(cast("bunk_2", &[2]))
.err()
.unwrap_or_default();
assert!(err.contains("already in a scene"), "{err}");
booking
.start(cast("bunk_2", &[3]))
.unwrap_or_else(|e| panic!("{e}"));
assert_eq!(booking.casts().len(), 2);
let offered = actors(&[(1, EAT), (2, EAT), (3, EAT), (4, EAT)]);
assert_eq!(
booking.free(&offered).iter().map(|a| a.id).collect::<Vec<_>>(),
vec![4]
);
assert!(booking.finish("galley_table_1").is_some());
assert!(!booking.is_engaged(1));
assert!(!booking.is_busy("galley_table_1"));
assert!(booking.finish("galley_table_1").is_none());
}
#[test]
fn repeated_allocation_never_strands_an_actor_or_a_seat() {
let interaction = dinner(vec![role("diner", RoleKind::Main, 1, 4, Some("diner"))]);
for seed in 0..64u64 {
for n in 0..6usize {
let mut booking = Booking::new();
let offered: Vec<Actor> = (1..=6)
.map(|id| Actor {
id,
can: Can(EAT),
})
.collect();
let free = booking.free(&offered);
if let Ok(cast) = allocate(
&location(&["t1"], vec![]),
&interaction,
&[0],
&free,
&seats(n),
&mut seeded(seed * 31 + n as u64),
) {
let seated = cast.filled.len();
booking.start(cast).unwrap_or_else(|e| panic!("{e}"));
assert!(seated <= n, "seated {seated} at {n} chair(s)");
assert_eq!(
booking.casts()[0].actors().count(),
seated,
"an actor appears twice in one cast"
);
}
let engaged = offered.iter().filter(|a| booking.is_engaged(a.id)).count();
assert_eq!(engaged + booking.free(&offered).len(), offered.len());
}
}
}
}