diff --git a/client/src/components/info.component.jsx b/client/src/components/info.component.jsx index 9d395a9c..6919ed49 100644 --- a/client/src/components/info.component.jsx +++ b/client/src/components/info.component.jsx @@ -153,15 +153,39 @@ function Info(args) { ); } + function playerRound(id) { + if (!instance.rounds.length) return null; + return instance.rounds[instance.rounds.length - 1].find(r => r.player_ids.includes(id)); + } + + function playerText(player) { + const round = playerRound(player.id); + if (!round) { + return player.ready + ? 'ready' + : ''; + } + + if (round.finished) return 'finished'; + + return player.ready + ? 'ready' + : ''; + } + function scoreBoard() { - const players = instance.players.map((p, i) => - - {p.name} - {p.score.wins} / {p.score.losses} - {p.ready ? 'ready' : ''} - - ); + const players = instance.players.map((p, i) => { + const pText = playerText(p); + console.log(pText); + return ( + + {p.name} + {p.score.wins} / {p.score.losses} + {pText} + + ); + }); return ( diff --git a/server/src/game.rs b/server/src/game.rs index dcb146a0..93e73327 100644 --- a/server/src/game.rs +++ b/server/src/game.rs @@ -26,16 +26,6 @@ pub enum Phase { Finish, } -#[derive(Debug,Clone,Copy,Serialize,Deserialize)] -pub enum GameMode { - Normal, - Pvp, - Zone3v2Attack, - Zone2v2Caster, - Zone3v3MeleeMiniboss, - Zone3v3HealerBoss, -} - #[derive(Debug,Clone,Serialize,Deserialize)] pub struct Game { pub id: Uuid, @@ -47,7 +37,6 @@ pub struct Game { pub resolved: Vec, pub log: Vec, pub instance: Option, - pub mode: GameMode, phase_start: DateTime, } @@ -63,7 +52,6 @@ impl Game { resolved: vec![], log: vec![], instance: None, - mode: GameMode::Normal, phase_start: Utc::now(), }; } @@ -83,16 +71,11 @@ impl Game { self } - pub fn set_mode(&mut self, mode: GameMode) -> &mut Game { - self.mode = mode; - self - } - pub fn joinable(&self) -> bool { self.can_start() } - pub fn player_add(&mut self, player: Player) -> Result<&mut Game, Error> { + 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")); } @@ -102,11 +85,12 @@ impl Game { } if player.cryps.iter().all(|c| c.skills.len() == 0) { - return Err(err_msg("your cryps have no skills")); + println!("WARNING: {:?} has no skills and has forfeited {:?}", player.name, self.id); + player.forfeit(); } let player_description = player.cryps.iter().map(|c| c.name.clone()).collect::>().join(", "); - self.log.push(format!("{:} has joined the game.", player_description)); + self.log.push(format!("{:} has joined the game. [{:?}]", player.name, player_description)); self.players.push(player); @@ -162,13 +146,19 @@ impl Game { self } - fn can_start(&self) -> bool { + pub fn can_start(&self) -> bool { return self.players.len() == self.player_num && self.players.iter().all(|t| t.cryps.len() == self.player_cryps) } pub fn start(mut self) -> Game { self.log.push("Game starting...".to_string()); + + // forfeit + if self.finished() { + return self.finish(); + } + self.skill_phase_start() } @@ -190,6 +180,7 @@ impl Game { self.phase = Phase::Skill; self.pve_add_skills(); + if self.skill_phase_finished() { return self.resolve_phase_start() } @@ -317,6 +308,10 @@ impl Game { } 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); @@ -571,10 +566,12 @@ impl Game { } pub fn upkeep(mut self) -> Game { - if self.phase != Phase::Skill { - panic!("{:?} game not in skill phase during upkeep", self); + if self.phase == Phase::Finish { + return self; } + println!("upkeep beginning: {:?} vs {:?}", self.players[0].name, self.players[1].name); + if !self.phase_timed_out() { return self; } @@ -583,8 +580,10 @@ impl Game { if !player.ready { player.set_ready(true); player.add_warning(); + println!("upkeep: {:?} warned", player.name); if player.warnings >= 3 { player.forfeit(); + println!("upkeep: {:?} forfeited", player.name); self.log.push(format!("{:?} forfeited.", player.name)); } } @@ -891,8 +890,7 @@ pub fn game_instance_new(tx: &mut Transaction, players: Vec, game_id: Uu game .set_player_num(2) .set_player_cryps(3) - .set_instance(instance_id) - .set_mode(GameMode::Pvp); + .set_instance(instance_id); // create the initiators player for player in players { diff --git a/server/src/instance.rs b/server/src/instance.rs index 6c732f1f..eca9ffb6 100644 --- a/server/src/instance.rs +++ b/server/src/instance.rs @@ -9,12 +9,16 @@ use failure::err_msg; use std::iter; +// timekeeping +use chrono::prelude::*; +use chrono::Duration; + use rpc::{InstanceLobbyParams, InstanceJoinParams, InstanceReadyParams, InstanceStateParams}; use account::Account; use player::{Player, player_create, player_get, player_global_update}; use cryp::{Cryp, cryp_get}; use mob::{instance_mobs}; -use game::{Game, Phase, game_get, game_write, game_instance_new, game_instance_join}; +use game::{Game, Phase, game_get, game_write, game_instance_new}; use vbox::{Var}; use rpc::{RpcResult}; use names::{name}; @@ -29,7 +33,7 @@ enum InstancePhase { #[derive(Debug,Clone,Serialize,Deserialize)] struct Round { player_ids: Vec, - game_id: Uuid, + game_id: Option, finished: bool, } @@ -41,8 +45,10 @@ pub struct Instance { rounds: Vec>, open: bool, max_players: usize, + max_rounds: usize, password: Option, pub name: String, + phase_start: DateTime, } impl Instance { @@ -54,8 +60,10 @@ impl Instance { phase: InstancePhase::Lobby, open: true, max_players: 2, + max_rounds: 16, name: String::new(), password: None, + phase_start: Utc::now(), } } @@ -67,13 +75,42 @@ impl Instance { phase: InstancePhase::InProgress, open: false, max_players: 0, + max_rounds: 1, name: "Global Matchmaking".to_string(), password: None, + phase_start: Utc::now(), } } - pub fn upkeep(mut self) -> Instance { - self + fn phase_timed_out(&self) -> bool { + Utc::now().signed_duration_since(self.phase_start).num_seconds() > 60 + } + + fn timed_out_players(&self) -> Vec { + self.players + .iter() + .filter(|p| !p.ready) + .filter(|p| self.current_game_id(p.id).is_none()) + .map(|p| p.id) + .collect::>() + } + + pub fn upkeep(mut self) -> (Instance, Vec) { + if self.phase != InstancePhase::InProgress { + return (self, vec![]); + } + + if !self.phase_timed_out() { + return (self, vec![]); + } + + let new_games = self + .timed_out_players() + .iter() + .filter_map(|p| self.player_ready(*p).unwrap()) + .collect::>(); + + (self, new_games) } fn set_max_players(mut self, max: usize) -> Result { @@ -94,6 +131,15 @@ impl Instance { Ok(self) } + fn set_max_rounds(mut self, rounds: usize) -> Result { + if rounds == 0 { + return Err(err_msg("max rounds must be nonzero")); + } + + self.max_rounds = rounds; + Ok(self) + } + fn add_bots(mut self) -> Instance { self.open = false; self.players = iter::repeat_with(|| { @@ -118,79 +164,98 @@ impl Instance { Ok(self) } - pub fn player_update(mut self, player: Player, ignore_phase: bool) -> Result { - if !ignore_phase && self.phase != InstancePhase::InProgress { - return Err(format_err!("instance not in vbox phase ({:?})", self.phase)); - } - - let i = self.players - .iter() - .position(|p| p.id == player.id) - .ok_or(err_msg("player_id not found"))?; - - self.players[i] = player; - - Ok(self) - } - - fn player_ready(&mut self, player_id: Uuid) -> Result<&mut Instance, Error> { + fn player_ready(&mut self, player_id: Uuid) -> Result, Error> { if ![InstancePhase::InProgress, InstancePhase::Lobby].contains(&self.phase) { return Err(err_msg("instance not in start or vbox phase")); } + // LOBBY CHECKS + if self.phase == InstancePhase::Lobby { + let i = self.players + .iter_mut() + .position(|p| p.id == player_id) + .ok_or(err_msg("player_id not found"))?; + + let v = !self.players[i].ready; + self.players[i].set_ready(v); + + match self.can_start() { + true => { + self.start(); + return Ok(None); + } + false => return Ok(None), + }; + } + + // GAME PHASE READY let i = self.players .iter_mut() .position(|p| p.id == player_id) .ok_or(err_msg("player_id not found"))?; - if self.phase != InstancePhase::Lobby && self.players[i].cryps.iter().all(|c| c.skills.len() == 0) { - return Err(err_msg("your cryps have no skills")); - } - let v = !self.players[i].ready; self.players[i].set_ready(v); - if self.phase == InstancePhase::Lobby && self.can_start() { - self.start(); + // start the game even if afk noobs have no skills + if !self.phase_timed_out() && self.players[i].cryps.iter().all(|c| c.skills.len() == 0) { + return Err(err_msg("your cryps have no skills")); } - Ok(self) + // create a game object if both players are ready + // this should only happen once + + let all_ready = self.round_ready_check(player_id); + + if !all_ready { + return Ok(None); + } + + let game = self.create_round_game(player_id); + + { + let round_num = self.rounds.len() - 1; + let current_round = self.rounds[round_num] + .iter_mut() + .find(|g| g.player_ids.contains(&player_id)) + .unwrap(); + + current_round.game_id = Some(game.id); + } + + return Ok(Some(game)); + } - fn player_has_pve_game(&self, player_id: Uuid) -> bool { - let opponent_id = self.current_round(player_id).player_ids - .iter() - .find(|p| **p != player_id) - .expect("unable to find opponent"); - - return self.players - .iter() - .find(|p| p.id == *opponent_id) - .expect("unable to find opponent") - .bot; - } - - fn bot_vs_player_game(&self, player_id: Uuid) -> Result { + fn round_ready_check(&mut self, player_id: Uuid) -> bool { let current_round = self.current_round(player_id); - let bot_id = current_round.player_ids.iter().find(|id| **id != player_id).unwrap(); - - let plr = self.players.clone().into_iter().find(|p| p.id == player_id).unwrap(); - let bot = self.players.clone().into_iter().find(|p| p.id == *bot_id).unwrap(); + self.players + .iter() + .filter(|p| current_round.player_ids.contains(&p.id)) + .all(|p| p.ready) + } + // maybe just embed the games in the instance + // but seems hella inefficient + fn create_round_game(&self, player_id: Uuid) -> Game { + let current_round = self.current_round(player_id); let mut game = Game::new(); - game.id = current_round.game_id; + game .set_player_num(2) .set_player_cryps(3) .set_instance(self.id); - game - .player_add(plr)? - .player_add(bot)?; + // create the initiators player + for player in self.players + .clone() + .into_iter() + .filter(|p| current_round.player_ids.contains(&p.id)) { + game.player_add(player).unwrap(); + } - game = game.start(); - - Ok(game) + assert!(game.can_start()); + return game.start(); } fn can_start(&self) -> bool { @@ -205,120 +270,47 @@ impl Instance { fn next_round(&mut self) -> &mut Instance { self.phase = InstancePhase::InProgress; + self.phase_start = Utc::now(); + + if self.rounds.len() >= self.max_rounds { + return self.finish(); + } self.players.iter_mut().for_each(|p| { - p.ready = false; + p.set_ready(false); p.vbox.fill(); }); self.generate_rounds(); - self.bot_vbox_phase(); - self.bot_games_phase(); + self.bot_round_actions(); self } - fn all_ready(&self) -> bool { - self.players.iter().all(|p| p.ready) + fn finish(&mut self) -> &mut Instance { + self.phase = InstancePhase::Finished; + self } - fn game_finished(&mut self, game: &Game) -> Result<&mut Instance, Error> { - let round_num = self.rounds.len() - 1; - self.rounds[round_num] - .iter_mut() - .find(|r| r.game_id == game.id) - .ok_or(err_msg("could not find matchup in current round"))? - .finished = true; - - // if you don't win, you lose - // ties can happen if both players forfeit - let winner_id = match game.winner() { - Some(w) => w.id, - None => Uuid::nil(), - }; - - for player in game.players.iter() { - let mut player = self.account_player(player.id)?; - match player.id == winner_id { - true => player.add_win(), - false => player.add_loss(), - }; - } - - if self.all_games_finished() { - self.next_round(); - } - - Ok(self) - } - - fn all_games_finished(&self) -> bool { - match self.rounds.last() { - Some(r) => r.iter().all(|g| g.finished), - None => true, - } - } - - fn bot_vbox_phase(&mut self) -> &mut Instance { + fn bot_round_actions(&mut self) -> &mut Instance { for bot in self.players.iter_mut().filter(|p| p.bot) { bot.vbox.fill(); bot.autobuy(); - bot.set_ready(true); } - self - } + let games = self.players + .clone() + .iter() + .filter(|b| b.bot) + .filter_map(|b| self.player_ready(b.id).unwrap()) + .collect::>(); - fn bot_games_phase(&mut self) -> &mut Instance { - if self.phase != InstancePhase::InProgress { - panic!("instance not in progress phase"); - } - - let r = self.rounds.len() - 1; - // println!("round num {:?}", r); - // println!("{:?}", self.rounds[r]); - for mut round in self.rounds[r].iter_mut() { - if self.players - .iter() - .filter(|p| round.player_ids.contains(&p.id) && p.bot && p.ready) - .count() == 2 { - // println!("should play a game between {:?}", round.player_ids); - let a = self.players.clone().into_iter().find(|p| p.id == round.player_ids[0]).unwrap(); - let b = self.players.clone().into_iter().find(|p| p.id == round.player_ids[1]).unwrap(); - - // println!("{:?} vs {:?}", a.name, b.name); - - let mut game = Game::new(); - game - .set_player_num(2) - .set_player_cryps(3); - - game - .player_add(a).unwrap() - .player_add(b).unwrap(); - - game = game.start(); - - if !game.finished() { - panic!("game not finished {:?}", game) - } - - let winner = match game.winner() { - Some(w) => w, - None => panic!("game has no winner {:?}", game), - }; - - round.finished = true; - - for player in game.players.iter() { - let mut player = self.players.iter_mut().find(|p| p.id == player.id).unwrap(); - match player.id == winner.id { - true => player.add_win(), - false => player.add_loss(), - }; - } - - } + for game in games { + if game.finished() { + self.game_finished(&game).unwrap(); + } else { + println!("{:?} unfishededes", game); + } } self @@ -345,7 +337,7 @@ impl Instance { .enumerate() .map(|(i, id)| Round { player_ids: vec![*id, matched_players[np - (i + 1)]], - game_id: Uuid::new_v4(), + game_id: None, finished: false, }) .collect::>(); @@ -366,24 +358,59 @@ impl Instance { current_round } - fn current_game(&self, player_id: Uuid) -> Option { + fn current_game_id(&self, player_id: Uuid) -> Option { if self.phase == InstancePhase::Lobby { return None; } let current_round = self.current_round(player_id); - let can_start = self.players - .iter() - .filter(|p| current_round.player_ids.contains(&p.id)) - .all(|p| p.ready); + if current_round.finished || current_round.game_id.is_none() { + return None; + } - match can_start { - true => match current_round.finished { - true => None, - false => Some(current_round.game_id), - }, - false => None, + return current_round.game_id; + } + + fn game_finished(&mut self, game: &Game) -> Result<&mut Instance, Error> { + let round_num = self.rounds.len() - 1; + self.rounds[round_num] + .iter_mut() + .filter(|r| r.game_id.is_some()) + .find(|r| r.game_id.unwrap() == game.id) + .ok_or(err_msg("could not find matchup in current round"))? + .finished = true; + + // if you don't win, you lose + // ties can happen if both players forfeit + let winner_id = match game.winner() { + Some(w) => w.id, + None => Uuid::nil(), + }; + + for player in game.players.iter() { + let mut player = self.account_player(player.id)?; + match player.id == winner_id { + true => player.add_win(), + false => player.add_loss(), + }; + } + + if self.all_games_finished() { + self.next_round(); + } + + Ok(self) + } + + fn all_ready(&self) -> bool { + self.players.iter().all(|p| p.ready) + } + + fn all_games_finished(&self) -> bool { + match self.rounds.last() { + Some(r) => r.iter().all(|g| g.finished), + None => true, } } @@ -399,7 +426,7 @@ impl Instance { if self.phase == InstancePhase::Lobby { return Err(err_msg("game not yet started")); } - if self.current_game(account).is_some() { + if self.current_game_id(account).is_some() { return Err(err_msg("you cannot perform vbox actions while in a game")); } @@ -552,8 +579,7 @@ pub fn instances_need_upkeep(tx: &mut Transaction) -> Result, Erro let query = " SELECT data, id FROM instances - WHERE updated_at < now() - interval '5 seconds' - AND id != '00000000-0000-0000-0000-000000000000'; + WHERE id != '00000000-0000-0000-0000-000000000000'; "; let result = tx @@ -636,32 +662,9 @@ pub fn instance_join(params: InstanceJoinParams, tx: &mut Transaction, account: pub fn instance_ready(params: InstanceReadyParams, tx: &mut Transaction, account: &Account) -> Result { let mut instance = instance_get(tx, params.instance_id)?; let player_id = instance.account_player(account.id)?.id; - instance.player_ready(player_id)?; - if let Some(game_id) = instance.current_game(player_id) { - match instance.player_has_pve_game(player_id) { - true => match game_get(tx, game_id) { - Ok(g) => g, - Err(_) => { - let game = instance.bot_vs_player_game(player_id)?; - game_write(tx, &game)?; - game - }, - }, - false => { - let opponent_id = *instance - .current_round(account.id) - .player_ids - .iter() - .find(|p| **p != account.id) - .expect("could not find opponent"); - - let a = instance.account_player(account.id)?.clone(); - let b = instance.account_player(opponent_id)?.clone(); - let players = vec![a, b]; - game_instance_new(tx, players, game_id, instance.id)? - } - }; + if let Some(game) = instance.player_ready(player_id)? { + game_write(tx, &game)?; } instance_update(tx, instance) @@ -670,11 +673,11 @@ pub fn instance_ready(params: InstanceReadyParams, tx: &mut Transaction, account pub fn instance_state(params: InstanceStateParams, tx: &mut Transaction, account: &Account) -> Result { let instance = instance_get(tx, params.instance_id)?; - if let Some(game_id) = instance.current_game(account.id) { + if let Some(game_id) = instance.current_game_id(account.id) { let game = game_get(tx, game_id)?; + // return the game until it's finished if game.phase != Phase::Finish { - return Ok(RpcResult::GameState(game)) } } @@ -716,6 +719,7 @@ mod tests { fn instance_pve_test() { let mut instance = Instance::new() .set_max_players(16).expect("unable to set max players") + .set_max_rounds(2).expect("max rounds failure") .add_bots(); let player_account = Uuid::new_v4(); @@ -728,21 +732,9 @@ mod tests { assert_eq!(instance.phase, InstancePhase::Lobby); instance.player_ready(player_id).unwrap(); - assert_eq!(instance.phase, InstancePhase::InProgress); + assert_eq!(instance.phase, InstancePhase::Finished); assert_eq!(instance.rounds[0].len(), 8); - - instance.player_ready(player_id).unwrap(); - - assert!(instance.all_games_finished()); - instance.next_round(); - - instance.player_ready(player_id).unwrap(); - - instance.next_round(); - - instance.player_ready(player_id).unwrap(); - - assert_eq!(instance.rounds.len(), 3); + assert_eq!(instance.rounds.len(), 2); } #[test] @@ -784,5 +776,55 @@ mod tests { assert_eq!(instance.phase, InstancePhase::InProgress); assert!(!instance.can_start()); + + instance.players[0].autobuy(); + instance.players[1].autobuy(); + + instance.player_ready(a_id).expect("a ready"); + let game = instance.player_ready(b_id).expect("b ready"); + + assert!(game.is_some()); + } + + #[test] + fn instance_upkeep_test() { + let mut instance = Instance::new() + .set_max_players(2) + .expect("could not create instance"); + + let player_account = Uuid::new_v4(); + let cryps = instance_mobs(player_account); + let player = Player::new(player_account, &"a".to_string(), cryps); + let a_id = player.id; + + instance.add_player(player).expect("could not add player"); + assert!(!instance.can_start()); + + let player_account = Uuid::new_v4(); + let cryps = instance_mobs(player_account); + let player = Player::new(player_account, &"b".to_string(), cryps); + let b_id = player.id; + instance.add_player(player).expect("could not add player"); + + instance.players[0].autobuy(); + + instance.player_ready(a_id).expect("a ready"); + instance.player_ready(b_id).expect("b ready"); + + instance.phase_start = Utc::now().checked_sub_signed(Duration::seconds(61)).unwrap(); + + let (mut instance, new_games) = instance.upkeep(); + + assert_eq!(new_games.len(), 1); + + let game = &new_games[0]; + assert!(game.finished()); + + instance.game_finished(game).unwrap(); + + assert_eq!(instance.rounds.len(), 2); + assert!(instance.players.iter().all(|p| !p.ready)); + + println!("{:#?}", instance); } } diff --git a/server/src/player.rs b/server/src/player.rs index 0cc2ec83..fbad6d60 100644 --- a/server/src/player.rs +++ b/server/src/player.rs @@ -73,14 +73,12 @@ impl Player { pub fn add_win(&mut self) -> &mut Player { self.score.wins += 1; - self.set_ready(false); self.vbox.balance_add(12); self } pub fn add_loss(&mut self) -> &mut Player { self.score.losses += 1; - self.set_ready(false); self.vbox.balance_add(9); self } diff --git a/server/src/warden.rs b/server/src/warden.rs index 4c6e08dd..cf92a9ea 100644 --- a/server/src/warden.rs +++ b/server/src/warden.rs @@ -1,25 +1,24 @@ -use std::time::{Duration}; -use std::thread::sleep; // Db Commons use postgres::transaction::Transaction; use failure::Error; -use failure::err_msg; -use r2d2::{Pool}; -use r2d2_postgres::{PostgresConnectionManager}; - -use game::{Game, games_need_upkeep, game_update}; -use instance::{Instance, instances_need_upkeep, instance_update}; +use game::{games_need_upkeep, game_update, game_write, game_delete}; +use instance::{instances_need_upkeep, instance_update}; use net::{Db}; fn fetch_games(mut tx: Transaction) -> Result { let games = games_need_upkeep(&mut tx)?; - println!("warden: {:?} games active", games.len()); - for mut game in games { - game_update(&mut tx, &game.upkeep())?; + let game = game.upkeep(); + match game_update(&mut tx, &game) { + Ok(_) => (), + Err(e) => { + println!("{:?}", e); + game_delete(&mut tx, game.id)?; + } + } } Ok(tx) @@ -29,18 +28,26 @@ fn fetch_instances(mut tx: Transaction) -> Result { let instances = instances_need_upkeep(&mut tx)?; for mut instance in instances { - instance_update(&mut tx, instance.upkeep())?; + let (instance, new_games) = instance.upkeep(); + println!("{:?} new games", new_games.len()); + for game in new_games { + game_write(&mut tx, &game)?; + } + instance_update(&mut tx, instance)?; } Ok(tx) } pub fn warden(db: Db) -> Result<(), Error> { + println!("upkeep beginning"); fetch_games(db.transaction()?)? .commit()?; fetch_instances(db.transaction()?)? .commit()?; + println!("upkeep done"); + Ok(()) } \ No newline at end of file