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, } #[derive(Debug,Clone,Copy)] enum Shape { Diamond, Line, Plus, Diode, Domino, Kite, } pub type ZoneGraph = UnGraph; #[derive(Debug,Clone,PartialEq,Eq,Hash,PartialOrd,Ord,Serialize,Deserialize)] pub struct Encounter { tag: String, game_id: Option, 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 { 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 = returned.get("data"); let zone = match from_slice::(&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 { 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 { 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 { 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::>(); // 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()); } }