Implement game mechanics.

This commit is contained in:
yukirij 2024-08-15 22:38:05 -07:00
parent e3fd0c703a
commit adf3cdef10
34 changed files with 2070 additions and 776 deletions

View File

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

View File

@ -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<u8>,
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<Piece>; 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
}
}

12
game/src/consts.rs Normal file
View File

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

View File

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

View File

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

View File

@ -1,5 +1,6 @@
#![allow(dead_code)]
pub mod consts;
pub mod util;
pub mod piece;
pub mod board;

View File

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

47
game/src/util/hex.rs Normal file
View File

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

View File

@ -1 +1,2 @@
mod binary; pub use binary::*;
mod hex; pub use hex::*;

View File

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

View File

@ -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<RwLock<SplitSink<WebSocketStream<TokioIo<Upgraded>>, Message>>>;
@ -13,4 +16,5 @@ pub struct Connection {
pub bus:u32,
pub stream:StreamType,
pub auth:Option<AuthToken>,
pub session:Option<SessionToken>,
}

View File

@ -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<u8>) -> Vec<u8>
{
[
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();
}
_ => { }
}
}
}
}
}

View File

@ -3,9 +3,9 @@ use game::Game;
pub type SessionToken = [u8; 8];
pub type SessionSecret = [u8; 8];
pub struct Viewer {
pub connection:Option<u32>,
pub struct Player {
pub user:Option<u32>,
pub connections:Vec<u32>,
}
pub struct Session {
@ -15,10 +15,36 @@ pub struct Session {
pub game:Game,
pub p_dawn:Viewer,
pub p_dusk:Viewer,
pub viewers:Vec<Viewer>,
pub p_dawn:Player,
pub p_dusk:Player,
pub connections:Vec<u32>,
pub time:u64,
pub chain_id:usize,
}
impl Session {
pub fn get_connections(&self) -> Vec<u32>
{
[
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;
}
}
}
}

View File

@ -28,87 +28,37 @@ async fn service_http(mut request:hyper::Request<hyper::body::Incoming>, 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() {

View File

@ -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<u8>) -> Vec<u8>
{
[
pack_u16(code),
data,
].concat()
}
pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
{
use futures::SinkExt;
use protocol::*;
use ring::rand::{SecureRandom, SystemRandom};
@ -34,7 +23,10 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
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<protocol::QRPacket>)
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<protocol::QRPacket>)
}
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<protocol::QRPacket>)
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<protocol::QRPacket>)
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<protocol::QRPacket>)
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<protocol::QRPacket>)
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<protocol::QRPacket>)
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<protocol::QRPacket>)
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<protocol::QRPacket>)
}
// 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::<QRPacket>::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;
}
}

View File

@ -124,6 +124,33 @@ pub async fn handle_ws(ws:WebSocketStream<TokioIo<Upgraded>>, 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

View File

@ -31,6 +31,15 @@ pub enum QRPacketData {
QSessionJoin(PacketSessionJoin),
RSessionJoin(PacketSessionJoinResponse),
QSessionRetire(PacketSessionRetire),
QSessionLeave,
QGameState(PacketGameState),
RGameState(PacketGameStateResponse),
QGamePlay(PacketGamePlay),
}
#[derive(Clone)]

View File

@ -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<u8>
{
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()
}
}

View File

@ -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<u8>
{
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()
}
}

View File

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

View File

@ -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<u8>, index:&mut usize) -> Result<Self::Data, ()>
{
let mut result = Self::new();
if data.len() - *index == 8 {
for i in 0..8 { result.token[i] = data[*index]; *index += 1; }
Ok(result)
} else {
Err(())
}
}
}

View File

@ -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<u8>,
}
struct WebCacheData {
html:String,
css:String,
js:String,
favicon:Vec<u8>,
dawn_omen:Vec<u8>,
dawn_dragon:Vec<u8>,
dawn_castle:Vec<u8>,
dawn_tower:Vec<u8>,
dawn_lance:Vec<u8>,
dawn_knight:Vec<u8>,
dawn_militia:Vec<u8>,
dusk_omen:Vec<u8>,
dusk_dragon:Vec<u8>,
dusk_castle:Vec<u8>,
dusk_tower:Vec<u8>,
dusk_lance:Vec<u8>,
dusk_knight:Vec<u8>,
dusk_militia:Vec<u8>,
objects:Trie<CacheData>,
}
#[derive(Clone)]
@ -30,230 +24,90 @@ pub struct WebCache {
data:Arc<RwLock<WebCacheData>>,
}
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::<u8>::new();
if let Ok(mut file) = File::open("www/favicon.png") {
file.read_to_end(&mut favicon).ok();
}
// Assets
let mut dawn_omen = Vec::<u8>::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::<u8>::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::<u8>::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::<u8>::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::<u8>::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::<u8>::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::<u8>::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::<u8>::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::<u8>::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::<u8>::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::<u8>::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::<u8>::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::<u8>::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::<u8>::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<u8>
{
match self.data.read() {
Ok(reader) => reader.favicon.clone(),
Err(_) => Vec::new(),
}
}
pub fn asset(&self, player:u8, id:u8) -> Vec<u8>
pub fn fetch(&self, object:&str) -> Option<CacheData>
{
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<u8>)
{
match self.data.write() {
Ok(mut writer) => {
writer.objects.set(object.as_bytes(), CacheData {
mime:mime.to_string(),
data,
});
},
Err(_) => { },
}
}
pub fn cache_file<P :AsRef<Path>>(&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<P :AsRef<Path>>(&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::<Vec<Vec<u8>>>().concat();
writer.objects.set(object.as_bytes(), CacheData {
mime:mime.to_string(),
data:data,
});
Ok(())
},
Err(_) => Err(()),
}
}
}

View File

@ -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<Session,()>
{
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,
})

View File

@ -32,6 +32,24 @@ pub fn unpack_u16(data:&[u8], index:&mut usize) -> u16
result
}
pub fn pack_u24(value:u32) -> Vec<u8>
{
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<u8>
{
vec![

View File

Before

Width:  |  Height:  |  Size: 3.2 KiB

After

Width:  |  Height:  |  Size: 3.2 KiB

View File

@ -4,6 +4,7 @@ let SCENE = null;
let CONNECTED = false;
let SOCKET = null;
let CONTEXT = {
Scene: null,
Auth: null,

View File

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

View File

@ -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") ],
],
};

View File

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

View File

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

View File

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

View File

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

View File

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