diff --git a/Cargo.toml b/Cargo.toml index 28110ab..d8bc412 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,3 +17,9 @@ strum = "0.24" strum_macros = "0.24" rayon = "1.7.0" +log = "0.4.18" +env_logger = "0.10.0" + +[dev-dependencies] + +test-log = "0.2.11" \ No newline at end of file diff --git a/examples/arena.rs b/examples/arena.rs index 84013a6..f28354a 100644 --- a/examples/arena.rs +++ b/examples/arena.rs @@ -44,6 +44,8 @@ where } fn main() { + env_logger::init(); + let mut rng = thread_rng(); let n = 100; @@ -52,8 +54,6 @@ fn main() { let mut characters: Vec = iter::repeat_with(random_character).take(n).collect(); print_chars(characters.iter().take(keep).collect::>()); - // print_chars(&characters1); - // print_chars(&characters2); for g in 0..gen { characters.shuffle(&mut rng); @@ -76,7 +76,7 @@ fn main() { for _i in 0..n_rounds { let mut c1l = c1.to_owned(); let mut c2l = c2.to_owned(); - let t = make_them_fight(&mut c1l, &mut c2l); + let t = make_them_fight(vec![&mut c1l], vec![&mut c2l]); match t { Some(_turns) => { diff --git a/examples/bob_v_drub.rs b/examples/bob_v_drub.rs index f88119e..d88170e 100644 --- a/examples/bob_v_drub.rs +++ b/examples/bob_v_drub.rs @@ -1,40 +1,43 @@ use kartsimrust::{ - character::create_character, - class::Class, - combat::EncounterType, - equipment::{Armor, WeaponType}, - stats::StatBlock, + combat::make_them_fight, + premade::{get_bob, get_drub}, }; fn main() { - let mut bob = create_character( - String::from("Bob"), - Class::Guard, - Class::Brawler, - StatBlock::from((1, 0, 1, 0, 0, 0, 0, 0)), - StatBlock::from((2, 0, 2, 0, -1, -2, 0, -1)), - Armor::Medium, - WeaponType::BladedWeapon.create_weapon("Longsword".to_owned()), - ); + let mut a = 1.0; + let mut b = 1.0; - let mut drub = create_character( - String::from("Drub"), - Class::Hunter, - Class::Thief, - StatBlock::from((0, 1, 1, 0, 0, 0, 0, 0)), - StatBlock::from((0, 2, 2, 0, -2, 0, 0, -2)), - Armor::Light, - WeaponType::RangedWeapon.create_weapon(String::from("Shortbow")), - ); + let mut total_turns = 0.0; + let mut battles = 0.0; - bob.init_dice_pool(EncounterType::Physical); - drub.init_dice_pool(EncounterType::Physical); + loop { + let mut bob = get_bob(); - println!("{}", bob); - println!("{}", drub); + let mut drub = get_drub(); + let mut drub2 = get_drub(); - bob.attacks(&mut drub); + if let Some(turns) = make_them_fight(vec![&mut bob], vec![&mut drub, &mut drub2]) { + battles += 1.0; + total_turns += turns as f64; - println!("{}", bob); - println!("{}", drub); + if bob.hp <= 0 { + b += 1.0; + println!("Bob died in {} turns.", turns) + } else { + a += 1.0; + println!("Drub died in {} turns.", turns) + } + + if (a * b) / (f64::powi(a + b, 2) * (a + b + 1.0)) <= 0.00001 { + break; + } + } else { + println!("No one died.") + } + } + println!( + "Bob won {:.2}% of battles in an average of {:.1} turns.", + a / (a + b) * 100.0, + total_turns / battles + ) } diff --git a/src/character.rs b/src/character.rs index 93fc07f..55fff1b 100644 --- a/src/character.rs +++ b/src/character.rs @@ -107,6 +107,10 @@ impl Character { adjusted_stat_block[stat] } + + pub fn is_dead(&self) -> bool { + self.hp <= 0 + } } pub fn create_character( @@ -120,6 +124,7 @@ pub fn create_character( ) -> Character { if !(Vec::from(stat_choice).iter().all(|s| *s >= 0) && Vec::from(stat_choice).iter().sum::() == 2i64) + && !(class1 == Class::NPC && class2 == Class::NPC) { panic!("Invalid stat choice: {:?}", stat_choice) } diff --git a/src/class.rs b/src/class.rs index c66b24f..db90cf7 100644 --- a/src/class.rs +++ b/src/class.rs @@ -19,6 +19,7 @@ pub enum Class { Hunter, Witch, Wizard, + NPC, // Stubbed in for "no class" } impl ToStatBlock for Class { @@ -36,6 +37,8 @@ impl ToStatBlock for Class { // ----- Class::Witch => StatBlock::from((0, 0, 0, 0, 1, 1, 1, 0)), Class::Wizard => StatBlock::from((0, 0, 0, 0, 1, 1, 0, 1)), + // ----- + Class::NPC => StatBlock::from((0, 0, 0, 0, 0, 0, 0, 0)), } } } @@ -103,6 +106,7 @@ impl ToProficiencies for Class { ..Default::default() }, Class::Wizard => Proficiencies::default(), + Class::NPC => Proficiencies::default(), } } } @@ -119,33 +123,23 @@ mod class_tests { use crate::{class::Class, stats::ToStatBlock}; - // const CLASSES: [Class; 9] = [ - // Class::Guard, - // Class::Knight, - // Class::Brawler, - // Class::Thief, - // Class::Swashbuckler, - // Class::Survivalist, - // Class::Hunter, - // Class::Witch, - // Class::Wizard, - // ]; - #[test] fn stat_sums() { for class in Class::iter() { let stat_block = class.stat_block(); - assert_eq!( - 3, - stat_block.STR - + stat_block.DEX - + stat_block.CON - + stat_block.STB - + stat_block.INT - + stat_block.KND - + stat_block.CMP - + stat_block.CHA - ) + if class != Class::NPC { + assert_eq!( + 3, + stat_block.STR + + stat_block.DEX + + stat_block.CON + + stat_block.STB + + stat_block.INT + + stat_block.KND + + stat_block.CMP + + stat_block.CHA + ) + } } } diff --git a/src/combat.rs b/src/combat.rs index 574fdf8..7fd73fd 100644 --- a/src/combat.rs +++ b/src/combat.rs @@ -1,6 +1,6 @@ use crate::{character::Character, dice::successes}; -#[derive(Debug,PartialEq, Eq, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum EncounterType { Physical, Mental, @@ -13,41 +13,158 @@ impl Character { (opponent.ac() - self.weapon_proficiency() as i64).clamp(1, 12), ); - let damage = self.compute_damage(margin);//(margin + self.weapon_proficiency()).max(0) * self.weapon.damage_dice; + let damage = self.compute_damage(margin); //(margin + self.weapon_proficiency()).max(0) * self.weapon.damage_dice; opponent.hp = opponent.hp - damage as i64; self.adjust_dice_pool((-fail_dice).min(0), EncounterType::Physical); - // println!( - // "{} attacks {} getting a margin of {} (s: {}, f: {}) and dealing {} damage.", - // self.name, opponent.name, margin, succ, fail, damage - // ) + log::trace!( + "{} attacks {} getting a margin of {} and dealing {} damage.", + self.name, + opponent.name, + margin, + damage + ) } } -pub fn make_them_fight(character1: &mut Character, character2: &mut Character) -> Option { - character1.init_encounter(EncounterType::Physical); - character2.init_encounter(EncounterType::Physical); +pub fn make_them_fight(mut red: Vec<&mut Character>, mut blue: Vec<&mut Character>) -> Option { + for character in red.iter_mut() { + character.init_encounter(EncounterType::Physical); + } + + for character in blue.iter_mut() { + character.init_encounter(EncounterType::Physical); + } let mut turn = 0; - loop { + let mut dead_r = 0; + let mut dead_b = 0; + + while red.iter().any(|c| !c.is_dead()) && blue.iter().any(|c| !c.is_dead()) { turn += 1; - character1.attacks(character2); - if character2.hp <= 0 { - break; + if turn > 15 { + return None; } - character2.attacks(character1); - if character1.hp <= 0 { - break; - } + log::trace!("Turn {}", turn); + log::trace!( + "{:?}", + red.iter() + .map(|c| (c.name.to_owned(), c.hp)) + .collect::>() + ); + log::trace!( + "{:?}", + blue.iter() + .map(|c| (c.name.to_owned(), c.hp)) + .collect::>() + ); - if turn > 15 { - return None + let mut init_r = 0; + let mut init_b = 0; + + let mut target_r = 0; + let mut target_b = 0; + + while init_r < red.len() || init_b < blue.len() { + // Find next living red char + log::trace!("Searching init"); + while init_r < red.len() && red[init_r].is_dead() { + init_r += 1; + } + + // If character is found + if init_r < red.len() { + // Find target + log::trace!("Searching target"); + while blue[target_b].is_dead() { + target_b += 1; + target_b %= blue.len(); + } + log::trace!("Target found"); + + // Attack target + red[init_r].attacks(blue[target_b]); + + if blue[target_b].is_dead() { + log::trace!("{} is dead.", blue[target_b].name); + dead_b += 1; + } + + target_b += 1; + target_b %= blue.len(); + init_r += 1; + } + + // All blues are dead + if dead_b == blue.len() { + break; + } + + // Find next living blue + log::trace!("Searching init"); + while init_b < blue.len() && blue[init_b].is_dead() { + init_b += 1; + } + + // If found + if init_b < blue.len() { + log::trace!("Searching target"); + while red[target_r].hp <= 0 { + target_r += 1; + target_r %= red.len(); + } + + blue[init_b].attacks(red[target_r]); + + if red[target_r].is_dead() { + log::trace!("{} is dead.", red[target_r].name); + dead_r += 1; + } + + target_r += 1; + target_r %= red.len(); + init_b += 1; + } + + if dead_r == red.len() { + break; + } } } Some(turn) } + +#[cfg(test)] +mod combat_tests { + use crate::premade::{get_bob, get_drub}; + + use super::make_them_fight; + + #[test_log::test] + fn make_them_fight_debug() { + let mut red = vec![get_bob(), get_bob()]; + red[1].name = "Bob 2".to_owned(); + let mut blue = vec![get_drub(), get_drub()]; + blue[1].name = "Drub 2".to_owned(); + + make_them_fight(red.iter_mut().collect(), blue.iter_mut().collect()); + + print!( + "{:?}", + red.into_iter() + .map(|c| (c.name, c.hp)) + .collect::>() + ); + print!( + "{:?}", + blue.into_iter() + .map(|c| (c.name, c.hp)) + .collect::>() + ); + } +} diff --git a/src/lib.rs b/src/lib.rs index f315974..6ceeddb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -7,4 +7,6 @@ pub mod equipment; pub mod skills; pub mod stats; -pub mod random_character; \ No newline at end of file +pub mod random_character; + +pub mod premade; \ No newline at end of file diff --git a/src/premade/mod.rs b/src/premade/mod.rs new file mode 100644 index 0000000..70e9ade --- /dev/null +++ b/src/premade/mod.rs @@ -0,0 +1,61 @@ +use super::character::{create_character, Character}; +use super::class::Class; +use super::equipment::{Armor, WeaponType}; +use super::stats::StatBlock; + +/* + * Bob has chosen to play a knight who is also an [NPC]. + * He has chosen to make a knight who is strong and dexterous. + * He has also decided that his knight is not charismatic, but rather athletic. + */ +pub fn get_bob() -> Character { + create_character( + "Bob".to_owned(), + Class::Knight, + Class::NPC, + StatBlock::from((1, 1, 0, 0, 0, 0, 0, 0)), + StatBlock::from((0, 0, 2, 0, 0, 0, 0, -2)), + Armor::Medium, + WeaponType::BladedWeapon.create_weapon("Longsword".to_owned()), + ) +} + +/* + * Drub is a goblin. + */ +pub fn get_drub() -> Character { + let mut drub = create_character( + String::from("Drub"), + Class::NPC, + Class::NPC, + StatBlock::from((2, 3, 2, 0, 1, 0, -3, -2)), + StatBlock::default(), + Armor::Light, + WeaponType::SimpleWeapon.create_weapon(String::from("Knife")), + ); + + drub.proficiencies.simple_weapons = true; + drub.proficiencies.light_armor = true; + + return drub +} + +#[cfg(test)] +mod premade_tests { + + use super::{get_bob, get_drub}; + + #[test] + fn print_bob() { + let bob = get_bob(); + + println!("{}", bob); + } + + #[test] + fn print_drub() { + let drub = get_drub(); + + println!("{}", drub); + } +}