diff --git a/game/Cargo.toml b/game/Cargo.toml index 505fb48..b237c7b 100644 --- a/game/Cargo.toml +++ b/game/Cargo.toml @@ -4,6 +4,4 @@ version = "0.1.0" edition = "2021" [dependencies] -wasm-bindgen = "0.2.92" -web-sys = "0.3.69" -js-sys = "0.3.69" +pool = { git = "https://git.tsukiyo.org/Utility/pool" } diff --git a/game/src/board/mod.rs b/game/src/board/mod.rs index f4975c6..448ab9c 100644 --- a/game/src/board/mod.rs +++ b/game/src/board/mod.rs @@ -1,31 +1,104 @@ -use crate::piece::{Piece, PIECE_NONE}; +use crate::{ + consts::*, + piece::*, + util::Hex, +}; #[derive(Clone, Copy)] -pub struct Tile { - piece:u8, +pub struct Column { + pub militia:[bool; 2], + pub extent:[u8; 2], } -impl Tile { +impl Column { pub fn new() -> Self { Self { - piece:PIECE_NONE, + militia:[false; 2], + extent:[0, 8], } } } #[derive(Clone, Copy)] +pub struct Tile { + pub piece:Option, + pub threat:[bool; 2], +} +impl Tile { + pub fn new() -> Self + { + Self { + piece:None, + threat:[false; 2], + } + } +} + +#[derive(Clone)] pub struct Board { - tiles:[Tile; 61], - pieces:[Piece; 38], - pool:[[usize; 6]; 2], + pub tiles:[Tile; 61], + pub columns:[Column; 9], + pub pieces:[Option; 38], } impl Board { pub fn new() -> Self { Self { tiles:[Tile::new(); 61], - pieces:[Piece::new(); 38], - pool:[[0; 6]; 2], + columns:[Column::new(); 9], + pieces:[None; 38], } } + + pub fn init(&mut self) { + let layout = [ + (Piece::new(PIECE_MILITIA, PLAYER_DAWN), Hex::from_hex(0, 1)), + (Piece::new(PIECE_MILITIA, PLAYER_DAWN), Hex::from_hex(1, 1)), + (Piece::new(PIECE_MILITIA, PLAYER_DAWN), Hex::from_hex(2, 2)), + (Piece::new(PIECE_MILITIA, PLAYER_DAWN), Hex::from_hex(3, 2)), + (Piece::new(PIECE_MILITIA, PLAYER_DAWN), Hex::from_hex(4, 3)), + (Piece::new(PIECE_MILITIA, PLAYER_DAWN), Hex::from_hex(5, 3)), + (Piece::new(PIECE_MILITIA, PLAYER_DAWN), Hex::from_hex(6, 4)), + (Piece::new(PIECE_MILITIA, PLAYER_DAWN), Hex::from_hex(7, 4)), + (Piece::new(PIECE_MILITIA, PLAYER_DAWN), Hex::from_hex(8, 5)), + + (Piece::new(PIECE_LANCE, PLAYER_DAWN), Hex::from_hex(0, 0)), + (Piece::new(PIECE_LANCE, PLAYER_DAWN), Hex::from_hex(8, 4)), + + (Piece::new(PIECE_KNIGHT, PLAYER_DAWN), Hex::from_hex(1, 0)), + (Piece::new(PIECE_KNIGHT, PLAYER_DAWN), Hex::from_hex(7, 3)), + + (Piece::new(PIECE_CASTLE, PLAYER_DAWN), Hex::from_hex(2, 0)), + (Piece::new(PIECE_CASTLE, PLAYER_DAWN), Hex::from_hex(6, 2)), + + (Piece::new(PIECE_TOWER, PLAYER_DAWN), Hex::from_hex(3, 0)), + (Piece::new(PIECE_TOWER, PLAYER_DAWN), Hex::from_hex(5, 1)), + + (Piece::new(PIECE_DRAGON, PLAYER_DAWN), Hex::from_hex(4, 1)), + + (Piece::new(PIECE_OMEN, PLAYER_DAWN), Hex::from_hex(4, 0)), + ]; + + for (piece, hex) in &layout { + self.set_piece(piece.clone(), hex.tile()); + } + + for (piece, hex) in &layout { + let mut piece = piece.clone(); + piece.player = 1; + let hex = Hex::from_hex(8 - hex.x(), 8 - hex.y()); + self.set_piece(piece, hex.tile()); + } + } + + pub fn set_piece(&mut self, mut piece:Piece, tile:u8) -> u8 + { + let mut index = 0; + while self.pieces[index as usize].is_some() { index += 1; } + + piece.tile = tile; + self.tiles[tile as usize].piece = Some(index); + self.pieces[index as usize] = Some(piece); + index + } } diff --git a/game/src/consts.rs b/game/src/consts.rs new file mode 100644 index 0000000..118b5db --- /dev/null +++ b/game/src/consts.rs @@ -0,0 +1,12 @@ +pub const PLAYER_DAWN :u8 = 0; +pub const PLAYER_DUSK :u8 = 1; + +pub const PIECES_COUNT :usize = 7; +pub const PIECE_NONE :u8 = PIECES_COUNT as u8 + 1; +pub const PIECE_MILITIA :u8 = 0; +pub const PIECE_KNIGHT :u8 = 1; +pub const PIECE_LANCE :u8 = 2; +pub const PIECE_TOWER :u8 = 3; +pub const PIECE_CASTLE :u8 = 4; +pub const PIECE_DRAGON :u8 = 5; +pub const PIECE_OMEN :u8 = 6; diff --git a/game/src/game/mod.rs b/game/src/game/mod.rs index 528d997..bc97c23 100644 --- a/game/src/game/mod.rs +++ b/game/src/game/mod.rs @@ -1,4 +1,9 @@ -use crate::board::Board; +use crate::{ + //consts::*, + board::Board, + history::Play, + piece::Piece, +}; #[derive(Clone, Copy, PartialEq)] pub enum GameState { @@ -9,15 +14,98 @@ pub enum GameState { } pub struct Game { + pub turn:u16, pub state:GameState, pub board:Board, + pub pool:[[u8; 6]; 2], + + pub history:Vec, } impl Game { pub fn new() -> Self { Self { state:GameState::Joinable, + + turn:0, + board:Board::new(), + pool:[[0; 6]; 2], + + history:Vec::new(), + }.init() + } + + pub fn init(mut self) -> Self + { + self.board.init(); + self + } + + pub fn process(&mut self, play:Play) -> Result<(),()> + { + let player = (self.turn & 1) as u8; + + + // + // TODO: + // - Check for piece promotion. + // - Validate plays against move sets and specific rules. + // - Determine game state (check, checkmate). + // + + + // Move piece on board. + if if play.source == 0 { + if let Some(pid) = self.board.tiles[play.from as usize].piece { + if let Some(mut piece) = self.board.pieces[pid as usize] { + let mut swap = false; + + if let Some(tid) = self.board.tiles[play.to as usize].piece { + if let Some(target) = &mut self.board.pieces[tid as usize] { + + // Check for piece swap. + if piece.player == target.player { + println!("swap"); + swap = true; + target.tile = play.from; + } + + // Add captured piece to pool. + else { + self.pool[piece.player as usize][target.class as usize] += 1; + } + } + + // Destroy piece if captured. + if !swap { self.board.pieces[tid as usize] = None; } + } + + if swap { + self.board.tiles[play.from as usize].piece = self.board.tiles[play.to as usize].piece; + } else { + self.board.tiles[play.from as usize].piece = None; + } + self.board.tiles[play.to as usize].piece = Some(pid); + piece.tile = play.to; + + self.board.pieces[pid as usize] = Some(piece); + true + } else { false } + } else { false } } + + // Place piece from pool. + else { + if self.pool[player as usize][play.from as usize] > 0 && self.board.tiles[play.to as usize].piece.is_none() { + self.pool[player as usize][play.from as usize] -= 1; + let piece = Piece::new(play.from, player); + self.board.set_piece(piece, play.to); + true + } else { false } + } { + self.turn += 1; + Ok(()) + } else { Err(()) } } } diff --git a/game/src/history/mod.rs b/game/src/history/mod.rs index c3bc5a0..65885d6 100644 --- a/game/src/history/mod.rs +++ b/game/src/history/mod.rs @@ -1,22 +1,16 @@ -#[derive(Clone, Copy)] -pub enum PlayType { - Move = 0, - Place = 1, -} - -#[derive(Clone, Copy)] -pub enum PlayState { - Normal = 0, - Check = 1, - Checkmate = 2, -} - #[derive(Clone, Copy)] pub struct Play { - form:PlayType, - from:u8, - to:u8, - piece:u8, - capture:u8, - state:PlayState, + pub source:u8, + pub from:u8, + pub to:u8, +} +impl Play { + pub fn new() -> Self + { + Self { + source:0, + from:0, + to:0, + } + } } diff --git a/game/src/lib.rs b/game/src/lib.rs index 84cd7fe..f574ce0 100644 --- a/game/src/lib.rs +++ b/game/src/lib.rs @@ -1,5 +1,6 @@ #![allow(dead_code)] +pub mod consts; pub mod util; pub mod piece; pub mod board; diff --git a/game/src/piece/mod.rs b/game/src/piece/mod.rs index ba7ba9c..bdef94d 100644 --- a/game/src/piece/mod.rs +++ b/game/src/piece/mod.rs @@ -1,119 +1,135 @@ -use crate::util::bit; +use crate::{ + consts::*, + util::bit, +}; #[derive(Clone, Copy)] pub struct MoveSet { - direction:u32, - distance:u32, + pub direction:u32, + pub stride:u32, +} +impl MoveSet { + pub fn rotate(&self) -> Self + { + Self { + direction:0, + stride:0, + } + } } #[derive(Clone, Copy)] pub struct PieceClass { - name:&'static str, - moves:MoveSet, - pmoves:MoveSet, + pub name:&'static str, + pub moves:MoveSet, + pub pmoves:MoveSet, } -pub const PIECES_COUNT :usize = 7; -pub const PIECE_NONE :u8 = PIECES_COUNT as u8 + 1; -pub const PIECE_MILITIA :u8 = 0; -pub const PIECE_KNIGHT :u8 = 1; -pub const PIECE_LANCE :u8 = 2; -pub const PIECE_TOWER :u8 = 3; -pub const PIECE_CASTLE :u8 = 4; -pub const PIECE_DRAGON :u8 = 5; -pub const PIECE_KING :u8 = 6; - pub const PIECES :[PieceClass; PIECES_COUNT] = [ PieceClass { name: "Militia", moves: MoveSet { direction:bit(0) | bit(1) | bit(5), - distance:0, + stride:0, }, pmoves: MoveSet{ direction:bit(0) | bit(1) | bit(2) | bit(4) | bit(5), - distance:0, + stride:0, }, }, PieceClass { name: "Knight", moves: MoveSet { direction:bit(3) | bit(6) | bit(11) | bit(13) | bit(17), - distance:0, + stride:0, }, pmoves: MoveSet{ direction:bit(3) | bit(6) | bit(7) | bit(10) | bit(11) | bit(13) | bit(14) | bit(16) | bit(17), - distance:0, + stride:0, }, }, PieceClass { name: "Lance", moves: MoveSet { direction:0, - distance:0, + stride:0, }, pmoves: MoveSet { direction:0, - distance:0, + stride:0, }, }, PieceClass { name: "Tower", moves: MoveSet { direction:0, - distance:0, + stride:0, }, pmoves: MoveSet { direction:0, - distance:0, + stride:0, }, }, PieceClass { name: "Castle", moves: MoveSet { direction:0, - distance:0, + stride:0, }, pmoves: MoveSet { direction:0, - distance:0, + stride:0, }, }, PieceClass { name: "Dragon", moves: MoveSet { direction:0, - distance:0, + stride:0, }, pmoves: MoveSet { direction:0, - distance:0, + stride:0, }, }, PieceClass { name: "King", moves: MoveSet { direction:0, - distance:0, + stride:0, }, pmoves: MoveSet { direction:0, - distance:0, + stride:0, }, }, ]; #[derive(Clone, Copy)] pub struct Piece { - class:u8, - promoted:bool, + pub class:u8, + pub promoted:bool, + pub player:u8, + pub tile:u8, } impl Piece { - pub fn new() -> Self + pub fn new(class:u8, player:u8) -> Self { Self { - class:PIECE_NONE, + class, promoted:false, + player, + tile:0, + } + } + + pub fn new_at(class:u8, player:u8, tile:u8) -> Self + { + Self { + class, + promoted:false, + player, + tile, } } } diff --git a/game/src/util/hex.rs b/game/src/util/hex.rs new file mode 100644 index 0000000..8b5503d --- /dev/null +++ b/game/src/util/hex.rs @@ -0,0 +1,47 @@ +const ROWS :[u8; 10] = [ 0, 5, 11, 18, 26, 35, 43, 50, 56, 61 ]; + +pub struct Hex { + x:u8, + y:u8, + tile:u8, +} +impl Hex { + pub fn new() -> Self + { + Self { + x:0, + y:0, + tile:0, + } + } + + pub fn from_hex(x:u8, y:u8) -> Self + { + let x = x as i32; + let y = y as i32; + let a = ((x + 4) * (x + 5) / 2) - 10; + let b = (x > 4) as i32 * ((x - 4) + ((x - 5) * (x - 4))); + let tile = a - b + y; + Self { + x:x as u8, + y:y as u8, + tile:tile as u8, + } + } + + pub fn from_tile(tile:u8) -> Self + { + let mut x = 0; + while tile >= ROWS[x as usize + 1] { x += 1; } + let y = tile - ROWS[x as usize] + ((x > 4) as u8 * (x - 4)); + Self { + x, + y, + tile, + } + } + + pub fn x(&self) -> u8 { self.x } + pub fn y(&self) -> u8 { self.y } + pub fn tile(&self) -> u8 { self.tile } +} diff --git a/game/src/util/mod.rs b/game/src/util/mod.rs index 44b6bc9..0b7a49d 100644 --- a/game/src/util/mod.rs +++ b/game/src/util/mod.rs @@ -1 +1,2 @@ mod binary; pub use binary::*; +mod hex; pub use hex::*; diff --git a/server/docs/protocol/requests.szun b/server/docs/protocol/requests.szun index a5e4a7c..a8c01d3 100644 --- a/server/docs/protocol/requests.szun +++ b/server/docs/protocol/requests.szun @@ -136,8 +136,9 @@ Res_GameState { # 0031 GAME_PLAY Req_GamePlay { - token :block<8> # session token -} -Res_GamePlay { - + turn :block<2> + move :block<2> + # from : 0[6] + # to : 6[6] } +Res_GamePlay = Req_GamePlay diff --git a/server/src/app/connection.rs b/server/src/app/connection.rs index 452962b..180287c 100644 --- a/server/src/app/connection.rs +++ b/server/src/app/connection.rs @@ -5,7 +5,10 @@ use hyper::upgrade::Upgraded; use hyper_util::rt::TokioIo; use tokio_tungstenite::{tungstenite::Message, WebSocketStream}; -use crate::app::authentication::AuthToken; +use crate::app::{ + authentication::AuthToken, + session::SessionToken, +}; type StreamType = Arc>, Message>>>; @@ -13,4 +16,5 @@ pub struct Connection { pub bus:u32, pub stream:StreamType, pub auth:Option, + pub session:Option, } diff --git a/server/src/app/mod.rs b/server/src/app/mod.rs index 4743727..0728348 100644 --- a/server/src/app/mod.rs +++ b/server/src/app/mod.rs @@ -6,6 +6,7 @@ use trie::Trie; use crate::{ system::filesystem::FileSystem, util::Chain, + protocol::QRPacket, }; pub mod connection; use connection::Connection; @@ -104,4 +105,84 @@ impl App { Err(()) } } + + pub fn get_user_by_id(&self, id:u32) -> Option<&User> + { + if let Some(uid) = self.user_id.get(id as isize) { + self.users.get(*uid) + } else { None } + } + + pub async fn send_response(&mut self, response:QRPacket) + { + use crate::protocol::*; + use tokio_tungstenite::tungstenite::Message; + use futures::SinkExt; + + fn encode_response(code:u16, data:Vec) -> Vec + { + [ + crate::util::pack::pack_u16(code), + data, + ].concat() + } + + if match response.data { QRPacketData::None => false, _ => true } { + if let Some(conn) = self.connections.get_mut(response.id as usize) { + let mut socket = conn.stream.write().await; + + match response.data { + QRPacketData::RRegister(response) => { + socket.send(Message::Binary( + encode_response(CODE_REGISTER, response.encode()) + )).await.ok(); + } + + QRPacketData::RAuth(response) => { + socket.send(Message::Binary( + encode_response(CODE_AUTH, response.encode()) + )).await.ok(); + } + + QRPacketData::RAuthResume(response) => { + socket.send(Message::Binary( + encode_response(CODE_AUTH_RESUME, response.encode()) + )).await.ok(); + } + + QRPacketData::RSessionList(response) => { + socket.send(Message::Binary( + encode_response(CODE_SESSION_LIST, response.encode()) + )).await.ok(); + } + + QRPacketData::RSessionCreate(response) => { + socket.send(Message::Binary( + encode_response(CODE_SESSION_CREATE, response.encode()) + )).await.ok(); + } + + QRPacketData::RSessionJoin(response) => { + socket.send(Message::Binary( + encode_response(CODE_SESSION_JOIN, response.encode()) + )).await.ok(); + } + + QRPacketData::RGameState(response) => { + socket.send(Message::Binary( + encode_response(CODE_GAME_STATE, response.encode()) + )).await.ok(); + } + + QRPacketData::QGamePlay(response) => { + socket.send(Message::Binary( + encode_response(CODE_GAME_PLAY, response.encode()) + )).await.ok(); + } + + _ => { } + } + } + } + } } diff --git a/server/src/app/session.rs b/server/src/app/session.rs index 47e32f2..a1039d7 100644 --- a/server/src/app/session.rs +++ b/server/src/app/session.rs @@ -3,9 +3,9 @@ use game::Game; pub type SessionToken = [u8; 8]; pub type SessionSecret = [u8; 8]; -pub struct Viewer { - pub connection:Option, +pub struct Player { pub user:Option, + pub connections:Vec, } pub struct Session { @@ -15,10 +15,36 @@ pub struct Session { pub game:Game, - pub p_dawn:Viewer, - pub p_dusk:Viewer, - pub viewers:Vec, + pub p_dawn:Player, + pub p_dusk:Player, + pub connections:Vec, pub time:u64, pub chain_id:usize, } +impl Session { + pub fn get_connections(&self) -> Vec + { + [ + self.p_dawn.connections.clone(), + self.p_dusk.connections.clone(), + self.connections.clone(), + ].concat() + } + + pub fn remove_connection(&mut self, source:u8, id:u32) + { + let connections = match source { + 0 => &mut self.p_dawn.connections, + 1 => &mut self.p_dusk.connections, + _ => &mut self.connections, + }; + + for i in 0..connections.len() { + if connections[i] == id { + connections.remove(i); + break; + } + } + } +} diff --git a/server/src/main.rs b/server/src/main.rs index 7b37baf..df3d385 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -28,87 +28,37 @@ async fn service_http(mut request:hyper::Request, args:Ht println!("Serving: {}", request.uri().path()); - match request.uri().path() { - "/.css" => Ok(Response::builder() - .header(CONTENT_TYPE, "text/css") - .header(CACHE_CONTROL, "no-cache") - .body(Full::new(Bytes::from(args.cache.css()))).unwrap()), + if hyper_tungstenite::is_upgrade_request(&request) { + if let Ok((response, websocket)) = hyper_tungstenite::upgrade(&mut request, None) { + tokio::task::spawn(async move { + match websocket.await { + Ok(websocket) => manager::handle_ws(websocket, args).await, + Err(_) => Err(()), + }.ok() + }); - "/.js" => Ok(Response::builder() - .header(CONTENT_TYPE, "text/javascript") - .header(CACHE_CONTROL, "no-cache") - .body(Full::new(Bytes::from(args.cache.js()))).unwrap()), + Ok(response) + } else { + Ok(Response::builder() + .status(401) + .body(Full::new(Bytes::new())) + .unwrap()) + } + } else { + match request.uri().path() { + "/" => Ok(Response::builder() + .header(CONTENT_TYPE, "text/html") + .header(CACHE_CONTROL, "no-cache") + .body(Full::new(Bytes::from(args.cache.fetch("/.html").unwrap().data))).unwrap()), - "/favicon.png" => Ok(Response::builder() - .header(CONTENT_TYPE, "image/png") - .body(Full::new(Bytes::from(args.cache.favicon()))).unwrap()), - - // Assets - "/asset/omen_dawn.svg" => Ok(Response::builder() - .header(CONTENT_TYPE, "image/svg") - .body(Full::new(Bytes::from(args.cache.asset(0, 6)))).unwrap()), - "/asset/dragon_dawn.svg" => Ok(Response::builder() - .header(CONTENT_TYPE, "image/svg") - .body(Full::new(Bytes::from(args.cache.asset(0, 5)))).unwrap()), - "/asset/castle_dawn.svg" => Ok(Response::builder() - .header(CONTENT_TYPE, "image/svg") - .body(Full::new(Bytes::from(args.cache.asset(0, 4)))).unwrap()), - "/asset/tower_dawn.svg" => Ok(Response::builder() - .header(CONTENT_TYPE, "image/svg") - .body(Full::new(Bytes::from(args.cache.asset(0, 3)))).unwrap()), - "/asset/lance_dawn.svg" => Ok(Response::builder() - .header(CONTENT_TYPE, "image/svg") - .body(Full::new(Bytes::from(args.cache.asset(0, 2)))).unwrap()), - "/asset/knight_dawn.svg" => Ok(Response::builder() - .header(CONTENT_TYPE, "image/svg") - .body(Full::new(Bytes::from(args.cache.asset(0, 1)))).unwrap()), - "/asset/militia_dawn.svg" => Ok(Response::builder() - .header(CONTENT_TYPE, "image/svg") - .body(Full::new(Bytes::from(args.cache.asset(0, 0)))).unwrap()), - "/asset/omen_dusk.svg" => Ok(Response::builder() - .header(CONTENT_TYPE, "image/svg") - .body(Full::new(Bytes::from(args.cache.asset(1, 6)))).unwrap()), - "/asset/dragon_dusk.svg" => Ok(Response::builder() - .header(CONTENT_TYPE, "image/svg") - .body(Full::new(Bytes::from(args.cache.asset(1, 5)))).unwrap()), - "/asset/castle_dusk.svg" => Ok(Response::builder() - .header(CONTENT_TYPE, "image/svg") - .body(Full::new(Bytes::from(args.cache.asset(1, 4)))).unwrap()), - "/asset/tower_dusk.svg" => Ok(Response::builder() - .header(CONTENT_TYPE, "image/svg") - .body(Full::new(Bytes::from(args.cache.asset(1, 3)))).unwrap()), - "/asset/lance_dusk.svg" => Ok(Response::builder() - .header(CONTENT_TYPE, "image/svg") - .body(Full::new(Bytes::from(args.cache.asset(1, 2)))).unwrap()), - "/asset/knight_dusk.svg" => Ok(Response::builder() - .header(CONTENT_TYPE, "image/svg") - .body(Full::new(Bytes::from(args.cache.asset(1, 1)))).unwrap()), - "/asset/militia_dusk.svg" => Ok(Response::builder() - .header(CONTENT_TYPE, "image/svg") - .body(Full::new(Bytes::from(args.cache.asset(1, 0)))).unwrap()), - - _ => { - if hyper_tungstenite::is_upgrade_request(&request) { - if let Ok((response, websocket)) = hyper_tungstenite::upgrade(&mut request, None) { - tokio::task::spawn(async move { - match websocket.await { - Ok(websocket) => manager::handle_ws(websocket, args).await, - Err(_) => Err(()), - }.ok() - }); - - Ok(response) - } else { - Ok(Response::builder() - .status(401) - .body(Full::new(Bytes::new())) - .unwrap()) - } - } else { - Ok(Response::builder() - .header(CONTENT_TYPE, "text/html") + _ => match args.cache.fetch(request.uri().path()) { + Some(data) => Ok(Response::builder() + .header(CONTENT_TYPE, &data.mime) .header(CACHE_CONTROL, "no-cache") - .body(Full::new(Bytes::from(args.cache.html()))).unwrap()) + .body(Full::new(Bytes::from(data.data))).unwrap()), + None => Ok(Response::builder() + .status(404) + .body(Full::new(Bytes::new())).unwrap()) } } } @@ -170,7 +120,53 @@ async fn main() // Initialize HTTPS service. match b_main.connect() { Ok(bus) => { - let cache = WebCache::init(); + let cache = WebCache::new(); + cache.cache_file("text/html", "/.html", "www/.html").ok(); + cache.cache_whitespace_minimize("/.html").ok(); + cache.cache_file_group("text/css", "/.css", &[ + "www/css/main.css", + "www/css/util.css", + "www/css/ui.css", + "www/css/form.css", + "www/css/game.css", + ]).ok(); + cache.cache_file_group("text/javascript", "/.js", &[ + "www/js/const.js", + "www/js/util.js", + "www/js/game_asset.js", + "www/js/game.js", + "www/js/interface.js", + "www/js/ui.js", + "www/js/scene.js", + "www/js/system.js", + "www/js/main.js", + ]).ok(); + cache.cache_file("image/png", "/favicon.png", "www/asset/favicon.png").ok(); + + let asset_path = std::path::Path::new("www/asset"); + for asset in [ + "promote.svg", + + "omen_dawn.svg", + "dragon_dawn.svg", + "castle_dawn.svg", + "tower_dawn.svg", + "lance_dawn.svg", + "knight_dawn.svg", + "militia_dawn.svg", + + "omen_dusk.svg", + "dragon_dusk.svg", + "castle_dusk.svg", + "tower_dusk.svg", + "lance_dusk.svg", + "knight_dusk.svg", + "militia_dusk.svg", + ] { + if cache.cache_file("image/svg+xml", &format!("/asset/{}", asset), asset_path.join(asset)).is_err() { + println!("error: failed to load: {}", asset); + } + } let mut server = TlsServer::new(); if server.add_cert("omen.kirisame.com", "cert/fullchain.pem", "cert/privkey.pem").await.is_ok() { diff --git a/server/src/manager/data.rs b/server/src/manager/data.rs index fc1066d..f1ccfdf 100644 --- a/server/src/manager/data.rs +++ b/server/src/manager/data.rs @@ -1,6 +1,3 @@ -use tokio_tungstenite::tungstenite::Message; -use futures::SinkExt; - use bus::Bus; use crate::{ app::{ @@ -10,19 +7,11 @@ use crate::{ connection::Connection, }, protocol, - util::pack::pack_u16, }; -fn encode_response(code:u16, data:Vec) -> Vec -{ - [ - pack_u16(code), - data, - ].concat() -} - pub async fn thread_system(mut app:App, bus:Bus) { + use futures::SinkExt; use protocol::*; use ring::rand::{SecureRandom, SystemRandom}; @@ -34,7 +23,10 @@ pub async fn thread_system(mut app:App, bus:Bus) let qr = packet.data; let mut user_id = None; + let mut session_id = None; if let Some(conn) = app.connections.get(qr.id as usize) { + session_id = conn.session; + if let Some(auth_id) = conn.auth { if let Some(auth) = app.auths.get(&auth_id) { user_id = Some(auth.user); @@ -48,6 +40,7 @@ pub async fn thread_system(mut app:App, bus:Bus) bus: request.bus_id, stream: request.stream, auth: None, + session: None, }); println!("Connect: {}", id); @@ -60,9 +53,19 @@ pub async fn thread_system(mut app:App, bus:Bus) } QRPacketData::QDisconn => { - // Close socket and remove connection if valid - // + // Uninitialize connection if if let Some(conn) = app.connections.get_mut(qr.id as usize) { + + // Disassociate session if present + if let Some(session_token) = conn.session { + if let Some(session) = app.sessions.get_mut(&session_token) { + if user_id == session.p_dawn.user { session.remove_connection(0, qr.id); } + else if user_id == session.p_dusk.user { session.remove_connection(1, qr.id); } + else { session.remove_connection(2, qr.id); } + } + } + + // Close socket let mut socket = conn.stream.write().await; socket.close().await.ok(); true @@ -279,24 +282,20 @@ pub async fn thread_system(mut app:App, bus:Bus) let mut valid = request.game_state == GameState::None || request.game_state == session.game.state; valid &= request.game_state != GameState::Joinable || session.p_dawn.user.is_none() || session.p_dusk.user.is_none(); valid &= !request.is_player || session.p_dawn.user == user_id || session.p_dusk.user == user_id; - valid &= !request.is_live || (session.p_dawn.connection.is_some() && session.p_dusk.connection.is_some()); + valid &= !request.is_live || (session.p_dawn.connections.len() > 0 && session.p_dusk.connections.len() > 0); if valid { let is_player = user_id.is_some() && (session.p_dawn.user == user_id || session.p_dusk.user == user_id); let dawn_handle = if let Some(uid) = session.p_dawn.user { - if let Some(cuid) = app.user_id.get(uid as isize) { - if let Some(user) = app.users.get(*cuid) { - user.handle.clone() - } else { String::new() } + if let Some(user) = app.get_user_by_id(uid) { + user.handle.clone() } else { String::new() } } else { String::new() }; let dusk_handle = if let Some(uid) = session.p_dusk.user { - if let Some(cuid) = app.user_id.get(uid as isize) { - if let Some(user) = app.users.get(*cuid) { - user.handle.clone() - } else { String::new() } + if let Some(user) = app.get_user_by_id(uid) { + user.handle.clone() } else { String::new() } } else { String::new() }; @@ -306,9 +305,9 @@ pub async fn thread_system(mut app:App, bus:Bus) dawn_handle, dusk_handle, ], - turn:0, + turn:session.game.turn, last_move:[0; 3], - viewers:0, + viewers:session.connections.len() as u32, player:is_player, }); @@ -348,19 +347,23 @@ pub async fn thread_system(mut app:App, bus:Bus) token, secret, game:game::Game::new(), - p_dawn:Viewer { - connection:None,//Some(qr.id), + p_dawn:Player { user:Some(uid), + connections:vec![qr.id], }, - p_dusk:Viewer { - connection:None, + p_dusk:Player { user:None, + connections:Vec::new(), }, - viewers:Vec::new(), + connections:Vec::new(), time:std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() as u64, chain_id, }; + if let Some(conn) = app.connections.get_mut(qr.id as usize) { + conn.session = Some(session.token); + } + session.id = app.filesystem.session_store(&session).unwrap(); app.sessions.set(&token, session); @@ -381,20 +384,21 @@ pub async fn thread_system(mut app:App, bus:Bus) let mut response = PacketSessionJoinResponse::new(); response.status = STATUS_ERROR; + response.token = request.token; - // Verify that session exists. + // Verify that session exists if let Some(session) = app.sessions.get_mut(&request.token) { - // Join game as player. - if request.join { + // Join game as player + if if request.join { - // Verify client is authenticated. + // Verify client is authenticated if let Some(uid) = user_id { - // User must not already be player. + // User must not already be player if session.p_dawn.user != user_id && session.p_dusk.user != user_id { - // Add user to empty player slot. + // Add user to empty player slot if if session.p_dawn.user.is_none() { session.p_dawn.user = Some(uid); response.mode = 0; @@ -404,7 +408,7 @@ pub async fn thread_system(mut app:App, bus:Bus) response.mode = 1; true } else { - // Session is not empty. + // Session is not empty response.status = STATUS_ERROR; false } { @@ -412,20 +416,38 @@ pub async fn thread_system(mut app:App, bus:Bus) app.filesystem.session_update(session.id, session).ok(); response.status = STATUS_OK; - } - } else { + true + } else { false } + } + + // Resume session for player connection + else { println!("User resumes session."); response.status = STATUS_OK; response.mode = (session.p_dusk.user == user_id) as u8; + true } - } else { response.status = STATUS_NOAUTH; } + } else { response.status = STATUS_NOAUTH; false } } // Join game as spectator. else { println!("User spectates session."); + response.status = STATUS_OK; response.mode = 2; + true + } { + // Associate session and connection on join + if let Some(conn) = app.connections.get_mut(qr.id as usize) { + conn.session = Some(session.token); + } + match response.mode { + 0 => session.p_dawn.connections.push(qr.id), + 1 => session.p_dusk.connections.push(qr.id), + 2 => session.connections.push(qr.id), + _ => { } + } } } @@ -433,62 +455,131 @@ pub async fn thread_system(mut app:App, bus:Bus) } // SessionRetire + QRPacketData::QSessionRetire(_request) => { + + // + // Not implemented + // + + Some(QRPacket::new(0, QRPacketData::None)) + } // SessionLeave + QRPacketData::QSessionLeave => { + println!("Request: Session Leave"); + + // Verify that session exists. + if let Some(session_token) = session_id { + if let Some(session) = app.sessions.get_mut(&session_token) { + if user_id == session.p_dawn.user { session.remove_connection(0, qr.id); } + else if user_id == session.p_dusk.user { session.remove_connection(1, qr.id); } + else { session.remove_connection(2, qr.id); } + } + } + + Some(QRPacket::new(0, QRPacketData::None)) + } // GameState + QRPacketData::QGameState(request) => { + let mut response = PacketGameStateResponse::new(); + + println!("Request: Game State"); + + if let Some(session) = app.sessions.get(&request.token) { + response.status = STATUS_OK; + response.turn = session.game.turn; + + // 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(); + } + } + + // Get pool sizes + response.dawn_pool = session.game.pool[0]; + response.dusk_pool = session.game.pool[1]; + + // Get list of pieces + for i in 0..session.game.board.pieces.len() { + response.pieces[i] = if let Some(piece) = &session.game.board.pieces[i] { + PacketGameStateResponsePiece { + valid:true, + piece:piece.class, + promoted:piece.promoted, + player:piece.player, + tile:piece.tile, + } + } else { + PacketGameStateResponsePiece::new() + }; + } + } else { + response.status = STATUS_ERROR; + } + + Some(QRPacket::new(qr.id, QRPacketData::RGameState(response))) + } // GamePlay + QRPacketData::QGamePlay(mut request) => { + println!("Request: Game Play"); + + request.status = STATUS_ERROR; + let mut packets = Vec::::new(); + + if let Some(sid) = session_id { + if let Some(session) = app.sessions.get_mut(&sid) { + if (user_id == session.p_dawn.user && session.game.turn & 1 == 0) + || (user_id == session.p_dusk.user && session.game.turn & 1 == 1) { + + // Check validation of play + if request.turn == session.game.turn { + + // Update internal representation + if session.game.process(request.play).is_ok() { + request.status = STATUS_OK; + + // Forward play to all clients + for cid in &session.get_connections() { + packets.push(QRPacket::new( + *cid, + QRPacketData::QGamePlay(request.clone()) + )); + } + } + } + } + } + } + + if request.status != STATUS_ERROR { + for packet in packets { + app.send_response(packet).await; + } + + // Updates will have already been sent, so nothing is needed here. + Some(QRPacket::new(qr.id, QRPacketData::None)) + } else { + + // Return error status. + Some(QRPacket::new(qr.id, QRPacketData::QGamePlay(request))) + } + } _ => { Some(QRPacket::new(0, QRPacketData::None)) } } } None => None, } { - if match response.data { QRPacketData::None => false, _ => true } { - if let Some(conn) = app.connections.get_mut(response.id as usize) { - let mut socket = conn.stream.write().await; - - match response.data { - QRPacketData::RRegister(response) => { - socket.send(Message::Binary( - encode_response(CODE_REGISTER, response.encode()) - )).await.ok(); - } - - QRPacketData::RAuth(response) => { - socket.send(Message::Binary( - encode_response(CODE_AUTH, response.encode()) - )).await.ok(); - } - - QRPacketData::RAuthResume(response) => { - socket.send(Message::Binary( - encode_response(CODE_AUTH_RESUME, response.encode()) - )).await.ok(); - } - - QRPacketData::RSessionList(response) => { - socket.send(Message::Binary( - encode_response(CODE_SESSION_LIST, response.encode()) - )).await.ok(); - } - - QRPacketData::RSessionCreate(response) => { - socket.send(Message::Binary( - encode_response(CODE_SESSION_CREATE, response.encode()) - )).await.ok(); - } - - QRPacketData::RSessionJoin(response) => { - socket.send(Message::Binary( - encode_response(CODE_SESSION_JOIN, response.encode()) - )).await.ok(); - } - - _ => { } - } - } - } + app.send_response(response).await; } } diff --git a/server/src/manager/ws.rs b/server/src/manager/ws.rs index 54ebff9..4004fbf 100644 --- a/server/src/manager/ws.rs +++ b/server/src/manager/ws.rs @@ -124,6 +124,33 @@ pub async fn handle_ws(ws:WebSocketStream>, args:HttpServiceAr Err(_) => { println!("error: packet decode failed."); } } + CODE_SESSION_LEAVE => { + args.bus.send( + bus_ds, + QRPacket::new(conn_id, QRPacketData::QSessionLeave) + ).ok(); + } + + CODE_GAME_STATE => match PacketGameState::decode(&data, &mut index) { + Ok(packet) => { + args.bus.send( + bus_ds, + QRPacket::new(conn_id, QRPacketData::QGameState(packet)) + ).ok(); + } + Err(_) => { println!("error: packet decode failed."); } + } + + CODE_GAME_PLAY => match PacketGamePlay::decode(&data, &mut index) { + Ok(packet) => { + args.bus.send( + bus_ds, + QRPacket::new(conn_id, QRPacketData::QGamePlay(packet)) + ).ok(); + } + Err(_) => { println!("error: packet decode failed."); } + } + _ => { } } true diff --git a/server/src/protocol/mod.rs b/server/src/protocol/mod.rs index 66dec2d..8456024 100644 --- a/server/src/protocol/mod.rs +++ b/server/src/protocol/mod.rs @@ -31,6 +31,15 @@ pub enum QRPacketData { QSessionJoin(PacketSessionJoin), RSessionJoin(PacketSessionJoinResponse), + + QSessionRetire(PacketSessionRetire), + + QSessionLeave, + + QGameState(PacketGameState), + RGameState(PacketGameStateResponse), + + QGamePlay(PacketGamePlay), } #[derive(Clone)] diff --git a/server/src/protocol/packet/game_play.rs b/server/src/protocol/packet/game_play.rs index 352e403..3745e93 100644 --- a/server/src/protocol/packet/game_play.rs +++ b/server/src/protocol/packet/game_play.rs @@ -1,18 +1,27 @@ -use crate::util::pack::{pack_u16, unpack_u16}; +use crate::util::pack::{ + pack_u16, + unpack_u16, +}; +use game::{ + history::Play, + util::mask, +}; use super::Packet; #[derive(Clone)] pub struct PacketGamePlay { - pub handle:String, - pub secret:String, + pub status:u16, + pub turn:u16, + pub play:Play, } impl PacketGamePlay { pub fn new() -> Self { Self { - handle:String::new(), - secret:String::new(), + status:0, + turn:0, + play:Play::new(), } } } @@ -23,55 +32,27 @@ impl Packet for PacketGamePlay { { let mut result = Self::new(); - let mut length = unpack_u16(data, index) as usize; - if data.len() >= *index + length { - match String::from_utf8(data[*index..*index+length].to_vec()) { - Ok(text) => { - *index += length; - result.handle = text; - - length = unpack_u16(data, index) as usize; - if data.len() >= *index + length { + result.status = unpack_u16(data, index); + result.turn = unpack_u16(data, index); - match String::from_utf8(data[*index..*index+length].to_vec()) { - Ok(text) => { - *index += length; - result.secret = text; + let play = unpack_u16(data, index) as u32; + result.play.source = (play & mask(1, 0)) as u8; + result.play.from = ((play & mask(6, 1)) >> 1) as u8; + result.play.to = ((play & mask(6, 7)) >> 7) as u8; - return Ok(result); - } - Err(_) => { } - } - } - } - Err(_) => { } - } - } - - Err(()) + Ok(result) } -} - - -#[derive(Clone)] -pub struct PacketGamePlayResponse { - pub status:u16, -} -impl PacketGamePlayResponse { - pub fn new() -> Self - { - Self { - status:0, - } - } -} -impl Packet for PacketGamePlayResponse { - type Data = Self; - + fn encode(&self) -> Vec { + let mut data = 0; + data |= self.play.source as u16; + data |= (self.play.from as u16) << 1; + data |= (self.play.to as u16) << 7; [ pack_u16(self.status), + pack_u16(self.turn), + pack_u16(data), ].concat() } } diff --git a/server/src/protocol/packet/game_state.rs b/server/src/protocol/packet/game_state.rs index 7025801..8b099e9 100644 --- a/server/src/protocol/packet/game_state.rs +++ b/server/src/protocol/packet/game_state.rs @@ -1,6 +1,6 @@ use crate::{ app::session::SessionToken, - util::pack::{pack_u16, unpack_u16}, + util::pack::pack_u16, }; use super::Packet; @@ -24,21 +24,61 @@ impl Packet for PacketGameState { { let mut result = Self::new(); - Err(()) + if data.len() - *index == 8 { + for i in 0..8 { + result.token[i] = data[*index]; + *index += 1; + } + Ok(result) + } else { + Err(()) + } } } +#[derive(Clone, Copy)] +pub struct PacketGameStateResponsePiece { + pub valid:bool, + pub piece:u8, + pub promoted:bool, + pub player:u8, + pub tile:u8, +} +impl PacketGameStateResponsePiece { + pub fn new() -> Self + { + Self { + valid:false, + piece:0, + promoted:false, + player:0, + tile:0, + } + } +} + #[derive(Clone)] pub struct PacketGameStateResponse { pub status:u16, + pub turn:u16, + pub dawn_handle:String, + pub dusk_handle:String, + pub dawn_pool:[u8; 6], + pub dusk_pool:[u8; 6], + pub pieces:[PacketGameStateResponsePiece; 38], } impl PacketGameStateResponse { pub fn new() -> Self { Self { status:0, - + turn:0, + dawn_handle:String::new(), + dusk_handle:String::new(), + dawn_pool:[0; 6], + dusk_pool:[0; 6], + pieces:[PacketGameStateResponsePiece::new(); 38], } } } @@ -47,9 +87,41 @@ impl Packet for PacketGameStateResponse { fn encode(&self) -> Vec { + let mut piece_bytes = Vec::new(); + for piece in &self.pieces { + let piece_data: u16 = piece.valid as u16 + | ((piece.piece as u16) << 1) + | ((piece.promoted as u16) << 4) + | ((piece.player as u16) << 5) + | ((piece.tile as u16) << 6); + + piece_bytes.append(&mut pack_u16(piece_data)); + } + + let dawn_pool_bytes :u16 = self.dawn_pool[0] as u16 + | ((self.dawn_pool[1] as u16) << 5) + | ((self.dawn_pool[2] as u16) << 7) + | ((self.dawn_pool[3] as u16) << 9) + | ((self.dawn_pool[4] as u16) << 11) + | ((self.dawn_pool[5] as u16) << 13); + + let dusk_pool_bytes :u16 = self.dusk_pool[0] as u16 + | ((self.dusk_pool[1] as u16) << 5) + | ((self.dusk_pool[2] as u16) << 7) + | ((self.dusk_pool[3] as u16) << 9) + | ((self.dusk_pool[4] as u16) << 11) + | ((self.dusk_pool[5] as u16) << 13); + [ pack_u16(self.status), - + pack_u16(self.turn), + 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(dawn_pool_bytes), + pack_u16(dusk_pool_bytes), + piece_bytes, ].concat() } } diff --git a/server/src/protocol/packet/mod.rs b/server/src/protocol/packet/mod.rs index d5845ca..6e2206e 100644 --- a/server/src/protocol/packet/mod.rs +++ b/server/src/protocol/packet/mod.rs @@ -8,7 +8,6 @@ mod session_list; pub use session_list::*; mod session_create; pub use session_create::*; mod session_join; pub use session_join::*; mod session_retire; pub use session_retire::*; -mod session_leave; pub use session_leave::*; mod game_state; pub use game_state::*; mod game_play; pub use game_play::*; diff --git a/server/src/protocol/packet/session_leave.rs b/server/src/protocol/packet/session_leave.rs deleted file mode 100644 index e69de29..0000000 diff --git a/server/src/protocol/packet/session_retire.rs b/server/src/protocol/packet/session_retire.rs index e69de29..f144458 100644 --- a/server/src/protocol/packet/session_retire.rs +++ b/server/src/protocol/packet/session_retire.rs @@ -0,0 +1,31 @@ +use crate::app::session::SessionToken; + +use super::Packet; + +#[derive(Clone)] +pub struct PacketSessionRetire { + pub token:SessionToken, +} +impl PacketSessionRetire { + pub fn new() -> Self + { + Self { + token:SessionToken::default(), + } + } +} +impl Packet for PacketSessionRetire { + 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(()) + } + } +} diff --git a/server/src/system/cache/mod.rs b/server/src/system/cache/mod.rs index 2056e31..8d7d992 100644 --- a/server/src/system/cache/mod.rs +++ b/server/src/system/cache/mod.rs @@ -1,28 +1,22 @@ -use std::{io::Read, sync::{Arc, RwLock}}; +use std::{ + io::Read, + sync::{Arc, RwLock}, + fs::File, + path::Path, +}; + +use trie::Trie; use crate::util::string::minimize_whitespace; +#[derive(Clone)] +pub struct CacheData { + pub mime:String, + pub data:Vec, +} + struct WebCacheData { - html:String, - css:String, - js:String, - favicon:Vec, - - dawn_omen:Vec, - dawn_dragon:Vec, - dawn_castle:Vec, - dawn_tower:Vec, - dawn_lance:Vec, - dawn_knight:Vec, - dawn_militia:Vec, - - dusk_omen:Vec, - dusk_dragon:Vec, - dusk_castle:Vec, - dusk_tower:Vec, - dusk_lance:Vec, - dusk_knight:Vec, - dusk_militia:Vec, + objects:Trie, } #[derive(Clone)] @@ -30,230 +24,90 @@ pub struct WebCache { data:Arc>, } impl WebCache { - pub fn init() -> Self + pub fn new() -> Self { - use std::fs::File; - - // HTML - let mut html = String::new(); - if let Ok(mut file) = File::open("www/.html") { - file.read_to_string(&mut html).ok(); - html = minimize_whitespace(&html); - } - - // CSS - let css_path = std::path::Path::new("www/css/"); - let css: String = [ - "main.css", - "util.css", - "ui.css", - "form.css", - "game.css", - ].map(|path| { - let mut buffer = String::new(); - if let Ok(mut file) = File::open(css_path.join(path)) { - file.read_to_string(&mut buffer).ok(); - } - buffer - }).concat(); - - // JavaScript - let js_path = std::path::Path::new("www/js/"); - let js = [ - "const.js", - "util.js", - "game_asset.js", - "game.js", - "interface.js", - "ui.js", - "scene.js", - "system.js", - "main.js", - ].map(|path| { - let mut buffer = String::new(); - if let Ok(mut file) = File::open(js_path.join(path)) { - file.read_to_string(&mut buffer).ok(); - } - buffer - }).concat(); - - // Favicon - let mut favicon = Vec::::new(); - if let Ok(mut file) = File::open("www/favicon.png") { - file.read_to_end(&mut favicon).ok(); - } - - // Assets - let mut dawn_omen = Vec::::new(); - if let Ok(mut file) = File::open("www/asset/omen_dawn.svg") { - file.read_to_end(&mut dawn_omen).ok(); - } - - let mut dawn_dragon = Vec::::new(); - if let Ok(mut file) = File::open("www/asset/dragon_dawn.svg") { - file.read_to_end(&mut dawn_dragon).ok(); - } - - let mut dawn_castle = Vec::::new(); - if let Ok(mut file) = File::open("www/asset/castle_dawn.svg") { - file.read_to_end(&mut dawn_castle).ok(); - } - - let mut dawn_tower = Vec::::new(); - if let Ok(mut file) = File::open("www/asset/tower_dawn.svg") { - file.read_to_end(&mut dawn_tower).ok(); - } - - let mut dawn_lance = Vec::::new(); - if let Ok(mut file) = File::open("www/asset/lance_dawn.svg") { - file.read_to_end(&mut dawn_lance).ok(); - } - - let mut dawn_knight = Vec::::new(); - if let Ok(mut file) = File::open("www/asset/knight_dawn.svg") { - file.read_to_end(&mut dawn_knight).ok(); - } - - let mut dawn_militia = Vec::::new(); - if let Ok(mut file) = File::open("www/asset/militia_dawn.svg") { - file.read_to_end(&mut dawn_militia).ok(); - } - - - let mut dusk_omen = Vec::::new(); - if let Ok(mut file) = File::open("www/asset/omen_dusk.svg") { - file.read_to_end(&mut dusk_omen).ok(); - } - - let mut dusk_dragon = Vec::::new(); - if let Ok(mut file) = File::open("www/asset/dragon_dusk.svg") { - file.read_to_end(&mut dusk_dragon).ok(); - } - - let mut dusk_castle = Vec::::new(); - if let Ok(mut file) = File::open("www/asset/castle_dusk.svg") { - file.read_to_end(&mut dusk_castle).ok(); - } - - let mut dusk_tower = Vec::::new(); - if let Ok(mut file) = File::open("www/asset/tower_dusk.svg") { - file.read_to_end(&mut dusk_tower).ok(); - } - - let mut dusk_lance = Vec::::new(); - if let Ok(mut file) = File::open("www/asset/lance_dusk.svg") { - file.read_to_end(&mut dusk_lance).ok(); - } - - let mut dusk_knight = Vec::::new(); - if let Ok(mut file) = File::open("www/asset/knight_dusk.svg") { - file.read_to_end(&mut dusk_knight).ok(); - } - - let mut dusk_militia = Vec::::new(); - if let Ok(mut file) = File::open("www/asset/militia_dusk.svg") { - file.read_to_end(&mut dusk_militia).ok(); - } - Self { data:Arc::new(RwLock::new(WebCacheData { - html, css, js, favicon, - - dawn_omen, - dawn_dragon, - dawn_castle, - dawn_tower, - dawn_lance, - dawn_knight, - dawn_militia, - - dusk_omen, - dusk_dragon, - dusk_castle, - dusk_tower, - dusk_lance, - dusk_knight, - dusk_militia, + objects:Trie::new(), })), } } - pub fn html(&self) -> String - { - match self.data.read() { - Ok(reader) => reader.html.to_string(), - Err(_) => String::new(), - } - } - - pub fn css(&self) -> String - { - match self.data.read() { - Ok(reader) => reader.css.to_string(), - Err(_) => String::new(), - } - } - - pub fn js(&self) -> String - { - match self.data.read() { - Ok(reader) => reader.js.to_string(), - Err(_) => String::new(), - } - } - - pub fn favicon(&self) -> Vec - { - match self.data.read() { - Ok(reader) => reader.favicon.clone(), - Err(_) => Vec::new(), - } - } - - pub fn asset(&self, player:u8, id:u8) -> Vec + pub fn fetch(&self, object:&str) -> Option { match self.data.read() { Ok(reader) => { - match id { - 0 => match player { - 0 => reader.dawn_militia.clone(), - 1 => reader.dusk_militia.clone(), - _ => Vec::new(), - } - 1 => match player { - 0 => reader.dawn_knight.clone(), - 1 => reader.dusk_knight.clone(), - _ => Vec::new(), - } - 2 => match player { - 0 => reader.dawn_lance.clone(), - 1 => reader.dusk_lance.clone(), - _ => Vec::new(), - } - 3 => match player { - 0 => reader.dawn_tower.clone(), - 1 => reader.dusk_tower.clone(), - _ => Vec::new(), - } - 4 => match player { - 0 => reader.dawn_castle.clone(), - 1 => reader.dusk_castle.clone(), - _ => Vec::new(), - } - 5 => match player { - 0 => reader.dawn_dragon.clone(), - 1 => reader.dusk_dragon.clone(), - _ => Vec::new(), - } - 6 => match player { - 0 => reader.dawn_omen.clone(), - 1 => reader.dusk_omen.clone(), - _ => Vec::new(), - } - _ => Vec::new(), + match reader.objects.get(object.as_bytes()) { + Some(data) => Some(data.clone()), + None => None, } } - Err(_) => Vec::new(), + Err(_) => None, + } + } + + pub fn cache(&self, mime:&str, object:&str, data:Vec) + { + match self.data.write() { + Ok(mut writer) => { + writer.objects.set(object.as_bytes(), CacheData { + mime:mime.to_string(), + data, + }); + }, + Err(_) => { }, + } + } + + pub fn cache_file

