430 lines
13 KiB
Rust
430 lines
13 KiB
Rust
use uuid::Uuid;
|
|
use rand::prelude::*;
|
|
|
|
use serde_cbor::{from_slice, to_vec};
|
|
|
|
use postgres::transaction::Transaction;
|
|
|
|
use failure::Error;
|
|
use failure::err_msg;
|
|
|
|
use account::Account;
|
|
use cryp::{Cryp, Colours, cryp_get};
|
|
use vbox::{Vbox};
|
|
use item::{Item, ItemEffect};
|
|
use rpc::{PlayerCrypsSetParams};
|
|
use instance::{Instance};
|
|
use skill::{Effect};
|
|
|
|
const DISCARD_COST: u16 = 5;
|
|
|
|
#[derive(Debug,Clone,Copy,Serialize,Deserialize)]
|
|
pub struct Score {
|
|
pub wins: u8,
|
|
pub losses: u8,
|
|
}
|
|
|
|
#[derive(Debug,Clone,Serialize,Deserialize)]
|
|
pub struct Player {
|
|
pub id: Uuid,
|
|
pub name: String,
|
|
pub vbox: Vbox,
|
|
pub score: Score,
|
|
pub cryps: Vec<Cryp>,
|
|
pub bot: bool,
|
|
pub ready: bool,
|
|
pub warnings: u8,
|
|
}
|
|
|
|
impl Player {
|
|
pub fn new(account: Uuid, name: &String, cryps: Vec<Cryp>) -> Player {
|
|
Player {
|
|
id: account,
|
|
name: name.clone(),
|
|
vbox: Vbox::new(),
|
|
score: Score { wins: 0, losses: 0 },
|
|
cryps,
|
|
bot: false,
|
|
ready: false,
|
|
warnings: 0,
|
|
}
|
|
}
|
|
|
|
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 add_warning(&mut self) -> &mut Player {
|
|
self.warnings += 1;
|
|
self
|
|
}
|
|
|
|
pub fn forfeit(&mut self) -> &mut Player {
|
|
for cryp in self.cryps.iter_mut() {
|
|
cryp.force_ko();
|
|
}
|
|
self
|
|
}
|
|
|
|
pub fn add_win(&mut self) -> &mut Player {
|
|
self.score.wins += 1;
|
|
self.vbox.balance_add(12);
|
|
self
|
|
}
|
|
|
|
pub fn add_loss(&mut self) -> &mut Player {
|
|
self.score.losses += 1;
|
|
self.vbox.balance_add(9);
|
|
self
|
|
}
|
|
|
|
pub fn cryp_get(&mut self, id: Uuid) -> Result<&mut Cryp, Error> {
|
|
self.cryps.iter_mut().find(|c| c.id == id).ok_or(err_msg("cryp not found"))
|
|
}
|
|
|
|
pub fn autobuy(&mut self) -> &mut Player {
|
|
let mut rng = thread_rng();
|
|
|
|
// first check if any cryps have no skills
|
|
// if there is one find an item in vbox that gives a skill
|
|
while let Some(c) = self.cryps.iter().position(|c| c.skills.len() == 0) {
|
|
if let Some(s) = self.vbox.bound.iter().position(|v| v.into_skill().is_some()) {
|
|
let cryp_id = self.cryps[c].id;
|
|
self.vbox_apply(s, cryp_id).expect("could not apply");
|
|
continue;
|
|
}
|
|
info!("no skills available...");
|
|
}
|
|
|
|
// now keep buying and applying items cause whynot
|
|
// inb4 montecarlo gan
|
|
|
|
loop {
|
|
let (target_cryp_i, target_cryp_id) = match self.cryps.iter().any(|c| c.skills.len() < 3) {
|
|
true => {
|
|
let mut target_cryp_i = 0;
|
|
for (j, c) in self.cryps.iter().enumerate() {
|
|
if c.skills.len() < self.cryps[target_cryp_i].skills.len() {
|
|
target_cryp_i = j;
|
|
}
|
|
}
|
|
(target_cryp_i, self.cryps[target_cryp_i].id)
|
|
},
|
|
false => {
|
|
let i = rng.gen_range(0, 3);
|
|
(i, self.cryps[i].id)
|
|
},
|
|
};
|
|
|
|
let needs_skills = self.cryps[target_cryp_i].skills.len() < 3;
|
|
let group_i = match needs_skills {
|
|
true => 1,
|
|
false => 2,
|
|
};
|
|
|
|
|
|
let num_colours = self.vbox.bound
|
|
.iter()
|
|
.filter(|v| [Item::Red, Item::Green, Item::Blue].contains(v))
|
|
.count();
|
|
|
|
if self.vbox.bound.len() < 3 || num_colours < 2 {
|
|
if (needs_skills && self.vbox.bits < 4) || self.vbox.bits < 5 {
|
|
// info!("insufficient balance");
|
|
break;
|
|
}
|
|
|
|
// get 2 colours and something else
|
|
if self.vbox.free[0].len() < 2 {
|
|
break;
|
|
}
|
|
self.vbox_accept(0, 0).expect("could't accept colour 0");
|
|
self.vbox_accept(0, 0).expect("could't accept colour 1");
|
|
self.vbox_accept(group_i, 0).expect("could't accept group 0");
|
|
}
|
|
|
|
// info!("{:?}", self.vbox.bound);
|
|
|
|
let skills = [Item::Attack, Item::Block, Item::Buff, Item::Debuff, Item::Stun];
|
|
let combo_i = match group_i {
|
|
1 => self.vbox.bound.iter().position(|v| skills.contains(v)).expect("no skill found"),
|
|
2 => self.vbox.bound.iter().position(|v| v.into_spec().is_some()).expect("no spec found"),
|
|
_ => panic!("unknown group_i"),
|
|
};
|
|
|
|
// first 2 colours can be whatever
|
|
self.vbox_combine(vec![0, 1, combo_i]).ok();
|
|
let item_i = self.vbox.bound.len() - 1;
|
|
self.vbox_apply(item_i, target_cryp_id).ok();
|
|
}
|
|
|
|
return self;
|
|
}
|
|
|
|
pub fn vbox_discard(&mut self) -> Result<&mut Player, Error> {
|
|
self.vbox.balance_sub(DISCARD_COST)?;
|
|
self.vbox.fill();
|
|
Ok(self)
|
|
}
|
|
|
|
pub fn vbox_accept(&mut self, group: usize, index: usize) -> Result<&mut Player, Error> {
|
|
self.vbox.accept(group, index)?;
|
|
Ok(self)
|
|
}
|
|
|
|
pub fn vbox_combine(&mut self, indices: Vec<usize>) -> Result<&mut Player, Error> {
|
|
self.vbox.combine(indices)?;
|
|
Ok(self)
|
|
}
|
|
|
|
pub fn vbox_reclaim(&mut self, index: usize) -> Result<&mut Player, Error> {
|
|
self.vbox.reclaim(index)?;
|
|
Ok(self)
|
|
}
|
|
|
|
pub fn vbox_apply(&mut self, index: usize, cryp_id: Uuid) -> Result<&mut Player, Error> {
|
|
if self.vbox.bound.get(index).is_none() {
|
|
return Err(format_err!("no item at index {:?}", index));
|
|
}
|
|
|
|
let item = self.vbox.bound.remove(index);
|
|
|
|
match item.effect() {
|
|
Some(ItemEffect::Skill) => {
|
|
let skill = item.into_skill().ok_or(format_err!("item {:?} has no associated skill", item))?;
|
|
let cryp = self.cryp_get(cryp_id)?;
|
|
// done here because i teach them a tonne of skills for tests
|
|
let max_skills = 3;
|
|
if cryp.skills.len() >= max_skills {
|
|
return Err(format_err!("cryp at max skills ({:?})", max_skills));
|
|
}
|
|
|
|
if cryp.knows(skill) {
|
|
return Err(format_err!("cryp already knows skill ({:?})" , skill));
|
|
}
|
|
|
|
cryp.learn_mut(skill);
|
|
},
|
|
Some(ItemEffect::Spec) => {
|
|
let spec = item.into_spec().ok_or(format_err!("item {:?} has no associated spec", item))?;
|
|
let cryp = self.cryp_get(cryp_id)?;
|
|
cryp.spec_add(spec)?;
|
|
|
|
},
|
|
None => return Err(err_msg("item has no effect on cryps")),
|
|
}
|
|
|
|
// now the item has been applied
|
|
// recalculate the stats of the whole player
|
|
let player_colours = self.cryps.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 cryp in self.cryps.iter_mut() {
|
|
cryp.apply_modifiers(&player_colours);
|
|
}
|
|
|
|
Ok(self)
|
|
}
|
|
|
|
pub fn vbox_unequip(&mut self, target: Item, cryp_id: Uuid) -> Result<&mut Player, Error> {
|
|
if self.vbox.bound.len() >= 9 {
|
|
return Err(err_msg("too many items bound"));
|
|
}
|
|
|
|
match target.effect() {
|
|
Some(ItemEffect::Skill) => {
|
|
let skill = target.into_skill().ok_or(format_err!("item {:?} has no associated skill", target))?;
|
|
let cryp = self.cryp_get(cryp_id)?;
|
|
cryp.forget(skill)?;
|
|
},
|
|
Some(ItemEffect::Spec) => {
|
|
let spec = target.into_spec().ok_or(format_err!("item {:?} has no associated spec", target))?;
|
|
let cryp = self.cryp_get(cryp_id)?;
|
|
cryp.spec_remove(spec)?;
|
|
},
|
|
None => return Err(err_msg("item has no effect on cryps")),
|
|
}
|
|
|
|
// now the item has been applied
|
|
// recalculate the stats of the whole player
|
|
let player_colours = self.cryps.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 cryp in self.cryps.iter_mut() {
|
|
cryp.apply_modifiers(&player_colours);
|
|
}
|
|
|
|
self.vbox.bound.push(target);
|
|
self.vbox.bound.sort_unstable();
|
|
|
|
Ok(self)
|
|
}
|
|
|
|
// GAME METHODS
|
|
pub fn skills_required(&self) -> usize {
|
|
let required = self.cryps.iter()
|
|
.filter(|c| !c.is_ko())
|
|
.filter(|c| c.available_skills().len() > 0)
|
|
.collect::<Vec<&Cryp>>().len();
|
|
// info!("{:} requires {:} skills this turn", self.id, required);
|
|
return required;
|
|
}
|
|
|
|
pub fn taunting(&self) -> Option<&Cryp> {
|
|
self.cryps.iter()
|
|
.find(|c| c.affected(Effect::Taunt))
|
|
}
|
|
|
|
pub fn set_cryps(&mut self, mut cryps: Vec<Cryp>) -> &mut Player {
|
|
cryps.sort_unstable_by_key(|c| c.id);
|
|
self.cryps = cryps;
|
|
self
|
|
}
|
|
|
|
pub fn cryp_by_id(&mut self, id: Uuid) -> Option<&mut Cryp> {
|
|
self.cryps.iter_mut().find(|c| c.id == id)
|
|
}
|
|
|
|
}
|
|
|
|
pub fn player_get(tx: &mut Transaction, account_id: Uuid, instance_id: Uuid) -> Result<Player, Error> {
|
|
let query = "
|
|
SELECT *
|
|
FROM players
|
|
WHERE account = $1
|
|
AND instance = $2
|
|
FOR UPDATE;
|
|
";
|
|
|
|
let result = tx
|
|
.query(query, &[&account_id, &instance_id])?;
|
|
|
|
let returned = match result.iter().next() {
|
|
Some(row) => row,
|
|
None => return Err(err_msg("player not found")),
|
|
};
|
|
|
|
// tells from_slice to cast into a cryp
|
|
let bytes: Vec<u8> = returned.get("data");
|
|
let data = from_slice::<Player>(&bytes)?;
|
|
|
|
return Ok(data);
|
|
}
|
|
|
|
pub fn player_create(tx: &mut Transaction, player: Player, instance: Uuid, account: &Account) -> Result<Player, Error> {
|
|
let player_bytes = to_vec(&player)?;
|
|
|
|
let query = "
|
|
INSERT INTO players (id, instance, account, data)
|
|
VALUES ($1, $2, $3, $4)
|
|
RETURNING id, account;
|
|
";
|
|
|
|
let result = tx
|
|
.query(query, &[&Uuid::new_v4(), &instance, &account.id, &player_bytes])?;
|
|
|
|
let _returned = result.iter().next().expect("no row written");
|
|
|
|
info!("wrote player {:} joined instance: {:}", account.name, instance);
|
|
|
|
return Ok(player);
|
|
}
|
|
|
|
pub fn player_global_update(tx: &mut Transaction, mut player: Player, ignore_phase: bool) -> Result<Player, Error> {
|
|
// sort vbox for niceness
|
|
player.vbox.bound.sort_unstable();
|
|
|
|
let bytes = to_vec(&player)?;
|
|
|
|
let query = "
|
|
UPDATE players
|
|
SET data = $1, updated_at = now()
|
|
WHERE id = $2
|
|
RETURNING id, data;
|
|
";
|
|
|
|
let result = tx
|
|
.query(query, &[&bytes, &player.id])?;
|
|
|
|
result.iter().next().ok_or(format_err!("player {:?} could not be written", player))?;
|
|
|
|
Ok(player)
|
|
}
|
|
|
|
pub fn player_delete(tx: &mut Transaction, id: Uuid) -> Result<(), Error> {
|
|
let query = "
|
|
DELETE
|
|
FROM players
|
|
WHERE id = $1;
|
|
";
|
|
|
|
let result = tx
|
|
.execute(query, &[&id])?;
|
|
|
|
if result != 1 {
|
|
return Err(format_err!("unable to delete player {:?}", id));
|
|
}
|
|
|
|
info!("player deleted {:?}", id);
|
|
|
|
return Ok(());
|
|
}
|
|
|
|
pub fn player_mm_cryps_set(params: PlayerCrypsSetParams, tx: &mut Transaction, account: &Account) -> Result<Instance, Error> {
|
|
if params.cryp_ids.len() != 3 {
|
|
return Err(err_msg("player size is 3"));
|
|
}
|
|
|
|
let cryps = params.cryp_ids
|
|
.iter()
|
|
.map(|id| cryp_get(tx, *id, account.id))
|
|
.collect::<Result<Vec<Cryp>, Error>>()?;
|
|
|
|
let player = match player_get(tx, account.id, Uuid::nil()) {
|
|
Ok(mut p) => {
|
|
p.cryps = cryps;
|
|
p.vbox = Vbox::new();
|
|
player_global_update(tx, p, false)?
|
|
},
|
|
Err(_) => {
|
|
player_create(tx, Player::new(account.id, &account.name, cryps), Uuid::nil(), &account)?
|
|
}
|
|
};
|
|
|
|
let instance = Instance::global(player);
|
|
Ok(instance)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use mob::instance_mobs;
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn player_bot_vbox_test() {
|
|
let player_account = Uuid::new_v4();
|
|
let cryps = instance_mobs(player_account);
|
|
let mut player = Player::new(player_account, &"test".to_string(), cryps).set_bot(true);
|
|
player.vbox.fill();
|
|
|
|
player.autobuy();
|
|
|
|
assert!(player.cryps.iter().all(|c| c.skills.len() >= 1));
|
|
}
|
|
} |