From 6ec00a03ffd754a33448af554c3656a907046732 Mon Sep 17 00:00:00 2001 From: David Peter Date: Thu, 11 Jul 2024 20:47:52 +0200 Subject: [PATCH] Disentangle system dependencies --- doc/states_and_events.svg | 492 +++++++++++++++++++ src/gui/ai.rs | 76 ++- src/gui/board_update_event.rs | 13 + src/gui/graphics.rs | 9 +- src/gui/{io.rs => history.rs} | 24 +- src/gui/information_display.rs | 12 +- src/gui/interaction.rs | 117 +++-- src/gui/{keyboard.rs => keyboard_control.rs} | 8 +- src/gui/mod.rs | 7 +- src/gui/state.rs | 108 ---- src/gui/state_update.rs | 152 ++++++ src/main.rs | 47 +- src/yinsh/board.rs | 2 +- 13 files changed, 830 insertions(+), 237 deletions(-) create mode 100644 doc/states_and_events.svg create mode 100644 src/gui/board_update_event.rs rename src/gui/{io.rs => history.rs} (72%) rename src/gui/{keyboard.rs => keyboard_control.rs} (75%) delete mode 100644 src/gui/state.rs create mode 100644 src/gui/state_update.rs diff --git a/doc/states_and_events.svg b/doc/states_and_events.svg new file mode 100644 index 0000000..bc42884 --- /dev/null +++ b/doc/states_and_events.svg @@ -0,0 +1,492 @@ + + + + + + + + + + + + + PlayerActionEvent + + AiComputationEvent + + Event + + Resource + + GameState + + InteractionState + + System + + update state + + history (undo, load) + + perform AI actions + + + mouse interaction + + + start/cancel AI comp. + + + + + grid cursorupdate boardmove board elementscolorize board elements + + BoardUpdateEvent + + + + + + System ordering + + + + + + + + update state + + + (next cycle) + + AI task + + + + + + diff --git a/src/gui/ai.rs b/src/gui/ai.rs index dd991ca..8ba6f47 100644 --- a/src/gui/ai.rs +++ b/src/gui/ai.rs @@ -1,41 +1,80 @@ use bevy::prelude::*; use bevy::tasks::futures_lite::future; -use bevy::tasks::{block_on, Task}; +use bevy::tasks::{block_on, AsyncComputeTaskPool, Task}; -use yinsh::Action; +use yinsh::{Action, GameState, TurnMode}; -use super::state::PlayerActionEvent; +use super::graphics::ANIMATION_DURATION; +use super::state_update::{PlayerActionEvent, StateUpdateSet}; use super::PLAYER_AI; +#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] +pub struct AiSet; + +#[derive(Resource)] +pub struct AiPlayerStrength(pub usize); + +#[derive(Event)] +pub enum AiComputationEvent { + Start(GameState), + Cancel, +} + #[derive(Resource)] -pub struct AiTask(Option>); +struct AiTask(Option>); impl AiTask { - pub fn new() -> Self { + fn new() -> Self { Self(None) } - pub fn is_running(&self) -> bool { + fn is_running(&self) -> bool { self.0.is_some() } - pub fn start(&mut self, task: Task) { + fn start(&mut self, task: Task) { self.0 = Some(task); } - pub fn cancel(&mut self) { + fn cancel(&mut self) { self.0 = None; } - pub fn get_status(&mut self) -> Option { + fn get_status(&mut self) -> Option { block_on(future::poll_once(self.0.as_mut().unwrap())) } } -#[derive(Resource)] -pub struct AiPlayerStrength(pub usize); +fn manage_ai_tasks( + mut task: ResMut, + mut events: EventReader, + strength: Res, +) { + for event in events.read() { + match event { + AiComputationEvent::Start(ref game_state) => { + let task_pool = AsyncComputeTaskPool::get(); + + let game_state = game_state.clone(); + let search_depth = strength.0; + task.start(task_pool.spawn(async move { + // TODO! This is a hack to make sure the AI takes at least as long as + // the animation. + if matches!(game_state.turn_mode, TurnMode::MarkerPlacement) { + std::thread::sleep(ANIMATION_DURATION); + } + + yinsh::get_ai_player_action(search_depth, &game_state) + })); + } + AiComputationEvent::Cancel => { + task.cancel(); + } + } + } +} -pub fn wait_for_ai_move( +fn perform_ai_actions( mut task: ResMut, mut player_action_events: EventWriter, ) { @@ -55,3 +94,16 @@ pub fn wait_for_ai_move( player_action_events.send(PlayerActionEvent(PLAYER_AI, action)); } + +pub fn plugin(app: &mut App) { + app.insert_resource(AiTask::new()) + .insert_resource(AiPlayerStrength(11)) + .add_event::() + .add_systems( + Update, + (manage_ai_tasks, perform_ai_actions) + .chain() + .in_set(AiSet) + .after(StateUpdateSet), + ); +} diff --git a/src/gui/board_update_event.rs b/src/gui/board_update_event.rs new file mode 100644 index 0000000..331d2c2 --- /dev/null +++ b/src/gui/board_update_event.rs @@ -0,0 +1,13 @@ +use bevy::prelude::Event; + +use yinsh::{Coord, Player}; + +#[derive(Event)] +pub enum BoardUpdateEvent { + AddRing(Coord, Player), + AddMarker(Coord, Player), + MoveRing(Coord, Coord), + RemoveRing(Coord), + RemoveRun(Vec), + FlipMarkers(Vec), +} diff --git a/src/gui/graphics.rs b/src/gui/graphics.rs index 9239402..0deac47 100644 --- a/src/gui/graphics.rs +++ b/src/gui/graphics.rs @@ -11,6 +11,7 @@ use yinsh::{Coord, Player}; use super::{ board::{BoardElement, Marker, Ring}, + grid::draw_grid, PLAYER_HUMAN, }; @@ -117,6 +118,8 @@ fn setup_graphics( mut config_store: ResMut, mut materials: ResMut>, ) { + commands.insert_resource(ClearColor(COLOR_BACKGROUND)); + // Render layer 1 is for the grid commands.spawn(( Camera2dBundle { @@ -157,6 +160,8 @@ fn setup_graphics( config.render_layers = BACKGROUND_RENDER_LAYER; } -pub fn graphics_plugin(app: &mut App) { - app.add_systems(PreStartup, setup_graphics); +pub fn plugin(app: &mut App) { + app.insert_resource(Msaa::Sample8) + .add_systems(PreStartup, setup_graphics) + .add_systems(Update, draw_grid); } diff --git a/src/gui/io.rs b/src/gui/history.rs similarity index 72% rename from src/gui/io.rs rename to src/gui/history.rs index 06a34ef..10479fd 100644 --- a/src/gui/io.rs +++ b/src/gui/history.rs @@ -1,24 +1,23 @@ use bevy::prelude::*; use yinsh::Player; -use crate::gui::{ - graphics::{spawn_marker, spawn_ring}, - state::InteractionState, -}; +use crate::gui::graphics::{spawn_marker, spawn_ring}; use super::{ - ai::AiTask, board::BoardElement, graphics::PlayerColors, interaction::CursorElement, - state::GameState, + ai::AiComputationEvent, board::BoardElement, graphics::PlayerColors, + interaction::CursorElement, state_update::GameState, }; +#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] +pub struct HistorySet; + pub fn save_and_load_game_state( + keyboard: Res>, mut commands: Commands, mut meshes: ResMut>, player_colors: Res, - keyboard: Res>, mut game_state: ResMut, - mut interaction_state: ResMut, - mut ai_task: ResMut, + mut ai_computation_events: EventWriter, q_board_elements: Query, Without)>, ) { let filename = "gamestate.yml"; @@ -30,8 +29,7 @@ pub fn save_and_load_game_state( println!("Loading game state from {}", filename); *game_state.as_deref_mut() = yinsh::GameState::load_from(filename); - *interaction_state = InteractionState::from_turn_mode(&game_state); - ai_task.cancel(); + ai_computation_events.send(AiComputationEvent::Cancel); // Despawn all board elements for entity in q_board_elements.iter() { @@ -50,3 +48,7 @@ pub fn save_and_load_game_state( } } } + +pub fn plugin(app: &mut App) { + app.add_systems(Update, save_and_load_game_state.in_set(HistorySet)); +} diff --git a/src/gui/information_display.rs b/src/gui/information_display.rs index c399a50..f40398d 100644 --- a/src/gui/information_display.rs +++ b/src/gui/information_display.rs @@ -6,7 +6,7 @@ use super::{ ai::AiPlayerStrength, graphics::BACKGROUND_RENDER_LAYER, interaction::CursorCoord, - state::{GameState, InteractionState}, + state_update::{GameState, InteractionState}, PLAYER_HUMAN, }; @@ -48,11 +48,11 @@ fn update_information_display( points_a=game_state.points_a, points_b=game_state.points_b, mode=match *interaction_state { - InteractionState::RingPlacement => "Place a ring on the board", - InteractionState::MarkerPlacement => "Place a marker in one of your rings", + InteractionState::RingPlacement(_) => "Place a ring on the board", + InteractionState::MarkerPlacement(_) => "Place a marker in one of your rings", InteractionState::RingMovement(_, _) => "Move the selected ring", InteractionState::RunRemoval { .. } => "Select a run of five markers to remove", - InteractionState::RingRemoval => "Select one of your rings to remove it", + InteractionState::RingRemoval(_) => "Select one of your rings to remove it", InteractionState::AutoMove | InteractionState::WaitForAI => "AI is thinking...", InteractionState::Winner(Player::A) => "Game over. You win!", InteractionState::Winner(Player::B) => "Game over. Floyd wins!", @@ -62,7 +62,7 @@ fn update_information_display( ); } -pub fn information_display_plugin(app: &mut App) { +pub fn plugin(app: &mut App) { app.add_systems(Startup, setup_information_display) - .add_systems(Update, update_information_display); + .add_systems(Update, update_information_display.ambiguous_with_all()); } diff --git a/src/gui/interaction.rs b/src/gui/interaction.rs index e6a8bb7..e73d469 100644 --- a/src/gui/interaction.rs +++ b/src/gui/interaction.rs @@ -2,19 +2,21 @@ use bevy::prelude::*; use bevy::window::PrimaryWindow; +use bevy_tweening::AnimationSystem; use bevy_tweening::{lens::TransformPositionLens, Animator, EaseFunction, Tween, TweeningPlugin}; -use yinsh::{Action, Coord}; +use yinsh::{all_coords, Action, Coord}; use super::board::{BoardElement, Marker, Ring}; +use super::board_update_event::BoardUpdateEvent; use super::graphics::{ marker_mesh, ring_mesh, spawn_marker, spawn_ring, MainCamera, PlayerColors, ANIMATION_DURATION, FOREGROUND_RENDER_LAYER, }; -use super::state::{GameState, PlayerActionEvent}; +use super::state_update::{GameState, PlayerActionEvent, StateUpdateSet}; use super::PLAYER_HUMAN; use super::{ graphics::{screen_point, COLOR_RING_MOVEMENT_INDICATOR, SPACING}, - state::InteractionState, + state_update::InteractionState, }; #[derive(Component)] @@ -65,7 +67,7 @@ fn draw_ring_move_indicators(mut gizmos: Gizmos, interaction_state: Res, + mut board_update_events: EventReader, mut commands: Commands, mut meshes: ResMut>, player_colors: Res, @@ -74,29 +76,20 @@ fn update_board_elements( (With, (Without, Without)), >, mut q_markers: Query<(Entity, &mut BoardElement), (With, Without)>, - game_state: Res, ) { - for PlayerActionEvent(player, action) in player_action_events.read() { - match *action { - Action::PlaceRing(coord) => { - spawn_ring(&mut commands, &mut meshes, &player_colors, coord, *player); + for event in board_update_events.read() { + match *event { + BoardUpdateEvent::AddRing(coord, player) => { + spawn_ring(&mut commands, &mut meshes, &player_colors, coord, player); } - Action::PlaceMarker(coord) => { - spawn_marker(&mut commands, &mut meshes, &player_colors, coord, *player); + BoardUpdateEvent::AddMarker(coord, player) => { + spawn_marker(&mut commands, &mut meshes, &player_colors, coord, player); } - Action::MoveRing(old_coord, new_coord) => { + BoardUpdateEvent::MoveRing(old_coord, new_coord) => { for (entity, mut ring) in q_rings.iter_mut() { if ring.0 == old_coord { ring.0 = new_coord; - // Flip markers between old and new coord - let coords_between = Coord::between(old_coord, new_coord); - for (_, mut element) in q_markers.iter_mut() { - if coords_between.contains(&element.0) { - element.1.flip(); - } - } - let tween = Tween::new( EaseFunction::QuadraticInOut, ANIMATION_DURATION, @@ -112,16 +105,14 @@ fn update_board_elements( } } } - Action::RemoveRun(seed) => { - let run_coords = game_state.board.run_coords_from(seed).unwrap(); - + BoardUpdateEvent::RemoveRun(ref run_coords) => { for (entity, element) in q_markers.iter_mut() { if run_coords.contains(&element.0) { commands.entity(entity).despawn(); } } } - Action::RemoveRing(coord) => { + BoardUpdateEvent::RemoveRing(coord) => { for (entity, element) in q_rings.iter_mut() { if element.0 == coord { commands.entity(entity).despawn(); @@ -129,7 +120,13 @@ fn update_board_elements( } } } - Action::Wait => {} + BoardUpdateEvent::FlipMarkers(ref marker_coords) => { + for (_, mut element) in q_markers.iter_mut() { + if marker_coords.contains(&element.0) { + element.1.flip(); + } + } + } } } } @@ -153,7 +150,6 @@ fn colorize_board_elements( interaction_state: Res, player_colors: Res, mouse_cursor_coord: Res, - game_state: Res, ) { for (BoardElement(coord, player), mut color_material, ring, marker, cursor_element) in query.iter_mut() @@ -170,10 +166,12 @@ fn colorize_board_elements( player_colors.human.clone() } } - InteractionState::RunRemoval { ref run_coords } => match mouse_cursor_coord.0 { - Some(cursor_coord) if run_coords.contains(&cursor_coord) => { - let run_from_cursor = - game_state.board.run_coords_from(cursor_coord).unwrap(); + InteractionState::RunRemoval { + ref all_run_coords, + ref run_from_seed, + } => match mouse_cursor_coord.0 { + Some(cursor_coord) if all_run_coords.contains(&cursor_coord) => { + let run_from_cursor = run_from_seed.get(&cursor_coord).unwrap(); if run_from_cursor.contains(coord) { player_colors.human_highlighted.clone() } else { @@ -181,7 +179,7 @@ fn colorize_board_elements( } } _ => { - if marker.is_some() && run_coords.contains(coord) { + if marker.is_some() && all_run_coords.contains(coord) { player_colors.human_highlighted.clone() } else { player_colors.human.clone() @@ -198,7 +196,6 @@ fn colorize_board_elements( } fn mouse_cursor_system( - game_state: Res, mut q_window: Query<&mut Window, With>, q_camera: Query<(&Camera, &GlobalTransform), With>, mut cursor_ring: Query< @@ -247,17 +244,14 @@ fn mouse_cursor_system( *cursor_marker_visibility = Visibility::Hidden; match *interaction_state { - InteractionState::RingPlacement => { - if game_state.board.is_free(cursor_coord) { + InteractionState::RingPlacement(ref free_coords) => { + if free_coords.contains(&cursor_coord) { *cursor_ring_visibility = Visibility::Visible; cursor_ring_coord.0 = cursor_coord; } } - InteractionState::MarkerPlacement => { - if game_state - .board - .can_place_marker_at(cursor_coord, PLAYER_HUMAN) - { + InteractionState::MarkerPlacement(ref ring_coords) => { + if ring_coords.contains(&cursor_coord) { *cursor_marker_visibility = Visibility::Visible; cursor_marker_coord.0 = cursor_coord; } @@ -269,7 +263,7 @@ fn mouse_cursor_system( } } InteractionState::RunRemoval { .. } => {} - InteractionState::RingRemoval => {} + InteractionState::RingRemoval(_) => {} InteractionState::AutoMove => {} InteractionState::WaitForAI => {} InteractionState::Winner(_) => {} @@ -281,7 +275,6 @@ fn mouse_cursor_system( } fn mouse_interaction_system( - game_state: Res, buttons: Res>, interaction_state: Res, cursor_coord: Res, @@ -295,19 +288,16 @@ fn mouse_interaction_system( if let Some(cursor_coord) = cursor_coord.0 { if buttons.just_pressed(MouseButton::Left) { match *interaction_state { - InteractionState::RingPlacement => { - if game_state.board.is_free(cursor_coord) { + InteractionState::RingPlacement(ref free_coords) => { + if free_coords.contains(&cursor_coord) { player_action_events.send(PlayerActionEvent( PLAYER_HUMAN, Action::PlaceRing(cursor_coord), )); } } - InteractionState::MarkerPlacement => { - if game_state - .board - .can_place_marker_at(cursor_coord, PLAYER_HUMAN) - { + InteractionState::MarkerPlacement(ref ring_coords) => { + if ring_coords.contains(&cursor_coord) { player_action_events.send(PlayerActionEvent( PLAYER_HUMAN, Action::PlaceMarker(cursor_coord), @@ -323,16 +313,18 @@ fn mouse_interaction_system( } } InteractionState::WaitForAI => {} - InteractionState::RunRemoval { ref run_coords } => { - if run_coords.contains(&cursor_coord) { + InteractionState::RunRemoval { + ref all_run_coords, .. + } => { + if all_run_coords.contains(&cursor_coord) { player_action_events.send(PlayerActionEvent( PLAYER_HUMAN, Action::RemoveRun(cursor_coord), )); } } - InteractionState::RingRemoval => { - if game_state.board.has_ring_at(cursor_coord, PLAYER_HUMAN) { + InteractionState::RingRemoval(ref ring_coords) => { + if ring_coords.contains(&cursor_coord) { player_action_events.send(PlayerActionEvent( PLAYER_HUMAN, Action::RemoveRing(cursor_coord), @@ -346,18 +338,25 @@ fn mouse_interaction_system( } } -pub fn interaction_plugin(app: &mut App) { +pub fn plugin(app: &mut App) { app.add_plugins(TweeningPlugin) + .insert_resource(InteractionState::RingPlacement(all_coords())) + .insert_resource(CursorCoord(None)) + .insert_resource(GameState::initial()) .add_systems(Startup, setup_interaction_cursors) .add_systems( Update, ( draw_ring_move_indicators, - update_board_elements, - mouse_cursor_system, - mouse_interaction_system, - move_board_elements, - colorize_board_elements, - ), + ( + mouse_cursor_system, + update_board_elements, + move_board_elements.ambiguous_with(AnimationSystem::AnimationUpdate), + colorize_board_elements.ambiguous_with(AnimationSystem::AnimationUpdate), + mouse_interaction_system, + ) + .chain(), + ) + .after(StateUpdateSet), ); } diff --git a/src/gui/keyboard.rs b/src/gui/keyboard_control.rs similarity index 75% rename from src/gui/keyboard.rs rename to src/gui/keyboard_control.rs index d490096..0859589 100644 --- a/src/gui/keyboard.rs +++ b/src/gui/keyboard_control.rs @@ -1,8 +1,8 @@ use bevy::prelude::*; -use super::ai::AiPlayerStrength; +use super::ai::{AiPlayerStrength, AiSet}; -pub fn keyboard_control( +fn keyboard_control( keyboard: Res>, mut exit: EventWriter, mut ai_player_strength: ResMut, @@ -15,3 +15,7 @@ pub fn keyboard_control( ai_player_strength.0 = (ai_player_strength.0 - 1).max(1); } } + +pub fn plugin(app: &mut App) { + app.add_systems(Update, keyboard_control.ambiguous_with(AiSet)); +} diff --git a/src/gui/mod.rs b/src/gui/mod.rs index e843ccb..04a69b3 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -2,14 +2,15 @@ use yinsh::Player; pub mod ai; pub mod board; +pub mod board_update_event; pub mod graphics; pub mod grid; +pub mod history; pub mod information_display; pub mod interaction; -pub mod io; -pub mod keyboard; +pub mod keyboard_control; pub mod resources; -pub mod state; +pub mod state_update; pub const PLAYER_HUMAN: Player = Player::A; pub const PLAYER_AI: Player = Player::B; diff --git a/src/gui/state.rs b/src/gui/state.rs deleted file mode 100644 index 43ec125..0000000 --- a/src/gui/state.rs +++ /dev/null @@ -1,108 +0,0 @@ -use std::ops::{Deref, DerefMut}; - -use yinsh::{Action, Coord, Player, TurnMode}; - -use bevy::{prelude::*, tasks::AsyncComputeTaskPool}; - -use crate::gui::{graphics::ANIMATION_DURATION, PLAYER_AI, PLAYER_HUMAN}; - -use super::ai::{AiPlayerStrength, AiTask}; - -#[derive(Event)] -pub struct PlayerActionEvent(pub Player, pub Action); - -#[derive(Resource)] -pub struct GameState(yinsh::GameState); - -impl Deref for GameState { - type Target = yinsh::GameState; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for GameState { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -impl GameState { - pub fn initial() -> Self { - Self(yinsh::GameState::initial()) - } -} - -#[derive(Resource)] -pub enum InteractionState { - RingPlacement, - MarkerPlacement, - RingMovement(Coord, Vec), - RunRemoval { run_coords: Vec }, - RingRemoval, - AutoMove, - WaitForAI, - Winner(Player), -} - -impl InteractionState { - pub fn from_turn_mode(game_state: &yinsh::GameState) -> Self { - assert!(game_state.active_player == PLAYER_HUMAN); - - match game_state.turn_mode { - TurnMode::RingPlacement => Self::RingPlacement, - TurnMode::MarkerPlacement => Self::MarkerPlacement, - TurnMode::RingMovement(start) => { - Self::RingMovement(start, game_state.board.ring_moves(start)) - } - TurnMode::RunRemoval(_) => Self::RunRemoval { - run_coords: game_state.board.run_coords(PLAYER_HUMAN), - }, - TurnMode::RingRemoval(_) => Self::RingRemoval, - TurnMode::WaitForRunRemoval(_) - | TurnMode::WaitForMarkerPlacement - | TurnMode::WaitForRingMovement(_) - | TurnMode::WaitForRingRemoval(_) => Self::AutoMove, - } - } -} - -pub fn update_game_state( - mut game_state: ResMut, - mut player_action_events: EventReader, - mut interaction_state: ResMut, - mut task: ResMut, - ai_player_strength: Res, -) { - for PlayerActionEvent(player, action) in player_action_events.read() { - assert!(player == &game_state.0.active_player); - - game_state.0.transition(action); - - if let Some(winner) = game_state.0.winner() { - *interaction_state = InteractionState::Winner(winner); - return; - } - - if game_state.0.active_player == PLAYER_AI { - let task_pool = AsyncComputeTaskPool::get(); - - let game_state = game_state.0.clone(); - let search_depth = ai_player_strength.0; - task.start(task_pool.spawn(async move { - // TODO! This is a hack to make sure the AI takes at least as long as - // the animation. - if matches!(game_state.turn_mode, TurnMode::MarkerPlacement) { - std::thread::sleep(ANIMATION_DURATION); - } - - yinsh::get_ai_player_action(search_depth, &game_state) - })); - - *interaction_state = InteractionState::WaitForAI; - } else { - *interaction_state = InteractionState::from_turn_mode(&game_state.0); - } - } -} diff --git a/src/gui/state_update.rs b/src/gui/state_update.rs new file mode 100644 index 0000000..f6fa27c --- /dev/null +++ b/src/gui/state_update.rs @@ -0,0 +1,152 @@ +use std::ops::{Deref, DerefMut}; + +use yinsh::{Action, Coord, Player, TurnMode}; + +use bevy::{prelude::*, utils::HashMap}; + +use crate::gui::PLAYER_HUMAN; + +use super::{ai::AiComputationEvent, board_update_event::BoardUpdateEvent, PLAYER_AI}; + +#[derive(SystemSet, Debug, Clone, PartialEq, Eq, Hash)] +pub struct StateUpdateSet; + +#[derive(Resource)] +pub struct GameState(yinsh::GameState); + +impl Deref for GameState { + type Target = yinsh::GameState; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for GameState { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl GameState { + pub fn initial() -> Self { + Self(yinsh::GameState::initial()) + } +} + +#[derive(Resource)] +pub enum InteractionState { + RingPlacement(Vec), + MarkerPlacement(Vec), + RingMovement(Coord, Vec), + RunRemoval { + all_run_coords: Vec, + run_from_seed: HashMap>, + }, + RingRemoval(Vec), + AutoMove, + WaitForAI, + Winner(Player), +} + +impl InteractionState { + pub fn from_game_state(game_state: &yinsh::GameState) -> Self { + if let Some(winner) = game_state.winner() { + Self::Winner(winner) + } else if game_state.active_player == PLAYER_AI { + Self::WaitForAI + } else { + match game_state.turn_mode { + TurnMode::RingPlacement => { + Self::RingPlacement(game_state.board.free_coords().collect()) + } + TurnMode::MarkerPlacement => Self::MarkerPlacement( + game_state + .board + .ring_coords(PLAYER_HUMAN) + .filter(|c| game_state.board.can_place_marker_at(*c, PLAYER_HUMAN)) + .collect(), + ), + TurnMode::RingMovement(start) => { + Self::RingMovement(start, game_state.board.ring_moves(start)) + } + TurnMode::RunRemoval(_) => { + let all_run_coords = game_state.board.run_coords(PLAYER_HUMAN); + Self::RunRemoval { + all_run_coords: all_run_coords.clone(), + run_from_seed: all_run_coords + .into_iter() + .map(|seed| (seed, game_state.board.run_coords_from(seed).unwrap())) + .collect(), + } + } + TurnMode::RingRemoval(player) => { + Self::RingRemoval(game_state.board.ring_coords(player).collect()) + } + TurnMode::WaitForRunRemoval(_) + | TurnMode::WaitForMarkerPlacement + | TurnMode::WaitForRingMovement(_) + | TurnMode::WaitForRingRemoval(_) => Self::AutoMove, + } + } + } +} + +#[derive(Event)] +pub struct PlayerActionEvent(pub Player, pub Action); + +fn state_update( + mut player_action_events: EventReader, + mut game_state: ResMut, + mut interaction_state: ResMut, + mut ai_computation_events: EventWriter, + mut board_update_events: EventWriter, +) { + for PlayerActionEvent(player, action) in player_action_events.read() { + assert!(player == &game_state.active_player); + + match action { + Action::PlaceRing(coord) => { + board_update_events.send(BoardUpdateEvent::AddRing(*coord, *player)); + } + Action::PlaceMarker(coord) => { + board_update_events.send(BoardUpdateEvent::AddMarker(*coord, *player)); + } + Action::MoveRing(start, end) => { + board_update_events.send(BoardUpdateEvent::MoveRing(*start, *end)); + board_update_events.send(BoardUpdateEvent::FlipMarkers( + Coord::between(*start, *end) + .into_iter() + .filter(|&coord| game_state.board.has_marker_at(coord)) + .collect(), + )); + } + Action::RemoveRun(seed) => { + board_update_events.send(BoardUpdateEvent::RemoveRun( + game_state.board.run_coords_from(*seed).unwrap(), + )); + } + Action::RemoveRing(coord) => { + board_update_events.send(BoardUpdateEvent::RemoveRing(*coord)); + } + Action::Wait => {} + } + + game_state.transition(action); + + *interaction_state = InteractionState::from_game_state(&game_state); + + if game_state.active_player == PLAYER_AI && game_state.winner().is_none() { + ai_computation_events.send(AiComputationEvent::Start(game_state.clone())); + } + } +} + +pub fn plugin(app: &mut App) { + let initial_game_state = GameState::initial(); + app.insert_resource(InteractionState::from_game_state(&initial_game_state)) + .insert_resource(initial_game_state) + .add_event::() + .add_event::() + .add_systems(Update, state_update.in_set(StateUpdateSet)); +} diff --git a/src/main.rs b/src/main.rs index 4ee1739..781ea27 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,21 +1,11 @@ mod gui; use bevy::{ + ecs::schedule::{LogLevel, ScheduleBuildSettings}, prelude::*, window::{PresentMode, WindowMode}, }; -use gui::{ - ai::{wait_for_ai_move, AiPlayerStrength, AiTask}, - graphics::{graphics_plugin, COLOR_BACKGROUND}, - grid::draw_grid, - information_display::information_display_plugin, - interaction::{interaction_plugin, CursorCoord}, - io::save_and_load_game_state, - keyboard::keyboard_control, - state::{update_game_state, GameState, InteractionState, PlayerActionEvent}, -}; - fn main() { App::new() .add_plugins(( @@ -30,28 +20,19 @@ fn main() { }), ..default() }), - graphics_plugin, - information_display_plugin, - interaction_plugin, + gui::state_update::plugin, + gui::ai::plugin, + gui::graphics::plugin, + gui::interaction::plugin, + gui::information_display::plugin, + gui::keyboard_control::plugin, + // gui::history::plugin, )) - .add_systems( - Update, - ( - save_and_load_game_state, - wait_for_ai_move, - update_game_state, - draw_grid, - keyboard_control, - ) - .chain(), - ) - .insert_resource(ClearColor(COLOR_BACKGROUND)) - .insert_resource(Msaa::Sample8) - .insert_resource(InteractionState::RingPlacement) - .insert_resource(AiTask::new()) - .insert_resource(AiPlayerStrength(11)) - .insert_resource(CursorCoord(None)) - .insert_resource(GameState::initial()) - .add_event::() + .edit_schedule(Update, |schedule| { + schedule.set_build_settings(ScheduleBuildSettings { + ambiguity_detection: LogLevel::Warn, + ..default() + }); + }) .run(); } diff --git a/src/yinsh/board.rs b/src/yinsh/board.rs index 7b64e96..5dd1ce2 100644 --- a/src/yinsh/board.rs +++ b/src/yinsh/board.rs @@ -91,7 +91,7 @@ impl Board { } /// Returns true if the element at the given point is a marker of any color. - fn has_marker_at(&self, coord: Coord) -> bool { + pub fn has_marker_at(&self, coord: Coord) -> bool { self.check_invariants(); self.element_at(coord).map_or(false, |e| e.is_marker())