>(&self, mime:&str, object:&str, path:P) -> Result<(),()> + { + match self.data.write() { + Ok(mut writer) => { + let mut data = Vec::new(); + if let Ok(mut file) = File::open(path) { + file.read_to_end(&mut data).ok(); + writer.objects.set(object.as_bytes(), CacheData { + mime:mime.to_string(), + data, + }); + Ok(()) + } else { Err(()) } + } + Err(_) => Err(()), + } + } + + pub fn cache_whitespace_minimize(&self, object:&str) -> Result<(),()> + { + match self.data.write() { + Ok(mut writer) => { + if let Some(data) = writer.objects.get_mut(object.as_bytes()) { + data.data = minimize_whitespace(&String::from_utf8(data.data.clone()).unwrap()).as_bytes().to_vec(); + Ok(()) + } else { Err(()) } + }, + Err(_) => Err(()), + } + } + + pub fn cache_file_group

>(&self, mime:&str, object:&str, paths:&[P]) -> Result<(),()> + { + match self.data.write() { + Ok(mut writer) => { + let data = paths.into_iter().map(|path| { + let mut buffer = Vec::new(); + if let Ok(mut file) = File::open(path) { + file.read_to_end(&mut buffer).ok(); + } + buffer + }).collect::>>().concat(); + writer.objects.set(object.as_bytes(), CacheData { + mime:mime.to_string(), + data:data, + }); + Ok(()) + }, + Err(_) => Err(()), } } } diff --git a/server/src/system/filesystem/mod.rs b/server/src/system/filesystem/mod.rs index 3d1d813..c24d21a 100644 --- a/server/src/system/filesystem/mod.rs +++ b/server/src/system/filesystem/mod.rs @@ -6,7 +6,7 @@ use game::Game; use crate::{ app::{ - session::{self, Session, SessionToken, SessionSecret}, + session::{Session, SessionToken, SessionSecret}, user::User, }, util::pack::*, @@ -144,6 +144,8 @@ impl FileSystem { pub fn session_fetch(&mut self, id:u32) -> Result { + use crate::app::session::Player; + let bucket_index = id & !HANDLE_BUCKET_MASK; let dir_index = id & HANDLE_BUCKET_MASK; @@ -189,15 +191,15 @@ impl FileSystem { token, secret, game:Game::new(), - p_dawn:session::Viewer { - connection:None, + p_dawn:Player { user:dawn, + connections:Vec::new(), }, - p_dusk:session::Viewer { - connection:None, + p_dusk:Player { user:dusk, + connections:Vec::new(), }, - viewers:Vec::new(), + connections:Vec::new(), time, chain_id:0, }) diff --git a/server/src/util/pack.rs b/server/src/util/pack.rs index 2fb68b2..f51302a 100644 --- a/server/src/util/pack.rs +++ b/server/src/util/pack.rs @@ -32,6 +32,24 @@ pub fn unpack_u16(data:&[u8], index:&mut usize) -> u16 result } +pub fn pack_u24(value:u32) -> Vec +{ + vec![(value >> 16) as u8, (value >> 8) as u8, (value & 0xFF) as u8] +} + +pub fn unpack_u24(data:&[u8], index:&mut usize) -> u32 +{ + let mut result = 0u32; + for _ in 0..3 { + result <<= 8; + if *index < data.len() { + result |= data[*index] as u32; + *index += 1; + } else { break; } + } + result +} + pub fn pack_u32(value:u32) -> Vec { vec![ diff --git a/www/favicon.png b/www/asset/favicon.png similarity index 100% rename from www/favicon.png rename to www/asset/favicon.png diff --git a/www/js/const.js b/www/js/const.js index 015f53c..37607ff 100644 --- a/www/js/const.js +++ b/www/js/const.js @@ -4,6 +4,7 @@ let SCENE = null; let CONNECTED = false; let SOCKET = null; + let CONTEXT = { Scene: null, Auth: null, diff --git a/www/js/game.js b/www/js/game.js index 2dd42ad..1c92aae 100644 --- a/www/js/game.js +++ b/www/js/game.js @@ -3,14 +3,15 @@ let GAME_DATA = null; GAME.Board = class { constructor() { - this.tiles = [ ]; for(let i = 0; i < 61; ++i) { this.tiles.push(new GAME.Tile()); } - this.pieces = [ ]; + this.tiles = [ ]; for(let i = 0; i < 61; ++i) { this.tiles.push(new GAME.Tile(i)); } + this.pieces = [ ]; for(let i = 0; i < 38; ++i) { this.pieces.push(null); } + this.columns = [ ]; for(let i = 0; i < 9; ++i) { this.columns.push(new GAME.Column()); } this.init(); } init() { - this.pieces = [ ]; + this.pieces = [ ]; for(let i = 0; i < 38; ++i) { this.pieces.push(null); } // Describe Dawn layout let layout = [ @@ -42,65 +43,74 @@ GAME.Board = class { // Add Dawn pieces for(let piece of layout) { - this.place_piece(piece.piece, GAME.Const.Player.Dawn, piece.hex); + this.set_piece(piece.piece, GAME.Const.Player.Dawn, piece.hex); } // Add Dusk pieces for(let piece of layout) { let hex = new MATH.Vec2(8 - piece.hex.x, 8 - piece.hex.y); - this.place_piece(piece.piece, GAME.Const.Player.Dusk, hex); + this.set_piece(piece.piece, GAME.Const.Player.Dusk, hex); } } - place_piece(piece, player, hex) { + set_piece(piece, player, hex) { + let index = 0; + while(this.pieces[index] !== null) { index++; } + let game_piece = new GAME.Piece(piece, player); - game_piece.tile = this.hex_to_tile(hex); - this.tiles[game_piece.tile].piece = this.pieces.length; - this.pieces.push(game_piece); + game_piece.tile = HEX.hex_to_tile(hex); + this.tiles[game_piece.tile].piece = index; + this.pieces[index] = game_piece; } - hex_to_tile(hex) { - let a = ((hex.x + 4) * (hex.x + 5) / 2) - 10; - let b = (hex.x > 4) * ((hex.x - 4) + ((hex.x - 5) * (hex.x - 4))); - return a - b + hex.y; - } - - tile_to_hex(tile) { - const ROWS = [ 0, 5, 11, 18, 26, 35, 43, 50, 56, 61 ]; - let column = 0; - while(tile >= ROWS[column + 1]) { column += 1; } - let row = tile - ROWS[column] + ((column > 4) * (column - 4)); - return new MATH.Vec2(column, row); - } -}; - -GAME.Player = class { - constructor() { - this.handle = ""; - this.pool = new GAME.Pool(); + reset() { + for(let i = 0; i < this.tiles.length; ++i) { this.tiles[i].reset(); } + for(let i = 0; i < this.columns.length; ++i) { this.columns[i].reset(); } } }; GAME.Pool = class { constructor() { - this.pieces = [ ]; for(let i = 0; i < 6; ++i) { this.pieces.push(0); } + this.pieces = [ ]; for(let i = 0; i < 6; ++i) { this.pieces.push(1); } + } +}; + +GAME.Column = class { + constructor() { + this.militia = [false, false]; + this.extent = [0, 8]; + } + + reset() { + this.militia = [false, false]; + this.extent = [0, 8]; } }; GAME.Tile = class { - constructor() { + constructor(index) { this.piece = null; - this.threaten = { dawn: 0, dusk: 0 }; - this.checking = { dawn: false, dusk: false }; + + this.threaten = [false, false]; + this.checking = [false, false]; + + this.hex = HEX.tile_to_hex(index); } reset() { - this.threaten = { dawn: 0, dusk: 0 }; - this.checking = { dawn: false, dusk: false }; + this.threaten = [0, 0]; + this.checking = [false, false]; } }; -GAME.Move = class { +GAME.MovementTile = class { + constructor(tile, status) { + this.tile = tile; + this.status = status; + } +}; + +GAME.Play = class { constructor(source, from, to) { this.source = source; this.from = from; @@ -109,20 +119,10 @@ GAME.Move = class { }; GAME.GamePiece = class { - constructor(name, assets, moves, promote_moves) { + constructor(name, moves, promote_moves) { this.name = name; this.moves = moves; this.pmoves = promote_moves; - this.assets = null; - - if(assets !== null) { - this.assets = [ - new Image(), - new Image() - ]; - this.assets[0].src = assets[0]; - this.assets[1].src = assets[1]; - } } }; @@ -160,6 +160,18 @@ GAME.Piece = class { this.tile = 0; this.blocking = 0; } + + moves() { + let moves = null; + if(this.promoted) { moves = GAME.Const.Piece[this.piece].pmoves; } + else { moves = GAME.Const.Piece[this.piece].moves; } + if(this.player == GAME.Const.Player.Dusk) { moves = moves.rotate(); } + return moves; + } + + reset() { + this.blocking = 0; + } }; GAME.Game = class { @@ -167,33 +179,271 @@ GAME.Game = class { this.turn = 0; this.board = new GAME.Board(); - this.dawn = new GAME.Player(); - this.dusk = new GAME.Player(); + this.pools = [ + new GAME.Pool(), + new GAME.Pool(), + ]; + + this.state = { + check:false, + checkmate:false, + }; + + this.update_board(); } update_board() { // Reset tiles - for(tile of this.board.tiles) { tile.reset(); } + this.board.reset(); + + this.state.check = false; + this.state.checkmate = false; // Determine threaten, check, and blocking for each piece - for(piece of this.board.pieces) { + for(let piece of this.board.pieces) { + if(piece !== null) { + let hex = this.board.tiles[piece.tile].hex; + + // Check if column has militia. + if(piece.piece == GAME.Const.PieceId.Militia) { + this.board.columns[hex.x].militia[piece.player] = true; + } + + // Check furthest piece in column. + if(piece.player == 0) { this.board.columns[hex.x].extent[0] = Math.max(hex.y, this.board.columns[hex.x].extent[0]); } + else { this.board.columns[hex.x].extent[1] = Math.min(hex.y, this.board.columns[hex.x].extent[1]); } + + for(let movement of this.movement_tiles_ext(piece, piece.tile)) { + if(movement.status != GAME.Const.MoveStatus.Invalid) { + if(movement.status == GAME.Const.MoveStatus.Check) { + this.state.check = true; + } + this.board.tiles[movement.tile].threaten[piece.player] = true; + } + } + } + } + + // Count moves available to next turn player to determine checkmate. + if(this.state.check) { } } process(move) { + // Check if swapped piece. + // Check if piece should be promoted. + // Check if swapped piece should be promoted. + // Add piece to pool if taken. + // Move pieces. + // TODO + // - Validate move. + // - Improve data safety validation. + // + + // Move piece on board. + if(move.source == 0) { + let piece_id = this.board.tiles[move.from].piece; + let piece = this.board.pieces[piece_id]; + piece.tile = move.to; + this.board.tiles[move.from].piece = null; + + let target_id = this.board.tiles[move.to].piece; + if(target_id !== null) { + let target = this.board.pieces[target_id]; + + // Swap piece with moving piece. + if(target.player == piece.player) { + target.tile = move.from; + this.board.tiles[move.from].piece = target_id; + + // Check if swap is promoted. + let hex = HEX.tile_to_hex(target.tile); + hex.y -= MATH.sign_branch(target.player); + if(!target.promoted && !HEX.is_valid(hex)) { + target.promoted = true; + } + } + + // Add captured piece to pool and destroy. + else { + console.log(piece.player); + this.pools[piece.player].pieces[target.piece] += 1; + this.board.pieces[target_id] = null; + } + } + + this.board.tiles[move.to].piece = piece_id; + + // Check if piece is promoted. + let hex = HEX.tile_to_hex(piece.tile); + hex.y -= MATH.sign_branch(piece.player); + if(!piece.promoted && !HEX.is_valid(hex)) { + piece.promoted = true; + } + } + + // Place piece from pool. + else { + + } + + this.turn++; // Recalculate new board state. - GAME.update_board(); + this.update_board(); + + // If check, detect checkmate. } - validate(move) { + movement_tiles(piece, tile) { + let tiles = movement_tiles_ext(piece, tile); + let result = [ ]; + for(let movement in tiles) { + if(movement.status) { result.push(movement); } + } + + return result; } - get_move_tiles(piece_id) { + movement_tiles_ext(piece, tile) { + let tiles = [ ]; + let moves = piece.moves(); + let hex = this.board.tiles[tile].hex; + let directions = moves.direction; + let permitted_moves = directions; + if(piece.blocking != 0) { + if(piece.piece == GAME.Const.PieceId.Omen) { + permitted_moves &= ~piece.blocking; + } else { + permitted_moves &= piece.blocking; + } + } + + // Check directions of movement. + for(let mask = BITWISE.lsb(directions); directions > 0; mask = BITWISE.lsb(directions)) { + let direction_id = BITWISE.ffs(mask); + let direction = GAME.Const.get_direction(direction_id); + let stride = (moves.stride & mask) != 0; + + // Get initial status in direction. + let status = (permitted_moves & mask)? GAME.Const.MoveStatus.Valid : GAME.Const.MoveStatus.Invalid; + + let move_hex = hex.copy(); + + // Check tiles in direction up to movement limit. + let max_dist = (stride)? 8 : 1; + for(let dist = 1; dist <= max_dist; ++dist) { + move_hex.add(direction); + + if(HEX.is_valid(move_hex)) { + let tile_id = HEX.hex_to_tile(move_hex); + let tile_data = this.board.tiles[tile_id]; + + let result = status; + + // King may not move onto threatened tile. + if(piece.piece == GAME.Const.PieceId.Omen && tile_data.threaten[+(!piece.player)]) { + status = GAME.Const.MoveStatus.Invalid; + } + + // Handle occupied tile. + if(tile_data.piece !== null) { + let target = this.board.pieces[tile_data.piece]; + + // Target piece is ally. + if(target.player == piece.player) { + + // Move is only valid if pieces are swappable. + if(!this.movement_swappable(target, mask, dist, tile)) { + result = GAME.Const.MoveStatus.Invalid; + } + status = GAME.Const.MoveStatus.Invalid; + } + + // Target piece is opposing. + else { + // Check if target piece is opposing king. + if(target.piece == GAME.Const.PieceId.Omen) { + if(status == GAME.Const.MoveStatus.Valid) { + result = GAME.Const.MoveStatus.Check; + } + } + status = GAME.Const.MoveStatus.Invalid; + } + } + + tiles.push(new GAME.MovementTile(tile_id, result)); + } else { break; } + } + + directions &= ~mask; + } + + return tiles; + } + + movement_swappable(target, mask, range, tile) { + mask = BITWISE.rotate_blocks(mask); + let moves = target.moves(); + + // King cannot swap onto tile that is threatened. + // This case should also cover blocking. + if(target.piece == GAME.Const.PieceId.Omen && this.board.tiles[tile].threaten[+(!target.player)]) { + return false; + } + return ((moves.direction & mask) != 0 && (range == 1 || (moves.stride & mask) != 0)); + } + + placement_tiles_ext(piece_id, player) { + let tiles = [ ]; + let piece = new GAME.Piece(piece_id, player); + + // Get tiles onto which piece may be placed. + for(let i = 0; i < this.board.tiles.length; ++i) { + let hex = HEX.tile_to_hex(i); + let tile = this.board.tiles[i]; + let status = GAME.Const.MoveStatus.Invalid; + + // Check if tile is occupied. + if(tile.piece === null) { + let position_valid = true; + + // Check off-sides. + if(piece.player == 0) { + position_valid = hex.y <= this.board.columns[hex.x].extent[+(!player)]; + } else { + position_valid = hex.y >= this.board.columns[hex.x].extent[+(!player)]; + } + + // Check militia stacking. + if(piece_id == GAME.Const.PieceId.Militia && this.board.columns[hex.x].militia[player]) { + position_valid = false; + } + + // Check if position puts king in check. + let checking = false; + let movements = this.movement_tiles_ext(piece, i); + for(let movement of movements) { + if(movement.status == GAME.Const.MoveStatus.Check) { + checking = true; + break; + } + } + + // Piece must have movements and not put king in check. + if(position_valid && movements.length > 0 && !checking) { + status = GAME.Const.MoveStatus.Valid; + } + } + + tiles.push(new GAME.MovementTile(i, status)); + } + + return tiles; } }; @@ -224,6 +474,12 @@ GAME.Const = { new MATH.Vec2(-1, 1), ], + MoveStatus: { + Valid: 0, + Invalid: 1, + Check: 2, + }, + PieceId: { Militia: 0, Knight: 1, @@ -237,7 +493,6 @@ GAME.Const = { Piece: [ new GAME.GamePiece( "Militia", - ["asset/militia_dusk.svg", "asset/militia_dawn.svg"], new GAME.PieceMovement() .add(0) .add(1) @@ -251,7 +506,6 @@ GAME.Const = { ), new GAME.GamePiece( "Knight", - ["asset/knight_dusk.svg", "asset/knight_dawn.svg"], new GAME.PieceMovement() .add(3) .add(6) @@ -271,7 +525,6 @@ GAME.Const = { ), new GAME.GamePiece( "Lance", - ["asset/lance_dusk.svg", "asset/lance_dawn.svg"], new GAME.PieceMovement() .add_stride(0) .add(1) @@ -286,7 +539,6 @@ GAME.Const = { ), new GAME.GamePiece( "Tower", - ["asset/tower_dusk.svg", "asset/tower_dawn.svg"], new GAME.PieceMovement() .add(0) .add(1) @@ -308,7 +560,6 @@ GAME.Const = { ), new GAME.GamePiece( "Castle", - ["asset/castle_dusk.svg", "asset/castle_dawn.svg"], new GAME.PieceMovement() .add(0) .add(1) @@ -329,7 +580,6 @@ GAME.Const = { ), new GAME.GamePiece( "Dragon", - ["asset/dragon_dusk.svg", "asset/dragon_dawn.svg"], new GAME.PieceMovement() .add_stride(6) .add_stride(7) @@ -353,7 +603,6 @@ GAME.Const = { ), new GAME.GamePiece( "Omen", - ["asset/king_dusk.svg", "asset/king_dawn.svg"], new GAME.PieceMovement() .add(0) .add(1) @@ -365,6 +614,12 @@ GAME.Const = { .add(10), ), ], + + get_direction(direction_id) { + let direction = GAME.Const.Direction[direction_id % 12].copy(); + direction.mul(Math.ceil((direction_id + 1) / 12)); + return direction; + }, }; GAME.init = () => { diff --git a/www/js/game_asset.js b/www/js/game_asset.js index e69de29..af24a8b 100644 --- a/www/js/game_asset.js +++ b/www/js/game_asset.js @@ -0,0 +1,20 @@ +const GAME_ASSET = { }; + +GAME_ASSET.load_image = (image) => { + let img = new Image(); + img.src = image; + return img; +}; + +GAME_ASSET.Image = { + Promote: GAME_ASSET.load_image("asset/promote.svg"), + Piece: [ + [ GAME_ASSET.load_image("asset/militia_dawn.svg"), GAME_ASSET.load_image("asset/militia_dusk.svg") ], + [ GAME_ASSET.load_image("asset/knight_dawn.svg"), GAME_ASSET.load_image("asset/knight_dusk.svg") ], + [ GAME_ASSET.load_image("asset/lance_dawn.svg"), GAME_ASSET.load_image("asset/lance_dusk.svg") ], + [ GAME_ASSET.load_image("asset/tower_dawn.svg"), GAME_ASSET.load_image("asset/tower_dusk.svg") ], + [ GAME_ASSET.load_image("asset/castle_dawn.svg"), GAME_ASSET.load_image("asset/castle_dusk.svg") ], + [ GAME_ASSET.load_image("asset/dragon_dawn.svg"), GAME_ASSET.load_image("asset/dragon_dusk.svg") ], + [ GAME_ASSET.load_image("asset/omen_dawn.svg"), GAME_ASSET.load_image("asset/omen_dusk.svg") ], + ], +}; diff --git a/www/js/interface.js b/www/js/interface.js index 65fa506..692aee2 100644 --- a/www/js/interface.js +++ b/www/js/interface.js @@ -16,55 +16,259 @@ const INTERFACE = { Dusk: "#f6a1bd", DuskDark: "#c51162", - HintHover: "#71a1e8", - HintSelect: "#4a148c", - HintValid: "#1a237e", - HintAllowed: "#6a1b9a", - HintInvalid: "b71c1c", - HintPlay: "#083242", - HintWarn: "#054719", - HintCheck: "#C62828", + HintHover: "#71a1e8", + HintSelect: "#4a148c", + HintValid: "#1a237e", HintValidBorder: "#5558fc", + HintThreat: "#054719", HintThreatBorder: "#22b54e", + HintOpponent: "#49136b", HintOpponentBorder: "#d74cef", + HintInvalid: "#b71c1c", HintInvalidBorder: "#ed3636", + HintPlay: "#083242", + HintCheck: "#C62828", + }, + + TileStatus: { + Valid: 1, + Threat: 2, + Invalid: 3, + Opponent: 4, + Play: 5, + Check: 6, + }, + Selection: class { + constructor(source, tile, hex) { + this.source = source; + this.tile = tile; + this.hex = hex; + } + }, + + resolve_board() { + for(let i = 0; i < INTERFACE_DATA.board_state.length; ++i) { + INTERFACE_DATA.board_state[i] = [0, 0]; + } + + if(INTERFACE_DATA.select !== null) { INTERFACE.resolve_piece(INTERFACE_DATA.select, 1); } + if(INTERFACE_DATA.hover !== null) { INTERFACE.resolve_piece(INTERFACE_DATA.hover, 0); } + }, + + resolve_piece(selection, zone) { + // Determine piece movement hints. + + let movements = null; + let target = null; + let player = 0; + if(selection.source == 0) { + let piece_id = GAME_DATA.board.tiles[selection.tile].piece; + if(piece_id !== null) { + let piece = GAME_DATA.board.pieces[piece_id]; + player = piece.player; + movements = GAME_DATA.movement_tiles_ext(piece, selection.tile); + } + } else { + player = Math.floor(selection.tile / 6); + player ^= (INTERFACE_DATA.player & 1) ^ INTERFACE_DATA.rotate; + movements = GAME_DATA.placement_tiles_ext(selection.tile % 6, player); + } + + if(movements !== null) { + // Generate hint for each potential movement. + for(let movement of movements) { + if(movement.status != GAME.Const.MoveStatus.Invalid) { + // Show valid/threat hints if piece belongs to player and is player turn. + if(INTERFACE_DATA.player == 2 || (player == INTERFACE_DATA.player && (GAME_DATA.turn & 1) == player)) { + if(GAME_DATA.board.tiles[movement.tile].threaten[+(!player)]) { + INTERFACE_DATA.board_state[movement.tile][zone] = INTERFACE.TileStatus.Threat; + } else { + INTERFACE_DATA.board_state[movement.tile][zone] = INTERFACE.TileStatus.Valid; + } + } + else { + INTERFACE_DATA.board_state[movement.tile][zone] = INTERFACE.TileStatus.Opponent; + } + } else { + INTERFACE_DATA.board_state[movement.tile][zone] = INTERFACE.TileStatus.Invalid; + } + } + } }, hover(event) { + let initial_hover = INTERFACE_DATA.hover; + let apothem = INTERFACE_DATA.Ui.scale; + let radius = INTERFACE.Radius * apothem; + let halfradius = radius / 2; + let grid_offset_x = 1.5 * radius; + + let hex_slope = 0.25 / 0.75; + + INTERFACE_DATA.hover = null; + + // Handle board area + if(event.offsetY >= INTERFACE_DATA.Ui.margin && event.offsetY < INTERFACE_DATA.Ui.margin + INTERFACE_DATA.Ui.area.y) { + if(event.offsetX >= INTERFACE_DATA.Ui.offset.x && event.offsetX < INTERFACE_DATA.Ui.offset.x + INTERFACE_DATA.Ui.board_width) { + + let basis_x = INTERFACE_DATA.Ui.offset.x + halfradius; + let basis_y = INTERFACE_DATA.Ui.offset.y + (14 * apothem); + + let x = (event.offsetX - basis_x) / grid_offset_x; + let y = -(event.offsetY - basis_y) / apothem; + + let kx = Math.floor(x); + let ky = Math.floor(y); + + let apo_offset = Math.abs(MATH.mod(y + (kx & 1), 2.0) - 1); + let rad_offset = MATH.mod(x, 1); + let rad_slope = 1 - (hex_slope * apo_offset); + + let hx = kx + (rad_offset > rad_slope); + let hy = Math.floor((ky + hx) / 2.0); + + if((INTERFACE_DATA.player & 1) ^ INTERFACE_DATA.rotate == 1) { + hx = 8 - hx; + hy = 8 - hy; + } + + let hex = new MATH.Vec2(hx, hy); + if(HEX.is_valid(hex)) { + let tile = HEX.hex_to_tile(hex); + INTERFACE_DATA.hover = new INTERFACE.Selection(0, tile, hex); + } + } + + // Handle pool area + else if(event.offsetX >= INTERFACE_DATA.Ui.pool_offset && event.offsetX < INTERFACE_DATA.Ui.offset.x + INTERFACE_DATA.Ui.area.x) { + + let basis_x = INTERFACE_DATA.Ui.pool_offset + halfradius; + let basis_y = INTERFACE_DATA.Ui.offset.y + (3 * apothem); + + let x = (event.offsetX - basis_x) / grid_offset_x; + let y = (event.offsetY - basis_y) / apothem; + + let kx = Math.floor(x); + let ky = Math.floor(y); + + let apo_offset = Math.abs(MATH.mod(y + (kx & 1), 2.0) - 1); + let rad_offset = MATH.mod(x, 1); + let rad_slope = 1 - (hex_slope * apo_offset); + + let hx = kx + (rad_offset > rad_slope); + let hy = Math.floor((ky - hx) / 2.0); + + let hex = new MATH.Vec2(hx, hy); + if(INTERFACE.Ui.pool_hex_is_valid(hex)) { + let tile = (hx * 6) + hy; + INTERFACE_DATA.hover = new INTERFACE.Selection(1, tile, hex); + } + } + } + + if(initial_hover != INTERFACE_DATA.hover) { INTERFACE.draw(); } + }, + + unhover() { + let redraw = (INTERFACE_DATA.hover !== null); + INTERFACE_DATA.hover = null; + if(redraw) { INTERFACE.draw(); } }, click(event) { + let initial_select = INTERFACE_DATA.select; + if(INTERFACE_DATA.hover !== null) { + if(INTERFACE.Ui.match_select(INTERFACE_DATA.hover, INTERFACE_DATA.select)) { + INTERFACE_DATA.select = null; + } else { + // Check if operation can be performed on new tile. + // Otherwise, switch selection. + let is_valid = false; + if(INTERFACE_DATA.select !== null && INTERFACE_DATA.hover.source == 0 && INTERFACE_DATA.player == (GAME_DATA.turn & 1)) { + let tile_state = INTERFACE_DATA.board_state[INTERFACE_DATA.hover.tile][1]; + is_valid = (tile_state == INTERFACE.TileStatus.Valid || tile_state == INTERFACE.TileStatus.Threat); + } + + if(is_valid) { + let move_data = INTERFACE_DATA.select.source | (INTERFACE_DATA.select.tile << 1) | (INTERFACE_DATA.hover.tile << 7); + console.log("SEND Play"); + MESSAGE_COMPOSE([ + PACK.u16(OpCode.GamePlay), + PACK.u16(0), + PACK.u16(GAME_DATA.turn), + PACK.u16(move_data), + ]); + INTERFACE_DATA.select = null; + } else { + INTERFACE_DATA.select = null; + if(INTERFACE_DATA.hover.source == 0) { + if(GAME_DATA.board.tiles[INTERFACE_DATA.hover.tile].piece !== null) { + INTERFACE_DATA.select = INTERFACE_DATA.hover; + } + } else { + let pool_player = Math.floor(INTERFACE_DATA.hover.tile / 6); + pool_player ^= (INTERFACE_DATA.player & 1) ^ INTERFACE_DATA.rotate; + + let pool_piece = INTERFACE_DATA.hover.tile % 6; + + if(GAME_DATA.pools[pool_player].pieces[pool_piece] > 0) { + INTERFACE_DATA.select = INTERFACE_DATA.hover; + } + } + } + } + } + + // Clear selection if no tile is hovered. + else { + INTERFACE_DATA.select = null; + } + + if(initial_select !== INTERFACE_DATA.select) { + INTERFACE.draw(); + } + }, + + resize() { + let width = INTERFACE_DATA.canvas.width = INTERFACE_DATA.canvas.clientWidth; + let height = INTERFACE_DATA.canvas.height = INTERFACE_DATA.canvas.clientHeight; + + let margin = INTERFACE_DATA.Ui.margin = Math.floor(Math.min(width, height) / 100); + + let gui_width = width - (margin * 2); + let gui_height = height - (margin * 2); + + if(gui_width < gui_height * INTERFACE.Ratio) { + gui_height = Math.floor(gui_width / INTERFACE.Ratio); + } else { + gui_width = Math.floor(gui_height * INTERFACE.Ratio); + } + + let gui_scale = gui_height * INTERFACE.Scale; + + INTERFACE_DATA.Ui.area.x = gui_width; + INTERFACE_DATA.Ui.area.y = gui_height; + INTERFACE_DATA.Ui.scale = gui_scale; + + INTERFACE_DATA.Ui.offset.x = (width - gui_width) / 2; + INTERFACE_DATA.Ui.offset.y = (height - gui_height) / 2; + + INTERFACE_DATA.Ui.board_width = Math.ceil(INTERFACE.BoardWidth * gui_scale); + INTERFACE_DATA.Ui.pool_offset = INTERFACE_DATA.Ui.offset.x + Math.floor(INTERFACE.PoolOffset * gui_scale); }, draw() { let canvas = INTERFACE_DATA.canvas; let ctx = INTERFACE_DATA.context; - // Determine interface configuration - canvas.width = canvas.clientWidth; - canvas.height = canvas.clientHeight; + INTERFACE.resize(); + INTERFACE.resolve_board(); let width = canvas.width; let height = canvas.height; - - // Interface margin - let gui_margin = Math.floor(Math.min(width, height) / 100); - // Interface width, height, and scale - let gui_width = width - (gui_margin * 2); - let gui_height = height - (gui_margin * 2); - - if(gui_width < gui_height * INTERFACE.Ratio) { - gui_height = Math.floor(gui_width / INTERFACE.Ratio); - } else { - gui_width = Math.floor(gui_height * INTERFACE.Ratio); - } - let gui_scale = gui_height * INTERFACE.Scale; - - // Boundries to center interface - let gui_offset = new MATH.Vec2( - (width - gui_width) / 2, - (height - gui_height) / 2, - ); + let gui_margin = INTERFACE_DATA.Ui.margin; + let gui_offset = INTERFACE_DATA.Ui.offset; + let gui_scale = INTERFACE_DATA.Ui.scale; // Draw background @@ -77,17 +281,28 @@ const INTERFACE = { let radius = INTERFACE.Radius * gui_scale; let basis_x = gui_offset.x + radius; let basis_y = gui_offset.y + (13 * gui_scale); - let icon_radius = 0.7 * radius; + let icon_radius = 0.69 * radius; const TILE_SCALE = 0.9; - ctx.lineWidth = gui_scale * 0.06; + ctx.lineWidth = Math.min(gui_scale * 0.06, 3); let draw = new INTERFACE.Draw(ctx, TILE_SCALE * gui_scale); for(let i = 0; i < GAME_DATA.board.tiles.length; ++i) { let tile = GAME_DATA.board.tiles[i]; - let coord = GAME_DATA.board.tile_to_hex(i); + let is_hover = INTERFACE.Ui.tile_is_hover(0, i); + let is_select = INTERFACE.Ui.tile_is_select(0, i); + + let tile_state = INTERFACE_DATA.board_state[i][1]; + let border_state = INTERFACE_DATA.board_state[i][0]; + + let coord = HEX.tile_to_hex(i); + if((INTERFACE_DATA.player & 1) ^ INTERFACE_DATA.rotate == 1) { + coord.x = 8 - coord.x; + coord.y = 8 - coord.y; + } + let gui_x = basis_x + (1.5 * radius * coord.x); let gui_y = basis_y - (2 * gui_scale * coord.y) + (gui_scale * coord.x); @@ -96,44 +311,54 @@ const INTERFACE = { // Draw background. // Select indicator color or default to tile color. - if(true) { - switch(MATH.mod(coord.x + coord.y, 3)) { - case 0: ctx.fillStyle = INTERFACE.Color.TileMedium; break; - case 1: ctx.fillStyle = INTERFACE.Color.TileLight; break; - case 2: ctx.fillStyle = INTERFACE.Color.TileDark; break; - } - } else { - + switch(MATH.mod(coord.x + coord.y, 3)) { + case 0: ctx.fillStyle = INTERFACE.Color.TileMedium; break; + case 1: ctx.fillStyle = INTERFACE.Color.TileLight; break; + case 2: ctx.fillStyle = INTERFACE.Color.TileDark; break; + } + switch(tile_state) { + case INTERFACE.TileStatus.Valid: ctx.fillStyle = INTERFACE.Color.HintValid; break; + case INTERFACE.TileStatus.Threat: ctx.fillStyle = INTERFACE.Color.HintThreat; break; + case INTERFACE.TileStatus.Invalid: ctx.fillStyle = INTERFACE.Color.HintInvalid; break; + case INTERFACE.TileStatus.Opponent: ctx.fillStyle = INTERFACE.Color.HintOpponent; break; + } + if(is_select) { + ctx.fillStyle = INTERFACE.Color.HintSelect; } ctx.beginPath(); draw.hex(); ctx.fill(); + // Draw border + ctx.strokeStyle = INTERFACE.Color.TileBorder; + if(tile.piece !== null) { + if(GAME_DATA.board.pieces[tile.piece].player == GAME.Const.Player.Dawn) { ctx.strokeStyle = INTERFACE.Color.DawnDark; } + else { ctx.strokeStyle = INTERFACE.Color.DuskDark; } + } + switch(border_state) { + case INTERFACE.TileStatus.Valid: ctx.strokeStyle = INTERFACE.Color.HintValidBorder; break; + case INTERFACE.TileStatus.Threat: ctx.strokeStyle = INTERFACE.Color.HintThreatBorder; break; + case INTERFACE.TileStatus.Invalid: ctx.strokeStyle = INTERFACE.Color.HintInvalidBorder; break; + case INTERFACE.TileStatus.Opponent: ctx.strokeStyle = INTERFACE.Color.HintOpponentBorder; break; + } + if(is_hover) { + ctx.strokeStyle = INTERFACE.Color.HintHover; + } + ctx.beginPath(); + draw.hex(); + ctx.stroke(); + // Draw tile content if(tile.piece !== null) { let piece = GAME_DATA.board.pieces[tile.piece]; - let game_piece = GAME.Const.Piece[piece.piece]; - - // Draw border - if(piece.player == GAME.Const.Player.Dawn) { ctx.strokeStyle = INTERFACE.Color.DawnDark; } - else { ctx.strokeStyle = INTERFACE.Color.DuskDark; } - ctx.beginPath(); - draw.hex(); - ctx.stroke(); // Draw border hints - draw.hints(piece); + if(!is_hover && border_state == 0) { draw.hints(piece); } // Draw piece icon - //if(piece.promoted) { ctx.drawImage(I_PROMOTE, -icon_radius, -icon_radius, icon_radius * 2., icon_radius * 2.); } - //ctx.drawImage(game_piece.assets[piece.player], -icon_radius, -icon_radius, icon_radius * 2., icon_radius * 2.); + if(piece.promoted) { ctx.drawImage(GAME_ASSET.Image.Promote, -icon_radius, -icon_radius, icon_radius * 2., icon_radius * 2.); } + ctx.drawImage(GAME_ASSET.Image.Piece[piece.piece][piece.player], -icon_radius, -icon_radius, icon_radius * 2., icon_radius * 2.); - } else { - // Draw standard border - ctx.strokeStyle = INTERFACE.Color.TileBorder; - ctx.beginPath(); - draw.hex(); - ctx.stroke(); } ctx.restore(); @@ -142,8 +367,21 @@ const INTERFACE = { ctx.font = Math.ceil(gui_scale / 2) + "px sans-serif"; + let player_identity = GAME.Const.Player.Dawn; + let player_color = INTERFACE.Color.Dawn; + let opponent_color = INTERFACE.Color.Dusk; + + if(INTERFACE_DATA.player == 1 || (INTERFACE_DATA.player == 2 && INTERFACE_DATA.rotate == 1)) { + player_identity = GAME.Const.Player.Dusk; + player_color = INTERFACE.Color.Dusk; + opponent_color = INTERFACE.Color.Dawn; + } + // Draw player pool for(let i = 0; i < 6; ++i) { + let is_hover = INTERFACE.Ui.tile_is_hover(1, i); + let is_select = INTERFACE.Ui.tile_is_select(1, i); + let gui_x = basis_x + (radius * 14); let gui_y = basis_y - (9 - (2 * i)) * gui_scale; @@ -151,79 +389,128 @@ const INTERFACE = { ctx.translate(gui_x, gui_y); // Draw background if indicator is present. - if(true) { - ctx.fillStyle = INTERFACE.Color.TileDark; - ctx.beginPath(); - draw.hex(); - ctx.fill(); - } - - // Draw border - ctx.strokeStyle = INTERFACE.Color.Dawn; + if(is_select) { ctx.fillStyle = INTERFACE.Color.HintSelect; } + else { ctx.fillStyle = INTERFACE.Color.TileDark; } ctx.beginPath(); draw.hex(); - ctx.stroke(); + ctx.fill(); - ctx.fillStyle = INTERFACE.Color.Dawn; + // Draw border + if(is_select || is_hover || INTERFACE_DATA.player == (GAME_DATA.turn & 1) || (INTERFACE_DATA.player == 2 && player_identity == (GAME_DATA.turn & 1))) { + if(is_hover) { ctx.strokeStyle = INTERFACE.Color.HintHover; } + else { ctx.strokeStyle = player_color; } + ctx.beginPath(); + draw.hex(); + ctx.stroke(); + } + + // Draw image + ctx.drawImage(GAME_ASSET.Image.Piece[i][+(player_identity == 1)], -icon_radius * 0.55, -icon_radius * 0.8, icon_radius * 1.6, icon_radius * 1.6); + + // Draw count + ctx.fillStyle = player_color; ctx.textBaseline = "middle"; - ctx.textAlign = "center"; - ctx.fillText(GAME_DATA.dawn.pool.pieces[i], -0.5 * radius, 0); + ctx.textAlign = "left"; + ctx.fillText(GAME_DATA.pools[+(player_identity == 1)].pieces[i], -0.6 * radius, 0); ctx.restore(); } // Draw opponent pool for(let i = 0; i < 6; ++i) { + let is_hover = INTERFACE.Ui.tile_is_hover(1, 6 + i); + let is_select = INTERFACE.Ui.tile_is_select(1, 6 + i); + let gui_x = basis_x + (radius * 15.5); let gui_y = basis_y - (8 - (2 * i)) * gui_scale; ctx.save(); ctx.translate(gui_x, gui_y); - // Draw background. - // Select indicator color or default to tile color. - if(true) { - ctx.fillStyle = INTERFACE.Color.TileDark; - } else { - - } + // Draw background if indicator is present. + if(is_select) { ctx.fillStyle = INTERFACE.Color.HintSelect; } + else { ctx.fillStyle = INTERFACE.Color.TileDark; } ctx.beginPath(); draw.hex(); ctx.fill(); // Draw border - ctx.strokeStyle = INTERFACE.Color.Dusk; - ctx.beginPath(); - draw.hex(); - ctx.stroke(); + if(is_select || is_hover || (INTERFACE_DATA.player == 2 && player_identity != (GAME_DATA.turn & 1))) { + if(is_hover) { ctx.strokeStyle = INTERFACE.Color.HintHover; } + else { ctx.strokeStyle = opponent_color; } + ctx.beginPath(); + draw.hex(); + ctx.stroke(); + } - ctx.fillStyle = INTERFACE.Color.Dusk; + // Draw image + ctx.drawImage(GAME_ASSET.Image.Piece[i][+(player_identity != 1)], -icon_radius * 0.55, -icon_radius * 0.8, icon_radius * 1.6, icon_radius * 1.6); + + // Draw count + ctx.fillStyle = opponent_color; ctx.textBaseline = "middle"; ctx.textAlign = "center"; - ctx.fillText(GAME_DATA.dusk.pool.pieces[i], -0.5 * radius, 0); + ctx.fillText(GAME_DATA.pools[+(player_identity != 1)].pieces[i], -0.5 * radius, 0); ctx.restore(); } // Draw informational text + + let handle_pos = [ + new MATH.Vec2( + basis_x + (radius * 12), + basis_y - (11 * gui_scale) + ), + new MATH.Vec2( + basis_x + (radius * 12), + basis_y + (3 * gui_scale) + ), + ]; + + // Player handles + ctx.font = Math.ceil(gui_scale / 1.3) + "px sans-serif"; + + if(INTERFACE_DATA.handles[0] !== null) { + let pos = handle_pos[(1 ^ INTERFACE_DATA.player ^ INTERFACE_DATA.rotate) & 1]; + + ctx.fillStyle = INTERFACE.Color.Dawn; + ctx.textBaseline = "middle"; + ctx.textAlign = "center"; + ctx.fillText(INTERFACE_DATA.handles[0], pos.x, pos.y); + } + + if(INTERFACE_DATA.handles[1] !== null) { + let pos = handle_pos[(INTERFACE_DATA.player ^ INTERFACE_DATA.rotate) & 1]; + + ctx.fillStyle = INTERFACE.Color.Dusk; + ctx.textBaseline = "middle"; + ctx.textAlign = "center"; + ctx.fillText(INTERFACE_DATA.handles[1], pos.x, pos.y); + } + + // Tile information ctx.font = Math.ceil(gui_scale / 2) + "px sans-serif"; - - /* - // Dawn handle - ctx.fillStyle = INTERFACE.Color.Text; - ctx.textBaseline = "top"; - ctx.textAlign = "center"; - ctx.fillText(GAME_DATA.turn, width / 2, gui_margin); + if(INTERFACE_DATA.hover !== null) { + let text = ""; + if(INTERFACE_DATA.hover.source == 0) { + text = INTERFACE.Ui.hex_to_alnum(INTERFACE_DATA.hover.hex); + let piece_id = GAME_DATA.board.tiles[INTERFACE_DATA.hover.tile].piece; + if(piece_id !== null) { + let piece_class = GAME_DATA.board.pieces[piece_id].piece; + text += " " + GAME.Const.Piece[piece_class].name; + } + } else { + text = " " + GAME.Const.Piece[INTERFACE_DATA.hover.tile % 6].name; + } - - // Dusk handle - ctx.fillStyle = INTERFACE.Color.Text; - ctx.textBaseline = "top"; - ctx.textAlign = "center"; - ctx.fillText(GAME_DATA.turn, width - gui_margin, gui_margin); - */ + ctx.fillStyle = INTERFACE.Color.Text; + ctx.textBaseline = "top"; + ctx.textAlign = "left"; + ctx.fillText(text, gui_margin, gui_margin); + } // Number of moves ctx.fillStyle = INTERFACE.Color.Text; @@ -257,7 +544,7 @@ const INTERFACE = { let descriptor = GAME.Const.Piece[piece.piece]; let moves = descriptor.moves; if(piece.promoted) { moves = descriptor.pmoves; } - if(piece.player == GAME.Const.Player.Dusk) { moves = moves.rotate(); } + if(((piece.player ^ INTERFACE_DATA.player ^ INTERFACE_DATA.rotate) & 1) != 0) { moves = moves.rotate(); } moves = moves.direction; for(let mask = BITWISE.lsb(moves); moves > 0; mask = BITWISE.lsb(moves)) { @@ -314,26 +601,85 @@ const INTERFACE = { } }, - init(player_id) { + Ui: { + tile_is_hover(source, tile) { + return INTERFACE_DATA.hover !== null + && INTERFACE_DATA.hover.source == source + && INTERFACE_DATA.hover.tile == tile; + }, + + tile_is_select(source, tile) { + return INTERFACE_DATA.select !== null + && INTERFACE_DATA.select.source == source + && INTERFACE_DATA.select.tile == tile; + }, + + pool_hex_is_valid(hex) { + return (hex.x >= 0 && hex.x < 2 && hex.y >= 0 && hex.y < 6); + }, + + hex_to_alnum(hex) { + return String.fromCharCode(65 + hex.x) + (hex.y + 1); + }, + + match_select(a, b) { + if(a !== null && b !== null) { + return ( + a.source == b.source + && a.tile == b.tile + && a.hex.x == b.hex.x + && a.hex.y == b.hex.y + ); + } + return false; + }, + }, + + init(data) { GAME.init(); INTERFACE_DATA = { + token:data.token, + canvas: document.getElementById("game"), context: null, - player_id: player_id, + player: data.mode, + rotate: 0, + + hover: null, + select: null, + + handles: [null, null], + board_state: [ ], message: null, + + Ui: { + scale: 0, + margin: 0, + offset: new MATH.Vec2(), + area: new MATH.Vec2(), + + board_width: 0, + pool_offset: 0, + }, }; + for(let i = 0; i < 61; ++i) { INTERFACE_DATA.board_state.push([0, 0]); } let canvas = INTERFACE_DATA.canvas; if(canvas !== undefined) { INTERFACE_DATA.context = canvas.getContext("2d"); canvas.addEventListener("mousemove", INTERFACE.hover); + canvas.addEventListener("mouseout", INTERFACE.unhover); canvas.addEventListener("mousedown", INTERFACE.click); window.addEventListener("resize", INTERFACE.draw); - this.draw(); + + MESSAGE_COMPOSE([ + PACK.u16(OpCode.GameState), + INTERFACE_DATA.token, + ]); } }, @@ -345,19 +691,45 @@ const INTERFACE = { message(code, data) { switch(code) { case OpCode.GameState: { - // Build game state + GAME_DATA.turn = data.turn; + if(data.dawn.length > 0) { INTERFACE_DATA.handles[0] = data.dawn; } + if(data.dusk.length > 0) { INTERFACE_DATA.handles[1] = data.dusk; } + + // Clear piece placement. + for(let i = 0; i < GAME_DATA.board.tiles.length; ++i) { + GAME_DATA.board.tiles[i].piece = null; + } + + // Update pools. + GAME_DATA.pools[0].pieces = data.pool_dawn; + GAME_DATA.pools[1].pieces = data.pool_dusk; + + // Replace pieces list. + for(let i = 0; i < GAME_DATA.board.pieces.length; ++i) { + GAME_DATA.board.pieces[i] = data.pieces[i]; + if(data.pieces[i] !== null) { + GAME_DATA.board.tiles[data.pieces[i].tile].piece = i; + } + } + + GAME_DATA.update_board(); INTERFACE.draw(); } break; case OpCode.GamePlay: { - // Apply play to board - GAME_DATA.turn += 1; - - INTERFACE.draw(); + if(data.status == Status.Ok) { + GAME_DATA.process(data.move); + INTERFACE.draw(); + } } break; } }, + + rotate() { + INTERFACE_DATA.rotate = +(!INTERFACE_DATA.rotate); + INTERFACE.draw(); + } }; INTERFACE.Radius = 2.0 / Math.sqrt(3.0); @@ -378,3 +750,6 @@ INTERFACE.HexVertex = [ // top-left face new MATH.Vec2(-INTERFACE.Radius, 0), ]; + +INTERFACE.BoardWidth = INTERFACE.Radius * 14; +INTERFACE.PoolOffset = INTERFACE.BoardWidth; diff --git a/www/js/scene.js b/www/js/scene.js index 402b9b1..2af47ab 100644 --- a/www/js/scene.js +++ b/www/js/scene.js @@ -2,7 +2,7 @@ const SCENES = { Init:{ load() { LOAD_STACK(SCENES.Offline); - CONTEXT.Scene = SCENES.Online; + CONTEXT.Scene = SCENES.Browse; RECONNECT(); return true; }, @@ -79,7 +79,7 @@ const SCENES = { switch(data.status) { case Status.Ok: { CONTEXT.Auth = data; - LOAD(SCENES.Online); + LOAD(SCENES.Browse); } break; default: { submit.removeAttribute("disabled"); @@ -158,7 +158,7 @@ const SCENES = { switch(data.status) { case Status.Ok: { CONTEXT.Auth = data; - LOAD(SCENES.Online); + LOAD(SCENES.Browse); } break; case Status.Error: { submit.removeAttribute("disabled"); @@ -172,15 +172,15 @@ const SCENES = { }, }, - Online:{ + Browse:{ load() { - UI.mainmenu(); - - CONTEXT.data = { + CONTEXT.Data = { page:0, records:[], }; + UI.mainmenu(); + let left_buttons = [ ]; if(CONTEXT.Auth !== null) { left_buttons.push(UI.button("Start", () => { @@ -210,25 +210,40 @@ const SCENES = { MESSAGE_SESSION_LIST(0, 0, false, false); }, message(code, data) { - if(code == OpCode.SessionList) { - let table = document.getElementById("content"); - UI.clear(table); + switch(code) { + case OpCode.SessionList: { + let table = document.getElementById("content"); + UI.clear(table); - if(data !== null) { - table.appendChild(UI.session_table(data.records)); - } + if(data !== null) { + table.appendChild(UI.session_table(data.records)); + } + } break; + case OpCode.SessionJoin: { + if(data.status == Status.Ok) { + LOAD(SCENES.Game, data); + } + } break; } - } + }, }, Continue:{ load() { if(CONTEXT.Auth === null) return false; + + CONTEXT.Data = { + page:0, + records:[], + }; + UI.mainmenu(); let left_buttons = [ ]; if(CONTEXT.Auth !== null) { - left_buttons.push(UI.button("Start", null)); + left_buttons.push(UI.button("Start", () => { + MESSAGE_SESSION_START(); + })); } UI.mainnav( @@ -237,41 +252,56 @@ const SCENES = { UI.div([UI.text("0 - 0 of 0")]), UI.button("◀", null), UI.button("▶", null), + UI.button("Refresh", null), ] ); let table = document.createElement("table"); table.setAttribute("id", "content"); + table.setAttribute("class", "list"); MAIN.appendChild(table); - MAIN.setAttribute("class", "list"); - SCENE.refresh(); return true; }, refresh() { - + MESSAGE_SESSION_LIST(0, 0, true, false); }, message(code, data) { - if(code == OpCode.SessionList) { - let table = document.getElementById("content"); - UI.clear(table); + switch(code) { + case OpCode.SessionList: { + let table = document.getElementById("content"); + UI.clear(table); - if(data !== null) { - table.appendChild(UI.session_table(data.records)); - } + if(data !== null) { + table.appendChild(UI.session_table(data.records)); + } + } break; + case OpCode.SessionJoin: { + if(data.status == Status.Ok) { + LOAD(SCENES.Game, data); + } + } break; } - } + }, }, Join:{ load() { if(CONTEXT.Auth === null) return false; + + CONTEXT.Data = { + page:0, + records:[], + }; + UI.mainmenu(); let left_buttons = [ ]; if(CONTEXT.Auth !== null) { - left_buttons.push(UI.button("Start", null)); + left_buttons.push(UI.button("Start", () => { + MESSAGE_SESSION_START(); + })); } UI.mainnav( @@ -280,40 +310,56 @@ const SCENES = { UI.div([UI.text("0 - 0 of 0")]), UI.button("◀", null), UI.button("▶", null), + UI.button("Refresh", null), ] ); let table = document.createElement("table"); table.setAttribute("id", "content"); + table.setAttribute("class", "list"); MAIN.appendChild(table); - MAIN.setAttribute("class", "list"); - SCENE.refresh(); return true; }, refresh() { - + MESSAGE_SESSION_LIST(0, 1, false, false); }, message(code, data) { - if(code == OpCode.SessionList) { - let table = document.getElementById("content"); - UI.clear(table); + switch(code) { + case OpCode.SessionList: { + let table = document.getElementById("content"); + UI.clear(table); - if(data !== null) { - table.appendChild(UI.session_table(data.records)); - } + if(data !== null) { + table.appendChild(UI.session_table(data.records)); + } + } break; + case OpCode.SessionJoin: { + if(data.status == Status.Ok) { + LOAD(SCENES.Game, data); + } + } break; } - } + }, }, Live:{ load() { + if(CONTEXT.Auth === null) return false; + + CONTEXT.Data = { + page:0, + records:[], + }; + UI.mainmenu(); let left_buttons = [ ]; if(CONTEXT.Auth !== null) { - left_buttons.push(UI.button("Start", null)); + left_buttons.push(UI.button("Start", () => { + MESSAGE_SESSION_START(); + })); } UI.mainnav( @@ -322,35 +368,47 @@ const SCENES = { UI.div([UI.text("0 - 0 of 0")]), UI.button("◀", null), UI.button("▶", null), + UI.button("Refresh", null), ] ); let table = document.createElement("table"); table.setAttribute("id", "content"); + table.setAttribute("class", "list"); MAIN.appendChild(table); - MAIN.setAttribute("class", "list"); - SCENE.refresh(); return true; }, refresh() { - + MESSAGE_SESSION_LIST(0, 2, false, true); }, message(code, data) { - if(code == OpCode.SessionList) { - let table = document.getElementById("content"); - UI.clear(table); + switch(code) { + case OpCode.SessionList: { + let table = document.getElementById("content"); + UI.clear(table); - if(data !== null) { - table.appendChild(UI.session_table(data.records)); - } + if(data !== null) { + table.appendChild(UI.session_table(data.records)); + } + } break; + case OpCode.SessionJoin: { + if(data.status == Status.Ok) { + LOAD(SCENES.Game, data); + } + } break; } - } + }, }, History:{ load() { + CONTEXT.Data = { + page:0, + records:[], + }; + UI.mainmenu(); UI.mainnav( @@ -415,19 +473,20 @@ const SCENES = { }, Game:{ - load() { + load(data) { + let buttons_bottom = [ ]; + if(data.mode != 2) { buttons_bottom.push(UI.button("Retire", () => { })); } + buttons_bottom.push(UI.button("Back", () => { LOAD(SCENES.Browse) })); + UI.nav([ - UI.button("Rotate", () => { }), - ], [ - UI.button("Back", () => { LOAD(SCENES.Online) }), - UI.button("Retire", () => { }), - ]); + UI.button("Rotate", () => { INTERFACE.rotate(); }), + ], buttons_bottom); let canvas = document.createElement("canvas"); canvas.setAttribute("id", "game"); MAIN.appendChild(canvas); - INTERFACE.init(); + INTERFACE.init(data); return true; }, @@ -438,28 +497,39 @@ const SCENES = { ]); }, message(code, data) { - if(code == OpCode.GameState || code == OpCode.GamePlay) { - INTERFACE.message(code, data); + switch(code) { + case OpCode.GameState: { + if(data.status == Status.Ok) { + INTERFACE.message(code, data); + } else { + LOAD(SCENES.Browse); + } + } break; + case OpCode.GamePlay: { + INTERFACE.message(code, data); + } break; } }, }, }; -function LOAD(scene) { +function LOAD(scene, data=null) { UNLOAD(); UI.rebuild(); SCENE = scene; CONTEXT.Scene = SCENE; - if(!SCENE.load()) { LOAD(SCENES.Online); } + CONTEXT.Data = null; + if(!SCENE.load(data)) { LOAD(SCENES.Browse); } } function UNLOAD() { if(SCENE !== null && SCENE.unload !== undefined) { SCENE.unload(); } } -function LOAD_STACK(scene) { +function LOAD_STACK(scene, data=null) { UNLOAD(); UI.rebuild(); SCENE = scene; - if(!SCENE.load()) { LOAD(SCENES.Online); } + CONTEXT.Data = null; + if(!SCENE.load(data)) { LOAD(SCENES.Browse); } } diff --git a/www/js/system.js b/www/js/system.js index bc739ef..d4ae12f 100644 --- a/www/js/system.js +++ b/www/js/system.js @@ -39,8 +39,6 @@ function RESUME() { } function MESSAGE(event) { - console.log("Message received."); - if(SCENE.message !== undefined) { let bytes = new Uint8Array(event.data); let code = 0; @@ -157,9 +155,9 @@ function MESSAGE(event) { index = result.index; record.turn = result.data; - if(index <= bytes.length + 5) { - let move = new Uint8Array(5); - for(let i = 0; i < 5; ++i) { + if(index <= bytes.length + 3) { + let move = new Uint8Array(3); + for(let i = 0; i < 3; ++i) { move[i] = bytes[index]; index += 1; } @@ -179,29 +177,131 @@ function MESSAGE(event) { case OpCode.SessionCreate: case OpCode.SessionJoin: { + console.log("RECV SessionCreate/Join"); + if(bytes.length - index == 11) { data = { + status:0, token:new Uint8Array(8), mode:2, }; - let result = UNPACK.u16(data, index); + result = UNPACK.u16(bytes, index); index = result.index; - let status = result.data; + data.status = result.data; - result = UNPACK.u8(data, index); + result = UNPACK.u8(bytes, index); index = result.index; data.mode = result.data; - for(let i = 0; i < 8; ++i) { data.token[i] = data[index++]; } - - if(status == Status.Ok) { - LOAD(SCENES.Game); - } + for(let i = 0; i < 8; ++i) { data.token[i] = bytes[index++]; } } } break; + case OpCode.GameState: { + console.log("RECV GameState"); + + //if(bytes.length - index >= 22) { + data = { + status:0, + turn:0, + dawn:"", + dusk:"", + pool_dawn:[ ], + pool_dusk:[ ], + pieces:[ ], + }; + + // Status + result = UNPACK.u16(bytes, index); + index = result.index; + data.status = result.data; + + // Turn + result = UNPACK.u16(bytes, index); + index = result.index; + data.turn = 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; + + // Dawn pool + result = UNPACK.u16(bytes, index); + index = result.index; + let dawn_pool_bits = result.data; + + data.pool_dawn.push(dawn_pool_bits & 0x1F); + data.pool_dawn.push((dawn_pool_bits >> 5) & 0x3); + data.pool_dawn.push((dawn_pool_bits >> 7) & 0x3); + data.pool_dawn.push((dawn_pool_bits >> 9) & 0x3); + data.pool_dawn.push((dawn_pool_bits >> 11) & 0x3); + data.pool_dawn.push((dawn_pool_bits >> 13) & 0x1); + + // Dusk pool + result = UNPACK.u16(bytes, index); + index = result.index; + let dusk_pool_bits = result.data; + + data.pool_dusk.push(dusk_pool_bits & 0x1f); + data.pool_dusk.push((dusk_pool_bits >> 5) & 0x3); + data.pool_dusk.push((dusk_pool_bits >> 7) & 0x3); + data.pool_dusk.push((dusk_pool_bits >> 9) & 0x3); + data.pool_dusk.push((dusk_pool_bits >> 11) & 0x3); + data.pool_dusk.push((dusk_pool_bits >> 13) & 0x1); + + // Pieces + for(let i = 0; i < 38; ++i) { + result = UNPACK.u16(bytes, index); + index = result.index; + let piece_data = result.data; + + if((piece_data & 1) != 0) { + let piece = new GAME.Piece((piece_data >> 1) & 0x7, (piece_data >> 5) & 1); + piece.promoted = ((piece_data >> 4) & 1) != 0; + piece.tile = (piece_data >> 6) & 0x3F; + + data.pieces.push(piece); + } else { + data.pieces.push(null); + } + } + //} + } break; + + case OpCode.GamePlay: { + console.log("RECV GamePlay"); + + data = { + status:0, + move:new GAME.Play(0, 0, 0), + }; + + // Status + result = UNPACK.u16(bytes, index); + index = result.index; + data.status = result.data; + + // Turn + result = UNPACK.u16(bytes, index); + index = result.index; + data.turn = result.data; + + // Play description + result = UNPACK.u16(bytes, index); + index = result.index; + data.move.source = result.data & 1; + data.move.from = (result.data >> 1) & 0x3F; + data.move.to = (result.data >> 7) & 0x3F; + } break; + default: + console.log("RECV Undefined " + code); return; } diff --git a/www/js/ui.js b/www/js/ui.js index 50bacf7..0b3aba6 100644 --- a/www/js/ui.js +++ b/www/js/ui.js @@ -114,7 +114,7 @@ const UI = { let top = [ ]; let bottom = [ ]; - top.push(UI.button("Online", () => { LOAD(SCENES.Online); })); + top.push(UI.button("Browse", () => { LOAD(SCENES.Browse); })); if(CONTEXT.Auth !== null) { top.push(UI.button("Continue", () => { LOAD(SCENES.Continue); })); top.push(UI.button("Join", () => { LOAD(SCENES.Join); })); diff --git a/www/js/util.js b/www/js/util.js index 44734a4..88e0a94 100644 --- a/www/js/util.js +++ b/www/js/util.js @@ -37,8 +37,8 @@ const UNPACK = { if(index + 4 <= data.length) { result = (data[index] << 24) + (data[index + 1] << 16) - + (data[index + 1] << 8) - + data[index + 1]; + + (data[index + 2] << 8) + + data[index + 3]; index += 4; } return { data: result, index: index }; @@ -59,7 +59,7 @@ const UNPACK = { index += length; result_str = dec.decode(bytes); - } + } else { console.log("INV DATA LEN"); } return { data: result_str, index: index }; }, @@ -112,10 +112,12 @@ const UNPACK = { const BITWISE = { lsb(x) { + // Least significant bit return x & -x; }, ffs(x) { + // Find first set return 31 - Math.clz32(x & -x); }, @@ -151,6 +153,20 @@ const MATH = { this.x = x; this.y = y; } + + add(v) { + this.x += v.x; + this.y += v.y; + } + + mul(v) { + this.x *= v; + this.y *= v; + } + + copy() { + return new MATH.Vec2(this.x, this.y); + } }, sign(a) { @@ -160,6 +176,10 @@ const MATH = { mod(a, b) { return ((a % b) + b) % b }, + + sign_branch(v) { + return (v * 2) - 1; + }, }; const COLOR = { @@ -171,3 +191,34 @@ const COLOR = { return "rgba(" + ur + "," + ug + "," + ub + "," + ua + ")"; }, } + +const HEX = { + hex_to_tile(hex) { + let a = ((hex.x + 4) * (hex.x + 5) / 2) - 10; + let b = (hex.x > 4) * ((hex.x - 4) + ((hex.x - 5) * (hex.x - 4))); + return a - b + hex.y; + }, + + tile_to_hex(tile) { + const ROWS = [ 0, 5, 11, 18, 26, 35, 43, 50, 56, 61 ]; + let column = 0; + while(tile >= ROWS[column + 1]) { column += 1; } + let row = tile - ROWS[column] + ((column > 4) * (column - 4)); + return new MATH.Vec2(column, row); + }, + + is_valid(hex) { + const COLUMNS = [ + new MATH.Vec2(0, 4), + new MATH.Vec2(0, 5), + new MATH.Vec2(0, 6), + new MATH.Vec2(0, 7), + new MATH.Vec2(0, 8), + new MATH.Vec2(1, 8), + new MATH.Vec2(2, 8), + new MATH.Vec2(3, 8), + new MATH.Vec2(4, 8), + ]; + return (hex.x >= 0 && hex.x < 9 && hex.y >= COLUMNS[hex.x].x && hex.y <= COLUMNS[hex.x].y); + }, +};