diff --git a/04/adventure_game/.run/Run 04_adventure_game.run.xml b/04/adventure_game/.run/Run 04_adventure_game.run.xml new file mode 100644 index 0000000..0aa8061 --- /dev/null +++ b/04/adventure_game/.run/Run 04_adventure_game.run.xml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/04/adventure_game/Cargo.lock b/04/adventure_game/Cargo.lock new file mode 100644 index 0000000..b38a59c --- /dev/null +++ b/04/adventure_game/Cargo.lock @@ -0,0 +1,164 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +[[package]] +name = "adventure_game" +version = "0.1.0" +dependencies = [ + "rand", + "serde", + "serde_json", +] + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "getrandom" +version = "0.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "itoa" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" + +[[package]] +name = "libc" +version = "0.2.76" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "755456fae044e6fa1ebbbd1b3e902ae19e73097ed4ed87bb79934a867c007bc3" + +[[package]] +name = "ppv-lite86" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c36fa947111f5c62a733b652544dd0016a43ce89619538a8ef92724a6f501a20" + +[[package]] +name = "proc-macro2" +version = "1.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04f5f085b5d71e2188cb8271e5da0161ad52c3f227a661a3c135fdf28e258b12" +dependencies = [ + "unicode-xid", +] + +[[package]] +name = "quote" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom", + "libc", + "rand_chacha", + "rand_core", + "rand_hc", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core", +] + +[[package]] +name = "ryu" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" + +[[package]] +name = "serde" +version = "1.0.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e54c9a88f2da7238af84b5101443f0c0d0a3bbdc455e34a5c9497b1903ed55d5" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "609feed1d0a73cc36a0182a840a9b37b4a82f0b1150369f0536a9e3f2a31dc48" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "164eacbdb13512ec2745fb09d51fd5b22b0d65ed294a1dcf7285a360c80a675c" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "syn" +version = "1.0.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d8d6567fe7c7f8835a3a98af4208f3846fba258c1bc3c31d6e506239f11f9" +dependencies = [ + "proc-macro2", + "quote", + "unicode-xid", +] + +[[package]] +name = "unicode-xid" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" diff --git a/04/adventure_game/Cargo.toml b/04/adventure_game/Cargo.toml new file mode 100644 index 0000000..6e191c6 --- /dev/null +++ b/04/adventure_game/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "adventure_game" +version = "0.1.0" +authors = ["Petr Janik <485122@mail.muni.cz>"] +edition = "2018" + +[dependencies] +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +rand = "0.7.3" \ No newline at end of file diff --git a/04/adventure_game/src/lib.rs b/04/adventure_game/src/lib.rs new file mode 100644 index 0000000..e77bfd6 --- /dev/null +++ b/04/adventure_game/src/lib.rs @@ -0,0 +1,173 @@ +use std::io; +use std::collections::HashMap; +use std::fs::File; +use std::io::{Error, Read}; + +use rand::Rng; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize)] +pub struct Scenes { + scenes: Vec +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct Scene { + id: u32, + fight_scene_info: Option, + story: String, + choices: Vec, +} + +#[derive(Serialize, Deserialize, Clone)] +struct FightSceneInfo { + enemy: Character, + player: Character, + on_death: u32, + on_win: u32, +} + +#[derive(Serialize, Deserialize, Clone)] +struct Character { + health: i32, + attack_min: i32, + attack_max: i32, +} + +#[derive(Serialize, Deserialize, Clone)] +struct Choice { + text: String, + next_scene_id: u32, +} + +trait Executable { + /// Takes current scene (as `self`) and map of all scenes, returns next scene. + fn execute(self, scenes: &HashMap) -> Scene; +} + +impl Executable for Scene { + fn execute(self, scenes: &HashMap) -> Scene { + print_scene_story(&self); + print_scene_choices(&self); + if self.fight_scene_info.is_none() { + let choice = get_choice_from_user(self.choices.len() as u32); + let next_scene_id: u32 = self.choices[choice as usize - 1].next_scene_id; + return scenes[&next_scene_id].clone(); + } + let mut info = self.fight_scene_info.unwrap(); + if info.player.health <= 0 { + return scenes[&info.on_death].clone(); + } + if info.enemy.health <= 0 { + return scenes[&info.on_win].clone(); + } + println!("Your health: {}; attack damage range: {}-{}", info.player.health, info.player.attack_min, info.player.attack_max); + println!("Enemy health: {}; attack damage range: {}-{}", info.enemy.health, info.enemy.attack_min, info.enemy.attack_max); + let choice = get_choice_from_user(self.choices.len() as u32); + // choice 1 is always attack + if choice == 1 { + let mut rng = rand::thread_rng(); + let player_dmg = rng.gen_range(info.player.attack_min, info.player.attack_max); + let enemy_dmg = rng.gen_range(info.enemy.attack_min, info.enemy.attack_max); + println!("Player attacks and deals {} damage.", player_dmg); + println!("Enemy attacks at the same time and deals {} damage.", enemy_dmg); + info.enemy.health -= player_dmg; + info.player.health -= enemy_dmg; + return Scene { + id: self.id, + fight_scene_info: Option::from(info), + story: self.story, + choices: self.choices, + }; + }; + let next_scene_id: u32 = self.choices[choice as usize - 1].next_scene_id; + return scenes[&next_scene_id].clone(); + } +} + +pub fn scenes_into_map(scenes: Scenes) -> HashMap { + let scenes_vec = scenes.scenes; + let mut scenes_map = HashMap::new(); + for scene in scenes_vec { + scenes_map.insert(scene.id, scene); + } + scenes_map +} + +pub fn play(scenes: HashMap) { + const INITIAL_SCENE_INDEX: u32 = 0; + + let mut current_scene: Scene = scenes.get(&INITIAL_SCENE_INDEX) + .expect(format!("Story file must contain scene with id {}", INITIAL_SCENE_INDEX).as_str()).clone(); + while !is_end_scene(¤t_scene) { + current_scene = current_scene.execute(&scenes); + } + print_scene_story(¤t_scene); +} + +fn is_end_scene(current_scene: &Scene) -> bool { + current_scene.choices.is_empty() +} + +fn print_scene_story(current_scene: &Scene) { + println!("\n{}", current_scene.story); +} + +fn print_scene_choices(current_scene: &Scene) { + for choice in ¤t_scene.choices { + println!("{}", choice.text); + } +} + +fn get_choice_from_user(num_of_choices: u32) -> u32 { + loop { + println!("What do you choose to do?"); + let mut choice = String::new(); + + io::stdin().read_line(&mut choice).expect("Cannot read user input."); + match choice.trim().parse::() { + Ok(val) => { + if val < 1 || val > num_of_choices { + continue; + } + break val; + } + Err(_) => continue + }; + } +} + +pub fn parse_scenes(story_file: &str) -> Result { + let mut input_file = File::open(story_file)?; + let mut data = String::new(); + input_file.read_to_string(&mut data)?; + let scenes: Scenes = serde_json::from_str(&*data)?; + Ok(scenes) +} + +#[cfg(test)] +mod tests { + use crate::{Choice, is_end_scene, Scene}; + + #[test] + fn scene_without_choices_is_end_scene() { + let end_scene = Scene { + id: 0, + fight_scene_info: None, + story: "".to_string(), + choices: vec![], + }; + assert!(is_end_scene(&end_scene)); + } + + #[test] + fn scene_with_choices_is_not_end_scene() { + let not_end_scene = Scene { + id: 0, + fight_scene_info: None, + story: "some story text".to_string(), + choices: vec![Choice { text: "another story text".to_string(), next_scene_id: 1 }], + }; + assert!(!is_end_scene(¬_end_scene)); + } +} \ No newline at end of file diff --git a/04/adventure_game/src/main.rs b/04/adventure_game/src/main.rs new file mode 100644 index 0000000..e2c0232 --- /dev/null +++ b/04/adventure_game/src/main.rs @@ -0,0 +1,19 @@ +use adventure_game::{parse_scenes, scenes_into_map, play}; +use std::env; + +fn main() { + let args = get_args(); + + let scenes = parse_scenes(&args[1]).expect("Unable to parse scenes"); + let scenes_map = scenes_into_map(scenes); + + play(scenes_map); +} + +fn get_args() -> Vec { + let arguments: Vec<_> = env::args().collect(); + if arguments.len() != 2 { + panic!("Usage: {} ", arguments[0]) + }; + arguments +} \ No newline at end of file diff --git a/04/adventure_game/story.json b/04/adventure_game/story.json new file mode 100644 index 0000000..55a15e4 --- /dev/null +++ b/04/adventure_game/story.json @@ -0,0 +1,71 @@ +{ + "scenes": [ + { + "id": 0, + "story": "The adventure begins!", + "choices": [ + { + "text": "1) Go to the city", + "next_scene_id": 1 + } + ] + }, + { + "id": 1, + "story": "You are in the middle of the city.", + "choices": [ + { + "text": "1) Go to shop", + "next_scene_id": 2 + }, + { + "text": "2) Go to forest", + "next_scene_id": 3 + } + ] + }, + { + "id": 2, + "story": "You are in the shop.", + "choices": [ + { + "text": "1) Leave the shop", + "next_scene_id": 1 + } + ] + }, + { + "id": 3, + "fight_scene_info": { + "enemy": { + "health": 10, + "attack_min": 0, + "attack_max": 5 + }, + "player": { + "health": 20, + "attack_min": 0, + "attack_max": 3 + }, + "on_death": 4, + "on_win": 1 + }, + "story": "You see a dragon.", + "choices": [ + { + "text": "1) Attack the dragon", + "next_scene_id": 3 + }, + { + "text": "2) Run away", + "next_scene_id": 1 + } + ] + }, + { + "id": 4, + "story": "You died, game over!", + "choices": [] + } + ] +} \ No newline at end of file