in the beginning there was code

main
HeNine 2 years ago
commit b7c5e7a37a

16
.gitignore vendored

@ -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

@ -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"

@ -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<Item = &'a Character>,
{
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<Character> = iter::repeat_with(random_character).take(n).collect();
print_chars(characters.iter().take(keep).collect::<Vec<&Character>>());
// 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{:#<c$}{:.<r$}",
"",
"",
c = (((g as f32) / (gen as f32)) * 100.0) as usize,
r = 100 - (((g as f32) / (gen as f32)) * 100.0) as usize
);
io::stdout().flush().unwrap();
}
}
println!("");
print_chars(characters.iter().take(keep).collect::<Vec<&Character>>());
}

@ -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);
}

@ -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);
}

@ -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)
}

@ -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::<i64>() / 2
// );
// }
for _i in 0..3 {
let mut c = random_character();
c.init_dice_pool(EncounterType::Physical);
println!("{}", c);
}
}

@ -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::<i64>() == 2i64)
{
panic!("Invalid stat choice: {:?}", stat_choice)
}
if Vec::from(stat_adjust).iter().sum::<i64>() != 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()
)
}
}

@ -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<Class> for Standard {
fn sample<R: rand::Rng + ?Sized>(&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())
}
}
}
}

@ -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<i64> {
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)
}

@ -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<Self::Item> {
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<Dice> 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<i64> = 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 {}

@ -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<Armor> for Standard {
fn sample<R: rand::Rng + ?Sized>(&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<Stat> {
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<Stat>,
}
impl Display for Weapon {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.pad(&self.name)
}
}
impl Distribution<Weapon> for Standard {
fn sample<R: rand::Rng + ?Sized>(&self, rng: &mut R) -> Weapon {
let wt = WeaponType::iter().choose(rng).unwrap();
wt.create_weapon(wt.to_string())
}
}

@ -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;

@ -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<Stat> = (&mut rng)
.sample_iter(Standard)
.filter(|stat| adjust_stats.contains(stat))
.take(total_adjusts)
.collect();
let stat_down: Vec<Stat> = rng
.sample_iter::<Stat, Standard>(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::<i64>(), 2);
}
}
#[test]
fn test_adjust() {
for _i in 0..1000 {
let adjust = Vec::from(random_stat_adjust());
println!("{:?}", adjust);
assert_eq!(adjust.iter().sum::<i64>(), 0);
}
}
#[test]
fn character_stats() {
for _i in 0..1000 {
assert_eq!(
Vec::from(random_character().stat_block).iter().sum::<i64>(),
16
);
}
}
}

@ -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()
}
)
}
}

@ -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<Stat> for Stat {
type Output = StatBlock;
fn add(self, rhs: Stat) -> Self::Output {
StatBlock::from(self) + rhs
}
}
impl Distribution<Stat> for Standard {
fn sample<R: rand::Rng + ?Sized>(&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<StatBlock> 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<StatBlock> for StatBlock {
type Output = StatBlock;
fn sub(self, rhs: StatBlock) -> Self::Output {
self + -1 * rhs
}
}
impl ops::Mul<StatBlock> 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<Stat> 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<Stat> 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<Stat> 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<StatBlock> for Vec<i64> {
fn from(value: StatBlock) -> Self {
vec![
value.STR, value.DEX, value.CON, value.STB, value.INT, value.KND, value.CMP, value.CHA,
]
}
}
Loading…
Cancel
Save