mnml/server/src/game.rs
2019-06-01 00:08:52 +10:00

1540 lines
51 KiB
Rust

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<Player>,
pub phase: Phase,
pub stack: Vec<Cast>,
pub resolved: Vec<Resolution>,
pub log: Vec<String>,
pub instance: Option<Uuid>,
phase_end: DateTime<Utc>,
phase_start: DateTime<Utc>,
}
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::<Vec<String>>().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<Construct> {
self.players.clone()
.into_iter()
.flat_map(
|t| t.constructs
.into_iter())
.collect::<Vec<Construct>>()
}
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("<Skill Phase>".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<Uuid>, 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::<Vec<&Cast>>()
// // 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("<Resolve Phase>".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<Uuid> {
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<Uuid> {
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::<Vec<Cast>>();
// 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<Cast>) -> &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<Game, Error> {
return game_get(tx, params.id)
}
pub fn game_get(tx: &mut Transaction, id: Uuid) -> Result<Game, Error> {
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<u8> = returned.get("data");
let game = from_slice::<Game>(&game_bytes)?;
return Ok(game);
}
pub fn games_need_upkeep(tx: &mut Transaction) -> Result<Vec<Game>, 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<u8> = row.get(0);
let id = row.get(1);
match from_slice::<Game>(&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<Game, Error> {
// 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<u8> = returned.get("data");
// let game = match from_slice::<Game>(&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<Game, Error> {
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<Game, Error> {
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<Uuid>, mode: GameMode, tx: &mut Transaction, account: &Account) -> Result<Game, Error> {
// 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::<Result<Vec<Construct>, 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<Game, Error> {
// 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<Player>, game_id: Uuid, instance_id: Uuid) -> Result<Game, Error> {
// 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<Game, Error> {
// 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);
}
}