Add session persistence and check handling.
This commit is contained in:
parent
adf3cdef10
commit
225cb34bab
@ -51,6 +51,10 @@ impl Board {
|
||||
}
|
||||
|
||||
pub fn init(&mut self) {
|
||||
self.tiles = [Tile::new(); 61];
|
||||
self.columns = [Column::new(); 9];
|
||||
self.pieces = [None; 38];
|
||||
|
||||
let layout = [
|
||||
(Piece::new(PIECE_MILITIA, PLAYER_DAWN), Hex::from_hex(0, 1)),
|
||||
(Piece::new(PIECE_MILITIA, PLAYER_DAWN), Hex::from_hex(1, 1)),
|
||||
|
@ -33,16 +33,24 @@ impl Game {
|
||||
pool:[[0; 6]; 2],
|
||||
|
||||
history:Vec::new(),
|
||||
}.init()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn init(mut self) -> Self
|
||||
pub fn init(&mut self)
|
||||
{
|
||||
self.board.init();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn process(&mut self, play:Play) -> Result<(),()>
|
||||
pub fn apply_history(&mut self, history:&Vec<Play>) -> Result<(),()>
|
||||
{
|
||||
self.init();
|
||||
for play in history {
|
||||
self.process(play)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn process(&mut self, play:&Play) -> Result<(),()>
|
||||
{
|
||||
let player = (self.turn & 1) as u8;
|
||||
|
||||
@ -105,6 +113,7 @@ impl Game {
|
||||
} else { false }
|
||||
} {
|
||||
self.turn += 1;
|
||||
self.history.push(*play);
|
||||
Ok(())
|
||||
} else { Err(()) }
|
||||
}
|
||||
|
@ -68,10 +68,15 @@ impl App {
|
||||
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();
|
||||
let mut session = filesystem.session_fetch(id as u32).unwrap();
|
||||
|
||||
times.push((session.time, session.token.clone()));
|
||||
|
||||
// Load session history
|
||||
if let Ok(history) = filesystem.session_history_fetch(id as u32) {
|
||||
session.game.apply_history(&history).ok();
|
||||
}
|
||||
|
||||
sessions.set(&session.token.clone(), session);
|
||||
}
|
||||
|
||||
|
@ -1,10 +1,11 @@
|
||||
use bus::Bus;
|
||||
use crate::{
|
||||
app::{
|
||||
App,
|
||||
authentication::Authentication,
|
||||
user::User,
|
||||
connection::Connection,
|
||||
user::User,
|
||||
App,
|
||||
session::Session,
|
||||
},
|
||||
protocol,
|
||||
};
|
||||
@ -359,6 +360,7 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
|
||||
time:std::time::SystemTime::now().duration_since(std::time::UNIX_EPOCH).unwrap().as_secs() as u64,
|
||||
chain_id,
|
||||
};
|
||||
session.game.init();
|
||||
|
||||
if let Some(conn) = app.connections.get_mut(qr.id as usize) {
|
||||
conn.session = Some(session.token);
|
||||
@ -386,6 +388,8 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
|
||||
response.status = STATUS_ERROR;
|
||||
response.token = request.token;
|
||||
|
||||
let mut send_gamestate = false;
|
||||
|
||||
// Verify that session exists
|
||||
if let Some(session) = app.sessions.get_mut(&request.token) {
|
||||
|
||||
@ -416,6 +420,8 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
|
||||
|
||||
app.filesystem.session_update(session.id, session).ok();
|
||||
response.status = STATUS_OK;
|
||||
|
||||
send_gamestate = true;
|
||||
true
|
||||
} else { false }
|
||||
}
|
||||
@ -451,6 +457,26 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
|
||||
}
|
||||
}
|
||||
|
||||
if send_gamestate {
|
||||
if let Some(session) = app.sessions.get(&request.token) {
|
||||
// Send GameState update to all connections.
|
||||
let mut packets = Vec::new();
|
||||
let game_state = generate_gamestate(&app, session);
|
||||
|
||||
for cid in &session.get_connections() {
|
||||
if *cid != qr.id {
|
||||
packets.push(QRPacket::new(
|
||||
*cid,
|
||||
QRPacketData::RGameState(game_state.clone()),
|
||||
));
|
||||
}
|
||||
}
|
||||
for packet in packets {
|
||||
app.send_response(packet).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Some(QRPacket::new(qr.id, QRPacketData::RSessionJoin(response)))
|
||||
}
|
||||
|
||||
@ -487,41 +513,7 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
|
||||
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()
|
||||
};
|
||||
}
|
||||
response = generate_gamestate(&app, &session);
|
||||
} else {
|
||||
response.status = STATUS_ERROR;
|
||||
}
|
||||
@ -538,22 +530,27 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
|
||||
|
||||
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) {
|
||||
if session.p_dawn.user.is_some() && session.p_dusk.user.is_some() {
|
||||
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 {
|
||||
// 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;
|
||||
// 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())
|
||||
));
|
||||
// Save play to game history.
|
||||
app.filesystem.session_history_push(session.id, request.play).ok();
|
||||
|
||||
// Forward play to all clients
|
||||
for cid in &session.get_connections() {
|
||||
packets.push(QRPacket::new(
|
||||
*cid,
|
||||
QRPacketData::QGamePlay(request.clone())
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -583,3 +580,46 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
|
||||
app.send_response(response).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_gamestate(app:&App, session:&Session) -> protocol::PacketGameStateResponse
|
||||
{
|
||||
use protocol::{PacketGameStateResponse, PacketGameStateResponsePiece};
|
||||
|
||||
let mut response = PacketGameStateResponse::new();
|
||||
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()
|
||||
};
|
||||
}
|
||||
|
||||
response
|
||||
}
|
||||
|
@ -2,7 +2,10 @@ use std::{
|
||||
fs::{self, File}, io::{Read, Seek, SeekFrom, Write}, path::Path
|
||||
};
|
||||
|
||||
use game::Game;
|
||||
use game::{
|
||||
history::Play,
|
||||
Game,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
app::{
|
||||
@ -98,15 +101,18 @@ impl FileSystem {
|
||||
self.index_session.seek(SeekFrom::Start(0)).map_err(|_| ())?;
|
||||
self.index_session.write(&pack_u32(size + 1)).map_err(|_| ())?;
|
||||
|
||||
// Write session config file
|
||||
match self.session_update(size, session) {
|
||||
Ok(_) => {
|
||||
|
||||
// Create session history file
|
||||
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(|_| ())?;
|
||||
fs::write(bucket_path.join(GENERIC_HISTORY), &[0; 2]).map_err(|_| ())?;
|
||||
Ok(size)
|
||||
}
|
||||
Err(_) => Err(()),
|
||||
@ -126,7 +132,7 @@ impl FileSystem {
|
||||
fs::create_dir_all(bucket_path.clone()).map_err(|_| ())?;
|
||||
}
|
||||
|
||||
// Open bucket file for record
|
||||
// Open session config file
|
||||
if let Ok(mut file) = File::options().write(true).create(true).open(bucket_path.join(GENERIC_CONFIG)) {
|
||||
|
||||
// Write session information
|
||||
@ -153,11 +159,8 @@ impl FileSystem {
|
||||
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
|
||||
// Open session config file
|
||||
if let Ok(mut file) = File::options().read(true).open(bucket_path.join(GENERIC_CONFIG)) {
|
||||
|
||||
// Extract session information
|
||||
@ -206,14 +209,73 @@ impl FileSystem {
|
||||
} else { Err(()) }
|
||||
}
|
||||
|
||||
pub fn session_history_push(&mut self, _id:u32, _history:()) -> Result<(),()>
|
||||
pub fn session_history_push(&mut self, id:u32, history:Play) -> Result<(),()>
|
||||
{
|
||||
Err(())
|
||||
let play_data :u16 = (history.source as u16) | ((history.from as u16) << 1) | ((history.to as u16) << 7);
|
||||
|
||||
let bucket_index = id & !HANDLE_BUCKET_MASK;
|
||||
let dir_index = id & HANDLE_BUCKET_MASK;
|
||||
|
||||
let bucket_path = Path::new(DIR_SESSION)
|
||||
.join(format!("{:08x}", bucket_index))
|
||||
.join(format!("{:08x}", dir_index));
|
||||
|
||||
// Open session history file
|
||||
if let Ok(mut file) = File::options().read(true).write(true).open(bucket_path.join(GENERIC_HISTORY)) {
|
||||
|
||||
let mut buffer_size = [0u8; 2];
|
||||
|
||||
// Append history information
|
||||
file.seek(SeekFrom::End(0)).map_err(|_| ())?;
|
||||
file.write(&pack_u16(play_data)).map_err(|_| ())?;
|
||||
|
||||
// Update length
|
||||
file.seek(SeekFrom::Start(0)).map_err(|_| ())?;
|
||||
file.read_exact(&mut buffer_size).map_err(|_| ())?;
|
||||
|
||||
file.seek(SeekFrom::Start(0)).map_err(|_| ())?;
|
||||
let size = unpack_u16(&buffer_size, &mut 0);
|
||||
file.write(&pack_u16(size + 1)).map_err(|_| ())?;
|
||||
|
||||
Ok(())
|
||||
} else { Err(()) }
|
||||
}
|
||||
|
||||
pub fn session_history_fetch(&mut self, _id:u32) -> Result<Vec<()>,()>
|
||||
pub fn session_history_fetch(&mut self, id:u32) -> Result<Vec<Play>,()>
|
||||
{
|
||||
Err(())
|
||||
let mut result = Vec::new();
|
||||
|
||||
let bucket_index = id & !HANDLE_BUCKET_MASK;
|
||||
let dir_index = id & HANDLE_BUCKET_MASK;
|
||||
|
||||
let bucket_path = Path::new(DIR_SESSION)
|
||||
.join(format!("{:08x}", bucket_index))
|
||||
.join(format!("{:08x}", dir_index));
|
||||
|
||||
// Open session history file
|
||||
if let Ok(mut file) = File::options().read(true).open(bucket_path.join(GENERIC_HISTORY)) {
|
||||
|
||||
// Read history length
|
||||
let mut buffer = [0u8; 2];
|
||||
file.seek(SeekFrom::Start(0)).map_err(|_| ())?;
|
||||
file.read_exact(&mut buffer).map_err(|_| ())?;
|
||||
let length = unpack_u16(&buffer, &mut 0);
|
||||
|
||||
// Read
|
||||
for _ in 0..length {
|
||||
file.read_exact(&mut buffer).map_err(|_| ())?;
|
||||
let data = unpack_u16(&buffer, &mut 0);
|
||||
result.push(Play {
|
||||
source:(data & 1) as u8,
|
||||
from:((data >> 1) & 0x3F) as u8,
|
||||
to:((data >> 7) & 0x3F) as u8,
|
||||
});
|
||||
}
|
||||
|
||||
println!("len {}", result.len());
|
||||
|
||||
Ok(result)
|
||||
} else { Err(()) }
|
||||
}
|
||||
|
||||
pub fn session_count(&mut self) -> Result<usize, ()>
|
||||
|
240
www/js/game.js
240
www/js/game.js
@ -42,25 +42,33 @@ GAME.Board = class {
|
||||
];
|
||||
|
||||
// Add Dawn pieces
|
||||
for(let piece of layout) {
|
||||
this.set_piece(piece.piece, GAME.Const.Player.Dawn, piece.hex);
|
||||
for(let lay of layout) {
|
||||
this.set_piece(
|
||||
lay.piece,
|
||||
GAME.Const.Player.Dawn,
|
||||
HEX.hex_to_tile(lay.hex)
|
||||
);
|
||||
}
|
||||
|
||||
// Add Dusk pieces
|
||||
for(let piece of layout) {
|
||||
let hex = new MATH.Vec2(8 - piece.hex.x, 8 - piece.hex.y);
|
||||
this.set_piece(piece.piece, GAME.Const.Player.Dusk, hex);
|
||||
for(let lay of layout) {
|
||||
this.set_piece(
|
||||
lay.piece,
|
||||
GAME.Const.Player.Dusk,
|
||||
HEX.hex_to_tile(new MATH.Vec2(8 - lay.hex.x, 8 - lay.hex.y))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
set_piece(piece, player, hex) {
|
||||
set_piece(piece, player, tile) {
|
||||
let index = 0;
|
||||
while(this.pieces[index] !== null) { index++; }
|
||||
|
||||
let game_piece = new GAME.Piece(piece, player);
|
||||
game_piece.tile = HEX.hex_to_tile(hex);
|
||||
this.tiles[game_piece.tile].piece = index;
|
||||
game_piece.tile = tile;
|
||||
this.tiles[tile].piece = index;
|
||||
this.pieces[index] = game_piece;
|
||||
return index;
|
||||
}
|
||||
|
||||
reset() {
|
||||
@ -92,21 +100,23 @@ GAME.Tile = class {
|
||||
this.piece = null;
|
||||
|
||||
this.threaten = [false, false];
|
||||
this.checking = [false, false];
|
||||
this.checking = false;
|
||||
|
||||
this.hex = HEX.tile_to_hex(index);
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.threaten = [0, 0];
|
||||
this.checking = [false, false];
|
||||
this.threaten = [false, false];
|
||||
this.checking = false;
|
||||
}
|
||||
};
|
||||
|
||||
GAME.MovementTile = class {
|
||||
constructor(tile, status) {
|
||||
constructor(tile, valid, threat, check) {
|
||||
this.tile = tile;
|
||||
this.status = status;
|
||||
this.valid = valid;
|
||||
this.threat = threat;
|
||||
this.check = check;
|
||||
}
|
||||
};
|
||||
|
||||
@ -213,24 +223,42 @@ GAME.Game = class {
|
||||
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]); }
|
||||
|
||||
// Get threatened tiles.
|
||||
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;
|
||||
}
|
||||
if(movement.threat) {
|
||||
this.board.tiles[movement.tile].threaten[piece.player] = true;
|
||||
}
|
||||
if(movement.check) {
|
||||
this.board.tiles[piece.tile].checking = true;
|
||||
this.board.tiles[movement.tile].checking = true;
|
||||
this.state.check = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Count moves available to next turn player to determine checkmate.
|
||||
if(this.state.check) {
|
||||
let moves = 0;
|
||||
|
||||
// Search for valid board moves.
|
||||
for(let piece of this.board.pieces) {
|
||||
if(piece !== null && piece.player == (this.turn & 1)) {
|
||||
moves += this.movement_tiles(piece, piece.tile).length;
|
||||
}
|
||||
}
|
||||
|
||||
// Search for valid pool placements.
|
||||
for(let i = 0; i < 6; ++i) {
|
||||
moves += this.placement_tiles(i, (this.turn & 1)).length;
|
||||
}
|
||||
|
||||
console.log("moves " + moves);
|
||||
if(moves == 0) { this.state.checkmate = true; }
|
||||
}
|
||||
}
|
||||
|
||||
process(move) {
|
||||
process(play) {
|
||||
// Check if swapped piece.
|
||||
// Check if piece should be promoted.
|
||||
// Check if swapped piece should be promoted.
|
||||
@ -242,21 +270,23 @@ GAME.Game = class {
|
||||
// - 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 player = this.turn & 1;
|
||||
|
||||
let target_id = this.board.tiles[move.to].piece;
|
||||
// Move piece on board.
|
||||
if(play.source == 0) {
|
||||
let piece_id = this.board.tiles[play.from].piece;
|
||||
let piece = this.board.pieces[piece_id];
|
||||
piece.tile = play.to;
|
||||
this.board.tiles[play.from].piece = null;
|
||||
|
||||
let target_id = this.board.tiles[play.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;
|
||||
target.tile = play.from;
|
||||
this.board.tiles[play.from].piece = target_id;
|
||||
|
||||
// Check if swap is promoted.
|
||||
let hex = HEX.tile_to_hex(target.tile);
|
||||
@ -274,7 +304,7 @@ GAME.Game = class {
|
||||
}
|
||||
}
|
||||
|
||||
this.board.tiles[move.to].piece = piece_id;
|
||||
this.board.tiles[play.to].piece = piece_id;
|
||||
|
||||
// Check if piece is promoted.
|
||||
let hex = HEX.tile_to_hex(piece.tile);
|
||||
@ -286,7 +316,11 @@ GAME.Game = class {
|
||||
|
||||
// Place piece from pool.
|
||||
else {
|
||||
|
||||
this.board.set_piece(
|
||||
play.from,
|
||||
player,
|
||||
play.to
|
||||
);
|
||||
}
|
||||
|
||||
this.turn++;
|
||||
@ -298,11 +332,11 @@ GAME.Game = class {
|
||||
}
|
||||
|
||||
movement_tiles(piece, tile) {
|
||||
let tiles = movement_tiles_ext(piece, tile);
|
||||
let tiles = this.movement_tiles_ext(piece, tile);
|
||||
let result = [ ];
|
||||
|
||||
for(let movement in tiles) {
|
||||
if(movement.status) { result.push(movement); }
|
||||
if(movement.valid) { result.push(movement); }
|
||||
}
|
||||
|
||||
return result;
|
||||
@ -330,7 +364,7 @@ GAME.Game = class {
|
||||
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 valid = (permitted_moves & mask) != 0;
|
||||
|
||||
let move_hex = hex.copy();
|
||||
|
||||
@ -339,15 +373,22 @@ GAME.Game = class {
|
||||
for(let dist = 1; dist <= max_dist; ++dist) {
|
||||
move_hex.add(direction);
|
||||
|
||||
let threat = valid;
|
||||
let check = false;
|
||||
|
||||
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;
|
||||
let result = valid;
|
||||
|
||||
if(piece.player == (this.turn & 1) && this.state.check && !tile_data.checking) {
|
||||
result = false;
|
||||
}
|
||||
|
||||
// King may not move onto threatened tile.
|
||||
if(piece.piece == GAME.Const.PieceId.Omen && tile_data.threaten[+(!piece.player)]) {
|
||||
status = GAME.Const.MoveStatus.Invalid;
|
||||
valid = false;
|
||||
}
|
||||
|
||||
// Handle occupied tile.
|
||||
@ -358,25 +399,30 @@ GAME.Game = class {
|
||||
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;
|
||||
if(!this.movement_swappable(piece, target, mask, dist, tile)) {
|
||||
result = false;
|
||||
}
|
||||
status = GAME.Const.MoveStatus.Invalid;
|
||||
valid = false;
|
||||
}
|
||||
|
||||
// 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;
|
||||
if(valid) {
|
||||
check = true;
|
||||
|
||||
// Apply check to previous moves.
|
||||
for(let idist = 1; idist < dist; idist++) {
|
||||
tiles[tiles.length - idist].check = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
status = GAME.Const.MoveStatus.Invalid;
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
|
||||
tiles.push(new GAME.MovementTile(tile_id, result));
|
||||
tiles.push(new GAME.MovementTile(tile_id, result, threat, check));
|
||||
} else { break; }
|
||||
}
|
||||
|
||||
@ -386,7 +432,11 @@ GAME.Game = class {
|
||||
return tiles;
|
||||
}
|
||||
|
||||
movement_swappable(target, mask, range, tile) {
|
||||
movement_swappable(piece, target, mask, range, tile) {
|
||||
if(piece.piece == target.piece && piece.promoted == target.promoted) {
|
||||
return false;
|
||||
}
|
||||
|
||||
mask = BITWISE.rotate_blocks(mask);
|
||||
let moves = target.moves();
|
||||
|
||||
@ -398,6 +448,17 @@ GAME.Game = class {
|
||||
return ((moves.direction & mask) != 0 && (range == 1 || (moves.stride & mask) != 0));
|
||||
}
|
||||
|
||||
placement_tiles(piece_id, player) {
|
||||
let tiles = this.placement_tiles_ext(piece_id, player);
|
||||
let result = [ ];
|
||||
|
||||
for(let movement in tiles) {
|
||||
if(movement.valid) { result.push(movement); }
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
placement_tiles_ext(piece_id, player) {
|
||||
let tiles = [ ];
|
||||
let piece = new GAME.Piece(piece_id, player);
|
||||
@ -406,17 +467,21 @@ GAME.Game = class {
|
||||
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;
|
||||
let valid = false;
|
||||
|
||||
// Check if tile is occupied.
|
||||
if(tile.piece === null) {
|
||||
let position_valid = true;
|
||||
|
||||
if(player == (this.turn & 1) && this.state.check && !tile.checking) {
|
||||
position_valid = false;
|
||||
}
|
||||
|
||||
// Check off-sides.
|
||||
if(piece.player == 0) {
|
||||
position_valid = hex.y <= this.board.columns[hex.x].extent[+(!player)];
|
||||
position_valid = position_valid && (hex.y <= this.board.columns[hex.x].extent[+(!player)]);
|
||||
} else {
|
||||
position_valid = hex.y >= this.board.columns[hex.x].extent[+(!player)];
|
||||
position_valid = position_valid && (hex.y >= this.board.columns[hex.x].extent[+(!player)]);
|
||||
}
|
||||
|
||||
// Check militia stacking.
|
||||
@ -428,7 +493,7 @@ GAME.Game = class {
|
||||
let checking = false;
|
||||
let movements = this.movement_tiles_ext(piece, i);
|
||||
for(let movement of movements) {
|
||||
if(movement.status == GAME.Const.MoveStatus.Check) {
|
||||
if(movement.check) {
|
||||
checking = true;
|
||||
break;
|
||||
}
|
||||
@ -436,11 +501,11 @@ GAME.Game = class {
|
||||
|
||||
// Piece must have movements and not put king in check.
|
||||
if(position_valid && movements.length > 0 && !checking) {
|
||||
status = GAME.Const.MoveStatus.Valid;
|
||||
valid = true;
|
||||
}
|
||||
}
|
||||
|
||||
tiles.push(new GAME.MovementTile(i, status));
|
||||
tiles.push(new GAME.MovementTile(i, valid, false, false));
|
||||
}
|
||||
|
||||
return tiles;
|
||||
@ -472,6 +537,19 @@ GAME.Const = {
|
||||
new MATH.Vec2(-1, -2),
|
||||
new MATH.Vec2(-2, -1),
|
||||
new MATH.Vec2(-1, 1),
|
||||
|
||||
new MATH.Vec2(1, 3),
|
||||
new MATH.Vec2(2, 3),
|
||||
new MATH.Vec2(3, 2),
|
||||
new MATH.Vec2(3, 1),
|
||||
new MATH.Vec2(2, -1),
|
||||
new MATH.Vec2(1, -2),
|
||||
new MATH.Vec2(-1, -3),
|
||||
new MATH.Vec2(-2, -3),
|
||||
new MATH.Vec2(-3, -2),
|
||||
new MATH.Vec2(-3, -1),
|
||||
new MATH.Vec2(-2, 1),
|
||||
new MATH.Vec2(-1, 2),
|
||||
],
|
||||
|
||||
MoveStatus: {
|
||||
@ -501,27 +579,41 @@ GAME.Const = {
|
||||
.add(0)
|
||||
.add(1)
|
||||
.add(2)
|
||||
.add(3)
|
||||
.add(4)
|
||||
.add(5),
|
||||
),
|
||||
new GAME.GamePiece(
|
||||
"Knight",
|
||||
new GAME.PieceMovement()
|
||||
.add(3)
|
||||
.add(6)
|
||||
.add(11)
|
||||
.add(13)
|
||||
.add(17),
|
||||
new GAME.PieceMovement()
|
||||
.add(3)
|
||||
.add(6)
|
||||
.add(7)
|
||||
.add(10)
|
||||
.add(11)
|
||||
.add(12)
|
||||
.add(13)
|
||||
.add(14)
|
||||
.add(15)
|
||||
.add(16)
|
||||
.add(17),
|
||||
.add(17)
|
||||
.add(18)
|
||||
.add(19)
|
||||
.add(20)
|
||||
.add(21)
|
||||
.add(22)
|
||||
.add(23),
|
||||
new GAME.PieceMovement()
|
||||
.add(1)
|
||||
.add(3)
|
||||
.add(5)
|
||||
.add(12)
|
||||
.add(13)
|
||||
.add(14)
|
||||
.add(15)
|
||||
.add(16)
|
||||
.add(17)
|
||||
.add(18)
|
||||
.add(19)
|
||||
.add(20)
|
||||
.add(21)
|
||||
.add(22)
|
||||
.add(23),
|
||||
),
|
||||
new GAME.GamePiece(
|
||||
"Lance",
|
||||
@ -530,12 +622,12 @@ GAME.Const = {
|
||||
.add(1)
|
||||
.add(5),
|
||||
new GAME.PieceMovement()
|
||||
.add_stride(0)
|
||||
.add_stride(1)
|
||||
.add_stride(2)
|
||||
.add_stride(3)
|
||||
.add_stride(4)
|
||||
.add_stride(5),
|
||||
.add(0)
|
||||
.add(1)
|
||||
.add(2)
|
||||
.add(3)
|
||||
.add(4)
|
||||
.add(5),
|
||||
),
|
||||
new GAME.GamePiece(
|
||||
"Tower",
|
||||
@ -588,12 +680,12 @@ GAME.Const = {
|
||||
.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(0)
|
||||
.add(1)
|
||||
.add(2)
|
||||
.add(3)
|
||||
.add(4)
|
||||
.add(5)
|
||||
.add_stride(6)
|
||||
.add_stride(7)
|
||||
.add_stride(8)
|
||||
@ -616,8 +708,8 @@ GAME.Const = {
|
||||
],
|
||||
|
||||
get_direction(direction_id) {
|
||||
let direction = GAME.Const.Direction[direction_id % 12].copy();
|
||||
direction.mul(Math.ceil((direction_id + 1) / 12));
|
||||
let direction = GAME.Const.Direction[direction_id % 24].copy();
|
||||
direction.mul(Math.ceil((direction_id + 1) / 24));
|
||||
return direction;
|
||||
},
|
||||
};
|
||||
|
@ -74,7 +74,7 @@ const INTERFACE = {
|
||||
if(movements !== null) {
|
||||
// Generate hint for each potential movement.
|
||||
for(let movement of movements) {
|
||||
if(movement.status != GAME.Const.MoveStatus.Invalid) {
|
||||
if(movement.valid) {
|
||||
// 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)]) {
|
||||
@ -309,6 +309,9 @@ const INTERFACE = {
|
||||
ctx.save();
|
||||
ctx.translate(gui_x, gui_y);
|
||||
|
||||
let piece = null;
|
||||
if(tile.piece !== null) { piece = GAME_DATA.board.pieces[tile.piece]; }
|
||||
|
||||
// Draw background.
|
||||
// Select indicator color or default to tile color.
|
||||
switch(MATH.mod(coord.x + coord.y, 3)) {
|
||||
@ -316,6 +319,9 @@ const INTERFACE = {
|
||||
case 1: ctx.fillStyle = INTERFACE.Color.TileLight; break;
|
||||
case 2: ctx.fillStyle = INTERFACE.Color.TileDark; break;
|
||||
}
|
||||
if(GAME_DATA.state.check && piece !== null && piece.piece == GAME.Const.PieceId.Omen && piece.player == (GAME_DATA.turn & 1)) {
|
||||
ctx.fillStyle = INTERFACE.Color.HintCheck;
|
||||
}
|
||||
switch(tile_state) {
|
||||
case INTERFACE.TileStatus.Valid: ctx.fillStyle = INTERFACE.Color.HintValid; break;
|
||||
case INTERFACE.TileStatus.Threat: ctx.fillStyle = INTERFACE.Color.HintThreat; break;
|
||||
@ -349,9 +355,7 @@ const INTERFACE = {
|
||||
ctx.stroke();
|
||||
|
||||
// Draw tile content
|
||||
if(tile.piece !== null) {
|
||||
let piece = GAME_DATA.board.pieces[tile.piece];
|
||||
|
||||
if(piece !== null) {
|
||||
// Draw border hints
|
||||
if(!is_hover && border_state == 0) { draw.hints(piece); }
|
||||
|
||||
@ -519,11 +523,22 @@ const INTERFACE = {
|
||||
ctx.fillText(GAME_DATA.turn, width - gui_margin, gui_margin);
|
||||
|
||||
// Game state message
|
||||
if(INTERFACE_DATA.message !== null) {
|
||||
ctx.fillStyle = INTERFACE.Color.Text;
|
||||
let message = null;
|
||||
ctx.fillStyle = INTERFACE.Color.Text;
|
||||
|
||||
if(GAME_DATA.state.check) {
|
||||
ctx.fillStyle = INTERFACE.Color.HintCheck;
|
||||
if(GAME_DATA.state.checkmate) {
|
||||
message = "Checkmate";
|
||||
} else {
|
||||
message = "Check";
|
||||
}
|
||||
}
|
||||
|
||||
if(message !== null) {
|
||||
ctx.textBaseline = "bottom";
|
||||
ctx.textAlign = "left";
|
||||
ctx.fillText(INTERFACE_DATA.message, gui_margin, height - gui_margin);
|
||||
ctx.fillText(message, gui_margin, height - gui_margin);
|
||||
}
|
||||
},
|
||||
|
||||
@ -653,8 +668,6 @@ const INTERFACE = {
|
||||
handles: [null, null],
|
||||
board_state: [ ],
|
||||
|
||||
message: null,
|
||||
|
||||
Ui: {
|
||||
scale: 0,
|
||||
margin: 0,
|
||||
@ -718,7 +731,7 @@ const INTERFACE = {
|
||||
} break;
|
||||
|
||||
case OpCode.GamePlay: {
|
||||
if(data.status == Status.Ok) {
|
||||
if(data.status == Status.Ok && data.turn == GAME_DATA.turn) {
|
||||
GAME_DATA.process(data.move);
|
||||
INTERFACE.draw();
|
||||
}
|
||||
|
@ -277,6 +277,7 @@ const SCENES = {
|
||||
table.appendChild(UI.session_table(data.records));
|
||||
}
|
||||
} break;
|
||||
case OpCode.SessionCreate:
|
||||
case OpCode.SessionJoin: {
|
||||
if(data.status == Status.Ok) {
|
||||
LOAD(SCENES.Game, data);
|
||||
@ -335,6 +336,7 @@ const SCENES = {
|
||||
table.appendChild(UI.session_table(data.records));
|
||||
}
|
||||
} break;
|
||||
case OpCode.SessionCreate:
|
||||
case OpCode.SessionJoin: {
|
||||
if(data.status == Status.Ok) {
|
||||
LOAD(SCENES.Game, data);
|
||||
@ -393,6 +395,7 @@ const SCENES = {
|
||||
table.appendChild(UI.session_table(data.records));
|
||||
}
|
||||
} break;
|
||||
case OpCode.SessionCreate:
|
||||
case OpCode.SessionJoin: {
|
||||
if(data.status == Status.Ok) {
|
||||
LOAD(SCENES.Game, data);
|
||||
|
@ -133,15 +133,15 @@ const BITWISE = {
|
||||
{
|
||||
const r1 = 0x00003F; // first 6 bits
|
||||
const r2 = 0x000FC0; // second 6 bits
|
||||
const r3 = 0x03F000; // third 6 bits
|
||||
const r3 = 0xFFF000; // third 12 bits
|
||||
|
||||
let v1 = (r1 & mask) << 3;
|
||||
let v2 = (r2 & mask) << 3;
|
||||
let v3 = (r3 & mask) << 3;
|
||||
let v3 = (r3 & mask) << 6;
|
||||
|
||||
v1 = (v1 & r1) | ((v1 & ~r1) >> 6);
|
||||
v2 = (v2 & r2) | ((v2 & ~r2) >> 6);
|
||||
v3 = (v3 & r3) | ((v3 & ~r3) >> 6);
|
||||
v3 = (v3 & r3) | ((v3 & ~r3) >> 12);
|
||||
|
||||
return v1 | v2 | v3;
|
||||
},
|
||||
|
Loading…
x
Reference in New Issue
Block a user