Add game history.

This commit is contained in:
yukirij 2024-08-19 12:53:39 -07:00
parent 91523a80a1
commit 7bdab36739
13 changed files with 380 additions and 44 deletions

View File

@ -187,6 +187,12 @@ impl App {
)).await.ok(); )).await.ok();
} }
QRPacketData::RGameHistory(response) => {
socket.send(Message::Binary(
encode_response(CODE_GAME_HISTORY, response.encode())
)).await.ok();
}
_ => { } _ => { }
} }
} }

View File

@ -463,7 +463,7 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
if let Some(session) = app.sessions.get(&request.token) { if let Some(session) = app.sessions.get(&request.token) {
// Send GameState update to all connections. // Send GameState update to all connections.
let mut packets = Vec::new(); 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() { for (cid, player) in session.get_connections() {
if cid != qr.id { if cid != qr.id {
@ -546,7 +546,8 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
println!("Request: Game State"); println!("Request: Game State");
if let Some(session) = app.sessions.get(&request.token) { 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.is_some() {
if user_id == session.p_dawn.user { response.player = 0; } if user_id == session.p_dawn.user { response.player = 0; }
@ -610,6 +611,21 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
} }
} }
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)) } _ => { Some(QRPacket::new(0, QRPacketData::None)) }
} }
} }
@ -619,7 +635,7 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
} }
} }
fn generate_gamestate(app:&App, session:&Session) -> protocol::PacketGameStateResponse fn generate_game_state(app:&App, session:&Session) -> protocol::PacketGameStateResponse
{ {
use protocol::{PacketGameStateResponse, PacketGameStateResponsePiece}; use protocol::{PacketGameStateResponse, PacketGameStateResponsePiece};
@ -666,3 +682,28 @@ fn generate_gamestate(app:&App, session:&Session) -> protocol::PacketGameStateRe
response 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
}

View File

@ -161,6 +161,16 @@ pub async fn handle_ws(ws:WebSocketStream<TokioIo<Upgraded>>, args:HttpServiceAr
Err(_) => { println!("error: packet decode failed."); } 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 true

View File

@ -31,3 +31,4 @@ pub const CODE_SESSION_LEAVE :u16 = 0x002F;
pub const CODE_GAME_STATE :u16 = 0x0030; pub const CODE_GAME_STATE :u16 = 0x0030;
pub const CODE_GAME_PLAY :u16 = 0x0031; pub const CODE_GAME_PLAY :u16 = 0x0031;
pub const CODE_GAME_HISTORY :u16 = 0x0032;

View File

@ -40,6 +40,9 @@ pub enum QRPacketData {
RGameState(PacketGameStateResponse), RGameState(PacketGameStateResponse),
QGamePlay(PacketGamePlay), QGamePlay(PacketGamePlay),
QGameHistory(PacketGameHistory),
RGameHistory(PacketGameHistoryResponse),
} }
#[derive(Clone)] #[derive(Clone)]

View File

@ -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<u8>, index:&mut usize) -> Result<Self::Data, ()>
{
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<Play>,
}
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<u8>
{
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()
}
}

View File

