diff --git a/game/src/game/mod.rs b/game/src/game/mod.rs index d357e6c..9dff684 100644 --- a/game/src/game/mod.rs +++ b/game/src/game/mod.rs @@ -42,6 +42,7 @@ impl Game { pub fn init(&mut self) { + *self = Self::new(); self.board.init(); } @@ -73,7 +74,6 @@ impl Game { // if valid { - // Move piece on board. if match play.source { 0 | 2 => { diff --git a/server/src/app/session.rs b/server/src/app/session.rs index b24d843..83e4b48 100644 --- a/server/src/app/session.rs +++ b/server/src/app/session.rs @@ -23,7 +23,7 @@ pub struct Session { pub time:u64, pub chain_id:usize, - pub undo:Option, + pub undo:u8, } impl Session { pub fn get_connections(&self) -> Vec<(u32, u8)> diff --git a/server/src/manager/data.rs b/server/src/manager/data.rs index a016cd4..68a9deb 100644 --- a/server/src/manager/data.rs +++ b/server/src/manager/data.rs @@ -637,8 +637,6 @@ pub async fn thread_system(mut app:App, bus:Bus) | GameMessageData::PlayDrop(turn, from, to) | GameMessageData::PlayAlt(turn, from, to) => { - println!("HERE"); - if !session.game.is_complete() { if (user_id == Some(session.p_dawn.user) && session.game.turn & 1 == 0) || (user_id == Some(session.p_dusk.user) && session.game.turn & 1 == 1) { @@ -653,8 +651,6 @@ pub async fn thread_system(mut app:App, bus:Bus) from, to, }; - println!("play {} {} {}", play.source, play.from, play.to); - if session.game.process(&play).is_ok() { // Commit play to history app.filesystem.session_history_push(session.id, play).ok(); @@ -670,6 +666,8 @@ pub async fn thread_system(mut app:App, bus:Bus) // Send status to players. send_user_status.push(session.p_dawn.user); send_user_status.push(session.p_dusk.user); + + session.undo = 0; } } } @@ -677,11 +675,59 @@ pub async fn thread_system(mut app:App, bus:Bus) } GameMessageData::Undo(turn, _) => { - if !session.game.is_complete() { - if (user_id == Some(session.p_dawn.user) && session.game.turn & 1 == 0) - || (user_id == Some(session.p_dusk.user) && session.game.turn & 1 == 1) { - if turn == session.game.turn { - // Request or commit undo + use packet::PacketGameMessage; + + if session.game.turn > 0 && !session.game.is_complete() { + let player = if user_id == Some(session.p_dawn.user) { 1 } + else if user_id == Some(session.p_dusk.user) { 2 } + else { 0 }; + + if player != 0 { + if turn == session.game.turn && (session.undo & player) == 0 { + if session.undo == 0 { + + // Send undo request to opposing player + let packet = GameMessageData::Undo(turn, 0); + + if player == 1 { + for cid in &session.p_dusk.connections { + packets.push(QRPacket::new( + *cid, + QRPacketData::GameMessage(PacketGameMessage::with(packet)) + )); + } + } else { + for cid in &session.p_dawn.connections { + packets.push(QRPacket::new( + *cid, + QRPacketData::GameMessage(PacketGameMessage::with(packet)) + )); + } + } + + session.undo |= player; + } else { + // Send undo command to clients + let revert_count = if session.game.turn > 1 && session.undo == (1 << (session.game.turn & 1)) { 2 } else { 1 }; + + for _ in 0..revert_count { + session.game.history.pop(); + app.filesystem.session_history_pop(session.id).ok(); + } + session.game.apply_history(&session.game.history.clone()).ok(); + + let packet = GameMessageData::Undo(session.game.turn, 1); + + // Send undo request to opposing player + for (cid, _) in session.get_connections() { + packets.push(QRPacket::new( + cid, + QRPacketData::GameMessage(PacketGameMessage::with(packet)) + )); + } + + session.undo = 0; + } } } } @@ -691,6 +737,8 @@ pub async fn thread_system(mut app:App, bus:Bus) if !session.game.is_complete() { if (user_id == Some(session.p_dawn.user) && session.game.turn & 1 == 0) || (user_id == Some(session.p_dusk.user) && session.game.turn & 1 == 1) { + + session.undo = 0; // Forward messsage to all clients for (cid, _) in session.get_connections() { @@ -818,7 +866,7 @@ pub async fn thread_system(mut app:App, bus:Bus) time:std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() as u64, chain_id, - undo:None, + undo:0, }; session.game.init(); @@ -930,11 +978,12 @@ fn generate_game_state(app:&App, session:&Session) -> protocol::PacketGameStateR if let Some(user) = app.get_user_by_id(session.p_dusk.user) { response.dusk_handle = user.handle.clone(); } - response.dawn_online = session.p_dawn.connections.len() > 0; response.dusk_online = session.p_dusk.connections.len() > 0; response.spectators = session.connections.len() as u32; + response.undo = session.undo; + // Get history response.history = session.game.history.clone(); diff --git a/server/src/protocol/packet/game_message.rs b/server/src/protocol/packet/game_message.rs index 255f10b..002c32d 100644 --- a/server/src/protocol/packet/game_message.rs +++ b/server/src/protocol/packet/game_message.rs @@ -35,6 +35,11 @@ impl PacketGameMessage { data:GameMessageData::Error, } } + + pub fn with(data:GameMessageData) -> Self + { + Self { data } + } } impl Packet for PacketGameMessage { type Data = Self; @@ -63,8 +68,8 @@ impl Packet for PacketGameMessage { ), GMSG_UNDO => GameMessageData::Undo( - ((data >> 8) & 0xFFFF) as u16, - ((data >> 24) & 0x1) as u8, + ((data >> 9) & 0xFFFF) as u16, + ((data >> 8) & 0x1) as u8, ), GMSG_RETIRE => GameMessageData::Resign, @@ -111,8 +116,8 @@ impl Packet for PacketGameMessage { GameMessageData::Undo(turn, state) => { GMSG_UNDO as u64 - | ((turn as u64) << 8) - | ((state as u64) << 24) + | ((state as u64) << 8) + | ((turn as u64) << 9) } GameMessageData::Resign => { GMSG_RETIRE as u64 diff --git a/server/src/protocol/packet/game_state.rs b/server/src/protocol/packet/game_state.rs index cd734c9..2a5c887 100644 --- a/server/src/protocol/packet/game_state.rs +++ b/server/src/protocol/packet/game_state.rs @@ -43,11 +43,15 @@ pub struct PacketGameStateResponse { pub status:u16, pub token:SessionToken, pub player:u8, + + pub undo:u8, + pub dawn_handle:String, pub dusk_handle:String, pub dawn_online:bool, pub dusk_online:bool, pub spectators:u32, + pub history:Vec, } impl PacketGameStateResponse { @@ -57,11 +61,15 @@ impl PacketGameStateResponse { status:0, token:SessionToken::default(), player:2, + + undo:0, + dawn_handle:String::new(), dusk_handle:String::new(), dawn_online:false, dusk_online:false, spectators:0, + history:Vec::new(), } } @@ -84,6 +92,7 @@ impl Packet for PacketGameStateResponse { flags |= self.player as u16; flags |= (self.dawn_online as u16) << 2; flags |= (self.dusk_online as u16) << 3; + flags |= (self.undo as u16) << 8; [ pack_u16(self.status), diff --git a/server/src/system/filesystem/mod.rs b/server/src/system/filesystem/mod.rs index a10bac3..1982e6d 100644 --- a/server/src/system/filesystem/mod.rs +++ b/server/src/system/filesystem/mod.rs @@ -204,7 +204,7 @@ impl FileSystem { time, chain_id:0, - undo:None, + undo:0, }) } else { Err(()) } } @@ -216,8 +216,6 @@ impl FileSystem { let bucket_index = id & !HANDLE_BUCKET_MASK; let dir_index = id & HANDLE_BUCKET_MASK; - println!("A"); - let bucket_path = Path::new(DIR_SESSION) .join(format!("{:08x}", bucket_index)) .join(format!("{:08x}", dir_index)); @@ -241,6 +239,37 @@ impl FileSystem { } else { Err(()) } } + pub fn session_history_pop(&mut self, id:u32) -> Result<(),()> + { + let bucket_index = id & !HANDLE_BUCKET_MASK; + let dir_index = id & HANDLE_BUCKET_MASK; + + let bucket_path = Path::new(DIR_SESSION) + .join(format!("{:08x}", bucket_index)) + .join(format!("{:08x}", dir_index)); + + // Open session history file + if let Ok(mut file) = File::options().read(true).write(true).open(bucket_path.join(GENERIC_HISTORY)) { + let mut buffer_size = [0u8; 2]; + + // Update length + file.seek(SeekFrom::Start(0)).map_err(|_| ())?; + file.read_exact(&mut buffer_size).map_err(|_| ())?; + + let size = unpack_u16(&buffer_size, &mut 0); + + if size > 0 { + let new_size = size - 1; + + file.seek(SeekFrom::Start(0)).map_err(|_| ())?; + file.write(&pack_u16(new_size)).map_err(|_| ())?; + file.set_len(2 + (2 * new_size as u64)).map_err(|_| ())?; + } + + Ok(()) + } else { Err(()) } + } + pub fn session_history_fetch(&mut self, id:u32) -> Result,()> { let mut result = Vec::new(); diff --git a/www/css/util.css b/www/css/util.css index 8214598..b6cbe6a 100644 --- a/www/css/util.css +++ b/www/css/util.css @@ -3,7 +3,7 @@ span.c_dawn{color:#ffe082;} span.c_dusk{color:#f6a1bd;} span.bold{font-weight:bold;} -button#button-resign.warn { +button.warn { background-color:#471414; color:#e0e0e0; } diff --git a/www/js/interface.js b/www/js/interface.js index 21611da..c36a800 100644 --- a/www/js/interface.js +++ b/www/js/interface.js @@ -202,13 +202,13 @@ const INTERFACE = { } } - if(initial_hover != INTERFACE_DATA.hover) { INTERFACE.step(); } + if(initial_hover != INTERFACE_DATA.hover) { INTERFACE.game_step(); } }, unhover() { let redraw = (INTERFACE_DATA.hover !== null); INTERFACE_DATA.hover = null; - if(redraw) { INTERFACE.step(); } + if(redraw) { INTERFACE.game_step(); } }, click(event) { @@ -326,7 +326,7 @@ const INTERFACE = { } break; } - INTERFACE.step(); + INTERFACE.game_step(); }, contextmenu() { @@ -341,7 +341,7 @@ const INTERFACE = { if(INTERFACE.Ui.match_select(INTERFACE_DATA.hover, INTERFACE_DATA.select)) { INTERFACE_DATA.select = null; INTERFACE_DATA.alt_mode = false; - INTERFACE.step(); + INTERFACE.game_step(); } else { INTERFACE.click({button:0}); } @@ -381,6 +381,46 @@ const INTERFACE = { INTERFACE_DATA.Render.pool_offset = INTERFACE_DATA.Render.offset.x + Math.floor(INTERFACE.PoolOffset * gui_scale); }, + game_step() { + if(INTERFACE_DATA === null) return; + + if(GAME_DATA.turn == 0) { + INTERFACE_DATA.Ui.request_undo = 0; + } + + switch(INTERFACE_DATA.mode) { + case INTERFACE.Mode.Player: { + let b_resign = document.getElementById("button-resign"); + if(GAME_DATA.state.code == 0 && (GAME_DATA.turn & 1) == INTERFACE_DATA.player) { + b_resign.removeAttribute("disabled"); + } else { + b_resign.setAttribute("disabled", ""); + } + + let b_undo = document.getElementById("button-undo"); + if(GAME_DATA.turn == 0 || INTERFACE_DATA.Ui.request_undo == 1) { + b_undo.setAttribute("disabled", ""); + b_undo.removeAttribute("class"); + } else { + b_undo.removeAttribute("disabled"); + + if(INTERFACE_DATA.Ui.request_undo == 2) { + b_undo.setAttribute("class", "warn"); + } else { + b_undo.removeAttribute("class"); + } + } + } break; + + case INTERFACE.Mode.Review: { + document.getElementById("indicator-turn").innerText = INTERFACE_DATA.Replay.turn + " / " + INTERFACE_DATA.Game.history.length; + document.getElementById("turn-slider").setAttribute("max", INTERFACE_DATA.Game.history.length); + } break; + } + + INTERFACE.step(); + }, + step() { if(INTERFACE_DATA === null) return; @@ -389,15 +429,6 @@ const INTERFACE = { if(INTERFACE_DATA.Timeout.draw === null) { INTERFACE.draw(); } - - if(INTERFACE_DATA.mode == INTERFACE.Mode.Player) { - let b_resign = document.getElementById("button-resign"); - if(GAME_DATA.state.code == 0 && (GAME_DATA.turn & 1) == INTERFACE_DATA.player) { - b_resign.removeAttribute("disabled"); - } else { - b_resign.setAttribute("disabled", ""); - } - } }, draw() { @@ -1154,7 +1185,7 @@ const INTERFACE = { }, Ui: { - request_undo:false, + request_undo:0, resign_warn:false, }, @@ -1202,7 +1233,7 @@ const INTERFACE = { switch(INTERFACE_DATA.mode) { case INTERFACE.Mode.Local: { - INTERFACE.step(); + INTERFACE.game_step(); } break; } } else { @@ -1246,11 +1277,29 @@ const INTERFACE = { }, undo() { - INTERFACE_DATA.Game.auto = null; - if(INTERFACE_DATA.Game.history.length > 0) { - INTERFACE_DATA.Replay.turn = INTERFACE_DATA.Game.history.length + 1; - INTERFACE_DATA.Game.history.pop(); - INTERFACE.replay_jump(INTERFACE_DATA.Game.history.length, false); + switch(INTERFACE_DATA.mode) { + case INTERFACE.Mode.Local: { + INTERFACE_DATA.Game.auto = null; + if(INTERFACE_DATA.Game.history.length > 0) { + INTERFACE_DATA.Replay.turn = INTERFACE_DATA.Game.history.length + 1; + INTERFACE_DATA.Game.history.pop(); + INTERFACE.replay_jump(INTERFACE_DATA.Game.history.length, false); + } + } break; + + case INTERFACE.Mode.Player: { + let high = 0; + let low = GameMessage.Undo | (GAME_DATA.turn << 9); + + MESSAGE_COMPOSE([ + PACK.u16(OpCode.GameMessage), + PACK.u32(high), + PACK.u32(low), + ]); + + INTERFACE_DATA.Ui.request_undo = 1; + INTERFACE.game_step(); + } break; } }, @@ -1261,6 +1310,13 @@ const INTERFACE = { case OpCode.GameState: { if(INTERFACE_DATA.mode == INTERFACE.Mode.Player) { INTERFACE_DATA.player = data.player; + if(data.undo > 0) { + if((1 << INTERFACE_DATA.player) == data.undo) { + INTERFACE_DATA.Ui.request_undo = 1; + } else { + INTERFACE_DATA.Ui.request_undo = 2; + } + } } INTERFACE_DATA.Game.history = data.history; @@ -1269,6 +1325,7 @@ const INTERFACE = { INTERFACE_DATA.Session.Client.Dawn.online = data.dawn_online; INTERFACE_DATA.Session.Client.Dusk.online = data.dusk_online; INTERFACE_DATA.Session.Client.Spectators.count = data.spectators; + if(INTERFACE_DATA.Game.history.length > 0) { if(INTERFACE_DATA.Replay.turn == 0) { @@ -1319,23 +1376,26 @@ const INTERFACE = { } break; case GameMessage.Undo: { - switch(data.state) { - case 0: { - // Request undo - if(data.turn == INTERFACE_DATA.Game.history.length) { - INTERFACE_DATA.Ui.request_undo = true; - } - } break; - case 1: { - // Perform undo - INTERFACE.undo(); - } break; + if(data.state == 0) { + // Request undo + if(data.turn == INTERFACE_DATA.Game.history.length) { + INTERFACE_DATA.Ui.request_undo = 2; + INTERFACE.game_step(); + } + } else { + // Perform undo + INTERFACE_DATA.Ui.request_undo = 0; + + while(data.turn < INTERFACE_DATA.Game.history.length) { + INTERFACE_DATA.Game.history.pop(); + } + INTERFACE.replay_last(false); } } break; case GameMessage.Resign: { GAME_DATA.state.code = GAME.Const.State.Resign; - INTERFACE.step(); + INTERFACE.game_step(); } break; case GameMessage.Reaction: { @@ -1379,7 +1439,7 @@ const INTERFACE = { case INTERFACE.Mode.Local: { INTERFACE.history_push(play, true); - INTERFACE.step(); + INTERFACE.game_step(); if(INTERFACE_DATA.Game.auto !== null && INTERFACE_DATA.Game.auto == (GAME_DATA.turn & 1)) { setTimeout(INTERFACE.auto_play, 1000); @@ -1410,12 +1470,12 @@ const INTERFACE = { rotate() { INTERFACE_DATA.rotate ^= 1; - INTERFACE.step(); + INTERFACE.game_step(); }, mirror() { INTERFACE_DATA.mirror = !INTERFACE_DATA.mirror; - INTERFACE.step(); + INTERFACE.game_step(); }, resign() { @@ -1449,13 +1509,13 @@ const INTERFACE = { }, history_push(play, animate=false) { + INTERFACE_DATA.Ui.request_undo = 0; + INTERFACE_DATA.Game.history.push(play); - if(INTERFACE_DATA.mode == INTERFACE.Mode.Review) { - document.getElementById("indicator-turn").innerText = INTERFACE_DATA.Replay.turn + " / " + INTERFACE_DATA.Game.history.length; - document.getElementById("turn-slider").setAttribute("max", INTERFACE_DATA.Game.history.length); - } if(INTERFACE_DATA.Replay.turn == INTERFACE_DATA.Game.history.length - 1) { INTERFACE.replay_next(animate); + } else { + INTERFACE.game_step(); } }, @@ -1519,7 +1579,7 @@ const INTERFACE = { document.getElementById("turn-slider").value = INTERFACE_DATA.Replay.turn; } - INTERFACE.step(); + INTERFACE.game_step(); } }, replay_first() { @@ -1562,7 +1622,7 @@ const INTERFACE = { } else { INTERFACE_DATA.Game.auto = null; } - INTERFACE.step(); + INTERFACE.game_step(); }, auto_play() { diff --git a/www/js/scene.js b/www/js/scene.js index fc99485..ae7bc1a 100644 --- a/www/js/scene.js +++ b/www/js/scene.js @@ -621,6 +621,10 @@ const SCENES = { // Bottom Buttons let buttons_bottom = [ ]; if(data.mode == INTERFACE.Mode.Player) { + let button_undo = UI.button(LANG("undo"), () => { INTERFACE.undo(); }); + button_undo.setAttribute("id", "button-undo"); + buttons_bottom.push(button_undo); + let button_resign = UI.button(LANG("resign"), () => { INTERFACE.resign(); }); button_resign.setAttribute("id", "button-resign"); buttons_bottom.push(button_resign); @@ -651,7 +655,6 @@ const SCENES = { callback_resume = callback_resume.bind({ token: data.token, }); - buttons_top.push(UI.button(LANG("resume"), callback_resume)); } } else { diff --git a/www/js/system.js b/www/js/system.js index 21ddccd..e49fc76 100644 --- a/www/js/system.js +++ b/www/js/system.js @@ -263,11 +263,15 @@ function MESSAGE(event) { status:0, token:new Uint8Array(8), player:2, + + undo:0, + dawn:"", dusk:"", dawn_online:false, dusk_online:false, spectators:0, + history:[ ], }; @@ -284,10 +288,12 @@ function MESSAGE(event) { index = result.index; let flags = result.data; - data.player = flags & 0x3; + data.player = flags & 3; data.dawn_online = (flags >> 2) & 1; data.dusk_online = (flags >> 3) & 1; + data.undo = (flags >> 8) & 3; + result = UNPACK.u32(bytes, index); index = result.index; data.spectators = result.data; @@ -365,7 +371,8 @@ function MESSAGE(event) { } break; case GameMessage.Undo: { - data.state = dat & 0x3; + data.state = dat & 0x1; + data.turn = (dat >> 1) & 0xFFFF; } break; case GameMessage.Resign: { } break;