629 lines
17 KiB
Rust
629 lines
17 KiB
Rust
|
|
use std::collections::{HashMap};
|
|
|
|
use uuid::Uuid;
|
|
|
|
use failure::Error;
|
|
use failure::err_msg;
|
|
|
|
// timekeeping
|
|
use chrono::prelude::*;
|
|
use chrono::Duration;
|
|
|
|
use player::{Player, Score};
|
|
use game::{Game};
|
|
use item::{Item};
|
|
use vbox;
|
|
|
|
|
|
#[derive(Debug,Clone,Copy,PartialEq,Serialize,Deserialize)]
|
|
enum InstancePhase {
|
|
Lobby,
|
|
InProgress,
|
|
Finished,
|
|
}
|
|
|
|
pub type ChatState = HashMap<Uuid, String>;
|
|
|
|
#[derive(Debug,Clone,Serialize,Deserialize)]
|
|
struct Round {
|
|
game_id: Option<Uuid>,
|
|
finished: bool,
|
|
}
|
|
|
|
impl Round {
|
|
fn new() -> Round {
|
|
Round { game_id: None, finished: false }
|
|
}
|
|
}
|
|
|
|
#[derive(Debug,Clone,Copy,Serialize,Deserialize)]
|
|
pub enum TimeControl {
|
|
Standard,
|
|
Slow,
|
|
Practice,
|
|
}
|
|
|
|
impl TimeControl {
|
|
fn vbox_time_seconds(&self) -> i64 {
|
|
match self {
|
|
TimeControl::Standard => 180,
|
|
TimeControl::Slow => 240,
|
|
TimeControl::Practice => panic!("practice vbox seconds called"),
|
|
}
|
|
}
|
|
|
|
fn game_time_seconds(&self) -> i64 {
|
|
match self {
|
|
TimeControl::Standard => 1,
|
|
TimeControl::Slow => 1,
|
|
TimeControl::Practice => 1,
|
|
}
|
|
}
|
|
|
|
pub fn vbox_phase_end(&self) -> Option<DateTime<Utc>> {
|
|
match self {
|
|
TimeControl::Practice => None,
|
|
_ => Some(Utc::now()
|
|
.checked_add_signed(Duration::seconds(self.vbox_time_seconds()))
|
|
.expect("could not set vbox phase end")),
|
|
}
|
|
}
|
|
|
|
pub fn lobby_timeout(&self) -> DateTime<Utc> {
|
|
Utc::now()
|
|
.checked_add_signed(Duration::seconds(15))
|
|
.expect("could not set phase end")
|
|
}
|
|
|
|
pub fn game_phase_end(&self, resolution_time_ms: i64) -> Option<DateTime<Utc>> {
|
|
Some(Utc::now()
|
|
.checked_add_signed(Duration::milliseconds(self.game_time_seconds() * 1000 + resolution_time_ms))
|
|
.expect("could not set game phase end"))
|
|
}
|
|
}
|
|
|
|
#[derive(Debug,Clone,Serialize,Deserialize)]
|
|
pub struct Instance {
|
|
pub id: Uuid,
|
|
pub name: String,
|
|
|
|
players: Vec<Player>,
|
|
rounds: Vec<Round>,
|
|
|
|
max_players: usize,
|
|
time_control: TimeControl,
|
|
|
|
phase: InstancePhase,
|
|
pub phase_end: Option<DateTime<Utc>>,
|
|
pub phase_start: DateTime<Utc>,
|
|
|
|
winner: Option<Uuid>,
|
|
}
|
|
|
|
impl Instance {
|
|
pub fn new() -> Instance {
|
|
Instance {
|
|
id: Uuid::new_v4(),
|
|
players: vec![],
|
|
rounds: vec![],
|
|
phase: InstancePhase::Lobby,
|
|
max_players: 2,
|
|
name: String::new(),
|
|
time_control: TimeControl::Standard,
|
|
phase_start: Utc::now(),
|
|
phase_end: Some(TimeControl::Standard.lobby_timeout()),
|
|
winner: None,
|
|
}
|
|
}
|
|
|
|
pub fn redact(mut self, account: Uuid) -> Instance {
|
|
self.players = self.players.into_iter()
|
|
.map(|p| p.redact(account))
|
|
.collect();
|
|
|
|
self
|
|
}
|
|
|
|
fn phase_timed_out(&self) -> bool {
|
|
match self.phase_end {
|
|
Some(t) => Utc::now().signed_duration_since(t).num_milliseconds() > 0,
|
|
None => false,
|
|
}
|
|
}
|
|
|
|
fn timed_out_players(&self) -> Vec<Uuid> {
|
|
self.players
|
|
.iter()
|
|
.filter(|p| !p.ready)
|
|
.map(|p| p.id)
|
|
.collect::<Vec<Uuid>>()
|
|
}
|
|
|
|
// time out lobbies that have been open too long
|
|
pub fn upkeep(mut self) -> (Instance, Option<Game>) {
|
|
if self.phase == InstancePhase::Lobby && self.phase_timed_out() {
|
|
self.finish();
|
|
return (self, None);
|
|
}
|
|
|
|
if self.phase != InstancePhase::InProgress {
|
|
return (self, None);
|
|
}
|
|
|
|
if !self.phase_timed_out() {
|
|
return (self, None);
|
|
}
|
|
|
|
let new_game = self
|
|
.timed_out_players()
|
|
.iter()
|
|
.filter_map(|p| self.player_ready(*p).unwrap())
|
|
.collect::<Vec<Game>>()
|
|
.into_iter()
|
|
.next();
|
|
|
|
(self, new_game)
|
|
}
|
|
|
|
pub fn set_name(mut self, name: String) -> Result<Instance, Error> {
|
|
if name.len() == 0 {
|
|
return Err(err_msg("name must have a length"));
|
|
}
|
|
|
|
self.name = name;
|
|
Ok(self)
|
|
}
|
|
|
|
pub fn set_time_control(mut self, tc: TimeControl) -> Instance {
|
|
self.time_control = tc;
|
|
self
|
|
}
|
|
|
|
pub fn add_player(&mut self, player: Player) -> Result<&mut Instance, Error> {
|
|
if self.players.len() >= self.max_players {
|
|
return Err(err_msg("game full"))
|
|
}
|
|
|
|
match self.players.iter().find(|p| p.id == player.id) {
|
|
Some(_p) => return Err(err_msg("already joined")),
|
|
None => (),
|
|
};
|
|
|
|
self.players.push(player);
|
|
Ok(self)
|
|
}
|
|
|
|
pub fn player_ready(&mut self, player_id: Uuid) -> Result<Option<Game>, 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"))?;
|
|
|
|
let v = !self.players[i].ready;
|
|
self.players[i].set_ready(v);
|
|
|
|
// start the game even if afk noobs have no skills
|
|
if !self.phase_timed_out() && self.players[i].constructs.iter().all(|c| c.skills.len() == 0) {
|
|
return Err(err_msg("your constructs have no skills"));
|
|
}
|
|
|
|
// create a game object if both players are ready
|
|
// this should only happen once
|
|
|
|
let all_ready = self.round_ready_check();
|
|
|
|
if !all_ready {
|
|
return Ok(None);
|
|
}
|
|
|
|
let game = self.create_round_game();
|
|
|
|
let current_round = self.rounds
|
|
.last_mut()
|
|
.expect("instance does not have any rounds");
|
|
|
|
current_round.game_id = Some(game.id);
|
|
|
|
return Ok(Some(game));
|
|
|
|
}
|
|
|
|
fn round_ready_check(&mut self) -> bool {
|
|
self.players
|
|
.iter()
|
|
.all(|p| p.ready)
|
|
}
|
|
|
|
// maybe just embed the games in the instance
|
|
// but seems hella inefficient
|
|
fn create_round_game(&mut self) -> Game {
|
|
let current_round = self.rounds
|
|
.last_mut()
|
|
.expect("instance does not have any rounds");
|
|
|
|
|
|
let mut game = Game::new();
|
|
current_round.game_id = Some(game.id);
|
|
|
|
// disable upkeep until players finish their game
|
|
self.phase_end = None;
|
|
|
|
game
|
|
.set_player_num(2)
|
|
.set_player_constructs(3)
|
|
.set_time_control(self.time_control)
|
|
.set_instance(self.id);
|
|
|
|
for player in self.players.clone().into_iter() {
|
|
game.player_add(player).unwrap();
|
|
}
|
|
|
|
assert!(game.can_start());
|
|
return game.start();
|
|
}
|
|
|
|
fn can_start(&self) -> bool {
|
|
self.players.len() == self.max_players && self.all_ready()
|
|
}
|
|
|
|
fn start(&mut self) -> &mut Instance {
|
|
// self.players.sort_unstable_by_key(|p| p.id);
|
|
self.next_round()
|
|
}
|
|
|
|
pub fn next_round(&mut self) -> &mut Instance {
|
|
if self.finish_condition() {
|
|
return self.finish();
|
|
}
|
|
|
|
self.phase = InstancePhase::InProgress;
|
|
self.phase_start = Utc::now();
|
|
self.phase_end = self.time_control.vbox_phase_end();
|
|
|
|
let bits = match self.rounds.len() > 0 {
|
|
true => 30,
|
|
false => 0,
|
|
};
|
|
|
|
self.players.iter_mut().for_each(|p| {
|
|
p.vbox.balance_add(bits);
|
|
p.set_ready(false);
|
|
p.vbox.fill();
|
|
});
|
|
|
|
self.rounds.push(Round::new());
|
|
self.bot_round_actions();
|
|
|
|
self
|
|
}
|
|
|
|
fn finish_condition(&mut self) -> bool {
|
|
self.players.iter().any(|p| p.score == Score::Win)
|
|
}
|
|
|
|
pub fn finish(&mut self) -> &mut Instance {
|
|
self.phase = InstancePhase::Finished;
|
|
|
|
for player in self.players.iter() {
|
|
if player.score == Score::Win {
|
|
self.winner = Some(player.id);
|
|
}
|
|
}
|
|
|
|
self
|
|
}
|
|
|
|
pub fn finished(&self) -> bool {
|
|
self.phase == InstancePhase::Finished
|
|
}
|
|
|
|
fn bot_round_actions(&mut self) -> &mut Instance {
|
|
for bot in self.players.iter_mut().filter(|p| p.bot) {
|
|
bot.vbox.fill();
|
|
bot.autobuy();
|
|
}
|
|
|
|
let games = self.players
|
|
.clone()
|
|
.iter()
|
|
.filter(|b| b.bot)
|
|
.filter_map(|b| self.player_ready(b.id).unwrap())
|
|
.collect::<Vec<Game>>();
|
|
|
|
for game in games {
|
|
if game.finished() {
|
|
self.game_finished(&game).unwrap();
|
|
} else {
|
|
info!("{:?} unfishededes", game);
|
|
}
|
|
}
|
|
|
|
self
|
|
}
|
|
|
|
pub fn current_game_id(&self) -> Option<Uuid> {
|
|
if self.phase != InstancePhase::InProgress {
|
|
return None;
|
|
}
|
|
|
|
let current_round = self.rounds
|
|
.last()
|
|
.expect("instance does not have any rounds");
|
|
|
|
if current_round.finished || current_round.game_id.is_none() {
|
|
return None;
|
|
}
|
|
|
|
return current_round.game_id;
|
|
}
|
|
|
|
pub fn game_finished(&mut self, game: &Game) -> Result<&mut Instance, Error> {
|
|
{
|
|
let current_round = self.rounds
|
|
.iter_mut()
|
|
.filter(|r| r.game_id.is_some())
|
|
.find(|r| r.game_id.unwrap() == game.id);
|
|
|
|
match current_round {
|
|
Some(c) => c.finished = true,
|
|
None => return Err(err_msg("instance does not have a round for this game")),
|
|
};
|
|
}
|
|
|
|
// if you don't win, you lose
|
|
// ties can happen if both players agree to a draw
|
|
// or ticks fire and knock everybody out
|
|
if let Some(winner) = game.winner() {
|
|
let winner = self.players.iter_mut()
|
|
.find(|p| p.id == winner.id)
|
|
.unwrap();
|
|
winner.score = winner.score.add_win(&Score::Zero);
|
|
};
|
|
|
|
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.finished,
|
|
None => true,
|
|
}
|
|
}
|
|
|
|
// PLAYER ACTIONS
|
|
pub fn account_player(&mut self, account: Uuid) -> Result<&mut Player, Error> {
|
|
self.players
|
|
.iter_mut()
|
|
.find(|p| p.id == account)
|
|
.ok_or(err_msg("account not in instance"))
|
|
}
|
|
|
|
pub fn account_opponent(&mut self, account: Uuid) -> Result<&mut Player, Error> {
|
|
self.players
|
|
.iter_mut()
|
|
.find(|p| p.id != account)
|
|
.ok_or(err_msg("opponent not in instance"))
|
|
}
|
|
|
|
pub fn vbox_action_allowed(&self, account: Uuid) -> Result<(), Error> {
|
|
if self.players.iter().find(|p| p.id == account).is_none() {
|
|
return Err(err_msg("player not in this instance"));
|
|
}
|
|
|
|
if self.phase == InstancePhase::Lobby {
|
|
return Err(err_msg("game not yet started"));
|
|
}
|
|
|
|
if self.current_game_id().is_some() {
|
|
return Err(err_msg("you cannot perform vbox actions while in a game"));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub fn vbox_refill(mut self, account: Uuid) -> Result<Instance, Error> {
|
|
self.vbox_action_allowed(account)?;
|
|
self.account_player(account)?
|
|
.vbox_refill()?;
|
|
Ok(self)
|
|
}
|
|
|
|
pub fn vbox_buy(mut self, account: Uuid, group: vbox::ItemType, index: String, construct_id: Option<Uuid>) -> Result<Instance, Error> {
|
|
self.vbox_action_allowed(account)?;
|
|
self.account_player(account)?
|
|
.vbox_buy(group, index, construct_id)?;
|
|
Ok(self)
|
|
}
|
|
|
|
pub fn vbox_combine(mut self, account: Uuid, inv_indices: Vec<String>, vbox_indices: vbox::VboxIndices) -> Result<Instance, Error> {
|
|
self.vbox_action_allowed(account)?;
|
|
self.account_player(account)?
|
|
.vbox_combine(inv_indices, vbox_indices)?;
|
|
Ok(self)
|
|
}
|
|
|
|
pub fn vbox_refund(mut self, account: Uuid, index: String) -> Result<Instance, Error> {
|
|
self.vbox_action_allowed(account)?;
|
|
self.account_player(account)?
|
|
.vbox_refund(index)?;
|
|
Ok(self)
|
|
}
|
|
|
|
pub fn vbox_apply(mut self, account: Uuid, index: String, construct_id: Uuid) -> Result<Instance, Error> {
|
|
self.vbox_action_allowed(account)?;
|
|
self.account_player(account)?
|
|
.vbox_equip(index, construct_id)?;
|
|
Ok(self)
|
|
}
|
|
|
|
pub fn vbox_unequip(mut self, account: Uuid, target: Item, construct_id: Uuid, target_construct: Option<Uuid>) -> Result<Instance, Error> {
|
|
self.vbox_action_allowed(account)?;
|
|
self.account_player(account)?
|
|
.vbox_unequip(target, construct_id, target_construct)?;
|
|
Ok(self)
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use mob::{bot_player, instance_mobs};
|
|
|
|
#[test]
|
|
fn instance_pve_test() {
|
|
let mut instance = Instance::new();
|
|
|
|
let bot = bot_player();
|
|
let bot_one = bot.id;
|
|
instance.add_player(bot).unwrap();
|
|
|
|
let bot = bot_player();
|
|
let bot_two = bot.id;
|
|
instance.add_player(bot).unwrap();
|
|
|
|
assert_eq!(instance.phase, InstancePhase::Lobby);
|
|
instance.player_ready(bot_one).unwrap();
|
|
instance.player_ready(bot_two).unwrap();
|
|
|
|
assert_eq!(instance.phase, InstancePhase::Finished);
|
|
}
|
|
|
|
#[test]
|
|
fn instance_bot_vbox_test() {
|
|
let _instance = Instance::new();
|
|
let player_account = Uuid::new_v4();
|
|
let constructs = instance_mobs(player_account);
|
|
let _player = Player::new(player_account, None, &"test".to_string(), constructs).set_bot(true);
|
|
}
|
|
|
|
#[test]
|
|
fn instance_start_test() {
|
|
let mut instance = Instance::new();
|
|
|
|
assert_eq!(instance.max_players, 2);
|
|
|
|
let player_account = Uuid::new_v4();
|
|
let constructs = instance_mobs(player_account);
|
|
let player = Player::new(player_account, None, &"a".to_string(), constructs);
|
|
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 constructs = instance_mobs(player_account);
|
|
let player = Player::new(player_account, None, &"b".to_string(), constructs);
|
|
let b_id = player.id;
|
|
|
|
instance.add_player(player).expect("could not add player");
|
|
|
|
assert_eq!(instance.phase, InstancePhase::Lobby);
|
|
instance.player_ready(a_id).expect("a ready");
|
|
assert!(!instance.can_start());
|
|
|
|
instance.player_ready(b_id).expect("b ready");
|
|
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();
|
|
|
|
let player_account = Uuid::new_v4();
|
|
let constructs = instance_mobs(player_account);
|
|
let player = Player::new(player_account, None, &"a".to_string(), constructs);
|
|
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 constructs = instance_mobs(player_account);
|
|
let player = Player::new(player_account, None, &"b".to_string(), constructs);
|
|
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_end = Some(Utc::now().checked_sub_signed(Duration::seconds(500)).unwrap());
|
|
|
|
let (mut instance, new_games) = instance.upkeep();
|
|
|
|
assert!(new_games.is_some());
|
|
|
|
let game = new_games.unwrap();
|
|
assert!(game.finished());
|
|
|
|
instance.game_finished(&game).unwrap();
|
|
|
|
assert_eq!(instance.rounds.len(), 2);
|
|
assert!(instance.players.iter().all(|p| !p.ready));
|
|
|
|
// info!("{:#?}", instance);
|
|
}
|
|
|
|
#[test]
|
|
fn instance_upkeep_idle_lobby_test() {
|
|
let mut instance = Instance::new();
|
|
|
|
let player_account = Uuid::new_v4();
|
|
let constructs = instance_mobs(player_account);
|
|
let player = Player::new(player_account, None, &"a".to_string(), constructs);
|
|
let _a_id = player.id;
|
|
|
|
instance.add_player(player).expect("could not add player");
|
|
assert!(!instance.can_start());
|
|
|
|
instance.phase_end = Some(Utc::now().checked_sub_signed(Duration::minutes(61)).unwrap());
|
|
let (instance, _new_games) = instance.upkeep();
|
|
|
|
assert!(instance.finished());
|
|
}
|
|
}
|