mnml/server/src/instance.rs
2019-07-21 20:38:01 +10:00

923 lines
25 KiB
Rust

use uuid::Uuid;
use serde_cbor::{from_slice, to_vec};
use postgres::transaction::Transaction;
use failure::Error;
use failure::err_msg;
// timekeeping
use chrono::prelude::*;
use chrono::Duration;
use account::Account;
use player::{Player, player_create};
use construct::{Construct, construct_get};
use mob::{bot_player, instance_mobs};
use game::{Game, Phase, game_get, game_write};
use item::{Item};
use rpc::{RpcMessage};
use img;
#[derive(Debug,Clone,Copy,PartialEq,Serialize,Deserialize)]
enum InstancePhase {
Lobby,
InProgress,
Finished,
}
#[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 => 60,
TimeControl::Slow => 120,
TimeControl::Practice => panic!("practice game seconds called"),
}
}
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::minutes(5))
.expect("could not set phase end")
}
pub fn game_phase_end(&self, resolution_time_ms: i64) -> Option<DateTime<Utc>> {
match self {
TimeControl::Practice => None,
_ => 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>,
open: bool,
max_players: usize,
max_rounds: usize,
password: Option<String>,
time_control: TimeControl,
phase: InstancePhase,
phase_end: Option<DateTime<Utc>>,
phase_start: DateTime<Utc>,
winner: Option<Uuid>,
}
impl Instance {
fn new() -> Instance {
Instance {
id: Uuid::new_v4(),
players: vec![],
rounds: vec![],
phase: InstancePhase::Lobby,
open: true,
max_players: 2,
max_rounds: 5,
name: String::new(),
time_control: TimeControl::Standard,
password: None,
phase_start: Utc::now(),
phase_end: Some(TimeControl::Standard.lobby_timeout()),
winner: None,
}
}
pub fn global(player: Player) -> Instance {
Instance {
id: Uuid::nil(),
players: vec![player],
rounds: vec![],
phase: InstancePhase::InProgress,
open: false,
max_players: 0,
max_rounds: 5,
time_control: TimeControl::Standard,
name: "Global Matchmaking".to_string(),
password: None,
phase_start: Utc::now(),
phase_end: Some(TimeControl::Standard.lobby_timeout()),
winner: None,
}
}
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>>()
}
pub fn upkeep(mut self) -> (Instance, Vec<Game>) {
// time out lobbies that have been open too long
if self.phase == InstancePhase::Lobby && self.phase_timed_out() {
self.finish();
return (self, 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::<Vec<Game>>();
(self, new_games)
}
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)
}
fn set_time_control(mut self, tc: TimeControl) -> Instance {
self.time_control = tc;
self
}
fn set_max_rounds(mut self, rounds: usize) -> Instance {
self.max_rounds = rounds;
self
}
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)
}
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);
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.open = false;
self.next_round()
}
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();
self.players.iter_mut().for_each(|p| {
p.set_ready(false);
p.vbox.fill();
});
self.rounds.push(Round::new());
self.bot_round_actions();
self
}
fn finish_condition(&mut self) -> bool {
if self.rounds.len() < 4 {
return false;
}
// tennis
for player in self.players.iter() {
if player.wins >= 4 && player.wins >= player.losses + 2 {
self.winner = Some(player.id);
return true;
}
}
// both players afk
if self.players.iter().all(|p| p.wins == 0) {
return true;
}
return false;
// boN
// self.players.iter()
// .any(|p| p.wins as usize >= self.max_rounds / 2 + 1)
// || self.rounds.len() == self.max_rounds
}
pub fn finish(&mut self) -> &mut Instance {
self.phase = InstancePhase::Finished;
self
}
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
}
fn current_game_id(&self) -> Option<Uuid> {
if self.phase == InstancePhase::Lobby {
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;
}
fn game_finished(&mut self, game: &Game) -> Result<&mut Instance, Error> {
{
let current_round = self.rounds
.last_mut()
.expect("instance does not have any rounds");
if current_round.game_id.unwrap() != game.id {
return Err(err_msg("instance does not have a round for this game"));
}
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.finished,
None => true,
}
}
// PLAYER ACTIONS
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 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_discard(mut self, account: Uuid) -> Result<Instance, Error> {
self.vbox_action_allowed(account)?;
self.account_player(account)?
.vbox_discard()?;
Ok(self)
}
pub fn vbox_accept(mut self, account: Uuid, group: usize, index: usize) -> Result<Instance, Error> {
self.vbox_action_allowed(account)?;
self.account_player(account)?
.vbox_accept(group, index)?;
Ok(self)
}
pub fn vbox_combine(mut self, account: Uuid, indices: Vec<usize>) -> Result<Instance, Error> {
self.vbox_action_allowed(account)?;
self.account_player(account)?
.vbox_combine(indices)?;
Ok(self)
}
pub fn vbox_reclaim(mut self, account: Uuid, index: usize) -> Result<Instance, Error> {
self.vbox_action_allowed(account)?;
self.account_player(account)?
.vbox_reclaim(index)?;
Ok(self)
}
pub fn vbox_apply(mut self, account: Uuid, index: usize, construct_id: Uuid) -> Result<Instance, Error> {
self.vbox_action_allowed(account)?;
self.account_player(account)?
.vbox_apply(index, construct_id)?;
Ok(self)
}
pub fn vbox_unequip(mut self, account: Uuid, target: Item, construct_id: Uuid) -> Result<Instance, Error> {
self.vbox_action_allowed(account)?;
self.account_player(account)?
.vbox_unequip(target, construct_id)?;
Ok(self)
}
}
pub fn instance_create(tx: &mut Transaction, instance: Instance) -> Result<Instance, Error> {
let instance_bytes = to_vec(&instance)?;
let query = "
INSERT INTO instances (id, data, open)
VALUES ($1, $2, $3)
RETURNING id;
";
let result = tx
.query(query, &[&instance.id, &instance_bytes, &instance.open])?;
result.iter().next().ok_or(format_err!("no instances written"))?;
return Ok(instance);
}
pub fn instance_update(tx: &mut Transaction, instance: Instance) -> Result<Instance, Error> {
let instance_bytes = to_vec(&instance)?;
let query = "
UPDATE instances
SET data = $1, open = $2, finished = $3, upkeep = $4, updated_at = now()
WHERE id = $5
RETURNING id, data;
";
let result = tx
.query(query, &[&instance_bytes, &instance.open, &instance.finished(), &instance.phase_end, &instance.id])?;
result.iter().next().ok_or(err_msg("no instance row returned"))?;
// info!("{:?} wrote instance", instance.id);
if instance.finished() {
info!("instance finished state={:?}", instance);
}
return Ok(instance);
}
pub fn instance_get(tx: &mut Transaction, instance_id: Uuid) -> Result<Instance, Error> {
let query = "
SELECT *
FROM instances
WHERE id = $1
FOR UPDATE;
";
let result = tx
.query(query, &[&instance_id])?;
let returned = match result.iter().next() {
Some(row) => row,
None => return Err(err_msg("instance not found")),
};
let instance_bytes: Vec<u8> = returned.get("data");
let instance = from_slice::<Instance>(&instance_bytes)?;
return Ok(instance);
}
pub fn instance_delete(tx: &mut Transaction, id: Uuid) -> Result<(), Error> {
let query = "
DELETE
FROM instances
WHERE id = $1;
";
let result = tx
.execute(query, &[&id])?;
if result != 1 {
return Err(format_err!("unable to delete instance {:?}", id));
}
info!("instance deleted {:?}", id);
return Ok(());
}
pub fn instance_list(tx: &mut Transaction) -> Result<Vec<Instance>, Error> {
let query = "
SELECT data, id
FROM instances
WHERE open = true
AND finished = false;
";
let result = tx
.query(query, &[])?;
let mut list = vec![];
for row in result.into_iter() {
let bytes: Vec<u8> = row.get(0);
let id = row.get(1);
match from_slice::<Instance>(&bytes) {
Ok(i) => list.push(i),
Err(_e) => {
instance_delete(tx, id)?;
}
};
}
return Ok(list);
}
pub fn instances_need_upkeep(tx: &mut Transaction) -> Result<Vec<Instance>, Error> {
let query = "
SELECT data, id
FROM instances
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<u8> = row.get(0);
let id = row.get(1);
match from_slice::<Instance>(&bytes) {
Ok(i) => list.push(i),
Err(_e) => {
instance_delete(tx, id)?;
}
};
}
return Ok(list);
}
// timed out instances with no time control
pub fn instances_idle(tx: &mut Transaction) -> Result<Vec<Instance>, Error> {
let query = "
SELECT data, id
FROM instances
WHERE finished = false
AND updated_at < now() - interval '1 hour'
FOR UPDATE;
";
let result = tx
.query(query, &[])?;
let mut list = vec![];
for row in result.into_iter() {
let bytes: Vec<u8> = row.get(0);
let id = row.get(1);
match from_slice::<Instance>(&bytes) {
Ok(i) => list.push(i),
Err(_e) => {
instance_delete(tx, id)?;
}
};
}
return Ok(list);
}
pub fn instance_new(tx: &mut Transaction, account: &Account, construct_ids: Vec<Uuid>, name: String, pve: bool, _password: Option<String>) -> Result<Instance, Error> {
let mut instance = match pve {
true => {
let bot = bot_player();
let bot_id = bot.id;
// generate bot imgs only in the real world
for c in bot.constructs.iter() {
img::molecular_write(c.img)?;
}
let mut instance = Instance::new()
.set_time_control(TimeControl::Practice)
.set_max_rounds(10)
.set_name(name)?;
instance.add_player(bot)?;
instance.player_ready(bot_id)?;
instance.open = false;
instance
},
false => Instance::new()
.set_name(name)?
};
instance = instance_create(tx, instance)?;
instance_join(tx, account, instance.id, construct_ids)
}
pub fn instance_join(tx: &mut Transaction, account: &Account, instance_id: Uuid, construct_ids: Vec<Uuid>) -> Result<Instance, Error> {
let mut instance = instance_get(tx, instance_id)?;
let constructs = construct_ids
.iter()
.map(|id| construct_get(tx, *id, account.id))
.collect::<Result<Vec<Construct>, Error>>()?;
if constructs.len() != 3 {
return Err(format_err!("incorrect player size. ({:})", 3));
}
if instance.players.len() >= instance.max_players {
return Err(err_msg("game is full"));
}
let player = player_create(tx, Player::new(account.id, &account.name, constructs), instance.id, account)?;
instance.add_player(player)?;
instance_update(tx, instance)
}
pub fn instance_ready(tx: &mut Transaction, account: &Account, instance_id: Uuid) -> Result<RpcMessage, Error> {
let mut instance = instance_get(tx, instance_id)?;
let player_id = instance.account_player(account.id)?.id;
if let Some(game) = instance.player_ready(player_id)? {
game_write(tx, &game)?;
instance_update(tx, instance)?;
return Ok(RpcMessage::GameState(game));
}
Ok(RpcMessage::InstanceState(instance_update(tx, instance)?))
}
pub fn instance_state(tx: &mut Transaction, _account: &Account, instance_id: Uuid) -> Result<RpcMessage, Error> {
let instance = instance_get(tx, instance_id)?;
if let Some(game_id) = instance.current_game_id() {
let game = game_get(tx, game_id)?;
// return the game until it's finished
if game.phase != Phase::Finish {
return Ok(RpcMessage::GameState(game))
}
}
Ok(RpcMessage::InstanceState(instance))
}
pub fn instance_game_finished(tx: &mut Transaction, game: &Game, instance_id: Uuid) -> Result<(), Error> {
let mut instance = instance_get(tx, instance_id)?;
instance.game_finished(game)?;
// info!("{:?}", instance_get(tx, instance_id)?);
instance_update(tx, instance)?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn instance_pve_test() {
let mut instance = Instance::new();
let bot_player = bot_player();
let bot = bot_player.id;
instance.add_player(bot_player).unwrap();
let player_account = Uuid::new_v4();
let constructs = instance_mobs(player_account);
let player = Player::new(player_account, &"test".to_string(), constructs).set_bot(true);
instance.add_player(player).expect("could not add player");
assert_eq!(instance.phase, InstancePhase::Lobby);
instance.player_ready(player_account).unwrap();
instance.player_ready(bot).unwrap();
assert_eq!(instance.phase, InstancePhase::Finished);
// assert!(instance.players.iter().any(|p| p.wins as usize == instance.max_rounds / 2 + 1));
}
#[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, &"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, &"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, &"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, &"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, &"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_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));
// 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, &"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());
}
}