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 connections:Pool<Connection>,
pub users:Pool<User>, pub users:Pool<User>,
pub user_next:u32,
pub user_id:Sparse<usize>, pub user_id:Sparse<usize>,
pub user_handle:Trie<u32>, pub user_handle:Trie<u32>,
pub salts:Sparse<[u8; 16]>, pub salts:Sparse<[u8; 16]>,
@ -48,25 +47,58 @@ impl App {
let handle_count = filesystem.handle_count()?; let handle_count = filesystem.handle_count()?;
for id in 0..handle_count { for id in 0..handle_count {
let (handle, user_id) = filesystem.handle_fetch(id as u32).unwrap(); let (handle, user_id) = filesystem.handle_fetch(id as u32).unwrap();
println!("got: {} = {}", handle, user_id);
user_handle.set(handle.as_bytes(), 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 { Ok(Self {
filesystem:filesystem, filesystem:filesystem,
connections:Pool::new(), connections:Pool::new(),
users:Pool::new(), users,
user_next:0, user_id,
user_id:Sparse::new(), user_handle,
user_handle:Trie::new(),
salts, salts,
auths:Trie::new(), auths:Trie::new(),
sessions:Trie::new(), sessions,
session_time:Chain::new(), session_time,
}) })
} else { } else {
Err(()) Err(())

View File

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

View File

@ -1,5 +1,6 @@
pub struct User { pub struct User {
pub id:u32, pub id:u32,
pub handle_id:u32,
pub handle:String, pub handle:String,
pub secret:Vec<u8>, pub secret:Vec<u8>,
pub na_key:u32, 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(); let salt_id = app.filesystem.salt_store(salt).unwrap();
app.salts.set(salt_id as isize, salt); app.salts.set(salt_id as isize, salt);
if let Ok(hash) = argon2::hash_raw(&request.secret, &salt, &argon_config) { if let Ok(secret) = argon2::hash_raw(&request.secret, &salt, &argon_config) {
let user_id = app.user_next; let user_id = app.filesystem.user_count().unwrap() as u32;
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,
});
// Register user pool id and handle // Register user pool id and handle
app.filesystem.handle_store(&request.handle, user_id).ok(); let handle_id = app.filesystem.handle_store(&request.handle, user_id).unwrap();
app.user_id.set(user_id as isize, user_pos);
app.user_handle.set(request.handle.as_bytes(), user_id); 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); println!("Registered user '{}' @ {} with id {}", request.handle, user_pos, user_id);
// Generate authentication token and secret // Generate authentication token and secret
@ -296,7 +301,7 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
} else { String::new() }; } else { String::new() };
response.records.push(PacketSessionListResponseRecord { response.records.push(PacketSessionListResponseRecord {
token:session.key, token:session.token,
handles:[ handles:[
dawn_handle, dawn_handle,
dusk_handle, dusk_handle,
@ -337,9 +342,11 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
rng.fill(&mut secret).ok(); rng.fill(&mut secret).ok();
let chain_id = app.session_time.add(token); let chain_id = app.session_time.add(token);
app.sessions.set(&token, Session {
key:token, let mut session = Session {
secret:secret, id:0,
token,
secret,
game:game::Game::new(), game:game::Game::new(),
p_dawn:Viewer { p_dawn:Viewer {
connection:None,//Some(qr.id), connection:None,//Some(qr.id),
@ -350,8 +357,13 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
user:None, user:None,
}, },
viewers:Vec::new(), 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); app.session_time.set(chain_id, token);
// Set player to Dawn. // Set player to Dawn.
@ -397,6 +409,8 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
false false
} { } {
println!("Add user to session."); println!("Add user to session.");
app.filesystem.session_update(session.id, session).ok();
response.status = STATUS_OK; response.status = STATUS_OK;
} }
} else { } 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))) Some(QRPacket::new(qr.id, QRPacketData::RSessionJoin(response)))
} }
// SessionRetire
// SessionLeave
// GameState
// GamePlay
_ => { Some(QRPacket::new(0, QRPacketData::None)) } _ => { 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_list; pub use session_list::*;
mod session_create; pub use session_create::*; mod session_create; pub use session_create::*;
mod session_join; pub use session_join::*; 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 { mod prelude {
pub trait Packet { pub trait Packet {

View File

@ -2,23 +2,31 @@ use std::{
fs::{self, File}, io::{Read, Seek, SeekFrom, Write}, path::Path fs::{self, File}, io::{Read, Seek, SeekFrom, Write}, path::Path
}; };
use game::Game;
use crate::{ use crate::{
app::session::Session, app::{
app::user::User, session::{self, Session, SessionToken, SessionSecret},
util::pack::{pack_u32, unpack_u32} user::User,
},
util::pack::*,
}; };
const HANDLE_BUCKET_MASK :u32 = 0xFF; const HANDLE_BUCKET_MASK :u32 = 0xFF;
const HANDLE_BUCKET_SIZE :u32 = HANDLE_BUCKET_MASK + 1; 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_DATA :&str = "data";
const DIR_HANDLE :&str = const_format::formatcp!("{}/h", DIR_DATA); const DIR_HANDLE :&str = const_format::formatcp!("{}/h", DIR_DATA);
const DIR_SESSION :&str = const_format::formatcp!("{}/s", DIR_DATA); const DIR_SESSION :&str = const_format::formatcp!("{}/s", DIR_DATA);
const DIR_USER :&str = const_format::formatcp!("{}/u", 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_HANDLE :&str = const_format::formatcp!("{d}/{f}", d= DIR_HANDLE, f= GENERIC_INDEX);
const INDEX_SESSION :&str = const_format::formatcp!("{}/i.bin", DIR_SESSION); const INDEX_SESSION :&str = const_format::formatcp!("{d}/{f}", d= DIR_SESSION, f= GENERIC_INDEX);
const INDEX_USER :&str = const_format::formatcp!("{}/i.bin", DIR_USER); 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); pub const FILE_SALT :&str = const_format::formatcp!("{}/x.bin", DIR_DATA);
@ -39,7 +47,7 @@ impl FileSystem {
// Initialize filesystem if does not exist. // 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() { if !Path::new(DIR_DATA).exists() {
fs::create_dir(DIR_DATA)?; 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(()) Err(())
} }
pub fn session_history_fetch(&mut self, _id:u32) -> Result<Vec<()>,()>
pub fn user_store(&mut self, _user:&User) -> Result<(),()>
{ {
Err(()) 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,()> pub fn handle_store(&mut self, handle:&String, user_id:u32) -> Result<u32,()>
// Add a salt to store. // Add a salt to store.
@ -111,7 +321,7 @@ impl FileSystem {
self.index_handle.write(&pack_u32(size + 1)).map_err(|_| ())?; self.index_handle.write(&pack_u32(size + 1)).map_err(|_| ())?;
// Create bucket file if not exists // 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() { if !bucket_path.exists() {
fs::write(bucket_path.clone(), vec![0u8; 12 * HANDLE_BUCKET_SIZE as usize]).map_err(|_| ())?; 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 record_offset = 12 * record_index as u64;
let path = Path::new(DIR_HANDLE) let path = Path::new(DIR_HANDLE)
.join(format!("{:x}.bin", bucket_index)); .join(format!("{:08x}.bin", bucket_index));
// Read bucket file for record // Read bucket file for record
if let Ok(mut file) = File::open(path) { 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 pub fn unpack_u8(data:&[u8], index:&mut usize) -> u8
{ {
let mut result :u8 = 0; let mut result = 0u8;
if *index < data.len() { if *index < data.len() {
result = data[*index]; result = data[*index];
*index += 1; *index += 1;
@ -20,7 +20,7 @@ pub fn pack_u16(value:u16) -> Vec<u8>
pub fn unpack_u16(data:&[u8], index:&mut usize) -> u16 pub fn unpack_u16(data:&[u8], index:&mut usize) -> u16
{ {
let mut result :u16 = 0; let mut result = 0u16;
if *index < data.len() { if *index < data.len() {
result = (data[*index] as u16) << 8; result = (data[*index] as u16) << 8;
*index += 1; *index += 1;
@ -44,22 +44,40 @@ pub fn pack_u32(value:u32) -> Vec<u8>
pub fn unpack_u32(data:&[u8], index:&mut usize) -> u32 pub fn unpack_u32(data:&[u8], index:&mut usize) -> u32
{ {
let mut result :u32 = 0; let mut result = 0u32;
if *index < data.len() { for _ in 0..4 {
result = (data[*index] as u32) << 24; result <<= 8;
*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;
}
if *index < data.len() { if *index < data.len() {
result |= data[*index] as u32; result |= data[*index] as u32;
*index += 1; *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 result
} }

View File

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

View File

@ -1,91 +1,254 @@
const GAME_CONST = { const GAME = { };
PLAYER_DAWN: 0, let GAME_DATA = null;
PLAYER_DUSK: 1,
SOURCE_BOARD: 0, GAME.Board = class {
SOURCE_POOL: 1, 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 = { GAME.Player = 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 {
constructor() { constructor() {
this.handle = ""; this.handle = "";
this.pool = new GAME_CLASS.Pool(); this.pool = new GAME.Pool();
} }
}, };
Pool: class { GAME.Pool = class {
constructor() { 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(0); }
} }
}, };
Tile: class { GAME.Tile = class {
constructor() { 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) { constructor(source, from, to) {
this.source = source; this.source = source;
this.from = from; this.from = from;
this.to = to; this.to = to;
} }
}, };
GamePiece: class { GAME.GamePiece = class {
constructor(name, assets, moves, promote_moves) { constructor(name, assets, moves, promote_moves) {
this.name = name; this.name = name;
this.assets = assets; this.assets = assets;
this.moves = moves; this.moves = moves;
this.pmoves = promote_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) { constructor(piece, player) {
this.piece = piece; this.piece = piece;
this.player = player; this.player = player;
this.promoted = false; 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 { Source: {
constructor() { Board: 0,
this.board = new GAME_CLASS.Board(); Pool: 1,
this.pieces = [ },
new GAME_CLASS.GamePiece("Militia",
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"], ["asset/militia_dusk.svg", "asset/militia_dawn.svg"],
new Move() new GAME.PieceMovement()
.add(0) .add(0)
.add(1) .add(1)
.add(5), .add(5),
new GAME.PieceMovement()
new Move()
.add(0) .add(0)
.add(1) .add(1)
.add(2) .add(2)
.add(4) .add(4)
.add(5) .add(5),
), ),
new GAME_CLASS.GamePiece("Knight", new GAME.GamePiece(
"Knight",
["asset/knight_dusk.svg", "asset/knight_dawn.svg"], ["asset/knight_dusk.svg", "asset/knight_dawn.svg"],
new Move() new GAME.PieceMovement()
.add(3) .add(3)
.add(6) .add(6)
.add(11) .add(11)
.add(13) .add(13)
.add(17), .add(17),
new GAME.PieceMovement()
new Move()
.add(3) .add(3)
.add(6) .add(6)
.add(7) .add(7)
@ -94,34 +257,34 @@ const GAME_CLASS = {
.add(13) .add(13)
.add(14) .add(14)
.add(16) .add(16)
.add(17) .add(17),
), ),
new GAME_CLASS.GamePiece("Lance", new GAME.GamePiece(
"Lance",
["asset/lance_dusk.svg", "asset/lance_dawn.svg"], ["asset/lance_dusk.svg", "asset/lance_dawn.svg"],
new Move() new GAME.PieceMovement()
.add(0, true) .add_stride(0)
.add(1) .add(1)
.add(5), .add(5),
new GAME.PieceMovement()
new Move() .add_stride(0)
.add(0, true) .add_stride(1)
.add(1, true) .add_stride(2)
.add(2, true) .add_stride(3)
.add(3, true) .add_stride(4)
.add(4, true) .add_stride(5),
.add(5, true)
), ),
new GAME_CLASS.GamePiece("Tower", new GAME.GamePiece(
"Tower",
["asset/tower_dusk.svg", "asset/tower_dawn.svg"], ["asset/tower_dusk.svg", "asset/tower_dawn.svg"],
new Move() new GAME.PieceMovement()
.add(0) .add(0)
.add(1) .add(1)
.add(3) .add(3)
.add(5) .add(5)
.add(6) .add(6)
.add(11), .add(11),
new GAME.PieceMovement()
new Move()
.add(0) .add(0)
.add(1) .add(1)
.add(2) .add(2)
@ -131,11 +294,12 @@ const GAME_CLASS = {
.add(6) .add(6)
.add(8) .add(8)
.add(9) .add(9)
.add(11) .add(11),
), ),
new GAME_CLASS.GamePiece("Castle", new GAME.GamePiece(
"Castle",
["asset/castle_dusk.svg", "asset/castle_dawn.svg"], ["asset/castle_dusk.svg", "asset/castle_dawn.svg"],
new Move() new GAME.PieceMovement()
.add(0) .add(0)
.add(1) .add(1)
.add(2) .add(2)
@ -143,44 +307,44 @@ const GAME_CLASS = {
.add(5) .add(5)
.add(7) .add(7)
.add(10), .add(10),
new GAME.PieceMovement()
new Move()
.add(0) .add(0)
.add(1) .add(1)
.add(2) .add(2)
.add(3) .add(3)
.add(4) .add(4)
.add(5) .add(5)
.add(7, true) .add_stride(7)
.add(10, true) .add_stride(10),
), ),
new GAME_CLASS.GamePiece("Dragon", new GAME.GamePiece(
"Dragon",
["asset/dragon_dusk.svg", "asset/dragon_dawn.svg"], ["asset/dragon_dusk.svg", "asset/dragon_dawn.svg"],
new Move() new GAME.PieceMovement()
.add(6, true) .add_stride(6)
.add(7, true) .add_stride(7)
.add(8, true) .add_stride(8)
.add(9, true) .add_stride(9)
.add(10, true) .add_stride(10)
.add(11, true), .add_stride(11),
new GAME.PieceMovement()
new Move() .add_stride(0)
.add(0, true) .add_stride(1)
.add(1, true) .add_stride(2)
.add(2, true) .add_stride(3)
.add(3, true) .add_stride(4)
.add(4, true) .add_stride(5)
.add(5, true) .add_stride(6)
.add(6, true) .add_stride(7)
.add(7, true) .add_stride(8)
.add(8, true) .add_stride(9)
.add(9, true) .add_stride(10)
.add(10, true) .add_stride(11),
.add(11, true)
), ),
new GAME_CLASS.GamePiece("Omen", new GAME.GamePiece(
"Omen",
["asset/king_dusk.svg", "asset/king_dawn.svg"], ["asset/king_dusk.svg", "asset/king_dawn.svg"],
new Move() new GAME.PieceMovement()
.add(0) .add(0)
.add(1) .add(1)
.add(2) .add(2)
@ -188,25 +352,11 @@ const GAME_CLASS = {
.add(4) .add(4)
.add(5) .add(5)
.add(7) .add(7)
.add(10) .add(10),
), ),
]; ],
}
}
}; };
let GAME_DATA = null; GAME.init = () => {
GAME_DATA = new GAME.Game();
const GAME = {
init() {
GAME_DATA = new GAME_CLASS.Game();
},
process(move) {
},
validate(move) {
},
}; };

View File

@ -1,11 +1,32 @@
let INTERFACE_DATA = { let INTERFACE_DATA = null;
canvas:null,
context:null,
scale:1,
};
const INTERFACE = { 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) { 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() { draw() {
this.resize(); let canvas = INTERFACE_DATA.canvas;
let ctx = INTERFACE_DATA.context;
// Determine // Determine interface configuration
let width = INTERFACE_DATA.canvas.width; canvas.width = canvas.clientWidth;
let height = INTERFACE_DATA.canvas.height; canvas.height = canvas.clientHeight;
let min_dimension = Math.min(width, height);
let scale = 1; let width = canvas.width;
//let margin = INTERFACE_DATA.canvas. 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 // Draw tiles
for(let i = 0; i < GAME.board.tiles.length; ++i) { let radius = INTERFACE.Radius * gui_scale;
// Draw background 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 piece
// Draw } else {
// Draw standard border
ctx.strokeStyle = INTERFACE.Color.TileBorder;
ctx.beginPath();
draw.hex();
ctx.stroke();
}
ctx.restore();
} }
// Draw player pool // 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 // 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(); GAME.init();
INTERFACE_DATA.canvas = document.getElementById("game"); INTERFACE_DATA = {
let canvas = INTERFACE_DATA.canvas; canvas: document.getElementById("game"),
context: null,
player_id: player_id,
message: null,
};
let canvas = INTERFACE_DATA.canvas;
if(canvas !== undefined) { if(canvas !== undefined) {
INTERFACE_DATA.context = canvas.getContext("2d"); INTERFACE_DATA.context = canvas.getContext("2d");
canvas.addEventListener("mousemove", INTERFACE.hover); canvas.addEventListener("mousemove", INTERFACE.hover);
canvas.addEventListener("mousedown", INTERFACE.click); canvas.addEventListener("mousedown", INTERFACE.click);
canvas.addEventListener("resize", INTERFACE.draw); window.addEventListener("resize", INTERFACE.draw);
this.draw(); this.draw();
} }
}, },
uninit() { uninit() {
INTERFACE_DATA.canvas = null; MAIN.removeChild(INTERFACE_DATA.canvas);
INTERFACE_DATA.context = null; 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; return true;
}, },
unload() { unload() {
INTERFACE.uninit();
MESSAGE_COMPOSE([ MESSAGE_COMPOSE([
PACK.u16(OpCode.SessionLeave), PACK.u16(OpCode.SessionLeave),
]); ]);

View File

@ -125,17 +125,49 @@ const BITWISE = {
mask = mask - ((mask >> 1) & 0x55555555); mask = mask - ((mask >> 1) & 0x55555555);
mask = (mask & 0x33333333) + ((mask >> 2) & 0x33333333); mask = (mask & 0x33333333) + ((mask >> 2) & 0x33333333);
return ((mask + (mask >> 4) & 0xF0F0F0F) * 0x1010101) >> 24; 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 = { const MATH = {
sign(a) Vec2: class {
{ constructor(x, y) {
this.x = x;
this.y = y;
}
},
sign(a) {
return 1 - ((a < 0) << 1); return 1 - ((a < 0) << 1);
}, },
mod(a, b) mod(a, b) {
{
return ((a % b) + b) % 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 + ")";
},
}