Add game interface.

This commit is contained in:
yukirij 2024-08-11 21:10:05 -07:00
parent ee12d74c67
commit 7ba9ce7ba0
16 changed files with 1179 additions and 289 deletions

View File

@ -20,7 +20,6 @@ pub struct App {
pub connections:Pool<Connection>,
pub users:Pool<User>,
pub user_next:u32,
pub user_id:Sparse<usize>,
pub user_handle:Trie<u32>,
pub salts:Sparse<[u8; 16]>,
@ -48,25 +47,58 @@ impl App {
let handle_count = filesystem.handle_count()?;
for id in 0..handle_count {
let (handle, user_id) = filesystem.handle_fetch(id as u32).unwrap();
println!("got: {} = {}", handle, user_id);
user_handle.set(handle.as_bytes(), user_id);
}
// Load users
println!("Load users..");
let mut users = Pool::new();
let mut user_id = Sparse::new();
let user_count = filesystem.user_count()?;
for id in 0..user_count {
let user = filesystem.user_fetch(id as u32).unwrap();
let user_local_id = users.add(user);
user_id.set(user_local_id as isize, id);
}
// Load sessions
println!("Load sessions..");
let mut sessions = Trie::new();
let mut times = Vec::<(u64, SessionToken)>::new();
let session_count = filesystem.session_count()?;
for id in 0..session_count {
let session = filesystem.session_fetch(id as u32).unwrap();
times.push((session.time, session.token.clone()));
sessions.set(&session.token.clone(), session);
}
// Organize sessions by most recent
let mut session_time = Chain::new();
times.sort_by(|(a, _), (b, _)| {
if a < b { std::cmp::Ordering::Greater } else { std::cmp::Ordering::Less }
});
for (_, token) in times {
let id = session_time.add(token);
sessions.get_mut(&token).unwrap().chain_id = id;
}
println!("Done.");
Ok(Self {
filesystem:filesystem,
connections:Pool::new(),
users:Pool::new(),
user_next:0,
user_id:Sparse::new(),
user_handle:Trie::new(),
users,
user_id,
user_handle,
salts,
auths:Trie::new(),
sessions:Trie::new(),
sessions,
session_time:Chain::new(),
session_time,
})
} else {
Err(())

View File

@ -9,7 +9,8 @@ pub struct Viewer {
}
pub struct Session {
pub key:SessionToken,
pub id:u32,
pub token:SessionToken,
pub secret:SessionSecret,
pub game:Game,
@ -18,5 +19,6 @@ pub struct Session {
pub p_dusk:Viewer,
pub viewers:Vec<Viewer>,
pub time:u64,
pub chain_id:usize,
}

View File

@ -1,5 +1,6 @@
pub struct User {
pub id:u32,
pub handle_id:u32,
pub handle:String,
pub secret:Vec<u8>,
pub na_key:u32,

View File

@ -94,23 +94,28 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
let salt_id = app.filesystem.salt_store(salt).unwrap();
app.salts.set(salt_id as isize, salt);
if let Ok(hash) = argon2::hash_raw(&request.secret, &salt, &argon_config) {
let user_id = app.user_next;
app.user_next += 1;
// Create user entry
let user_pos = app.users.add(User {
id:user_id,
handle:request.handle.clone(),
secret:hash,
na_key:salt_id,
});
if let Ok(secret) = argon2::hash_raw(&request.secret, &salt, &argon_config) {
let user_id = app.filesystem.user_count().unwrap() as u32;
// Register user pool id and handle
app.filesystem.handle_store(&request.handle, user_id).ok();
app.user_id.set(user_id as isize, user_pos);
let handle_id = app.filesystem.handle_store(&request.handle, user_id).unwrap();
app.user_handle.set(request.handle.as_bytes(), user_id);
let user_data = User {
id:user_id,
handle_id,
handle:request.handle.clone(),
secret,
na_key:salt_id,
};
app.filesystem.user_store(&user_data).ok();
// Create user entry
let user_pos = app.users.add(user_data);
app.user_id.set(user_id as isize, user_pos);
println!("Registered user '{}' @ {} with id {}", request.handle, user_pos, user_id);
// Generate authentication token and secret
@ -296,7 +301,7 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
} else { String::new() };
response.records.push(PacketSessionListResponseRecord {
token:session.key,
token:session.token,
handles:[
dawn_handle,
dusk_handle,
@ -337,9 +342,11 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
rng.fill(&mut secret).ok();
let chain_id = app.session_time.add(token);
app.sessions.set(&token, Session {
key:token,
secret:secret,
let mut session = Session {
id:0,
token,
secret,
game:game::Game::new(),
p_dawn:Viewer {
connection:None,//Some(qr.id),
@ -350,8 +357,13 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
user:None,
},
viewers:Vec::new(),
chain_id:chain_id,
});
time:std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() as u64,
chain_id,
};
session.id = app.filesystem.session_store(&session).unwrap();
app.sessions.set(&token, session);
app.session_time.set(chain_id, token);
// Set player to Dawn.
@ -397,6 +409,8 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
false
} {
println!("Add user to session.");
app.filesystem.session_update(session.id, session).ok();
response.status = STATUS_OK;
}
} else {
@ -418,6 +432,14 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
Some(QRPacket::new(qr.id, QRPacketData::RSessionJoin(response)))
}
// SessionRetire
// SessionLeave
// GameState
// GamePlay
_ => { Some(QRPacket::new(0, QRPacketData::None)) }
}
}

View File

@ -0,0 +1,77 @@
use crate::util::pack::{pack_u16, unpack_u16};
use super::Packet;
#[derive(Clone)]
pub struct PacketGamePlay {
pub handle:String,
pub secret:String,
}
impl PacketGamePlay {
pub fn new() -> Self
{
Self {
handle:String::new(),
secret:String::new(),
}
}
}
impl Packet for PacketGamePlay {
type Data = Self;
fn decode(data:&Vec<u8>, index:&mut usize) -> Result<Self::Data, ()>
{
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 {
match String::from_utf8(data[*index..*index+length].to_vec()) {
Ok(text) => {
*index += length;
result.secret = text;
return Ok(result);
}
Err(_) => { }
}
}
}
Err(_) => { }
}
}
Err(())
}
}
#[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>
{
[
pack_u16(self.status),
].concat()
}
}

