use rand::prelude::*; use uuid::Uuid; // timekeeping use chrono::prelude::*; use chrono::Duration; // Db Commons use serde_cbor::{from_slice, to_vec}; use postgres::transaction::Transaction; use failure::Error; use failure::err_msg; use account::Account; use rpc::{GameStateParams, GameSkillParams}; use construct::{Construct}; use skill::{Skill, Effect, Cast, Resolution, Event, resolution_steps}; use player::{Player}; use instance::{instance_game_finished, global_game_finished}; #[derive(Debug,Clone,Copy,PartialEq,Serialize,Deserialize)] pub enum Phase { Start, Skill, Resolve, Finish, } #[derive(Debug,Clone,Serialize,Deserialize)] pub struct Game { pub id: Uuid, pub player_constructs: usize, pub player_num: usize, pub players: Vec, pub phase: Phase, pub stack: Vec, pub resolved: Vec, pub log: Vec, pub instance: Option, phase_end: DateTime, phase_start: DateTime, } impl Game { pub fn new() -> Game { return Game { id: Uuid::new_v4(), player_constructs: 0, player_num: 0, players: vec![], phase: Phase::Start, stack: vec![], resolved: vec![], log: vec![], instance: None, phase_end: Utc::now(), phase_start: Utc::now(), }; } pub fn set_player_num(&mut self, size: usize) -> &mut Game { self.player_num = size; self } pub fn set_player_constructs(&mut self, size: usize) -> &mut Game { self.player_constructs = size; self } pub fn set_instance(&mut self, id: Uuid) -> &mut Game { self.instance = Some(id); self } pub fn joinable(&self) -> bool { self.can_start() } pub fn player_add(&mut self, mut player: Player) -> Result<&mut Game, Error> { if self.players.len() == self.player_num { return Err(err_msg("maximum number of players")); } if self.players.iter().any(|t| t.id == player.id) { return Err(err_msg("player already in game")); } if player.constructs.iter().all(|c| c.skills.len() == 0) { info!("WARNING: {:?} has no skills and has forfeited {:?}", player.name, self.id); player.forfeit(); } let player_description = player.constructs.iter().map(|c| c.name.clone()).collect::>().join(", "); self.log.push(format!("{:} has joined the game. [{:}]", player.name, player_description)); player.constructs.sort_unstable_by_key(|c| c.id); self.players.push(player); Ok(self) } // handle missing player properly pub fn player_by_id(&mut self, id: Uuid) -> Result<&mut Player, Error> { self.players .iter_mut() .find(|t| t.id == id) .ok_or(format_err!("{:?} not in game", id)) } pub fn construct_by_id(&mut self, id: Uuid) -> Option<&mut Construct> { match self.players.iter_mut().find(|t| t.constructs.iter().any(|c| c.id == id)) { Some(player) => player.constructs.iter_mut().find(|c| c.id == id), None => None, } } pub fn construct_by_id_take(&mut self, id: Uuid) -> Construct { match self.players.iter_mut().find(|t| t.constructs.iter().any(|c| c.id == id)) { Some(player) => { let i = player.constructs.iter().position(|c| c.id == id).unwrap(); player.constructs.remove(i) } None => panic!("id not in game {:}", id), } } fn all_constructs(&self) -> Vec { self.players.clone() .into_iter() .flat_map( |t| t.constructs .into_iter()) .collect::>() } pub fn update_construct(&mut self, construct: &mut Construct) -> &mut Game { match self.players.iter_mut().find(|t| t.constructs.iter().any(|c| c.id == construct.id)) { Some(player) => { let index = player.constructs.iter().position(|t| t.id == construct.id).unwrap(); player.constructs.remove(index); player.constructs.push(construct.clone()); player.constructs.sort_unstable_by_key(|c| c.id); }, None => panic!("construct not in game"), }; self } pub fn can_start(&self) -> bool { return self.players.len() == self.player_num && self.players.iter().all(|t| t.constructs.len() == self.player_constructs) } pub fn start(mut self) -> Game { self.log.push("Game starting...".to_string()); // forfeit if self.finished() { return self.finish(); } self.skill_phase_start(0) } fn skill_phase_start(mut self, num_resolutions: usize) -> Game { let resolution_animation_ms = num_resolutions * 2500; let phase_add_time_ms = 60000 + resolution_animation_ms; self.phase_start = Utc::now() .checked_add_signed(Duration::milliseconds(resolution_animation_ms as i64)) .expect("could not set phase start"); self.phase_end = Utc::now() .checked_add_signed(Duration::milliseconds(phase_add_time_ms as i64)) .expect("could not set phase end"); for player in self.players.iter_mut() { if player.skills_required() == 0 { continue; } player.set_ready(false); for construct in player.constructs.iter_mut() { for i in 0..construct.skills.len() { if let Some(_d) = construct.disabled(construct.skills[i].skill) { // info!("{:?} disabled {:?}", construct.skills[i].skill, d); construct.skills[i].disabled = true; } else { construct.skills[i].disabled = false; } } } } self.log.push("".to_string()); if ![Phase::Start, Phase::Resolve].contains(&self.phase) { panic!("game not in Resolve or start phase"); } self.phase = Phase::Skill; self.pve_add_skills(); if self.skill_phase_finished() { return self.resolve_phase_start() } self } fn pve_add_skills(&mut self) -> &mut Game { let mut pve_skills = vec![]; for mobs in self.players .iter() .filter(|t| t.bot) { let player_player = self.players.iter().find(|t| t.id != mobs.id).unwrap(); for mob in mobs.constructs.iter() { let skill = mob.mob_select_skill(); // info!("{:?} {:?}", mob.name, skill); match skill { Some(s) => { let mut rng = thread_rng(); // the mut marks it as being able to be called // more than once let mut find_target = || { match s.defensive() { true => &mobs.constructs[rng.gen_range(0, mobs.constructs.len())], false => &player_player.constructs[rng.gen_range(0, player_player.constructs.len())], } }; let mut target = find_target(); while target.is_ko() { target = find_target(); } pve_skills.push((mobs.id, mob.id, Some(target.id), s)); }, None => continue, }; } } for (player_id, mob_id, target_id, s) in pve_skills { match self.add_skill(player_id, mob_id, target_id, s) { Ok(_) => (), Err(e) => { info!("{:?}", self.construct_by_id(mob_id)); panic!("{:?} unable to add pve mob skill {:?}", e, s); }, } self.player_ready(player_id).unwrap(); } self } fn add_skill(&mut self, player_id: Uuid, source_construct_id: Uuid, target_construct_id: Option, skill: Skill) -> Result<&mut Game, Error> { // check player in game self.player_by_id(player_id)?; if self.phase != Phase::Skill { return Err(err_msg("game not in skill phase")); } let final_target_id = match skill.self_targeting() { true => source_construct_id, false => match target_construct_id { Some(t) => t, None => return Err(err_msg("skill requires a target")), } }; // target checks { let target = match self.construct_by_id(final_target_id) { Some(c) => c, None => return Err(err_msg("target construct not in game")), }; // fixme for rez if target.is_ko() { return Err(err_msg("target construct is ko")); } } // construct checks { let construct = match self.construct_by_id(source_construct_id) { Some(c) => c, None => return Err(err_msg("construct not in game")), }; if construct.is_ko() { return Err(err_msg("construct is ko")); } // check the construct has the skill if !construct.knows(skill) { return Err(err_msg("construct does not have that skill")); } if construct.skill_on_cd(skill).is_some() { return Err(err_msg("abiltity on cooldown")); } // check here as well so uncastable spells don't go on the stack if let Some(disable) = construct.disabled(skill) { return Err(format_err!("skill disabled {:?}", disable)); } } // replace construct skill if let Some(s) = self.stack.iter_mut().position(|s| s.source_construct_id == source_construct_id) { self.stack.remove(s); } let skill = Cast::new(source_construct_id, player_id, final_target_id, skill); self.stack.push(skill); return Ok(self); } fn player_ready(&mut self, player_id: Uuid) -> Result<&mut Game, Error> { if self.phase != Phase::Skill { return Err(err_msg("game not in skill phase")); } self.player_by_id(player_id)? .set_ready(true); Ok(self) } fn skill_phase_finished(&self) -> bool { self.players.iter().all(|t| t.ready) // self.players.iter() // // for every player // .all(|t| self.stack.iter() // // the number of skills they have cast // .filter(|s| s.source_player_id == t.id).collect::>() // // should equal the number required this turn // .len() == t.skills_required() // ) } // requires no input // just do it fn resolve_phase_start(mut self) -> Game { if self.phase != Phase::Skill { panic!("game not in skill phase"); } assert!(self.skill_phase_finished()); self.phase = Phase::Resolve; self.log.push("".to_string()); self.resolve_skills() } fn stack_sort_speed(&mut self) -> &mut Game { let mut sorted = self.stack.clone(); sorted.iter_mut() .for_each(|s| { let caster = self.construct_by_id(s.source_construct_id).unwrap(); let speed = caster.skill_speed(s.skill); s.speed = speed; }); sorted.sort_unstable_by_key(|s| s.speed); self.stack = sorted; self } fn construct_aoe_targets(&self, construct_id: Uuid) -> Vec { self.players.iter() .find(|t| t.constructs.iter().any(|c| c.id == construct_id)) .unwrap() .constructs .iter() .map(|c| c.id) .collect() } pub fn get_targets(&self, skill: Skill, source: &Construct, target_construct_id: Uuid) -> Vec { let target_player = self.players.iter() .find(|t| t.constructs.iter().any(|c| c.id == target_construct_id)) .unwrap(); if let Some(t) = target_player.taunting() { return vec![t.id]; } match source.skill_is_aoe(skill) { true => self.construct_aoe_targets(target_construct_id), false => vec![target_construct_id], } } fn resolve_skills(mut self) -> Game { if self.phase != Phase::Resolve { panic!("game not in Resolve phase"); } // find their statuses with ticks let mut ticks = self.all_constructs() .iter() .flat_map( |c| c.effects .iter() .cloned() .filter_map(|e| e.tick)) .collect::>(); // add them to the stack self.stack.append(&mut ticks); self.stack_sort_speed(); // temp vec of this round's resolving skills // because need to check cooldown use before pushing them into the complete list let mut casts = vec![]; let mut turn_events = 0; while let Some(cast) = self.stack.pop() { // info!("{:} casts ", cast); let mut resolutions = resolution_steps(&cast, &mut self); resolutions.reverse(); turn_events += resolutions.len(); while let Some(resolution) = resolutions.pop() { self.log_resolution(cast.speed, &resolution); // the results go into the resolutions self.resolved.push(resolution); } // the cast itself goes into this temp vec // to handle cooldowns casts.push(cast); // sort the stack again in case speeds have changed self.stack_sort_speed(); }; // info!("{:#?}", self.casts); // handle cooldowns and statuses self.progress_durations(&casts); if self.finished() { return self.finish() } self.skill_phase_start(turn_events) } fn progress_durations(&mut self, resolved: &Vec) -> &mut Game { for mut construct in self.all_constructs() { // info!("progressing durations for {:}", construct.name); if construct.is_ko() { continue; } // only reduce cooldowns if no cd was used // have to borrow self for the skill check { if let Some(skill) = resolved.iter().find(|s| s.source_construct_id == construct.id) { if skill.used_cooldown() { construct.skill_set_cd(skill.skill); } else { construct.reduce_cooldowns(); } } else { construct.reduce_cooldowns(); } } // always reduce durations construct.reduce_effect_durations(); self.update_construct(&mut construct); } self } fn log_resolution(&mut self, speed: u64, resolution: &Resolution) -> &mut Game { let Resolution { source, target, event, stages } = resolution; match event { Event::Ko { skill: _ }=> self.log.push(format!("{:} KO!", target.name)), Event::Disable { skill, disable } => self.log.push(format!("{:} {:?} {:} disabled {:?}", source.name, skill, target.name, disable)), Event::Immunity { skill, immunity } => self.log.push(format!("[{:}] {:} {:?} {:} immune {:?}", speed, source.name, skill, target.name, immunity)), Event::TargetKo { skill } => self.log.push(format!("[{:}] {:} {:?} {:} - target is KO", speed, source.name, skill, target.name)), Event::Damage { skill, amount, mitigation, colour: _ } => self.log.push(format!("[{:}] {:} {:?} {:} {:} ({:} mitigated)", speed, source.name, skill, target.name, amount, mitigation)), Event::Healing { skill, amount, overhealing } => self.log.push(format!("[{:}] {:} {:?} {:} {:} healing ({:}OH)", speed, source.name, skill, target.name, amount, overhealing)), Event::Inversion { skill } => self.log.push(format!("[{:}] {:} {:?} {:} INVERTED", speed, source.name, skill, target.name)), Event::Reflection { skill } => self.log.push(format!("[{:}] {:} {:?} {:} REFLECTED", speed, source.name, skill, target.name)), Event::Effect { skill, effect, duration, construct_effects: _ } => self.log.push(format!("[{:}] {:} {:?} {:} {:?} {:}T", speed, source.name, skill, target.name, effect, duration)), Event::Skill { skill } => self.log.push(format!("[{:}] {:} {:?} {:}", speed, source.name, skill, target.name)), Event::Removal { effect, construct_effects: _ } => self.log.push(format!("[{:}] {:?} removed {:} {:?}", speed, source.name, target.name, effect)), Event::Recharge { skill, red, blue } => self.log.push(format!("[{:}] {:} {:?} {:} {:}R {:}B", speed, source.name, skill, target.name, red, blue)), Event::Evasion { skill, evasion_rating } => self.log.push(format!("[{:}] {:} {:?} {:} evaded ({:}%)", speed, source.name, skill, target.name, evasion_rating)), Event::Incomplete => panic!("incomplete resolution {:?}", resolution), } self } pub fn finished(&self) -> bool { self.players.iter().any(|t| t.constructs.iter().all(|c| c.is_ko())) } pub fn winner(&self) -> Option<&Player> { self.players.iter().find(|t| t.constructs.iter().any(|c| !c.is_ko())) } fn finish(mut self) -> Game { self.phase = Phase::Finish; self.log.push(format!("Game finished.")); { let winner = self.players.iter().find(|t| t.constructs.iter().any(|c| !c.is_ko())); match winner { Some(w) => self.log.push(format!("Winner: {:}", w.name)), None => self.log.push(format!("Game was drawn.")), }; } self } fn phase_timed_out(&self) -> bool { Utc::now().signed_duration_since(self.phase_end).num_milliseconds() > 0 } pub fn upkeep(mut self) -> Game { if self.phase == Phase::Finish { return self; } if !self.phase_timed_out() { return self; } info!("upkeep game: {:} vs {:}", self.players[0].name, self.players[1].name); for player in self.players.iter_mut() { if !player.ready { player.set_ready(true); player.add_warning(); info!("upkeep: {:} warned", player.name); if player.warnings >= 3 { player.forfeit(); info!("upkeep: {:} forfeited", player.name); self.log.push(format!("{:} forfeited.", player.name)); } } } self = self.resolve_phase_start(); self } } pub fn game_write(tx: &mut Transaction, game: &Game) -> Result<(), Error> { let game_bytes = to_vec(&game)?; let query = " INSERT INTO games (id, data, upkeep) VALUES ($1, $2, $3) RETURNING id; "; // no games should be sent to db that are not in progress let result = tx .query(query, &[&game.id, &game_bytes, &game.phase_end])?; result.iter().next().ok_or(format_err!("no game written"))?; // info!("{:} wrote game", game.id); return Ok(()); } pub fn game_state(params: GameStateParams, tx: &mut Transaction, _account: &Account) -> Result { return game_get(tx, params.id) } pub fn game_get(tx: &mut Transaction, id: Uuid) -> Result { let query = " SELECT * FROM games WHERE id = $1 FOR UPDATE; "; let result = tx .query(query, &[&id])?; let returned = match result.iter().next() { Some(row) => row, None => return Err(err_msg("game not found")), }; // tells from_slice to cast into a construct let game_bytes: Vec = returned.get("data"); let game = from_slice::(&game_bytes)?; return Ok(game); } pub fn games_need_upkeep(tx: &mut Transaction) -> Result, Error> { let query = " SELECT data, id FROM games WHERE finished = false AND upkeep < now() FOR UPDATE; "; let result = tx .query(query, &[])?; let mut list = vec![]; for row in result.into_iter() { let bytes: Vec = row.get(0); let id = row.get(1); match from_slice::(&bytes) { Ok(i) => list.push(i), Err(_e) => { game_delete(tx, id)?; } }; } return Ok(list); } pub fn game_delete(tx: &mut Transaction, id: Uuid) -> Result<(), Error> { let query = " DELETE FROM games WHERE id = $1; "; let result = tx .execute(query, &[&id])?; if result != 1 { return Err(format_err!("unable to delete player {:?}", id)); } info!("game deleted {:?}", id); return Ok(()); } // pub fn game_global_startup(tx: &mut Transaction) -> Result<(), Error> { // if game_global_get(tx).is_ok() { // info!("global mm game exists"); // return Ok(()); // } // let mut game = Game::new(); // game // .set_player_num(2) // .set_player_constructs(3) // .set_mode(GameMode::Pvp); // game_write(tx, &game)?; // let query = " // INSERT INTO matchmaking (id, game) // VALUES ($1, $2) // RETURNING id; // "; // let result = tx // .query(query, &[&Uuid::nil(), &game.id])?; // result.iter().next().ok_or(format_err!("no game written"))?; // info!("{:} wrote global mm startup", game.id); // return Ok(()); // } // pub fn game_global_set(tx: &mut Transaction, game: &Game) -> Result<(), Error> { // let query = " // UPDATE matchmaking // SET game = $1 // WHERE id = $2 // RETURNING id, game; // "; // let result = tx // .query(query, &[&game.id, &Uuid::nil()])?; // result.iter() // .next() // .ok_or(err_msg("could not set global game mm"))?; // return Ok(()); // } // pub fn game_global_get(tx: &mut Transaction) -> Result { // let query = " // SELECT * from games // WHERE id = ( // SELECT game // FROM matchmaking // WHERE id = $1 // ); // "; // let delete_query = " // DELETE from matchmaking; // "; // let result = tx // .query(query, &[&Uuid::nil()])?; // let returned = match result.iter().next() { // Some(row) => row, // None => return Err(err_msg("game not found")), // }; // // tells from_slice to cast into a construct // let game_bytes: Vec = returned.get("data"); // let game = match from_slice::(&game_bytes) { // Ok(g) => g, // Err(_) => { // tx.query(delete_query, &[])?; // return Err(err_msg("matchmaking game was invalid")) // } // }; // return Ok(game); // } pub fn game_update(tx: &mut Transaction, game: &Game) -> Result<(), Error> { let game_bytes = to_vec(&game)?; let query = " UPDATE games SET data = $1, finished = $2, upkeep = $3, updated_at = now() WHERE id = $4 RETURNING id, data; "; let result = tx .query(query, &[&game_bytes, &game.finished(), &game.phase_end, &game.id])?; result.iter().next().ok_or(format_err!("game {:?} could not be written", game))?; if game.finished() { if let Some(i) = game.instance { match i == Uuid::nil() { true => global_game_finished(tx, &game)?, false => instance_game_finished(tx, &game, i)?, } } } return Ok(()); } pub fn game_skill(params: GameSkillParams, tx: &mut Transaction, account: &Account) -> Result { let mut game = game_get(tx, params.game_id)?; game.add_skill(account.id, params.construct_id, params.target_construct_id, params.skill)?; if game.skill_phase_finished() { game = game.resolve_phase_start(); } game_update(tx, &game)?; Ok(game) } pub fn game_ready(params: GameStateParams, tx: &mut Transaction, account: &Account) -> Result { let mut game = game_get(tx, params.id)?; game.player_ready(account.id)?; if game.skill_phase_finished() { game = game.resolve_phase_start(); } game_update(tx, &game)?; Ok(game) } // pub fn game_pve_new(construct_ids: Vec, mode: GameMode, tx: &mut Transaction, account: &Account) -> Result { // if construct_ids.len() == 0 { // return Err(err_msg("no constructs selected")); // } // let constructs = construct_ids // .iter() // .map(|id| construct_get(tx, *id, account.id)) // .collect::, Error>>()?; // if constructs.len() > 3 { // return Err(err_msg("player size too large (3 max)")); // } // // create the game // let mut game = Game::new(); // // let game_id = game.id; // game; // .set_player_num(2) // .set_player_constructs(constructs.len()) // .set_mode(mode); // // create the mob player // let mob_player = generate_mob_player(mode, &constructs); // // add the players // let mut plr_player = Player::new(account.id); // plr_player // .set_constructs(constructs); // game // .player_add(plr_player)? // .player_add(mob_player)?; // game.start(); // return Ok(game); // } // pub fn game_pve(params: GamePveParams, tx: &mut Transaction, account: &Account) -> Result { // let game = game_pve_new(params.construct_ids, GameMode::Normal, tx, account)?; // // persist // game_write(tx, &game)?; // Ok(game) // } pub fn game_instance_new(tx: &mut Transaction, players: Vec, game_id: Uuid, instance_id: Uuid) -> Result { // create the game let mut game = Game::new(); game.id = game_id; game .set_player_num(2) .set_player_constructs(3) .set_instance(instance_id); // create the initiators player for player in players { game.player_add(player)?; } if game.can_start() { game = game.start(); } // persist game_write(tx, &game)?; Ok(game) } // pub fn game_instance_join(tx: &mut Transaction, player: Player, game_id: Uuid) -> Result { // let mut game = game_get(tx, game_id)?; // game.player_add(player)?; // if game.can_start() { // game = game.start(); // } // info!("{:?} game joined", game.id); // game_update(tx, &game)?; // Ok(game) // } #[cfg(test)] mod tests { use game::*; use construct::*; use util::IntPct; fn create_test_game() -> Game { let mut x = Construct::new() .named(&"pronounced \"creeep\"".to_string()) .learn(Skill::Attack) .learn(Skill::Stun) .learn(Skill::Attack) .learn(Skill::Block) .learn(Skill::ParryI) .learn(Skill::SiphonI) .learn(Skill::AmplifyI) .learn(Skill::Stun) .learn(Skill::Block); let mut y = Construct::new() .named(&"lemongrass tea".to_string()) .learn(Skill::Attack) .learn(Skill::Stun) .learn(Skill::Attack) .learn(Skill::Block) .learn(Skill::ParryI) .learn(Skill::SiphonI) .learn(Skill::AmplifyI) .learn(Skill::Stun) .learn(Skill::Block); let mut game = Game::new(); game .set_player_num(2) .set_player_constructs(1); let x_player_id = Uuid::new_v4(); x.account = x_player_id; let mut x_player = Player::new(x_player_id, &"ntr".to_string(), vec![x]); let y_player_id = Uuid::new_v4(); y.account = y_player_id; let mut y_player = Player::new(y_player_id, &"mash".to_string(), vec![y]); game .player_add(x_player).unwrap() .player_add(y_player).unwrap(); assert!(game.can_start()); return game.start(); } fn create_2v2_test_game() -> Game { let mut i = Construct::new() .named(&"pretaliate".to_string()) .learn(Skill::Attack) .learn(Skill::Attack); let mut j = Construct::new() .named(&"poy sian".to_string()) .learn(Skill::Attack) .learn(Skill::Attack); let mut x = Construct::new() .named(&"pronounced \"creeep\"".to_string()) .learn(Skill::Attack) .learn(Skill::Attack); let mut y = Construct::new() .named(&"lemongrass tea".to_string()) .learn(Skill::Attack) .learn(Skill::Attack); let mut game = Game::new(); game .set_player_num(2) .set_player_constructs(2); let i_player_id = Uuid::new_v4(); i.account = i_player_id; j.account = i_player_id; let i_player = Player::new(i_player_id, &"ntr".to_string(), vec![i, j]); let x_player_id = Uuid::new_v4(); x.account = x_player_id; y.account = x_player_id; let x_player = Player::new(x_player_id, &"mashy".to_string(), vec![x, y]); game .player_add(i_player).unwrap() .player_add(x_player).unwrap(); assert!(game.can_start()); return game.start(); } #[test] fn phase_test() { let mut game = create_test_game(); let x_player = game.players[0].clone(); let y_player = game.players[1].clone(); let x_construct = x_player.constructs[0].clone(); let y_construct = y_player.constructs[0].clone(); game.add_skill(x_player.id, x_construct.id, Some(y_construct.id), Skill::Attack).unwrap(); game.add_skill(y_player.id, y_construct.id, Some(x_construct.id), Skill::Attack).unwrap(); game.player_ready(x_player.id).unwrap(); game.player_ready(y_player.id).unwrap(); assert!(game.skill_phase_finished()); game = game.resolve_phase_start(); assert!([Phase::Skill, Phase::Finish].contains(&game.phase)); return; } #[test] fn stun_test() { let mut game = create_test_game(); let x_player = game.players[0].clone(); let y_player = game.players[1].clone(); let x_construct = x_player.constructs[0].clone(); let y_construct = y_player.constructs[0].clone(); while game.construct_by_id(x_construct.id).unwrap().skill_on_cd(Skill::Stun).is_some() { game.construct_by_id(x_construct.id).unwrap().reduce_cooldowns(); } game.add_skill(x_player.id, x_construct.id, Some(y_construct.id), Skill::Stun).unwrap(); game.add_skill(y_player.id, y_construct.id, Some(x_construct.id), Skill::Attack).unwrap(); game.player_ready(x_player.id).unwrap(); game.player_ready(y_player.id).unwrap(); assert!(game.skill_phase_finished()); game = game.resolve_phase_start(); // should auto progress back to skill phase assert!(game.phase == Phase::Skill); // assert!(game.player_by_id(y_player.id).constructs[0].is_stunned()); // assert!(game.player_by_id(y_player.id).skills_required() == 0); } #[test] fn ko_resolution_test() { let mut game = create_test_game(); let x_player = game.players[0].clone(); let y_player = game.players[1].clone(); let x_construct = x_player.constructs[0].clone(); let y_construct = y_player.constructs[0].clone(); game.player_by_id(y_player.id).unwrap().construct_by_id(y_construct.id).unwrap().red_power.force(1000000000); game.player_by_id(y_player.id).unwrap().construct_by_id(y_construct.id).unwrap().speed.force(1000000000); while game.construct_by_id(x_construct.id).unwrap().skill_on_cd(Skill::Stun).is_some() { game.construct_by_id(x_construct.id).unwrap().reduce_cooldowns(); } // just in case // remove all mitigation game.player_by_id(x_player.id).unwrap().construct_by_id(x_construct.id).unwrap().red_life.force(0); game.add_skill(x_player.id, x_construct.id, Some(y_construct.id), Skill::Stun).unwrap(); game.add_skill(y_player.id, y_construct.id, Some(x_construct.id), Skill::Attack).unwrap(); game.player_ready(x_player.id).unwrap(); game.player_ready(y_player.id).unwrap(); assert!(game.skill_phase_finished()); game = game.resolve_phase_start(); assert!(!game.player_by_id(y_player.id).unwrap().constructs[0].is_stunned()); assert!(game.phase == Phase::Finish); } #[test] fn cooldown_test() { let mut game = create_test_game(); let x_player = game.players[0].clone(); let y_player = game.players[1].clone(); let x_construct = x_player.constructs[0].clone(); let y_construct = y_player.constructs[0].clone(); // should auto progress back to skill phase assert!(game.phase == Phase::Skill); assert!(game.player_by_id(y_player.id).unwrap().constructs[0].skill_on_cd(Skill::Block).is_none()); assert!(game.player_by_id(y_player.id).unwrap().constructs[0].skill_on_cd(Skill::Stun).is_some()); assert!(game.player_by_id(x_player.id).unwrap().constructs[0].skill_on_cd(Skill::Block).is_none()); game.add_skill(x_player.id, x_construct.id, Some(y_construct.id), Skill::Attack).unwrap(); game.add_skill(y_player.id, y_construct.id, Some(x_construct.id), Skill::Attack).unwrap(); game.player_ready(x_player.id).unwrap(); game.player_ready(y_player.id).unwrap(); game = game.resolve_phase_start(); // should auto progress back to skill phase assert!(game.phase == Phase::Skill); assert!(game.player_by_id(y_player.id).unwrap().constructs[0].skill_on_cd(Skill::Stun).is_some()); // second round // now we block and it should go back on cd // game.add_skill(x_player.id, x_construct.id, Some(y_construct.id), Skill::Stun).unwrap(); game.add_skill(y_player.id, y_construct.id, Some(x_construct.id), Skill::Attack).unwrap(); game.player_ready(x_player.id).unwrap(); game.player_ready(y_player.id).unwrap(); game = game.resolve_phase_start(); assert!(game.player_by_id(x_player.id).unwrap().constructs[0].skill_on_cd(Skill::Stun).is_none()); assert!(game.player_by_id(y_player.id).unwrap().constructs[0].skill_on_cd(Skill::Block).is_none()); } #[test] fn parry_test() { let mut game = create_test_game(); let x_player = game.players[0].clone(); let y_player = game.players[1].clone(); let x_construct = x_player.constructs[0].clone(); let y_construct = y_player.constructs[0].clone(); while game.construct_by_id(y_construct.id).unwrap().skill_on_cd(Skill::Stun).is_some() { game.construct_by_id(y_construct.id).unwrap().reduce_cooldowns(); } while game.construct_by_id(x_construct.id).unwrap().skill_on_cd(Skill::ParryI).is_some() { game.construct_by_id(x_construct.id).unwrap().reduce_cooldowns(); } game.add_skill(x_player.id, x_construct.id, None, Skill::ParryI).unwrap(); game.add_skill(y_player.id, y_construct.id, Some(x_construct.id), Skill::Stun).unwrap(); game.player_ready(x_player.id).unwrap(); game.player_ready(y_player.id).unwrap(); game = game.resolve_phase_start(); // should not be stunned because of parry assert!(game.player_by_id(x_player.id).unwrap().constructs[0].is_stunned() == false); // riposte assert_eq!(game.player_by_id(y_player.id).unwrap().constructs[0].green_life(), (1024 - x_construct.red_power().pct(Skill::RiposteI.multiplier()))); } #[test] fn corrupt_test() { let mut game = create_test_game(); let x_player = game.players[0].clone(); let y_player = game.players[1].clone(); let x_construct = x_player.constructs[0].clone(); let y_construct = y_player.constructs[0].clone(); game.construct_by_id(x_construct.id).unwrap().learn_mut(Skill::CorruptI); while game.construct_by_id(x_construct.id).unwrap().skill_on_cd(Skill::CorruptI).is_some() { game.construct_by_id(x_construct.id).unwrap().reduce_cooldowns(); } // apply buff game.add_skill(x_player.id, x_construct.id, None, Skill::CorruptI).unwrap(); game.player_ready(x_player.id).unwrap(); game.player_ready(y_player.id).unwrap(); game = game.resolve_phase_start(); assert!(game.construct_by_id(x_construct.id).unwrap().affected(Effect::Corrupt)); // attack and receive debuff game.add_skill(y_player.id, y_construct.id, Some(x_construct.id), Skill::Attack).unwrap(); game.player_ready(x_player.id).unwrap(); game.player_ready(y_player.id).unwrap(); game = game.resolve_phase_start(); assert!(game.construct_by_id(y_construct.id).unwrap().affected(Effect::Corruption)); } #[test] fn scatter_test() { let mut game = create_test_game(); let x_player = game.players[0].clone(); let y_player = game.players[1].clone(); let x_construct = x_player.constructs[0].clone(); let y_construct = y_player.constructs[0].clone(); game.construct_by_id(x_construct.id).unwrap().learn_mut(Skill::ScatterI); while game.construct_by_id(x_construct.id).unwrap().skill_on_cd(Skill::ScatterI).is_some() { game.construct_by_id(x_construct.id).unwrap().reduce_cooldowns(); } // apply buff game.add_skill(x_player.id, x_construct.id, Some(y_construct.id), Skill::ScatterI).unwrap(); game.player_ready(x_player.id).unwrap(); game.player_ready(y_player.id).unwrap(); game = game.resolve_phase_start(); assert!(game.construct_by_id(x_construct.id).unwrap().affected(Effect::Scatter)); let Resolution { source: _, target: _, event, stages: _ } = game.resolved.pop().unwrap(); match event { Event::Effect { effect, skill: _, duration: _, construct_effects: _ } => assert_eq!(effect, Effect::Scatter), _ => panic!("not siphon"), }; let Resolution { source: _, target: _, event, stages: _ } = game.resolved.pop().unwrap(); match event { Event::Recharge { red: _, blue: _, skill: _ } => (), _ => panic!("scatter result was not recharge"), } // attack and receive scatter hit game.add_skill(y_player.id, y_construct.id, Some(x_construct.id), Skill::Attack).unwrap(); game.player_ready(x_player.id).unwrap(); game.player_ready(y_player.id).unwrap(); game = game.resolve_phase_start(); let Resolution { source: _, target, event, stages: _ } = game.resolved.pop().unwrap(); assert_eq!(target.id, y_construct.id); match event { Event::Damage { amount, skill: _, mitigation: _, colour: _} => assert_eq!(amount, 256.pct(Skill::Attack.multiplier()) >> 1), _ => panic!("not damage scatter"), }; } // #[test] // fn hatred_test() { // let mut game = create_test_game(); // let x_player = game.players[0].clone(); // let y_player = game.players[1].clone(); // let x_construct = x_player.constructs[0].clone(); // let y_construct = y_player.constructs[0].clone(); // game.construct_by_id(x_construct.id).unwrap().learn_mut(Skill::Hostility); // while game.construct_by_id(x_construct.id).unwrap().skill_on_cd(Skill::Hostility).is_some() { // game.construct_by_id(x_construct.id).unwrap().reduce_cooldowns(); // } // // apply buff // game.add_skill(x_player.id, x_construct.id, Some(x_construct.id), Skill::Hostility).unwrap(); // game.player_ready(x_player.id).unwrap(); // game.player_ready(y_player.id).unwrap(); // game = game.resolve_phase_start(); // assert!(game.construct_by_id(x_construct.id).unwrap().affected(Effect::Hostility)); // // attack and receive debuff // game.add_skill(y_player.id, y_construct.id, Some(x_construct.id), Skill::TestAttack).unwrap(); // game.player_ready(x_player.id).unwrap(); // game.player_ready(y_player.id).unwrap(); // game = game.resolve_phase_start(); // info!("{:#?}", game); // assert!(game.construct_by_id(y_construct.id).unwrap().affected(Effect::Hatred)); // } #[test] fn aoe_test() { let mut game = create_2v2_test_game(); let i_player = game.players[0].clone(); let x_player = game.players[1].clone(); let i_construct = i_player.constructs[0].clone(); let j_construct = i_player.constructs[1].clone(); let x_construct = x_player.constructs[0].clone(); let y_construct = x_player.constructs[1].clone(); game.construct_by_id(x_construct.id).unwrap().learn_mut(Skill::RuinI); while game.construct_by_id(x_construct.id).unwrap().skill_on_cd(Skill::RuinI).is_some() { game.construct_by_id(x_construct.id).unwrap().reduce_cooldowns(); } game.add_skill(i_player.id, i_construct.id, Some(x_construct.id), Skill::Attack).unwrap(); game.add_skill(i_player.id, j_construct.id, Some(x_construct.id), Skill::Attack).unwrap(); game.add_skill(x_player.id, x_construct.id, Some(i_construct.id), Skill::RuinI).unwrap(); game.add_skill(x_player.id, y_construct.id, Some(i_construct.id), Skill::Attack).unwrap(); game.player_ready(i_player.id).unwrap(); game.player_ready(x_player.id).unwrap(); assert!(game.skill_phase_finished()); game = game.resolve_phase_start(); let ruins = game.resolved .into_iter() .filter(|r| { let Resolution { source, target: _, event, stages: _ } = r; match source.id == x_construct.id { true => match event { Event::Effect { effect, duration, skill: _, construct_effects: _ } => { assert!(*effect == Effect::Stun); assert!(*duration == 1); true }, Event::Skill { skill: _ } => false, _ => panic!("ruin result not effect {:?}", event), } false => false, } }) .count(); assert!(ruins == 2); } #[test] fn taunt_test() { let mut game = create_2v2_test_game(); let i_player = game.players[0].clone(); let x_player = game.players[1].clone(); let i_construct = i_player.constructs[0].clone(); let j_construct = i_player.constructs[1].clone(); let x_construct = x_player.constructs[0].clone(); let y_construct = x_player.constructs[1].clone(); game.construct_by_id(x_construct.id).unwrap().learn_mut(Skill::TauntI); while game.construct_by_id(x_construct.id).unwrap().skill_on_cd(Skill::TauntI).is_some() { game.construct_by_id(x_construct.id).unwrap().reduce_cooldowns(); } game.add_skill(i_player.id, i_construct.id, Some(x_construct.id), Skill::Attack).unwrap(); game.add_skill(i_player.id, j_construct.id, Some(x_construct.id), Skill::Attack).unwrap(); game.add_skill(x_player.id, x_construct.id, Some(i_construct.id), Skill::TauntI).unwrap(); game.add_skill(x_player.id, y_construct.id, Some(i_construct.id), Skill::Attack).unwrap(); game.player_ready(i_player.id).unwrap(); game.player_ready(x_player.id).unwrap(); game = game.resolve_phase_start(); assert!(game.resolved.len() == 5); while let Some(r) = game.resolved.pop() { let Resolution { source , target, event: _, stages: _ } = r; if [i_construct.id, j_construct.id].contains(&source.id) { assert!(target.id == x_construct.id); } } } #[test] fn ko_pve_test() { let mut game = create_2v2_test_game(); let i_player = game.players[0].clone(); let x_player = game.players[1].clone(); let i_construct = i_player.constructs[0].clone(); let j_construct = i_player.constructs[1].clone(); let x_construct = x_player.constructs[0].clone(); let y_construct = x_player.constructs[1].clone(); game.add_skill(i_player.id, i_construct.id, Some(x_construct.id), Skill::Attack).unwrap(); game.add_skill(i_player.id, j_construct.id, Some(x_construct.id), Skill::Attack).unwrap(); game.add_skill(x_player.id, x_construct.id, Some(i_construct.id), Skill::Attack).unwrap(); game.add_skill(x_player.id, y_construct.id, Some(i_construct.id), Skill::Attack).unwrap(); game.player_ready(i_player.id).unwrap(); game.player_ready(x_player.id).unwrap(); assert!(game.skill_phase_finished()); game = game.resolve_phase_start(); assert!([Phase::Skill, Phase::Finish].contains(&game.phase)); // kill a construct game.player_by_id(i_player.id).unwrap().construct_by_id(i_construct.id).unwrap().green_life.reduce(u64::max_value()); assert!(game.player_by_id(i_player.id).unwrap().skills_required() == 1); assert!(game.player_by_id(x_player.id).unwrap().skills_required() == 2); // add some more skills game.add_skill(i_player.id, j_construct.id, Some(x_construct.id), Skill::Attack).unwrap(); game.add_skill(x_player.id, x_construct.id, Some(j_construct.id), Skill::Attack).unwrap(); game.add_skill(x_player.id, y_construct.id, Some(j_construct.id), Skill::Attack).unwrap(); assert!(game.add_skill(x_player.id, x_construct.id, Some(i_construct.id), Skill::Attack).is_err()); game.player_ready(i_player.id).unwrap(); game.player_ready(x_player.id).unwrap(); assert!(game.skill_phase_finished()); game = game.resolve_phase_start(); assert!(game.player_by_id(i_player.id).unwrap().skills_required() == 1); assert!(game.player_by_id(x_player.id).unwrap().skills_required() == 2); return; } #[test] fn tick_removal_test() { let mut game = create_test_game(); let x_player = game.players[0].clone(); let y_player = game.players[1].clone(); let x_construct = x_player.constructs[0].clone(); let y_construct = y_player.constructs[0].clone(); // make the purify construct super fast so it beats out decay game.construct_by_id(y_construct.id).unwrap().speed.force(10000000); game.construct_by_id(x_construct.id).unwrap().learn_mut(Skill::DecayI); while game.construct_by_id(x_construct.id).unwrap().skill_on_cd(Skill::DecayI).is_some() { game.construct_by_id(x_construct.id).unwrap().reduce_cooldowns(); } game.construct_by_id(x_construct.id).unwrap().learn_mut(Skill::SiphonI); while game.construct_by_id(x_construct.id).unwrap().skill_on_cd(Skill::SiphonI).is_some() { game.construct_by_id(x_construct.id).unwrap().reduce_cooldowns(); } game.construct_by_id(y_construct.id).unwrap().learn_mut(Skill::PurifyI); while game.construct_by_id(y_construct.id).unwrap().skill_on_cd(Skill::PurifyI).is_some() { game.construct_by_id(y_construct.id).unwrap().reduce_cooldowns(); } // apply buff game.add_skill(x_player.id, x_construct.id, Some(y_construct.id), Skill::DecayI).unwrap(); game.player_ready(x_player.id).unwrap(); game.player_ready(y_player.id).unwrap(); game = game.resolve_phase_start(); assert!(game.construct_by_id(y_construct.id).unwrap().affected(Effect::Decay)); let Resolution { source: _, target: _, event, stages: _ } = game.resolved.pop().unwrap(); match event { Event::Damage { amount: _, skill, mitigation: _, colour: _ } => assert_eq!(skill, Skill::DecayTickI), _ => panic!("not decay"), }; game.resolved.clear(); // remove game.add_skill(y_player.id, y_construct.id, Some(y_construct.id), Skill::PurifyI).unwrap(); game.player_ready(x_player.id).unwrap(); game.player_ready(y_player.id).unwrap(); game = game.resolve_phase_start(); while let Some(Resolution { source: _, target: _, event, stages: _ }) = game.resolved.pop() { match event { Event::Damage { amount: _, skill: _, mitigation: _, colour: _ } => panic!("{:?} damage event", event), _ => (), } }; game.add_skill(y_player.id, x_construct.id, Some(y_construct.id), Skill::SiphonI).unwrap(); game.player_ready(x_player.id).unwrap(); game.player_ready(y_player.id).unwrap(); game = game.resolve_phase_start(); game.resolved.clear(); game.add_skill(y_player.id, y_construct.id, Some(y_construct.id), Skill::PurifyI).unwrap(); game.player_ready(x_player.id).unwrap(); game.player_ready(y_player.id).unwrap(); game = game.resolve_phase_start(); while let Some(Resolution { source: _, target: _, event, stages: _ }) = game.resolved.pop() { match event { Event::Damage { amount: _, skill: _, mitigation: _, colour: _ } => panic!("{:#?} {:#?} damage event", game.resolved, event), _ => (), } }; } #[test] fn upkeep_test() { let mut game = create_2v2_test_game(); game.players[0].set_ready(true); game.phase_end = Utc::now().checked_sub_signed(Duration::seconds(61)).unwrap(); game = game.upkeep(); assert!(game.players[1].warnings == 1); } }