From b7c5e7a37aa80399a0e4d1e8edcdb9a8379cd011 Mon Sep 17 00:00:00 2001 From: HeNine <> Date: Sat, 27 May 2023 19:01:42 +0200 Subject: [PATCH] in the beginning there was code --- .gitignore | 16 ++++ Cargo.toml | 19 ++++ examples/arena.rs | 127 +++++++++++++++++++++++++ examples/bob_v_drub.rs | 40 ++++++++ examples/bob_v_glob.rs | 40 ++++++++ examples/char_print_test.rs | 20 ++++ examples/random_char.rs | 18 ++++ src/character.rs | 183 ++++++++++++++++++++++++++++++++++++ src/class.rs | 160 +++++++++++++++++++++++++++++++ src/combat.rs | 53 +++++++++++ src/dice.rs | 92 ++++++++++++++++++ src/equipment.rs | 150 +++++++++++++++++++++++++++++ src/lib.rs | 10 ++ src/random_character.rs | 99 +++++++++++++++++++ src/skills.rs | 90 ++++++++++++++++++ src/stats.rs | 168 +++++++++++++++++++++++++++++++++ 16 files changed, 1285 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 examples/arena.rs create mode 100644 examples/bob_v_drub.rs create mode 100644 examples/bob_v_glob.rs create mode 100644 examples/char_print_test.rs create mode 100644 examples/random_char.rs create mode 100644 src/character.rs create mode 100644 src/class.rs create mode 100644 src/combat.rs create mode 100644 src/dice.rs create mode 100644 src/equipment.rs create mode 100644 src/lib.rs create mode 100644 src/random_character.rs create mode 100644 src/skills.rs create mode 100644 src/stats.rs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8557f61 --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# Generated by Cargo +# will have compiled files and executables +debug/ +target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk + +# MSVC Windows builds of rustc generate these, which store debugging information +*.pdb + +.vscode \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..6b8ada1 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = 'simrust' +version = '0.1.0' +edition = '2021' + +[lib] +crate-type = [ 'lib' ] + +[profile.release] +debug = true + +[dependencies] + +rand = '0.8.4' +rand_distr = '0.4.3' +strum = "0.24" +strum_macros = "0.24" +rayon = "1.7.0" + diff --git a/examples/arena.rs b/examples/arena.rs new file mode 100644 index 0000000..b6d233c --- /dev/null +++ b/examples/arena.rs @@ -0,0 +1,127 @@ +use std::{ + io::{self, Write}, + iter, +}; + +use rand::{seq::SliceRandom, thread_rng}; +// use rayon::prelude::*; +use simrust::{ + character::Character, + combat::{make_them_fight, EncounterType}, + random_character::random_character, + stats::Stat, +}; + +fn print_chars<'a, L>(characters: L) +where + L: IntoIterator, +{ + println!("Name | Classes | HP MP AC DP | STR DEX CON STB | INT KND CMP CHA | Armor | Weapon"); + for ch in characters { + let mut c = ch.to_owned(); + c.init_encounter(EncounterType::Physical); + println!( + "{name:<8} | {class1:<12} {class2:<12} | {hp:>2} {mp:>2} {ac:>2} {dp:>2} | {str:>3} {dex:>3} {con:>3} {stb:>3} | {int:>3} {knd:>3} {cmp:>3} {cha:>3} | {armor:<6} | {weapon:<15}", + name = c.name, + class1 = c.class1, + class2 = c.class2, + hp=c.hp, + mp = c.mp, + ac=c.ac(), + dp = c.dice_pool(), + str=c.stat(Stat::STR), + dex=c.stat(Stat::DEX), + con=c.stat(Stat::CON), + stb=c.stat(Stat::STB), + int=c.stat(Stat::INT), + knd=c.stat(Stat::KND), + cmp=c.stat(Stat::CMP), + cha=c.stat(Stat::CHA), + armor = c.armor, + weapon = c.weapon, + ); + } +} + +fn main() { + let mut rng = thread_rng(); + + let n = 100; + let keep = 20; + let gen = 50000; + + 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); + + let teams: (Vec<(usize, Character)>, Vec<(usize, Character)>) = characters + .iter() + .cloned() + .enumerate() + .partition(|(i, _)| *i % 2 == 0); + let matches: Vec<(Character, Character)> = iter::zip(teams.0.iter(), teams.1.iter()) + .map(|((_, c1), (_, c2))| (c1.to_owned(), c2.to_owned())) + .collect(); + + let mut winners: Vec<(i64, Character)> = matches + .iter() + .map(|(c1, c2)| { + let n_rounds = 50; + let mut w: i64 = 0; + + 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); + + match t { + Some(_turns) => { + if c2l.hp <= 0 { + w += 1 + } + } + None => (), + } + } + if w >= n_rounds / 2 { + (-w, c1.to_owned()) + } else { + (-(n_rounds - w), c2.to_owned()) + } + }) + .collect(); + + winners.sort_by_key(|(i, _)| *i); + + characters = winners + .iter() + .take(keep) + .map(|(_, c)| c.clone()) + .chain( + (0..(n - keep)) + .into_iter() + .map(|_| random_character()) + .take(n - keep), + ) + .collect(); + + if g % (gen / 100) == 0 { + print!( + "\r{:#>()); +} diff --git a/examples/bob_v_drub.rs b/examples/bob_v_drub.rs new file mode 100644 index 0000000..ec9710f --- /dev/null +++ b/examples/bob_v_drub.rs @@ -0,0 +1,40 @@ +use simrust::{ + character::create_character, + class::Class, + combat::EncounterType, + equipment::{Armor, WeaponType}, + stats::StatBlock, +}; + +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 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")), + ); + + bob.init_dice_pool(EncounterType::Physical); + drub.init_dice_pool(EncounterType::Physical); + + println!("{}", bob); + println!("{}", drub); + + bob.attacks(&mut drub); + + println!("{}", bob); + println!("{}", drub); +} diff --git a/examples/bob_v_glob.rs b/examples/bob_v_glob.rs new file mode 100644 index 0000000..cd78c3e --- /dev/null +++ b/examples/bob_v_glob.rs @@ -0,0 +1,40 @@ +use simrust::{ + character::create_character, + class::Class, + combat::EncounterType, + equipment::{Armor, WeaponType}, + stats::StatBlock, +}; + +fn main() { + let mut bob = create_character( + String::from("Bob"), + Class::Hunter, + Class::Knight, + 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 glob = create_character( + String::from("Glob"), + 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()), + ); + + bob.init_dice_pool(EncounterType::Physical); + glob.init_dice_pool(EncounterType::Physical); + + println!("{}", bob); + println!("{}", glob); + + bob.attacks(&mut glob); + + println!("{}", bob); + println!("{}", glob); +} diff --git a/examples/char_print_test.rs b/examples/char_print_test.rs new file mode 100644 index 0000000..e3acf50 --- /dev/null +++ b/examples/char_print_test.rs @@ -0,0 +1,20 @@ +use simrust::{ + character::create_character, + class::Class, + equipment::{Armor, WeaponType}, + random_character::{random_stat_adjust, random_stat_choice}, +}; + +fn main() { + let c = create_character( + "Callypigian".into(), + Class::Swashbuckler, + Class::Survivalist, + random_stat_choice(), + random_stat_adjust(), + Armor::Medium, + WeaponType::RangedWeapon.create_weapon(WeaponType::RangedWeapon.to_string()), + ); + + println!("{}", c) +} diff --git a/examples/random_char.rs b/examples/random_char.rs new file mode 100644 index 0000000..08276d9 --- /dev/null +++ b/examples/random_char.rs @@ -0,0 +1,18 @@ +use simrust::{random_character::{random_character}, combat::EncounterType}; + +fn main() { + // for _i in 0..10 { + // let sb = random_stat_adjust(); + // println!("{:?}", sb); + // println!( + // "{:?}", + // Vec::from(sb).iter().map(|x| { x.abs() }).sum::() / 2 + // ); + // } + + for _i in 0..3 { + let mut c = random_character(); + c.init_dice_pool(EncounterType::Physical); + println!("{}", c); + } +} diff --git a/src/character.rs b/src/character.rs new file mode 100644 index 0000000..1493438 --- /dev/null +++ b/src/character.rs @@ -0,0 +1,183 @@ +use std::fmt::Display; + +use crate::{ + class::Class, + combat::EncounterType, + equipment::{Armor, Weapon, WeaponType}, + skills::{Proficiencies, ToProficiencies}, + stats::{Stat, StatBlock, ToStatBlock}, +}; + +#[derive(Debug, Clone)] +pub struct Character { + pub name: String, + pub class1: Class, + pub class2: Class, + pub stat_block: StatBlock, + pub hp: i64, + pub mp: i64, + pub proficiency: i64, + pub proficiencies: Proficiencies, + pub armor: Armor, + pub weapon: Weapon, + dice_pool: i64, +} + +impl Character { + pub fn ac(&self) -> i64 { + self.stat(Stat::STB) + self.stat(Stat::DEX) + self.armor_value() + } + + pub fn weapon_proficiency(&self) -> i64 { + if match self.weapon.weapon_type { + WeaponType::SimpleWeapon => self.proficiencies.simple_weapons, + WeaponType::BladedWeapon => self.proficiencies.bladed_weapons, + WeaponType::BluntWeapon => self.proficiencies.blunt_weapons, + WeaponType::PoleWeapon => self.proficiencies.pole_weapons, + WeaponType::ThrownWeapon => self.proficiencies.thrown_weapons, + WeaponType::RangedWeapon => self.proficiencies.ranged_weapons, + } { + self.proficiency + } else { + 0 + } + } + + pub fn armor_value(&self) -> i64 { + if match self.armor { + Armor::Heavy => self.proficiencies.heavy_armor, + Armor::Medium => self.proficiencies.medium_armor, + Armor::Light => self.proficiencies.light_armor, + Armor::None => true, + } { + self.armor.armor() + } else { + 0 + } + } + + pub fn compute_damage(&self, dice: i64) -> i64 { + (dice + self.weapon_proficiency()).max(0) * self.weapon.damage_dice + + if let Some(damage_stat) = self.weapon.stat_damage { + self.stat(damage_stat) + } else { + 0 + } + } + + pub fn dice_pool(&self) -> i64 { + self.dice_pool + } + + pub fn init_dice_pool(&mut self, encounter_type: EncounterType) { + self.dice_pool = match encounter_type { + EncounterType::Physical => { + self.stat_block[self.weapon.weapon_stat] + self.weapon_proficiency() + } + EncounterType::Mental => self.stat_block.INT, + } + .max(0) + } + + pub fn adjust_dice_pool(&mut self, adjustment: i64, encounter_type: EncounterType) { + self.dice_pool = (self.dice_pool + + adjustment + + match encounter_type { + EncounterType::Physical => self.stat_block.CON, + EncounterType::Mental => self.stat_block.CMP, + }) + .max(0) + } + + pub fn init_hp_mp(&mut self, encounter_type: EncounterType) { + if encounter_type == EncounterType::Physical { + self.hp = (self.stat(Stat::CON) + self.stat(Stat::STB)) * 3; + } else { + self.mp = (self.stat(Stat::CMP) + self.stat(Stat::CHA)) * 3; + } + } + + pub fn init_encounter(&mut self, encounter_type: EncounterType) { + self.init_dice_pool(encounter_type); + self.init_hp_mp(encounter_type); + } + + pub fn stat(&self, stat: Stat) -> i64 { + let adjusted_stat_block = self.stat_block + self.armor.stat_adjust(); + + adjusted_stat_block[stat] + } +} + +pub fn create_character( + name: String, + class1: Class, + class2: Class, + stat_choice: StatBlock, + stat_adjust: StatBlock, + armor: Armor, + weapon: Weapon, +) -> Character { + if !(Vec::from(stat_choice).iter().all(|s| *s >= 0) + && Vec::from(stat_choice).iter().sum::() == 2i64) + { + panic!("Invalid stat choice: {:?}", stat_choice) + } + if Vec::from(stat_adjust).iter().sum::() != 0 { + panic!("Invalid stat adjust: {:?}", stat_adjust) + } + let stat_block = + StatBlock::ones() + class1.stat_block() + class2.stat_block() + stat_choice + stat_adjust; + Character { + name, + class1, + class2, + stat_block, + hp: (stat_block.CON + stat_block.STB) * 3, + mp: (stat_block.CMP + stat_block.CHA) * 3, + proficiency: 1, + proficiencies: class1.proficiencies() | class2.proficiencies(), + armor, + weapon, + dice_pool: 0, + } +} + +impl Display for Character { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + r#"╔═══════════════════════════════╤═══════╗ +║ {name:<15} {class1:>12} │ HP {hp:>2} ║ +║ {class2:>12} │ MP {mp:>2} ║ +╠═══════════════════╤═══════════╧═══════╣ +║ STR {str:>2} │ {int:>2} INT ║ +║ DEX {dex:>2} │ {knd:>2} KND ║ +║ CON {con:>2} │ {cmp:>2} CMP ║ +║ STB {stb:>2} │ {cha:>2} CHA ║ +╠═══════════════════╧═══════════════════╣ +║ Armor {armor:<15} AC {ac:>2} ║ +║ Weapon {weapon:<15} ║ +╠═══════════════════════════════════════╣ +║ Dice pool {dp:>2} ║ +╚═══════════════════════════════════════╝"#, + name = self.name, + class1 = self.class1, + class2 = self.class2, + hp = self.hp, + mp = self.mp, + ac = self.ac(), + str = self.stat_block.STR, + dex = self.stat_block.DEX, + con = self.stat_block.CON, + stb = self.stat_block.STB, + int = self.stat_block.INT, + knd = self.stat_block.KND, + cmp = self.stat_block.CMP, + cha = self.stat_block.CHA, + armor = self.armor, + weapon = self.weapon, + dp = self.dice_pool() + ) + } +} diff --git a/src/class.rs b/src/class.rs new file mode 100644 index 0000000..c66b24f --- /dev/null +++ b/src/class.rs @@ -0,0 +1,160 @@ +use rand::seq::IteratorRandom; +use rand_distr::{Distribution, Standard}; +use strum::IntoEnumIterator; +use strum_macros::{Display, EnumIter}; + +use crate::{ + skills::{Proficiencies, ToProficiencies}, + stats::{StatBlock, ToStatBlock}, +}; + +#[derive(Debug, PartialEq, Eq, EnumIter, Clone, Copy, Display)] +pub enum Class { + Guard, + Knight, + Brawler, + Thief, + Swashbuckler, + Survivalist, + Hunter, + Witch, + Wizard, +} + +impl ToStatBlock for Class { + fn stat_block(&self) -> StatBlock { + match self { + Class::Guard => StatBlock::from((1, 0, 1, 1, 0, 0, 0, 0)), + Class::Knight => StatBlock::from((1, 0, 0, 1, 0, 0, 0, 1)), + Class::Brawler => StatBlock::from((1, 1, 1, 0, 0, 0, 0, 0)), + // ----- + Class::Thief => StatBlock::from((0, 1, 0, 0, 1, 0, 1, 0)), + Class::Swashbuckler => StatBlock::from((1, 1, 0, 0, 0, 0, 0, 1)), + // ----- + Class::Survivalist => StatBlock::from((0, 1, 1, 0, 1, 0, 0, 0)), + Class::Hunter => StatBlock::from((0, 1, 1, 0, 0, 1, 0, 0)), + // ----- + Class::Witch => StatBlock::from((0, 0, 0, 0, 1, 1, 1, 0)), + Class::Wizard => StatBlock::from((0, 0, 0, 0, 1, 1, 0, 1)), + } + } +} + +impl ToProficiencies for Class { + fn proficiencies(&self) -> Proficiencies { + match self { + Class::Guard => Proficiencies { + light_armor: true, + medium_armor: true, + heavy_armor: true, + // ----- + // bladed_weapons: true, + simple_weapons: true, + pole_weapons: true, + ..Default::default() + }, + Class::Knight => Proficiencies { + light_armor: true, + medium_armor: true, + heavy_armor: true, + // ----- + bladed_weapons: true, + ..Default::default() + }, + Class::Brawler => Proficiencies { + light_armor: true, + medium_armor: true, + // ----- + simple_weapons: true, + blunt_weapons: true, + thrown_weapons: true, + ..Default::default() + }, + Class::Thief => Proficiencies { + simple_weapons: true, + thrown_weapons: true, + ranged_weapons: true, + ..Default::default() + }, + Class::Swashbuckler => Proficiencies { + light_armor: true, + simple_weapons: true, + bladed_weapons: true, + thrown_weapons: true, + ranged_weapons: true, + ..Default::default() + }, + Class::Survivalist => Proficiencies { + // ----- + simple_weapons: true, + thrown_weapons: true, + ..Default::default() + }, + Class::Hunter => Proficiencies { + light_armor: true, + // ----- + simple_weapons: true, + pole_weapons: true, + ranged_weapons: true, + ..Default::default() + }, + Class::Witch => Proficiencies { + simple_weapons: true, + ..Default::default() + }, + Class::Wizard => Proficiencies::default(), + } + } +} + +impl Distribution for Standard { + fn sample(&self, rng: &mut R) -> Class { + Class::iter().choose(rng).unwrap() + } +} + +#[cfg(test)] +mod class_tests { + use strum::IntoEnumIterator; + + 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 + ) + } + } + + #[test] + fn all_unique() { + for class1 in Class::iter() { + for class2 in Class::iter() { + assert!(class1 == class2 || class1.stat_block() != class2.stat_block()) + } + } + } +} diff --git a/src/combat.rs b/src/combat.rs new file mode 100644 index 0000000..574fdf8 --- /dev/null +++ b/src/combat.rs @@ -0,0 +1,53 @@ +use crate::{character::Character, dice::successes}; + +#[derive(Debug,PartialEq, Eq, Clone, Copy)] +pub enum EncounterType { + Physical, + Mental, +} + +impl Character { + pub fn attacks(&mut self, opponent: &mut Character) { + let (margin, _succ, _fail, _succ_dice, fail_dice) = successes( + self.dice_pool().max(0) as usize, + (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; + + 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 + // ) + } +} + +pub fn make_them_fight(character1: &mut Character, character2: &mut Character) -> Option { + character1.init_encounter(EncounterType::Physical); + character2.init_encounter(EncounterType::Physical); + + let mut turn = 0; + + loop { + turn += 1; + character1.attacks(character2); + if character2.hp <= 0 { + break; + } + + character2.attacks(character1); + if character1.hp <= 0 { + break; + } + + if turn > 15 { + return None + } + } + + Some(turn) +} diff --git a/src/dice.rs b/src/dice.rs new file mode 100644 index 0000000..fb5096b --- /dev/null +++ b/src/dice.rs @@ -0,0 +1,92 @@ +use rand::Rng; +use std::ops; + +#[derive(Debug, Clone, Copy)] +#[allow(non_camel_case_types)] +pub enum Dice { + d4, + d6, + d8, + d10, + d12, + d20, + d100, +} + +impl Iterator for Dice { + type Item = i64; + + fn next(self: &mut Dice) -> Option { + let mut rng = rand::thread_rng(); + match self { + Dice::d4 => Some(rng.gen_range(1..4)), + Dice::d6 => Some(rng.gen_range(1..6)), + Dice::d8 => Some(rng.gen_range(1..8)), + Dice::d10 => Some(rng.gen_range(1..10)), + Dice::d12 => Some(rng.gen_range(1..12)), + Dice::d20 => Some(rng.gen_range(1..20)), + Dice::d100 => Some(rng.gen_range(1..100)), + } + } +} + +impl ops::Mul for i64 { + type Output = i64; + + fn mul(self, rhs: Dice) -> Self::Output { + rhs.take(self as usize).sum() + } +} + +pub fn successes(n: usize, threshold: i64) -> (i64, i64, i64, i64, i64) { + if threshold < 1 || threshold > 12 { + panic!("Invalid threshold: {}", threshold) + } + + let rolls: Vec = Dice::d12.take(n).collect(); + + let succ_dice = rolls.iter().filter(|roll| **roll > threshold).count() as i64; + let crit_succ = rolls.iter().filter(|roll| **roll == 12).count() as i64; + + let fail_dice = rolls.iter().filter(|roll| **roll <= threshold).count() as i64; + let crit_fail = rolls.iter().filter(|roll| **roll == 1).count() as i64; + + ( + succ_dice + crit_succ - fail_dice - crit_fail, + succ_dice + crit_succ, + fail_dice + crit_fail, + succ_dice, + fail_dice, + ) +} + +#[cfg(test)] +mod dice_tests { + + use super::*; + + #[test] + fn in_range() { + assert!(Dice::d4.take(1000).all(|roll| roll >= 1 && roll <= 4)); + assert!(Dice::d6.take(1000).all(|roll| roll >= 1 && roll <= 6)); + assert!(Dice::d8.take(1000).all(|roll| roll >= 1 && roll <= 8)); + assert!(Dice::d10.take(1000).all(|roll| roll >= 1 && roll <= 10)); + assert!(Dice::d12.take(1000).all(|roll| roll >= 1 && roll <= 12)); + assert!(Dice::d20.take(1000).all(|roll| roll >= 1 && roll <= 20)); + assert!(Dice::d100.take(1000).all(|roll| roll >= 1 && roll <= 100)) + } + + #[test] + #[should_panic] + fn wrong_threshold() { + successes(10, 0); + } +} + +pub trait Clampable: Ord + Copy { + fn clamp(&self, lb: &Self, ub: &Self) -> Self { + *self.min(ub).max(lb) + } +} + +impl Clampable for i64 {} diff --git a/src/equipment.rs b/src/equipment.rs new file mode 100644 index 0000000..790d58b --- /dev/null +++ b/src/equipment.rs @@ -0,0 +1,150 @@ +use std::fmt::Display; + +use crate::{ + dice::Dice, + stats::{Stat, StatBlock}, +}; +use rand::seq::IteratorRandom; +use rand_distr::{Distribution, Standard}; +use strum::IntoEnumIterator; +use strum_macros::{Display, EnumIter}; + +#[derive(Debug, Clone, Copy, EnumIter, Display)] +pub enum Armor { + Heavy, + Medium, + Light, + None, +} + +impl Armor { + pub fn armor(&self) -> i64 { + match self { + Armor::Heavy => 5, + Armor::Medium => 3, + Armor::Light => 1, + Armor::None => 0, + } + } + + pub fn stat_adjust(&self) -> StatBlock { + match self { + Armor::Heavy => StatBlock { + DEX: -1, + ..Default::default() + }, + Armor::Medium => Default::default(), + Armor::Light => Default::default(), + Armor::None => Default::default(), + } + } +} + +impl Distribution for Standard { + fn sample(&self, rng: &mut R) -> Armor { + Armor::iter().choose(rng).unwrap() + } +} + +#[derive(Debug, Clone, Copy, EnumIter)] +pub enum WeaponType { + SimpleWeapon, + BladedWeapon, + BluntWeapon, + PoleWeapon, + ThrownWeapon, + RangedWeapon, +} + +impl WeaponType { + pub fn create_weapon(&self, name: String) -> Weapon { + Weapon { + name, + weapon_type: *self, + damage_dice: self.damage_dice(), + weapon_stat: self.weapon_stat(), + stat_damage: self.stat_damage(), + } + } + pub fn damage_dice(&self) -> Dice { + match self { + WeaponType::SimpleWeapon => Dice::d4, + WeaponType::BladedWeapon => Dice::d6, + WeaponType::BluntWeapon => Dice::d10, + WeaponType::PoleWeapon => Dice::d4, + WeaponType::ThrownWeapon => Dice::d4, + WeaponType::RangedWeapon => Dice::d4, + } + } + + pub fn weapon_stat(&self) -> Stat { + match self { + WeaponType::SimpleWeapon => Stat::STR, + WeaponType::BladedWeapon => Stat::STR, + WeaponType::BluntWeapon => Stat::STR, + WeaponType::PoleWeapon => Stat::DEX, + WeaponType::ThrownWeapon => Stat::DEX, + WeaponType::RangedWeapon => Stat::DEX, + } + } + + pub fn stat_damage(&self) -> Option { + match self { + WeaponType::SimpleWeapon => None, + WeaponType::BladedWeapon => None, + WeaponType::BluntWeapon => Some(Stat::STR), + WeaponType::PoleWeapon => Some(Stat::DEX), + WeaponType::ThrownWeapon => Some(Stat::STR), + WeaponType::RangedWeapon => None, + } + } + + pub fn stat_adjust(&self) -> StatBlock { + match self { + WeaponType::SimpleWeapon => StatBlock::default(), + WeaponType::BladedWeapon => StatBlock::default(), + WeaponType::BluntWeapon => StatBlock { + DEX: -1, + ..Default::default() + }, + WeaponType::PoleWeapon => StatBlock::default(), + WeaponType::ThrownWeapon => StatBlock::default(), + WeaponType::RangedWeapon => StatBlock::default(), + } + } +} + +impl Display for WeaponType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + WeaponType::SimpleWeapon => write!(f, "Simple Weapon"), + WeaponType::BladedWeapon => write!(f, "Bladed Weapon"), + WeaponType::BluntWeapon => write!(f, "Blunt Weapon"), + WeaponType::PoleWeapon => write!(f, "Pole Weapon"), + WeaponType::ThrownWeapon => write!(f, "Thrown Weapon"), + WeaponType::RangedWeapon => write!(f, "Ranged Weapon"), + } + } +} + +#[derive(Debug, Clone)] +pub struct Weapon { + pub name: String, + pub weapon_type: WeaponType, + pub damage_dice: Dice, + pub weapon_stat: Stat, + pub stat_damage: Option, +} + +impl Display for Weapon { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.pad(&self.name) + } +} + +impl Distribution for Standard { + fn sample(&self, rng: &mut R) -> Weapon { + let wt = WeaponType::iter().choose(rng).unwrap(); + wt.create_weapon(wt.to_string()) + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..f315974 --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,10 @@ +#![allow(dead_code)] +pub mod character; +pub mod class; +pub mod combat; +pub mod dice; +pub mod equipment; +pub mod skills; +pub mod stats; + +pub mod random_character; \ No newline at end of file diff --git a/src/random_character.rs b/src/random_character.rs new file mode 100644 index 0000000..02e0450 --- /dev/null +++ b/src/random_character.rs @@ -0,0 +1,99 @@ +use rand::{ + seq::{IteratorRandom, SliceRandom}, + thread_rng, Rng, +}; +use rand_distr::{Distribution, Poisson, Standard}; +use strum::IntoEnumIterator; + +use crate::{ + character::{create_character, Character}, + stats::{Stat, StatBlock}, +}; + +pub fn random_stat_choice() -> StatBlock { + let mut rng = thread_rng(); + + let stat1: Stat = rng.gen(); + let stat2: Stat = rng.gen(); + + stat1 + stat2 +} + +pub fn random_stat_adjust() -> StatBlock { + let mut rng = thread_rng(); + + let total_adjusts: usize = (Poisson::new(2.0).unwrap().sample(&mut rng) as usize).min(7); + let n_adjust_stats = rng.gen_range(1..=4); + let adjust_stats = Stat::iter().choose_multiple(&mut rng, n_adjust_stats as usize); + + let stat_up: Vec = (&mut rng) + .sample_iter(Standard) + .filter(|stat| adjust_stats.contains(stat)) + .take(total_adjusts) + .collect(); + + let stat_down: Vec = rng + .sample_iter::(Standard) + .filter(|stat| !adjust_stats.contains(stat)) + .take(total_adjusts) + .collect(); + + stat_up.iter().fold(StatBlock::default(), |x, y| x + *y) + - stat_down.iter().fold(StatBlock::default(), |x, y| x + *y) +} + +const NAMES: [&str; 20] = [ + "Garry", "Brad", "Rob", "Adrian", "Jeff", "Larry", "Chad", "Fred", "Jack", "Jerry", "Mitch", + "Bob", "Billy", "Johnny", "Terry", "Berk", "Dave", "Steve", "Joe", "Bort", +]; + +pub fn random_character() -> Character { + let mut rng = thread_rng(); + + let class1 = rng.gen(); + + create_character( + NAMES.choose(&mut rng).unwrap().clone().into(), + class1, + (&mut rng) + .sample_iter(Standard) + .filter(|c| *c != class1) + .next() + .unwrap(), + random_stat_choice(), + random_stat_adjust(), + rng.gen(), + rng.gen(), + ) +} + +#[cfg(test)] +mod random_character_tests { + use super::*; + + #[test] + fn test_choice() { + for _i in 0..1000 { + assert_eq!(Vec::from(random_stat_choice()).iter().sum::(), 2); + } + } + + #[test] + fn test_adjust() { + for _i in 0..1000 { + let adjust = Vec::from(random_stat_adjust()); + println!("{:?}", adjust); + assert_eq!(adjust.iter().sum::(), 0); + } + } + + #[test] + fn character_stats() { + for _i in 0..1000 { + assert_eq!( + Vec::from(random_character().stat_block).iter().sum::(), + 16 + ); + } + } +} diff --git a/src/skills.rs b/src/skills.rs new file mode 100644 index 0000000..cef62d7 --- /dev/null +++ b/src/skills.rs @@ -0,0 +1,90 @@ +use std::ops; + +#[derive(Debug, Default, PartialEq, Eq, Clone, Copy)] +pub struct Proficiencies { + // Armor + pub light_armor: bool, + pub medium_armor: bool, + pub heavy_armor: bool, + // Weapon + pub simple_weapons: bool, + pub bladed_weapons: bool, + pub blunt_weapons: bool, + pub pole_weapons: bool, + pub thrown_weapons: bool, + pub ranged_weapons: bool, +} + +pub trait ToProficiencies { + fn proficiencies(&self) -> Proficiencies; +} + +impl ops::BitOr for Proficiencies { + type Output = Proficiencies; + + fn bitor(self, rhs: Self) -> Self::Output { + Proficiencies { + light_armor: self.light_armor || rhs.light_armor, + medium_armor: self.medium_armor || rhs.medium_armor, + heavy_armor: self.heavy_armor || rhs.heavy_armor, + simple_weapons: self.simple_weapons || rhs.simple_weapons, + bladed_weapons: self.bladed_weapons || rhs.bladed_weapons, + blunt_weapons: self.blunt_weapons || rhs.blunt_weapons, + pole_weapons: self.pole_weapons || rhs.pole_weapons, + thrown_weapons: self.thrown_weapons || rhs.thrown_weapons, + ranged_weapons: self.ranged_weapons || rhs.ranged_weapons, + } + } +} + +#[cfg(test)] +mod skills_tests { + use crate::{ + class::Class, + skills::{Proficiencies, ToProficiencies}, + }; + + #[test] + fn defaults() { + assert_eq!( + Proficiencies::default() | Proficiencies::default(), + Proficiencies::default() + ); + assert_eq!( + Class::Brawler.proficiencies() | Proficiencies::default(), + Class::Brawler.proficiencies() + ) + } + + #[test] + fn combine() { + assert_eq!( + Proficiencies { + light_armor: true, + ..Default::default() + } | Proficiencies { + light_armor: true, + ..Default::default() + }, + Proficiencies { + light_armor: true, + ..Default::default() + } + ); + + assert_eq!( + Proficiencies { + light_armor: true, + ..Default::default() + } | Proficiencies { + heavy_armor: true, + ..Default::default() + }, + Proficiencies { + light_armor: true, + heavy_armor: true, + ..Default::default() + } + ) + } +} diff --git a/src/stats.rs b/src/stats.rs new file mode 100644 index 0000000..e079aa0 --- /dev/null +++ b/src/stats.rs @@ -0,0 +1,168 @@ +use std::{ + convert, + ops::{self, Index}, +}; + +use rand::{distributions::Standard, prelude::Distribution, seq::IteratorRandom}; +use strum::IntoEnumIterator; +use strum_macros::EnumIter; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, EnumIter)] +pub enum Stat { + STR, + DEX, + CON, + STB, + + INT, + KND, + CMP, + CHA, +} + +impl ops::Add for Stat { + type Output = StatBlock; + + fn add(self, rhs: Stat) -> Self::Output { + StatBlock::from(self) + rhs + } +} + +impl Distribution for Standard { + fn sample(&self, rng: &mut R) -> Stat { + Stat::iter().choose(rng).unwrap() + } +} + +#[allow(non_snake_case)] +#[derive(Debug, PartialEq, Eq, Default, Clone, Copy)] +pub struct StatBlock { + // Physical + pub STR: i64, + pub DEX: i64, + pub CON: i64, + pub STB: i64, + // Mental + pub INT: i64, + pub KND: i64, + pub CMP: i64, + pub CHA: i64, +} + +impl StatBlock { + pub fn ones() -> StatBlock { + StatBlock::from((1, 1, 1, 1, 1, 1, 1, 1)) + } +} + +pub trait ToStatBlock { + fn stat_block(&self) -> StatBlock; +} + +impl ops::Add for StatBlock { + type Output = StatBlock; + + fn add(self, rhs: StatBlock) -> Self::Output { + StatBlock { + STR: self.STR + rhs.STR, + DEX: self.DEX + rhs.DEX, + CON: self.CON + rhs.CON, + STB: self.STB + rhs.STB, + INT: self.INT + rhs.INT, + KND: self.KND + rhs.KND, + CMP: self.CMP + rhs.CMP, + CHA: self.CHA + rhs.CHA, + } + } +} + +impl ops::Sub for StatBlock { + type Output = StatBlock; + + fn sub(self, rhs: StatBlock) -> Self::Output { + self + -1 * rhs + } +} + +impl ops::Mul for i64 { + type Output = StatBlock; + + fn mul(self, rhs: StatBlock) -> Self::Output { + StatBlock { + STR: self * rhs.STR, + DEX: self * rhs.DEX, + CON: self * rhs.CON, + STB: self * rhs.STB, + INT: self * rhs.INT, + KND: self * rhs.KND, + CMP: self * rhs.CMP, + CHA: self * rhs.CHA, + } + } +} + +impl ops::Add for StatBlock { + type Output = StatBlock; + + fn add(self, rhs: Stat) -> Self::Output { + self + StatBlock::from(rhs) + } +} + +impl convert::From<(i64, i64, i64, i64, i64, i64, i64, i64)> for StatBlock { + #[allow(non_snake_case)] + fn from( + (STR, DEX, CON, STB, INT, KND, CMP, CHA): (i64, i64, i64, i64, i64, i64, i64, i64), + ) -> Self { + StatBlock { + STR, + DEX, + CON, + STB, + INT, + KND, + CMP, + CHA, + } + } +} + +impl From for StatBlock { + fn from(value: Stat) -> Self { + match value { + Stat::STR => StatBlock::from((1, 0, 0, 0, 0, 0, 0, 0)), + Stat::DEX => StatBlock::from((0, 1, 0, 0, 0, 0, 0, 0)), + Stat::CON => StatBlock::from((0, 0, 1, 0, 0, 0, 0, 0)), + Stat::STB => StatBlock::from((0, 0, 0, 1, 0, 0, 0, 0)), + Stat::INT => StatBlock::from((0, 0, 0, 0, 1, 0, 0, 0)), + Stat::KND => StatBlock::from((0, 0, 0, 0, 0, 1, 0, 0)), + Stat::CMP => StatBlock::from((0, 0, 0, 0, 0, 0, 1, 0)), + Stat::CHA => StatBlock::from((0, 0, 0, 0, 0, 0, 0, 1)), + } + } +} + +impl Index for StatBlock { + type Output = i64; + + fn index(&self, index: Stat) -> &Self::Output { + match index { + Stat::STR => &self.STR, + Stat::DEX => &self.DEX, + Stat::CON => &self.CON, + Stat::STB => &self.STB, + Stat::INT => &self.INT, + Stat::KND => &self.KND, + Stat::CMP => &self.CMP, + Stat::CHA => &self.CHA, + } + } +} + +impl convert::From for Vec { + fn from(value: StatBlock) -> Self { + vec![ + value.STR, value.DEX, value.CON, value.STB, value.INT, value.KND, value.CMP, value.CHA, + ] + } +}