Merge branch 'core' of ssh://git.mnml.gg:40022/~/mnml into core

This commit is contained in:
Mashy 2019-12-05 17:26:38 +10:00
commit da9736256e
29 changed files with 876 additions and 9988 deletions

View File

@ -1,6 +1,8 @@
# FIXME # FIXME
game ready not auto starting resolve phase
remove immunity remove immunity
banish dmg
aoe event aoe event
cooldown events leak skills cooldown events leak skills
hit event hit event

View File

@ -57,9 +57,9 @@ pub struct Game {
pub stack: Vec<Cast>, pub stack: Vec<Cast>,
pub events: Vec<Vec<Event>>, pub events: Vec<Vec<Event>>,
pub instance: Option<Uuid>, pub instance: Option<Uuid>,
time_control: TimeControl, pub time_control: TimeControl,
phase_start: DateTime<Utc>, pub phase_start: DateTime<Utc>,
phase_end: Option<DateTime<Utc>>, pub phase_end: Option<DateTime<Utc>>,
} }
impl Game { impl Game {
@ -285,7 +285,7 @@ impl Game {
self self
} }
fn add_skill(&mut self, player_id: Uuid, source: Uuid, target: Uuid, skill: Skill) -> Result<&mut Game, Error> { pub fn add_skill(&mut self, player_id: Uuid, source: Uuid, target: Uuid, skill: Skill) -> Result<&mut Game, Error> {
// check player in game // check player in game
self.player_by_id(player_id)?; self.player_by_id(player_id)?;
@ -343,7 +343,7 @@ impl Game {
return Ok(self); return Ok(self);
} }
fn offer_draw(mut self, player_id: Uuid) -> Result<Game, Error> { pub fn offer_draw(mut self, player_id: Uuid) -> Result<Game, Error> {
if self.phase != Phase::Skill { if self.phase != Phase::Skill {
return Err(err_msg("game not in skill phase")); return Err(err_msg("game not in skill phase"));
} }
@ -367,7 +367,7 @@ impl Game {
return Ok(self); return Ok(self);
} }
fn concede(mut self, player_id: Uuid) -> Result<Game, Error> { pub fn concede(mut self, player_id: Uuid) -> Result<Game, Error> {
if self.phase != Phase::Skill { if self.phase != Phase::Skill {
return Err(err_msg("game not in skill phase")); return Err(err_msg("game not in skill phase"));
} }
@ -379,7 +379,7 @@ impl Game {
} }
fn clear_skill(&mut self, player_id: Uuid) -> Result<&mut Game, Error> { pub fn clear_skill(&mut self, player_id: Uuid) -> Result<&mut Game, Error> {
self.player_by_id(player_id)?; self.player_by_id(player_id)?;
if self.phase != Phase::Skill { if self.phase != Phase::Skill {
return Err(err_msg("game not in skill phase")); return Err(err_msg("game not in skill phase"));
@ -390,7 +390,7 @@ impl Game {
return Ok(self); return Ok(self);
} }
fn player_ready(&mut self, player_id: Uuid) -> Result<&mut Game, Error> { pub fn player_ready(&mut self, player_id: Uuid) -> Result<&mut Game, Error> {
if self.phase != Phase::Skill { if self.phase != Phase::Skill {
return Err(err_msg("game not in skill phase")); return Err(err_msg("game not in skill phase"));
} }
@ -401,7 +401,7 @@ impl Game {
Ok(self) Ok(self)
} }
fn skill_phase_finished(&self) -> bool { pub fn skill_phase_finished(&self) -> bool {
self.players.iter().all(|t| t.ready) self.players.iter().all(|t| t.ready)
// self.players.iter() // self.players.iter()
// // for every player // // for every player
@ -413,7 +413,7 @@ impl Game {
// ) // )
} }
fn resolve_phase_start(mut self) -> Game { pub fn resolve_phase_start(mut self) -> Game {
if self.phase != Phase::Skill { if self.phase != Phase::Skill {
panic!("game not in skill phase"); panic!("game not in skill phase");
} }
@ -722,11 +722,11 @@ mod tests {
let x_player_id = Uuid::new_v4(); let x_player_id = Uuid::new_v4();
x.account = x_player_id; x.account = x_player_id;
let x_player = Player::new(x_player_id, &"ntr".to_string(), vec![x]); let x_player = Player::new(x_player_id, None, &"ntr".to_string(), vec![x]);
let y_player_id = Uuid::new_v4(); let y_player_id = Uuid::new_v4();
y.account = y_player_id; y.account = y_player_id;
let y_player = Player::new(y_player_id, &"mash".to_string(), vec![y]); let y_player = Player::new(y_player_id, None, &"mash".to_string(), vec![y]);
game game
.player_add(x_player).unwrap() .player_add(x_player).unwrap()
@ -767,12 +767,12 @@ mod tests {
let i_player_id = Uuid::new_v4(); let i_player_id = Uuid::new_v4();
i.account = i_player_id; i.account = i_player_id;
j.account = i_player_id; j.account = i_player_id;
let i_player = Player::new(i_player_id, &"ntr".to_string(), vec![i, j]); let i_player = Player::new(i_player_id, None, &"ntr".to_string(), vec![i, j]);
let x_player_id = Uuid::new_v4(); let x_player_id = Uuid::new_v4();
x.account = x_player_id; x.account = x_player_id;
y.account = x_player_id; y.account = x_player_id;
let x_player = Player::new(x_player_id, &"mashy".to_string(), vec![x, y]); let x_player = Player::new(x_player_id, None, &"mashy".to_string(), vec![x, y]);
game game
.player_add(i_player).unwrap() .player_add(i_player).unwrap()

View File

@ -99,14 +99,14 @@ pub struct Instance {
time_control: TimeControl, time_control: TimeControl,
phase: InstancePhase, phase: InstancePhase,
phase_end: Option<DateTime<Utc>>, pub phase_end: Option<DateTime<Utc>>,
phase_start: DateTime<Utc>, pub phase_start: DateTime<Utc>,
winner: Option<Uuid>, winner: Option<Uuid>,
} }
impl Instance { impl Instance {
fn new() -> Instance { pub fn new() -> Instance {
Instance { Instance {
id: Uuid::new_v4(), id: Uuid::new_v4(),
players: vec![], players: vec![],
@ -170,7 +170,7 @@ impl Instance {
(self, new_game) (self, new_game)
} }
fn set_name(mut self, name: String) -> Result<Instance, Error> { pub fn set_name(mut self, name: String) -> Result<Instance, Error> {
if name.len() == 0 { if name.len() == 0 {
return Err(err_msg("name must have a length")); return Err(err_msg("name must have a length"));
} }
@ -179,12 +179,12 @@ impl Instance {
Ok(self) Ok(self)
} }
fn set_time_control(mut self, tc: TimeControl) -> Instance { pub fn set_time_control(mut self, tc: TimeControl) -> Instance {
self.time_control = tc; self.time_control = tc;
self self
} }
fn add_player(&mut self, player: Player) -> Result<&mut Instance, Error> { pub fn add_player(&mut self, player: Player) -> Result<&mut Instance, Error> {
if self.players.len() >= self.max_players { if self.players.len() >= self.max_players {
return Err(err_msg("game full")) return Err(err_msg("game full"))
} }
@ -198,7 +198,7 @@ impl Instance {
Ok(self) Ok(self)
} }
fn player_ready(&mut self, player_id: Uuid) -> Result<Option<Game>, Error> { pub fn player_ready(&mut self, player_id: Uuid) -> Result<Option<Game>, Error> {
if ![InstancePhase::InProgress, InstancePhase::Lobby].contains(&self.phase) { if ![InstancePhase::InProgress, InstancePhase::Lobby].contains(&self.phase) {
return Err(err_msg("instance not in start or vbox phase")); return Err(err_msg("instance not in start or vbox phase"));
} }
@ -300,7 +300,7 @@ impl Instance {
self.next_round() self.next_round()
} }
fn next_round(&mut self) -> &mut Instance { pub fn next_round(&mut self) -> &mut Instance {
if self.finish_condition() { if self.finish_condition() {
return self.finish(); return self.finish();
} }
@ -342,7 +342,7 @@ impl Instance {
self self
} }
fn finished(&self) -> bool { pub fn finished(&self) -> bool {
self.phase == InstancePhase::Finished self.phase == InstancePhase::Finished
} }
@ -370,7 +370,7 @@ impl Instance {
self self
} }
fn current_game_id(&self) -> Option<Uuid> { pub fn current_game_id(&self) -> Option<Uuid> {
if self.phase != InstancePhase::InProgress { if self.phase != InstancePhase::InProgress {
return None; return None;
} }
@ -386,7 +386,7 @@ impl Instance {
return current_round.game_id; return current_round.game_id;
} }
fn game_finished(&mut self, game: &Game) -> Result<&mut Instance, Error> { pub fn game_finished(&mut self, game: &Game) -> Result<&mut Instance, Error> {
{ {
let current_round = self.rounds let current_round = self.rounds
.iter_mut() .iter_mut()
@ -428,14 +428,14 @@ impl Instance {
} }
// PLAYER ACTIONS // PLAYER ACTIONS
fn account_player(&mut self, account: Uuid) -> Result<&mut Player, Error> { pub fn account_player(&mut self, account: Uuid) -> Result<&mut Player, Error> {
self.players self.players
.iter_mut() .iter_mut()
.find(|p| p.id == account) .find(|p| p.id == account)
.ok_or(err_msg("account not in instance")) .ok_or(err_msg("account not in instance"))
} }
fn account_opponent(&mut self, account: Uuid) -> Result<&mut Player, Error> { pub fn account_opponent(&mut self, account: Uuid) -> Result<&mut Player, Error> {
self.players self.players
.iter_mut() .iter_mut()
.find(|p| p.id != account) .find(|p| p.id != account)
@ -529,7 +529,7 @@ mod tests {
let _instance = Instance::new(); let _instance = Instance::new();
let player_account = Uuid::new_v4(); let player_account = Uuid::new_v4();
let constructs = instance_mobs(player_account); let constructs = instance_mobs(player_account);
let _player = Player::new(player_account, &"test".to_string(), constructs).set_bot(true); let _player = Player::new(player_account, None, &"test".to_string(), constructs).set_bot(true);
} }
#[test] #[test]
@ -540,7 +540,7 @@ mod tests {
let player_account = Uuid::new_v4(); let player_account = Uuid::new_v4();
let constructs = instance_mobs(player_account); let constructs = instance_mobs(player_account);
let player = Player::new(player_account, &"a".to_string(), constructs); let player = Player::new(player_account, None, &"a".to_string(), constructs);
let a_id = player.id; let a_id = player.id;
instance.add_player(player).expect("could not add player"); instance.add_player(player).expect("could not add player");
@ -548,7 +548,7 @@ mod tests {
let player_account = Uuid::new_v4(); let player_account = Uuid::new_v4();
let constructs = instance_mobs(player_account); let constructs = instance_mobs(player_account);
let player = Player::new(player_account, &"b".to_string(), constructs); let player = Player::new(player_account, None, &"b".to_string(), constructs);
let b_id = player.id; let b_id = player.id;
instance.add_player(player).expect("could not add player"); instance.add_player(player).expect("could not add player");
@ -577,7 +577,7 @@ mod tests {
let player_account = Uuid::new_v4(); let player_account = Uuid::new_v4();
let constructs = instance_mobs(player_account); let constructs = instance_mobs(player_account);
let player = Player::new(player_account, &"a".to_string(), constructs); let player = Player::new(player_account, None, &"a".to_string(), constructs);
let a_id = player.id; let a_id = player.id;
instance.add_player(player).expect("could not add player"); instance.add_player(player).expect("could not add player");
@ -585,7 +585,7 @@ mod tests {
let player_account = Uuid::new_v4(); let player_account = Uuid::new_v4();
let constructs = instance_mobs(player_account); let constructs = instance_mobs(player_account);
let player = Player::new(player_account, &"b".to_string(), constructs); let player = Player::new(player_account, None, &"b".to_string(), constructs);
let b_id = player.id; let b_id = player.id;
instance.add_player(player).expect("could not add player"); instance.add_player(player).expect("could not add player");
@ -617,7 +617,7 @@ mod tests {
let player_account = Uuid::new_v4(); let player_account = Uuid::new_v4();
let constructs = instance_mobs(player_account); let constructs = instance_mobs(player_account);
let player = Player::new(player_account, &"a".to_string(), constructs); let player = Player::new(player_account, None, &"a".to_string(), constructs);
let _a_id = player.id; let _a_id = player.id;
instance.add_player(player).expect("could not add player"); instance.add_player(player).expect("could not add player");

View File

@ -25,6 +25,6 @@ pub fn instance_mobs(player_id: Uuid) -> Vec<Construct> {
pub fn bot_player() -> Player { pub fn bot_player() -> Player {
let bot_id = Uuid::new_v4(); let bot_id = Uuid::new_v4();
let constructs = instance_mobs(bot_id); let constructs = instance_mobs(bot_id);
Player::new(bot_id, &name(), constructs).set_bot(true) Player::new(bot_id, None, &name(), constructs).set_bot(true)
} }

View File

@ -66,10 +66,10 @@ pub struct Player {
} }
impl Player { impl Player {
pub fn new(account: Uuid, name: &String, constructs: Vec<Construct>) -> Player { pub fn new(account: Uuid, img: Option<Uuid>, name: &String, constructs: Vec<Construct>) -> Player {
Player { Player {
id: account, id: account,
img: Some(account), img,
name: name.clone(), name: name.clone(),
vbox: Vbox::new(), vbox: Vbox::new(),
constructs, constructs,
@ -401,7 +401,7 @@ mod tests {
fn player_bot_vbox_test() { fn player_bot_vbox_test() {
let player_account = Uuid::new_v4(); let player_account = Uuid::new_v4();
let constructs = instance_mobs(player_account); let constructs = instance_mobs(player_account);
let mut player = Player::new(player_account, &"test".to_string(), constructs).set_bot(true); let mut player = Player::new(player_account, None, &"test".to_string(), constructs).set_bot(true);
player.vbox.fill(); player.vbox.fill();
player.autobuy(); player.autobuy();
@ -413,7 +413,7 @@ mod tests {
fn player_score_test() { fn player_score_test() {
let player_account = Uuid::new_v4(); let player_account = Uuid::new_v4();
let constructs = instance_mobs(player_account); let constructs = instance_mobs(player_account);
let mut player = Player::new(player_account, &"test".to_string(), constructs).set_bot(true); let mut player = Player::new(player_account, None, &"test".to_string(), constructs).set_bot(true);
player.score = player.score.add_win(&Score::Zero); player.score = player.score.add_win(&Score::Zero);
player.score = player.score.add_win(&Score::Zero); player.score = player.score.add_win(&Score::Zero);

View File

@ -176,34 +176,23 @@ impl Cast {
}, },
], ],
// fn bash(source: &mut Construct, target: &mut Construct, skill: Skill) { Skill::Blast |
// skill.effect().into_iter() Skill::BlastPlus |
// .for_each(|e| (game.event(Event::new(source, target).event(target.add_effect(skill, e))))); Skill::BlastPlusPlus => vec![
Action::Damage {
construct: self.target,
colour: Colour::Blue,
values: vec![Value::Stat { construct: self.source, stat: Stat::BluePower, mult: self.skill.multiplier() }],
},
],
// if resolutions.iter().any(|r| match r.event {
// Event::Effect { effect, skill: effect_skill, duration: _, construct_effects: _ }
// => effect == Effect::Stun && skill == effect_skill,
// _ => false,
// }) {
// let mut cds = 0;
// for cs in target.skills.iter_mut() {
// if cs.skill.base_cd().is_some() {
// cs.cd = match cs.cd {
// None => Some(1),
// Some(i) => Some(i + 1),
// };
// cds += 1;
// }
// }
// let amount = source.red_power().pct(skill.multiplier().pct(100 + 45usize.saturating_mul(cds)));
// target.deal_red_damage(skill, amount)
// .into_iter()
// .for_each(|e| game.event(Event::new(source, target).event(e).stages(EventStages::PostOnly)));
// }
// }
Skill::Strike | Skill::Strike |
Skill::StrikePlus | Skill::StrikePlus |
@ -1349,13 +1338,6 @@ impl Skill {
// .for_each(|e| game.event(Event::new(source, target).event(e).stages(EventStages::PostOnly))); // .for_each(|e| game.event(Event::new(source, target).event(e).stages(EventStages::PostOnly)));
// } // }
// fn blast(source: &mut Construct, target: &mut Construct, skill: Skill) {
// let amount = source.blue_power().pct(skill.multiplier());
// target.deal_blue_damage(skill, amount)
// .into_iter()
// .for_each(|e| game.event(Event::new(source, target).event(e)));
// }
// fn amplify(source: &mut Construct, target: &mut Construct, skill: Skill) { // fn amplify(source: &mut Construct, target: &mut Construct, skill: Skill) {
// game.event(Event::new(source, target).event(target.add_effect(skill, skill.effect()[0]))); // game.event(Event::new(source, target).event(target.add_effect(skill, skill.effect()[0])));
// } // }

View File

@ -44,3 +44,5 @@ stripe-rust = "0.10"
reqwest = "0.9" reqwest = "0.9"
url = "1" url = "1"
mnml_core = { path = "./../core" }

View File

@ -8,19 +8,19 @@ use serde_cbor::{from_slice};
use postgres::transaction::Transaction; use postgres::transaction::Transaction;
use http::MnmlHttpError;
use names::{name as generate_name};
use construct::{Construct, ConstructSkeleton, construct_spawn};
use instance::{Instance, instance_delete};
use instance;
use mtx::{Mtx, FREE_MTX};
use pg::Db;
use img;
use failure::Error; use failure::Error;
use failure::{err_msg, format_err}; use failure::{err_msg, format_err};
use mnml_core::construct::{Construct, ConstructSkeleton};
use mnml_core::instance::{Instance};
use mnml_core::player::{Player};
use http::MnmlHttpError;
use names::{name as generate_name};
use mtx::{Mtx, FREE_MTX};
use pg::{Db, instance_delete, construct_spawn, instance_practice};
use img;
static PASSWORD_MIN_LEN: usize = 3; static PASSWORD_MIN_LEN: usize = 3;
static PASSWORD_ROUNDS: u32 = 10; static PASSWORD_ROUNDS: u32 = 10;
@ -33,6 +33,19 @@ pub struct Account {
pub subscribed: bool, pub subscribed: bool,
} }
impl Account {
pub fn to_player(&self, tx: &mut Transaction) -> Result<Player, Error> {
let constructs = team(tx, self)?;
let img = match self.subscribed {
true => Some(self.img),
false => None,
};
Ok(Player::new(self.id, Some(self.img), &self.name, constructs))
}
}
impl<'a> TryFrom<postgres::rows::Row<'a>> for Account { impl<'a> TryFrom<postgres::rows::Row<'a>> for Account {
type Error = Error; type Error = Error;
@ -523,7 +536,7 @@ pub fn tutorial(tx: &mut Transaction, account: &Account) -> Result<Option<Instan
let count: i64 = row.get(0); let count: i64 = row.get(0);
if count == 0 { if count == 0 {
return Ok(Some(instance::instance_practice(tx, account)?)); return Ok(Some(instance_practice(tx, account)?));
} }
return Ok(None); return Ok(None);

File diff suppressed because it is too large Load Diff

View File

@ -1,209 +0,0 @@
use construct::{Stat, EffectMeta};
use skill::{Skill};
use util::{IntPct};
pub type Cooldown = Option<u8>;
#[derive(Debug,Clone,Copy,PartialEq,Serialize,Deserialize)]
pub enum Effect {
Amplify,
Banish,
Block,
Buff,
Counter,
Curse,
Haste,
Hybrid,
Intercept,
Invert,
Pure,
Purge,
Reflect,
Restrict,
Silence,
Slow,
Stun,
Sustain,
Vulnerable,
Wither, // Reduce green dmg (healing) taken
// electric is the buff that applies
// electrocute the dmg debuff
Electric,
Electrocute,
// absorbtion is the buff
// absorb is the increased damage
Absorb,
Absorption,
// magic immunity
// effects over time
Triage,
Decay,
Regen,
Siphon,
// Airborne,
// Boost
// Bleed,
// Blind,
// Deadly,
// Enslave,
// Fury,
// Injured,
// Leech,
// Mesmerise,
// Untouchable,
// SpeedSiphon,
// SpeedIncrease,
Ko,
}
impl Effect {
pub fn immune(&self, skill: Skill) -> bool {
match self {
Effect::Banish => true,
Effect::Sustain => [
Skill::Stun,
Skill::Silence,
Skill::SilencePlus,
Skill::SilencePlusPlus,
Skill::Ruin,
Skill::RuinPlus,
Skill::RuinPlusPlus,
Skill::Restrict,
Skill::RestrictPlus,
Skill::RestrictPlusPlus
].contains(&skill),
_ => false,
}
}
pub fn disables_skill(&self, skill: Skill) -> bool {
if skill.is_tick() {
return false;
}
match self {
Effect::Stun => true,
Effect::Banish => true,
Effect::Silence => skill.colours().contains(&Colour::Blue),
Effect::Restrict => skill.colours().contains(&Colour::Red),
Effect::Purge => skill.colours().contains(&Colour::Green),
Effect::Ko => skill.ko_castable(),
_ => false,
}
}
pub fn modifications(&self) -> Vec<Stat> {
match self {
// Bases
Effect::Block => vec![Stat::RedDamageTaken, Stat::BlueDamageTaken],
Effect::Buff => vec![Stat::BluePower, Stat::RedPower, Stat::Speed],
Effect::Slow => vec![Stat::Speed],
// Power changes
Effect::Absorption => vec![Stat::RedPower, Stat::BluePower],
Effect::Amplify => vec![Stat::RedPower, Stat::BluePower],
Effect::Hybrid => vec![Stat::GreenPower],
// Damage taken changes
Effect::Curse => vec![Stat::RedDamageTaken, Stat::BlueDamageTaken],
Effect::Pure => vec![Stat::GreenDamageTaken], // increased green taken
Effect::Vulnerable => vec![Stat::RedDamageTaken],
Effect::Wither => vec![Stat::GreenDamageTaken], // reduced green taken
// Speed
Effect::Haste => vec![Stat::Speed],
_ => vec![],
}
}
pub fn apply(&self, value: u64, meta: Option<EffectMeta>) -> u64 {
match self {
Effect::Amplify |
Effect::Vulnerable |
Effect::Block |
Effect::Buff |
Effect::Curse |
Effect::Haste |
Effect::Slow |
Effect::Hybrid |
Effect::Pure |
Effect::Wither => value.pct(match meta {
Some(EffectMeta::Multiplier(d)) => d,
_ => 100,
}),
Effect::Absorption => value + match meta {
Some(EffectMeta::AddedDamage(d)) => d,
_ => {
warn!("absorb meta not damage");
return 0;
}
},
_ => {
warn!("{:?} does not have a mod effect", self);
return value;
},
}
}
pub fn colour(&self) -> Option<Colour> {
match self {
// physical
Effect::Stun => Some(Colour::Red),
Effect::Block => Some(Colour::Green),
Effect::Buff => Some(Colour::Green),
Effect::Counter => Some(Colour::Green),
Effect::Vulnerable => Some(Colour::Red),
Effect::Restrict => Some(Colour::Red),
Effect::Sustain => Some(Colour::Green),
Effect::Intercept => Some(Colour::Green),
// magic
Effect::Curse => Some(Colour::Blue),
Effect::Banish => None,
// Effect::Banish => rng.gen_bool(0.5),
Effect::Slow => Some(Colour::Blue),
Effect::Haste => Some(Colour::Green),
Effect::Absorption => Some(Colour::Green),
Effect::Reflect => Some(Colour::Green),
Effect::Amplify => Some(Colour::Green),
Effect::Silence => Some(Colour::Blue),
Effect::Wither => Some(Colour::Blue),
Effect::Purge => Some(Colour::Blue),
Effect::Electric => Some(Colour::Green),
Effect::Electrocute => Some(Colour::Blue),
Effect::Absorb => Some(Colour::Green),
// magic
Effect::Hybrid => Some(Colour::Green),
Effect::Invert => Some(Colour::Green),
// effects over time
Effect::Triage => Some(Colour::Green),
Effect::Decay => Some(Colour::Blue),
Effect::Regen => Some(Colour::Green),
Effect::Siphon => Some(Colour::Blue),
Effect::Pure => Some(Colour::Green),
Effect::Ko => None,
}
}
}
#[derive(Debug,Clone,Copy,PartialEq,Serialize,Deserialize)]
pub enum Colour {
Red,
Blue,
Green,
}

View File

@ -1,107 +0,0 @@
// This example illustrates the error flow of a Request in the middleware Chain.
// Here is the chain used and the path of the request through the middleware pieces:
//
// Normal Flow : __[ErrorProducer::before]__ [ErrorRecover::before] __[handle::HelloWorldHandler]__[ErrorProducer::after]__ [ErrorRecover::after] __ ...
// Error Flow : [ErrorProducer::catch ] |__[ErrorRecover::catch ]__| [ErrorProducer::catch] |__[ErrorRecover::catch]__|
//
// --------------- BEFORE MIDDLEWARE ----------------- || --------- HANDLER -------- || ---------------- AFTER MIDDLEWARE --------------
extern crate iron;
use iron::prelude::*;
use iron::StatusCode;
use iron::{AfterMiddleware, BeforeMiddleware, Handler};
use std::error::Error;
use std::fmt::{self, Debug};
struct HelloWorldHandler;
struct ErrorProducer;
struct ErrorRecover;
#[derive(Debug)]
struct StringError(String);
impl fmt::Display for StringError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
Debug::fmt(self, f)
}
}
impl Error for StringError {
fn description(&self) -> &str {
&*self.0
}
}
impl Handler for HelloWorldHandler {
fn handle(&self, _: &mut Request) -> IronResult<Response> {
// This will be called since we are in the normal flow before reaching the Handler.
// However, the AfterMiddleware chain will override the Response.
println!("The HelloWorldHandler has been called !");
Ok(Response::with((StatusCode::OK, "Hello world !")))
}
}
impl BeforeMiddleware for ErrorProducer {
fn before(&self, _: &mut Request) -> IronResult<()> {
// The error produced here switches to the error flow.
// The catch method of following middleware pieces will be called.
// The Handler will be skipped unless the error is handled by another middleware piece.
// IronError::error tells the next middleware what went wrong.
// IronError::response is the Response that will be sent back to the client if this error is not handled.
// Here status::BadRequest acts as modifier, thus we can put more there than just a status.
Err(IronError::new(
StringError("Error in ErrorProducer BeforeMiddleware".to_string()),
StatusCode::BAD_REQUEST,
))
}
}
impl AfterMiddleware for ErrorProducer {
fn after(&self, _: &mut Request, _: Response) -> IronResult<Response> {
// The behavior here is the same as in ErrorProducer::before.
// The previous response (from the Handler) is discarded and replaced with a new response (created from the modifier).
Err(IronError::new(
StringError("Error in ErrorProducer AfterMiddleware".to_string()),
(StatusCode::BAD_REQUEST, "Response created in ErrorProducer"),
))
}
}
impl BeforeMiddleware for ErrorRecover {
fn catch(&self, _: &mut Request, err: IronError) -> IronResult<()> {
// We can use the IronError from previous middleware to decide what to do.
// Returning Ok() from a catch method resumes the normal flow and
// passes the Request forward to the next middleware piece in the chain (here the HelloWorldHandler).
println!("{} caught in ErrorRecover BeforeMiddleware.", err.error);
match err.response.status {
Some(StatusCode::BAD_REQUEST) => Ok(()),
_ => Err(err),
}
}
}
impl AfterMiddleware for ErrorRecover {
fn catch(&self, _: &mut Request, err: IronError) -> IronResult<Response> {
// Just like in the BeforeMiddleware, we can return Ok(Response) here to return to the normal flow.
// In this case, ErrorRecover is the last middleware in the chain
// and the Response created in the ErrorProducer is modified and sent back to the client.
println!("{} caught in ErrorRecover AfterMiddleware.", err.error);
match err.response.status {
Some(StatusCode::BAD_REQUEST) => Ok(err.response.set(StatusCode::OK)),
_ => Err(err),
}
}
}
fn main() {
let mut chain = Chain::new(HelloWorldHandler);
chain.link_before(ErrorProducer);
chain.link_before(ErrorRecover);
chain.link_after(ErrorProducer);
chain.link_after(ErrorRecover);
Iron::new(chain).http("localhost:3000");
}

View File

@ -10,13 +10,9 @@ use failure::{err_msg, format_err};
use crossbeam_channel::{Sender, Receiver}; use crossbeam_channel::{Sender, Receiver};
use account;
use account::Account; use account::Account;
use game;
use instance;
use names; use names;
use pg::{Db, PgPool};
use rpc::RpcMessage; use rpc::RpcMessage;
use warden::{GameEvent}; use warden::{GameEvent};
use mail::Mail; use mail::Mail;

File diff suppressed because it is too large Load Diff

View File

@ -14,9 +14,9 @@ use persistent::{Read, Write};
use router::Router; use router::Router;
use mount::{Mount}; use mount::{Mount};
use serde::{Serialize, Deserialize}; use serde::{Serialize, Deserialize};
use lettre::{SendableEmail, SmtpClient, SmtpTransport, Transport}; use lettre::{SmtpTransport};
use acp; // use acp;
use account; use account;
use mail; use mail;
use mail::Mail; use mail::Mail;
@ -517,7 +517,7 @@ pub fn start(pool: PgPool, mailer: SmtpTransport) {
mounts.mount("/api/account/", account_mount()); mounts.mount("/api/account/", account_mount());
mounts.mount("/api/payments/", payment_mount()); mounts.mount("/api/payments/", payment_mount());
mounts.mount("/api/acp/", acp::acp_mount()); // mounts.mount("/api/acp/", acp::acp_mount());
let mut chain = Chain::new(mounts); let mut chain = Chain::new(mounts);
chain.link(Read::<State>::both(State { pool })); chain.link(Read::<State>::both(State { pool }));

View File

@ -1,959 +0,0 @@
use std::fs::File;
use std::collections::{HashMap};
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 vbox;
use player::{Player, Score, player_create};
use mob::{bot_player, instance_mobs};
use game::{Game, Phase, game_get, game_write, game_update};
use item::{Item};
use rpc::{RpcMessage};
use img;
#[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 => 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::seconds(15))
.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>,
max_players: usize,
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,
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>>()
}
pub fn upkeep(mut self) -> (Instance, Option<Game>) {
// time out lobbies that have been open too long
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)
}
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 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);
// 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()
}
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
}
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::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;
}
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
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"))
}
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_id: Option<Uuid>) -> Result<Instance, Error> {
self.vbox_action_allowed(account)?;
self.account_player(account)?
.vbox_unequip(target, construct_id, 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, upkeep)
VALUES ($1, $2, $3)
RETURNING id;
";
let result = tx
.query(query, &[&instance.id, &instance_bytes, &instance.phase_end])?;
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, finished = $2, upkeep = $3, updated_at = now()
WHERE id = $4
RETURNING id, data;
";
let result = tx
.query(query, &[&instance_bytes, &instance.finished(), &instance.phase_end, &instance.id])?;
result.iter().next().ok_or(err_msg("no instance row returned"))?;
trace!("{:?} wrote instance", instance.id);
if instance.finished() {
info!("finished id={:?}", instance.id);
match instance_json_file_write(&instance) {
Ok(dest) => info!("wrote dest={:?}", dest),
Err(e) => error!("json write error={:?}", e),
};
}
return Ok(instance);
}
fn instance_json_file_write(g: &Instance) -> Result<String, Error> {
let dest = format!("/var/lib/mnml/data/instances/{}.mnml.instance.json", g.id);
serde_json::to_writer(File::create(&dest)?, g)?;
Ok(dest)
}
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
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_practice(tx: &mut Transaction, account: &Account) -> Result<Instance, Error> {
let bot = bot_player();
let bot_id = bot.id;
// generate bot imgs for the client to see
for c in bot.constructs.iter() {
img::shapes_write(c.img)?;
}
let mut instance = Instance::new()
.set_time_control(TimeControl::Practice)
.set_name(bot.name.clone())?;
let player = Player::from_account(tx, account)?;
instance.add_player(player.clone())?;
instance.add_player(bot)?;
instance.player_ready(bot_id)?;
// skip faceoff
instance.player_ready(player.id)?;
instance = instance_create(tx, instance)?;
player_create(tx, player, instance.id, account)?;
Ok(instance)
}
pub fn pvp(tx: &mut Transaction, a: &Account, b: &Account) -> Result<Instance, Error> {
let mut instance = Instance::new()
// TODO generate nice game names
.set_name("PVP".to_string())?;
instance = instance_create(tx, instance)?;
for account in [a, b].iter() {
let acc_p = Player::from_account(tx, &account)?;
let player = player_create(tx, acc_p, instance.id, account)?;
instance.add_player(player)?;
}
instance_update(tx, instance)
}
pub fn instance_abandon(tx: &mut Transaction, account: &Account, instance_id: Uuid) -> Result<RpcMessage, Error> {
let mut instance = instance_get(tx, instance_id)?;
if let Some(game_id) = instance.current_game_id() {
let mut game = game_get(tx, game_id)?;
game.player_by_id(account.id)?.forfeit();
game = game.start(); // actually finishes it...
game_update(tx, &game)?;
}
instance.account_player(account.id)?.set_lose();
instance.account_opponent(account.id)?.set_win();
instance.next_round();
Ok(RpcMessage::InstanceState(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)?;
// ensures cleanup for warden etc is done
game_update(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, 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::Finished {
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(())
}
pub fn bot_instance() -> Instance {
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");
instance.player_ready(player_account).unwrap();
instance.player_ready(bot).unwrap();
return instance;
}
pub fn demo() -> Result<Vec<Player>, Error> {
let bot = bot_player();
// generate bot imgs for the client to see
for c in bot.constructs.iter() {
img::shapes_write(c.img)?;
};
let bot2 = bot_player();
// generate bot imgs for the client to see
for c in bot2.constructs.iter() {
img::shapes_write(c.img)?;
};
Ok(vec![bot, bot2])
}
#[cfg(test)]
mod tests {
use super::*;
#[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, &"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!(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, &"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());
}
}

File diff suppressed because it is too large Load Diff

View File

@ -33,28 +33,19 @@ extern crate lettre_email;
extern crate ws; extern crate ws;
extern crate crossbeam_channel; extern crate crossbeam_channel;
extern crate mnml_core;
mod account; mod account;
mod acp; // mod acp;
mod construct;
mod effect;
mod game;
mod instance;
mod item;
mod img; mod img;
mod mail; mod mail;
mod mob;
mod mtx; mod mtx;
mod names; mod names;
mod http; mod http;
mod payments; mod payments;
mod pg; mod pg;
mod player;
mod events; mod events;
pub mod rpc; pub mod rpc;
mod skill;
mod spec;
mod util;
mod vbox;
mod warden; mod warden;
use std::thread::{spawn}; use std::thread::{spawn};

View File

@ -1,30 +0,0 @@
use uuid::Uuid;
use std::iter;
use construct::{Construct};
use names::{name};
use player::{Player};
pub fn generate_mob() -> Construct {
let mob = Construct::new()
.named(&name());
return mob;
}
pub fn instance_mobs(player_id: Uuid) -> Vec<Construct> {
iter::repeat_with(||
generate_mob()
.set_account(player_id))
// .learn(Skill::Attack))
.take(3)
.collect::<Vec<Construct>>()
}
pub fn bot_player() -> Player {
let bot_id = Uuid::new_v4();
let constructs = instance_mobs(bot_id);
Player::new(bot_id, &name(), constructs).set_bot(true)
}

View File

@ -10,7 +10,9 @@ use failure::err_msg;
use account; use account;
use account::Account; use account::Account;
use construct::{Construct, construct_select, construct_write, construct_spawn}; use mnml_core::construct::{Construct};
use pg::{construct_select, construct_write, construct_spawn};
use names::{name as generate_name}; use names::{name as generate_name};
use img; use img;

View File

@ -1,70 +0,0 @@
use petgraph::graph::{Graph, UnGraph, NodeIndex};
use petgraph::dot::{Dot, Config};
#[derive(Debug,Clone,Copy,PartialEq,Eq,Hash,PartialOrd,Ord,Serialize,Deserialize)]
pub struct Passive {
id: &'static str,
allocated: bool,
effect: usize,
}
impl Passive {
fn new(id: &'static str) -> Passive {
return Passive {
id,
allocated: false,
effect: 0,
};
}
}
pub fn create_passive_graph() -> UnGraph<Passive, ()> {
let mut gr = Graph::new_undirected();
let start = gr.add_node(Passive::new("START"));
let mut last;
let mut next;
// Natural Selection nodes
next = gr.add_node(Passive::new("NS"));
gr.add_edge(start, next, ());
last = next;
next = gr.add_node(Passive::new("NSPD0000"));
gr.add_edge(last, next, ());
last = next;
next = gr.add_node(Passive::new("NSPD0001"));
gr.add_edge(last, next, ());
last = next;
next = gr.add_node(Passive::new("NSPD0002"));
gr.add_edge(last, next, ());
last = next;
next = gr.add_node(Passive::new("NSPD0003"));
gr.add_edge(last, next, ());
last = next;
next = gr.add_node(Passive::new("NSBLOCK"));
gr.add_edge(last, next, ());
last = next;
return gr;
}
#[cfg(test)]
mod tests {
use passives::*;
#[test]
fn create_graph() {
let _graph = create_passive_graph();
// good shit;
// let nodes = graph.node_indices().collect::<Vec<NodeIndex>>();
// info!("{:?}", nodes[0]);
// info!("{:?}", graph.node_weight(nodes[0]));
// info!("{:?}", Dot::with_config(&graph, &[Config::EdgeNoLabel]));
}
}

View File

@ -1,22 +1,35 @@
use std::fs::File;
use std::env; use std::env;
use std::thread::spawn; use std::thread::spawn;
use uuid::Uuid; use uuid::Uuid;
use failure::err_msg;
use failure::Error; use failure::Error;
use r2d2::{Pool}; use r2d2::{Pool, PooledConnection};
use r2d2::{PooledConnection};
use r2d2_postgres::{TlsMode, PostgresConnectionManager}; use r2d2_postgres::{TlsMode, PostgresConnectionManager};
use fallible_iterator::{FallibleIterator}; use fallible_iterator::{FallibleIterator};
use postgres::transaction::Transaction;
use crossbeam_channel::{Sender}; use crossbeam_channel::{Sender};
use serde_cbor::{from_slice, to_vec};
use mnml_core::construct::{Construct, ConstructSkeleton};
use mnml_core::game::{Game, Phase};
use mnml_core::player::Player;
use mnml_core::mob::instance_mobs;
use mnml_core::vbox::{ItemType, VboxIndices};
use mnml_core::item::Item;
use mnml_core::skill::Skill;
use mnml_core::mob::bot_player;
use mnml_core::instance::{Instance, TimeControl};
use events::{Event}; use events::{Event};
use account;
use game;
use instance;
use rpc::RpcMessage; use rpc::RpcMessage;
use account;
use account::{Account};
use img;
pub type Db = PooledConnection<PostgresConnectionManager>; pub type Db = PooledConnection<PostgresConnectionManager>;
pub type PgPool = Pool<PostgresConnectionManager>; pub type PgPool = Pool<PostgresConnectionManager>;
@ -88,9 +101,9 @@ fn handle_notification(n: Notification, pool: &PgPool, events: &Sender<Event>) {
Table::Accounts => Table::Accounts =>
Some(Event::Push(n.id, RpcMessage::AccountState(account::select(&db, n.id).unwrap()))), Some(Event::Push(n.id, RpcMessage::AccountState(account::select(&db, n.id).unwrap()))),
Table::Instances => Table::Instances =>
Some(Event::Push(n.id, instance::instance_state(&mut tx, n.id).unwrap())), Some(Event::Push(n.id, instance_state(&mut tx, n.id).unwrap())),
Table::Games => Table::Games =>
Some(Event::Push(n.id, RpcMessage::GameState(game::game_get(&mut tx, n.id).unwrap()))), Some(Event::Push(n.id, RpcMessage::GameState(game_get(&mut tx, n.id).unwrap()))),
_ => { _ => {
// warn!("unimplemented update notification {:?}", n); // warn!("unimplemented update notification {:?}", n);
None None
@ -128,3 +141,721 @@ pub fn listen(pool: PgPool, events: Sender<Event>) -> Result<(), Error> {
} }
} }
} }
pub fn construct_delete(tx: &mut Transaction, id: Uuid, account_id: Uuid) -> Result<(), Error> {
let query = "
DELETE
FROM constructs
WHERE id = $1
and account = $2;
";
let result = tx
.execute(query, &[&id, &account_id])?;
if result != 1 {
return Err(format_err!("unable to delete construct {:?}", id));
}
info!("construct deleted {:?}", id);
return Ok(());
}
pub fn construct_get(tx: &mut Transaction, id: Uuid, account_id: Uuid) -> Result<Construct, Error> {
let query = "
SELECT data
FROM constructs
WHERE id = $1
AND account = $2;
";
let result = tx
.query(query, &[&id, &account_id])?;
let result = result.iter().next().ok_or(format_err!("construct {:} not found", id))?;
let construct_bytes: Vec<u8> = result.get(0);
let skeleton = from_slice::<ConstructSkeleton>(&construct_bytes)?;
return Ok(Construct::from_skeleton(&skeleton));
}
pub fn construct_select(tx: &mut Transaction, id: Uuid, account_id: Uuid) -> Result<Construct, Error> {
let query = "
SELECT data
FROM constructs
WHERE id = $1
AND account = $2
FOR UPDATE;
";
let result = tx
.query(query, &[&id, &account_id])?;
let result = result.iter().next().ok_or(format_err!("construct {:} not found", id))?;
let construct_bytes: Vec<u8> = result.get(0);
let skeleton = from_slice::<ConstructSkeleton>(&construct_bytes)?;
return Ok(Construct::from_skeleton(&skeleton));
}
pub fn construct_spawn(tx: &mut Transaction, account: Uuid, name: String, team: bool) -> Result<Construct, Error> {
let construct = Construct::new()
.named(&name)
.set_account(account);
let construct_bytes = to_vec(&construct)?;
let query = "
INSERT INTO constructs (id, account, data, team)
VALUES ($1, $2, $3, $4)
RETURNING id, account;
";
let result = tx
.query(query, &[&construct.id, &account, &construct_bytes, &team])?;
let _returned = result.iter().next().ok_or(err_msg("no row returned"))?;
img::shapes_write(construct.img)?;
info!("spawned construct account={:} name={:?}", account, construct.name);
return Ok(construct);
}
pub fn construct_write(tx: &mut Transaction, construct: Construct) -> Result<Construct, Error> {
let construct_bytes = to_vec(&construct.to_skeleton())?;
let query = "
UPDATE constructs
SET data = $1, updated_at = now()
WHERE id = $2
RETURNING id, account, data;
";
let result = tx
.query(query, &[&construct_bytes, &construct.id])?;
let _returned = result.iter().next().expect("no row returned");
// info!("{:?} wrote construct", construct.id);
return Ok(construct);
}
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(tx: &mut Transaction, account: &Account, id: Uuid) -> Result<Game, Error> {
Ok(game_get(tx, id)?.redact(account.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 select(db: &Db, id: Uuid) -> Result<Game, Error> {
let query = "
SELECT *
FROM games
WHERE id = $1;
";
let result = db
.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 list(db: &Db, number: u32) -> Result<Vec<Game>, Error> {
let query = "
SELECT data
FROM games
ORDER BY created_at
LIMIT $1;
";
let result = db
.query(query, &[&number])?;
let mut list = vec![];
for row in result.into_iter() {
let bytes: Vec<u8> = row.get(0);
match from_slice::<Game>(&bytes) {
Ok(i) => list.push(i),
Err(e) => {
warn!("{:?}", e);
}
};
}
return Ok(list);
}
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_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() {
info!("finished id={:?}", game.id);
match game_json_file_write(&game) {
Ok(dest) => info!("wrote dest={:?}", dest),
Err(e) => error!("json write error={:?}", e),
};
if let Some(i) = game.instance {
instance_game_finished(tx, &game, i)?;
}
}
return Ok(());
}
fn game_json_file_write(g: &Game) -> Result<String, Error> {
let dest = format!("/var/lib/mnml/data/games/{}.mnml.game.json", g.id);
serde_json::to_writer(File::create(&dest)?, g)?;
Ok(dest)
}
pub fn game_skill(tx: &mut Transaction, account: &Account, game_id: Uuid, construct_id: Uuid, target_construct_id: Uuid, skill: Skill) -> Result<Game, Error> {
let mut game = game_get(tx, game_id)?;
game.add_skill(account.id, construct_id, target_construct_id, skill)?;
if game.skill_phase_finished() {
game = game.resolve_phase_start();
}
game_update(tx, &game)?;
Ok(game)
}
pub fn game_offer_draw(tx: &mut Transaction, account: &Account, game_id: Uuid) -> Result<Game, Error> {
let game = game_get(tx, game_id)?
.offer_draw(account.id)?;
game_update(tx, &game)?;
Ok(game)
}
pub fn game_concede(tx: &mut Transaction, account: &Account, game_id: Uuid) -> Result<Game, Error> {
let game = game_get(tx, game_id)?
.concede(account.id)?;
game_update(tx, &game)?;
Ok(game)
}
pub fn game_skill_clear(tx: &mut Transaction, account: &Account, game_id: Uuid) -> Result<Game, Error> {
let mut game = game_get(tx, game_id)?;
game.clear_skill(account.id)?;
game_update(tx, &game)?;
Ok(game)
}
pub fn game_ready(tx: &mut Transaction, account: &Account, id: Uuid) -> Result<Game, Error> {
let mut game = game_get(tx, id)?;
game.player_ready(account.id)?;
if game.skill_phase_finished() {
game = game.resolve_phase_start();
}
game_update(tx, &game)?;
Ok(game)
}
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, upkeep)
VALUES ($1, $2, $3)
RETURNING id;
";
let result = tx
.query(query, &[&instance.id, &instance_bytes, &instance.phase_end])?;
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, finished = $2, upkeep = $3, updated_at = now()
WHERE id = $4
RETURNING id, data;
";
let result = tx
.query(query, &[&instance_bytes, &instance.finished(), &instance.phase_end, &instance.id])?;
result.iter().next().ok_or(err_msg("no instance row returned"))?;
trace!("{:?} wrote instance", instance.id);
if instance.finished() {
info!("finished id={:?}", instance.id);
match instance_json_file_write(&instance) {
Ok(dest) => info!("wrote dest={:?}", dest),
Err(e) => error!("json write error={:?}", e),
};
}
return Ok(instance);
}
fn instance_json_file_write(g: &Instance) -> Result<String, Error> {
let dest = format!("/var/lib/mnml/data/instances/{}.mnml.instance.json", g.id);
serde_json::to_writer(File::create(&dest)?, g)?;
Ok(dest)
}
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
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_practice(tx: &mut Transaction, account: &Account) -> Result<Instance, Error> {
let bot = bot_player();
let bot_id = bot.id;
// generate bot imgs for the client to see
for c in bot.constructs.iter() {
img::shapes_write(c.img)?;
}
let mut instance = Instance::new()
.set_time_control(TimeControl::Practice)
.set_name(bot.name.clone())?;
let player = account.to_player(tx)?;
instance.add_player(player.clone())?;
instance.add_player(bot)?;
instance.player_ready(bot_id)?;
// skip faceoff
instance.player_ready(player.id)?;
instance = instance_create(tx, instance)?;
player_create(tx, player, instance.id, account)?;
Ok(instance)
}
pub fn pvp(tx: &mut Transaction, a: &Account, b: &Account) -> Result<Instance, Error> {
let mut instance = Instance::new()
// TODO generate nice game names
.set_name("PVP".to_string())?;
instance = instance_create(tx, instance)?;
for account in [a, b].iter() {
let acc_p = account.to_player(tx)?;
let player = player_create(tx, acc_p, instance.id, account)?;
instance.add_player(player)?;
}
instance_update(tx, instance)
}
pub fn player_create(tx: &mut Transaction, player: Player, instance: Uuid, account: &Account) -> Result<Player, Error> {
let query = "
INSERT INTO players (id, instance, account)
VALUES ($1, $2, $3)
RETURNING id, account;
";
let result = tx
.query(query, &[&Uuid::new_v4(), &instance, &account.id])?;
let _returned = result.iter().next().expect("no row written");
info!("wrote player {:} joined instance: {:}", account.name, instance);
return Ok(player);
}
pub fn instance_abandon(tx: &mut Transaction, account: &Account, instance_id: Uuid) -> Result<RpcMessage, Error> {
let mut instance = instance_get(tx, instance_id)?;
if let Some(game_id) = instance.current_game_id() {
let mut game = game_get(tx, game_id)?;
game.player_by_id(account.id)?.forfeit();
game = game.start(); // actually finishes it...
game_update(tx, &game)?;
}
instance.account_player(account.id)?.set_lose();
instance.account_opponent(account.id)?.set_win();
instance.next_round();
Ok(RpcMessage::InstanceState(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)?;
// ensures cleanup for warden etc is done
game_update(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, 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::Finished {
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(())
}
pub fn bot_instance() -> Instance {
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, None, &"test".to_string(), constructs).set_bot(true);
instance.add_player(player).expect("could not add player");
instance.player_ready(player_account).unwrap();
instance.player_ready(bot).unwrap();
return instance;
}
pub fn demo() -> Result<Vec<Player>, Error> {
let bot = bot_player();
// generate bot imgs for the client to see
for c in bot.constructs.iter() {
img::shapes_write(c.img)?;
};
let bot2 = bot_player();
// generate bot imgs for the client to see
for c in bot2.constructs.iter() {
img::shapes_write(c.img)?;
};
Ok(vec![bot, bot2])
}
pub fn vbox_refill(tx: &mut Transaction, account: &Account, instance_id: Uuid) -> Result<Instance, Error> {
let instance = instance_get(tx, instance_id)?
.vbox_refill(account.id)?;
return instance_update(tx, instance);
}
pub fn vbox_buy(tx: &mut Transaction, account: &Account, instance_id: Uuid, group: ItemType, index: String, construct_id: Option<Uuid>) -> Result<Instance, Error> {
let instance = instance_get(tx, instance_id)?
.vbox_buy(account.id, group, index, construct_id)?;
return instance_update(tx, instance);
}
pub fn vbox_combine(tx: &mut Transaction, account: &Account, instance_id: Uuid, stash_indices: Vec<String>, vbox_indices: VboxIndices) -> Result<Instance, Error> {
let instance = instance_get(tx, instance_id)?
.vbox_combine(account.id, stash_indices, vbox_indices)?;
return instance_update(tx, instance);
}
pub fn vbox_refund(tx: &mut Transaction, account: &Account, instance_id: Uuid, index: String) -> Result<Instance, Error> {
let instance = instance_get(tx, instance_id)?
.vbox_refund(account.id, index)?;
return instance_update(tx, instance);
}
pub fn vbox_apply(tx: &mut Transaction, account: &Account, instance_id: Uuid, construct_id: Uuid, index: String) -> Result<Instance, Error> {
let instance = instance_get(tx, instance_id)?
.vbox_apply(account.id, index, construct_id)?;
return instance_update(tx, instance);
}
pub fn vbox_unequip(tx: &mut Transaction, account: &Account, instance_id: Uuid, construct_id: Uuid, target: Item, target_construct_id: Option<Uuid>) -> Result<Instance, Error> {
let instance = instance_get(tx, instance_id)?
.vbox_unequip(account.id, target, construct_id, target_construct_id)?;
return instance_update(tx, instance);
}

View File

@ -1,490 +0,0 @@
use std::collections::{HashMap};
use uuid::Uuid;
use rand::prelude::*;
use postgres::transaction::Transaction;
use failure::Error;
use failure::err_msg;
use account;
use account::Account;
use construct::{Construct, Colours};
use vbox::{Vbox, ItemType, VboxIndices};
use item::{Item, ItemEffect};
use effect::{Effect};
const DISCARD_COST: usize = 2;
#[derive(Debug,Copy,Clone,Serialize,Deserialize,Eq,PartialEq)]
pub enum Score {
Zero,
One,
Two,
Three,
Adv,
Win,
Lose,
}
impl Score {
pub fn add_win(self, _opp: &Score) -> Score {
match self {
Score::Zero => Score::One,
Score::One => Score::Two,
Score::Two => Score::Win,
// Tennis scoring
// Score::Three => match opp {
// Score::Adv => Score::Three,
// Score::Three => Score::Adv,
// _ => Score::Win,
// }
// Score::Adv => Score::Win,
_ => panic!("faulty score increment {:?}", self),
}
}
pub fn add_loss(self) -> Score {
match self {
// Score::Adv => Score::Three,
_ => self,
}
}
}
#[derive(Debug,Clone,Serialize,Deserialize)]
pub struct Player {
pub id: Uuid,
pub img: Option<Uuid>,
pub name: String,
pub vbox: Vbox,
pub constructs: Vec<Construct>,
pub bot: bool,
pub ready: bool,
pub draw_offered: bool,
pub score: Score,
}
impl Player {
pub fn from_account(tx: &mut Transaction, account: &Account) -> Result<Player, Error> {
let constructs = account::team(tx, account)?;
let img = match account.subscribed {
true => Some(account.img),
false => None,
};
Ok(Player {
id: account.id,
img,
name: account.name.clone(),
vbox: Vbox::new(),
constructs,
bot: false,
ready: false,
draw_offered: false,
score: Score::Zero,
})
}
pub fn new(account: Uuid, name: &String, constructs: Vec<Construct>) -> Player {
Player {
id: account,
img: Some(account),
name: name.clone(),
vbox: Vbox::new(),
constructs,
bot: false,
ready: false,
draw_offered: false,
score: Score::Zero,
}
}
pub fn redact(mut self, account: Uuid) -> Player {
// all g
if account == self.id {
return self;
}
// remove vbox
self.vbox = Vbox::new();
// hide skills
for construct in self.constructs.iter_mut() {
construct.skills = vec![];
construct.specs = vec![];
}
self
}
pub fn set_bot(mut self, bot: bool) -> Player {
self.bot = bot;
self
}
pub fn set_ready(&mut self, ready: bool) -> &mut Player {
self.ready = ready;
self
}
pub fn forfeit(&mut self) -> &mut Player {
for construct in self.constructs.iter_mut() {
construct.force_ko();
}
self
}
pub fn set_win(&mut self) -> &mut Player {
self.score = Score::Win;
self
}
pub fn set_lose(&mut self) -> &mut Player {
self.score = Score::Lose;
self
}
pub fn construct_get(&mut self, id: Uuid) -> Result<&mut Construct, Error> {
self.constructs.iter_mut().find(|c| c.id == id).ok_or(err_msg("construct not found"))
}
pub fn autobuy(&mut self) -> &mut Player {
let mut rng = thread_rng();
// skill buying phase
while self.constructs.iter().any(|c| c.skills.len() < 3) {
// find the construct with the smallest number of skills
let construct_id = match self.constructs.iter().min_by_key(|c| c.skills.len()) {
None => panic!("no constructs in autobuy"),
Some(c) => c.id,
};
let i = self.vbox.stash.iter()
.find(|(_i, v)| v.into_skill().is_some())
.map(|(i, _v)| i.clone());
// got a skill in stash
if let Some(i) = i {
// AAAAAAAAAAAAAAAAAAAA
// there's a bad bug here where if this apply fails
// the item in question will be silently dropped
let item = self.vbox.stash.remove(&i).unwrap();
self.vbox_apply(item, construct_id).ok();
continue;
}
// need to buy one
else {
// do we have any colours in store?
let colours = self.vbox.store[&ItemType::Colours].keys()
.cloned()
.take(2)
.collect::<Vec<String>>();
// how about a base skill?
let base = match self.vbox.store[&ItemType::Skills].iter().next() {
Some(b) => Some(b.0.clone()),
None => None,
};
// if no: try to refill and start again
match colours.len() != 2 || base.is_none() {
true => {
match self.vbox_refill() {
Ok(_) => continue,
Err(_) => break, // give up
};
}
false => {
let mut vbox_items = HashMap::new();
vbox_items.insert(ItemType::Colours, colours);
vbox_items.insert(ItemType::Skills, vec![base.unwrap()]);
match self.vbox_combine(vec![], Some(vbox_items)) {
Ok(_) => continue,
Err(_) => break, // give up
}
}
}
}
}
// spec buying phase
while self.constructs.iter().any(|c| c.specs.len() < 3) {
// find the construct with the smallest number of skills
let construct_id = match self.constructs.iter().min_by_key(|c| c.specs.len()) {
None => panic!("no constructs in autobuy"),
Some(c) => c.id,
};
let i = self.vbox.stash.iter()
.find(|(_i, v)| v.into_spec().is_some())
.map(|(i, _v)| i.clone());
// got a skill in stash
if let Some(i) = i {
// AAAAAAAAAAAAAAAAAAAA
// there's a bad bug here where if this apply fails
// the item in question will be silently dropped
let item = self.vbox.stash.remove(&i).unwrap();
self.vbox_apply(item, construct_id).unwrap();
continue;
}
// need to buy one
else {
// do we have any colours in store?
let colours = self.vbox.store[&ItemType::Colours].keys()
.cloned()
.take(2)
.collect::<Vec<String>>();
// how about a base spec?
let base = match self.vbox.store[&ItemType::Specs].iter().next() {
Some(b) => Some(b.0.clone()),
None => None,
};
// if no: try to refill and start again
match colours.len() != 2 || base.is_none() {
true => match self.vbox_refill() {
Ok(_) => continue,
Err(_) => break, // give up
},
false => {
let mut vbox_items = HashMap::new();
vbox_items.insert(ItemType::Colours, colours);
vbox_items.insert(ItemType::Specs, vec![base.unwrap()]);
match self.vbox_combine(vec![], Some(vbox_items)) {
Ok(_) => continue,
Err(_) => break, // give up
}
}
}
}
}
// upgrading phase
// NYI
return self;
}
pub fn vbox_refill(&mut self) -> Result<&mut Player, Error> {
self.vbox.balance_sub(DISCARD_COST)?;
self.vbox.fill();
Ok(self)
}
pub fn bot_vbox_accept(&mut self, group: ItemType) -> Result<&mut Player, Error> {
let item = self.vbox.bot_buy(group)?;
self.vbox.stash_add(item, None)?;
Ok(self)
}
pub fn vbox_buy(&mut self, group: ItemType, index: String, construct_id: Option<Uuid>) -> Result<&mut Player, Error> {
let item = self.vbox.buy(group, &index)?;
match construct_id {
Some(id) => { self.vbox_apply(item, id)?; },
None => { self.vbox.stash_add(item, None)?; },
};
Ok(self)
}
pub fn vbox_combine(&mut self, inv_indices: Vec<String>, vbox_indices: VboxIndices) -> Result<&mut Player, Error> {
self.vbox.combine(inv_indices, vbox_indices)?;
Ok(self)
}
pub fn vbox_refund(&mut self, index: String) -> Result<&mut Player, Error> {
self.vbox.refund(index)?;
Ok(self)
}
pub fn vbox_equip(&mut self, index: String, construct_id: Uuid) -> Result<&mut Player, Error> {
let item = self.vbox.stash.remove(&index)
.ok_or(format_err!("no item at index {:?} {:}", self, &index))?;
self.vbox_apply(item, construct_id)
}
pub fn vbox_apply(&mut self, item: Item, construct_id: Uuid) -> Result<&mut Player, Error> {
match item.effect() {
Some(ItemEffect::Skill) => {
let skill = item.into_skill().ok_or(format_err!("item {:?} has no associated skill", item))?;
let construct = self.construct_get(construct_id)?;
// done here because i teach them a tonne of skills for tests
let max_skills = 3;
if construct.skills.len() >= max_skills {
return Err(format_err!("construct at max skills ({:?})", max_skills));
}
if construct.knows(skill) {
return Err(format_err!("construct already knows skill ({:?})" , skill));
}
construct.learn_mut(skill);
},
Some(ItemEffect::Spec) => {
let spec = item.into_spec().ok_or(format_err!("item {:?} has no associated spec", item))?;
let construct = self.construct_get(construct_id)?;
construct.spec_add(spec)?;
},
None => return Err(err_msg("item has no effect on constructs")),
}
// now the item has been applied
// recalculate the stats of the whole player
let player_colours = self.constructs.iter().fold(Colours::new(), |tc, c| {
Colours {
red: tc.red + c.colours.red,
green: tc.green + c.colours.green,
blue: tc.blue + c.colours.blue
}
});
for construct in self.constructs.iter_mut() {
construct.apply_modifiers(&player_colours);
}
Ok(self)
}
pub fn vbox_unequip(&mut self, target: Item, construct_id: Uuid, target_construct_id: Option<Uuid>) -> Result<&mut Player, Error> {
if self.vbox.stash.len() >= 9 && !target_construct_id.is_some() {
return Err(err_msg("too many items stash"));
}
match target.effect() {
Some(ItemEffect::Skill) => {
let skill = target.into_skill().ok_or(format_err!("item {:?} has no associated skill", target))?;
let construct = self.construct_get(construct_id)?;
construct.forget(skill)?;
},
Some(ItemEffect::Spec) => {
let spec = target.into_spec().ok_or(format_err!("item {:?} has no associated spec", target))?;
let construct = self.construct_get(construct_id)?;
construct.spec_remove(spec)?;
},
None => return Err(err_msg("item has no effect on constructs")),
}
// now the item has been applied
// recalculate the stats of the whole player
let player_colours = self.constructs.iter().fold(Colours::new(), |tc, c| {
Colours {
red: tc.red + c.colours.red,
green: tc.green + c.colours.green,
blue: tc.blue + c.colours.blue
}
});
for construct in self.constructs.iter_mut() {
construct.apply_modifiers(&player_colours);
}
match target_construct_id {
Some(cid) => { self.vbox_apply(target, cid)?; },
None => { self.vbox.stash_add(target, None)?; },
};
Ok(self)
}
// GAME METHODS
pub fn skills_required(&self) -> usize {
let required = self.constructs.iter()
.filter(|c| !c.is_ko())
.filter(|c| c.available_skills().len() > 0)
.collect::<Vec<&Construct>>().len();
// info!("{:} requires {:} skills this turn", self.id, required);
return required;
}
pub fn intercepting(&self) -> Option<&Construct> {
self.constructs.iter()
.find(|c| c.affected(Effect::Intercept))
}
pub fn construct_by_id(&mut self, id: Uuid) -> Option<&mut Construct> {
self.constructs.iter_mut().find(|c| c.id == id)
}
}
pub fn player_create(tx: &mut Transaction, player: Player, instance: Uuid, account: &Account) -> Result<Player, Error> {
let query = "
INSERT INTO players (id, instance, account)
VALUES ($1, $2, $3)
RETURNING id, account;
";
let result = tx
.query(query, &[&Uuid::new_v4(), &instance, &account.id])?;
let _returned = result.iter().next().expect("no row written");
info!("wrote player {:} joined instance: {:}", account.name, instance);
return Ok(player);
}
#[cfg(test)]
mod tests {
use mob::instance_mobs;
use super::*;
#[test]
fn player_bot_vbox_test() {
let player_account = Uuid::new_v4();
let constructs = instance_mobs(player_account);
let mut player = Player::new(player_account, &"test".to_string(), constructs).set_bot(true);
player.vbox.fill();
player.vbox.bits = 100;
player.autobuy();
assert!(player.constructs.iter().all(|c| c.skills.len() > 1));
assert!(player.constructs.iter().all(|c| c.specs.len() >= 1));
}
#[test]
fn player_score_test() {
let player_account = Uuid::new_v4();
let constructs = instance_mobs(player_account);
let mut player = Player::new(player_account, &"test".to_string(), constructs).set_bot(true);
player.score = player.score.add_win(&Score::Zero);
player.score = player.score.add_win(&Score::Zero);
player.score = player.score.add_win(&Score::Zero);
assert_eq!(player.score, Score::Win); // 40 / 0
// Bo7 tennis scoring
/*assert_eq!(player.score, Score::Three); // 40 / 0
player.score = player.score.add_loss(); // adv -> deuce
assert_eq!(player.score, Score::Three);
player.score = player.score.add_loss(); // adv -> deuce
assert_eq!(player.score, Score::Three);
player.score = player.score.add_win(&Score::Adv); // opp adv -> stays deuce
assert_eq!(player.score, Score::Three);
player.score = player.score.add_win(&Score::Three);
assert_eq!(player.score, Score::Adv);
player.score = player.score.add_win(&Score::Three);
assert_eq!(player.score, Score::Win);*/
}
}

View File

@ -1,3 +1,6 @@
use mnml_core::item::ItemInfoCtr;
use mnml_core::instance::ChatState;
use mnml_core::item::item_info;
use std::collections::HashMap; use std::collections::HashMap;
use std::time::{Instant}; use std::time::{Instant};
use std::thread::{spawn}; use std::thread::{spawn};
@ -19,23 +22,47 @@ use crossbeam_channel::{unbounded, Sender as CbSender};
use ws::{Builder, CloseCode, Message, Handler, Request, Response, Settings, Sender as WsSender}; use ws::{Builder, CloseCode, Message, Handler, Request, Response, Settings, Sender as WsSender};
use ws::deflate::DeflateHandler; use ws::deflate::DeflateHandler;
use pg::{
demo,
game_concede,
game_offer_draw,
game_ready,
game_skill,
game_skill_clear,
game_state,
instance_abandon,
instance_practice,
instance_ready,
instance_state,
vbox_apply,
vbox_buy,
vbox_combine,
vbox_refill,
vbox_refund,
vbox_unequip,
};
use account::{Account}; use account::{Account};
use account; use account;
use construct::{Construct};
use events::{Event}; use events::{Event};
use game::{Game, game_state, game_skill, game_skill_clear, game_ready, game_offer_draw, game_concede};
use instance::{Instance, ChatState, instance_state, instance_practice, instance_ready, instance_abandon, demo}; use mnml_core::construct::{Construct};
use item::{Item, ItemInfoCtr, item_info}; use mnml_core::game::{Game};
use mnml_core::player::Player;
use mnml_core::vbox::{ItemType};
use mnml_core::item::Item;
use mnml_core::skill::Skill;
// use mnml_core::skill::{dev_resolve, Resolutions};
use mnml_core::instance::{Instance};
use mtx; use mtx;
use mail; use mail;
use player::{Player};
use payments; use payments;
use mail::Email; use mail::Email;
use pg::{Db}; use pg::{Db};
use pg::{PgPool}; use pg::{PgPool};
use skill::{Skill, dev_resolve, Resolutions};
use vbox::{ItemType, vbox_buy, vbox_apply, vbox_refill, vbox_combine, vbox_refund, vbox_unequip};
use http::{AUTH_CLEAR, TOKEN_HEADER}; use http::{AUTH_CLEAR, TOKEN_HEADER};
#[derive(Debug,Clone,Serialize)] #[derive(Debug,Clone,Serialize)]
@ -61,7 +88,7 @@ pub enum RpcMessage {
Pong(()), Pong(()),
DevResolutions(Resolutions), // DevResolutions(Resolutions),
QueueRequested(()), QueueRequested(()),
QueueJoined(()), QueueJoined(()),
@ -144,8 +171,8 @@ impl Connection {
match v { match v {
RpcRequest::Ping {} => return Ok(RpcMessage::Pong(())), RpcRequest::Ping {} => return Ok(RpcMessage::Pong(())),
RpcRequest::ItemInfo {} => return Ok(RpcMessage::ItemInfo(item_info())), RpcRequest::ItemInfo {} => return Ok(RpcMessage::ItemInfo(item_info())),
RpcRequest::DevResolve {a, b, skill } => // RpcRequest::DevResolve {a, b, skill } =>
return Ok(RpcMessage::DevResolutions(dev_resolve(a, b, skill))), // return Ok(RpcMessage::DevResolutions(dev_resolve(a, b, skill))),
_ => (), _ => (),
}; };

File diff suppressed because it is too large Load Diff

View File

@ -1,734 +0,0 @@
use construct::{Stat, Colours};
use util::{IntPct};
#[derive(Debug,Clone,Serialize,Deserialize)]
pub struct SpecBonus {
pub req: Colours,
pub bonus: u64,
}
impl SpecBonus {
pub fn get_bonus(&self, c: &Colours) -> u64 {
if c.red >= self.req.red && c.blue >= self.req.blue && c.green >= self.req.green {
return self.bonus;
}
return 0;
}
}
#[derive(Debug,Clone,Serialize,Deserialize)]
pub struct SpecValues {
pub base: u64,
pub bonuses: Vec<SpecBonus>,
}
impl SpecValues {
pub fn max_value (&self, c: &Colours) -> u64 {
self.bonuses.iter().fold(self.base, |acc, s| acc + s.get_bonus(c))
}
pub fn base (self) -> u64 {
self.base
}
}
#[derive(Debug,Copy,Clone,Serialize,Deserialize,PartialEq,PartialOrd,Ord,Eq)]
pub enum Spec {
Speed,
SpeedRR,
SpeedBB,
SpeedGG,
SpeedRG,
SpeedGB,
SpeedRB,
SpeedRRPlus,
SpeedBBPlus,
SpeedGGPlus,
SpeedRGPlus,
SpeedGBPlus,
SpeedRBPlus,
SpeedRRPlusPlus,
SpeedBBPlusPlus,
SpeedGGPlusPlus,
SpeedRGPlusPlus,
SpeedGBPlusPlus,
SpeedRBPlusPlus,
Life,
LifeGG,
LifeRR,
LifeBB,
LifeRG,
LifeGB,
LifeRB,
LifeGGPlus,
LifeRRPlus,
LifeBBPlus,
LifeRGPlus,
LifeGBPlus,
LifeRBPlus,
LifeGGPlusPlus,
LifeRRPlusPlus,
LifeBBPlusPlus,
LifeRGPlusPlus,
LifeGBPlusPlus,
LifeRBPlusPlus,
Power,
PowerRR,
PowerGG,
PowerBB,
PowerRG,
PowerGB,
PowerRB,
PowerRRPlus,
PowerGGPlus,
PowerBBPlus,
PowerRGPlus,
PowerGBPlus,
PowerRBPlus,
PowerRRPlusPlus,
PowerGGPlusPlus,
PowerBBPlusPlus,
PowerRGPlusPlus,
PowerGBPlusPlus,
PowerRBPlusPlus,
}
impl Spec {
pub fn affects(&self) -> Vec<Stat> {
match *self {
Spec::Power => vec![Stat::BluePower, Stat::RedPower, Stat::GreenPower],
Spec::PowerRR => vec![Stat::RedPower],
Spec::PowerGG => vec![Stat::GreenPower],
Spec::PowerBB => vec![Stat::BluePower],
Spec::PowerRG => vec![Stat::GreenPower, Stat::RedPower],
Spec::PowerGB => vec![Stat::GreenPower, Stat::BluePower],
Spec::PowerRB => vec![Stat::RedPower, Stat::BluePower],
Spec::PowerRRPlus => vec![Stat::RedPower],
Spec::PowerGGPlus => vec![Stat::GreenPower],
Spec::PowerBBPlus => vec![Stat::BluePower],
Spec::PowerRGPlus => vec![Stat::GreenPower, Stat::RedPower],
Spec::PowerGBPlus => vec![Stat::GreenPower, Stat::BluePower],
Spec::PowerRBPlus => vec![Stat::RedPower, Stat::BluePower],
Spec::PowerRRPlusPlus => vec![Stat::RedPower],
Spec::PowerGGPlusPlus => vec![Stat::GreenPower],
Spec::PowerBBPlusPlus => vec![Stat::BluePower],
Spec::PowerRGPlusPlus => vec![Stat::GreenPower, Stat::RedPower],
Spec::PowerGBPlusPlus => vec![Stat::GreenPower, Stat::BluePower],
Spec::PowerRBPlusPlus => vec![Stat::RedPower, Stat::BluePower],
Spec::Speed => vec![Stat::Speed],
Spec::SpeedRR => vec![Stat::Speed],
Spec::SpeedBB => vec![Stat::Speed],
Spec::SpeedGG => vec![Stat::Speed],
Spec::SpeedRG => vec![Stat::Speed],
Spec::SpeedGB => vec![Stat::Speed],
Spec::SpeedRB => vec![Stat::Speed],
Spec::SpeedRRPlus => vec![Stat::Speed],
Spec::SpeedBBPlus => vec![Stat::Speed],
Spec::SpeedGGPlus => vec![Stat::Speed],
Spec::SpeedRGPlus => vec![Stat::Speed],
Spec::SpeedGBPlus => vec![Stat::Speed],
Spec::SpeedRBPlus => vec![Stat::Speed],
Spec::SpeedRRPlusPlus => vec![Stat::Speed],
Spec::SpeedBBPlusPlus => vec![Stat::Speed],
Spec::SpeedGGPlusPlus => vec![Stat::Speed],
Spec::SpeedRGPlusPlus => vec![Stat::Speed],
Spec::SpeedGBPlusPlus => vec![Stat::Speed],
Spec::SpeedRBPlusPlus => vec![Stat::Speed],
Spec::Life => vec![Stat::GreenLife],
Spec::LifeRR => vec![Stat::RedLife],
Spec::LifeBB => vec![Stat::BlueLife],
Spec::LifeGG => vec![Stat::GreenLife],
Spec::LifeRG => vec![Stat::GreenLife, Stat::RedLife],
Spec::LifeGB => vec![Stat::GreenLife, Stat::BlueLife],
Spec::LifeRB => vec![Stat::BlueLife, Stat::RedLife],
Spec::LifeRRPlus => vec![Stat::RedLife],
Spec::LifeBBPlus => vec![Stat::BlueLife],
Spec::LifeGGPlus => vec![Stat::GreenLife],
Spec::LifeRGPlus => vec![Stat::GreenLife, Stat::RedLife],
Spec::LifeGBPlus => vec![Stat::GreenLife, Stat::BlueLife],
Spec::LifeRBPlus => vec![Stat::BlueLife, Stat::RedLife],
Spec::LifeRRPlusPlus => vec![Stat::RedLife],
Spec::LifeBBPlusPlus => vec![Stat::BlueLife],
Spec::LifeGGPlusPlus => vec![Stat::GreenLife],
Spec::LifeRGPlusPlus => vec![Stat::GreenLife, Stat::RedLife],
Spec::LifeGBPlusPlus => vec![Stat::GreenLife, Stat::BlueLife],
Spec::LifeRBPlusPlus => vec![Stat::BlueLife, Stat::RedLife],
}
}
pub fn values(&self) -> SpecValues {
match *self {
Spec::Power => SpecValues {
base: 10,
bonuses: vec![]
},
Spec::PowerRR=> SpecValues {
base: 10,
bonuses: vec![
SpecBonus { req: Colours { red: 5, green: 0, blue: 0 }, bonus: 5 },
SpecBonus { req: Colours { red: 10, green: 0, blue: 0 }, bonus: 9 },
SpecBonus { req: Colours { red: 20, green: 0, blue: 0 }, bonus: 13 }
],
},
Spec::PowerGG=> SpecValues {
base: 10,
bonuses: vec![
SpecBonus { req: Colours { red: 0, green: 5, blue: 0 }, bonus: 5 },
SpecBonus { req: Colours { red: 0, green: 10, blue: 0 }, bonus: 9 },
SpecBonus { req: Colours { red: 0, green: 20, blue: 0 }, bonus: 13 }
],
},
Spec::PowerBB=> SpecValues {
base: 10,
bonuses: vec![
SpecBonus { req: Colours { red: 0, green: 0, blue: 5 }, bonus: 5 },
SpecBonus { req: Colours { red: 0, green: 0, blue: 10 }, bonus: 9 },
SpecBonus { req: Colours { red: 0, green: 0, blue: 20 }, bonus: 13 }
],
},
Spec::PowerRG=> SpecValues {
base: 10,
bonuses: vec![
SpecBonus { req: Colours { red: 2, green: 2, blue: 0 }, bonus: 3 },
SpecBonus { req: Colours { red: 5, green: 5, blue: 0 }, bonus: 6 },
SpecBonus { req: Colours { red: 10, green: 10, blue: 0 }, bonus: 9 }
],
},
Spec::PowerGB=> SpecValues {
base: 10,
bonuses: vec![
SpecBonus { req: Colours { red: 0, green: 2, blue: 2 }, bonus: 3 },
SpecBonus { req: Colours { red: 0, green: 5, blue: 5 }, bonus: 6 },
SpecBonus { req: Colours { red: 0, green: 10, blue: 10 }, bonus: 9 }
],
},
Spec::PowerRB=> SpecValues {
base: 10,
bonuses: vec![
SpecBonus { req: Colours { red: 2, green: 0, blue: 2 }, bonus: 3 },
SpecBonus { req: Colours { red: 5, green: 0, blue: 5 }, bonus: 6 },
SpecBonus { req: Colours { red: 10, green: 0, blue: 10 }, bonus: 9 }
],
},
Spec::PowerRRPlus => SpecValues {
base: 15,
bonuses: vec![
SpecBonus { req: Colours { red: 5, green: 0, blue: 0 }, bonus: 6 },
SpecBonus { req: Colours { red: 10, green: 0, blue: 0 }, bonus: 12 },
SpecBonus { req: Colours { red: 20, green: 0, blue: 0 }, bonus: 18 }
],
},
Spec::PowerGGPlus => SpecValues {
base: 15,
bonuses: vec![
SpecBonus { req: Colours { red: 0, green: 5, blue: 0 }, bonus: 6 },
SpecBonus { req: Colours { red: 0, green: 10, blue: 0 }, bonus: 12 },
SpecBonus { req: Colours { red: 0, green: 20, blue: 0 }, bonus: 18 }
],
},
Spec::PowerBBPlus => SpecValues {
base: 15,
bonuses: vec![
SpecBonus { req: Colours { red: 0, green: 0, blue: 5 }, bonus: 6 },
SpecBonus { req: Colours { red: 0, green: 0, blue: 10 }, bonus: 12 },
SpecBonus { req: Colours { red: 0, green: 0, blue: 20 }, bonus: 18 }
],
},
Spec::PowerRGPlus => SpecValues {
base: 15,
bonuses: vec![
SpecBonus { req: Colours { red: 2, green: 2, blue: 0 }, bonus: 4 },
SpecBonus { req: Colours { red: 5, green: 5, blue: 0 }, bonus: 8 },
SpecBonus { req: Colours { red: 10, green: 10, blue: 0 }, bonus: 12 }
],
},
Spec::PowerGBPlus => SpecValues {
base: 15,
bonuses: vec![
SpecBonus { req: Colours { red: 0, green: 2, blue: 2 }, bonus: 4 },
SpecBonus { req: Colours { red: 0, green: 5, blue: 5 }, bonus: 8 },
SpecBonus { req: Colours { red: 0, green: 10, blue: 10 }, bonus: 12 }
],
},
Spec::PowerRBPlus => SpecValues {
base: 15,
bonuses: vec![
SpecBonus { req: Colours { red: 2, green: 0, blue: 2 }, bonus: 4 },
SpecBonus { req: Colours { red: 5, green: 0, blue: 5 }, bonus: 8 },
SpecBonus { req: Colours { red: 10, green: 0, blue: 10 }, bonus: 12 }
],
},
Spec::PowerRRPlusPlus => SpecValues {
base: 25,
bonuses: vec![
SpecBonus { req: Colours { red: 5, green: 0, blue: 0 }, bonus: 8 },
SpecBonus { req: Colours { red: 10, green: 0, blue: 0 }, bonus: 16 },
SpecBonus { req: Colours { red: 20, green: 0, blue: 0 }, bonus: 24 }
],
},
Spec::PowerGGPlusPlus => SpecValues {
base: 25,
bonuses: vec![
SpecBonus { req: Colours { red: 0, green: 5, blue: 0 }, bonus: 8 },
SpecBonus { req: Colours { red: 0, green: 10, blue: 0 }, bonus: 16 },
SpecBonus { req: Colours { red: 0, green: 20, blue: 0 }, bonus: 24 }
],
},
Spec::PowerBBPlusPlus => SpecValues {
base: 25,
bonuses: vec![
SpecBonus { req: Colours { red: 0, green: 0, blue: 5 }, bonus: 8 },
SpecBonus { req: Colours { red: 0, green: 0, blue: 10 }, bonus: 16 },
SpecBonus { req: Colours { red: 0, green: 0, blue: 20 }, bonus: 24 }
],
},
Spec::PowerRGPlusPlus => SpecValues {
base: 25,
bonuses: vec![
SpecBonus { req: Colours { red: 2, green: 2, blue: 0 }, bonus: 5 },
SpecBonus { req: Colours { red: 5, green: 5, blue: 0 }, bonus: 10 },
SpecBonus { req: Colours { red: 10, green: 10, blue: 0 }, bonus: 15 }
],
},
Spec::PowerGBPlusPlus => SpecValues {
base: 25,
bonuses: vec![
SpecBonus { req: Colours { red: 0, green: 2, blue: 2 }, bonus: 5 },
SpecBonus { req: Colours { red: 0, green: 5, blue: 5 }, bonus: 10 },
SpecBonus { req: Colours { red: 0, green: 10, blue: 10 }, bonus: 15 }
],
},
Spec::PowerRBPlusPlus => SpecValues {
base: 25,
bonuses: vec![
SpecBonus { req: Colours { red: 2, green: 0, blue: 2 }, bonus: 5 },
SpecBonus { req: Colours { red: 5, green: 0, blue: 5 }, bonus: 10 },
SpecBonus { req: Colours { red: 10, green: 0, blue: 10 }, bonus: 15 }
],
},
Spec::Speed => SpecValues {
base: 40,
bonuses: vec![]
},
Spec::SpeedRR=> SpecValues {
base: 80,
bonuses: vec![
SpecBonus { req: Colours { red: 5, green: 0, blue: 0 }, bonus: 80 },
SpecBonus { req: Colours { red: 10, green: 0, blue: 0 }, bonus: 80 },
SpecBonus { req: Colours { red: 20, green: 0, blue: 0 }, bonus: 80 }
],
},
Spec::SpeedGG=> SpecValues {
base: 80,
bonuses: vec![
SpecBonus { req: Colours { red: 0, green: 5, blue: 0 }, bonus: 80 },
SpecBonus { req: Colours { red: 0, green: 10, blue: 0 }, bonus: 80 },
SpecBonus { req: Colours { red: 0, green: 20, blue: 0 }, bonus: 80 }
],
},
Spec::SpeedBB=> SpecValues {
base: 80,
bonuses: vec![
SpecBonus { req: Colours { red: 0, green: 0, blue: 5 }, bonus: 80 },
SpecBonus { req: Colours { red: 0, green: 0, blue: 10 }, bonus: 80 },
SpecBonus { req: Colours { red: 0, green: 0, blue: 20 }, bonus: 80 }
],
},
Spec::SpeedRG=> SpecValues {
base: 60,
bonuses: vec![
SpecBonus { req: Colours { red: 2, green: 2, blue: 0 }, bonus: 60 },
SpecBonus { req: Colours { red: 5, green: 5, blue: 0 }, bonus: 60 },
SpecBonus { req: Colours { red: 10, green: 10, blue: 0 }, bonus: 60 }
],
},
Spec::SpeedGB=> SpecValues {
base: 60,
bonuses: vec![
SpecBonus { req: Colours { red: 0, green: 2, blue: 2 }, bonus: 60 },
SpecBonus { req: Colours { red: 0, green: 5, blue: 5 }, bonus: 60 },
SpecBonus { req: Colours { red: 0, green: 10, blue: 10 }, bonus: 60 }
],
},
Spec::SpeedRB=> SpecValues {
base: 60,
bonuses: vec![
SpecBonus { req: Colours { red: 2, green: 0, blue: 2 }, bonus: 60 },
SpecBonus { req: Colours { red: 5, green: 0, blue: 5 }, bonus: 60 },
SpecBonus { req: Colours { red: 10, green: 0, blue: 10 }, bonus: 60 }
],
},
Spec::SpeedRRPlus => SpecValues {
base: 120,
bonuses: vec![
SpecBonus { req: Colours { red: 5, green: 0, blue: 0 }, bonus: 120 },
SpecBonus { req: Colours { red: 10, green: 0, blue: 0 }, bonus: 120 },
SpecBonus { req: Colours { red: 20, green: 0, blue: 0 }, bonus: 120 }
],
},
Spec::SpeedGGPlus => SpecValues {
base: 120,
bonuses: vec![
SpecBonus { req: Colours { red: 0, green: 5, blue: 0 }, bonus: 120 },
SpecBonus { req: Colours { red: 0, green: 10, blue: 0 }, bonus: 120 },
SpecBonus { req: Colours { red: 0, green: 20, blue: 0 }, bonus: 120 }
],
},
Spec::SpeedBBPlus => SpecValues {
base: 120,
bonuses: vec![
SpecBonus { req: Colours { red: 0, green: 0, blue: 5 }, bonus: 120 },
SpecBonus { req: Colours { red: 0, green: 0, blue: 10 }, bonus: 120 },
SpecBonus { req: Colours { red: 0, green: 0, blue: 20 }, bonus: 120 }
],
},
Spec::SpeedRGPlus => SpecValues {
base: 80,
bonuses: vec![
SpecBonus { req: Colours { red: 2, green: 2, blue: 0 }, bonus: 80 },
SpecBonus { req: Colours { red: 5, green: 5, blue: 0 }, bonus: 80 },
SpecBonus { req: Colours { red: 10, green: 10, blue: 0 }, bonus: 80 }
],
},
Spec::SpeedGBPlus => SpecValues {
base: 80,
bonuses: vec![
SpecBonus { req: Colours { red: 0, green: 2, blue: 2 }, bonus: 80 },
SpecBonus { req: Colours { red: 0, green: 5, blue: 5 }, bonus: 80 },
SpecBonus { req: Colours { red: 0, green: 10, blue: 10 }, bonus: 80 }
],
},
Spec::SpeedRBPlus => SpecValues {
base: 80,
bonuses: vec![
SpecBonus { req: Colours { red: 2, green: 0, blue: 2 }, bonus: 80 },
SpecBonus { req: Colours { red: 5, green: 0, blue: 5 }, bonus: 80 },
SpecBonus { req: Colours { red: 10, green: 0, blue: 10 }, bonus: 80 }
],
},
Spec::SpeedRRPlusPlus => SpecValues {
base: 160,
bonuses: vec![
SpecBonus { req: Colours { red: 5, green: 0, blue: 0 }, bonus: 160 },
SpecBonus { req: Colours { red: 10, green: 0, blue: 0 }, bonus: 160 },
SpecBonus { req: Colours { red: 20, green: 0, blue: 0 }, bonus: 160 }
],
},
Spec::SpeedGGPlusPlus => SpecValues {
base: 160,
bonuses: vec![
SpecBonus { req: Colours { red: 0, green: 5, blue: 0 }, bonus: 160 },
SpecBonus { req: Colours { red: 0, green: 10, blue: 0 }, bonus: 160 },
SpecBonus { req: Colours { red: 0, green: 20, blue: 0 }, bonus: 160 }
],
},
Spec::SpeedBBPlusPlus => SpecValues {
base: 160,
bonuses: vec![
SpecBonus { req: Colours { red: 0, green: 0, blue: 5 }, bonus: 160 },
SpecBonus { req: Colours { red: 0, green: 0, blue: 10 }, bonus: 160 },
SpecBonus { req: Colours { red: 0, green: 0, blue: 20 }, bonus: 160 }
],
},
Spec::SpeedRGPlusPlus => SpecValues {
base: 120,
bonuses: vec![
SpecBonus { req: Colours { red: 2, green: 2, blue: 0 }, bonus: 120 },
SpecBonus { req: Colours { red: 5, green: 5, blue: 0 }, bonus: 120 },
SpecBonus { req: Colours { red: 10, green: 10, blue: 0 }, bonus: 120 }
],
},
Spec::SpeedGBPlusPlus => SpecValues {
base: 120,
bonuses: vec![
SpecBonus { req: Colours { red: 0, green: 2, blue: 2 }, bonus: 120 },
SpecBonus { req: Colours { red: 0, green: 5, blue: 5 }, bonus: 120 },
SpecBonus { req: Colours { red: 0, green: 10, blue: 10 }, bonus: 120 }
],
},
Spec::SpeedRBPlusPlus => SpecValues {
base: 120,
bonuses: vec![
SpecBonus { req: Colours { red: 2, green: 0, blue: 2 }, bonus: 120 },
SpecBonus { req: Colours { red: 5, green: 0, blue: 5 }, bonus: 120 },
SpecBonus { req: Colours { red: 10, green: 0, blue: 10 }, bonus: 120 }
],
},
Spec::Life => SpecValues {
base: 125,
bonuses: vec![]},
Spec::LifeRR=> SpecValues {
base: 275,
bonuses: vec![
SpecBonus { req: Colours { red: 5, green: 0, blue: 0 }, bonus: 75 },
SpecBonus { req: Colours { red: 10, green: 0, blue: 0 }, bonus: 125 },
SpecBonus { req: Colours { red: 20, green: 0, blue: 0 }, bonus: 175 }
],
},
Spec::LifeGG=> SpecValues {
base: 225,
bonuses: vec![
SpecBonus { req: Colours { red: 0, green: 5, blue: 0 }, bonus: 50 },
SpecBonus { req: Colours { red: 0, green: 10, blue: 0 }, bonus: 75 },
SpecBonus { req: Colours { red: 0, green: 20, blue: 0 }, bonus: 125 }
],
},
Spec::LifeBB=> SpecValues {
base: 275,
bonuses: vec![
SpecBonus { req: Colours { red: 0, green: 0, blue: 5 }, bonus: 75 },
SpecBonus { req: Colours { red: 0, green: 0, blue: 10 }, bonus: 125 },
SpecBonus { req: Colours { red: 0, green: 0, blue: 20 }, bonus: 175 }
],
},
Spec::LifeRG=> SpecValues {
base: 125,
bonuses: vec![
SpecBonus { req: Colours { red: 2, green: 2, blue: 0 }, bonus: 50 },
SpecBonus { req: Colours { red: 5, green: 5, blue: 0 }, bonus: 75 },
SpecBonus { req: Colours { red: 10, green: 10, blue: 0 }, bonus: 125 }
],
},
Spec::LifeGB=> SpecValues {
base: 125,
bonuses: vec![
SpecBonus { req: Colours { red: 0, green: 2, blue: 2 }, bonus: 50 },
SpecBonus { req: Colours { red: 0, green: 5, blue: 5 }, bonus: 75 },
SpecBonus { req: Colours { red: 0, green: 10, blue: 10 }, bonus: 125 }
],
},
Spec::LifeRB=> SpecValues {
base: 175,
bonuses: vec![
SpecBonus { req: Colours { red: 2, green: 0, blue: 2 }, bonus: 50 },
SpecBonus { req: Colours { red: 5, green: 0, blue: 5 }, bonus: 75 },
SpecBonus { req: Colours { red: 10, green: 0, blue: 10 }, bonus: 125 }
],
},
Spec::LifeRRPlus => SpecValues {
base: 500,
bonuses: vec![
SpecBonus { req: Colours { red: 5, green: 0, blue: 0 }, bonus: 125 },
SpecBonus { req: Colours { red: 10, green: 0, blue: 0 }, bonus: 225 },
SpecBonus { req: Colours { red: 20, green: 0, blue: 0 }, bonus: 300 }
],
},
Spec::LifeGGPlus => SpecValues {
base: 400,
bonuses: vec![
SpecBonus { req: Colours { red: 0, green: 5, blue: 0 }, bonus: 90 },
SpecBonus { req: Colours { red: 0, green: 10, blue: 0 }, bonus: 130 },
SpecBonus { req: Colours { red: 0, green: 20, blue: 0 }, bonus: 225 }
],
},
Spec::LifeBBPlus => SpecValues {
base: 500,
bonuses: vec![
SpecBonus { req: Colours { red: 0, green: 0, blue: 5 }, bonus: 125 },
SpecBonus { req: Colours { red: 0, green: 0, blue: 10 }, bonus: 225 },
SpecBonus { req: Colours { red: 0, green: 0, blue: 20 }, bonus: 300 }
],
},
Spec::LifeRGPlus => SpecValues {
base: 225,
bonuses: vec![
SpecBonus { req: Colours { red: 2, green: 2, blue: 0 }, bonus: 100 },
SpecBonus { req: Colours { red: 5, green: 5, blue: 0 }, bonus: 150 },
SpecBonus { req: Colours { red: 10, green: 10, blue: 0 }, bonus: 225 }
],
},
Spec::LifeGBPlus => SpecValues {
base: 225,
bonuses: vec![
SpecBonus { req: Colours { red: 0, green: 2, blue: 2 }, bonus: 100 },
SpecBonus { req: Colours { red: 0, green: 5, blue: 5 }, bonus: 150 },
SpecBonus { req: Colours { red: 0, green: 10, blue: 10 }, bonus: 225 }
],
},
Spec::LifeRBPlus => SpecValues {
base: 350,
bonuses: vec![
SpecBonus { req: Colours { red: 2, green: 0, blue: 2 }, bonus: 100 },
SpecBonus { req: Colours { red: 5, green: 0, blue: 5 }, bonus: 150 },
SpecBonus { req: Colours { red: 10, green: 0, blue: 10 }, bonus: 225 }
],
},
Spec::LifeRRPlusPlus => SpecValues {
base: 875,
bonuses: vec![
SpecBonus { req: Colours { red: 5, green: 0, blue: 0 }, bonus: 225 },
SpecBonus { req: Colours { red: 10, green: 0, blue: 0 }, bonus: 400 },
SpecBonus { req: Colours { red: 20, green: 0, blue: 0 }, bonus: 525 }
],
},
Spec::LifeGGPlusPlus => SpecValues {
base: 475,
bonuses: vec![
SpecBonus { req: Colours { red: 0, green: 5, blue: 0 }, bonus: 130 },
SpecBonus { req: Colours { red: 0, green: 10, blue: 0 }, bonus: 225 },
SpecBonus { req: Colours { red: 0, green: 20, blue: 0 }, bonus: 300 }
],
},
Spec::LifeBBPlusPlus => SpecValues {
base: 875,
bonuses: vec![
SpecBonus { req: Colours { red: 0, green: 0, blue: 5 }, bonus: 225 },
SpecBonus { req: Colours { red: 0, green: 0, blue: 10 }, bonus: 400 },
SpecBonus { req: Colours { red: 0, green: 0, blue: 20 }, bonus: 525 }
],
},
Spec::LifeRGPlusPlus => SpecValues {
base: 400,
bonuses: vec![
SpecBonus { req: Colours { red: 2, green: 2, blue: 0 }, bonus: 175 },
SpecBonus { req: Colours { red: 5, green: 5, blue: 0 }, bonus: 275 },
SpecBonus { req: Colours { red: 10, green: 10, blue: 0 }, bonus: 400 }
],
},
Spec::LifeGBPlusPlus => SpecValues {
base: 625,
bonuses: vec![
SpecBonus { req: Colours { red: 0, green: 2, blue: 2 }, bonus: 175 },
SpecBonus { req: Colours { red: 0, green: 5, blue: 5 }, bonus: 275 },
SpecBonus { req: Colours { red: 0, green: 10, blue: 10 }, bonus: 400 }
],
},
Spec::LifeRBPlusPlus => SpecValues {
base: 400,
bonuses: vec![
SpecBonus { req: Colours { red: 2, green: 0, blue: 2 }, bonus: 175 },
SpecBonus { req: Colours { red: 5, green: 0, blue: 5 }, bonus: 275 },
SpecBonus { req: Colours { red: 10, green: 0, blue: 10 }, bonus: 400 }
],
},
}
}
pub fn apply(&self, modified: u64, base: u64, player_colours: &Colours) -> u64 {
match *self {
// Percentage multipliers based on base value
Spec::Power |
Spec::Speed => modified + base.pct(self.values().base),
Spec::PowerRR|
Spec::PowerGG|
Spec::PowerBB|
Spec::PowerRG|
Spec::PowerGB|
Spec::PowerRB|
Spec::PowerRRPlus |
Spec::PowerGGPlus |
Spec::PowerBBPlus |
Spec::PowerRGPlus |
Spec::PowerGBPlus |
Spec::PowerRBPlus |
Spec::PowerRRPlusPlus |
Spec::PowerGGPlusPlus |
Spec::PowerBBPlusPlus |
Spec::PowerRGPlusPlus |
Spec::PowerGBPlusPlus |
Spec::PowerRBPlusPlus |
Spec::SpeedRR|
Spec::SpeedGG|
Spec::SpeedBB|
Spec::SpeedRG|
Spec::SpeedGB|
Spec::SpeedRB|
Spec::SpeedRRPlus |
Spec::SpeedGGPlus |
Spec::SpeedBBPlus |
Spec::SpeedRGPlus |
Spec::SpeedGBPlus |
Spec::SpeedRBPlus |
Spec::SpeedRRPlusPlus |
Spec::SpeedGGPlusPlus |
Spec::SpeedBBPlusPlus |
Spec::SpeedRGPlusPlus |
Spec::SpeedGBPlusPlus |
Spec::SpeedRBPlusPlus => modified + base.pct(self.values().max_value(player_colours)),
// Flat bonus
Spec::Life => modified + self.values().base,
Spec::LifeRR|
Spec::LifeGG|
Spec::LifeBB|
Spec::LifeRG|
Spec::LifeGB|
Spec::LifeRB|
Spec::LifeRRPlus |
Spec::LifeGGPlus |
Spec::LifeBBPlus |
Spec::LifeRGPlus |
Spec::LifeGBPlus |
Spec::LifeRBPlus |
Spec::LifeRRPlusPlus |
Spec::LifeGGPlusPlus |
Spec::LifeBBPlusPlus |
Spec::LifeRGPlusPlus |
Spec::LifeGBPlusPlus |
Spec::LifeRBPlusPlus => modified + self.values().max_value(player_colours),
}
}
}

View File

@ -1,40 +0,0 @@
// use net::Db;
// Db Commons
// use failure::Error;
// pub fn startup(db: Db) -> Result<(), Error> {
// let tx = db.transaction()?;
// info!("running startup fns");
// match tx.commit() {
// Ok(_) => {
// info!("startup processes completed");
// Ok(())
// },
// Err(e) => Err(format_err!("failed to commit startup tx {:?}", e)),
// }
// }
pub trait IntPct {
fn pct(self, pct: u64) -> u64;
}
impl IntPct for u64 {
fn pct(self, pct: u64) -> u64 {
self * pct / 100
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn int_pct_test() {
assert_eq!(100.pct(110), 110);
assert_eq!(100.pct(50), 50);
assert_eq!(1.pct(200), 2);
assert_eq!(1.pct(50), 0);
}
}

View File

@ -1,345 +0,0 @@
use uuid::Uuid;
use std::iter;
use std::collections::HashMap;
// refunds
use rand::prelude::*;
use rand::{thread_rng};
use rand::distributions::{WeightedIndex};
use postgres::transaction::Transaction;
use failure::Error;
use failure::err_msg;
use account::Account;
use instance::{Instance, instance_get, instance_update};
use construct::{Colours};
use item::*;
pub type VboxIndices = Option<HashMap<ItemType, Vec<String>>>;
#[derive(Debug,Clone,Serialize,Deserialize)]
pub struct Vbox {
pub bits: usize,
pub store: HashMap<ItemType, HashMap<String, Item>>,
pub stash: HashMap<String, Item>,
}
#[derive(Debug,Copy,Clone,Serialize,Deserialize,Hash,PartialEq,Eq)]
pub enum ItemType {
Colours,
Skills,
Specs,
}
const STORE_COLOURS_CAPACITY: usize = 6;
const STORE_SKILLS_CAPACITY: usize = 3;
const STORE_SPECS_CAPACITY: usize = 3;
const STASH_CAPACITY: usize = 6;
const STARTING_ATTACK_COUNT: usize = 3;
impl Vbox {
pub fn new() -> Vbox {
let mut colours: HashMap<String, Item> = HashMap::new();
let mut skills: HashMap<String, Item> = HashMap::new();
let mut specs: HashMap<String, Item> = HashMap::new();
let store = [
(ItemType::Colours, colours),
(ItemType::Skills, skills),
(ItemType::Colours, specs),
].iter().cloned().collect();
let mut stash = HashMap::new();
for i in 0..STARTING_ATTACK_COUNT {
stash.insert(i.to_string(), Item::Attack);
}
Vbox {
store,
stash,
bits: 30,
}
}
pub fn balance_sub(&mut self, amount: usize) -> Result<&mut Vbox, Error> {
let new_balance = self.bits
.checked_sub(amount)
.ok_or(format_err!("insufficient balance: {:?}", self.bits))?;
self.bits = new_balance;
Ok(self)
}
pub fn balance_add(&mut self, amount: usize) -> &mut Vbox {
self.bits = self.bits.saturating_add(amount);
self
}
pub fn fill(&mut self) -> &mut Vbox {
let mut rng = thread_rng();
let colours = vec![
(Item::Red, 1),
(Item::Green, 1),
(Item::Blue, 1),
];
let colour_dist = WeightedIndex::new(colours.iter().map(|item| item.1)).unwrap();
let skills = vec![
(Item::Attack, 1),
(Item::Block, 1),
(Item::Buff, 1),
(Item::Debuff, 1),
(Item::Stun, 1),
];
let skill_dist = WeightedIndex::new(skills.iter().map(|item| item.1)).unwrap();
let specs = vec![
(Item::Power, 1),
(Item::Life, 1),
(Item::Speed, 1),
];
let spec_dist = WeightedIndex::new(specs.iter().map(|item| item.1)).unwrap();
for item_type in [ItemType::Colours, ItemType::Skills, ItemType::Specs].iter() {
let (items, num, dist) = match item_type {
ItemType::Colours => (&colours, STORE_COLOURS_CAPACITY, &colour_dist),
ItemType::Skills => (&skills, STORE_SKILLS_CAPACITY, &skill_dist),
ItemType::Specs => (&specs, STORE_SPECS_CAPACITY, &spec_dist),
};
let drops = iter::repeat_with(|| items[dist.sample(&mut rng)].0)
.take(num)
.enumerate()
.map(|(i, item)| (i.to_string(), item))
.collect::<HashMap<String, Item>>();
self.store.insert(*item_type, drops);
}
self
}
pub fn buy(&mut self, item: ItemType, i: &String) -> Result<Item, Error> {
// check item exists
let selection = self.store
.get_mut(&item).ok_or(format_err!("no item group {:?}", item))?
.remove(i).ok_or(format_err!("no item at index {:?} {:}", self, i))?;
self.balance_sub(selection.cost())?;
Ok(selection)
}
pub fn stash_add(&mut self, item: Item, index: Option<&String>) -> Result<String, Error> {
if self.stash.len() >= STASH_CAPACITY {
return Err(err_msg("stash full"));
}
if let Some(index) = index {
if self.stash.contains_key(index) {
return Err(format_err!("slot occupied {:?}", index));
}
self.stash.insert(index.clone(), item);
return Ok(index.to_string());
}
for i in (0..STASH_CAPACITY).map(|i| i.to_string()) {
if !self.stash.contains_key(&i) {
self.stash.insert(i.clone(), item);
return Ok(i);
}
}
return Err(err_msg("stash full"));
}
pub fn bot_buy(&mut self, item: ItemType) -> Result<Item, Error> {
let buy_index = self.store[&item]
.keys()
.next()
.ok_or(format_err!("no item in group {:?}", item))?
.clone();
self.buy(item, &buy_index)
}
pub fn refund(&mut self, i: String) -> Result<&mut Vbox, Error> {
let refunded = self.stash.remove(&i)
.ok_or(format_err!("no item at index {:?} {:?}", self.stash, i))?;
let refund = refunded.cost();
// info!("refunding {:?} for {:?}", refund, refunded);
self.balance_add(refund);
Ok(self)
}
pub fn combine(&mut self, stash_indices: Vec<String>, store_indices: Option<HashMap<ItemType, Vec<String>>>) -> Result<&mut Vbox, Error> {
// find base item for index to insert into
let base_index = stash_indices.iter()
.find(|i| match self.stash.get(i.clone()) {
Some(item) => item.into_skill().is_some(),
None => false,
});
let mut input = stash_indices
.iter()
.map(|i| self.stash.remove(i)
.ok_or(format_err!("no item at index {:?} {:?}", self.stash, i)))
.collect::<Result<Vec<Item>, Error>>()?;
if let Some(store_indices) = store_indices {
let mut purchased = store_indices.iter()
.map(|(g, list)|
list.iter()
.map(|i| self.buy(*g, i))
.collect::<Result<Vec<Item>, Error>>()
)
.collect::<Result<Vec<Vec<Item>>, Error>>()?
.into_iter()
.flatten()
.collect();
input.append(&mut purchased);
}
// sort the input to align with the combinations
// combos are sorted when created
input.sort_unstable();
let combos = get_combos();
let combo = combos.iter().find(|c| c.components == input).ok_or(err_msg("not a combo"))?;
self.stash_add(combo.item, base_index)?;
Ok(self)
}
}
pub fn vbox_refill(tx: &mut Transaction, account: &Account, instance_id: Uuid) -> Result<Instance, Error> {
let instance = instance_get(tx, instance_id)?
.vbox_refill(account.id)?;
return instance_update(tx, instance);
}
pub fn vbox_buy(tx: &mut Transaction, account: &Account, instance_id: Uuid, group: ItemType, index: String, construct_id: Option<Uuid>) -> Result<Instance, Error> {
let instance = instance_get(tx, instance_id)?
.vbox_buy(account.id, group, index, construct_id)?;
return instance_update(tx, instance);
}
pub fn vbox_combine(tx: &mut Transaction, account: &Account, instance_id: Uuid, stash_indices: Vec<String>, vbox_indices: VboxIndices) -> Result<Instance, Error> {
let instance = instance_get(tx, instance_id)?
.vbox_combine(account.id, stash_indices, vbox_indices)?;
return instance_update(tx, instance);
}
pub fn vbox_refund(tx: &mut Transaction, account: &Account, instance_id: Uuid, index: String) -> Result<Instance, Error> {
let instance = instance_get(tx, instance_id)?
.vbox_refund(account.id, index)?;
return instance_update(tx, instance);
}
pub fn vbox_apply(tx: &mut Transaction, account: &Account, instance_id: Uuid, construct_id: Uuid, index: String) -> Result<Instance, Error> {
let instance = instance_get(tx, instance_id)?
.vbox_apply(account.id, index, construct_id)?;
return instance_update(tx, instance);
}
pub fn vbox_unequip(tx: &mut Transaction, account: &Account, instance_id: Uuid, construct_id: Uuid, target: Item, target_construct_id: Option<Uuid>) -> Result<Instance, Error> {
let instance = instance_get(tx, instance_id)?
.vbox_unequip(account.id, target, construct_id, target_construct_id)?;
return instance_update(tx, instance);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn combine_test() {
let mut vbox = Vbox::new();
vbox.stash.insert(0.to_string(), Item::Attack);
vbox.stash.insert(1.to_string(), Item::Green);
vbox.stash.insert(2.to_string(), Item::Green);
vbox.combine(vec![0.to_string(), 1.to_string(), 2.to_string()], None).unwrap();
assert_eq!(vbox.stash["0"], Item::Heal);
}
#[test]
fn buy_test() {
let mut vbox = Vbox::new();
vbox.fill();
// cannot rebuy same
vbox.buy(ItemType::Skills, &0.to_string()).unwrap();
assert!(vbox.store[&ItemType::Skills].get(&0.to_string()).is_none());
assert!(vbox.buy(ItemType::Skills, &0.to_string()).is_err());
}
#[test]
fn capacity_test() {
let mut vbox = Vbox::new();
vbox.fill();
vbox.stash_add(Item::Red, None).unwrap();
vbox.stash_add(Item::Red, None).unwrap();
vbox.stash_add(Item::Red, None).unwrap();
assert!(vbox.stash_add(Item::Red, None).is_err());
}
#[test]
fn store_and_stash_combine_test() {
let mut vbox = Vbox::new();
vbox.fill();
let mut skill_combine_args = HashMap::new();
skill_combine_args.insert(ItemType::Colours, vec![0.to_string(), 1.to_string()]);
skill_combine_args.insert(ItemType::Skills, vec![0.to_string()]);
let mut spec_combine_args = HashMap::new();
spec_combine_args.insert(ItemType::Colours, vec![2.to_string(), 3.to_string()]);
spec_combine_args.insert(ItemType::Specs, vec![0.to_string()]);
vbox.combine(vec![], Some(skill_combine_args)).unwrap();
vbox.combine(vec![], Some(spec_combine_args)).unwrap();
}
#[test]
fn combos_test() {
let mut input = vec![Item::Green, Item::Attack, Item::Green];
let combos = get_combos();
// sort input so they align
input.sort_unstable();
let combo = combos.iter().find(|c| c.components == input);
assert!(combo.is_some());
}
#[test]
fn refund_test() {
let mut vbox = Vbox::new();
vbox.stash.insert(0.to_string(), Item::Strike);
vbox.refund(0.to_string()).unwrap();
assert_eq!(vbox.bits, 32);
}
#[test]
fn colours_count_test() {
let strike = Item::Strike;
let mut count = Colours::new();
strike.colours(&mut count);
assert_eq!(count.red, 2);
}
// #[test]
// fn item_info_test() {
// info!("{:#?}", item_info());
// }
}

View File

@ -9,12 +9,20 @@ use postgres::transaction::Transaction;
use failure::Error; use failure::Error;
use account; use account;
use game::{games_need_upkeep, game_update, game_write, game_delete};
use instance;
use instance::{instances_need_upkeep, instances_idle, instance_update, instance_delete};
use pg::{Db, PgPool};
use events::{Event, EventsTx, PvpRequest}; use events::{Event, EventsTx, PvpRequest};
use rpc::{RpcMessage}; use rpc::{RpcMessage};
use pg::{
PgPool,
games_need_upkeep,
game_update,
game_write,
game_delete,
instances_need_upkeep,
instances_idle,
instance_update,
pvp,
};
type Id = usize; type Id = usize;
type Pair = (PvpRequest, PvpRequest); type Pair = (PvpRequest, PvpRequest);
@ -100,7 +108,7 @@ impl Warden {
let a = account::select(&db, pair.0.account)?; let a = account::select(&db, pair.0.account)?;
let b = account::select(&db, pair.1.account)?; let b = account::select(&db, pair.1.account)?;
let instance = instance::pvp(&mut tx, &a, &b)?; let instance = pvp(&mut tx, &a, &b)?;
tx.commit()?; tx.commit()?;
// subscribe users to instance events // subscribe users to instance events

View File

@ -1,417 +0,0 @@
use uuid::Uuid;
use petgraph::graph::{Graph, UnGraph, NodeIndex};
// use petgraph::dot::{Dot, Config};
// Db Commons
use account::Account;
use serde_cbor::{from_slice, to_vec};
use postgres::transaction::Transaction;
use failure::Error;
use failure::err_msg;
// shapes
use rand::prelude::*;
use rand::{thread_rng};
use rand::distributions::{WeightedIndex};
use game::{Game, GameMode, game_pve_new, game_write};
use rpc::{ZoneJoinParams, ZoneCloseParams};
#[derive(Debug,Clone,Serialize,Deserialize)]
pub struct Zone {
id: Uuid,
account: Uuid,
active: bool,
graph: UnGraph<Encounter, ()>,
}
#[derive(Debug,Clone,Copy)]
enum Shape {
Diamond,
Line,
Plus,
Diode,
Domino,
Kite,
}
pub type ZoneGraph = UnGraph<Encounter, ()>;
#[derive(Debug,Clone,PartialEq,Eq,Hash,PartialOrd,Ord,Serialize,Deserialize)]
pub struct Encounter {
tag: String,
game_id: Option<Uuid>,
success: bool,
x: i8,
y: i8,
}
impl Encounter {
fn new(tag: &'static str, x: i8, y: i8) -> Encounter {
return Encounter {
tag: tag.to_string(),
success: false,
game_id: None,
x,
y,
};
}
fn start() -> Encounter {
return Encounter {
tag: "START".to_string(),
success: true,
game_id: None,
x: 0,
y: 0,
};
}
}
pub fn zone_delete(tx: &mut Transaction, id: Uuid) -> Result<(), Error> {
let query = "
DELETE
FROM zones
WHERE id = $1;
";
let result = tx
.execute(query, &[&id])?;
if result != 1 {
return Err(format_err!("unable to delete zone {:?}", id));
}
info!("zone deleted {:?}", id);
return Ok(());
}
pub fn zone_get(tx: &mut Transaction, id: Uuid) -> Result<Zone, Error> {
let query = "
SELECT *
FROM zones
WHERE id = $1
";
let result = tx
.query(query, &[&id])?;
let returned = match result.iter().next() {
Some(row) => row,
None => return Err(err_msg("zone not found")),
};
// tells from_slice to cast into a construct
let bytes: Vec<u8> = returned.get("data");
let zone = match from_slice::<Zone>(&bytes) {
Ok(z) => z,
Err(_) => {
zone_delete(tx, id)?;
return Err(err_msg("invalid zone removed"))
},
};
return Ok(zone);
}
pub fn zone_create(tx: &mut Transaction, account: &Account) -> Result<Zone, Error> {
let id = Uuid::new_v4();
let graph = create_zone_graph();
let zone = Zone {
id,
account: account.id,
graph,
active: true,
};
let bytes = to_vec(&zone)?;
let query = "
INSERT INTO zones (id, data, account)
VALUES ($1, $2, $3)
RETURNING id;
";
let result = tx
.query(query, &[&id, &bytes, &account.id])?;
result.iter().next().ok_or(format_err!("no zone written"))?;
return Ok(zone);
}
pub fn zone_update(zone: &Zone, tx: &mut Transaction) -> Result<(), Error> {
let bytes = to_vec(&zone)?;
let query = "
UPDATE zones
SET data = $1, active = $2
WHERE id = $3
RETURNING id, data;
";
let result = tx
.query(query, &[&bytes, &zone.active, &zone.id])?;
result.iter().next().ok_or(format_err!("zone {:?} could not be written", zone))?;
return Ok(());
}
pub fn zone_join(params: ZoneJoinParams, tx: &mut Transaction, account: &Account) -> Result<Game, Error> {
let mut zone = zone_get(tx, params.zone_id)?;
let mut game;
// check node joinable
node_joinable(&zone.graph, NodeIndex::from(params.node_id))?;
// borrow zone.graph to make the game
{
let node_index = NodeIndex::from(params.node_id);
let encounter = zone.graph
.node_weight_mut(node_index)
.ok_or(err_msg("invalid encounter id"))?;
let mode = match encounter.tag.as_ref() {
"NORMAL" => GameMode::Zone3v2Attack,
"CASTER" => GameMode::Zone2v2Caster,
"MINIBOSS" => GameMode::Zone3v3MeleeMiniboss,
"BOSS" => GameMode::Zone3v3HealerBoss,
_ => return Err(err_msg("unknown zone tag")),
};
game = game_pve_new(params.construct_ids, mode, tx, account)?;
game.set_zone(zone.id, params.node_id);
encounter.game_id = Some(game.id);
}
// persist
game_write(tx, &game)?;
zone_update(&zone, tx)?;
return Ok(game);
}
pub fn zone_close(params: ZoneCloseParams, tx: &mut Transaction, _account: &Account) -> Result<(), Error> {
let mut zone = zone_get(tx, params.zone_id)?;
zone.active = false;
zone_update(&zone, tx)?;
return Ok(());
}
// shapes should always add the exit normal node
fn add_shape(shape: Shape, gr: &mut ZoneGraph, start: NodeIndex, x: i8, y: i8) -> (NodeIndex, i8, i8) {
match shape {
Shape::Line => {
let mut next = gr.add_node(Encounter::new("CASTER", x, y));
gr.add_edge(start, next, ());
let exit = gr.add_node(Encounter::new("NORMAL", x + 1, y));
gr.add_edge(next, exit, ());
return (exit, x + 1, y);
},
Shape::Plus => {
let top = gr.add_node(Encounter::new("MINIBOSS", x, y + 1));
gr.add_edge(start, top, ());
let bottom = gr.add_node(Encounter::new("MINIBOSS", x, y - 1));
gr.add_edge(start, bottom, ());
let exit = gr.add_node(Encounter::new("NORMAL", x + 1, y));
gr.add_edge(start, exit, ());
return (exit, x + 1, y);
},
Shape::Diode => {
let top = gr.add_node(Encounter::new("MINIBOSS", x, y + 1));
gr.add_edge(start, top, ());
let bottom = gr.add_node(Encounter::new("MINIBOSS", x, y - 1));
gr.add_edge(start, bottom, ());
let exit = gr.add_node(Encounter::new("NORMAL", x + 1, y));
gr.add_edge(start, exit, ());
// connect top and exit for an extra chance
gr.add_edge(top, exit, ());
gr.add_edge(bottom, exit, ());
return (exit, x + 1, y);
},
Shape::Kite => {
let top = gr.add_node(Encounter::new("MINIBOSS", x + 1, y + 1));
gr.add_edge(start, top, ());
let top_tip = gr.add_node(Encounter::new("BOSS", x + 1, y + 2));
gr.add_edge(top, top_tip, ());
let bottom = gr.add_node(Encounter::new("MINIBOSS", x + 1, y - 1));
gr.add_edge(start, bottom, ());
let bottom_tip = gr.add_node(Encounter::new("BOSS", x + 1, y - 2));
gr.add_edge(bottom, bottom_tip, ());
let side = gr.add_node(Encounter::new("CASTER", x + 2, y));
gr.add_edge(start, side, ());
gr.add_edge(top, side, ());
gr.add_edge(bottom, side, ());
let exit = gr.add_node(Encounter::new("NORMAL", x + 3, y));
gr.add_edge(side, exit, ());
return (exit, x + 3, y);
},
Shape::Domino => {
let top = gr.add_node(Encounter::new("NORMAL", x, y + 1));
gr.add_edge(start, top, ());
let top_tip = gr.add_node(Encounter::new("MINIBOSS", x + 1, y + 1));
gr.add_edge(top, top_tip, ());
let bottom = gr.add_node(Encounter::new("MINIBOSS", x, y - 1));
gr.add_edge(start, bottom, ());
let bottom_tip = gr.add_node(Encounter::new("NORMAL", x + 1, y - 1));
gr.add_edge(bottom, bottom_tip, ());
let side = gr.add_node(Encounter::new("CASTER", x + 1, y));
gr.add_edge(top_tip, side, ());
gr.add_edge(bottom_tip, side, ());
let exit = gr.add_node(Encounter::new("NORMAL", x + 2, y));
gr.add_edge(side, exit, ());
return (exit, x + 2, y);
},
Shape::Diamond => {
let top = gr.add_node(Encounter::new("NORMAL", x + 1, y + 1));
gr.add_edge(start, top, ());
let top_tip = gr.add_node(Encounter::new("MINIBOSS", x + 2, y + 1));
gr.add_edge(top, top_tip, ());
let bottom = gr.add_node(Encounter::new("MINIBOSS", x + 1, y - 1));
gr.add_edge(start, bottom, ());
let bottom_tip = gr.add_node(Encounter::new("NORMAL", x + 2, y - 1));
gr.add_edge(bottom, bottom_tip, ());
let exit = gr.add_node(Encounter::new("CASTER", x + 3, y));
gr.add_edge(top_tip, exit, ());
gr.add_edge(bottom_tip, exit, ());
return (exit, x + 3, y);
},
// _ => panic!("nyi shape"),
}
}
pub fn create_zone_graph() -> ZoneGraph {
let mut gr = Graph::new_undirected();
let mut rng = thread_rng();
let mut last = gr.add_node(Encounter::start());
let mut x = 0;
let mut y = 0;
for _i in 0..4 {
let shapes = vec![
(Shape::Line, 1),
(Shape::Diamond, 1),
(Shape::Diode, 1),
(Shape::Kite, 1),
(Shape::Domino, 1),
(Shape::Plus, 1),
];
let dist = WeightedIndex::new(shapes.iter().map(|item| item.1)).unwrap();
let shape = shapes[dist.sample(&mut rng)].0;
let result = add_shape(shape, &mut gr, last, x, y);
last = result.0;
x = result.1;
y = result.2;
}
return gr;
}
pub fn node_joinable(graph: &ZoneGraph, target_index: NodeIndex) -> Result<(), Error> {
// early return for already attempted
{
let target_encounter = match graph.node_weight(target_index) {
Some(encounter) => encounter,
None => panic!("{:?} has no weight for {:?}", graph, target_index),
};
if target_encounter.game_id.is_some() {
return Err(err_msg("node already attempted"));
}
}
let success_indices = graph.node_indices().filter(|i| {
match graph.node_weight(*i) {
Some(encounter) => encounter.success,
None => panic!("no weight for {:?}", i),
}
});
// if a node is a neighbour of that graph
// and hasn't been attempted
// it is joinable
for i in success_indices {
match graph.neighbors(i).find(|n| *n == target_index) {
Some(_n) => return Ok(()),
None => continue,
};
}
return Err(err_msg("node requirements not met"));
}
pub fn node_finish(game: &Game, zone_id: Uuid, node_index: u32, tx: &mut Transaction) -> Result<Zone, Error> {
let mut zone = zone_get(tx, zone_id)?;
let winner_id = match game.winner() {
Some(w) => w.id,
None => return Ok(zone),
};
if zone.account == winner_id {
{
let encounter = zone.graph
.node_weight_mut(NodeIndex::from(node_index))
.ok_or(err_msg("encounter not found for game zone update"))?;
encounter.success = true;
}
zone_update(&zone, tx)?;
}
Ok(zone)
}
#[cfg(test)]
mod tests {
use zone::*;
#[test]
fn create_zone_test() {
let _graph = create_zone_graph();
// good shit;
// let nodes = graph.node_indices().collect::<Vec<NodeIndex>>();
// info!("{:?}", nodes[0]);
// info!("{:?}", graph.node_weight(nodes[0]));
// info!("{:?}", Dot::with_config(&graph, &[Config::EdgeNoLabel]));
}
#[test]
fn zone_joinable_test() {
let graph = create_zone_graph();
assert!(node_joinable(&graph, NodeIndex::from(1)).is_ok());
assert!(node_joinable(&graph, NodeIndex::from(graph.node_count() as u32 - 1)).is_err());
}
}