View File

@ -0,0 +1,55 @@
use crate::{
app::session::SessionToken,
util::pack::{pack_u16, unpack_u16},
};
use super::Packet;
#[derive(Clone)]
pub struct PacketGameState {
pub token:SessionToken,
}
impl PacketGameState {
pub fn new() -> Self
{
Self {
token:SessionToken::default(),
}
}
}
impl Packet for PacketGameState {
type Data = Self;
fn decode(data:&Vec<u8>, index:&mut usize) -> Result<Self::Data, ()>
{
let mut result = Self::new();
Err(())
}
}
#[derive(Clone)]
pub struct PacketGameStateResponse {
pub status:u16,
}
impl PacketGameStateResponse {
pub fn new() -> Self
{
Self {
status:0,
}
}
}
impl Packet for PacketGameStateResponse {
type Data = Self;
fn encode(&self) -> Vec<u8>
{
[
pack_u16(self.status),
].concat()
}
}

View File

@ -7,6 +7,11 @@ mod resume; pub use resume::*;
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::*;
mod prelude {
pub trait Packet {

View File

@ -2,23 +2,31 @@ use std::{
fs::{self, File}, io::{Read, Seek, SeekFrom, Write}, path::Path
};
use game::Game;
use crate::{
app::session::Session,
app::user::User,
util::pack::{pack_u32, unpack_u32}
app::{
session::{self, Session, SessionToken, SessionSecret},
user::User,
},
util::pack::*,
};
const HANDLE_BUCKET_MASK :u32 = 0xFF;
const HANDLE_BUCKET_SIZE :u32 = HANDLE_BUCKET_MASK + 1;
const GENERIC_CONFIG :&str = "c.bin";
const GENERIC_INDEX :&str = "i.bin";
const GENERIC_HISTORY :&str = "h.bin";
const DIR_DATA :&str = "data";
const DIR_HANDLE :&str = const_format::formatcp!("{}/h", DIR_DATA);
const DIR_SESSION :&str = const_format::formatcp!("{}/s", DIR_DATA);
const DIR_USER :&str = const_format::formatcp!("{}/u", DIR_DATA);
const INDEX_HANDLE :&str = const_format::formatcp!("{}/i.bin", DIR_HANDLE);
const INDEX_SESSION :&str = const_format::formatcp!("{}/i.bin", DIR_SESSION);
const INDEX_USER :&str = const_format::formatcp!("{}/i.bin", DIR_USER);
const INDEX_HANDLE :&str = const_format::formatcp!("{d}/{f}", d= DIR_HANDLE, f= GENERIC_INDEX);
const INDEX_SESSION :&str = const_format::formatcp!("{d}/{f}", d= DIR_SESSION, f= GENERIC_INDEX);
const INDEX_USER :&str = const_format::formatcp!("{d}/{f}", d= DIR_USER, f= GENERIC_INDEX);
pub const FILE_SALT :&str = const_format::formatcp!("{}/x.bin", DIR_DATA);
@ -39,7 +47,7 @@ impl FileSystem {
// Initialize filesystem if does not exist.
//
// Note: does not currently check for corruption.
// Notice: does not currently check for corruption.
//
if !Path::new(DIR_DATA).exists() {
fs::create_dir(DIR_DATA)?;
@ -82,17 +90,219 @@ impl FileSystem {
}
pub fn session_store(&mut self, _session:&Session) -> Result<(),()>
pub fn session_store(&mut self, session:&Session) -> Result<u32,()>
{
let size = self.session_count()? as u32;
// Update size record
self.index_session.seek(SeekFrom::Start(0)).map_err(|_| ())?;
self.index_session.write(&pack_u32(size + 1)).map_err(|_| ())?;
match self.session_update(size, session) {
Ok(_) => {
let bucket_index = size & !HANDLE_BUCKET_MASK;
let dir_index = size & HANDLE_BUCKET_MASK;
let bucket_path = Path::new(DIR_SESSION)
.join(format!("{:08x}", bucket_index))
.join(format!("{:08x}", dir_index));
fs::write(bucket_path.join(GENERIC_HISTORY), Vec::<u8>::new()).map_err(|_| ())?;
Ok(size)
}
Err(_) => Err(()),
}
}
pub fn session_update(&mut self, id:u32, session:&Session) -> Result<(),()>
{
let bucket_index = id & !HANDLE_BUCKET_MASK;
let dir_index = id & HANDLE_BUCKET_MASK;
// Create bucket file if not exists
let bucket_path = Path::new(DIR_SESSION)
.join(format!("{:08x}", bucket_index))
.join(format!("{:08x}", dir_index));
if !bucket_path.exists() {
fs::create_dir_all(bucket_path.clone()).map_err(|_| ())?;
}
// Open bucket file for record
if let Ok(mut file) = File::options().write(true).create(true).open(bucket_path.join(GENERIC_CONFIG)) {
// Write session information
file.write(&session.token).map_err(|_| ())?;
file.write(&session.secret).map_err(|_| ())?;
file.write(&pack_u8(session.p_dawn.user.is_some() as u8)).map_err(|_| ())?;
file.write(&pack_u32(session.p_dawn.user.unwrap_or(0))).map_err(|_| ())?;
file.write(&pack_u8(session.p_dusk.user.is_some() as u8)).map_err(|_| ())?;
file.write(&pack_u32(session.p_dusk.user.unwrap_or(0))).map_err(|_| ())?;
file.write(&pack_u64(session.time)).map_err(|_| ())?;
Ok(())
} else { Err(()) }
}
pub fn session_fetch(&mut self, id:u32) -> Result<Session,()>
{
let bucket_index = id & !HANDLE_BUCKET_MASK;
let dir_index = id & HANDLE_BUCKET_MASK;
// Create bucket file if not exists
let bucket_path = Path::new(DIR_SESSION)
.join(format!("{:08x}", bucket_index))
.join(format!("{:08x}", dir_index));
if !bucket_path.exists() {
fs::create_dir_all(bucket_path.clone()).map_err(|_| ())?;
}
// Open bucket file for record
if let Ok(mut file) = File::options().read(true).open(bucket_path.join(GENERIC_CONFIG)) {
// Extract session information
let mut buffer_u8 = [0u8; 1];
let mut buffer_u32 = [0u8; 4];
let mut buffer_u64 = [0u8; 8];
let mut token = SessionToken::default();
let mut secret = SessionSecret::default();
file.read_exact(&mut token).map_err(|_| ())?;
file.read_exact(&mut secret).map_err(|_| ())?;
file.read_exact(&mut buffer_u8).map_err(|_| ())?;
file.read_exact(&mut buffer_u32).map_err(|_| ())?;
let dawn = if unpack_u8(&buffer_u8, &mut 0) != 0 {
Some(unpack_u32(&buffer_u32, &mut 0))
} else { None };
file.read_exact(&mut buffer_u8).map_err(|_| ())?;
file.read_exact(&mut buffer_u32).map_err(|_| ())?;
let dusk = if unpack_u8(&buffer_u8, &mut 0) != 0 {
Some(unpack_u32(&buffer_u32, &mut 0))
} else { None };
file.read_exact(&mut buffer_u64).map_err(|_| ())?;
let time = unpack_u64(&buffer_u64, &mut 0);
Ok(Session {
id,
token,
secret,
game:Game::new(),
p_dawn:session::Viewer {
connection:None,
user:dawn,
},
p_dusk:session::Viewer {
connection:None,
user:dusk,
},
viewers:Vec::new(),
time,
chain_id:0,
})
} else { Err(()) }
}
pub fn session_history_push(&mut self, _id:u32, _history:()) -> Result<(),()>
{
Err(())
}
pub fn user_store(&mut self, _user:&User) -> Result<(),()>
pub fn session_history_fetch(&mut self, _id:u32) -> Result<Vec<()>,()>
{
Err(())
}
pub fn session_count(&mut self) -> Result<usize, ()>
// Get number of salts in store.
//
{
Self::get_header_size(&mut self.index_session)
}
pub fn user_store(&mut self, user:&User) -> Result<u32,()>
{
let size = self.user_count()? as u32;
let bucket_index = size & !HANDLE_BUCKET_MASK;
let file_index = size & HANDLE_BUCKET_MASK;
// Update size record
self.index_user.seek(SeekFrom::Start(0)).map_err(|_| ())?;
self.index_user.write(&pack_u32(size + 1)).map_err(|_| ())?;
// Create bucket file if not exists
let bucket_path = Path::new(DIR_USER).join(format!("{:08x}", bucket_index));
if !bucket_path.exists() {
fs::create_dir(bucket_path.clone()).map_err(|_| ())?;
}
// Open bucket file for record
let file_path = bucket_path.join(format!("{:08x}.bin", file_index));
if let Ok(mut file) = File::options().write(true).create(true).open(file_path) {
// Write user information
file.write(&pack_u32(user.handle_id)).map_err(|_| ())?;
file.write(&pack_u32(user.na_key)).map_err(|_| ())?;
file.write(&pack_u16(user.secret.len() as u16)).map_err(|_| ())?;
file.write(&user.secret).map_err(|_| ())?;
Ok(size)
} else { Err(()) }
}
pub fn user_fetch(&mut self, id:u32) -> Result<User,()>
// Retrieve a salt from store.
//
{
let bucket_index = id & !HANDLE_BUCKET_MASK;
let file_index = id & HANDLE_BUCKET_MASK;
// Open bucket file for record
let file_path = Path::new(DIR_USER)
.join(format!("{:08x}", bucket_index))
.join(format!("{:08x}.bin", file_index));
if let Ok(mut file) = File::options().read(true).open(file_path) {
file.seek(SeekFrom::Start(0)).map_err(|_| ())?;
// Extract user information
let mut buffer_u16 = [0u8; 2];
let mut buffer_u32 = [0u8; 4];
file.read_exact(&mut buffer_u32).map_err(|_| ())?;
let handle_id = unpack_u32(&buffer_u32, &mut 0);
file.read_exact(&mut buffer_u32).map_err(|_| ())?;
let na_key = unpack_u32(&buffer_u32, &mut 0);
file.read_exact(&mut buffer_u16).map_err(|_| ())?;
let secret_length = unpack_u16(&buffer_u16, &mut 0);
let mut secret = vec![0u8; secret_length as usize];
file.read_exact(&mut secret).map_err(|_| ())?;
let (handle, _) = self.handle_fetch(handle_id)?;
Ok(User {
id,
handle_id,
handle,
secret,
na_key,
})
} else { Err(()) }
}
pub fn user_count(&mut self) -> Result<usize, ()>
// Get number of salts in store.
//
{
Self::get_header_size(&mut self.index_user)
}
pub fn handle_store(&mut self, handle:&String, user_id:u32) -> Result<u32,()>
// Add a salt to store.
@ -111,7 +321,7 @@ impl FileSystem {
self.index_handle.write(&pack_u32(size + 1)).map_err(|_| ())?;
// Create bucket file if not exists
let bucket_path = Path::new(DIR_HANDLE).join(format!("{:x}.bin", bucket_index));
let bucket_path = Path::new(DIR_HANDLE).join(format!("{:08x}.bin", bucket_index));
if !bucket_path.exists() {
fs::write(bucket_path.clone(), vec![0u8; 12 * HANDLE_BUCKET_SIZE as usize]).map_err(|_| ())?;
}
@ -153,7 +363,7 @@ impl FileSystem {
let record_offset = 12 * record_index as u64;
let path = Path::new(DIR_HANDLE)
.join(format!("{:x}.bin", bucket_index));
.join(format!("{:08x}.bin", bucket_index));
// Read bucket file for record
if let Ok(mut file) = File::open(path) {

View File

@ -5,7 +5,7 @@ pub fn pack_u8(value:u8) -> Vec<u8>
pub fn unpack_u8(data:&[u8], index:&mut usize) -> u8
{
let mut result :u8 = 0;
let mut result = 0u8;
if *index < data.len() {
result = data[*index];
*index += 1;
@ -20,7 +20,7 @@ pub fn pack_u16(value:u16) -> Vec<u8>
pub fn unpack_u16(data:&[u8], index:&mut usize) -> u16
{
let mut result :u16 = 0;
let mut result = 0u16;
if *index < data.len() {
result = (data[*index] as u16) << 8;
*index += 1;
@ -44,22 +44,40 @@ pub fn pack_u32(value:u32) -> Vec<u8>
pub fn unpack_u32(data:&[u8], index:&mut usize) -> u32
{
let mut result :u32 = 0;
if *index < data.len() {
result = (data[*index] as u32) << 24;
*index += 1;
}
if *index < data.len() {
result = (data[*index] as u32) << 16;
*index += 1;
}
if *index < data.len() {
result = (data[*index] as u32) << 8;
*index += 1;
}
let mut result = 0u32;
for _ in 0..4 {
result <<= 8;
if *index < data.len() {
result |= data[*index] as u32;
*index += 1;
} else { break; }
}
result
}
pub fn pack_u64(value:u64) -> Vec<u8>
{
vec![
(value >> 56) as u8,
(value >> 48) as u8,
(value >> 40) as u8,
(value >> 32) as u8,
(value >> 24) as u8,
(value >> 16) as u8,
(value >> 8) as u8,
(value & 0xFF) as u8,
]
}
pub fn unpack_u64(data:&[u8], index:&mut usize) -> u64
{
let mut result = 0u64;
for _ in 0..8 {
result <<= 8;
if *index < data.len() {
result |= data[*index] as u64;
*index += 1;
} else { break; }
}
result
}

View File

@ -2,7 +2,7 @@ main>canvas#game {
display: block;
position: relative;
width: 100%;
flex-grow: 1;
height: 100%;
background-color: #101010;
}

View File

@ -1,91 +1,254 @@
const GAME_CONST = {
PLAYER_DAWN: 0,
PLAYER_DUSK: 1,
const GAME = { };
let GAME_DATA = null;
SOURCE_BOARD: 0,
SOURCE_POOL: 1,
GAME.Board = class {
constructor() {
this.tiles = [ ]; for(let i = 0; i < 61; ++i) { this.tiles.push(new GAME.Tile()); }
this.pieces = [ ];
this.dawn = new GAME.Player();
this.dusk = new GAME.Player();
this.init();
}
init() {
this.pieces = [ ];
// Describe Dawn layout
let layout = [
{ piece:GAME.Const.PieceId.Militia, hex:new MATH.Vec2(0, 1) },
{ piece:GAME.Const.PieceId.Militia, hex:new MATH.Vec2(1, 1) },
{ piece:GAME.Const.PieceId.Militia, hex:new MATH.Vec2(2, 2) },
{ piece:GAME.Const.PieceId.Militia, hex:new MATH.Vec2(3, 2) },
{ piece:GAME.Const.PieceId.Militia, hex:new MATH.Vec2(4, 3) },
{ piece:GAME.Const.PieceId.Militia, hex:new MATH.Vec2(5, 3) },
{ piece:GAME.Const.PieceId.Militia, hex:new MATH.Vec2(6, 4) },
{ piece:GAME.Const.PieceId.Militia, hex:new MATH.Vec2(7, 4) },
{ piece:GAME.Const.PieceId.Militia, hex:new MATH.Vec2(8, 5) },
{ piece:GAME.Const.PieceId.Lance, hex:new MATH.Vec2(0, 0) },
{ piece:GAME.Const.PieceId.Lance, hex:new MATH.Vec2(8, 4) },
{ piece:GAME.Const.PieceId.Knight, hex:new MATH.Vec2(1, 0) },
{ piece:GAME.Const.PieceId.Knight, hex:new MATH.Vec2(7, 3) },
{ piece:GAME.Const.PieceId.Castle, hex:new MATH.Vec2(2, 0) },
{ piece:GAME.Const.PieceId.Castle, hex:new MATH.Vec2(6, 2) },
{ piece:GAME.Const.PieceId.Tower, hex:new MATH.Vec2(3, 0) },
{ piece:GAME.Const.PieceId.Tower, hex:new MATH.Vec2(5, 1) },
{ piece:GAME.Const.PieceId.Dragon, hex:new MATH.Vec2(4, 1) },
{ piece:GAME.Const.PieceId.Omen, hex:new MATH.Vec2(4, 0) },
];
// Add Dawn pieces
for(let piece of layout) {
this.place_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);
}
}
place_piece(piece, player, hex) {
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);
}
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);
}
};
const GAME_CLASS = {
Board: class {
constructor() {
this.tiles = [ ]; for(let i = 0; i < 61; ++i) { this.tiles.push(new GAME_CLASS.Tile()); }
this.dawn = new GAME_CLASS.Player();
}
},
Player: class {
GAME.Player = class {
constructor() {
this.handle = "";
this.pool = new GAME_CLASS.Pool();
this.pool = new GAME.Pool();
}
},
};
Pool: class {
GAME.Pool = class {
constructor() {
this.pieces = [ ]; for(let i = 0; i < 6; ++i) { this.pieces.push(0); }
}
},
};
Tile: class {
GAME.Tile = class {
constructor() {
this.piece = 0;
this.piece = null;
this.threaten = { dawn: 0, dusk: 0 };
this.checking = { dawn: false, dusk: false };
}
},
Move: class {
reset() {
this.threaten = { dawn: 0, dusk: 0 };
this.checking = { dawn: false, dusk: false };
}
};
GAME.Move = class {
constructor(source, from, to) {
this.source = source;
this.from = from;
this.to = to;
}
},
};
GamePiece: class {
GAME.GamePiece = class {
constructor(name, assets, moves, promote_moves) {
this.name = name;
this.assets = assets;
this.moves = moves;
this.pmoves = promote_moves;
}
},
};
Piece: class {
GAME.PieceMovement = class {
constructor() {
this.direction = 0;
this.stride = 0;
}
add(direction) {
this.direction |= 1 << direction;
return this;
}
add_stride(direction) {
this.direction |= 1 << direction;
this.stride |= 1 << direction;
return this;
}
rotate() {
let copy = new GAME.PieceMovement();
copy.direction = BITWISE.rotate_blocks(this.direction);
copy.stride = BITWISE.rotate_blocks(this.stride);
return copy;
}
};
GAME.Piece = class {
constructor(piece, player) {
this.piece = piece;
this.player = player;
this.promoted = false;
this.tile = 0;
this.blocking = 0;
}
};
GAME.Game = class {
constructor() {
this.turn = 0;
this.board = new GAME.Board();
}
update_board() {
// Reset tiles
for(tile of this.board.tiles) { tile.reset(); }
// Determine threaten, check, and blocking for each piece
for(piece of this.board.pieces) {
}
}
process(move) {
// Recalculate new board state.
GAME.update_board();
}
validate(move) {
}
get_move_tiles(piece_id) {
}
};
GAME.Const = {
Player: {
Dawn: 0,
Dusk: 1,
},
Game: class {
constructor() {
this.board = new GAME_CLASS.Board();
this.pieces = [
new GAME_CLASS.GamePiece("Militia",
Source: {
Board: 0,
Pool: 1,
},
Direction: [
new MATH.Vec2(0, 1),
new MATH.Vec2(1, 1),
new MATH.Vec2(1, 0),
new MATH.Vec2(0, -1),
new MATH.Vec2(-1, -1),
new MATH.Vec2(-1, 0),
new MATH.Vec2(1, 2),
new MATH.Vec2(2, 1),
new MATH.Vec2(1, -1),
new MATH.Vec2(-1, -2),
new MATH.Vec2(-2, -1),
new MATH.Vec2(-1, 1),
],
PieceId: {
Militia: 0,
Knight: 1,
Lance: 2,
Tower: 3,
Castle: 4,
Dragon: 5,
Omen: 6,
},
Piece: [
new GAME.GamePiece(
"Militia",
["asset/militia_dusk.svg", "asset/militia_dawn.svg"],
new Move()
new GAME.PieceMovement()
.add(0)
.add(1)
.add(5),
new Move()
new GAME.PieceMovement()
.add(0)
.add(1)
.add(2)
.add(4)
.add(5)
.add(5),
),
new GAME_CLASS.GamePiece("Knight",
new GAME.GamePiece(
"Knight",
["asset/knight_dusk.svg", "asset/knight_dawn.svg"],
new Move()
new GAME.PieceMovement()
.add(3)
.add(6)
.add(11)
.add(13)
.add(17),
new Move()
new GAME.PieceMovement()
.add(3)
.add(6)
.add(7)
@ -94,34 +257,34 @@ const GAME_CLASS = {
.add(13)
.add(14)
.add(16)
.add(17)
.add(17),
),
new GAME_CLASS.GamePiece("Lance",
new GAME.GamePiece(
"Lance",
["asset/lance_dusk.svg", "asset/lance_dawn.svg"],
new Move()
.add(0, true)
new GAME.PieceMovement()
.add_stride(0)
.add(1)
.add(5),
new Move()
.add(0, true)
.add(1, true)
.add(2, true)
.add(3, true)
.add(4, true)
.add(5, true)
new GAME.PieceMovement()
.add_stride(0)
.add_stride(1)
.add_stride(2)
.add_stride(3)
.add_stride(4)
.add_stride(5),
),
new GAME_CLASS.GamePiece("Tower",
new GAME.GamePiece(
"Tower",
["asset/tower_dusk.svg", "asset/tower_dawn.svg"],
new Move()
new GAME.PieceMovement()
.add(0)
.add(1)
.add(3)
.add(5)
.add(6)
.add(11),
new Move()
new GAME.PieceMovement()
.add(0)
.add(1)
.add(2)
@ -131,11 +294,12 @@ const GAME_CLASS = {
.add(6)
.add(8)
.add(9)
.add(11)
.add(11),
),
new GAME_CLASS.GamePiece("Castle",
new GAME.GamePiece(
"Castle",
["asset/castle_dusk.svg", "asset/castle_dawn.svg"],
new Move()
new GAME.PieceMovement()
.add(0)
.add(1)
.add(2)
@ -143,44 +307,44 @@ const GAME_CLASS = {
.add(5)
.add(7)
.add(10),
new Move()
new GAME.PieceMovement()
.add(0)
.add(1)
.add(2)
.add(3)
.add(4)
.add(5)
.add(7, true)
.add(10, true)
.add_stride(7)
.add_stride(10),
),
new GAME_CLASS.GamePiece("Dragon",
new GAME.GamePiece(
"Dragon",
["asset/dragon_dusk.svg", "asset/dragon_dawn.svg"],
new Move()
.add(6, true)
.add(7, true)
.add(8, true)
.add(9, true)
.add(10, true)
.add(11, true),
new Move()
.add(0, true)
.add(1, true)
.add(2, true)
.add(3, true)
.add(4, true)
.add(5, true)
.add(6, true)
.add(7, true)
.add(8, true)
.add(9, true)
.add(10, true)
.add(11, true)
new GAME.PieceMovement()
.add_stride(6)
.add_stride(7)
.add_stride(8)
.add_stride(9)
.add_stride(10)
.add_stride(11),
new GAME.PieceMovement()
.add_stride(0)
.add_stride(1)
.add_stride(2)
.add_stride(3)
.add_stride(4)
.add_stride(5)
.add_stride(6)
.add_stride(7)
.add_stride(8)
.add_stride(9)
.add_stride(10)
.add_stride(11),
),
new GAME_CLASS.GamePiece("Omen",
new GAME.GamePiece(
"Omen",
["asset/king_dusk.svg", "asset/king_dawn.svg"],
new Move()
new GAME.PieceMovement()
.add(0)
.add(1)
.add(2)
@ -188,25 +352,11 @@ const GAME_CLASS = {
.add(4)
.add(5)
.add(7)
.add(10)
.add(10),
),
];
}
}
],
};
let GAME_DATA = null;
const GAME = {
init() {
GAME_DATA = new GAME_CLASS.Game();
},
process(move) {
},
validate(move) {
},
GAME.init = () => {
GAME_DATA = new GAME.Game();
};

View File

@ -1,11 +1,32 @@
let INTERFACE_DATA = {
canvas:null,
context:null,
scale:1,
};
let INTERFACE_DATA = null;
const INTERFACE = {
Color: {
Background: "#101010",
Text: "#c0c0c0",
TileBorder: "#606060",
TileLight: "#383838",
TileMedium: "#242424",
TileDark: "#101010",
Dawn: "#ffe082",
DawnDark: "#ff6d00",
Dusk: "#f6a1bd",
DuskDark: "#c51162",
HintHover: "#71a1e8",
HintSelect: "#4a148c",
HintValid: "#1a237e",
HintAllowed: "#6a1b9a",
HintInvalid: "b71c1c",
HintPlay: "#083242",
HintWarn: "#054719",
HintCheck: "#C62828",
},
hover(event) {
},
@ -14,66 +35,330 @@ const INTERFACE = {
},
resize() {
INTERFACE_DATA.canvas.width = INTERFACE_DATA.canvas.clientWidth;
INTERFACE_DATA.canvas.height = INTERFACE_DATA.canvas.clientHeight;
},
draw() {
this.resize();
let canvas = INTERFACE_DATA.canvas;
let ctx = INTERFACE_DATA.context;
// Determine
let width = INTERFACE_DATA.canvas.width;
let height = INTERFACE_DATA.canvas.height;
let min_dimension = Math.min(width, height);
// Determine interface configuration
canvas.width = canvas.clientWidth;
canvas.height = canvas.clientHeight;
let scale = 1;
//let margin = INTERFACE_DATA.canvas.
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,
);
// Draw indicator gradient if player's turn.
// Draw background
ctx.clearRect(0, 0, width, height);
ctx.fillStyle = INTERFACE.Color.Background;
ctx.fillRect(0, 0, width, height);
// Draw tiles
for(let i = 0; i < GAME.board.tiles.length; ++i) {
// Draw background
let radius = INTERFACE.Radius * gui_scale;
let basis_x = gui_offset.x + radius;
let basis_y = gui_offset.y + (13 * gui_scale);
const TILE_SCALE = 0.9;
ctx.lineWidth = gui_scale * 0.06;
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 gui_x = basis_x + (1.5 * radius * coord.x);
let gui_y = basis_y - (2 * gui_scale * coord.y) + (gui_scale * coord.x);
ctx.save();
ctx.translate(gui_x, gui_y);
// 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 {
}
ctx.beginPath();
draw.hex();
ctx.fill();
// Draw tile content
if(tile.piece !== null) {
let piece = GAME_DATA.board.pieces[tile.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);
// Draw piece
// Draw
} else {
// Draw standard border
ctx.strokeStyle = INTERFACE.Color.TileBorder;
ctx.beginPath();
draw.hex();
ctx.stroke();
}
ctx.restore();
}
// Draw player pool
for(let i = 0; i < 6; ++i) {
let gui_x = basis_x + (radius * 14);
let gui_y = basis_y - (9 - (2 * i)) * gui_scale;
ctx.save();
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;
ctx.beginPath();
draw.hex();
ctx.stroke();
ctx.restore();
}
// Draw opponent pool
for(let i = 0; i < 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 {
}
ctx.beginPath();
draw.hex();
ctx.fill();
// Draw border
ctx.strokeStyle = INTERFACE.Color.Dusk;
ctx.beginPath();
draw.hex();
ctx.stroke();
ctx.restore();
}
// Draw informational text
ctx.font = Math.ceil(gui_scale / 24) + "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);
// Dusk handle
ctx.fillStyle = INTERFACE.Color.Text;
ctx.textBaseline = "top";
ctx.textAlign = "center";
ctx.fillText(GAME_DATA.turn, width - gui_margin, gui_margin);
*/
// Number of moves
ctx.fillStyle = INTERFACE.Color.Text;
ctx.textBaseline = "top";
ctx.textAlign = "right";
ctx.fillText(GAME_DATA.turn, width - gui_margin, gui_margin);
// Game state message
if(INTERFACE_DATA.message !== null) {
ctx.fillStyle = INTERFACE.Color.Text;
ctx.textBaseline = "bottom";
ctx.textAlign = "left";
ctx.fillText(INTERFACE_DATA.message, gui_margin, height - gui_margin);
}
},
init() {
Draw: class {
constructor(ctx, scale) {
this.ctx = ctx;
this.scale = scale;
}
hex() {
this.ctx.moveTo(INTERFACE.HexVertex[5].x * this.scale, INTERFACE.HexVertex[5].y * this.scale);
for(let i = 0; i < INTERFACE.HexVertex.length; ++i) {
this.ctx.lineTo(INTERFACE.HexVertex[i].x * this.scale, INTERFACE.HexVertex[i].y * this.scale);
}
}
hints(piece) {
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(); }
moves = moves.direction;
for(let mask = BITWISE.lsb(moves); moves > 0; mask = BITWISE.lsb(moves)) {
let move = BITWISE.ffs(mask);
let nmove = move % 12;
this.ctx.strokeStyle = (piece.player == GAME.Const.Player.Dawn)? INTERFACE.Color.Dawn : INTERFACE.Color.Dusk;
this.ctx.beginPath();
// draw edge marker
if(nmove < 6) {
let fr = INTERFACE.HexVertex[nmove];
let to = INTERFACE.HexVertex[(nmove + 1) % 6];
let dx = (to.x - fr.x) / 3.0;
let dy = (to.y - fr.y) / 3.0;
let fqx = fr.x + dx;
let fqy = fr.y + dy;
let tqx = fr.x + (2 * dx);
let tqy = fr.y + (2 * dy);
this.ctx.moveTo(fqx * this.scale, fqy * this.scale);
this.ctx.lineTo(tqx * this.scale, tqy * this.scale);
this.ctx.stroke();
}
// draw vertex marker
else {
let fr = INTERFACE.HexVertex[nmove % 6];
let mid = INTERFACE.HexVertex[(nmove + 1) % 6];
let to = INTERFACE.HexVertex[(nmove + 2) % 6];
let dx1 = (mid.x - fr.x) / 3.0;
let dy1 = (mid.y - fr.y) / 3.0;
let dx2 = (to.x - mid.x) / 3.0;
let dy2 = (to.y - mid.y) / 3.0;
let fqx = mid.x - dx1;
let fqy = mid.y - dy1;
let tqx = mid.x + dx2;
let tqy = mid.y + dy2;
this.ctx.moveTo(fqx * this.scale, fqy * this.scale);
this.ctx.lineTo(mid.x * this.scale, mid.y * this.scale);
this.ctx.lineTo(tqx * this.scale, tqy * this.scale);
this.ctx.stroke();
}
moves &= ~mask;
}
}
},
init(player_id) {
GAME.init();
INTERFACE_DATA.canvas = document.getElementById("game");
let canvas = INTERFACE_DATA.canvas;
INTERFACE_DATA = {
canvas: document.getElementById("game"),
context: null,
player_id: player_id,
message: null,
};
let canvas = INTERFACE_DATA.canvas;
if(canvas !== undefined) {
INTERFACE_DATA.context = canvas.getContext("2d");
canvas.addEventListener("mousemove", INTERFACE.hover);
canvas.addEventListener("mousedown", INTERFACE.click);
canvas.addEventListener("resize", INTERFACE.draw);
window.addEventListener("resize", INTERFACE.draw);
this.draw();
}
},
uninit() {
INTERFACE_DATA.canvas = null;
INTERFACE_DATA.context = null;
MAIN.removeChild(INTERFACE_DATA.canvas);
INTERFACE_DATA = null;
},
message(data) {
message(code, data) {
switch(code) {
case OpCode.GameState: {
// Build game state
INTERFACE.draw();
} break;
case OpCode.GamePlay: {
// Apply play to board
GAME_DATA.turn += 1;
INTERFACE.draw();
} break;
}
},
};
INTERFACE.Radius = 2.0 / Math.sqrt(3.0);
INTERFACE.HalfRadius = 1.0 / Math.sqrt(3.0);
INTERFACE.Scale = 1 / 18;
INTERFACE.Ratio = (17.5 * INTERFACE.Radius) * INTERFACE.Scale;
INTERFACE.HexVertex = [
// top face
new MATH.Vec2(-INTERFACE.HalfRadius, -1),
// top-right face
new MATH.Vec2(INTERFACE.HalfRadius, -1),
// bottom-right face
new MATH.Vec2(INTERFACE.Radius, 0),
// bottom face
new MATH.Vec2(INTERFACE.HalfRadius, 1),
// bottom-left face
new MATH.Vec2(-INTERFACE.HalfRadius, 1),
// top-left face
new MATH.Vec2(-INTERFACE.Radius, 0),
];

View File

@ -432,6 +432,7 @@ const SCENES = {
return true;
},
unload() {
INTERFACE.uninit();
MESSAGE_COMPOSE([
PACK.u16(OpCode.SessionLeave),
]);

View File

@ -125,17 +125,49 @@ const BITWISE = {
mask = mask - ((mask >> 1) & 0x55555555);
mask = (mask & 0x33333333) + ((mask >> 2) & 0x33333333);
return ((mask + (mask >> 4) & 0xF0F0F0F) * 0x1010101) >> 24;
}
},
rotate_blocks(mask)
{
const r1 = 0x00003F; // first 6 bits
const r2 = 0x000FC0; // second 6 bits
const r3 = 0x03F000; // third 6 bits
let v1 = (r1 & mask) << 3;
let v2 = (r2 & mask) << 3;
let v3 = (r3 & mask) << 3;
v1 = (v1 & r1) | ((v1 & ~r1) >> 6);
v2 = (v2 & r2) | ((v2 & ~r2) >> 6);
v3 = (v3 & r3) | ((v3 & ~r3) >> 6);
return v1 | v2 | v3;
},
};
const MATH = {
sign(a)
{
Vec2: class {
constructor(x, y) {
this.x = x;
this.y = y;
}
},
sign(a) {
return 1 - ((a < 0) << 1);
},
mod(a, b)
{
mod(a, b) {
return ((a % b) + b) % b
},
};
const COLOR = {
rgba(r, g, b, a) {
let ur = Math.floor(r * 255);
let ug = Math.floor(r * 255);
let ub = Math.floor(r * 255);
let ua = Math.floor(r * 255);
return "rgba(" + ur + "," + ug + "," + ub + "," + ua + ")";
},
}