diff --git a/server/src/app/mod.rs b/server/src/app/mod.rs index 707f503..5aa3586 100644 --- a/server/src/app/mod.rs +++ b/server/src/app/mod.rs @@ -187,6 +187,12 @@ impl App { )).await.ok(); } + QRPacketData::RGameHistory(response) => { + socket.send(Message::Binary( + encode_response(CODE_GAME_HISTORY, response.encode()) + )).await.ok(); + } + _ => { } } } diff --git a/server/src/manager/data.rs b/server/src/manager/data.rs index ea103f1..b1b7eea 100644 --- a/server/src/manager/data.rs +++ b/server/src/manager/data.rs @@ -463,7 +463,7 @@ pub async fn thread_system(mut app:App, bus:Bus) if let Some(session) = app.sessions.get(&request.token) { // Send GameState update to all connections. let mut packets = Vec::new(); - let game_state = generate_gamestate(&app, session); + let game_state = generate_game_state(&app, session); for (cid, player) in session.get_connections() { if cid != qr.id { @@ -546,7 +546,8 @@ pub async fn thread_system(mut app:App, bus:Bus) println!("Request: Game State"); if let Some(session) = app.sessions.get(&request.token) { - response = generate_gamestate(&app, &session); + response.status = STATUS_OK; + response = generate_game_state(&app, &session); if user_id.is_some() { if user_id == session.p_dawn.user { response.player = 0; } @@ -610,6 +611,21 @@ pub async fn thread_system(mut app:App, bus:Bus) } } + QRPacketData::QGameHistory(request) => { + let mut response = PacketGameHistoryResponse::new(); + + println!("Request: Game History"); + + if let Some(session) = app.sessions.get(&request.token) { + response.status = STATUS_OK; + response = generate_game_history(&app, &session); + } else { + response.status = STATUS_ERROR; + } + + Some(QRPacket::new(qr.id, QRPacketData::RGameHistory(response))) + } + _ => { Some(QRPacket::new(0, QRPacketData::None)) } } } @@ -619,7 +635,7 @@ pub async fn thread_system(mut app:App, bus:Bus) } } -fn generate_gamestate(app:&App, session:&Session) -> protocol::PacketGameStateResponse +fn generate_game_state(app:&App, session:&Session) -> protocol::PacketGameStateResponse { use protocol::{PacketGameStateResponse, PacketGameStateResponsePiece}; @@ -666,3 +682,28 @@ fn generate_gamestate(app:&App, session:&Session) -> protocol::PacketGameStateRe response } + +fn generate_game_history(app:&App, session:&Session) -> protocol::PacketGameHistoryResponse +{ + use protocol::PacketGameHistoryResponse; + + let mut response = PacketGameHistoryResponse::new(); + + // Get Dawn handle + if let Some(id) = session.p_dawn.user { + if let Some(user) = app.get_user_by_id(id) { + response.dawn_handle = user.handle.clone(); + } + } + + // Get Dusk handle + if let Some(id) = session.p_dusk.user { + if let Some(user) = app.get_user_by_id(id) { + response.dusk_handle = user.handle.clone(); + } + } + + response.history = session.game.history.clone(); + + response +} diff --git a/server/src/manager/ws.rs b/server/src/manager/ws.rs index 284d985..ac0e958 100644 --- a/server/src/manager/ws.rs +++ b/server/src/manager/ws.rs @@ -161,6 +161,16 @@ pub async fn handle_ws(ws:WebSocketStream>, args:HttpServiceAr Err(_) => { println!("error: packet decode failed."); } } + CODE_GAME_HISTORY => match PacketGameHistory::decode(&data, &mut index) { + Ok(packet) => { + args.bus.send( + bus_ds, + QRPacket::new(conn_id, QRPacketData::QGameHistory(packet)) + ).ok(); + } + Err(_) => { println!("error: packet decode failed."); } + } + _ => { } } true diff --git a/server/src/protocol/code.rs b/server/src/protocol/code.rs index 49c1392..0fe8741 100644 --- a/server/src/protocol/code.rs +++ b/server/src/protocol/code.rs @@ -31,3 +31,4 @@ pub const CODE_SESSION_LEAVE :u16 = 0x002F; pub const CODE_GAME_STATE :u16 = 0x0030; pub const CODE_GAME_PLAY :u16 = 0x0031; +pub const CODE_GAME_HISTORY :u16 = 0x0032; diff --git a/server/src/protocol/mod.rs b/server/src/protocol/mod.rs index 8456024..1d0c1e6 100644 --- a/server/src/protocol/mod.rs +++ b/server/src/protocol/mod.rs @@ -40,6 +40,9 @@ pub enum QRPacketData { RGameState(PacketGameStateResponse), QGamePlay(PacketGamePlay), + + QGameHistory(PacketGameHistory), + RGameHistory(PacketGameHistoryResponse), } #[derive(Clone)] diff --git a/server/src/protocol/packet/game_history.rs b/server/src/protocol/packet/game_history.rs new file mode 100644 index 0000000..59291c4 --- /dev/null +++ b/server/src/protocol/packet/game_history.rs @@ -0,0 +1,83 @@ +use crate::{ + app::session::SessionToken, + util::pack::pack_u16, +}; + +use game::history::Play; + +use super::Packet; + +#[derive(Clone)] +pub struct PacketGameHistory { + pub token:SessionToken, +} +impl PacketGameHistory { + pub fn new() -> Self + { + Self { + token:SessionToken::default(), + } + } +} +impl Packet for PacketGameHistory { + type Data = Self; + + fn decode(data:&Vec, index:&mut usize) -> Result + { + let mut result = Self::new(); + + if data.len() - *index == 8 { + for i in 0..8 { + result.token[i] = data[*index]; + *index += 1; + } + Ok(result) + } else { + Err(()) + } + } +} + +#[derive(Clone)] +pub struct PacketGameHistoryResponse { + pub status:u16, + pub dawn_handle:String, + pub dusk_handle:String, + pub history:Vec, +} +impl PacketGameHistoryResponse { + pub fn new() -> Self + { + Self { + status:0, + dawn_handle:String::new(), + dusk_handle:String::new(), + history:Vec::new(), + } + } +} +impl Packet for PacketGameHistoryResponse { + type Data = Self; + + fn encode(&self) -> Vec + { + let mut history_bytes = Vec::new(); + for play in &self.history { + let mut data = 0; + data |= play.source as u16; + data |= (play.from as u16) << 4; + data |= (play.to as u16) << 10; + history_bytes.append(&mut pack_u16(data)); + } + + [ + pack_u16(self.status), + pack_u16(self.dawn_handle.len() as u16), + self.dawn_handle.as_bytes().to_vec(), + pack_u16(self.dusk_handle.len() as u16), + self.dusk_handle.as_bytes().to_vec(), + pack_u16(self.history.len() as u16), + history_bytes, + ].concat() + } +} diff --git a/server/src/protocol/packet/game_state.rs b/server/src/protocol/packet/game_state.rs index 3cf1c0a..c968d12 100644 --- a/server/src/protocol/packet/game_state.rs +++ b/server/src/protocol/packet/game_state.rs @@ -98,8 +98,8 @@ impl Packet for PacketGameStateResponse { let mut play = 0; play |= self.play.source as u16; - play |= (self.play.from as u16) << 1; - play |= (self.play.to as u16) << 7; + play |= (self.play.from as u16) << 4; + play |= (self.play.to as u16) << 10; let mut piece_bytes = Vec::new(); for piece in &self.pieces { diff --git a/server/src/protocol/packet/mod.rs b/server/src/protocol/packet/mod.rs index 6e2206e..bb20e12 100644 --- a/server/src/protocol/packet/mod.rs +++ b/server/src/protocol/packet/mod.rs @@ -11,6 +11,7 @@ mod session_retire; pub use session_retire::*; mod game_state; pub use game_state::*; mod game_play; pub use game_play::*; +mod game_history; pub use game_history::*; mod prelude { pub trait Packet { diff --git a/www/js/const.js b/www/js/const.js index 37607ff..d7de756 100644 --- a/www/js/const.js +++ b/www/js/const.js @@ -35,6 +35,7 @@ const OpCode = { GameState :0x0030, GamePlay :0x0031, + GameHistory :0x0032, }; const GameState = { diff --git a/www/js/interface.js b/www/js/interface.js index b52ee2a..4144d20 100644 --- a/www/js/interface.js +++ b/www/js/interface.js @@ -1,6 +1,12 @@ let INTERFACE_DATA = null; const INTERFACE = { + Mode: { + Local: 0, + Online: 1, + Replay: 2, + }, + Color: { Background: "#101010", Text: "#c0c0c0", @@ -196,27 +202,37 @@ const INTERFACE = { if(is_valid) { // Send message to server for online game. - if(INTERFACE_DATA.online) { - let move_data = INTERFACE_DATA.select.source | (INTERFACE_DATA.select.tile << 1) | (INTERFACE_DATA.hover.tile << 7); - MESSAGE_COMPOSE([ - PACK.u16(OpCode.GamePlay), - PACK.u16(0), - PACK.u16(GAME_DATA.turn), - PACK.u16(move_data), - ]); - } - - // Apply action and change turn for offline game. - else { - let play = new GAME.Play(INTERFACE_DATA.select.source, INTERFACE_DATA.select.tile, INTERFACE_DATA.hover.tile); - INTERFACE_DATA.play = play; - GAME_DATA.process(play); + switch(INTERFACE_DATA.mode) { - INTERFACE_DATA.player = +(!INTERFACE_DATA.player); - INTERFACE_DATA.rotate = +(!INTERFACE_DATA.rotate); + // Apply action and change turn for local game. + case INTERFACE.Mode.Local: { + let play = new GAME.Play(INTERFACE_DATA.select.source, INTERFACE_DATA.select.tile, INTERFACE_DATA.hover.tile); + INTERFACE_DATA.play = play; + GAME_DATA.process(play); + + INTERFACE_DATA.player = +(!INTERFACE_DATA.player); + INTERFACE_DATA.rotate = +(!INTERFACE_DATA.rotate); + + INTERFACE.draw(); + } break; - INTERFACE.draw(); + // Send action to server for validation. + case INTERFACE.Mode.Online: { + let move_data = INTERFACE_DATA.select.source | (INTERFACE_DATA.select.tile << 1) | (INTERFACE_DATA.hover.tile << 7); + MESSAGE_COMPOSE([ + PACK.u16(OpCode.GamePlay), + PACK.u16(0), + PACK.u16(GAME_DATA.turn), + PACK.u16(move_data), + ]); + } break; + + // Branch into local game from here. + case INTERFACE.Mode.Replay: { + + } break; } + INTERFACE_DATA.select = null; } @@ -288,6 +304,10 @@ const INTERFACE = { draw() { if(INTERFACE_DATA === null) return; + if(INTERFACE_DATA.mode == INTERFACE.Mode.Replay) { + document.getElementById("ind_turn").innerText = INTERFACE_DATA.replay_turn + " of " + INTERFACE_DATA.history.length; + } + let canvas = INTERFACE_DATA.canvas; let ctx = INTERFACE_DATA.context; @@ -363,7 +383,7 @@ const INTERFACE = { case 1: ctx.fillStyle = INTERFACE.Color.TileLight; break; case 2: ctx.fillStyle = INTERFACE.Color.TileDark; break; } - if(GAME_DATA.turn > 0 && (INTERFACE_DATA.play.to == i || (INTERFACE_DATA.play.source == 0 && INTERFACE_DATA.play.from == i))) { + if(GAME_DATA.turn > 0 && INTERFACE_DATA.play.source < 2 && (INTERFACE_DATA.play.to == i || (INTERFACE_DATA.play.source == 0 && INTERFACE_DATA.play.from == i))) { ctx.fillStyle = INTERFACE.Color.HintPlay; } else if(GAME_DATA.state.check && piece !== null && piece.piece == GAME.Const.PieceId.Omen && piece.player == (GAME_DATA.turn & 1)) { ctx.fillStyle = INTERFACE.Color.HintCheck; @@ -691,18 +711,28 @@ const INTERFACE = { }, }, - init(data, online) { + init(data, mode) { GAME.init(); let token = null; let player = 0; + let history = [ ]; if(data !== null) { - token = data.token; - player = data.mode; + switch(mode) { + case INTERFACE.Mode.Online: { + token = data.token; + player = data.mode; + } break; + + case INTERFACE.Mode.Replay: { + history = data.history; + } break; + } + } INTERFACE_DATA = { - online: online, + mode: mode, token: token, @@ -721,6 +751,9 @@ const INTERFACE = { play: null, retire:false, + replay_turn: 0, + history: history, + Ui: { scale: 0, margin: 0, @@ -742,20 +775,25 @@ const INTERFACE = { canvas.addEventListener("mousedown", INTERFACE.click); window.addEventListener("resize", INTERFACE.draw); - if(INTERFACE_DATA.online) { - MESSAGE_COMPOSE([ - PACK.u16(OpCode.GameState), - INTERFACE_DATA.token, - ]); - } else { - INTERFACE.draw(); + switch(INTERFACE_DATA.mode) { + case INTERFACE.Mode.Replay: + case INTERFACE.Mode.Local: { + INTERFACE.draw(); + } break; + + case INTERFACE.Mode.Online: { + MESSAGE_COMPOSE([ + PACK.u16(OpCode.GameState), + INTERFACE_DATA.token, + ]); + } break; } } }, uninit() { if(INTERFACE_DATA !== null) { - if(INTERFACE_DATA.online) { + if(INTERFACE_DATA.mode == INTERFACE.Mode.Online) { MESSAGE_COMPOSE([ PACK.u16(OpCode.SessionLeave), ]); @@ -833,13 +871,44 @@ const INTERFACE = { }, retire() { - if(INTERFACE_DATA.online) { + if(INTERFACE_DATA.mode == INTERFACE.Mode.Online) { MESSAGE_COMPOSE([ PACK.u16(OpCode.SessionRetire), INTERFACE_DATA.token, ]); } - } + }, + + replay_prev() { + if(INTERFACE_DATA.mode == INTERFACE.Mode.Replay) { + if(INTERFACE_DATA.replay_turn > 0) { + INTERFACE_DATA.replay_turn--; + + GAME.init(); + for(let i = 0; i < INTERFACE_DATA.replay_turn; ++i) { + GAME_DATA.process(INTERFACE_DATA.history[i]); + } + + INTERFACE_DATA.play = null; + if(INTERFACE_DATA.replay_turn > 0) { + INTERFACE_DATA.play = INTERFACE_DATA.history[INTERFACE_DATA.replay_turn - 1]; + } + + INTERFACE.draw(); + } + } + }, + + replay_next() { + if(INTERFACE_DATA.mode == INTERFACE.Mode.Replay) { + if(INTERFACE_DATA.replay_turn < INTERFACE_DATA.history.length) { + GAME_DATA.process(INTERFACE_DATA.history[INTERFACE_DATA.replay_turn]); + INTERFACE_DATA.play = INTERFACE_DATA.history[INTERFACE_DATA.replay_turn]; + INTERFACE_DATA.replay_turn++; + INTERFACE.draw(); + } + } + }, }; INTERFACE.Radius = 2.0 / Math.sqrt(3.0); diff --git a/www/js/scene.js b/www/js/scene.js index 9a672e5..02d66b9 100644 --- a/www/js/scene.js +++ b/www/js/scene.js @@ -464,13 +464,12 @@ const SCENES = { UI.clear(table); if(data !== null) { - table.appendChild(UI.session_table(data.records)); + table.appendChild(UI.session_table_history(data.records)); } } break; - case OpCode.SessionCreate: - case OpCode.SessionJoin: { + case OpCode.GameHistory: { if(data.status == Status.Ok) { - LOAD(SCENES.Game, data); + LOAD(SCENES.GameHistory, data); } } break; } @@ -525,7 +524,7 @@ const SCENES = { canvas.setAttribute("id", "game"); MAIN.appendChild(canvas); - INTERFACE.init(data, true); + INTERFACE.init(data, INTERFACE.Mode.Online); return true; }, @@ -566,7 +565,51 @@ const SCENES = { canvas.setAttribute("id", "game"); MAIN.appendChild(canvas); - INTERFACE.init(data, false); + INTERFACE.init(data, INTERFACE.Mode.Local); + + return true; + }, + unload() { + INTERFACE.uninit(); + }, + }, + + GameHistory:{ + load(data) { + let buttons_bottom = [ ]; + buttons_bottom.push(UI.button("Back", () => { LOAD(SCENES.Browse) })); + + UI.nav([ + UI.button("Rotate", () => { INTERFACE.rotate(); }), + UI.button("Mirror", () => { INTERFACE.mirror(); }), + ], buttons_bottom); + + + let left_buttons = [ ]; + if(CONTEXT.Auth !== null) { + left_buttons.push(UI.button("Start", () => { + MESSAGE_SESSION_START(); + })); + } + + let ind_turn = UI.div([UI.text("0 of 0")]); + ind_turn.setAttribute("id", "ind_turn"); + + UI.mainnav( + left_buttons, + [ + ind_turn, + UI.button("◀", () => { INTERFACE.replay_prev(); }), + UI.button("▶", () => { INTERFACE.replay_next(); }), + ] + ); + + + let canvas = document.createElement("canvas"); + canvas.setAttribute("id", "game"); + MAIN.appendChild(canvas); + + INTERFACE.init(data, INTERFACE.Mode.Replay); return true; }, diff --git a/www/js/system.js b/www/js/system.js index f17196a..defe51e 100644 --- a/www/js/system.js +++ b/www/js/system.js @@ -319,6 +319,47 @@ function MESSAGE(event) { data.play.to = (result.data >> 10) & 0x3F; } break; + case OpCode.GameHistory: { + console.log("RECV GameHistory"); + + data = { + status:0, + dawn:"", + dusk:"", + history:[ ], + }; + + // Status + result = UNPACK.u16(bytes, index); + index = result.index; + data.status = result.data; + + // Handles + result = UNPACK.string(bytes, index); + index = result.index; + data.dawn = result.data; + + result = UNPACK.string(bytes, index); + index = result.index; + data.dusk = result.data; + + // Pieces + result = UNPACK.u16(bytes, index); + index = result.index; + let history_length = result.data; + + for(let i = 0; i < history_length; ++i) { + result = UNPACK.u16(bytes, index); + index = result.index; + + data.history.push(new GAME.Play( + result.data & 0xF, + (result.data >> 4) & 0x3F, + (result.data >> 10) & 0x3F, + )); + } + } break; + default: console.log("RECV Undefined " + code); return; diff --git a/www/js/ui.js b/www/js/ui.js index 793d1e9..980b8c8 100644 --- a/www/js/ui.js +++ b/www/js/ui.js @@ -219,6 +219,43 @@ const UI = { return tbody; }, + session_table_history(records) { + let rows = [ ]; + + for(let r = 0; r < records.length; ++r) { + let buttons = [ ]; + let view_callback = function() { + MESSAGE_COMPOSE([ + PACK.u16(OpCode.GameHistory), + this.token, + ]); + }; + view_callback = view_callback.bind({token: records[r].token}); + + buttons.push(UI.button("View", view_callback)); + + let dawn = UI.text(records[r].dawn); + if(records[r].dawn == "") { dawn = UI.span([UI.text("Vacant")], "text-system"); } + + let dusk = UI.text(records[r].dusk); + if(records[r].dusk == "") { dusk = UI.span([UI.text("Vacant")], "text-system"); } + + rows.push([ + dawn, + dusk, + UI.text(records[r].turn), + buttons, + ]); + } + + let tbody = UI.table_content( + [ "Dawn", "Dusk", "Turn", "" ], + rows, + ); + + return tbody; + }, + clear(dom) { while(dom.lastChild !== null) { dom.removeChild(document.body.lastChild); } },