Implement server-side move validation.

This commit is contained in:
yukirij 2024-10-11 18:38:12 -07:00
parent 180a782eff
commit 30eaa81d1b
22 changed files with 1248 additions and 290 deletions

View File

@ -1,5 +1,5 @@
use crate::{
consts::*,
constant::*,
piece::*,
util::Hex,
};
@ -87,7 +87,7 @@ impl Board {
(Piece::new(PIECE_DRAGON, PLAYER_DAWN), Hex::from_hex(4, 2)),
(Piece::new(PIECE_BEHEMOTH, PLAYER_DAWN), Hex::from_hex(4, 1)),
(Piece::new(PIECE_SOURCE, PLAYER_DAWN), Hex::from_hex(4, 0)),
(Piece::new(PIECE_HEART, PLAYER_DAWN), Hex::from_hex(4, 0)),
];
for (piece, hex) in &layout {

4
game/src/config/mod.rs Normal file
View File

@ -0,0 +1,4 @@
#[derive(Clone)]
pub struct GameConfig {
}

View File

@ -11,4 +11,27 @@ pub const PIECE_TOWER :u8 = 3;
pub const PIECE_CASTLE :u8 = 4;
pub const PIECE_DRAGON :u8 = 5;
pub const PIECE_BEHEMOTH :u8 = 6;
pub const PIECE_SOURCE :u8 = 7;
pub const PIECE_HEART :u8 = 7;
pub mod flags {
pub const NONE :u32 = 0x00;
pub const IGNORE_EXTENT :u32 = 0x01;
pub const IGNORE_CHECK :u32 = 0x02;
pub const IGNORE_STACKING :u32 = 0x04;
}
pub const DIRECTIONS :&[(i8, i8)] = &[
(0, 1),
(1, 1),
(1, 0),
(0, -1),
(-1, -1),
(-1, 0),
(1, 2),
(2, 1),
(1, -1),
(-1, -2),
(-2, -1),
(-1, 1),
];

View File

@ -0,0 +1,68 @@
#[derive(Clone, Copy)]
pub struct CheckState {
data:u8,
}
impl CheckState {
pub fn new() -> Self
{
Self {
data:0,
}
}
pub fn immediate(mut self) -> Self
{
self.data |= 0x80;
self
}
pub fn direct(mut self) -> Self
{
self.data += 1;
self.data |= 0x40;
self
}
pub fn stride(mut self) -> Self
{
self.data += 1;
self.data |= 0x20;
self
}
pub fn apply(&mut self, other:Self)
{
self.data += other.data & 0x1F;
self.data |= other.data & 0xC0;
}
pub fn reset(&mut self)
{
self.data = 0;
}
pub fn is_check(&self) -> bool
{
self.data != 0
}
pub fn is_immediate(&self) -> bool
{
(self.data & 0x80) != 0
}
pub fn is_direct(&self) -> bool
{
(self.data & 0x40) != 0
}
pub fn is_stride(&self) -> bool
{
(self.data & 0x20) != 0
}
pub fn count(&self) -> usize
{
(self.data & 0x1F) as usize
}
}

View File

@ -1,172 +1 @@
use crate::{
//consts::*,
board::Board,
history::Play,
piece::Piece, util::Hex,
};
#[derive(Clone, Copy)]
enum GameStatus {
Normal,
Check(u32),
Checkmate,
Resign,
}
pub type Pool = [u8; 7];
#[derive(Clone)]
pub struct Game {
status:GameStatus,
pub turn:u16,
pub board:Board,
pub pool:[Pool; 2],
pub history:Vec<Play>,
}
impl Game {
pub fn new() -> Self
{
Self {
status:GameStatus::Normal,
turn:0,
board:Board::new(),
pool:[Pool::default(); 2],
history:Vec::new(),
}
}
pub fn init(&mut self)
{
*self = Self::new();
self.board.init();
}
pub fn apply_history(&mut self, history:&Vec<Play>) -> Result<(),()>
{
self.init();
for play in history {
self.process(play).ok();
}
Ok(())
}
pub fn process(&mut self, play:&Play) -> Result<(),()>
{
let player = (self.turn & 1) as u8;
match self.status {
GameStatus::Checkmate | GameStatus::Resign => { return Err(()); }
_ => { }
}
let valid = true;
//
// TODO:
// - Check for piece promotion.
// - Validate plays against move sets and specific rules.
// - Determine game state (check, checkmate).
//
if valid {
// Move piece on board.
if match play.source {
0 | 2 => {
if let Some(pid) = self.board.tiles[play.from as usize].piece {
if let Some(mut piece) = self.board.pieces[pid as usize] {
let mut swap = false;
if let Some(tid) = self.board.tiles[play.to as usize].piece {
if let Some(target) = &mut self.board.pieces[tid as usize] {
// Check for piece swap.
if piece.player == target.player {
swap = true;
target.tile = play.from;
// Check for target promotion.
let hex = Hex::from_tile(play.from);
if !target.promoted && target.has_promotion() && Hex::is_back(hex.x, hex.y, target.player) {
target.promoted = true;
}
}
// Add captured piece to pool.
else {
self.pool[piece.player as usize][target.class as usize] += 1;
}
}
// Destroy piece if captured.
if !swap { self.board.pieces[tid as usize] = None; }
}
// Set tile/piece associations.
if swap {
self.board.tiles[play.from as usize].piece = self.board.tiles[play.to as usize].piece;
} else {
self.board.tiles[play.from as usize].piece = None;
}
self.board.tiles[play.to as usize].piece = Some(pid);
piece.tile = play.to;
self.board.pieces[pid as usize] = Some(piece);
// Check for piece promotion.
let hex = Hex::from_tile(play.to);
if !piece.promoted && piece.has_promotion() && Hex::is_back(hex.x, hex.y, piece.player) {
if let Some(piece) = &mut self.board.pieces[pid as usize] {
piece.promoted = true;
}
}
self.turn += 1;
true
} else { false }
} else { false }
}
// Place piece from pool.
1 => {
if self.pool[player as usize][play.from as usize] > 0 && self.board.tiles[play.to as usize].piece.is_none() {
self.pool[player as usize][play.from as usize] -= 1;
let piece = Piece::new(play.from, player);
self.board.set_piece(piece, play.to);
self.turn += 1;
true
} else { false }
}
// Player retired.
0xF => {
self.status = GameStatus::Resign;
true
}
_ => false,
} {
self.history.push(*play);
Ok(())
} else { Err(()) }
} else { Err(()) }
}
fn get_moves(_piece:&Piece) -> Vec<Play>
{
Vec::new()
}
pub fn is_complete(&self) -> bool
{
match self.status {
GameStatus::Checkmate | GameStatus::Resign => true,
_ => false,
}
}
}
mod checkstate; pub use checkstate::CheckState;

View File

@ -1,3 +1,5 @@
use crate::CheckState;
#[derive(Clone, Copy)]
pub struct Play {
pub source:u8,
@ -13,4 +15,38 @@ impl Play {
to:0,
}
}
pub fn from_move(from:u8, to:u8) -> Self
{
Self {
source:0,
from,
to,
}
}
pub fn from_alt(from:u8, to:u8) -> Self
{
Self {
source:2,
from,
to,
}
}
pub fn from_drop(piece:u8, tile:u8) -> Self
{
Self {
source:1,
from:piece,
to:tile,
}
}
}
#[derive(Clone, Copy)]
pub struct PlayInfo {
pub play:Play,
pub check:CheckState,
pub blocking:u32,
}

