418 lines
12 KiB
Rust
418 lines
12 KiB
Rust
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));
|
|
}
|
|
|
|
println!("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 cryp
|
|
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.cryp_ids, mode, tx, account)?;
|
|
game.set_zone(zone.id, params.node_id);
|
|
|
|
encounter.game_id = Some(game.id);
|
|
}
|
|
|
|
// persist
|
|
game_write(&game, tx)?;
|
|
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>>();
|
|
// println!("{:?}", nodes[0]);
|
|
// println!("{:?}", graph.node_weight(nodes[0]));
|
|
|
|
// println!("{:?}", 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());
|
|
}
|
|
}
|