diff --git a/.gitignore b/.gitignore index 6936990..d889fbf 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ /target **/*.rs.bk Cargo.lock +.idea diff --git a/Cargo.toml b/Cargo.toml index bd426d8..2245be1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,3 +7,4 @@ version = "0.1.0" authors = ["Jan Christian Grünhage "] [dependencies] +rand = "0.5.4" diff --git a/README.md b/README.md new file mode 100644 index 0000000..7213872 --- /dev/null +++ b/README.md @@ -0,0 +1,49 @@ +### dsa-rs + +This is a ***work-in-progress*** library for "Das Schwarze Auge", a german pen and paper RPG. +It targets the fifth edition and intentionally does not come with any content, +to avoid infringement of the copyright of Ulisses Medien & Spiel Distribution GmbH. +Instead, it does come with a schema of how the content needs to be formatted. +For usage, please create the content files yourself, using their rule books as sources. + +## Current features: + - Characteristics + - Skills + - Trials for each of the above. + +The architecture of the code above isn't very good though, +so for now I'd just wait (or participate in planning a better architecture). + +## Planned features: + - [ ] Heroes + - [ ] Create and change heroes, based on the rules + - [ ] Skills + - [ ] trials + - [ ] and probabilities + - [ ] Characteristics + - [ ] trials + - [ ] Derived Values + - [ ] management of current values for things like LE/AE/KE + - [ ] Conditions + - [ ] Wounds + - [ ] Special abilities + - [ ] Advantages/Disadvantages + - [ ] Species + - [ ] Culture + - [ ] Profession + - [ ] Inventory, including money + - [ ] Equipment + - [ ] Notes about adventures + - [ ] Relationships + - [ ] AP/SE + - [ ] Combat + - [ ] Rolling back changes (each change should be saved as a transaction) + - [ ] Insert your proposal here! + + +## Out of scope (for this library): + + - Any kind of UI/CLI for the things above + - Content: This library should only implement concepts, like skills, + but not contain actual instances of those concepts. The content is something the user needs + to obtain themselves. \ No newline at end of file diff --git a/src/characteristic.rs b/src/characteristic.rs new file mode 100644 index 0000000..afcb2c5 --- /dev/null +++ b/src/characteristic.rs @@ -0,0 +1,17 @@ +use super::dice::DiceThrow; + +/// A characteristic is a basic trait of a hero. +/// There are 8 of those, and they are usually abbreviated with two capital letters. +pub struct Characteristic { + /// This is the identifier, it's a static str containing the two capital letter abbreviation. + pub identifier: &'static str, + /// This is the value indicating how good the hero is in regard of this characteristic. + pub value: i8, +} + +impl Characteristic { + /// Test the hero's ability to do something regarding this characteristic. + pub fn trial(&self, dice_throw: DiceThrow, modificator: i8) -> i8 { + self.value + modificator - (dice_throw.throw as i8) + } +} diff --git a/src/dice.rs b/src/dice.rs new file mode 100644 index 0000000..935f4f9 --- /dev/null +++ b/src/dice.rs @@ -0,0 +1,60 @@ +use rand::distributions::{Distribution, Uniform}; +use rand::{rngs::ThreadRng, thread_rng}; + +// TODO: We'd want move to constant generics here and for DiceThrows, +// but that requires new lang features https://github.com/rust-lang/rust/issues/44580 +#[derive(Copy, Clone)] +pub struct Dice { + pub sides: u8, +} + +impl Dice { + pub fn new(sides: u8) -> Dice { + Dice { sides } + } + pub fn throw_multiple(&self, amount: u8) -> Vec { + let mut results = Vec::new(); + for _ in 0..amount { + results.push(self.throw()); + } + results + } + pub fn throw(&self) -> DiceThrow { + self.throw_with_result(Uniform::from(1..self.sides).sample::(&mut thread_rng())) + } + + pub fn throw_with_result(&self, result: u8) -> DiceThrow { + DiceThrow { + sides: self.sides, + throw: result, + } + } +} + +#[derive(Copy, Clone)] +pub struct DiceThrow { + pub sides: u8, + pub throw: u8, +} + +#[cfg(test)] +mod tests { + use super::Dice; + #[test] + fn create_dice() { + assert_eq!(Dice::new(20).sides, 20) + } + + #[test] + fn throw_dice() { + let throw = Dice::new(20).throw(); + assert!(throw.throw < 21); + assert!(throw.throw > 0); + } + + #[test] + fn throw_dice_with_result() { + let throw = Dice::new(20).throw_with_result(15); + assert_eq!(throw.throw, 15); + } +} diff --git a/src/hero.rs b/src/hero.rs new file mode 100644 index 0000000..5b693f1 --- /dev/null +++ b/src/hero.rs @@ -0,0 +1,43 @@ +use super::{characteristic::Characteristic, dice::DiceThrow, skill::Skill}; +use std::collections::HashMap; + +pub struct Hero<'a> { + pub characteristics: HashMap<&'static str, Characteristic>, + pub skills: HashMap<&'static str, Skill<'a>>, +} + +impl Hero { + pub fn new() -> Hero { + Hero { + characteristics: HashMap::new(), + skills: HashMap::new(), + } + } + + pub fn add_characteristic(&mut self, identifier: &'static str, value: i8) { + match self.characteristics + .insert(identifier, Characteristic { identifier, value }) + { + _ => {} //TODO: handle whatever this returns + }; + } + + pub fn add_skill( + &mut self, + identifier: &'static str, + value: i8, + characteristics: [&'static str; 3], + ) { + match self.skills.insert( + identifier, + Skill { + identifier, + value, + characteristics, + hero: self, + }, + ) { + _ => {} //TODO: handle whatever this returns + }; + } +} diff --git a/src/lib.rs b/src/lib.rs index 31e1bb2..047b69f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,5 @@ -#[cfg(test)] -mod tests { - #[test] - fn it_works() { - assert_eq!(2 + 2, 4); - } -} +extern crate rand; +pub mod characteristic; +pub mod dice; +pub mod hero; +pub mod skill; diff --git a/src/skill.rs b/src/skill.rs new file mode 100644 index 0000000..02dca4e --- /dev/null +++ b/src/skill.rs @@ -0,0 +1,296 @@ +use super::{characteristic::Characteristic, dice::DiceThrow, hero::Hero}; + +/// A skill identifier with a value how good the hero is in it. +pub struct Skill { + /// The identifier takes the form of `/` + pub identifier: &'static str, + pub value: i8, + pub characteristics: [&'static str; 3], + pub hero: Hero, +} + +impl Skill { + pub fn new( + identifier: &'static str, + value: i8, + characteristics: [&'static str; 3], + hero: Hero, + ) -> Skill { + Skill { + identifier, + value, + characteristics, + hero, + } + } + + pub fn trial( + &self, + dice_throws: [DiceThrow; 3], + modifier: i8, + ) -> ([DiceThrow; 3], SkillTrialResult) { + let mut ones = 0; + let mut twenties = 0; + let mut per_trait_mod = 0; + let mut remainder = self.value + modifier; + if remainder < 0 { + per_trait_mod = remainder; + remainder = 0; + } + for i in 0..3 { + let trait_trial_result = self.hero + .characteristics + .get(self.characteristics[i]) + .unwrap() + .trial(dice_throws[i], per_trait_mod); + if trait_trial_result < 0 { + remainder += trait_trial_result; + } + match dice_throws[i].throw { + 20 => twenties += 1, + 1 => ones += 1, + _ => {} + } + } + { + use self::SkillTrialResult::*; + ( + dice_throws, + match (ones, twenties, remainder) { + (_, 3, _) => TripleTwenty, + (_, 2, _) => DoubleTwenty, + (3, _, _) => TripleOne, + (2, _, _) => DoubleOne, + (_, _, n) if n < 0 => Failure, + (_, _, n) if n >= 0 && n <= 3 => Success(1), + (_, _, n) if n >= 4 && n <= 6 => Success(2), + (_, _, n) if n >= 7 && n <= 9 => Success(3), + (_, _, n) if n >= 10 && n <= 12 => Success(4), + (_, _, n) if n >= 13 && n <= 15 => Success(5), + (_, _, n) if n >= 16 => Success(6), + (_, _, _) => Failure, + // TODO: exhaustive integer matching, this is unreachable, removal blocked by + // https://github.com/rust-lang/rust/pull/50912 being in stable + }, + ) + } + } +} + +pub enum SkillTrialResult { + TripleTwenty, + DoubleTwenty, + Failure, + Success(u8), + DoubleOne, + TripleOne, +} + +#[cfg(test)] +mod tests { + use super::super::{ + characteristic::Characteristic, dice::Dice, hero::Hero, skill::{Skill, SkillTrialResult}, + }; + + #[test] + fn create_skill() { + let skill = skill(); + } + + fn skill() -> Skill { + let mut hero = Hero::new(); + hero.add_characteristic("AA", 12); + hero.add_characteristic("BB", 15); + hero.add_characteristic("CC", 09); + Skill::new("skill", 6, ["AA", "BB", "CC"], hero) + } + + #[test] + fn triple_twenty() { + let skill = skill(); + let d20 = Dice::new(20); + let throws = [ + d20.throw_with_result(20), + d20.throw_with_result(20), + d20.throw_with_result(20), + ]; + match skill.trial(throws, 0) { + (_, SkillTrialResult::TripleTwenty) => {} + (_, _) => panic!("Putting in three twenties doesn't return triple twenty"), + } + } + + #[test] + fn double_twenty() { + let skill = skill(); + let d20 = Dice::new(20); + let throws = [ + d20.throw_with_result(20), + d20.throw_with_result(19), + d20.throw_with_result(20), + ]; + match skill.trial(throws, 0) { + (_, SkillTrialResult::DoubleTwenty) => {} + (_, _) => panic!("Putting in two twenties doesn't return double twenty"), + } + } + + #[test] + fn triple_one() { + let skill = skill(); + let d20 = Dice::new(20); + let throws = [ + d20.throw_with_result(1), + d20.throw_with_result(1), + d20.throw_with_result(1), + ]; + match skill.trial(throws, 0) { + (_, SkillTrialResult::TripleOne) => {} + (_, _) => panic!("Putting in three ones doesn't return triple one"), + } + } + + #[test] + fn double_one() { + let skill = skill(); + let d20 = Dice::new(20); + let throws = [ + d20.throw_with_result(1), + d20.throw_with_result(1), + d20.throw_with_result(20), + ]; + match skill.trial(throws, 0) { + (_, SkillTrialResult::DoubleOne) => {} + (_, _) => panic!("Putting in two ones doesn't return double one"), + } + } + + #[test] + fn failure() { + let skill = skill(); + let d20 = Dice::new(20); + let throws = [ + d20.throw_with_result(19), + d20.throw_with_result(19), + d20.throw_with_result(19), + ]; + match skill.trial(throws, 0) { + (_, SkillTrialResult::Failure) => {} + (_, _) => panic!("Failing doesn't fail"), + } + } + + #[test] + fn success_qs1() { + let skill = skill(); + let d20 = Dice::new(20); + let throws = [ + d20.throw_with_result(2), + d20.throw_with_result(2), + d20.throw_with_result(2), + ]; + match skill.trial(throws, -4) { + (_, SkillTrialResult::Success(1)) => {} + (_, _) => panic!("QS 1 is failing"), + } + } + + #[test] + fn success_qs2() { + let skill = skill(); + let d20 = Dice::new(20); + let throws = [ + d20.throw_with_result(2), + d20.throw_with_result(2), + d20.throw_with_result(2), + ]; + match skill.trial(throws, -1) { + (_, SkillTrialResult::Success(2)) => {} + (_, _) => panic!("QS 1 is failing"), + } + } + + #[test] + fn success_qs3() { + let skill = skill(); + let d20 = Dice::new(20); + let throws = [ + d20.throw_with_result(2), + d20.throw_with_result(2), + d20.throw_with_result(2), + ]; + match skill.trial(throws, 2) { + (_, SkillTrialResult::Success(3)) => {} + (_, _) => panic!("QS 1 is failing"), + } + } + + #[test] + fn success_qs4() { + let skill = skill(); + let d20 = Dice::new(20); + let throws = [ + d20.throw_with_result(2), + d20.throw_with_result(2), + d20.throw_with_result(2), + ]; + match skill.trial(throws, 5) { + (_, SkillTrialResult::Success(4)) => {} + (_, _) => panic!("QS 1 is failing"), + } + } + + #[test] + fn success_qs5() { + let skill = skill(); + let d20 = Dice::new(20); + let throws = [ + d20.throw_with_result(2), + d20.throw_with_result(2), + d20.throw_with_result(2), + ]; + match skill.trial(throws, 8) { + (_, SkillTrialResult::Success(5)) => {} + (_, _) => panic!("QS 1 is failing"), + } + } + + #[test] + fn success_qs6() { + let skill = skill(); + let d20 = Dice::new(20); + let throws = [ + d20.throw_with_result(2), + d20.throw_with_result(2), + d20.throw_with_result(2), + ]; + match skill.trial(throws, 11) { + (_, SkillTrialResult::Success(6)) => {} + (_, _) => panic!("QS 1 is failing"), + } + } + + #[test] + fn success_with_19_qs6() { + let skill = skill(); + let d20 = Dice::new(20); + let throws = [ + d20.throw_with_result(19), + d20.throw_with_result(2), + d20.throw_with_result(2), + ]; + match skill.trial(throws, 20) { + (_, SkillTrialResult::Success(6)) => {} + (_, _) => panic!("QS 1 is failing"), + } + } +} + + +struct A { + bs: Vec, +} + +struct B { + a: A +} \ No newline at end of file