View File

@ -1,8 +1,713 @@
#![allow(dead_code)]
pub mod consts;
pub mod util;
pub mod piece;
pub mod board;
pub mod history;
pub mod game; pub use game::Game;
pub mod constant; use constant::*;
pub mod util; use util::Hex;
pub mod piece; use piece::Piece;
pub mod board; use board::Board;
pub mod history; use history::*;
pub mod config; use config::GameConfig;
mod game; use game::*;
#[derive(Clone, Copy)]
pub enum GameStatus {
Normal,
Check(CheckState),
Checkmate,
Resign,
}
pub type Pool = [u8; 7];
#[derive(Clone)]
pub struct Game {
pub config:GameConfig,
status:GameStatus,
pub turn:u16,
pub board:Board,
pub pool:[Pool; 2],
pub history:Vec<Play>,
}
impl Game {
pub fn new() -> Self
{
Self {
config:GameConfig { },
status:GameStatus::Normal,
turn:0,
board:Board::new(),
pool:[Pool::default(); 2],
history:Vec::new(),
}
}
pub fn init(&mut self)
{
*self = Self::new();
self.board.init();
}
pub fn apply_history(&mut self, history:&Vec<Play>) -> Result<(),()>
{
self.init();
for play in history {
self.process(play).ok();
}
Ok(())
}
pub fn update_board(&mut self)
{
let mut player_moves = 0usize;
let current_player = (self.turn & 1) as u8;
/*
** Reset board meta data.
*/
self.status = GameStatus::Normal;
// Reset columns
for column in &mut self.board.columns {
column.extent = [0, 9];
column.militia = [false; 2];
}
// Reset tiles
for tile in &mut self.board.tiles {
tile.check = false;
tile.threat = [false; 2];
}
// Reset pieces
for piece in &mut self.board.pieces {
if let Some(piece) = piece {
piece.blocking = 0;
}
}
/*
** Fill board meta data; count piece moves.
*/
for piece in &self.board.pieces.clone() {
if let Some(piece) = piece {
let moves = self.get_moves_data(&piece);
let alt_moves = self.get_alts_data(&piece);
/*
** Mark threats, checks, and extents.
*/
for info in &moves {
let hex = Hex::from_tile(info.play.to);
/*
** Mark tile as threatened.
*/
self.board.tiles[info.play.to as usize].threat[piece.player as usize] = true;
/*
** Apply checks.
*/
if info.check.is_check() {
if info.check.is_immediate() {
if let GameStatus::Check(data) = &mut self.status {
data.apply(info.check);
} else {
self.status = GameStatus::Check(info.check);
}
// Mark checking piece's tile.
self.board.tiles[info.play.from as usize].check = true;
}
// Mark tile on checking path.
self.board.tiles[info.play.to as usize].check = true;
}
/*
** Apply column extent.
*/
self.board.columns[hex.x as usize].extent[piece.player as usize] = hex.y as u8;
/*
** Mark column as having unpromoted militia, if present.
*/
if piece.class == PIECE_MILITIA && !piece.promoted {
self.board.columns[hex.x as usize].militia[piece.player as usize] = true;
}
/*
** Count moves for current player.
*/
if piece.player == current_player {
player_moves += moves.len() + alt_moves.len();
}
/*
** Apply blocking to piece on tile, if present.
*/
if info.blocking != 0 {
if let Some(piece_id) = self.board.tiles[info.play.to as usize].piece {
if let Some(piece) = &mut self.board.pieces[piece_id as usize] {
piece.blocking = info.blocking;
}
}
}
}
}
}
/*
** Determine if game is in checkmate.
*/
if player_moves == 0 {
self.status = GameStatus::Checkmate;
}
}
pub fn process(&mut self, play:&Play) -> Result<(),()>
{
let player = (self.turn & 1) as u8;
if self.play_is_valid(play) {
// Move piece on board.
if match play.source {
0 | 2 => {
if let Some(pid) = self.board.tiles[play.from as usize].piece {
if let Some(mut piece) = self.board.pieces[pid as usize] {
let mut swap = false;
if let Some(tid) = self.board.tiles[play.to as usize].piece {
if let Some(target) = &mut self.board.pieces[tid as usize] {
// Check for piece swap.
if piece.player == target.player {
swap = true;
target.tile = play.from;
// Check for target promotion.
let hex = Hex::from_tile(play.from);
if !target.promoted && target.has_promotion() && Hex::is_back(hex.x, hex.y, target.player) {
target.promoted = true;
}
}
// Add captured piece to pool.
else {
self.pool[piece.player as usize][target.class as usize] += 1;
}
}
// Destroy piece if captured.
if !swap { self.board.pieces[tid as usize] = None; }
}
// Set tile/piece associations.
if swap {
self.board.tiles[play.from as usize].piece = self.board.tiles[play.to as usize].piece;
} else {
self.board.tiles[play.from as usize].piece = None;
}
self.board.tiles[play.to as usize].piece = Some(pid);
piece.tile = play.to;
self.board.pieces[pid as usize] = Some(piece);
// Check for piece promotion.
let hex = Hex::from_tile(play.to);
if !piece.promoted && piece.has_promotion() && Hex::is_back(hex.x, hex.y, piece.player) {
if let Some(piece) = &mut self.board.pieces[pid as usize] {
piece.promoted = true;
}
}
self.turn += 1;
true
} else { false }
} else { false }
}
// Place piece from pool.
1 => {
if self.pool[player as usize][play.from as usize] > 0 && self.board.tiles[play.to as usize].piece.is_none() {
self.pool[player as usize][play.from as usize] -= 1;
let piece = Piece::new(play.from, player);
self.board.set_piece(piece, play.to);
self.turn += 1;
true
} else { false }
}
// Player retired.
0xF => {
self.status = GameStatus::Resign;
true
}
_ => false,
} {
self.history.push(*play);
Ok(())
} else { Err(()) }
} else { Err(()) }
}
pub fn play_is_valid(&self, play:&Play) -> bool
// Returns whether a play may be made by the current player.
//
{
//println!("play_is_valid {} {} {}", play.source, play.from, play.to);
let mut valid = false;
let player = (self.turn & 1) as u8;
match play.source {
0 => {
if let Some(piece_id) = self.board.tiles[play.from as usize].piece {
if let Some(piece) = &self.board.pieces[piece_id as usize] {
//println!("piece {} {}", piece.class, piece.player);
if piece.player == player {
for p in self.get_moves(piece) {
if p.to == play.to {
valid = true;
break;
}
}
}
}
}
valid
}
1 => {
if (play.from as usize) < self.pool[player as usize].len() {
if self.pool[player as usize][play.from as usize] > 0 {
for p in self.get_drops(&Piece::new(play.from, player)) {
if p.to == play.to {
valid = true;
break;
}
}
}
}
valid
}
2 => {
if let Some(piece_id) = self.board.tiles[play.from as usize].piece {
if let Some(piece) = &self.board.pieces[piece_id as usize] {
if piece.player == player {
for p in self.get_alts(piece) {
if p.to == play.to {
valid = true;
break;
}
}
}
}
}
valid
}
0xF => { true }
_ => false,
}
}
fn get_moves_data(&self, piece:&Piece) -> Vec<PlayInfo>
{
let mut plays = Vec::<PlayInfo>::new();
let moves = piece.moves();
/*
** Get permitted move directions.
*/
let mut directions = moves.direction;
let blocking_directions = piece.blocking | util::rotate6(piece.blocking);
if blocking_directions != 0 {
if piece.class == PIECE_HEART {
directions &= !blocking_directions;
} else {
directions &= blocking_directions;
}
}
//println!("dir {:b}", directions);
//println!("blk {:b}", blocking_directions);
/*
** Handle movable directions.
*/
while directions != 0 {
let mask_original = util::lsb(directions);
let mut multiplier = 1;
let mut mask = mask_original;
/*
** Shift second tile jumps to standard direction masks.
*/
if (mask & 0xFFF) == 0 {
mask >>= 12;
multiplier = 2;
}
let direction = util::ffs(mask);
//println!(" - dir {}", direction);
/*
** Extract stride value:
** 00 - 1 Tile
** 01 - 2 Tiles
** 10 - Reserved
** 11 - Unlimited
*/
let stride_offset: u32 = direction * 2;
let stride = if direction < 12 {
match ((3 << stride_offset) & moves.stride) >> stride_offset {
0 => 1,
1 => 2,
_ => 8,
}
} else { 1 };
let (mut rx, mut ry) = DIRECTIONS[direction as usize];
rx *= multiplier;
ry *= multiplier;
let mut block_count = 0;
/*
** Step along direction up to max stride.
*/
let mut current_hex = Hex::from_tile(piece.tile);
for stride_dist in 0..stride {
if Hex::is_valid(current_hex.x + rx, current_hex.y + ry) {
current_hex = Hex::from_hex(current_hex.x + rx, current_hex.y + ry);
let mut valid = block_count == 0;
let tile = self.board.tiles[current_hex.tile as usize];
let mut checkstate = CheckState::new();
/*
** If in check, move must break check.
** King may only move to non-threatened tiles.
*/
if piece.class == PIECE_HEART {
valid = valid && !tile.threat[(piece.player == 0) as usize];
} else {
valid = valid && match self.status {
GameStatus::Check(state) => {
state.count() == 1 && tile.check
}
GameStatus::Normal => true,
_ => false,
};
}
/*
** If tile is occupied, piece must be opposing or swappable.
*/
if let Some(target_id) = tile.piece {
if let Some(target) = &self.board.pieces[target_id as usize] {
if target.player == piece.player {
/*
** Move is valid if piece can swap.
*/
valid = valid && stride_dist == 0 && self.can_swap(piece, mask_original, current_hex.tile);
block_count += 2;
} else {
/*
** Handle capturing of king.
*/
if target.class == PIECE_HEART {
if stride_dist == 0 {
checkstate = checkstate.direct();
} else {
checkstate = checkstate.stride();
}
match block_count {
0 => {
// Mark plays in direction as check.
for i in 1..stride_dist {
let index = plays.len() - i;
plays[index].check = checkstate;
}
}
1 => {
// Mark last piece as blocking.
let index = plays.len() - 1;
plays[index].blocking = mask;
}
_ => { }
}
}
block_count += 1;
}
}
}
if valid {
plays.push(PlayInfo {
play:Play { source:0, from:piece.tile, to:current_hex.tile, },
check:checkstate,
blocking:0,
});
}
}
}
directions &= !mask_original;
}
plays
}
pub fn get_moves(&self, piece:&Piece) -> Vec<Play>
{
let mut plays = Vec::new();
for info in self.get_moves_data(piece) {
plays.push(info.play);
}
plays
}
fn get_alts_data(&self, piece:&Piece) -> Vec<PlayInfo>
{
let mut plays = Vec::<PlayInfo>::new();
let piece_moves = piece.moves();
let subject_tiles = if piece.blocking == 0 {
(0..61u8).collect()
} else {
let mut tiles = Vec::new();
let mut directions = piece.blocking | util::rotate6(piece.blocking);
while directions != 0 {
let mask = util::lsb(directions);
let direction = util::ffs(mask);
let (rx, ry) = DIRECTIONS[direction as usize];
let mut current_hex = Hex::from_tile(piece.tile);
for _ in 0..8 {
if Hex::is_valid(current_hex.x + rx, current_hex.y + ry) {
current_hex = Hex::from_hex(current_hex.x + rx, current_hex.y + ry);
if self.board.tiles[current_hex.tile as usize].piece.is_none() {
tiles.push(current_hex.tile);
} else { break }
} else { break }
}
directions &= !mask;
}
tiles
};
if let Some(alt_mode) = piece_moves.alt {
match alt_mode {
// Knight
1 => {
for tile_id in subject_tiles {
if self.can_drop(piece, tile_id, flags::IGNORE_CHECK) {
plays.push(PlayInfo {
play: Play::from_alt(piece.tile, tile_id),
check: CheckState::new(),
blocking: 0,
});
}
}
}
// Caslte
2 => {
for tile_id in subject_tiles {
let hex = Hex::from_tile(piece.tile);
let tile_hex = Hex::from_tile(tile_id);
let in_rear_cone = if piece.player == 0 {
if tile_hex.x >= hex.x {
tile_hex.y <= hex.y
} else {
tile_hex.y <= hex.y - (hex.x - tile_hex.x)
}
} else {
if tile_hex.x >= hex.x {
tile_hex.y >= hex.y + (tile_hex.x - hex.x)
} else {
tile_hex.y >= hex.y
}
};
if in_rear_cone && self.can_drop(piece, tile_id, flags::IGNORE_CHECK | flags::IGNORE_EXTENT) {
plays.push(PlayInfo {
play: Play::from_alt(piece.tile, tile_id),
check: CheckState::new(),
blocking: 0,
});
}
}
}
_ => { }
}
}
plays
}
pub fn get_alts(&self, piece:&Piece) -> Vec<Play>
{
let mut plays = Vec::new();
for info in self.get_alts_data(piece) {
plays.push(info.play);
}
plays
}
pub fn get_drops(&self, piece:&Piece) -> Vec<Play>
{
let mut piece = piece.clone();
let mut moves = Vec::new();
for tile_id in 0..self.board.tiles.len() {
let tile = &self.board.tiles[tile_id];
if tile.piece.is_none() {
piece.tile = tile_id as u8;
if self.can_drop(&piece, tile_id as u8, flags::NONE) {
moves.push(Play::from_drop(piece.class, tile_id as u8));
}
}
}
moves
}
fn can_swap(&self, piece:&Piece, mask:u32, tile_id:u8) -> bool
{
let mut valid = false;
if let Some(target_id) = self.board.tiles[tile_id as usize].piece {
let target_id = target_id as usize;
if let Some(target) = self.board.pieces[target_id as usize] {
valid = true;
/*
** Target must be same color.
*/
valid = valid && piece.player == target.player;
/*
** Target must not be same piece.
*/
valid = valid && !(piece.class == target.class && piece.promoted == target.promoted);
/*
** King may not swap onto a contested tile.
*/
if target.class == PIECE_HEART
&& self.board.tiles[piece.tile as usize].threat[(piece.player == 0) as usize]
{
valid = false;
}
/*
** Target must have movement in reverse direction.
*/
let moves = target.moves().rotate().direction;
valid = valid && (mask & moves) != 0;
}
}
valid
}
fn can_drop(&self, piece:&Piece, tile_id:u8, flags:u32) -> bool
{
let hex = Hex::from_tile(tile_id);
let tile = &self.board.tiles[tile_id as usize];
/*
** Tile must not be occupied.
*/
let mut valid = tile.piece.is_none();
/*
** If in check, a piece may only be dropped if check
** is due to a multi-tile move from a single piece.
*/
valid = valid && match self.status {
GameStatus::Check(state) => {
!state.is_direct() && state.count() == 1 && tile.check
}
GameStatus::Normal => true,
_ => false,
};
/*
** A piece may not be dropped behind the first opposing piece in a column.
*/
if (flags & flags::IGNORE_EXTENT as u32) == 0 {
valid = valid && if piece.player == 0 {
hex.y > self.board.columns[hex.x as usize].extent[1] as i8
} else {
hex.y < self.board.columns[hex.x as usize].extent[0] as i8
};
}
/*
** Militia may not be dropped in a column with another militia present.
*/
if (flags & flags::IGNORE_STACKING as u32) == 0 {
if piece.class == PIECE_MILITIA
&& self.board.columns[hex.x as usize].militia[piece.player as usize]
{
valid = false;
}
}
/*
** A piece must be able to move from its drop position.
** A piece may not be dropped onto a position that puts the opposing king in check.
*/
let piece_moves = self.get_moves(piece);
if piece_moves.len() > 0 {
if (flags & flags::IGNORE_CHECK as u32) == 0 {
for mv in piece_moves {
if let Some(target_id) = self.board.tiles[mv.to as usize].piece {
if let Some(target) = self.board.pieces[target_id as usize] {
if target.class == PIECE_HEART && target.player != piece.player {
valid = false;
break;
}
}
}
}
}
} else {
valid = false;
}
valid
}
pub fn is_complete(&self) -> bool
{
match self.status {
GameStatus::Checkmate | GameStatus::Resign => true,
_ => false,
}
}
}

