508 lines
16 KiB
Rust
508 lines
16 KiB
Rust
use std::collections::{HashMap};
|
|
|
|
use uuid::Uuid;
|
|
|
|
use failure::Error;
|
|
use failure::err_msg;
|
|
|
|
use construct::{Construct, Colours};
|
|
use vbox::{Vbox, ItemType, VboxIndices};
|
|
use item::{Item, ItemEffect, get_combos};
|
|
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 new(account: Uuid, img: Option<Uuid>, name: &String, mut constructs: Vec<Construct>) -> Player {
|
|
constructs.sort_unstable_by_key(|c| c.id);
|
|
|
|
Player {
|
|
id: account,
|
|
img,
|
|
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)?;
|
|
|
|
construct.skills = vec![];
|
|
construct.learn_mut(skill);
|
|
|
|
// // done here because i teach them a tonne of skills for tests
|
|
// let max_skills = 1;
|
|
// 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")),
|
|
}
|
|
|
|
let construct = self.construct_get(construct_id)?;
|
|
|
|
// force 1 skill for auto
|
|
let mut colour_counts = vec![
|
|
(Item::Red, construct.colours.red),
|
|
(Item::Green, construct.colours.green),
|
|
(Item::Blue, construct.colours.blue),
|
|
];
|
|
colour_counts.sort_unstable_by_key(|cc| cc.1);
|
|
colour_counts.reverse();
|
|
|
|
println!("{:?}", colour_counts);
|
|
|
|
let total = (construct.colours.red + construct.colours.green + construct.colours.blue) as f64;
|
|
let colour_pcts = colour_counts.iter_mut()
|
|
.map(|cc| (cc.0, cc.1 as f64 / total))
|
|
.collect::<Vec<_>>();
|
|
|
|
println!("{:?}", colour_pcts);
|
|
|
|
let skill_item = match item.into_skill().is_some() {
|
|
true => item,
|
|
false => construct.skills[0].skill.base(),
|
|
};
|
|
|
|
let colour_skill = {
|
|
// no colours
|
|
if colour_pcts[0].1.is_infinite() {
|
|
construct.skills[0].skill
|
|
} else {
|
|
let mut skill_item_combo = {
|
|
if colour_pcts[0].1 > 0.75 {
|
|
vec![skill_item, colour_pcts[0].0, colour_pcts[0].0]
|
|
} else if colour_pcts[1].1 > 0.4 {
|
|
vec![skill_item, colour_pcts[0].0, colour_pcts[1].0]
|
|
} else {
|
|
// special triple skill_item
|
|
vec![skill_item, colour_pcts[0].0, colour_pcts[1].0]
|
|
}
|
|
};
|
|
|
|
skill_item_combo.sort_unstable();
|
|
|
|
println!("{:?}", skill_item_combo);
|
|
let combos = get_combos();
|
|
let combo = combos.iter().find(|c| c.components == skill_item_combo)
|
|
.ok_or(err_msg("no combo for colour skill"))?;
|
|
|
|
combo.item.into_skill()
|
|
.ok_or(format_err!("item {:?} has no associated skill", combo.item))?
|
|
}
|
|
};
|
|
|
|
// unlearn everything
|
|
construct.skills = vec![];
|
|
construct.learn_mut(colour_skill);
|
|
|
|
// // 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, construct: Option<Uuid>) -> Result<&mut Player, Error> {
|
|
if self.vbox.stash.len() >= 9 && !construct.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 construct {
|
|
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)
|
|
}
|
|
|
|
}
|
|
|
|
#[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, None, &"test".to_string(), constructs).set_bot(true);
|
|
|
|
player.vbox.fill();
|
|
player.autobuy();
|
|
|
|
assert!(player.constructs.iter().all(|c| c.skills.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, 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);
|
|
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);*/
|
|
}
|
|
|
|
} |