diff --git a/aufgaben/sheet5/sol1/Cargo.lock b/aufgaben/sheet5/sol1/Cargo.lock new file mode 100755 index 0000000..345edff --- /dev/null +++ b/aufgaben/sheet5/sol1/Cargo.lock @@ -0,0 +1,49 @@ +[root] +name = "poki" +version = "0.1.0" +dependencies = [ + "term-painter 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "kernel32-sys" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "term" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)", + "winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "term-painter" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +dependencies = [ + "term 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)", +] + +[[package]] +name = "winapi" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[[package]] +name = "winapi-build" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" + +[metadata] +"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +"checksum term 0.4.4 (registry+https://github.com/rust-lang/crates.io-index)" = "3deff8a2b3b6607d6d7cc32ac25c0b33709453ca9cceac006caac51e963cf94a" +"checksum term-painter 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "bea17428eebaef979ea9602d2141612dbdff9caaec7db369537c4ebac6d00aa1" +"checksum winapi 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)" = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" +"checksum winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" diff --git a/aufgaben/sheet5/sol1/Cargo.toml b/aufgaben/sheet5/sol1/Cargo.toml new file mode 100755 index 0000000..b06b091 --- /dev/null +++ b/aufgaben/sheet5/sol1/Cargo.toml @@ -0,0 +1,7 @@ +[package] +name = "poki" +version = "0.1.0" +authors = ["Lukas Kalbertodt "] + +[dependencies] +term-painter = "0.2" diff --git a/aufgaben/sheet5/sol1/input.txt b/aufgaben/sheet5/sol1/input.txt new file mode 100755 index 0000000..24ae88c --- /dev/null +++ b/aufgaben/sheet5/sol1/input.txt @@ -0,0 +1,9 @@ +Bulbasaur +Blastoise +1 +0 +0 +0 +0 +0 +0 diff --git a/aufgaben/sheet5/sol1/src/db/data.rs b/aufgaben/sheet5/sol1/src/db/data.rs new file mode 100755 index 0000000..4133978 --- /dev/null +++ b/aufgaben/sheet5/sol1/src/db/data.rs @@ -0,0 +1,169 @@ +use db::types::*; + + +pub const POKEDEX: &'static [PokemonModel] = &[ + PokemonModel { + name: "Bulbasaur", + id: 001, + type_: PokemonType::Two(Type::Poison, Type::Grass), + base_stats: Stats { + hp: 45, + attack: 49, + defense: 49, + special_attack: 65, + special_defense: 65, + speed: 45, + }, + attacks: &[TACKLE], + }, + PokemonModel { + name: "Ivysaur", + id: 002, + type_: PokemonType::Two(Type::Poison, Type::Grass), + base_stats: Stats { + hp: 60, + attack: 62, + defense: 63, + special_attack: 80, + special_defense: 80, + speed: 60, + }, + attacks: &[TACKLE, VINE_WHIP], + }, + PokemonModel { + name: "Venusaur", + id: 003, + type_: PokemonType::Two(Type::Poison, Type::Grass), + base_stats: Stats { + hp: 80, + attack: 82, + defense: 83, + special_attack: 100, + special_defense: 100, + speed: 80, + }, + attacks: &[TACKLE, VINE_WHIP], + }, + PokemonModel { + name: "Charmander", + id: 004, + type_: PokemonType::One(Type::Fire), + base_stats: Stats { + hp: 39, + attack: 52, + defense: 43, + special_attack: 60, + special_defense: 50, + speed: 65, + }, + attacks: &[TACKLE], + }, + PokemonModel { + name: "Charmeleon", + id: 005, + type_: PokemonType::One(Type::Fire), + base_stats: Stats { + hp: 58, + attack: 64, + defense: 58, + special_attack: 80, + special_defense: 65, + speed: 80, + }, + attacks: &[TACKLE, EMBER], + }, + PokemonModel { + name: "Charizard", + id: 006, + type_: PokemonType::Two(Type::Fire, Type::Flying), + base_stats: Stats { + hp: 78, + attack: 84, + defense: 78, + special_attack: 109, + special_defense: 85, + speed: 100, + }, + attacks: &[TACKLE, EMBER], + }, + PokemonModel { + name: "Squirtle", + id: 007, + type_: PokemonType::One(Type::Water), + base_stats: Stats { + hp: 44, + attack: 48, + defense: 65, + special_attack: 50, + special_defense: 64, + speed: 43, + }, + attacks: &[TACKLE], + }, + PokemonModel { + name: "Wartortle", + id: 008, + type_: PokemonType::One(Type::Water), + base_stats: Stats { + hp: 59, + attack: 63, + defense: 80, + special_attack: 65, + special_defense: 80, + speed: 58, + }, + attacks: &[TACKLE, WATER_GUN], + }, + PokemonModel { + name: "Blastoise", + id: 009, + type_: PokemonType::One(Type::Water), + base_stats: Stats { + hp: 79, + attack: 83, + defense: 100, + special_attack: 85, + special_defense: 105, + speed: 78, + }, + attacks: &[TACKLE, WATER_GUN], + }, +]; + + +/// List of all attacks. +/// +/// Of course, these are not all attacks. We will probably provide a much +/// bigger database with the next sheet. +pub const ATTACK_DB: &'static [Attack] = &[ + Attack { + category: AttackCategory::Physical, + name: "Tackle", + base_power: 50, + type_: Type::Normal, + }, + Attack { + category: AttackCategory::Special, + name: "Vine Whip", + base_power: 45, + type_: Type::Grass, + }, + Attack { + category: AttackCategory::Special, + name: "Ember", + base_power: 40, + type_: Type::Fire, + }, + Attack { + category: AttackCategory::Special, + name: "Water Gun", + base_power: 40, + type_: Type::Water, + }, +]; + +// These are just some easy names to be more expressive in the Pokedex. +pub const TACKLE: &'static Attack = &ATTACK_DB[0]; +pub const VINE_WHIP: &'static Attack = &ATTACK_DB[1]; +pub const EMBER: &'static Attack = &ATTACK_DB[2]; +pub const WATER_GUN: &'static Attack = &ATTACK_DB[3]; diff --git a/aufgaben/sheet5/sol1/src/db/mod.rs b/aufgaben/sheet5/sol1/src/db/mod.rs new file mode 100755 index 0000000..81193bc --- /dev/null +++ b/aufgaben/sheet5/sol1/src/db/mod.rs @@ -0,0 +1,22 @@ +pub mod types; +pub mod data; + +pub use self::types::*; + + +// pub fn pokemon_by_id(id: u16) -> Option { +// match data::POKEDEX.get(id as usize) { +// None => None, +// Some(&pm) => Some(pm), +// } +// } + +pub fn pokemon_by_name(name: &str) -> Option { + for &pm in data::POKEDEX { + if pm.name == name { + return Some(pm); + } + } + + None +} diff --git a/aufgaben/sheet5/sol1/src/db/types.rs b/aufgaben/sheet5/sol1/src/db/types.rs new file mode 100755 index 0000000..c773064 --- /dev/null +++ b/aufgaben/sheet5/sol1/src/db/types.rs @@ -0,0 +1,118 @@ +// This module defines types only. Not all fields or variants of those are +// necessarily used, so we don't want all those warnings. This should be safe +// since no algorithm is defined in this module (in which such a warning +// could hint the existance of a bug). +// #![allow(dead_code)] + +/// Describes an attack with all its properties. This type is similar to +/// `PokemonModel`, as there are finite many, immutable instances of this type +/// in a database. This is not a type whose instances change over time. +#[derive(Debug, Clone, Copy)] +pub struct Attack { + pub category: AttackCategory, + pub name: &'static str, + /// Base power of the move. The actual inflicted damage is calculated with + /// a formula using the move's power and a few other parameters. + pub base_power: u8, + pub type_: Type, +} + +/// Category of an attack. +/// +/// Note: currently, the category 'status' is missing. +#[derive(Debug, Clone, Copy)] +pub enum AttackCategory { + /// Attacks with body contact, like "Tackle" or "Bite" + Physical, + /// Attacks without body contact, like "Bubble Beam" or "Thunderbolt" + Special, +} + +/// Describes how effective an attack of one type is on a Pokemon of another +/// type. +/// +/// Note that a Pokemon can have two types. In order to determine the +/// effectiveness, the multipliers of the effectivenesses on both types +/// are multiplied. As such, there can be 0.25 and 4.0 multipliers! +#[derive(Debug, Clone, Copy)] +pub enum TypeEffectiveness { + NotEffective, + NotVeryEffective, + Normal, + SuperEffective, +} + + +/// Types (sometimes called "elements") of the Pokemon universe. Each +/// attack-move has exactly one type, Pokemons can have one or two types. +#[derive(Debug, Clone, Copy)] +#[allow(dead_code)] +pub enum Type { + Normal, + Fire, + Fighting, + Water, + Flying, + Grass, + Poison, + Electric, + Ground, + Psychic, + Rock, + Ice, + Bug, + Dragon, + Ghost, + Dark, + Steel, + Fairy, +} + +/// Describes the type of a Pokemon. Pokemon can have one or two types. +#[derive(Debug, Clone, Copy)] +pub enum PokemonType { + One(Type), + Two(Type, Type), +} + +/// Describes a kind of Pokemon, e.g. "Pikachu". +/// +/// This is different than an actual, living Pokemon. This struct just +/// describes properties that are the same for every creature of this +/// Pokemon kind. +#[derive(Debug, Clone, Copy)] +pub struct PokemonModel { + /// Name of the Pokemon + pub name: &'static str, + /// ID in the international Pokedex + pub id: u16, + pub type_: PokemonType, + pub base_stats: Stats, + /// This is different from the real Pokemon games: attacks are not part + /// of the Pokemon model, but of the Pokemon itself (as they change over + /// time). A pokemon just has an abstract learnset of potential attacks. + /// But this is easier for now. + pub attacks: &'static [&'static Attack] +} + +/// Describes the basic stats of a Pokemon. +/// +/// Each living Pokemon has an actual stat value, but each Pokemon kind also +/// has so called "base stats". These base stats are used to calculate the +/// actual stats, whose depend on the Pokemon's current level. Stronger Pokemon +/// have higher base stats. +#[derive(Debug, Clone, Copy)] +pub struct Stats { + /// Health points + pub hp: u16, + /// Speed, sometimes called initiative (INIT) + pub speed: u16, + /// Strength of physical attacks (like "Tackle") + pub attack: u16, + /// Strength of special attacks (like "Bubble Beam") + pub special_attack: u16, + /// Defense power against physical attacks (like "Tackle") + pub defense: u16, + /// Defense power against special attacks (like "Bubble Beam") + pub special_defense: u16, +} diff --git a/aufgaben/sheet5/sol1/src/engine/canon.rs b/aufgaben/sheet5/sol1/src/engine/canon.rs new file mode 100755 index 0000000..3312fda --- /dev/null +++ b/aufgaben/sheet5/sol1/src/engine/canon.rs @@ -0,0 +1,120 @@ +use db::types::*; +use super::Pokemon; + +impl TypeEffectiveness { + /// Returns the type effectiveness of an attack from one attacker type + /// on one defender type. + pub fn of_attack(attacker: Type, defender: Type) -> Self { + use db::Type::*; + use db::TypeEffectiveness as Te; + + // TODO: complete this + match (attacker, defender) { + (Fire, Water) => Te::NotVeryEffective, + (Fire, Grass) => Te::SuperEffective, + (Water, Fire) => Te::SuperEffective, + (Water, Grass) => Te::NotVeryEffective, + (Grass, Fire) => Te::NotVeryEffective, + (Grass, Water) => Te::SuperEffective, + (Ground, Flying) => Te::NotEffective, + _ => Te::Normal, + } + } + + /// Returns the corresponding multiplier for the damage formula. + pub fn multiplier(&self) -> f64 { + match *self { + TypeEffectiveness::NotEffective => 0.0, + TypeEffectiveness::NotVeryEffective => 0.5, + TypeEffectiveness::Normal => 1.0, + TypeEffectiveness::SuperEffective => 2.0, + } + } +} + + +impl Stats { + /// Given the base stats and a level, this function returns the actual + /// stats for that level. + /// + /// This function doesn't implement the correct formula used by Pokemon + /// games. It is a simplified version of the original formula for now: we + /// ignore IVs, EVs and the Pokemon's nature). The complete formula can be + /// found [here (HP)][1] and [here (other stats)][2]. + /// + /// [1]: http://bulbapedia.bulbagarden.net/wiki/File:HPStatCalcGen34.png + /// [2]: http://bulbapedia.bulbagarden.net/wiki/File:OtherStatCalcGen34.png + pub fn at_level(base: Self, level: u8) -> Self { + /// The formula is the same for all stats != hp + fn stat_formula(base: u16, level: u8) -> u16 { + ((base as f64 * level as f64) / 50.0 + 5.0) as u16 + } + + let hp = ( + (base.hp as f64 * level as f64) / 50.0 + + level as f64 + + 10.0 + ) as u16; + + Stats { + hp: hp, + speed: stat_formula(base.speed, level), + attack: stat_formula(base.attack, level), + special_attack: stat_formula(base.special_attack, level), + defense: stat_formula(base.defense, level), + special_defense: stat_formula(base.special_defense, level), + } + } +} + +// =========================================================================== +// =========================================================================== +// =========================================================================== +// Formulas to calculate stuff +// =========================================================================== +// =========================================================================== +// =========================================================================== + +/// Calculates the damage of an attack. We don't use the exact formula, but +/// a simplified version of it. In particular, we simplified the "Modifier" +/// term quite a bit. The correct and complete formula can be found [here][1]. +/// +/// [1]: http://bulbapedia.bulbagarden.net/wiki/Damage#Damage_formula +pub fn attack_damage(attacker: &Pokemon, defender: &Pokemon, attack: Attack) -> u16 { + // Depending on the attack category, get the correct stats + let (attack_mod, defense_mod) = match attack.category { + AttackCategory::Physical => { + (attacker.stats().attack, defender.stats().defense) + } + AttackCategory::Special => { + (attacker.stats().special_attack, defender.stats().special_defense) + } + }; + + // Cast everything to f64 to reduce noise in actual formula + let (attack_mod, defense_mod) = (attack_mod as f64, defense_mod as f64); + let base_power = attack.base_power as f64; + let attacker_level = attacker.level() as f64; + + // The modifier only depends on the type effectiveness (in our simplified + // version!). + let modifier = match defender.model().type_ { + PokemonType::One(ty) => { + TypeEffectiveness::of_attack(attack.type_, ty).multiplier() + } + PokemonType::Two(ty_a, ty_b) => { + TypeEffectiveness::of_attack(attack.type_, ty_a).multiplier() + * TypeEffectiveness::of_attack(attack.type_, ty_b).multiplier() + } + }; + + // With every parameter prepared above, here is the formula + ( + ( + ((2.0 * attacker_level + 10.0) / 250.0) + * (attack_mod / defense_mod) + * base_power + + 2.0 + ) * modifier + ) as u16 +} diff --git a/aufgaben/sheet5/sol1/src/engine/mod.rs b/aufgaben/sheet5/sol1/src/engine/mod.rs new file mode 100755 index 0000000..505531a --- /dev/null +++ b/aufgaben/sheet5/sol1/src/engine/mod.rs @@ -0,0 +1,57 @@ +use db::types::*; + +pub mod canon; + +pub struct Pokemon { + /// Reference to the kind of the Pokemon, which contains the name, base + /// stats, id and more global data. + model: PokemonModel, + /// These are the actual stats of the pokemon, that fit to the current + /// level + stats: Stats, + /// The current level of the Pokemon. + level: u8, +} + +impl Pokemon { + /// Creates a new living Pokemon of the given Pokemon kind (model) with the + /// specified level. + pub fn with_level(model: PokemonModel, level: u8) -> Self { + Pokemon { + model: model, + stats: Stats::at_level(model.base_stats, level), + level: level, + } + } + + /// Returns the current stats. + pub fn stats(&self) -> &Stats { + &self.stats + } + + /// Returns the Pokemon kind of this Pokemon. + pub fn model(&self) -> &PokemonModel { + &self.model + } + + /// Returns the name. + pub fn name(&self) -> &str { + self.model.name + } + + /// Returns the current level. + pub fn level(&self) -> u8 { + self.level + } + + /// Decreases the Pokemon's HP according to the given attack and attacker. + pub fn endure_attack(&mut self, attacker: &Pokemon, attack: Attack) { + let damage = canon::attack_damage(attacker, self, attack); + self.stats.hp = self.stats.hp.saturating_sub(damage); + } + + /// Returns whether or not the Pokemon is still alive (more than 0 HP). + pub fn is_alive(&self) -> bool { + self.stats.hp > 0 + } +} diff --git a/aufgaben/sheet5/sol1/src/game.rs b/aufgaben/sheet5/sol1/src/game.rs new file mode 100755 index 0000000..8fc5edf --- /dev/null +++ b/aufgaben/sheet5/sol1/src/game.rs @@ -0,0 +1,162 @@ +use db::types::*; +use db; +use engine::Pokemon; + +use term_painter::Color::*; +use term_painter::ToStyle; + +pub fn fight(mut red: Pokemon, mut blue: Pokemon) { + loop { + fn check_dead(poki: &Pokemon) -> bool { + if poki.is_alive() { + false + } else { + println!(">>>>> {} fainted!", Yellow.paint(poki.name())); + true + } + } + + // Print status + println!( + ">>>>> Status: {} has {} HP, {} has {} HP", + Yellow.paint(red.name()), + Red.paint(red.stats().hp), + Yellow.paint(blue.name()), + Red.paint(blue.stats().hp), + ); + + // Execute both attack + if red.stats().speed > blue.stats().speed { + // Red attacks blue + execute_round(&red, &mut blue); + if check_dead(&blue) { + break; + } + + // BLue attacks red + execute_round(&blue, &mut red); + if check_dead(&red) { + break; + } + } else { + // BLue attacks red + execute_round(&blue, &mut red); + if check_dead(&red) { + break; + } + + // Red attacks blue + execute_round(&red, &mut blue); + if check_dead(&blue) { + break; + } + } + } +} + + +/// Executes one round of one player: +/// +/// - the player chooses one attack to execute +/// - the attack is excuted and the enemy's HP +fn execute_round(attacker: &Pokemon, defender: &mut Pokemon) { + // Tell the user to choose an attack + println!( + ">>>>> {} is about to attack! Which move shall it execute?", + Yellow.paint(attacker.model().name) + ); + + // Print a list of available attacks + let num_attacks = attacker.model().attacks.len(); + for i in 0..num_attacks { + println!( + " {}: {}", + i, + Blue.paint(attacker.model().attacks[i].name) + ); + } + println!(" !!! Please give me the attack ID:"); + + // Read attack ID from the user + let attack_id; + loop { + let input = read_usize(); + if input >= num_attacks { + println!(" !!! There is no attack with index {}!", input); + } else { + attack_id = input; + break; + } + } + + // Execute attack + let attack = *attacker.model().attacks[attack_id]; + defender.endure_attack(attacker, attack); + + // Status update + println!( + ">>>>> {} uses {}! ({} has {} HP left)", + Yellow.paint(attacker.model().name), + Blue.paint(attack.name), + Yellow.paint(defender.model().name), + Red.paint(defender.stats().hp), + ); +} + +/// Let's the player choose a Pokemon from the database. +pub fn choose_pokemon(player: &str) -> PokemonModel { + // Loop forever until the user has chosen a Pokemon + loop { + println!( + "{}, please choose a Pokemon (or type '?' to get a complete list)", + player, + ); + + let input = read_string(); + if input == "?" { + print_pokemon_list(); + } else { + // Try to find a Pokemon with the given name + match db::pokemon_by_name(&input) { + Some(poki) => return poki, + None => { + println!("No pokemon with the name '{}' was found!", input); + } + } + } + } +} + +/// Prints a list of all Pokemon in the database. +fn print_pokemon_list() { + for poki in db::data::POKEDEX { + // This strange formatter will print the pokemon ID with three digits, + // filling in 0 from the left if necessary (#003). + println!("#{:0>3} {}", poki.id, poki.name); + } +} + + +/// Reads a string from the terminal/user. +fn read_string() -> String { + let mut buffer = String::new(); + ::std::io::stdin() + .read_line(&mut buffer) + .expect("something went horribly wrong..."); + + // Discard trailing newline + let new_len = buffer.trim_right().len(); + buffer.truncate(new_len); + + buffer +} + +/// Reads a valid `usize` integer from the terminal/user. +fn read_usize() -> usize { + loop { + match read_string().parse() { + Ok(res) => return res, + Err(_) => println!("That was not an integer! Please try again!"), + } + } +} diff --git a/aufgaben/sheet5/sol1/src/main.rs b/aufgaben/sheet5/sol1/src/main.rs new file mode 100755 index 0000000..51e1bef --- /dev/null +++ b/aufgaben/sheet5/sol1/src/main.rs @@ -0,0 +1,20 @@ +extern crate term_painter; + +mod db; +mod engine; +mod game; + +use game::{choose_pokemon, fight}; +use engine::Pokemon; + +fn main() { + // Let both players choose their Pokemon + let model_red = choose_pokemon("Player Red"); + let poki_red = Pokemon::with_level(model_red, 5); + + let model_blue = choose_pokemon("Player Blue"); + let poki_blue = Pokemon::with_level(model_blue, 5); + + // Let both pokis fight! + fight(poki_red, poki_blue); +}