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