diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..d1987c7 --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,13 @@ +{ + // See https://go.microsoft.com/fwlink/?LinkId=827846 to learn about workspace recommendations. + // Extension identifier format: ${publisher}.${name}. Example: vscode.csharp + + // List of extensions which should be recommended for users of this workspace. + "recommendations": [ + + ], + // List of extensions recommended by VS Code that should not be recommended for users of this workspace. + "unwantedRecommendations": [ + + ] +} \ No newline at end of file diff --git a/src/battle/actor.rs b/src/battle/actor.rs index 3ca5e51..f4c78df 100644 --- a/src/battle/actor.rs +++ b/src/battle/actor.rs @@ -1,64 +1,124 @@ -use std::cell::RefCell; -use std::num::NonZeroU32; -use std::rc::Rc; +use super::base::*; -pub type ActorId = u32; -pub type TeamId = u32; -pub type AiId = u32; -pub type SkillId = u32; +use std::collections::{BTreeMap, HashMap}; pub struct Actor { pub id: ActorId, pub team_id: TeamId, - pub ai_id: Option, pub name: String, - pub state: ActorState, - pub initiative: NonZeroU32, + pub stats: ActorStats, pub skills: Vec, + pub ai: Option, } -type Hp = std::num::NonZeroU32; -pub enum ActorState { - Dead, - Alive(Hp), +pub struct ActorStats { + pub max_hp: Hp, + pub initiative: Increment, +} + +pub struct ActorDb { + pub container: BTreeMap, } -impl ActorState { - pub fn is_dead(&self) -> bool { - match self { - Self::Dead => true, - _ => false, + +impl ActorDb { + #[allow(dead_code)] + pub fn from(actors: Vec) -> ActorDb { + ActorDb { + container: actors.into_iter().map(|actor| (actor.id, actor)).collect(), } } - pub fn is_alive(&self) -> bool { - match self { - Self::Alive(_) => true, - _ => false, - } + #[allow(dead_code)] + pub fn get(&self, id: ActorId) -> Option<&Actor> { + self.container.get(&id) } -} -pub struct TeamInfo(Option); + pub fn iter(&self) -> std::collections::btree_map::Iter { + self.container.iter() + } +} -type ActorWrp = Rc>; pub struct Team { pub id: TeamId, - pub info: TeamInfo, - pub members: Box>, + pub actors: Vec, + pub ai: Option, +} + +pub struct TeamDb { + container: BTreeMap, } -pub trait ActorDb<'a> { - fn lookup_actor(&self, id: ActorId) -> Option; +pub struct TeamAiOverrides(pub Vec<(TeamId, AiId)>); - fn lookup_team_info(&self, id: TeamId) -> Option; +#[allow(dead_code, unused_variables)] +impl TeamDb { + pub fn from(actor_db: &ActorDb, ai_overrides: TeamAiOverrides) -> TeamDb { + let mut map: BTreeMap = BTreeMap::new(); - fn lookup_team(&self, id: TeamId) -> Option; + actor_db.iter().for_each(|(actor_id, actor)| { + let team_id = actor.team_id; + if let Some(entry) = map.get_mut(&team_id) { + entry.actors.push(*actor_id); + } else { + let ai = ai_overrides + .0 + .iter() + .find(|(tid, aid)| *tid == team_id) + .map(|(_, aid)| *aid); + map.insert( + team_id, + Team { + id: team_id, + actors: vec![*actor_id], + ai, + }, + ); + } + }); - fn lookup_actor_ids(&self) -> Box>; + TeamDb { container: map } + } - fn lookup_team_ids(&self) -> Box>; + pub fn get(&self, id: TeamId) -> Option<&Team> { + self.container.get(&id) + } - fn lookup_actors(&self) -> Box>; + pub fn iter(&self) -> std::collections::btree_map::Iter { + self.container.iter() + } +} - fn lookup_teams(&self) -> Box>; +#[allow(dead_code)] +pub struct ActorStateDb { + container: HashMap, +} + +#[allow(dead_code, unused_variables)] +impl ActorStateDb { + pub fn new() -> ActorStateDb { + ActorStateDb { + container: HashMap::new(), + } + } + + pub fn init(&mut self, actor: &Actor) { + self.container + .insert(actor.id, ActorState::Alive(actor.stats.max_hp)) + .unwrap_none() + } + + pub fn get(&self, actor_id: ActorId) -> Option { + self.container.get(&actor_id).cloned() + } + + pub fn set(&mut self, actor_id: ActorId, state: ActorState) { + self.container.insert(actor_id, state); + } +} + +#[derive(Debug, Clone, Copy)] +#[allow(dead_code)] +pub enum ActorState { + Dead, + Alive(Hp), } diff --git a/src/battle/base.rs b/src/battle/base.rs new file mode 100644 index 0000000..59021de --- /dev/null +++ b/src/battle/base.rs @@ -0,0 +1,17 @@ +pub type ActorId = u32; +#[allow(dead_code)] +pub type TeamId = u32; +#[allow(dead_code)] +pub type SkillId = u32; +pub type Increment = u32; +pub type Hp = u32; +pub type AiId = u32; + +#[allow(dead_code)] +pub enum SkillTarget { + None, + Actor(ActorId), + Team(TeamId), +} + +pub struct EffectTarget(pub ActorId); diff --git a/src/battle/mod.rs b/src/battle/mod.rs index f0e76f5..7635343 100644 --- a/src/battle/mod.rs +++ b/src/battle/mod.rs @@ -1,105 +1,202 @@ -#![allow(dead_code,unused_variables,unused_imports,unused_mut)] +pub mod actor; +pub mod base; +pub mod queue; +pub mod skill; +pub mod uic; -mod actor; -mod queue; +use crate::context::*; +use crate::require; use actor::*; -use queue::{Queue, QueueElement}; - -use std::cell::RefCell; -use std::rc::Rc; - -fn battle( - actor_db: &dyn ActorDb, - decider: &dyn Decider, - effector: &dyn Effector, -) -> Result { - if let Err(err_msg) = check(actor_db) { - return Err(err_msg); - } +use base::*; +use queue::*; +use skill::*; +use uic::*; + +#[allow(dead_code, unused_variables)] +pub fn run_battle(context: Context) -> Result<(), String> { + require!(context; ActorDb, ActorStateDb, TeamDb, DeciderDb, SkillDb, Uic)?; + let uic = context.get::().unwrap(); + init_states(&context); + uic.start_battle(&context); + let mut queue = create_queue(&context.get::().unwrap()); + uic.create_queue(&context, &queue); + + let battle_result = loop { + let actor_id = match queue.dequeue() { + None => break BattleResult::AllDead, + Some((_, actor_id)) => actor_id, + }; + uic.start_turn(&context, actor_id); + let (skill_id, target) = determine_decision(&context, actor_id)?; + let effects = determine_effects(&context, actor_id, skill_id, target)?; + let results = apply_effects(&context, effects)?; + if !results.0.is_empty() { + if let Some(battle_result) = check_result(&context) { + break battle_result; + } + } + let actor_db = context.get::().unwrap(); + let actor_state_db = context.get::().unwrap(); + if let ActorState::Alive(_) = actor_state_db.get(actor_id).unwrap() { + let actor = actor_db.get(actor_id).unwrap(); + queue.enqueue((actor.stats.initiative, actor_id)); + } + }; + + println!("Result: {:?}", battle_result); + Ok(()) +} + +fn create_queue(actor_db: &ActorDb) -> Queue { let mut queue = Queue::new(); - for actor in actor_db.lookup_actors() { - let actor = actor.borrow(); - queue.enqueue(actor.initiative.get(), actor.id); - } + actor_db + .iter() + .for_each(|(id, actor)| queue.enqueue((actor.stats.initiative, *id))); - while let Some(QueueElement(incr, actor_id)) = queue.next() { - let actor = match actor_db.lookup_actor(actor_id) { - Some(actor) => actor, - None => return Err("Couldn't find actor with specified id"), - }; - let team_info = match actor_db.lookup_team_info(actor.borrow().team_id) { - Some(team_info) => team_info, - None => return Err("Couldn't find team with specified id"), - }; + queue +} - let decision = decider.decide(actor_db, actor.clone())?; - effector.effect(actor_db, actor.clone(), decision)?; +fn determine_ai( + context: &Context, + actor_id: base::ActorId, + default: base::AiId, +) -> Result { + require!(context; actor::ActorDb, actor::TeamDb)?; + + let actor_db: &actor::ActorDb = &*context.get().unwrap(); + let actor = actor_db + .get(actor_id) + .ok_or_else(|| String::from("Actor not found"))?; + if let Some(ai_id) = actor.ai { + return Ok(ai_id); + } - if at_most_one_team_left_alive(actor_db) { - break; - } + let team_db: &actor::TeamDb = &*context.get().unwrap(); + let team = team_db + .get(actor.team_id) + .ok_or_else(|| String::from("Team not found"))?; + if let Some(ai_id) = team.ai { + return Ok(ai_id); } - Err("not implemented") + Ok(default) } -fn check(actor_db: &dyn ActorDb) -> Result<(), &'static str> { - let stats = actor_db - .lookup_teams() - .map(|mut team| team.members.next().is_none()) - .fold((0, 0), |(non_empty, empty), is_empty| { - if is_empty { - (non_empty, empty + 1) - } else { - (non_empty + 1, empty) - } - }); +fn determine_decision( + context: &Context, + actor_id: ActorId, +) -> Result<(SkillId, SkillTarget), String> { + let decider_db: &DeciderDb = &*context.get().unwrap(); + let ai_id = determine_ai(&context, actor_id, 0)?; + let decider = decider_db + .get(ai_id) + .ok_or_else(|| String::from("Decider not found"))?; + + decider.decide(context, actor_id) +} - match stats { - (0, 0) => Err("No teams provided for battle"), - (1, 0) => Err("More than one team must be provided for battle"), - (_, 0) => Ok(()), - (_, empty) => Err("Empty teams cannot participate in battle"), +fn determine_effects( + context: &Context, + actor_id: ActorId, + skill_id: SkillId, + target: SkillTarget, +) -> Result, String> { + let skill_db: &SkillDb = &*context.get().unwrap(); + let skill = skill_db + .get(skill_id) + .ok_or_else(|| String::from("Skill not found"))?; + + skill.func.apply(context, actor_id, target) +} + +struct DeadList(Vec); + +fn apply_effects(context: &Context, effects: Vec) -> Result { + let mut dead_vec = Vec::new(); + for effect in effects { + let actor_db = context.get::().unwrap(); + let uic = context.get::().unwrap(); + let mut actor_state_db = context.get_mut::().unwrap(); + + match effect { + SkillEffect::Damage(dmg, EffectTarget(id)) => { + // TODO: error handling + let state = actor_state_db.get(id).unwrap(); + + if let ActorState::Alive(hp_left) = state { + let new_hp = hp_left.saturating_sub(dmg); + let new_state = if new_hp > 0 { + ActorState::Alive(new_hp) + } else { + dead_vec.push(id); + ActorState::Dead + }; + actor_state_db.set(id, new_state); + uic.hp_effect(context, id, state, new_state); + } + } + SkillEffect::Heal(hp, EffectTarget(id)) => { + // TODO: error handling + let max_hp = actor_db.get(id).unwrap().stats.max_hp; + let state = actor_state_db.get(id).unwrap(); + if let ActorState::Alive(hp_left) = state { + let after_heal = hp_left.saturating_add(hp); + let new_state = ActorState::Alive(if after_heal > max_hp { + max_hp + } else { + after_heal + }); + + actor_state_db.set(id, new_state); + uic.hp_effect(context, id, state, new_state); + } + } + } } + Ok(DeadList(dead_vec)) } -fn at_most_one_team_left_alive(actor_db: &dyn ActorDb) -> bool { - let (alive, _) = actor_db - .lookup_teams() - .map(|mut team| team.members.all(|actor| actor.borrow().state.is_dead())) - .fold((0, 0), |(alive, dead), all_dead| { - if all_dead { - (alive, dead + 1) +fn check_result(context: &Context) -> Option { + let team_db = context.get::().unwrap(); + let actor_state_db = context.get::().unwrap(); + + let alive: Vec = team_db + .iter() + .filter_map(|(team_id, team)| { + if team.actors.iter().any(|actor_id| { + if let ActorState::Dead = actor_state_db.get(*actor_id).unwrap() { + false + } else { + true + } + }) { + Some(*team_id) } else { - (alive + 1, dead) + None } - }); - - alive <= 1 -} + }) + .collect(); -enum Target { - Oneself, - Actor(ActorId), - Team(TeamId), + match alive.len() { + 0 => Some(BattleResult::AllDead), + 1 => Some(BattleResult::TeamWin(alive[0])), + _ => None, + } } -struct Decision(SkillId, Target); +fn init_states(context: &Context) { + let actor_db = context.get::().unwrap(); + let mut actor_state_db = context.get_mut::().unwrap(); -trait Decider { - fn decide( - &self, - actor_db: &dyn ActorDb, - actor: Rc>, - ) -> Result; + for (_, actor) in actor_db.iter() { + actor_state_db.init(actor) + } } -trait Effector { - fn effect( - &self, - actor_db: &dyn ActorDb, - actor: Rc>, - decision: Decision, - ) -> Result<(), &'static str>; +#[allow(dead_code)] +#[derive(Debug)] +enum BattleResult { + TeamWin(base::TeamId), + AllDead, } diff --git a/src/battle/queue.rs b/src/battle/queue.rs index 5d00c3c..584b0b3 100644 --- a/src/battle/queue.rs +++ b/src/battle/queue.rs @@ -1,81 +1,68 @@ -type Increment = u32; -type Sum = u64; +use super::base::*; -#[derive(Debug, PartialEq)] -pub struct QueueElement(pub Increment, pub T); - -pub struct Queue { - container: Vec>, - sum: Sum, +pub struct Queue { + container: Vec<(Increment, ActorId)>, } -impl Queue { - pub fn new() -> Queue { +impl Queue { + #[allow(dead_code)] + pub fn new() -> Queue { Queue { container: Vec::new(), - sum: 0, } } - pub fn next(&mut self) -> Option> { + #[allow(dead_code)] + pub fn enqueue(&mut self, (incr, id): (Increment, ActorId)) { + let mut incr = incr; + let mut insert = None; + for (i, (incr2, _)) in self.container.iter().enumerate() { + if incr < *incr2 { + insert = Some(i); + break; + } else if *incr2 != 0 { + incr -= incr2; + } + } + + if let Some(ix) = insert { + self.container[ix].0 -= incr; + self.container.insert(ix, (incr, id)); + } else { + self.container.push((incr, id)); + } + } + + #[allow(dead_code)] + pub fn dequeue(&mut self) -> Option<(Increment, ActorId)> { if self.container.is_empty() { None } else { - let next = self.container.remove(0); - if self.sum < next.0.into() { - panic!("Sum of increments should not be smaller than a single increment"); - } else { - self.sum -= next.0 as u64; - Some(next) - } + Some(self.container.remove(0)) } } - pub fn enqueue(&mut self, mut incr: Increment, t: T) { - let mut insert_pos = 0; - for (i, el) in self.container.iter_mut().enumerate() { - if el.0 > incr { - el.0 -= incr; + #[allow(dead_code)] + pub fn remove(&mut self, id: ActorId) { + let mut i = 0; + loop { + if i >= self.container.len() { break; } - - insert_pos = i + 1; - if el.0 < incr { - incr -= el.0; + if id == self.container[i].1 { + let (incr, _) = self.container.remove(i); + if i < self.container.len() { + self.container[i].0 += incr; + } else { + break; + } } + i += 1; } - if insert_pos == self.container.len() { - self.sum += incr as u64; - } - self.container.insert(insert_pos, QueueElement(incr, t)); - } - - pub fn from, F: Fn(&T) -> Increment>( - iter: I, - key_extractor: F, - ) -> Queue { - let mut queue = Queue::new(); - iter.into_iter() - .map(move |t| (key_extractor(&t), t)) - .for_each(|(incr, t)| queue.enqueue(incr, t)); - queue - } - - pub fn get_container(&self) -> &[QueueElement] { - &self.container } -} -impl<'a, T: 'a + Clone> Queue { - pub fn from_clone, F: Fn(&T) -> Increment>( - iter: I, - key_extractor: F, - ) -> Queue { - let mut queue = Queue::new(); - iter.into_iter() - .map(|t| (key_extractor(t), t.clone())) - .for_each(|(incr, t)| queue.enqueue(incr, t)); - queue + pub fn iter(&self) -> impl Iterator { + self.container.iter() } } @@ -84,128 +71,75 @@ mod tests { use super::*; #[test] - fn retrieve_from_empty() { + fn test_enqueue_between_elements() { // Given - let mut queue: Queue<&'static str> = Queue::new(); - - // When - let no_more = queue.next(); - - // Then - assert_eq!(no_more, None); + let mut queue = Queue::new(); + queue.enqueue((2, 1)); + queue.enqueue((7, 2)); + queue.enqueue((3, 3)); + + // Expect + // id-1 remains first + assert_eq!(queue.dequeue(), Some((2, 1))); + // id-3 moves before id-2, with increment relative to id-1 + assert_eq!(queue.dequeue(), Some((1, 3))); + // id-2 has increment adjusted to be relative to id-3 + assert_eq!(queue.dequeue(), Some((4, 2))); + // no more remain + assert_eq!(queue.dequeue(), None); } #[test] - fn retrieve_only_one() { + fn test_remove_by_id_from_middle() { // Given - let mut queue: Queue<&'static str> = Queue::new(); - queue.enqueue(1, "A"); + let mut queue = Queue::new(); + queue.enqueue((1, 1)); + queue.enqueue((2, 2)); + queue.enqueue((3, 3)); + queue.enqueue((4, 4)); // When - let first = queue.next(); - let no_more = queue.next(); + queue.remove(3); - // Then - assert_eq!(first, Some(QueueElement(1, "A"))); - assert_eq!(no_more, None); + // Then expect + assert_eq!(queue.dequeue(), Some((1, 1))); + assert_eq!(queue.dequeue(), Some((1, 2))); + assert_eq!(queue.dequeue(), Some((2, 4))); } #[test] - fn inserting_two_reversing_order_and_adjusting_increments() { + fn test_remove_by_id_from_back() { // Given - let mut queue: Queue<&'static str> = Queue::new(); - queue.enqueue(2, "A"); - queue.enqueue(1, "B"); + let mut queue = Queue::new(); + queue.enqueue((1, 1)); + queue.enqueue((2, 2)); + queue.enqueue((3, 3)); + queue.enqueue((4, 4)); // When - let first = queue.next(); - let second = queue.next(); - let no_more = queue.next(); - - // Then - assert_eq!(first, Some(QueueElement(1, "B"))); - assert_eq!(second, Some(QueueElement(1, "A"))); - assert_eq!(no_more, None); - } + queue.remove(4); - #[test] - fn insert_two_maintaining_order_and_adjusting_increments() { - // Given - let mut queue: Queue<&'static str> = Queue::new(); - queue.enqueue(1, "A"); - queue.enqueue(2, "B"); - - // When - let first = queue.next(); - let second = queue.next(); - let no_more = queue.next(); - - // Then - assert_eq!(first, Some(QueueElement(1, "A"))); - assert_eq!(second, Some(QueueElement(1, "B"))); - assert_eq!(no_more, None); + // Then expect + assert_eq!(queue.dequeue(), Some((1, 1))); + assert_eq!(queue.dequeue(), Some((1, 2))); + assert_eq!(queue.dequeue(), Some((1, 3))); } #[test] - fn insert_three_maintaining_order_and_adjusting_increments() { + fn test_remove_by_id_from_front() { // Given - let mut queue: Queue<&'static str> = Queue::new(); - queue.enqueue(1, "A"); - queue.enqueue(2, "B"); - queue.enqueue(3, "C"); - - // When - let first = queue.next(); - let second = queue.next(); - let third = queue.next(); - let no_more = queue.next(); - - // Then - assert_eq!(first, Some(QueueElement(1, "A"))); - assert_eq!(second, Some(QueueElement(1, "B"))); - assert_eq!(third, Some(QueueElement(1, "C"))); - assert_eq!(no_more, None); - } - - #[test] - fn insert_three_with_last_in_the_middle_and_adjusting_increments() { - // Given - let mut queue: Queue<&'static str> = Queue::new(); - queue.enqueue(1, "A"); - queue.enqueue(3, "B"); - queue.enqueue(2, "C"); + let mut queue = Queue::new(); + queue.enqueue((1, 1)); + queue.enqueue((2, 2)); + queue.enqueue((3, 3)); + queue.enqueue((4, 4)); // When - let first = queue.next(); - let second = queue.next(); - let third = queue.next(); - let no_more = queue.next(); - - // Then - assert_eq!(first, Some(QueueElement(1, "A"))); - assert_eq!(second, Some(QueueElement(1, "C"))); - assert_eq!(third, Some(QueueElement(1, "B"))); - assert_eq!(no_more, None); - } + queue.remove(1); - #[test] - fn insert_three_reversing_order_and_adjusting_increments() { - // Given - let mut queue: Queue<&'static str> = Queue::new(); - queue.enqueue(3, "A"); - queue.enqueue(2, "B"); - queue.enqueue(1, "C"); - - // When - let first = queue.next(); - let second = queue.next(); - let third = queue.next(); - let no_more = queue.next(); - - // Then - assert_eq!(first, Some(QueueElement(1, "C"))); - assert_eq!(second, Some(QueueElement(1, "B"))); - assert_eq!(third, Some(QueueElement(1, "A"))); - assert_eq!(no_more, None); + // Then expect + assert_eq!(queue.dequeue(), Some((2, 2))); + assert_eq!(queue.dequeue(), Some((1, 3))); + assert_eq!(queue.dequeue(), Some((1, 4))); } } diff --git a/src/battle/skill.rs b/src/battle/skill.rs new file mode 100644 index 0000000..8c54d34 --- /dev/null +++ b/src/battle/skill.rs @@ -0,0 +1,126 @@ +use super::base::*; +use crate::context::Context; + +use std::collections::HashMap; + +#[allow(dead_code)] +pub struct DeciderDb { + container: HashMap>, +} +#[allow(dead_code)] +impl DeciderDb { + pub fn from(deciders: Vec<(AiId, Box)>) -> DeciderDb { + DeciderDb { + container: deciders.into_iter().collect(), + } + } + + pub fn get(&self, ai_id: AiId) -> Option<&dyn Decider> { + self.container.get(&ai_id).map(|x| x.as_ref()) + } +} + +pub trait Decider { + fn decide( + &self, + context: &Context, + actor_id: ActorId, + ) -> Result<(SkillId, SkillTarget), String>; +} + +pub struct HumanDecider; + +impl Decider for HumanDecider { + fn decide( + &self, + context: &Context, + actor_id: ActorId, + ) -> Result<(SkillId, SkillTarget), String> { + let actor_db = context.get::().unwrap(); + let skill_db = context.get::().unwrap(); + let uic = context.get::().unwrap(); + let actor = actor_db.get(actor_id).unwrap(); + let skill_id = uic.chose_skill(context, &actor.skills); + match skill_db.get(skill_id).unwrap().accepts { + AcceptableTargetTypes::Actor(alignments) => Ok(( + skill_id, + SkillTarget::Actor(uic.chose_target_actor(context, actor.team_id, alignments)), + )), + AcceptableTargetTypes::Team(aligments) => Ok(( + skill_id, + SkillTarget::Team(uic.chose_target_team(context, actor.team_id, aligments)), + )), + AcceptableTargetTypes::None => Ok((skill_id, SkillTarget::None)), + } + } +} + +#[allow(dead_code)] +pub enum SkillEffect { + Damage(Hp, EffectTarget), + Heal(Hp, EffectTarget), +} + +#[allow(dead_code)] +#[derive(Clone, Copy)] +pub enum AcceptableTargetAlignments { + Friend, + Enemy, + All, +} + +#[allow(dead_code)] +pub enum AcceptableTargetTypes { + Actor(AcceptableTargetAlignments), + Team(AcceptableTargetAlignments), + None, +} + +#[allow(dead_code)] +pub struct SkillDb { + container: HashMap, +} + +#[allow(dead_code)] +impl SkillDb { + pub fn from(skills: Vec) -> SkillDb { + SkillDb { + container: skills.into_iter().map(|skill| (skill.id, skill)).collect(), + } + } + + pub fn get(&self, skill_id: SkillId) -> Option<&Skill> { + self.container.get(&skill_id) + } +} + +#[allow(dead_code)] +pub struct Skill { + pub id: SkillId, + pub name: String, + pub accepts: AcceptableTargetTypes, + pub func: Box, +} + +pub trait SkillFunc { + fn apply( + &self, + context: &Context, + actor_id: ActorId, + target: SkillTarget, + ) -> Result, String>; +} + +impl SkillFunc for T +where + T: Fn(&Context, ActorId, SkillTarget) -> Result, String>, +{ + fn apply( + &self, + context: &Context, + actor_id: ActorId, + target: SkillTarget, + ) -> Result, String> { + self(context, actor_id, target) + } +} diff --git a/src/battle/uic/mod.rs b/src/battle/uic/mod.rs new file mode 100644 index 0000000..cd29e4e --- /dev/null +++ b/src/battle/uic/mod.rs @@ -0,0 +1,157 @@ +use super::actor::{Actor, ActorDb, ActorState, ActorStateDb, Team, TeamDb}; +use super::base::{ActorId, SkillId, TeamId}; +use super::queue::Queue; +use super::skill::{AcceptableTargetAlignments, Skill, SkillDb}; +use crate::context::Context; +#[allow(dead_code, unused_variables)] +pub struct Uic; +#[allow(dead_code, unused_variables)] +impl Uic { + pub fn start_battle(&self, context: &Context) { + println!("Starting battle!"); + } + + pub fn create_queue(&self, context: &Context, queue: &Queue) { + print!("Turn Order: ["); + let actor_db: &ActorDb = &*context.get().unwrap(); + let mut iter = queue.iter(); + if let Some((_, id)) = iter.next() { + print!("{}", actor_db.get(*id).unwrap().name); + } + for (_, id) in iter { + print!(", {}", actor_db.get(*id).unwrap().name); + } + println!("]"); + } + + pub fn start_turn(&self, context: &Context, actor_id: ActorId) { + let actor_db = context.get::().unwrap(); + let actor_state_db = context.get::().unwrap(); + let actor = actor_db.get(actor_id).unwrap(); + let actor_state = actor_state_db.get(actor_id).unwrap(); + println!("Turn of {}", actor.name); + println!( + "Hp: {}/{}", + if let ActorState::Alive(hp) = actor_state { + hp + } else { + panic!() + }, + actor.stats.max_hp + ); + } + + pub fn chose_skill(&self, context: &Context, skill_ids: &[SkillId]) -> SkillId { + let skill_db = context.get::().unwrap(); + let skills: Vec<&Skill> = skill_ids + .iter() + .map(|id| skill_db.get(*id).unwrap()) + .collect(); + + println!("Choose skill:"); + for (i, skill) in skills.iter().enumerate() { + println!("{}: {}", i, skill.name); + } + + let stdin = std::io::stdin(); + let mut buf = String::new(); + stdin.read_line(&mut buf).unwrap(); + let i: usize = buf.trim().parse().unwrap(); + + skills[i].id + } + + pub fn chose_target_actor( + &self, + context: &Context, + team_id: TeamId, + alignments: AcceptableTargetAlignments, + ) -> ActorId { + let team_db = context.get::().unwrap(); + let actor_db = context.get::().unwrap(); + let condition: Box bool> = match alignments { + AcceptableTargetAlignments::Friend => { + Box::from(|(id, _): &(&TeamId, &Team)| **id == team_id) + } + AcceptableTargetAlignments::Enemy => { + Box::from(|(id, _): &(&TeamId, &Team)| **id != team_id) + } + AcceptableTargetAlignments::All => Box::from(|_: &(&TeamId, &Team)| true), + }; + let targets: Vec<&Actor> = team_db + .iter() + .filter(condition) + .flat_map(|(team_id, team)| team.actors.clone()) + .map(|id| actor_db.get(id).unwrap()) + .collect(); + println!("Choose target:"); + for (i, actor) in targets.iter().enumerate() { + println!("{}: {}", i, actor.name) + } + + let stdin = std::io::stdin(); + let mut buf = String::new(); + stdin.read_line(&mut buf).unwrap(); + let i: usize = buf.trim().parse().unwrap(); + + targets[i].id + } + + pub fn chose_target_team( + &self, + context: &Context, + team_id: TeamId, + alignments: AcceptableTargetAlignments, + ) -> TeamId { + let team_db = context.get::().unwrap(); + let actor_db = context.get::().unwrap(); + let condition: Box bool> = match alignments { + AcceptableTargetAlignments::Friend => { + Box::from(|(id, _): &(&TeamId, &Team)| **id == team_id) + } + AcceptableTargetAlignments::Enemy => { + Box::from(|(id, _): &(&TeamId, &Team)| **id != team_id) + } + AcceptableTargetAlignments::All => Box::from(|_: &(&TeamId, &Team)| true), + }; + let targets: Vec = team_db + .iter() + .filter(condition) + .map(|(team_id, team)| *team_id) + .collect(); + println!("Choose target:"); + for (i, team_id) in targets.iter().enumerate() { + println!("{}: {}", i, team_id) + } + + let stdin = std::io::stdin(); + let mut buf = String::new(); + stdin.read_line(&mut buf).unwrap(); + let i: usize = buf.trim().parse().unwrap(); + + targets[i] + } + + pub fn hp_effect( + &self, + context: &Context, + actor_id: ActorId, + old_state: ActorState, + new_state: ActorState, + ) { + let actor_db = context.get::().unwrap(); + let actor = actor_db.get(actor_id).unwrap(); + let max_hp = actor.stats.max_hp; + let name = &actor.name; + let old_str = match old_state { + ActorState::Alive(hp) => format!("{}/{}", hp, max_hp), + ActorState::Dead => String::from("Dead"), + }; + let new_str = match new_state { + ActorState::Alive(hp) => format!("{}/{}", hp, max_hp), + ActorState::Dead => String::from("Dead"), + }; + + println!("Effect on {}: {} => {}", name, old_str, new_str); + } +} diff --git a/src/context.rs b/src/context.rs new file mode 100644 index 0000000..62bddbd --- /dev/null +++ b/src/context.rs @@ -0,0 +1,99 @@ +use std::any::{Any, TypeId}; +use std::cell::{Ref, RefCell, RefMut}; +use std::collections::HashMap; + +#[allow(dead_code)] +pub struct Context { + map: HashMap>>, +} + +#[allow(dead_code)] +impl Context { + pub fn new() -> Context { + Context { + map: HashMap::new(), + } + } + pub fn set(&mut self, t: T) { + self.map + .insert(TypeId::of::(), Box::new(RefCell::new(t))); + } + + pub fn get(&self) -> Option> { + self.map + .get(&TypeId::of::()) + .map(|cell| Ref::map(cell.borrow(), |t| t.downcast_ref::().unwrap())) + } + + pub fn get_mut(&self) -> Option> { + self.map.get(&TypeId::of::()).map(|cell| { + RefMut::map(cell.borrow_mut(), |t: &mut dyn Any| { + t.downcast_mut::().unwrap() + }) + }) + } + + pub fn contains(&self) -> bool { + self.map.contains_key(&TypeId::of::()) + } +} + +#[macro_export] +macro_rules! require { + ($context:expr; $( $T:ty ),* ) => { + Ok(()) + $( + .and( + if !$context.contains::<$T>() { + Err(format!("Context does not contain {}", std::any::type_name::<$T>())) + } else { + Ok(()) + } + ) + )* + as std::result::Result<(), String> + + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Eq, PartialEq, Debug)] + struct TestStruct(&'static str); + + #[test] + fn test_context() { + // Given + let test_data = TestStruct("some content"); + let mut context = Context::new(); + + // When + context.set(test_data); + let retrieved_data: Ref = context.get().unwrap(); + + // Then + assert_eq!(*retrieved_data, TestStruct("some content")) + } + + #[test] + fn test_mut() { + // Given + let test_data = TestStruct("some content"); + let mut context = Context::new(); + + // When + context.set(test_data); + { + let mut retrieved_data: RefMut = context.get_mut().unwrap(); + retrieved_data.0 = "some other content"; + } + + // Then + assert_eq!( + *context.get::().unwrap(), + TestStruct("some other content") + ); + } +} diff --git a/src/main.rs b/src/main.rs index d53640b..e29f1ce 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,86 @@ +#![feature(trait_alias, option_unwrap_none)] mod battle; +mod context; + +use battle::actor::*; +use battle::base::*; +use battle::skill::*; +use battle::uic::Uic; +use context::Context; fn main() { println!("Hello, world!"); + + let mut context = Context::new(); + context.set(ActorDb::from(actors_example())); + context.set(ActorStateDb::new()); + let team_db = TeamDb::from(&context.get().unwrap(), TeamAiOverrides(Vec::new())); + context.set(team_db); + context.set(DeciderDb::from(vec![(0, Box::from(HumanDecider))])); + let skill_db = SkillDb::from(skills_example()); + context.set(skill_db); + context.set(Uic); + battle::run_battle(context).unwrap(); +} + +fn actors_example() -> Vec { + vec![ + Actor { + id: 0, + team_id: 0, + name: String::from("Geralt"), + stats: ActorStats { + initiative: 100, + max_hp: 100, + }, + ai: None, + skills: vec![0, 1], + }, + Actor { + id: 1, + team_id: 1, + name: String::from("Steff"), + stats: ActorStats { + initiative: 120, + max_hp: 60, + }, + ai: None, + skills: vec![0, 2], + }, + ] +} + +fn skills_example() -> Vec { + vec![ + Skill { + id: 0, + name: String::from("Wait"), + accepts: AcceptableTargetTypes::None, + func: Box::from(|_: &Context, _: ActorId, _: SkillTarget| Ok(Vec::new())), + }, + Skill { + id: 1, + name: String::from("Heavy Scratch"), + accepts: AcceptableTargetTypes::Actor(AcceptableTargetAlignments::Enemy), + func: Box::from(|_: &Context, _: ActorId, t: SkillTarget| { + if let SkillTarget::Actor(id) = t { + Ok(vec![SkillEffect::Damage(25, EffectTarget(id))]) + } else { + Err(String::from("Accepts only actor-targets")) + } + }), + }, + Skill { + id: 2, + name: String::from("Light Scratch"), + accepts: AcceptableTargetTypes::Actor(AcceptableTargetAlignments::Enemy), + func: Box::from(|_: &Context, _: ActorId, t: SkillTarget| { + if let SkillTarget::Actor(id) = t { + Ok(vec![SkillEffect::Damage(20, EffectTarget(id))]) + } else { + Err(String::from("Accepts only actor-targets")) + } + }), + }, + ] }