You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
martian-chess/src/game.rs

517 lines
16 KiB
Rust

use bevy::utils::HashSet;
use crate::prelude::*;
pub(crate) struct GamePlugin;
impl Plugin for GamePlugin {
fn build(&self, app: &mut App) {
app.add_event::<Move>()
.add_systems(Startup, setup_board)
.add_systems(
Update,
(
menu::exit_to_menu.run_if(in_state(GameState::Play)),
update_board.run_if(on_event::<Move>()).after(select_sync),
set_side.run_if(on_event::<Move>()), // TODO: correct run_if?
cancel_place.run_if(|buttons: Res<Input<MouseButton>>| -> bool {
buttons.just_pressed(MouseButton::Right)
}),
select_sync
.run_if(any_component_added::<Selected>)
.after(deselect_sync),
deselect_sync.run_if(any_component_removed::<Selected>()),
move_piece.run_if(any_component_added::<Selected>),
capture_piece.run_if(any_component_added::<Captured>),
null_selections.run_if(any_component_added::<Selected>),
pick_up_audio::<display2d::Display2d>
.run_if(in_state(DisplayState::Display2d))
.run_if(any_component_added::<Selected>),
pick_up_audio::<display3d::Display3d>
.run_if(in_state(DisplayState::Display3d))
.run_if(any_component_added::<Selected>),
),
)
.add_systems(
PreUpdate,
(
asserts::<display2d::Display2d>,
asserts::<display3d::Display3d>,
),
)
.add_systems(
PostUpdate,
(debug_board.run_if(resource_exists::<debug::DebugEnabled>()),),
);
}
}
#[derive(Debug, Component, Clone, PartialEq, Copy)]
pub(crate) enum Piece {
Pawn,
Drone,
Queen,
}
#[derive(Debug, Component, Clone, PartialEq)]
pub(crate) enum Tile {
Dark,
Light,
}
pub(crate) fn tiles() -> impl Iterator<Item = (BoardIndex, Tile)> {
(0..32).map(|i| {
let x = i % 8;
let y = i / 8;
let s = (x % 2) ^ (y % 2);
let index = BoardIndex { x, y };
let tile = if s == 0 { Tile::Dark } else { Tile::Light };
(index, tile)
})
}
#[derive(Debug, Component)]
pub(crate) struct BoardComponent;
#[derive(Debug, Component)]
pub(crate) struct Captured;
// manually for the type.
impl std::fmt::Display for Piece {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
match self {
Piece::Queen => write!(f, "@"),
Piece::Drone => write!(f, "^"),
Piece::Pawn => write!(f, "*"),
}
}
}
#[derive(Debug)]
pub(crate) enum GameError {
NullMove,
InvalidIndex,
}
/// The board is setup like this:
/// ```text
/// 0 1 2 3 4 5 6 7
/// +--+--+--+--+--+--+--+--+
/// a | | | | I | d| Q| Q|
/// +--+--+--+--+--+--+--+--+
/// b |d |p |p | I | p| d| Q|
/// +--+--+--+--+--+--+--+--+
/// c |Q |d |p | I | p| p| d|
/// +--+--+--+--+--+--+--+--+
/// d |Q |Q |d | I | | | |
/// +--+--+--+--+--+--+--+--+
/// ````
#[derive(Debug, Resource)]
pub(crate) struct Board {
inner: Vec<Vec<Option<Piece>>>,
moves: Vec<Move>,
}
#[derive(Debug, Default, Event, Clone)]
pub(crate) struct Move {
pub epoch: usize,
pub from: BoardIndex,
pub to: Option<BoardIndex>,
}
#[derive(Debug, Component, PartialEq, Clone, Default, Copy, Eq, Hash)]
pub(crate) struct BoardIndex {
pub x: usize,
pub y: usize,
}
#[derive(Debug, Component, PartialEq)]
pub(crate) enum Side {
A,
B,
}
impl Board {
/// Returns the piece at the given location
pub(crate) fn at(&self, BoardIndex { x, y }: BoardIndex) -> Option<&Piece> {
self.inner[y][x].as_ref()
}
/// Returns a list of all pieces on the board with their location
pub(crate) fn pieces(&self) -> Vec<(BoardIndex, Piece)> {
self.inner
.iter()
.enumerate()
.flat_map(|(y, nested)| {
nested.iter().enumerate().filter_map(move |(x, p)| {
p.as_ref().map(|val| (BoardIndex { x, y }, val.clone()))
})
})
.collect()
}
/// Moves a piece from -> to
pub(crate) fn move_piece(
&mut self,
from: BoardIndex,
to: BoardIndex,
) -> Result<Vec<Move>, GameError> {
if from == to {
Err(GameError::NullMove)
} else {
match self.at(from.clone()) {
Some(from_val) => {
// The current epoch is the last epoch + 1
let epoch = self.current_epoch();
// Local moves vec we can return
let mut moves = vec![];
// If the position we are moving to is occupied, capture the removal in the ledger
if self.inner[to.y][to.x].is_some() {
moves.push(Move {
epoch,
from: to.clone(),
to: None,
});
}
// Capture the intened move in the moves ledger
moves.push(Move {
epoch,
from: from.clone(),
to: Some(to.clone()),
});
self.inner[to.y][to.x] = Some(*from_val);
self.inner[from.y][from.x] = None;
self.moves.extend(moves.clone());
Ok(moves)
}
None => Err(GameError::NullMove),
}
}
}
/// Returns the Side of a piece
pub(crate) fn side(BoardIndex { x, .. }: BoardIndex) -> Result<Side, GameError> {
match x {
0..=3 => Ok(Side::A),
4..=7 => Ok(Side::B),
_ => Err(GameError::InvalidIndex),
}
}
/// Returns the possible moves the piece at this tile can make.
pub(crate) fn possible_moves(&self, BoardIndex { x, y }: BoardIndex) -> HashSet<BoardIndex> {
let f = |(a, b): (Option<usize>, Option<usize>)| {
if let (Some(x), Some(y)) = (a, b) {
if (0..=7).contains(&x) && (0..=3).contains(&y) {
Some(BoardIndex { x, y })
} else {
None
}
} else {
None
}
};
match self.at(BoardIndex { x, y }) {
// One space in any diagonal
Some(Piece::Pawn) => std::iter::empty()
.chain(
(-1..=1)
.zip(-1..=1)
.map(move |(a, b)| (x.checked_add_signed(a), y.checked_add_signed(b))),
)
.chain(
(-1..=1)
.zip((-1..=1).rev())
.map(move |(a, b)| (x.checked_add_signed(a), y.checked_add_signed(b))),
)
.filter_map(f)
.collect(),
// One or two spaces in either horizontal
Some(Piece::Drone) => std::iter::empty()
.chain((-2..=2).map(|i| (x.checked_add_signed(i), Some(y))))
.chain((-2..=2).map(|i| (Some(x), y.checked_add_signed(i))))
.filter_map(f)
.collect(),
// Any distance in any straight line
Some(Piece::Queen) => std::iter::empty()
.chain((-7..=7).map(|i| (x.checked_add_signed(i), Some(y))))
.chain((-3..=3).map(|i| (Some(x), y.checked_add_signed(i))))
.chain(
(-3..=3)
.zip(-3..=3)
.map(move |(a, b)| (x.checked_add_signed(a), y.checked_add_signed(b))),
)
.chain(
(-3..=3)
.zip((-3..=3).rev())
.map(move |(a, b)| (x.checked_add_signed(a), y.checked_add_signed(b))),
)
.filter_map(f)
.collect(),
None => std::iter::empty().collect(),
}
}
pub(crate) fn current_epoch(&self) -> usize {
self.moves.last().unwrap_or(&Move { ..default() }).epoch + 1
}
}
impl std::fmt::Display for Board {
fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
self.inner.iter().rev().for_each(|row| {
let _ = write!(f, "+--+--+--+--+--+--+--+--+\n");
let _ = write!(f, "|");
row.iter().for_each(|piece| {
let _ = match piece {
Some(p) => write!(f, "{} |", p),
None => write!(f, " |"),
};
});
let _ = write!(f, "\n");
});
let _ = write!(f, "+--+--+--+--+--+--+--+--+");
Ok(())
}
}
/// Marker component for currently selected entities
#[derive(Debug, Default, Component)]
pub(crate) struct Selected;
/// Marker component for selectable entities
#[derive(Debug, Default, Component)]
pub(crate) struct Selectable;
fn setup_board(mut commands: Commands) {
use Piece::*;
commands.insert_resource(Board {
moves: vec![],
inner: vec![
vec![
Some(Queen),
Some(Queen),
Some(Drone),
None,
None,
None,
None,
None,
],
vec![
Some(Queen),
Some(Drone),
Some(Pawn),
None,
None,
Some(Pawn),
Some(Pawn),
Some(Drone),
],
vec![
Some(Drone),
Some(Pawn),
Some(Pawn),
None,
None,
Some(Pawn),
Some(Drone),
Some(Queen),
],
vec![
None,
None,
None,
None,
None,
Some(Drone),
Some(Queen),
Some(Queen),
],
],
});
}
fn debug_board(board: Res<Board>, mut debug_info: ResMut<debug::DebugInfo>) {
debug_info.set("board".into(), format!("\n{}", *board));
}
/// Update this method to use a diff between the board and the state of the 2d/3d worlds
pub(crate) fn update_board(
mut events: EventReader<Move>,
mut pieces: Query<(Entity, &mut BoardIndex), With<Piece>>,
selected: Query<Entity, With<Selected>>,
mut commands: Commands,
) {
events.iter().for_each(|Move { from, to, .. }| {
pieces.iter_mut().for_each(|(entity, mut index)| {
if *index == *from {
match to {
Some(to_idx) => {
info!("Moving piece {:?} to {:?}", entity, to_idx);
*index = to_idx.clone();
}
None => {
info!("Capturing piece {:?}", entity);
commands
.entity(entity)
.remove::<BoardIndex>()
.insert(Captured);
}
}
}
});
selected.iter().for_each(|entity| {
info!("De-selecting selected piece {:?}", entity);
commands.entity(entity).remove::<Selected>();
});
})
}
pub(crate) fn set_side(mut events: Query<(&mut Side, &BoardIndex), Changed<BoardIndex>>) {
events
.iter_mut()
.for_each(|(mut side, idx)| match Board::side(*idx) {
Ok(s) => {
debug!("Set side event {:?} {:?} -> {:?}", idx, side, s);
if *side != s {
*side = s
}
}
Err(e) => warn!("{:?}", e),
});
}
fn select_sync(
events: Query<Entity, Added<Selected>>,
pieces: Query<(Entity, &BoardIndex), (With<Selectable>, With<Piece>)>,
mut commands: Commands,
) {
events.iter().for_each(|entity| {
if let Ok((this_e, this_idx)) = pieces.get(entity) {
if let Some((entangled, _)) = pieces
.iter()
.find(|(e, idx)| *idx == this_idx && *e != this_e)
{
commands.entity(entangled).insert(Selected);
}
}
})
}
fn deselect_sync(
mut events: RemovedComponents<Selected>,
pieces: Query<(Entity, &BoardIndex), (With<Selectable>, With<Piece>)>,
mut commands: Commands,
) {
events.iter().for_each(|entity| {
if let Ok((this_e, this_idx)) = pieces.get(entity) {
if let Some((entangled, _)) = pieces
.iter()
.find(|(e, idx)| *idx == this_idx && *e != this_e)
{
info!("De-selecting entangled piece {:?}", entity);
commands.entity(entangled).remove::<Selected>();
}
}
})
}
/// Triggered when right-mouse-button clicked
fn cancel_place(current: Query<Entity, With<Selected>>, mut commands: Commands) {
current.iter().for_each(|entity| {
info!("De-selecting {:?}", entity);
commands.entity(entity).remove::<Selected>();
});
}
/// When a tile is selected, move all selected pieces to that index
fn move_piece(
events: Query<&BoardIndex, (With<BoardIndex>, Added<Selected>)>,
selected_pieces: Query<&BoardIndex, (With<Selected>, With<Piece>)>,
mut board: ResMut<Board>,
mut move_events: EventWriter<Move>,
mut writer: EventWriter<audio::AudioEvent>,
) {
events.iter().for_each(|to| {
selected_pieces.iter().for_each(|from| {
if from != to {
info!("Applying move {:?} -> {:?}", from, to);
// Move piece
match board.move_piece(*from, *to) {
Ok(moves) => {
// De-select the piece
info!("Applying moves {:?}", moves);
moves.iter().for_each(|m| move_events.send(m.clone()));
writer.send(audio::AudioEvent::PutDown)
}
Err(GameError::NullMove) => warn!("Null move!"),
Err(GameError::InvalidIndex) => warn!("Invalid index!"),
}
}
});
});
}
fn pick_up_audio<D: Component>(
selected: Query<Entity, (With<Selected>, With<Piece>, With<D>)>,
mut writer: EventWriter<audio::AudioEvent>,
) {
if selected.iter().len() == 1 {
writer.send(audio::AudioEvent::PickUp);
}
}
/// De-select anything that shouldn't be selected
/// Namely tiles when there are not selected pieces
fn null_selections(
events: Query<Entity, Added<Selected>>,
selected_pieces: Query<Entity, (With<Selected>, With<Piece>)>,
mut commands: Commands,
mut writer: EventWriter<audio::AudioEvent>,
) {
events.iter().for_each(|entity| {
if selected_pieces.is_empty() {
info!(
"De-selecting piece that should not be selected {:?}",
entity
);
commands.entity(entity).remove::<Selected>();
writer.send(audio::AudioEvent::PutDown);
}
});
}
/// When a piece's _BoardIndex_ is removed, we hide that entity from the viewer
fn capture_piece(mut events: Query<&mut Visibility, Added<Captured>>) {
events.iter_mut().for_each(|mut vis| {
info!("Hiding captured piece");
*vis = Visibility::Hidden
});
}
/// Panics if more than two pieces are selected at a time
fn asserts<T: Component>(
selected_pieces: Query<Entity, (With<Piece>, With<T>, With<Selected>)>,
selected_tiles: Query<Entity, (With<Tile>, With<T>, With<Selected>)>,
) {
if selected_pieces.iter().len() > 2 {
panic!("More than two pieces selected");
}
if selected_tiles.iter().len() > 2 {
panic!(
"More than two tiles selected {:?}",
selected_tiles.iter().len()
);
}
}