View File

@ -1,19 +1,59 @@
use crate::{
consts::*,
util::bit,
constant::*,
util::*,
};
#[derive(Clone, Copy)]
pub struct MoveSet {
pub direction:u32,
pub stride:u32,
pub alt:Option<u32>,
}
impl MoveSet {
pub fn rotate(&self) -> Self
pub const fn new() -> Self
{
Self {
direction:0,
stride:0,
alt:None,
}
}
pub const fn add(mut self, bits:u32) -> Self
{
self.direction |= bits;
self
}
pub const fn add_stride(mut self, bits:u32, mode:u32) -> Self
{
self.direction |= bits;
let mut bits_set = bits;
while bits_set != 0 {
let mask = lsb(bits_set);
let index = ffs(mask);
self.stride |= (mode & 3) << (index * 2);
bits_set &= !mask;
}
self
}
pub const fn add_alt(mut self, mode:u32) -> Self
{
self.alt = Some(mode);
self
}
pub fn rotate(&self) -> Self
{
Self {
direction:rotate6(self.direction),
stride:rotate12(self.stride),
alt:None,
}
}
}
@ -28,119 +68,113 @@ pub struct PieceClass {
pub const PIECES :[PieceClass; PIECES_COUNT] = [
PieceClass {
name: "Militia",
moves: MoveSet {
direction:bit(0) | bit(1) | bit(5),
stride:0,
},
pmoves: MoveSet{
direction:bit(0) | bit(1) | bit(2) | bit(4) | bit(5),
stride:0,
},
moves: MoveSet::new()
.add(bit(0) | bit(1) | bit(5)),
pmoves: MoveSet::new()
.add(bit(0) | bit(1) | bit(2) | bit(3) | bit(4) | bit(5)),
},
PieceClass {
name: "Lance",
moves: MoveSet {
direction:1,
stride:0,
},
pmoves: MoveSet {
direction:1,
stride:0,
},
moves: MoveSet::new()
.add(bit(1) | bit(5))
.add_stride(bit(0), 3),
pmoves: MoveSet::new()
.add(bit(0) | bit(3))
.add_stride(bit(1) | bit(2) | bit(4) | bit(5), 1),
},
PieceClass {
name: "Knight",
moves: MoveSet {
direction:bit(3) | bit(6) | bit(11) | bit(13) | bit(17),
stride:0,
},
pmoves: MoveSet{
direction:0,
stride:0,
},
moves: MoveSet::new()
.add(bit(6) | bit(8) | bit(9) | bit(11) | bit(13) | bit(14) | bit(16) | bit(17)),
pmoves: MoveSet::new()
.add(bit(6) | bit(8) | bit(9) | bit(11) | bit(13) | bit(14) | bit(16) | bit(17))
.add_alt(1),
},
PieceClass {
name: "Tower",
moves: MoveSet {
direction:1,
stride:0,
},
pmoves: MoveSet {
direction:1,
stride:0,
},
moves: MoveSet::new()
.add(bit(0) | bit(1) | bit(3) | bit(5) | bit(6) | bit(11)),
pmoves: MoveSet::new()
.add(bit(0) | bit(1) | bit(2) | bit(3) | bit(4) | bit(5) | bit(6) | bit(8) | bit(9) | bit(11)),
},
PieceClass {
name: "Castle",
moves: MoveSet {
direction:1,
stride:0,
},
pmoves: MoveSet {
direction:1,
stride:0,
},
moves: MoveSet::new()
.add(bit(0) | bit(1) | bit(2) | bit(4) | bit(5) | bit(7) | bit(10)),
pmoves: MoveSet::new()
.add(bit(0) | bit(1) | bit(2) | bit(3) | bit(4) | bit(5) | bit(7) | bit(10))
.add_alt(2),
},
PieceClass {
name: "Dragon",
moves: MoveSet {
direction:1,
stride:0,
},
pmoves: MoveSet {
direction:1,
stride:0,
},
moves: MoveSet::new()
.add_stride(bit(6) | bit(7) | bit(8) | bit(9) | bit(10) | bit(11), 3),
pmoves: MoveSet::new()
.add_stride(bit(6) | bit(7) | bit(8) | bit(9) | bit(10) | bit(11), 3)
.add(bit(0) | bit(1) | bit(2) | bit(3) | bit(4) | bit(5)),
},
PieceClass {
name: "Behemoth",
moves: MoveSet {
direction:1,
stride:0,
},
pmoves: MoveSet {
direction:1,
stride:0,
},
moves: MoveSet::new()
.add_stride(bit(0) | bit(1) | bit(2) | bit(3) | bit(4) | bit(5), 3),
pmoves: MoveSet::new()
.add_stride(bit(0) | bit(1) | bit(2) | bit(3) | bit(4) | bit(5), 3)
.add(bit(12) | bit(13) | bit(14) | bit(15) | bit(16) | bit(17)),
},
PieceClass {
name: "Source",
moves: MoveSet {
direction:0,
stride:0,
},
pmoves: MoveSet {
direction:0,
stride:0,
},
name: "Heart",
moves: MoveSet::new()
.add(bit(0) | bit(1) | bit(2) | bit(3) | bit(4) | bit(5) | bit(7) | bit(10)),
pmoves: MoveSet::new(),
},
];
#[derive(Clone, Copy)]
pub struct Piece {
pub class:u8,
pub promoted:bool,
pub player:u8,
pub promoted:bool,
pub tile:u8,
pub blocking:u32,
}
impl Piece {
pub fn new(class:u8, player:u8) -> Self
{
Self {
class,
promoted:false,
player,
promoted:false,
tile:0,
blocking:0,
}
}
pub fn new_at(class:u8, player:u8, tile:u8) -> Self
pub fn at(mut self, tile:u8) -> Self
{
Self {
class,
promoted:false,
player,
tile,
self.tile = tile;
self
}
pub fn promote(mut self, promoted:bool) -> Self
{
self.promoted = promoted;
self
}
pub fn moves(&self) -> MoveSet
{
let class = &crate::piece::PIECES[self.class as usize];
let moves = if self.promoted {
class.pmoves
} else {
class.moves
};
if self.player == 0 {
moves
} else {
moves.rotate()
}
}

View File

@ -1,2 +1,32 @@
pub const fn bit(v:u32) -> u32 { 1u32 << v }
pub const fn mask(b:u32, s:u32) -> u32 { ((1u32 << b) - 1) << s }
pub const fn lsb(x:u32) -> u32 { x & x.wrapping_neg() }
pub const fn ffs(x:u32) -> u32 { 31 - x.leading_zeros() }
pub const fn rotate6(x:u32) -> u32
{
const R1 :u32 = 0x0000_003F;
const R2 :u32 = 0x0000_0FC0;
const R3 :u32 = 0x0003_F000;
let a = (x & R1) << 3;
let b = (x & R2) << 3;
let c = (x & R3) << 3;
(a & R1) | ((a >> 6) & R1)
| (b & R2) | ((b >> 6) & R2)
| (c & R3) | ((c >> 6) & R3)
}
pub const fn rotate12(x:u32) -> u32
{
const R1 :u32 = 0x0000_0FFF;
const R2 :u32 = 0x00FF_F000;
let a = (x & R1) << 3;
let b = (x & R2) << 3;
(a & R1) | ((a >> 12) & R1)
| (b & R2) | ((b >> 12) & R2)
}

View File

@ -1,9 +1,9 @@
const ROWS :[u8; 10] = [ 0, 5, 11, 18, 26, 35, 43, 50, 56, 61 ];
const ROWS :[i8; 10] = [ 0, 5, 11, 18, 26, 35, 43, 50, 56, 61 ];
#[derive(Clone, Copy)]
pub struct Hex {
pub x:u8,
pub y:u8,
pub x:i8,
pub y:i8,
pub tile:u8,
}
impl Hex {
@ -16,7 +16,7 @@ impl Hex {
}
}
pub fn from_hex(x:u8, y:u8) -> Self
pub fn from_hex(x:i8, y:i8) -> Self
{
let x = x as i32;
let y = y as i32;
@ -24,8 +24,8 @@ impl Hex {
let b = (x > 4) as i32 * ((x - 4) + ((x - 5) * (x - 4)));
let tile = a - b + y;
Self {
x:x as u8,
y:y as u8,
x:x as i8,
y:y as i8,
tile:tile as u8,
}
}
@ -33,16 +33,16 @@ impl Hex {
pub fn from_tile(tile:u8) -> Self
{
let mut x = 0i32;
while tile >= ROWS[x as usize + 1] { x += 1; }
let y = tile - ROWS[x as usize] + ((x > 4) as i32 * (x - 4)) as u8;
while tile as i8 >= ROWS[x as usize + 1] { x += 1; }
let y = tile as i8 - ROWS[x as usize] + ((x > 4) as i32 * (x - 4)) as i8;
Self {
x:x as u8,
x:x as i8,
y,
tile,
}
}
/*pub fn is_valid(x:i8, y:i8) -> bool
pub fn is_valid(x:i8, y:i8) -> bool
{
const COLUMNS :[(i8, i8); 9] = [
(0, 4),
@ -61,11 +61,11 @@ impl Hex {
let (min, max) = COLUMNS[x as usize];
y >= min && y <= max
} else { false }
}*/
}
pub fn is_back(x:u8, y:u8, player:u8) -> bool
pub fn is_back(x:i8, y:i8, player:u8) -> bool
{
const COLUMNS :[[u8; 2]; 9] = [
const COLUMNS :[[i8; 2]; 9] = [
[0, 4],
[0, 5],
[0, 6],

View File

@ -217,6 +217,12 @@ impl App {
)).await.ok();
}
QRPacketData::TestResult(response) => {
socket.send(Message::Binary(
encode_response(CODE_TEST_RESULT, response.encode())
)).await.ok();
}
_ => { }
}
}
@ -239,6 +245,8 @@ impl App {
session.p_dusk.connections.len() > 0,
session.connections.len() as u32,
),
test: false,
expected: false,
}),
));
}

