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) pub fn init(&mut self)
{ {
*self = Self::new();
self.board.init(); self.board.init();
} }
@ -73,7 +74,6 @@ impl Game {
// //
if valid { if valid {
// Move piece on board. // Move piece on board.
if match play.source { if match play.source {
0 | 2 => { 0 | 2 => {

View File

@ -23,7 +23,7 @@ pub struct Session {
pub time:u64, pub time:u64,
pub chain_id:usize, pub chain_id:usize,
pub undo:Option<u8>, pub undo:u8,
} }
impl Session { impl Session {
pub fn get_connections(&self) -> Vec<(u32, u8)> 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::PlayDrop(turn, from, to)
| GameMessageData::PlayAlt(turn, from, to) | GameMessageData::PlayAlt(turn, from, to)
=> { => {
println!("HERE");
if !session.game.is_complete() { if !session.game.is_complete() {
if (user_id == Some(session.p_dawn.user) && session.game.turn & 1 == 0) 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) { || (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, from, to,
}; };
println!("play {} {} {}", play.source, play.from, play.to);
if session.game.process(&play).is_ok() { if session.game.process(&play).is_ok() {
// Commit play to history // Commit play to history
app.filesystem.session_history_push(session.id, play).ok(); 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 status to players.
send_user_status.push(session.p_dawn.user); send_user_status.push(session.p_dawn.user);
send_user_status.push(session.p_dusk.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, _) => { GameMessageData::Undo(turn, _) => {
if !session.game.is_complete() { use packet::PacketGameMessage;
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 session.game.turn > 0 && !session.game.is_complete() {
if turn == session.game.turn { let player = if user_id == Some(session.p_dawn.user) { 1 }
// Request or commit undo 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<protocol::QRPacket>)
if !session.game.is_complete() { if !session.game.is_complete() {
if (user_id == Some(session.p_dawn.user) && session.game.turn & 1 == 0) 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) { || (user_id == Some(session.p_dusk.user) && session.game.turn & 1 == 1) {
session.undo = 0;
// Forward messsage to all clients // Forward messsage to all clients
for (cid, _) in session.get_connections() { for (cid, _) in session.get_connections() {
@ -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, time:std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() as u64,
chain_id, chain_id,
undo:None, undo:0,
}; };
session.game.init(); 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) { if let Some(user) = app.get_user_by_id(session.p_dusk.user) {
response.dusk_handle = user.handle.clone(); response.dusk_handle = user.handle.clone();
} }
response.dawn_online = session.p_dawn.connections.len() > 0; response.dawn_online = session.p_dawn.connections.len() > 0;
response.dusk_online = session.p_dusk.connections.len() > 0; response.dusk_online = session.p_dusk.connections.len() > 0;
response.spectators = session.connections.len() as u32; response.spectators = session.connections.len() as u32;
response.undo = session.undo;
// Get history // Get history
response.history = session.game.history.clone(); response.history = session.game.history.clone();

View File

@ -35,6 +35,11 @@ impl PacketGameMessage {
data:GameMessageData::Error, data:GameMessageData::Error,
} }
} }
pub fn with(data:GameMessageData) -> Self
{
Self { data }
}
} }
impl Packet for PacketGameMessage { impl Packet for PacketGameMessage {
type Data = Self; type Data = Self;
@ -63,8 +68,8 @@ impl Packet for PacketGameMessage {
), ),
GMSG_UNDO => GameMessageData::Undo( GMSG_UNDO => GameMessageData::Undo(
((data >> 8) & 0xFFFF) as u16, ((data >> 9) & 0xFFFF) as u16,
((data >> 24) & 0x1) as u8, ((data >> 8) & 0x1) as u8,
), ),
GMSG_RETIRE => GameMessageData::Resign, GMSG_RETIRE => GameMessageData::Resign,
@ -111,8 +116,8 @@ impl Packet for PacketGameMessage {
GameMessageData::Undo(turn, state) => { GameMessageData::Undo(turn, state) => {
GMSG_UNDO as u64 GMSG_UNDO as u64
| ((turn as u64) << 8) | ((state as u64) << 8)
| ((state as u64) << 24) | ((turn as u64) << 9)
} }
GameMessageData::Resign => { GameMessageData::Resign => {
GMSG_RETIRE as u64 GMSG_RETIRE as u64

View File

@ -43,11 +43,15 @@ pub struct PacketGameStateResponse {
pub status:u16, pub status:u16,
pub token:SessionToken, pub token:SessionToken,
pub player:u8, pub player:u8,
pub undo:u8,
pub dawn_handle:String, pub dawn_handle:String,
pub dusk_handle:String, pub dusk_handle:String,
pub dawn_online:bool, pub dawn_online:bool,
pub dusk_online:bool, pub dusk_online:bool,
pub spectators:u32, pub spectators:u32,
pub history:Vec<Play>, pub history:Vec<Play>,
} }
impl PacketGameStateResponse { impl PacketGameStateResponse {
@ -57,11 +61,15 @@ impl PacketGameStateResponse {
status:0, status:0,
token:SessionToken::default(), token:SessionToken::default(),
player:2, player:2,
undo:0,
dawn_handle:String::new(), dawn_handle:String::new(),
dusk_handle:String::new(), dusk_handle:String::new(),
dawn_online:false, dawn_online:false,
dusk_online:false, dusk_online:false,
spectators:0, spectators:0,
history:Vec::new(), history:Vec::new(),
} }
} }
@ -84,6 +92,7 @@ impl Packet for PacketGameStateResponse {
flags |= self.player as u16; flags |= self.player as u16;
flags |= (self.dawn_online as u16) << 2; flags |= (self.dawn_online as u16) << 2;
flags |= (self.dusk_online as u16) << 3; flags |= (self.dusk_online as u16) << 3;
flags |= (self.undo as u16) << 8;
[ [
pack_u16(self.status), pack_u16(self.status),

View File

@ -204,7 +204,7 @@ impl FileSystem {
time, time,
chain_id:0, chain_id:0,
undo:None, undo:0,
}) })
} else { Err(()) } } else { Err(()) }
} }
@ -216,8 +216,6 @@ impl FileSystem {
let bucket_index = id & !HANDLE_BUCKET_MASK; let bucket_index = id & !HANDLE_BUCKET_MASK;
let dir_index = id & HANDLE_BUCKET_MASK; let dir_index = id & HANDLE_BUCKET_MASK;
println!("A");
let bucket_path = Path::new(DIR_SESSION) let bucket_path = Path::new(DIR_SESSION)
.join(format!("{:08x}", bucket_index)) .join(format!("{:08x}", bucket_index))
.join(format!("{:08x}", dir_index)); .join(format!("{:08x}", dir_index));
@ -241,6 +239,37 @@ impl FileSystem {
} else { Err(()) } } 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>,()> pub fn session_history_fetch(&mut self, id:u32) -> Result<Vec<Play>,()>
{ {
let mut result = Vec::new(); let mut result = Vec::new();

View File

@ -3,7 +3,7 @@ span.c_dawn{color:#ffe082;}
span.c_dusk{color:#f6a1bd;} span.c_dusk{color:#f6a1bd;}
span.bold{font-weight:bold;} span.bold{font-weight:bold;}
button#button-resign.warn { button.warn {
background-color:#471414; background-color:#471414;
color:#e0e0e0; 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() { unhover() {
let redraw = (INTERFACE_DATA.hover !== null); let redraw = (INTERFACE_DATA.hover !== null);
INTERFACE_DATA.hover = null; INTERFACE_DATA.hover = null;
if(redraw) { INTERFACE.step(); } if(redraw) { INTERFACE.game_step(); }
}, },
click(event) { click(event) {
@ -326,7 +326,7 @@ const INTERFACE = {
} break; } break;
} }
INTERFACE.step(); INTERFACE.game_step();
}, },
contextmenu() { contextmenu() {
@ -341,7 +341,7 @@ const INTERFACE = {
if(INTERFACE.Ui.match_select(INTERFACE_DATA.hover, INTERFACE_DATA.select)) { if(INTERFACE.Ui.match_select(INTERFACE_DATA.hover, INTERFACE_DATA.select)) {
INTERFACE_DATA.select = null; INTERFACE_DATA.select = null;
INTERFACE_DATA.alt_mode = false; INTERFACE_DATA.alt_mode = false;
INTERFACE.step(); INTERFACE.game_step();
} else { } else {
INTERFACE.click({button:0}); 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); 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() { step() {
if(INTERFACE_DATA === null) return; if(INTERFACE_DATA === null) return;
@ -389,15 +429,6 @@ const INTERFACE = {
if(INTERFACE_DATA.Timeout.draw === null) { if(INTERFACE_DATA.Timeout.draw === null) {
INTERFACE.draw(); 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() { draw() {
@ -1154,7 +1185,7 @@ const INTERFACE = {
}, },
Ui: { Ui: {
request_undo:false, request_undo:0,
resign_warn:false, resign_warn:false,
}, },
@ -1202,7 +1233,7 @@ const INTERFACE = {
switch(INTERFACE_DATA.mode) { switch(INTERFACE_DATA.mode) {
case INTERFACE.Mode.Local: { case INTERFACE.Mode.Local: {
INTERFACE.step(); INTERFACE.game_step();
} break; } break;
} }
} else { } else {
@ -1246,11 +1277,29 @@ const INTERFACE = {
}, },
undo() { undo() {
INTERFACE_DATA.Game.auto = null; switch(INTERFACE_DATA.mode) {
if(INTERFACE_DATA.Game.history.length > 0) { case INTERFACE.Mode.Local: {
INTERFACE_DATA.Replay.turn = INTERFACE_DATA.Game.history.length + 1; INTERFACE_DATA.Game.auto = null;
INTERFACE_DATA.Game.history.pop(); if(INTERFACE_DATA.Game.history.length > 0) {
INTERFACE.replay_jump(INTERFACE_DATA.Game.history.length, false); 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: { case OpCode.GameState: {
if(INTERFACE_DATA.mode == INTERFACE.Mode.Player) { if(INTERFACE_DATA.mode == INTERFACE.Mode.Player) {
INTERFACE_DATA.player = data.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; 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.Dawn.online = data.dawn_online;
INTERFACE_DATA.Session.Client.Dusk.online = data.dusk_online; INTERFACE_DATA.Session.Client.Dusk.online = data.dusk_online;
INTERFACE_DATA.Session.Client.Spectators.count = data.spectators; INTERFACE_DATA.Session.Client.Spectators.count = data.spectators;
if(INTERFACE_DATA.Game.history.length > 0) { if(INTERFACE_DATA.Game.history.length > 0) {
if(INTERFACE_DATA.Replay.turn == 0) { if(INTERFACE_DATA.Replay.turn == 0) {
@ -1319,23 +1376,26 @@ const INTERFACE = {
} break; } break;
case GameMessage.Undo: { case GameMessage.Undo: {
switch(data.state) { if(data.state == 0) {
case 0: { // Request undo
// Request undo if(data.turn == INTERFACE_DATA.Game.history.length) {
if(data.turn == INTERFACE_DATA.Game.history.length) { INTERFACE_DATA.Ui.request_undo = 2;
INTERFACE_DATA.Ui.request_undo = true; INTERFACE.game_step();
} }
} break; } else {
case 1: { // Perform undo
// Perform undo INTERFACE_DATA.Ui.request_undo = 0;
INTERFACE.undo();
} break; while(data.turn < INTERFACE_DATA.Game.history.length) {
INTERFACE_DATA.Game.history.pop();
}
INTERFACE.replay_last(false);
} }
} break; } break;
case GameMessage.Resign: { case GameMessage.Resign: {
GAME_DATA.state.code = GAME.Const.State.Resign; GAME_DATA.state.code = GAME.Const.State.Resign;
INTERFACE.step(); INTERFACE.game_step();
} break; } break;
case GameMessage.Reaction: { case GameMessage.Reaction: {
@ -1379,7 +1439,7 @@ const INTERFACE = {
case INTERFACE.Mode.Local: { case INTERFACE.Mode.Local: {
INTERFACE.history_push(play, true); INTERFACE.history_push(play, true);
INTERFACE.step(); INTERFACE.game_step();
if(INTERFACE_DATA.Game.auto !== null && INTERFACE_DATA.Game.auto == (GAME_DATA.turn & 1)) { if(INTERFACE_DATA.Game.auto !== null && INTERFACE_DATA.Game.auto == (GAME_DATA.turn & 1)) {
setTimeout(INTERFACE.auto_play, 1000); setTimeout(INTERFACE.auto_play, 1000);
@ -1410,12 +1470,12 @@ const INTERFACE = {
rotate() { rotate() {
INTERFACE_DATA.rotate ^= 1; INTERFACE_DATA.rotate ^= 1;
INTERFACE.step(); INTERFACE.game_step();
}, },
mirror() { mirror() {
INTERFACE_DATA.mirror = !INTERFACE_DATA.mirror; INTERFACE_DATA.mirror = !INTERFACE_DATA.mirror;
INTERFACE.step(); INTERFACE.game_step();
}, },
resign() { resign() {
@ -1449,13 +1509,13 @@ const INTERFACE = {
}, },
history_push(play, animate=false) { history_push(play, animate=false) {
INTERFACE_DATA.Ui.request_undo = 0;
INTERFACE_DATA.Game.history.push(play); 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) { if(INTERFACE_DATA.Replay.turn == INTERFACE_DATA.Game.history.length - 1) {
INTERFACE.replay_next(animate); INTERFACE.replay_next(animate);
} else {
INTERFACE.game_step();
} }
}, },
@ -1519,7 +1579,7 @@ const INTERFACE = {
document.getElementById("turn-slider").value = INTERFACE_DATA.Replay.turn; document.getElementById("turn-slider").value = INTERFACE_DATA.Replay.turn;
} }
INTERFACE.step(); INTERFACE.game_step();
} }
}, },
replay_first() { replay_first() {
@ -1562,7 +1622,7 @@ const INTERFACE = {
} else { } else {
INTERFACE_DATA.Game.auto = null; INTERFACE_DATA.Game.auto = null;
} }
INTERFACE.step(); INTERFACE.game_step();
}, },
auto_play() { auto_play() {

View File

@ -621,6 +621,10 @@ const SCENES = {
// Bottom Buttons // Bottom Buttons
let buttons_bottom = [ ]; let buttons_bottom = [ ];
if(data.mode == INTERFACE.Mode.Player) { 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(); }); let button_resign = UI.button(LANG("resign"), () => { INTERFACE.resign(); });
button_resign.setAttribute("id", "button-resign"); button_resign.setAttribute("id", "button-resign");
buttons_bottom.push(button_resign); buttons_bottom.push(button_resign);
@ -651,7 +655,6 @@ const SCENES = {
callback_resume = callback_resume.bind({ callback_resume = callback_resume.bind({
token: data.token, token: data.token,
}); });
buttons_top.push(UI.button(LANG("resume"), callback_resume)); buttons_top.push(UI.button(LANG("resume"), callback_resume));
} }
} else { } else {

View File

@ -263,11 +263,15 @@ function MESSAGE(event) {
status:0, status:0,
token:new Uint8Array(8), token:new Uint8Array(8),
player:2, player:2,
undo:0,
dawn:"", dawn:"",
dusk:"", dusk:"",
dawn_online:false, dawn_online:false,
dusk_online:false, dusk_online:false,
spectators:0, spectators:0,
history:[ ], history:[ ],
}; };
@ -284,10 +288,12 @@ function MESSAGE(event) {
index = result.index; index = result.index;
let flags = result.data; let flags = result.data;
data.player = flags & 0x3; data.player = flags & 3;
data.dawn_online = (flags >> 2) & 1; data.dawn_online = (flags >> 2) & 1;
data.dusk_online = (flags >> 3) & 1; data.dusk_online = (flags >> 3) & 1;
data.undo = (flags >> 8) & 3;
result = UNPACK.u32(bytes, index); result = UNPACK.u32(bytes, index);
index = result.index; index = result.index;
data.spectators = result.data; data.spectators = result.data;
@ -365,7 +371,8 @@ function MESSAGE(event) {
} break; } break;
case GameMessage.Undo: { case GameMessage.Undo: {
data.state = dat & 0x3; data.state = dat & 0x1;
data.turn = (dat >> 1) & 0xFFFF;
} break; } break;
case GameMessage.Resign: { } break; case GameMessage.Resign: { } break;