490 lines
15 KiB
Rust
490 lines
15 KiB
Rust
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);*/
|
|
}
|
|
|
|
} |