use uuid::Uuid; use rand::prelude::*; // 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, GamePveParams, GamePvpParams, GameTargetParams, GameJoinParams}; use cryp::{Cryp, cryp_get}; use skill::{Skill, Cast}; #[derive(Debug,Clone,Serialize,Deserialize)] pub struct Team { id: Uuid, cryps: Vec, } impl Team { pub fn new(account: Uuid) -> Team { return Team { id: account, cryps: vec![], }; } fn skills_required(&self) -> usize { let required = self.cryps.iter().filter(|c| c.available_skills().len() > 0).collect::>().len(); println!("{:?} requires {:?} skills this turn", self.id, required); return required; } pub fn set_cryps(&mut self, cryps: Vec) -> &mut Team { self.cryps = cryps; self } pub fn cryp_by_id(&mut self, id: Uuid) -> Option<&mut Cryp> { self.cryps.iter_mut().find(|c| c.id == id) } } #[derive(Debug,Clone,Copy,PartialEq,Serialize,Deserialize)] pub enum Phase { Start, Skill, Target, Damage, Finish, } #[derive(Debug,Clone,Serialize,Deserialize)] pub struct Game { pub id: Uuid, pub team_size: usize, pub team_num: usize, pub teams: Vec, pub is_pve: bool, pub phase: Phase, pub skills: Vec, pub resolutions: Vec, pub log: Vec, } impl Game { fn new() -> Game { return Game { id: Uuid::new_v4(), team_size: 0, team_num: 0, teams: vec![], is_pve: true, phase: Phase::Start, skills: vec![], resolutions: vec![], log: vec![], }; } fn set_team_num(&mut self, size: usize) -> &mut Game { self.team_num = size; self } fn set_team_size(&mut self, size: usize) -> &mut Game { self.team_size = size; self } fn set_pve(&mut self, pve: bool) -> &mut Game { self.is_pve = pve; self } // check team not already in fn add_team(&mut self, team: Team) -> Result<&mut Game, Error> { if self.teams.len() == self.team_num { return Err(err_msg("maximum number of teams")); } self.teams.push(team); Ok(self) } // handle missing team properly fn team_by_id(&mut self, id: Uuid) -> &mut Team { match self.teams.iter_mut().find(|t| t.id == id) { Some(t) => t, None => panic!("id not in game {:?}", id), } } fn cryp_by_id(&mut self, id: Uuid) -> Option<&mut Cryp> { match self.teams.iter_mut().find(|t| t.cryps.iter().any(|c| c.id == id)) { Some(team) => { return team.cryps.iter_mut().find(|c| c.id == id); }, None => panic!("cryp not in game"), }; } fn update_cryp(&mut self, cryp: &mut Cryp) -> &mut Game { match self.teams.iter_mut().find(|t| t.cryps.iter().any(|c| c.id == cryp.id)) { Some(team) => { let index = team.cryps.iter().position(|t| t.id == cryp.id).unwrap(); team.cryps.remove(index); team.cryps.push(cryp.clone()); }, None => panic!("cryp not in game"), }; self } fn can_start(&self) -> bool { self.teams.len() == self.team_num } fn start(&mut self) -> &mut Game { self.skill_phase_start(); self } fn skill_phase_start(&mut self) -> &mut Game { if ![Phase::Start, Phase::Damage].contains(&self.phase) { panic!("game not in damage or start phase"); } self.phase = Phase::Skill; self.skills.clear(); if self.is_pve { self.pve_add_skills(); } self } fn pve_add_skills(&mut self) -> &mut Game { { let mob_team_id = Uuid::nil(); let mobs = self.team_by_id(mob_team_id).clone(); // TODO attack multiple players based on some criteria let player_team_id = self.teams.iter().find(|t| t.id != mob_team_id).unwrap().id; for mob in &mobs.cryps { // doesn't matter if the cryp can't cast self.add_skill(mob_team_id, mob.id, player_team_id, Skill::Attack).ok(); } } self } // skills can target any team, but we have to check if the caller is the owner of the cryp // and that the cryp has the skill they are trying to add fn add_skill(&mut self, team_id: Uuid, source_cryp_id: Uuid, target_team_id: Uuid, skill: Skill) -> Result { if self.phase != Phase::Skill { return Err(err_msg("game not in skill phase")); } { let cryp = match self.cryp_by_id(source_cryp_id) { Some(c) => c, None => return Err(err_msg("cryp not in team")), }; // check the cryp has the skill if !cryp.knows(skill) { return Err(err_msg("cryp does not have that skill")); } if cryp.skill_on_cd(skill).is_some() { return Err(err_msg("abiltity on cooldown")); } if !cryp.skill_can_cast(skill) { return Err(err_msg("cryp cannot cast spell")); } } // replace cryp skill if let Some(s) = self.skills.iter_mut().position(|s| s.source_cryp_id == source_cryp_id) { self.skills.remove(s); } let skill = Cast::new(source_cryp_id, team_id, target_team_id, skill); self.skills.push(skill); return Ok(skill.id); } fn skill_phase_finished(&self) -> bool { self.teams.iter() // for every team .all(|t| self.skills.iter() // the number of skills they have cast .filter(|s| s.target_team_id == t.id) .collect::>() // should equal the number required this turn .len() == t.skills_required() ) } // move all skills into their target team's targets list fn target_phase_start(&mut self) -> &mut Game { if self.phase != Phase::Skill { panic!("game not in skill phase"); } self.phase = Phase::Target; if self.is_pve { self.pve_add_targets(); } // all cryps are stunned or otherwise inactive if self.target_phase_finished() { self.damage_phase_start(); } self } fn pve_add_targets(&mut self) -> &mut Game { { let mob_team_id = Uuid::nil(); let mobs = self.team_by_id(mob_team_id).clone(); // TODO attack multiple players based on some criteria for incoming_skill_id in self.skills.clone().iter().filter(|s| s.target_team_id == mob_team_id).map(|s| s.id) { self.add_target(mob_team_id, mobs.cryps[0].id, incoming_skill_id).unwrap(); } } self } // targets can only be added by the owner of the team fn add_target(&mut self, team_id: Uuid, cryp_id: Uuid, cast_id: Uuid) -> Result<&mut Cast, Error> { { // whose team is this? let team = self.team_by_id(team_id); // is the target in the team? match team.cryp_by_id(cryp_id) { Some(c) => c, None => return Err(err_msg("cryp not in team")), }; } // set the target let cast = match self.skills.iter_mut().find(|s| s.id == cast_id) { Some(c) => c, None => return Err(err_msg("cast_id not found")), }; Ok(cast.set_target(cryp_id)) } fn target_phase_finished(&self) -> bool { self.skills.iter().all(|s| s.target_cryp_id.is_some()) } // requires no input // just do it fn damage_phase_start(&mut self) -> &mut Game { if self.phase != Phase::Target { panic!("game not in target phase"); } self.phase = Phase::Damage; self.resolve_skills(); if self.is_finished() { return self.finish() } self.skill_phase_start(); self } fn resolve_skills(&mut self) -> &mut Game { if self.phase != Phase::Damage { panic!("game not in damage phase"); } for skill in self.skills.clone().iter_mut() { let mut source = self.cryp_by_id(skill.source_cryp_id).unwrap().clone(); let mut target = self.cryp_by_id(skill.target_cryp_id.unwrap()).unwrap().clone(); let resolution = skill.resolve(&mut source, &mut target); self.update_cryp(&mut source); self.update_cryp(&mut target); } // now damage has all been assigned // handle cooldowns self.progress_cooldowns(); self } fn progress_cooldowns(&mut self) -> &mut Game { for skill in self.skills.clone().iter() { // copy the creep cause we will replace it let mut cryp = self.cryp_by_id(skill.source_cryp_id).unwrap().clone(); if skill.used_cooldown() { cryp.skill_set_cd(skill.skill); self.update_cryp(&mut cryp); continue; } cryp.decrease_cooldowns(); println!("{:?}", cryp); self.update_cryp(&mut cryp); } self } fn is_finished(&self) -> bool { self.teams.iter().any(|t| t.cryps.iter().all(|c| c.is_ko())) } fn finish(&mut self) -> &mut Game { self.phase = Phase::Finish; self.skills.clear(); self } } pub fn game_skill(params: GameSkillParams, tx: &mut Transaction, account: &Account) -> Result { let query = " SELECT * FROM games WHERE id = $1 "; let result = tx .query(query, &[¶ms.game_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 cryp let game_bytes: Vec = returned.get("data"); let mut game = from_slice::(&game_bytes)?; game.add_skill(account.id, params.cryp_id, params.target_team_id, params.skill)?; if game.skill_phase_finished() { game.target_phase_start(); } game_update(&game, tx)?; Ok(game) } pub fn game_target(params: GameTargetParams, tx: &mut Transaction, account: &Account) -> Result { let query = " SELECT * FROM games WHERE id = $1 "; let result = tx .query(query, &[¶ms.game_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 cryp let game_bytes: Vec = returned.get("data"); let mut game = from_slice::(&game_bytes)?; game.add_target(account.id, params.cryp_id, params.skill_id)?; if game.target_phase_finished() { game.damage_phase_start(); } game_update(&game, tx)?; Ok(game) } pub fn game_new(game: &Game, tx: &mut Transaction) -> Result<(), Error> { let game_bytes = to_vec(&game)?; let query = " INSERT INTO games (id, data) VALUES ($1, $2) RETURNING id; "; let result = tx .query(query, &[&game.id, &game_bytes])?; result.iter().next().ok_or(format_err!("no game written"))?; println!("{:?} 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 "; 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 cryp let game_bytes: Vec = returned.get("data"); let game = from_slice::(&game_bytes)?; return Ok(game); } /// write a row for every cryp in a team when added to a battle pub fn players_write(team: &Team, game_id: Uuid, tx: &mut Transaction) -> Result<(), Error> { // pve if !team.id.is_nil() { for cryp in &team.cryps { let id = Uuid::new_v4(); let query = " INSERT INTO players (id, game, cryp, account) VALUES ($1, $2, $3, $4) RETURNING id, account; "; let result = tx .query(query, &[&id, &game_id, &cryp.id, &team.id])?; let _returned = result.iter().next().expect("no row written"); println!("wrote player entry game:{:?} cryp:{:?} account:{:?}", game_id, cryp.id, team.id); } } return Ok(()); } pub fn game_update(game: &Game, tx: &mut Transaction) -> Result<(), Error> { let game_bytes = to_vec(&game)?; let query = " UPDATE games SET data = $1 WHERE id = $2 RETURNING id, data; "; let result = tx .query(query, &[&game_bytes, &game.id])?; result.iter().next().ok_or(format_err!("game {:?} could not be written", game))?; println!("{:?} wrote game", game.id); return Ok(()); } fn generate_mob(plr: &Cryp) -> Cryp { let mut rng = thread_rng(); // rng panics on min == max let mob_lvl: u8 = match plr.lvl { 1 => 1, _ => rng.gen_range(1, plr.lvl) }; return Cryp::new() .named(&"bamboo basher".to_string()) .level(mob_lvl) .create(); } pub fn game_pve(params: GamePveParams, tx: &mut Transaction, account: &Account) -> Result { let query = " SELECT * FROM cryps WHERE id = $1 AND account = $2; "; let result = tx .query(query, &[¶ms.id, &account.id])?; let returned = match result.iter().next() { Some(row) => row, None => return Err(err_msg("cryp not found")), }; // tells from_slice to cast into a cryp let cryp_bytes: Vec = returned.get("data"); let plr: Cryp = from_slice::(&cryp_bytes)?; // TEMP if plr.hp.value == 0 { return Err(err_msg("cryp is ko")); // plr.rez(); } let mob = generate_mob(&plr); let mut game = Game::new(); let game_id = game.id; game .set_team_num(2) .set_team_size(1); let mut plr_team = Team::new(account.id); plr_team .set_cryps(vec![plr]); let mut mob_team = Team::new(Uuid::nil()); mob_team .set_cryps(vec![mob]); game .add_team(plr_team)? .add_team(mob_team)?; game.start(); // persist game_new(&game, tx)?; players_write(&game.team_by_id(account.id), game_id, tx)?; Ok(game) } pub fn game_pvp(params: GamePvpParams, tx: &mut Transaction, account: &Account) -> Result { let cryps = params.cryp_ids .iter() .map(|id| cryp_get(tx, *id, account.id)) .collect::, Error>>()?; // create the game let mut game = Game::new(); let game_id = game.id; game .set_pve(false) .set_team_num(2) .set_team_size(1); // create the initiators team let mut team = Team::new(account.id); team.set_cryps(cryps); game.add_team(team)?; // persist game_new(&game, tx)?; players_write(&game.team_by_id(account.id), game_id, tx)?; Ok(game) } pub fn game_join(params: GameJoinParams, tx: &mut Transaction, account: &Account) -> Result { let mut game = game_get(tx, params.game_id)?; let game_id = game.id; let cryps = params.cryp_ids .iter() .map(|id| cryp_get(tx, *id, account.id)) .collect::, Error>>()?; let mut team = Team::new(account.id); team.set_cryps(cryps); game.add_team(team)?; if game.can_start() { game.start(); } game_update(&game, tx)?; players_write(&game.team_by_id(account.id), game_id, tx)?; Ok(game) } #[cfg(test)] mod tests { use game::*; use cryp::*; fn create_test_game() -> Game { let x = Cryp::new() .named(&"pronounced \"creeep\"".to_string()) .level(8) .learn(Skill::TestStun) .learn(Skill::TestTouch) .learn(Skill::Block) .create(); let y = Cryp::new() .named(&"lemongrass tea".to_string()) .level(8) .learn(Skill::TestStun) .learn(Skill::TestTouch) .learn(Skill::Block) .create(); let mut game = Game::new(); game .set_team_num(2) .set_team_size(1) .set_pve(false); let x_team_id = Uuid::new_v4(); let mut x_team = Team::new(x_team_id); x_team .set_cryps(vec![x]); let y_team_id = Uuid::new_v4(); let mut y_team = Team::new(y_team_id); y_team .set_cryps(vec![y]); game .add_team(x_team).unwrap() .add_team(y_team).unwrap(); assert!(game.can_start()); game.start(); return game; } #[test] fn phase_test() { let mut game = create_test_game(); let x_team = game.teams[0].clone(); let y_team = game.teams[1].clone(); let x_cryp = x_team.cryps[0].clone(); let y_cryp = y_team.cryps[0].clone(); let x_attack_id = game.add_skill(x_team.id, x_cryp.id, y_team.id, Skill::Attack).unwrap(); let y_attack_id = game.add_skill(y_team.id, y_cryp.id, x_team.id, Skill::Attack).unwrap(); assert!(game.skill_phase_finished()); game.target_phase_start(); game.add_target(x_team.id, x_cryp.id, y_attack_id).unwrap(); game.add_target(y_team.id, y_cryp.id, x_attack_id).unwrap(); assert!(game.target_phase_finished()); game.damage_phase_start(); assert!([Phase::Skill, Phase::Finish].contains(&game.phase)); return; } #[test] fn stun_test() { let mut game = create_test_game(); let x_team = game.teams[0].clone(); let y_team = game.teams[1].clone(); let x_cryp = x_team.cryps[0].clone(); let y_cryp = y_team.cryps[0].clone(); let x_stun_id = game.add_skill(x_team.id, x_cryp.id, y_team.id, Skill::TestStun).unwrap(); let y_attack_id = game.add_skill(y_team.id, y_cryp.id, x_team.id, Skill::TestTouch).unwrap(); assert!(game.skill_phase_finished()); game.target_phase_start(); game.add_target(x_team.id, x_cryp.id, y_attack_id).unwrap(); game.add_target(y_team.id, y_cryp.id, x_stun_id).unwrap(); assert!(game.target_phase_finished()); game.damage_phase_start(); // should auto progress back to skill phase assert!(game.phase == Phase::Skill); println!("{:#?}", game); assert!(game.team_by_id(y_team.id).cryps[0].is_stunned()); assert!(game.team_by_id(y_team.id).skills_required() == 0); } #[test] fn cooldown_test() { let mut game = create_test_game(); let x_team = game.teams[0].clone(); let y_team = game.teams[1].clone(); let x_cryp = x_team.cryps[0].clone(); let y_cryp = y_team.cryps[0].clone(); let x_stun_id = game.add_skill(x_team.id, x_cryp.id, y_team.id, Skill::TestTouch).unwrap(); let y_attack_id = game.add_skill(y_team.id, y_cryp.id, x_team.id, Skill::TestTouch).unwrap(); game.target_phase_start(); game.add_target(x_team.id, x_cryp.id, y_attack_id).unwrap(); game.add_target(y_team.id, y_cryp.id, x_stun_id).unwrap(); game.damage_phase_start(); // should auto progress back to skill phase assert!(game.phase == Phase::Skill); // after 1 turn block should be off cooldown assert!(game.team_by_id(y_team.id).cryps[0].skill_on_cd(Skill::Block).is_none()); // second round // now we block and it should go back on cd let x_block_id = game.add_skill(x_team.id, x_cryp.id, y_team.id, Skill::Block).unwrap(); let y_block_id = game.add_skill(y_team.id, y_cryp.id, x_team.id, Skill::Block).unwrap(); game.target_phase_start(); game.add_target(x_team.id, x_cryp.id, y_block_id).unwrap(); game.add_target(y_team.id, y_cryp.id, x_block_id).unwrap(); game.damage_phase_start(); assert!(game.team_by_id(y_team.id).cryps[0].skill_on_cd(Skill::Block).is_some()); println!("{:#?}", game); } }