fog-of-chess

[toy] Chess, but with the fog of war.
Log | Files | Refs

commit bfc4d5a2d5ea22503851170953ea2e1675c538a6
parent 6578ca7a56f5fc6d735c6c299f2aa9f55017b0bf
Author: Jack Mordaunt <jackmordaunt@gmail.com>
Date:   Mon,  6 Apr 2020 19:09:05 +0800

Feat: State reset with 'R' key. Use set for selection, avoid dupes.

Diffstat:
Msrc/main.rs | 188++++++++++++++++++++++++++++++++++++++++++++++++-------------------------------
1 file changed, 114 insertions(+), 74 deletions(-)

diff --git a/src/main.rs b/src/main.rs @@ -8,6 +8,7 @@ use ggez::input::keyboard::{is_key_pressed, KeyCode, KeyMods}; use ggez::input::mouse::MouseButton; use ggez::{conf::WindowMode, conf::WindowSetup}; use ggez::{Context, ContextBuilder, GameResult}; +use std::collections::HashSet; const PURE_APPLE: Color = Color { r: 106.0 / 256.0, @@ -72,11 +73,11 @@ fn main() { .window_setup(WindowSetup::default().title("Fog of Chess")) .build() .expect("creating game loop"); - let mut g = GameBuilder::default() + let state = StateBuilder::default() .board(board) .single_player(single_player) .fog(!app.is_present("no-fog")) - .selected(vec![]) + .selected(HashSet::new()) .turn(Player::White) .font( Font::new_glyph_font_bytes(&mut ctx, include_bytes!("../res/DejaVuSansMono.ttf")) @@ -85,7 +86,14 @@ fn main() { .debug_stats(app.is_present("debug-stats")) .build() .expect("building game object"); - match event::run(&mut ctx, &mut event_loop, &mut g) { + match event::run( + &mut ctx, + &mut event_loop, + &mut Game { + state: state.clone(), + initial: state.clone(), + }, + ) { Ok(_) => {} Err(e) => println!("error: {}", e), } @@ -99,49 +107,46 @@ impl EventHandler for Game { fn key_up_event(&mut self, _ctx: &mut Context, kc: KeyCode, _keymods: KeyMods) { if cfg!(debug_assertions) { match kc { - KeyCode::F => self.fog = !self.fog, - KeyCode::F3 => self.debug_stats = !self.debug_stats, + KeyCode::F => self.state.fog = !self.state.fog, + KeyCode::F3 => self.state.debug_stats = !self.state.debug_stats, + KeyCode::R => self.state = self.initial.clone(), _ => {} }; } } fn mouse_button_up_event(&mut self, ctx: &mut Context, _b: MouseButton, x: f32, y: f32) { - let (w_size, h_size) = self.cell_size(ctx); - let (col, row) = ((x / w_size).floor() as i32, (y / h_size).floor() as i32); + let (col, row) = self.pixels_to_grid(ctx, (x, y)); if is_key_pressed(ctx, KeyCode::LShift) { if self.contains_ally((col, row)) { // BUG: Avoid duplicates. - self.selected.push((col, row)); + self.state.selected.insert((col, row)); } } else { - match self.board.get((col, row)) { - // Move. + match self.state.board.get((col, row)) { None => { // Multi selection is a potential compound move. // Given the only compound move in standard chess is the // "castle", we directly call into it. - if self.selected.len() > 1 { - self.castle_move() + if self.state.selected.len() > 1 { + self.castle_move(); } else { - if let Some((x, y)) = self.first_selected() { + if let Some((x, y)) = self.state.selected.iter().next().cloned() { if self.moves((x, y)).contains(&(col, row)) { self.move_turn((x, y), (col, row)); } } } } - // Attack move. Some(Piece { player, .. }) => { - if *player != self.turn && self.selected.len() == 1 { - if let Some((x, y)) = self.first_selected() { - if self.moves((x, y)).contains(&(col, row)) { - self.move_turn((x, y), (col, row)); - } + if self.is_enemy(player) && self.state.selected.len() == 1 { + if let Some((x, y)) = self.state.selected.iter().next().cloned() { + self.attack_move((x, y), (col, row)); } } else { if self.contains_ally((col, row)) { - self.selected = vec![(col as i32, row as i32)]; + self.state.selected.clear(); + self.state.selected.insert((col, row)); } } } @@ -153,10 +158,10 @@ impl EventHandler for Game { self.draw_board(ctx)?; self.draw_pieces(ctx)?; self.draw_highlights(ctx)?; - if self.fog { + if self.state.fog { self.draw_fog(ctx)?; } - if self.debug_stats { + if self.state.debug_stats { self.draw_debug_stats(ctx)?; } graphics::draw_queued_text( @@ -206,12 +211,18 @@ pub struct Piece { pub struct Board([[Option<Piece>; 8]; 8]); /// Game contains meta information. -#[derive(Clone, Builder)] +#[derive(Clone)] pub struct Game { + pub initial: State, + pub state: State, +} + +#[derive(Clone, Builder)] +pub struct State { pub board: Board, pub turn: Player, // TODO: Use a set to avoid duplicates. - pub selected: Vec<(i32, i32)>, + pub selected: HashSet<(i32, i32)>, pub font: graphics::Font, pub fog: bool, pub single_player: bool, @@ -223,7 +234,7 @@ impl Game { pub fn moves(&self, pos: (i32, i32)) -> Vec<(i32, i32)> { let (x, y) = pos; use Unit::*; - match self.board.get((x, y)) { + match self.state.board.get((x, y)) { Some(Piece { unit, player, @@ -246,9 +257,10 @@ impl Game { if self.contains_enemy((x + 1, y + 1)) { moves.push((x + 1, y + 1)); } - if self.board.0[y as usize + 1][x as usize].is_none() { + if self.state.board.0[y as usize + 1][x as usize].is_none() { moves.push((x, y + 1)); - if *moved == 0 && self.board.0[y as usize + 2][x as usize].is_none() + if *moved == 0 + && self.state.board.0[y as usize + 2][x as usize].is_none() { moves.push((x, y + 2)); } @@ -261,9 +273,10 @@ impl Game { if self.contains_enemy((x + 1, y - 1)) { moves.push((x + 1, y - 1)); } - if self.board.0[y as usize - 1][x as usize].is_none() { + if self.state.board.0[y as usize - 1][x as usize].is_none() { moves.push((x, y - 1)); - if *moved == 0 && self.board.0[y as usize - 2][x as usize].is_none() + if *moved == 0 + && self.state.board.0[y as usize - 2][x as usize].is_none() { moves.push((x, y - 2)); } @@ -286,53 +299,77 @@ impl Game { // Rook moves in all non diagonal directions. Rook => vec![] .into_iter() - .chain(LineOfSight::new((1..8).map(|ii| (x + ii, y)), &self.board)) - .chain(LineOfSight::new((1..8).map(|ii| (x - ii, y)), &self.board)) - .chain(LineOfSight::new((1..8).map(|ii| (x, y + ii)), &self.board)) - .chain(LineOfSight::new((1..8).map(|ii| (x, y - ii)), &self.board)) + .chain(LineOfSight::new( + (1..8).map(|ii| (x + ii, y)), + &self.state.board, + )) + .chain(LineOfSight::new( + (1..8).map(|ii| (x - ii, y)), + &self.state.board, + )) + .chain(LineOfSight::new( + (1..8).map(|ii| (x, y + ii)), + &self.state.board, + )) + .chain(LineOfSight::new( + (1..8).map(|ii| (x, y - ii)), + &self.state.board, + )) .collect(), // Bishop moves all diagonal directions. Bishop => vec![] .into_iter() .chain(LineOfSight::new( (1..8).map(|ii| (x + ii, y + ii)), - &self.board, + &self.state.board, )) .chain(LineOfSight::new( (1..8).map(|ii| (x - ii, y - ii)), - &self.board, + &self.state.board, )) .chain(LineOfSight::new( (1..8).map(|ii| (x - ii, y + ii)), - &self.board, + &self.state.board, )) .chain(LineOfSight::new( (1..8).map(|ii| (x + ii, y - ii)), - &self.board, + &self.state.board, )) .collect(), // Queen moves in all eight directions. Queen => vec![] .into_iter() - .chain(LineOfSight::new((1..8).map(|ii| (x + ii, y)), &self.board)) - .chain(LineOfSight::new((1..8).map(|ii| (x - ii, y)), &self.board)) - .chain(LineOfSight::new((1..8).map(|ii| (x, y + ii)), &self.board)) - .chain(LineOfSight::new((1..8).map(|ii| (x, y - ii)), &self.board)) + .chain(LineOfSight::new( + (1..8).map(|ii| (x + ii, y)), + &self.state.board, + )) + .chain(LineOfSight::new( + (1..8).map(|ii| (x - ii, y)), + &self.state.board, + )) + .chain(LineOfSight::new( + (1..8).map(|ii| (x, y + ii)), + &self.state.board, + )) + .chain(LineOfSight::new( + (1..8).map(|ii| (x, y - ii)), + &self.state.board, + )) .chain(LineOfSight::new( (1..8).map(|ii| (x + ii, y + ii)), - &self.board, + &self.state.board, )) .chain(LineOfSight::new( (1..8).map(|ii| (x - ii, y - ii)), - &self.board, + &self.state.board, )) .chain(LineOfSight::new( (1..8).map(|ii| (x - ii, y + ii)), - &self.board, + &self.state.board, )) .chain(LineOfSight::new( (1..8).map(|ii| (x + ii, y - ii)), - &self.board, + &self.state.board, )) .collect(), // King can move to any adjacent cell that isn't occupied by @@ -377,14 +414,20 @@ impl Game { /// Move a piece and conclude the turn. pub fn move_turn(&mut self, from: (i32, i32), to: (i32, i32)) { if self.contains_ally(from) { - self.board.move_piece((from.0, from.1), (to.0, to.1)); - if !self.single_player { - self.turn = match self.turn { + self.state.board.move_piece((from.0, from.1), (to.0, to.1)); + if !self.state.single_player { + self.state.turn = match self.state.turn { Player::Black => Player::White, Player::White => Player::Black, }; } - self.selected = vec![]; + self.state.selected.clear(); + } + } + /// Attack move one piece onto another. + pub fn attack_move(&mut self, from: (i32, i32), to: (i32, i32)) { + if self.moves((from.0, from.1)).contains(&(to.0, to.1)) { + self.move_turn((from.0, from.1), (to.0, to.1)); } } /// Contains enemy if the specified position is occupied by a piece owned @@ -392,8 +435,8 @@ impl Game { pub fn contains_enemy(&self, pos: (i32, i32)) -> bool { let (x, y) = pos; if x > -1 && y > -1 && x - 1 < 7 && y - 1 < 7 { - match &self.board.0[y as usize][x as usize] { - Some(Piece { player, .. }) => *player != self.turn, + match &self.state.board.0[y as usize][x as usize] { + Some(Piece { player, .. }) => self.is_enemy(player), _ => false, } } else { @@ -405,22 +448,14 @@ impl Game { pub fn contains_ally(&self, pos: (i32, i32)) -> bool { let (x, y) = pos; if x > -1 && y > -1 && x - 1 < 7 && y - 1 < 7 { - match &self.board.0[y as usize][x as usize] { - Some(Piece { player, .. }) => *player == self.turn, + match &self.state.board.0[y as usize][x as usize] { + Some(Piece { player, .. }) => *player == self.state.turn, None => false, } } else { false } } - /// Coordinate of the first selected piece. - fn first_selected(&mut self) -> Option<(i32, i32)> { - if self.selected.len() > 0 { - Some(self.selected[0].clone()) - } else { - None - } - } /// Perform castle move if valid. /// Castle move where King and Rook crossover into the 2 spaces between them. /// Only valid if: @@ -429,10 +464,11 @@ impl Game { /// - Nothing is in the two spaces between them. fn castle_move(&mut self) { let moves = self + .state .selected .iter() .take(2) - .filter_map(|pos| match self.board.get(*pos).cloned() { + .filter_map(|pos| match self.state.board.get(*pos).cloned() { Some(piece) => Some((pos, piece)), None => None, }) @@ -448,7 +484,7 @@ impl Game { } => (pos.0 - 2, pos.1), _ => return None, }; - if piece.moved > 0 || self.board.get(projected_move).is_some() { + if piece.moved > 0 || self.state.board.get(projected_move).is_some() { None } else { Some((*pos, projected_move)) @@ -457,15 +493,16 @@ impl Game { .collect::<Vec<((i32, i32), (i32, i32))>>(); if moves.len() == 2 { for (from, to) in moves { - self.board.move_piece(from, to); + self.state.board.move_piece(from, to); } + self.state.selected.clear(); } } /// Draw the board which the pieces are placed onto. fn draw_board(&self, ctx: &mut Context) -> GameResult<()> { let (w, h) = self.cell_size(ctx); let mut mb = MeshBuilder::new(); - for Position { x, y, .. } in self.board.iter() { + for Position { x, y, .. } in self.state.board.iter() { let (x, y) = (x as i32, y as i32); // TODO: get color from color map. let color = if x % 2 == 0 && y % 2 == 0 { @@ -489,7 +526,7 @@ impl Game { fn draw_pieces(&self, ctx: &mut Context) -> GameResult<()> { let (w, h) = self.cell_size(ctx); let size = w.min(h); - for Position { x, y, piece } in self.board.iter() { + for Position { x, y, piece } in self.state.board.iter() { if let Some(Piece { player, unit, .. }) = piece { // Chess pieces are part of unicode. // All we need is a font that provides these. @@ -510,7 +547,7 @@ impl Game { // Then queue and draw the text immediately, centering the text horizontally. // The fixed offset of -2.0 is required to counteract 1px borders (I think!). // The text must be drawn individually so that we can scale each fragment individually. - let fragment: graphics::TextFragment = (text, self.font, size).into(); + let fragment: graphics::TextFragment = (text, self.state.font, size).into(); graphics::queue_text(ctx, &Text::new(fragment), [0.0, 0.0], Some(color)); let scale = if h > w { [1.0, h / w] @@ -535,7 +572,7 @@ impl Game { fn draw_highlights(&self, ctx: &mut Context) -> GameResult<()> { let mut mb = MeshBuilder::new(); let (w, h) = self.cell_size(ctx); - for (x, y) in self.selected.iter() { + for (x, y) in self.state.selected.iter() { let (x, y) = (*x as f32, *y as f32); mb.rectangle( DrawMode::stroke(2.0), @@ -558,7 +595,7 @@ impl Game { let mut mask = [[Visibility::Fog; 8]; 8]; let mut mb = MeshBuilder::new(); let (w, h) = self.cell_size(ctx); - for Position { x, y, piece } in self.board.iter() { + for Position { x, y, piece } in self.state.board.iter() { if let Some(Piece { player, .. }) = piece { if self.is_enemy(player) { continue; @@ -610,10 +647,7 @@ impl Game { Ok(()) } fn is_enemy(&self, player: &Player) -> bool { - self.turn != *player - } - fn is_ally(&self, player: &Player) -> bool { - self.turn == *player + self.state.turn != *player } // Calculate cell size based on window size (width, height). fn cell_size(&self, ctx: &mut Context) -> (f32, f32) { @@ -628,7 +662,7 @@ impl Game { scale: f32, color: Option<Color>, ) { - let fragment: graphics::TextFragment = (text, self.font, scale).into(); + let fragment: graphics::TextFragment = (text, self.state.font, scale).into(); graphics::queue_text( ctx, &Text::new(fragment), @@ -636,6 +670,12 @@ impl Game { color.or(Some(graphics::BLACK)), ); } + // Translate pixel coordinates to grid cells. + fn pixels_to_grid(&self, ctx: &mut Context, coord: (f32, f32)) -> (i32, i32) { + let (x, y) = coord; + let (w, h) = self.cell_size(ctx); + ((x / w).floor() as i32, (y / h).floor() as i32) + } } impl Board {