View File

@ -37,6 +37,9 @@ async fn service_http(mut request:hyper::Request<hyper::body::Incoming>, args:Ht
println!("Serving: {}", request.uri().path());
/*
** Get client language.
*/
let mut language_code = 0;
if let Some(languages) = request.headers().get(ACCEPT_LANGUAGE) {
if let Ok(languages) = languages.to_str() {
@ -55,6 +58,9 @@ async fn service_http(mut request:hyper::Request<hyper::body::Incoming>, args:Ht
}
}
/*
** Upgrade to websocket, if reqeusted.
*/
if hyper_tungstenite::is_upgrade_request(&request) {
if let Ok((response, websocket)) = hyper_tungstenite::upgrade(&mut request, None) {
tokio::task::spawn(async move {
@ -71,7 +77,12 @@ async fn service_http(mut request:hyper::Request<hyper::body::Incoming>, args:Ht
.body(Full::new(Bytes::new()))
.unwrap())
}
} else {
}
/*
** Otherwise, serve cached assets.
*/
else {
match args.cache.fetch(request.uri().path()) {
Some(data) => {
let mut output = data.data;
@ -137,16 +148,20 @@ async fn main()
{
println!("Server Version: {}", config::VERSION);
// Initialize application data.
let app;
if let Ok(result) = App::init() {
app = result;
/*
** Initialize application data.
*/
let app = if let Ok(result) = App::init() {
result
} else {
println!("fatal: failed to initialize server.");
return;
}
};
// Initialize central bus and data serivce.
/*
** Initialize central bus and data serivce.
*/
let b_main :Bus<protocol::QRPacket> = Bus::new_as(bus::Mode::Transmitter);
match b_main.connect() {
Ok(bus) => {
@ -160,7 +175,10 @@ async fn main()
}
}
// Load image assets
/*
** Load image assets.
*/
let mut js_asset_data = String::from("const GAME_ASSET = { Image: {");
let asset_path = Path::new("www/asset/");
for name in [
@ -182,9 +200,12 @@ async fn main()
}
js_asset_data += "} };";
// Initialize HTTPS service.
match b_main.connect() {
Ok(bus) => {
/*
** Cache source files.
*/
let cache = WebCache::new();
cache.cache("text/html", "/.html", &[
WebCache::file("www/.html"),
@ -242,6 +263,10 @@ async fn main()
}
}
/*
** Initialize network services.
*/
let mut tcp_server = TcpServer::new();
match tcp_server.bind("127.0.0.1:38611").await {
Ok(_) => {

View File

@ -566,6 +566,8 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
cid,
QRPacketData::GameMessage(PacketGameMessage {
data: GameMessageData::Resign,
test: false,
expected: false,
}),
));
}
@ -628,6 +630,7 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
println!("Request: Game Message");
let mut packets = Vec::<QRPacket>::new();
let mut response = QRPacketData::None;
if let Some(sid) = session_id {
if let Some(session) = app.sessions.get_mut(&sid) {
@ -637,7 +640,37 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
| GameMessageData::PlayDrop(turn, from, to)
| GameMessageData::PlayAlt(turn, from, to)
=> {
if !session.game.is_complete() {
if request.test {
let play = Play {
source: match request.data {
GameMessageData::PlayMove(..) => 0,
GameMessageData::PlayDrop(..) => 1,
GameMessageData::PlayAlt(..) => 2,
_ => 0,
},
from, to,
};
let text = format!("PLAY {} {} {}", play.source, play.from, play.to);
let result = if session.game.play_is_valid(&play) {
response = QRPacketData::TestResult(PacketTestResult::new(
request.expected,
&text,
));
request.expected
} else {
response = QRPacketData::TestResult(PacketTestResult::new(
!request.expected,
&text,
));
!request.expected
};
if result {
println!("OK {} {}", request.expected, text);
} else {
println!("NO {} {}", request.expected, text);
}
} else if !session.game.is_complete() {
if (user_id == Some(session.p_dawn.user) && session.game.turn & 1 == 0)
|| (user_id == Some(session.p_dusk.user) && session.game.turn & 1 == 1) {
if turn == session.game.turn {
@ -780,7 +813,7 @@ pub async fn thread_system(mut app:App, bus:Bus<protocol::QRPacket>)
}
// Updates already sent; nothing to do here.
Some(QRPacket::new(qr.id, QRPacketData::None))
Some(QRPacket::new(qr.id, response))
}
}
}

View File

@ -48,6 +48,7 @@ pub const CODE_USER_LIST :u16 = 0x0100;
//pub const CODE_USER_AWAIT_GET :u16 = 0x0110;
//pub const CODE_USER_AWAIT_SET :u16 = 0x0111;
pub const CODE_TEST_RESULT :u16 = 0xFFFF;
/*
** Game Messages

View File

@ -51,6 +51,8 @@ pub enum QRPacketData {
QChallengeList,
RChallengeList(PacketChallengeListResponse),
TestResult(PacketTestResult),
}
#[derive(Clone)]

View File

@ -27,18 +27,26 @@ pub enum GameMessageData {
#[derive(Clone)]
pub struct PacketGameMessage {
pub data:GameMessageData,
pub test:bool,
pub expected:bool,
}
impl PacketGameMessage {
pub fn new() -> Self
{
Self {
data:GameMessageData::Error,
test:false,
expected:false,
}
}
pub fn with(data:GameMessageData) -> Self
{
Self { data }
Self {
data,
test:false,
expected:false,
}
}
}
impl Packet for PacketGameMessage {
@ -79,15 +87,20 @@ impl Packet for PacketGameMessage {
),
_ => GameMessageData::Error,
}
}
};
result.test = (data & (1<<63)) != 0;
result.expected = (data & (1<<62)) != 0;
Ok(result)
} else {
Err(())
}
}
fn encode(&self) -> Vec<u8>
{
pack_u64(match self.data {
let mut output = match self.data {
GameMessageData::PlayMove(turn, from, to) => {
GMSG_PLAY_MOVE as u64
| ((turn as u64) << 8)
@ -129,6 +142,11 @@ impl Packet for PacketGameMessage {
}
_ => { 0 }
})
};
output |= (self.test as u64) << 63;
output |= (self.expected as u64) << 62;
pack_u64(output)
}
}

View File

@ -22,6 +22,8 @@ mod challenge_list; pub use challenge_list::*;
mod user_list; pub use user_list::*;
mod test_result; pub use test_result::*;
mod prelude {
pub trait Packet {
type Data;

View File

@ -0,0 +1,28 @@
use crate::util::pack::{pack_u8, pack_u16};
use super::Packet;
#[derive(Clone)]
pub struct PacketTestResult {
pub result:bool,
pub text:String,
}
impl PacketTestResult {
pub fn new(result:bool, text:&str) -> Self
{
Self { result, text:text.to_string() }
}
}
impl Packet for PacketTestResult {
type Data = Self;
fn encode(&self) -> Vec<u8>
{
let bytes = self.text.as_bytes().to_vec();
[
pack_u8(self.result as u8),
pack_u16(bytes.len() as u16),
bytes,
].concat()
}
}

View File

@ -54,6 +54,8 @@ const OpCode = {
ChallengeList :0x0062,
UserList :0x0100,
TestResult :0xFFFF,
};
const GameState = {

View File

@ -419,6 +419,56 @@ GAME.Game = class {
this.update_board();
}
play_is_valid(play) {
let player = this.turn & 1;
switch(play.source) {
case 0: {
let piece_id = this.board.tiles[play.from].piece;
if(piece_id !== null) {
let piece = this.board.pieces[piece_id];
if(piece.player == player) {
let moves = this.movement_tiles(piece, play.from);
for(let move of moves) {
if(move.tile == play.to && move.valid) {
return true;
}
}
}
}
} break;
case 1: {
if(play.from >= 0 && play.from <= 6) {
if(this.pools[player].pieces[play.from] > 0) {
let moves = this.placement_tiles(play.from, player);
for(let move of moves) {
if(move.tile == play.to && move.valid) {
return true;
}
}
}
}
} break;
case 2: {
let piece_id = this.board.tiles[play.from].piece;
if(piece_id !== null) {
let piece = this.board.pieces[piece_id];
if(piece.player == player) {
let moves = this.movement_tiles_alt(piece);
for(let move of moves) {
if(move.tile == play.to && move.valid) {
return true;
}
}
}
}
} break;
}
return false;
}
movement_tiles(piece, tile) {
let tiles = [ ];
let moves = piece.moves();
@ -449,7 +499,7 @@ GAME.Game = class {
let max_dist = 1;
switch(stride) {
case 1: max_dist = 2; break;
case 3: max_dist = 9; break;
case 3: max_dist = 8; break;
}
for(let dist = 1; dist <= max_dist; ++dist) {
move_hex.add(direction);
@ -735,9 +785,9 @@ GAME.Game = class {
// Check off-sides.
if(params.extent !== false) {
if(piece.player == 0) {
position_valid = position_valid && (hex.y <= this.board.columns[hex.x].extent[+(!piece.player)]);
position_valid = position_valid && (hex.y <= this.board.columns[hex.x].extent[1]);
} else {
position_valid = position_valid && (hex.y >= this.board.columns[hex.x].extent[+(!piece.player)]);
position_valid = position_valid && (hex.y >= this.board.columns[hex.x].extent[0]);
}
}

View File

@ -214,6 +214,7 @@ const INTERFACE = {
click(event) {
console.log("CLICK");
switch(event.button) {
// Main button
case 0: {
console.log("A");
@ -1829,6 +1830,51 @@ const INTERFACE = {
setTimeout(INTERFACE.reaction_generate, 50);
}
}
},
run_test(source=0, from=0, to=0, all=true)
{
let next_source = source;
let next_from = from;
let next_to = to + 1;
if(next_to == 61) {
next_to = 0;
next_from += 1;
}
if(next_from == 61 || (next_source == 1 && next_from == 7)) {
next_from = 0;
next_source += 1;
}
let msg = GAME_DATA.turn | (from << 16) | (to << 22);
let high = msg >> 24;
let low = (msg << 8) & 0xFFFF_FFFF;
switch(source) {
case 0: low |= GameMessage.Move; break;
case 1: low |= GameMessage.Drop; break;
case 2: low |= GameMessage.Alt; break;
}
// Mark test and expected value
let expect = GAME_DATA.play_is_valid(new GAME.Play(source, from, to));
high |= 1 << 31;
high |= (+expect) << 30;
console.log("Test Expect: " + expect);
MESSAGE_COMPOSE([
PACK.u16(OpCode.GameMessage),
PACK.u32(high),
PACK.u32(low),
]);
if(all && next_source < 3) {
setTimeout(INTERFACE.run_test, 100, next_source, next_from, next_to);
}
}
};

View File

@ -470,6 +470,20 @@ function MESSAGE(event) {
}
} break;
case OpCode.TestResult: {
result = UNPACK.u8(bytes, index);
index = result.index;
let test_result = result.data;
result = UNPACK.string(bytes, index, UNPACK.u16);
index = result.index;
let text = result.data;
if(test_result == 0) {
console.log("BD " + text);
}
} break;
default:
console.log("RECV Undefined " + code);
return;