@ -98,8 +98,8 @@ impl Packet for PacketGameStateResponse {
let mut play = 0; let mut play = 0;
play |= self.play.source as u16; play |= self.play.source as u16;
play |= (self.play.from as u16) << 1; play |= (self.play.from as u16) << 4;
play |= (self.play.to as u16) << 7; play |= (self.play.to as u16) << 10;
let mut piece_bytes = Vec::new(); let mut piece_bytes = Vec::new();
for piece in &self.pieces { for piece in &self.pieces {

View File

@ -11,6 +11,7 @@ mod session_retire; pub use session_retire::*;
mod game_state; pub use game_state::*; mod game_state; pub use game_state::*;
mod game_play; pub use game_play::*; mod game_play; pub use game_play::*;
mod game_history; pub use game_history::*;
mod prelude { mod prelude {
pub trait Packet { pub trait Packet {

View File

@ -35,6 +35,7 @@ const OpCode = {
GameState :0x0030, GameState :0x0030,
GamePlay :0x0031, GamePlay :0x0031,
GameHistory :0x0032,
}; };
const GameState = { const GameState = {

View File

@ -1,6 +1,12 @@
let INTERFACE_DATA = null; let INTERFACE_DATA = null;
const INTERFACE = { const INTERFACE = {
Mode: {
Local: 0,
Online: 1,
Replay: 2,
},
Color: { Color: {
Background: "#101010", Background: "#101010",
Text: "#c0c0c0", Text: "#c0c0c0",
@ -196,27 +202,37 @@ const INTERFACE = {
if(is_valid) { if(is_valid) {
// Send message to server for online game. // Send message to server for online game.
if(INTERFACE_DATA.online) { switch(INTERFACE_DATA.mode) {
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);
INTERFACE_DATA.player = +(!INTERFACE_DATA.player); // Apply action and change turn for local game.
INTERFACE_DATA.rotate = +(!INTERFACE_DATA.rotate); 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; INTERFACE_DATA.select = null;
} }
@ -288,6 +304,10 @@ const INTERFACE = {
draw() { draw() {
if(INTERFACE_DATA === null) return; 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 canvas = INTERFACE_DATA.canvas;
let ctx = INTERFACE_DATA.context; let ctx = INTERFACE_DATA.context;
@ -363,7 +383,7 @@ const INTERFACE = {
case 1: ctx.fillStyle = INTERFACE.Color.TileLight; break; case 1: ctx.fillStyle = INTERFACE.Color.TileLight; break;
case 2: ctx.fillStyle = INTERFACE.Color.TileDark; 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; 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)) { } 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; ctx.fillStyle = INTERFACE.Color.HintCheck;
@ -691,18 +711,28 @@ const INTERFACE = {
}, },
}, },
init(data, online) { init(data, mode) {
GAME.init(); GAME.init();
let token = null; let token = null;
let player = 0; let player = 0;
let history = [ ];
if(data !== null) { if(data !== null) {
token = data.token; switch(mode) {
player = data.mode; case INTERFACE.Mode.Online: {
token = data.token;
player = data.mode;
} break;
case INTERFACE.Mode.Replay: {
history = data.history;
} break;
}
} }
INTERFACE_DATA = { INTERFACE_DATA = {
online: online, mode: mode,
token: token, token: token,
@ -721,6 +751,9 @@ const INTERFACE = {
play: null, play: null,
retire:false, retire:false,
replay_turn: 0,
history: history,
Ui: { Ui: {
scale: 0, scale: 0,
margin: 0, margin: 0,
@ -742,20 +775,25 @@ const INTERFACE = {
canvas.addEventListener("mousedown", INTERFACE.click); canvas.addEventListener("mousedown", INTERFACE.click);
window.addEventListener("resize", INTERFACE.draw); window.addEventListener("resize", INTERFACE.draw);
if(INTERFACE_DATA.online) { switch(INTERFACE_DATA.mode) {
MESSAGE_COMPOSE([ case INTERFACE.Mode.Replay:
PACK.u16(OpCode.GameState), case INTERFACE.Mode.Local: {
INTERFACE_DATA.token, INTERFACE.draw();
]); } break;
} else {
INTERFACE.draw(); case INTERFACE.Mode.Online: {
MESSAGE_COMPOSE([
PACK.u16(OpCode.GameState),
INTERFACE_DATA.token,
]);
} break;
} }
} }
}, },
uninit() { uninit() {
if(INTERFACE_DATA !== null) { if(INTERFACE_DATA !== null) {
if(INTERFACE_DATA.online) { if(INTERFACE_DATA.mode == INTERFACE.Mode.Online) {
MESSAGE_COMPOSE([ MESSAGE_COMPOSE([
PACK.u16(OpCode.SessionLeave), PACK.u16(OpCode.SessionLeave),
]); ]);
@ -833,13 +871,44 @@ const INTERFACE = {
}, },
retire() { retire() {
if(INTERFACE_DATA.online) { if(INTERFACE_DATA.mode == INTERFACE.Mode.Online) {
MESSAGE_COMPOSE([ MESSAGE_COMPOSE([
PACK.u16(OpCode.SessionRetire), PACK.u16(OpCode.SessionRetire),
INTERFACE_DATA.token, 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); INTERFACE.Radius = 2.0 / Math.sqrt(3.0);

View File

@ -464,13 +464,12 @@ const SCENES = {
UI.clear(table); UI.clear(table);
if(data !== null) { if(data !== null) {
table.appendChild(UI.session_table(data.records)); table.appendChild(UI.session_table_history(data.records));
} }
} break; } break;
case OpCode.SessionCreate: case OpCode.GameHistory: {
case OpCode.SessionJoin: {
if(data.status == Status.Ok) { if(data.status == Status.Ok) {
LOAD(SCENES.Game, data); LOAD(SCENES.GameHistory, data);
} }
} break; } break;
} }
@ -525,7 +524,7 @@ const SCENES = {
canvas.setAttribute("id", "game"); canvas.setAttribute("id", "game");
MAIN.appendChild(canvas); MAIN.appendChild(canvas);
INTERFACE.init(data, true); INTERFACE.init(data, INTERFACE.Mode.Online);
return true; return true;
}, },
@ -566,7 +565,51 @@ const SCENES = {
canvas.setAttribute("id", "game"); canvas.setAttribute("id", "game");
MAIN.appendChild(canvas); 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; return true;
}, },

View File

@ -319,6 +319,47 @@ function MESSAGE(event) {
data.play.to = (result.data >> 10) & 0x3F; data.play.to = (result.data >> 10) & 0x3F;
} break; } 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: default:
console.log("RECV Undefined " + code); console.log("RECV Undefined " + code);
return; return;

View File

@ -219,6 +219,43 @@ const UI = {
return tbody; 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) { clear(dom) {
while(dom.lastChild !== null) { dom.removeChild(document.body.lastChild); } while(dom.lastChild !== null) { dom.removeChild(document.body.lastChild); }
}, },