From 7ae33fba15b2adf9f903869b3c896a7490427b04 Mon Sep 17 00:00:00 2001 From: Arne Dußin Date: Mon, 26 Apr 2021 10:06:58 +0200 Subject: Initial commit --- src/character/mod.rs | 207 ++++++++++++++++++++++++++++++++++++++++++++ src/character/stats.rs | 117 +++++++++++++++++++++++++ src/dice.rs | 227 +++++++++++++++++++++++++++++++++++++++++++++++++ src/inventory.rs | 41 +++++++++ src/main.rs | 5 ++ 5 files changed, 597 insertions(+) create mode 100644 src/character/mod.rs create mode 100644 src/character/stats.rs create mode 100644 src/dice.rs create mode 100644 src/inventory.rs create mode 100644 src/main.rs (limited to 'src') diff --git a/src/character/mod.rs b/src/character/mod.rs new file mode 100644 index 0000000..fc67e42 --- /dev/null +++ b/src/character/mod.rs @@ -0,0 +1,207 @@ +pub mod stats; +use std::io; + +use io::BufRead; +pub use stats::*; + +use crate::inventory::Inventory; + +pub const MAX_LEVEL: u8 = 10; + +fn swap_stats_according_to_string(stats: &mut Stats, string: String) +{ + let string = string.trim(); + let parts: Vec<&str> = string.split_whitespace().collect(); + + if parts.len() == 2 && stats.swap(parts[0], parts[1]) { + println!("{} and {} values have been switched.", parts[0], parts[1]); + } + else { + println!("Not switching any stats."); + } +} + +pub fn cli_create_character() -> Character +{ + println!("Starting character creation."); + println!("Rolling initial stats..."); + + let mut stats = Stats::roll_initial(); + println!("Initial stats: {}", stats); + + println!( + "Would you like to swap two stats' values? If so, write the names of them seperated by a \ + whitespace and press enter, if not just press enter." + ); + + let mut buf = String::new(); + + { + let stdin = io::stdin(); + let mut lock = stdin.lock(); + lock.read_line(&mut buf) + .expect("Unable to read from console"); + } + + swap_stats_according_to_string(&mut stats, buf); + + unimplemented!() +} + +#[derive(Clone, Debug)] +pub struct Character +{ + health: u8, + max_health: u8, + stats: Stats, + /// Free text describing the special trait of the character + special_traits: Vec, + /// Total value of experience. The level can be calculated through it + experience: u32, + wealth: u32, + inventory: Inventory, + goals: Vec, +} + +impl Character +{ + pub fn new( + health: u8, + max_health: u8, + stats: Stats, + special_traits: Vec, + experience: u32, + wealth: u32, + inventory: Inventory, + goals: Vec, + ) -> Self + { + Self { + health, + max_health, + stats, + special_traits, + experience, + wealth, + inventory, + goals, + } + } + + pub fn stats(&self) -> &Stats { &self.stats } + pub fn stats_mut(&mut self) -> &mut Stats { &mut self.stats } + + pub fn special_traits(&self) -> &Vec { &self.special_traits } + + pub fn experience(&self) -> u32 { self.experience } + + pub fn wealth(&self) -> u32 { self.wealth } + + pub fn level(&self) -> u8 + { + /* Start iteration at first level */ + let mut level_counter = 1; + let mut xp_remaining = self.experience; + let mut xp_required_for_levelup = 200; + + while level_counter < MAX_LEVEL && xp_remaining >= xp_required_for_levelup { + level_counter += 1; + xp_remaining -= xp_required_for_levelup; + xp_required_for_levelup += 100; + } + + level_counter + } +} + +#[cfg(test)] +mod tests +{ + use super::*; + + const DEFAULT_STATS: Stats = Stats::from_values([10; NUM_STATS]); + + #[test] + fn create_character() + { + let character = Character::new( + 10, + 10, + DEFAULT_STATS.clone(), + Vec::new(), + 1, + 2, + Inventory::new(), + Vec::new(), + ); + + assert_eq!(character.stats(), &DEFAULT_STATS); + assert_eq!(character.special_traits(), &Vec::::new()); + assert_eq!(character.experience(), 1); + assert_eq!(character.wealth, 2); + } + + #[test] + fn min_level_calculation() + { + let character = Character::new( + 10, + 10, + DEFAULT_STATS.clone(), + Vec::new(), + 199, + 0, + Inventory::new(), + Vec::new(), + ); + + assert_eq!(character.level(), 1); + } + + #[test] + fn mid_level_calculation() + { + let character = Character::new( + 10, + 10, + DEFAULT_STATS.clone(), + Vec::new(), + 900, + 0, + Inventory::new(), + Vec::new(), + ); + + assert_eq!(character.level(), 4); + } + + #[test] + fn max_level_calculation() + { + let character = Character::new( + 10, + 10, + DEFAULT_STATS.clone(), + Vec::new(), + 5400, + 0, + Inventory::new(), + Vec::new(), + ); + + assert_eq!(character.level(), 10); + + let character = Character::new( + 10, + 10, + DEFAULT_STATS.clone(), + Vec::new(), + 6500, + 0, + Inventory::new(), + Vec::new(), + ); + + assert_eq!(character.level(), 10); + } +} diff --git a/src/character/stats.rs b/src/character/stats.rs new file mode 100644 index 0000000..d405eb7 --- /dev/null +++ b/src/character/stats.rs @@ -0,0 +1,117 @@ +use std::fmt::{self, Display, Formatter}; + +use rand::prelude::ThreadRng; +use rand::Rng; + +pub const NUM_STATS: usize = 7; +pub const STATS: [&str; NUM_STATS] = [ + "charisma", + "constitution", + "dexterity", + "health", + "intelligence", + "strength", + "wisdom", +]; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Stats +{ + values: [u8; NUM_STATS], +} + +impl Stats +{ + pub const fn from_values(values: [u8; NUM_STATS]) -> Self { Self { values } } + + fn roll_initial_value(rng: &mut ThreadRng) -> u8 + { + let mut v = 0; + for _ in 0..3 { + v += rng.gen_range(1..=6); + } + + v + } + + pub fn roll_initial() -> Self + { + let mut rng = rand::thread_rng(); + let mut values = [0; NUM_STATS]; + for v in &mut values { + *v = Self::roll_initial_value(&mut rng); + } + + Self { values } + } + + pub fn swap(&mut self, val1: &str, val2: &str) -> bool + { + unimplemented!(); + } + + pub fn get(&self, attribute: &str) -> Option + { + match STATS.binary_search(&attribute) { + Ok(pos) => Some(self.values[pos]), + Err(_) => None, + } + } + + pub fn get_mut(&mut self, attribute: &str) -> Option<&mut u8> + { + match STATS.binary_search(&attribute) { + Ok(pos) => Some(self.values.get_mut(pos).unwrap()), + Err(_) => None, + } + } +} + +impl Display for Stats +{ + fn fmt(&self, fmt: &mut Formatter) -> Result<(), fmt::Error> + { + writeln!(fmt, "[")?; + for i in 0..NUM_STATS { + writeln!(fmt, " {} = {}", STATS[i], self.values[i])?; + } + writeln!(fmt, "]")?; + + Ok(()) + } +} + +#[cfg(test)] +mod tests +{ + use super::*; + + fn test_stats() -> Stats { Stats::from_values([1, 2, 3, 4, 5, 6, 7]) } + + #[test] + fn swap_same_argument() + { + let mut stats = test_stats(); + assert_eq!(stats.get("intelligence"), Some(5)); + assert!(stats.swap("intelligence", "intelligence")); + assert_eq!(stats.get("intelligence"), Some(5)); + + assert_eq!(stats.get("wisdom"), Some(7)); + assert!(stats.swap("wisdom", "wisdom")); + assert_eq!(stats.get("wisdom"), Some(7)); + } + + #[test] + fn swap_invalid_argument() + { + let mut stats = test_stats(); + assert!(!stats.swap("", "intelligence")); + assert_eq!(stats, test_stats()); + assert!(!stats.swap("intelligence", "")); + assert_eq!(stats, test_stats()); + assert!(!stats.swap("aoeu", "aoeu")); + assert_eq!(stats, test_stats()); + assert!(!stats.swap("", "")); + assert_eq!(stats, test_stats()); + } +} diff --git a/src/dice.rs b/src/dice.rs new file mode 100644 index 0000000..c89c4c9 --- /dev/null +++ b/src/dice.rs @@ -0,0 +1,227 @@ +use std::ops::Add; + +use rand::rngs::ThreadRng; +use rand::Rng; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub struct RiskDie(u8); +pub const NUM_DIE_LEVELS: usize = 6; +pub const RISK_DIE_VALUES: [u8; NUM_DIE_LEVELS] = [4, 6, 8, 10, 12, 20]; +pub const DELTA_4: RiskDie = RiskDie(0); +pub const DELTA_6: RiskDie = RiskDie(1); +pub const DELTA_8: RiskDie = RiskDie(2); +pub const DELTA_10: RiskDie = RiskDie(3); +pub const DELTA_12: RiskDie = RiskDie(4); +pub const DELTA_20: RiskDie = RiskDie(5); + +#[derive(Debug, PartialEq, Eq)] +pub enum Error +{ + InvalidLevel, + InvalidValue, +} + +impl RiskDie +{ + pub fn from_level(level: u8) -> Result + { + if (level as usize) < RISK_DIE_VALUES.len() { + Ok(Self(level)) + } + else { + Err(Error::InvalidLevel) + } + } + + pub fn from_value(value: u8) -> Result + { + for (i, val) in RISK_DIE_VALUES.iter().enumerate() { + if *val == value { + return Ok(Self(i as u8)); + } + } + + Err(Error::InvalidValue) + } + + pub fn level(&self) -> u8 { self.0 } + + pub fn value(&self) -> u8 { RISK_DIE_VALUES[self.0 as usize] } + + /// Roll the die. If the die shows 3 or less, it returns the next lower die. + /// If it is more, it returns itself. If it was already the lowest die value + /// and it would be stepped down it returns `None`. + pub fn roll(self) -> Option + { + let mut rng = rand::thread_rng(); + self.roll_with_rng(&mut rng) + } + + pub fn roll_with_rng(self, rng: &mut ThreadRng) -> Option + { + let rolled = rng.gen_range(1..=self.value()); + if rolled > 3 { + Some(self) + } + else { + self.level_down() + } + } + + pub fn can_be_lowered(&self) -> bool { self.0 > 0 } + + pub fn level_down(mut self) -> Option + { + if self.can_be_lowered() { + self.0 -= 1; + Some(self) + } + else { + /* Resource depleted */ + None + } + } + + pub fn can_be_upped(&self) -> bool { self.0 < NUM_DIE_LEVELS as u8 - 1 } + + pub fn level_up(mut self) -> Self + { + if self.can_be_upped() { + self.0 += 1; + } + + self + } +} + +#[rustfmt::skip] +const DELTA_ADDITION_TABLE: [[u8; NUM_DIE_LEVELS]; NUM_DIE_LEVELS] = [ + /* ------------------- with d4 d6 d8 d10 d12 d20 */ + /* delta 4 combinations */ [1, 2, 3, 4, 4, 5], + /* delta 6 combinations */ [2, 2, 3, 4, 5, 5], + /* delta 8 combinations */ [3, 3, 4, 4, 5, 5], + /* delta 10 combinations */ [4, 4, 4, 5, 5, 5], + /* delta 12 combinations */ [4, 5, 5, 5, 5, 5], + /* delta 20 combinations */ [5, 5, 5, 5, 5, 5], +]; +impl Add for RiskDie +{ + type Output = Self; + + fn add(self, other: Self) -> Self::Output + { + RiskDie(DELTA_ADDITION_TABLE[self.0 as usize][other.0 as usize]) + } +} + +#[cfg(test)] +mod tests +{ + use super::*; + + #[test] + fn create_die_from_level() + { + let die = RiskDie::from_level(0).expect("Unable to create die for correct level"); + assert_eq!(die.value(), 4); + let die = RiskDie::from_level(1).expect("Unable to create die for correct level"); + assert_eq!(die.value(), 6); + let die = RiskDie::from_level(2).expect("Unable to create die for correct level"); + assert_eq!(die.value(), 8); + let die = RiskDie::from_level(3).expect("Unable to create die for correct level"); + assert_eq!(die.value(), 10); + let die = RiskDie::from_level(4).expect("Unable to create die for correct level"); + assert_eq!(die.value(), 12); + let die = RiskDie::from_level(5).expect("Unable to create die for correct level"); + assert_eq!(die.value(), 20); + } + + #[test] + fn create_die_from_invalid_level() + { + assert_eq!( + RiskDie::from_level(6).expect_err("Should not be able to create die from level"), + Error::InvalidLevel + ); + } + + #[test] + fn create_from_value() + { + let die = RiskDie::from_value(4).expect("Unable to create die from correct value"); + assert_eq!(die.level(), 0); + let die = RiskDie::from_value(6).expect("Unable to create die from correct value"); + assert_eq!(die.level(), 1); + let die = RiskDie::from_value(8).expect("Unable to create die from correct value"); + assert_eq!(die.level(), 2); + let die = RiskDie::from_value(10).expect("Unable to create die from correct value"); + assert_eq!(die.level(), 3); + let die = RiskDie::from_value(12).expect("Unable to create die from correct value"); + assert_eq!(die.level(), 4); + let die = RiskDie::from_value(20).expect("Unable to create die from correct value"); + assert_eq!(die.level(), 5); + } + + #[test] + fn level_down() + { + assert_eq!(DELTA_4.level_down(), None); + assert_eq!(DELTA_6.level_down(), Some(RiskDie(0))); + assert_eq!(DELTA_8.level_down(), Some(RiskDie(1))); + assert_eq!(DELTA_10.level_down(), Some(RiskDie(2))); + assert_eq!(DELTA_12.level_down(), Some(RiskDie(3))); + assert_eq!(DELTA_20.level_down(), Some(RiskDie(4))); + } + + #[test] + fn level_up() + { + assert_eq!(DELTA_4.level_up(), RiskDie(1)); + assert_eq!(DELTA_6.level_up(), RiskDie(2)); + assert_eq!(DELTA_8.level_up(), RiskDie(3)); + assert_eq!(DELTA_10.level_up(), RiskDie(4)); + assert_eq!(DELTA_12.level_up(), RiskDie(5)); + assert_eq!(DELTA_20.level_up(), RiskDie(5)); + } + + #[test] + fn add_risk_dice() + { + assert_eq!(DELTA_4 + DELTA_4, DELTA_6); + assert_eq!(DELTA_4 + DELTA_6, DELTA_8); + assert_eq!(DELTA_4 + DELTA_8, DELTA_10); + assert_eq!(DELTA_4 + DELTA_10, DELTA_12); + assert_eq!(DELTA_4 + DELTA_12, DELTA_12); + assert_eq!(DELTA_4 + DELTA_20, DELTA_20); + assert_eq!(DELTA_6 + DELTA_4, DELTA_8); + assert_eq!(DELTA_6 + DELTA_6, DELTA_8); + assert_eq!(DELTA_6 + DELTA_8, DELTA_10); + assert_eq!(DELTA_6 + DELTA_10, DELTA_12); + assert_eq!(DELTA_6 + DELTA_12, DELTA_20); + assert_eq!(DELTA_6 + DELTA_20, DELTA_20); + assert_eq!(DELTA_8 + DELTA_4, DELTA_10); + assert_eq!(DELTA_8 + DELTA_6, DELTA_10); + assert_eq!(DELTA_8 + DELTA_8, DELTA_12); + assert_eq!(DELTA_8 + DELTA_10, DELTA_12); + assert_eq!(DELTA_8 + DELTA_12, DELTA_20); + assert_eq!(DELTA_8 + DELTA_20, DELTA_20); + assert_eq!(DELTA_10 + DELTA_4, DELTA_12); + assert_eq!(DELTA_10 + DELTA_6, DELTA_12); + assert_eq!(DELTA_10 + DELTA_8, DELTA_12); + assert_eq!(DELTA_10 + DELTA_10, DELTA_20); + assert_eq!(DELTA_10 + DELTA_12, DELTA_20); + assert_eq!(DELTA_10 + DELTA_20, DELTA_20); + assert_eq!(DELTA_12 + DELTA_4, DELTA_12); + assert_eq!(DELTA_12 + DELTA_6, DELTA_20); + assert_eq!(DELTA_12 + DELTA_8, DELTA_20); + assert_eq!(DELTA_12 + DELTA_10, DELTA_20); + assert_eq!(DELTA_12 + DELTA_12, DELTA_20); + assert_eq!(DELTA_12 + DELTA_20, DELTA_20); + assert_eq!(DELTA_20 + DELTA_4, DELTA_20); + assert_eq!(DELTA_20 + DELTA_6, DELTA_20); + assert_eq!(DELTA_20 + DELTA_8, DELTA_20); + assert_eq!(DELTA_20 + DELTA_10, DELTA_20); + assert_eq!(DELTA_20 + DELTA_12, DELTA_20); + assert_eq!(DELTA_20 + DELTA_20, DELTA_20); + } +} diff --git a/src/inventory.rs b/src/inventory.rs new file mode 100644 index 0000000..0e44313 --- /dev/null +++ b/src/inventory.rs @@ -0,0 +1,41 @@ +use crate::dice::RiskDie; + +#[derive(Clone, Debug)] +pub enum Item +{ + Countable(String, u8), + Approximate(String, RiskDie), +} + +#[derive(Clone, Debug)] +pub struct Inventory +{ + items: Vec, +} + +impl Inventory +{ + pub fn new() -> Self { Self { items: Vec::new() } } + + pub fn size(&self) -> usize { self.items.len() } + + pub fn add_item(&mut self, item: Item) { self.items.push(item); } +} + +impl Default for Inventory +{ + fn default() -> Self { Self::new() } +} + +#[cfg(test)] +mod tests +{ + use super::*; + + #[test] + pub fn new_empty_inventory() + { + let inventory = Inventory::new(); + assert_eq!(inventory.size(), 0); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..8534936 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,5 @@ +pub mod character; +pub mod dice; +pub mod inventory; + +fn main() {} -- cgit v1.2.3-70-g09d2