Implement undo button for live games.

This commit is contained in:
yukirij 2024-10-05 17:54:31 -07:00
parent b848492c5a
commit 60b48e4708
10 changed files with 227 additions and 65 deletions

View File

@ -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 => {

View File

@ -23,7 +23,7 @@ pub struct Session {
pub time:u64,
pub chain_id:usize,
pub undo:Option<u8>,
pub undo:u8,
}
impl Session {
pub fn get_connections(&self) -> Vec<(u32, u8)>

View File

@ -637,8 +637,6 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
| 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<protocol::QRPacket>)
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<protocol::QRPacket>)
// 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<protocol::QRPacket>)
}
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;
}
}
}
}
@ -692,6 +738,8 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
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() {
packets.push(QRPacket::new(
@ -818,7 +866,7 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
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();

View File

@ -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

View File

@ -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<Play>,
}
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),

View File

@ -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<Vec<Play>,()>
{
let mut result = Vec::new();

View File

@ -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;
}

View File

@ -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,12 +1277,30 @@ const INTERFACE = {
},
undo() {
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;
}
},
message(code, data) {
@ -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;
@ -1270,6 +1326,7 @@ const INTERFACE = {
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) {
//if(INTERFACE_DATA.Game.history[INTERFACE_DATA.Game.history.length-1].source == 2) {
@ -1319,23 +1376,26 @@ const INTERFACE = {
} break;
case GameMessage.Undo: {
switch(data.state) {
case 0: {
if(data.state == 0) {
// Request undo
if(data.turn == INTERFACE_DATA.Game.history.length) {
INTERFACE_DATA.Ui.request_undo = true;
INTERFACE_DATA.Ui.request_undo = 2;
INTERFACE.game_step();
}
} break;
case 1: {
} else {
// Perform undo
INTERFACE.undo();
} break;
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() {

View File

@ -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 {

View File

@ -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;