Implement major part of main game

This commit is contained in:
hodasemi 2023-05-11 14:00:24 +02:00
parent 2720750829
commit a83c3a4ab6
4 changed files with 435 additions and 190 deletions

26
.vscode/launch.json vendored Normal file
View file

@ -0,0 +1,26 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "Debug executable 'mill_game'",
"cargo": {
"args": [
"build",
"--bin=mill_game",
"--package=mill_game"
],
"filter": {
"name": "mill_game",
"kind": "bin"
}
},
"args": [],
"cwd": "${workspaceFolder}"
}
]
}

View file

@ -14,7 +14,7 @@ pub struct BoardSlot {
pub y: usize, pub y: usize,
pub z: usize, pub z: usize,
pub state: Mutex<BoardSlotState>, state: Mutex<BoardSlotState>,
pub position: Vector2<f32>, pub position: Vector2<f32>,
pub slot_marker: Option<Entity>, pub slot_marker: Option<Entity>,
} }
@ -100,10 +100,22 @@ impl BoardSlot {
Ok(mesh) Ok(mesh)
} }
pub fn state(&self) -> BoardSlotState {
*self.state.lock().unwrap()
}
pub fn set_state(&self, state: BoardSlotState) {
*self.state.lock().unwrap() = state;
}
pub fn valid(&self) -> bool { pub fn valid(&self) -> bool {
*self.state.lock().unwrap() != BoardSlotState::Invalid *self.state.lock().unwrap() != BoardSlotState::Invalid
} }
pub fn is_empty(&self) -> bool {
*self.state.lock().unwrap() == BoardSlotState::Empty
}
pub fn white(&self) -> bool { pub fn white(&self) -> bool {
match *self.state.lock().unwrap() { match *self.state.lock().unwrap() {
BoardSlotState::White(_) => true, BoardSlotState::White(_) => true,
@ -290,6 +302,75 @@ impl Board {
None None
} }
pub fn get_neighbours<'a>(
slot: &BoardSlot,
board: &'a [[[BoardSlot; 3]; 3]; 3],
) -> Vec<&'a BoardSlot> {
let mut neighbours = Vec::new();
for i in 0..6 {
// find free neighbour field
if let Some(neighbour) = match i {
0 => {
if slot.x > 0 {
Some(&board[slot.x - 1][slot.y][slot.z])
} else {
None
}
}
1 => {
if slot.x < 2 {
Some(&board[slot.x + 1][slot.y][slot.z])
} else {
None
}
}
2 => {
if slot.y > 0 {
Some(&board[slot.x][slot.y - 1][slot.z])
} else {
None
}
}
3 => {
if slot.y < 2 {
Some(&board[slot.x][slot.y + 1][slot.z])
} else {
None
}
}
4 => {
if slot.z > 0 {
Some(&board[slot.x][slot.y][slot.z - 1])
} else {
None
}
}
5 => {
if slot.z < 2 {
Some(&board[slot.x][slot.y][slot.z + 1])
} else {
None
}
}
_ => unreachable!(),
} {
let neighbour_state = neighbour.state.lock().unwrap();
match *neighbour_state {
BoardSlotState::Empty => {
neighbours.push(neighbour);
}
_ => (),
}
}
}
neighbours
}
} }
impl Board { impl Board {

View file

@ -27,6 +27,7 @@ pub struct Stone {
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)] #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
pub enum GameState { pub enum GameState {
Waiting,
Placing, Placing,
Removing, Removing,
Main, Main,
@ -60,6 +61,13 @@ impl PlayerColor {
} }
} }
} }
pub fn other(&self) -> Self {
match self {
PlayerColor::White => PlayerColor::Black,
PlayerColor::Black => PlayerColor::White,
}
}
} }
pub struct MillGame { pub struct MillGame {
@ -78,6 +86,8 @@ pub struct MillGame {
turn_finished: AtomicBool, turn_finished: AtomicBool,
selected_field: Mutex<Option<(usize, usize, usize)>>,
simple_ai: SimpleAI, simple_ai: SimpleAI,
white_player_label: Arc<Label>, white_player_label: Arc<Label>,
@ -185,7 +195,7 @@ impl MillGame {
white_stones: Mutex::new(white_stones.unwrap()), white_stones: Mutex::new(white_stones.unwrap()),
black_stones: Mutex::new(black_stones.unwrap()), black_stones: Mutex::new(black_stones.unwrap()),
state: Mutex::new(GameState::Placing), state: Mutex::new(GameState::Waiting),
current_player: Mutex::new(PlayerColor::White), current_player: Mutex::new(PlayerColor::White),
scene: Mutex::new(scene), scene: Mutex::new(scene),
@ -196,6 +206,8 @@ impl MillGame {
turn_finished: AtomicBool::new(false), turn_finished: AtomicBool::new(false),
selected_field: Mutex::default(),
simple_ai: SimpleAI::new(PlayerColor::White), simple_ai: SimpleAI::new(PlayerColor::White),
grid: grid.clone(), grid: grid.clone(),
@ -233,7 +245,7 @@ impl MillGame {
pub fn finish_turn(&self) { pub fn finish_turn(&self) {
Self::log(&format!( Self::log(&format!(
"{:?} finised turn", "{:?} finished turn",
*self.current_player.lock().unwrap() *self.current_player.lock().unwrap()
)); ));
@ -241,12 +253,10 @@ impl MillGame {
} }
fn next_game_step(&self) -> Result<()> { fn next_game_step(&self) -> Result<()> {
Self::log("next_game_step");
{ {
let mut state = self.state.lock().unwrap(); let mut state = self.state.lock().unwrap();
Self::log(&format!("\tstate: {:?}", state)); Self::log(&format!(" ===== NEW TURN ({:?}) =====", state));
match *state { match *state {
GameState::Placing => { GameState::Placing => {
@ -295,6 +305,7 @@ impl MillGame {
GameState::Main => { GameState::Main => {
self.current_player.lock().unwrap().swap(); self.current_player.lock().unwrap().swap();
} }
GameState::Waiting => unreachable!(),
} }
} }
@ -318,14 +329,14 @@ impl MillGame {
if player == self.simple_ai.player_color { if player == self.simple_ai.player_color {
Self::log("current player is AI"); Self::log("current player is AI");
self.simple_ai.step( if !self.simple_ai.step(
&mut self.stones(self.simple_ai.player_color), &mut self.stones(self.simple_ai.player_color),
self.board.lock().unwrap().slots(), self.board.lock().unwrap().slots(),
&mut *self.scene.lock().unwrap(), &mut *self.scene.lock().unwrap(),
*self.state.lock().unwrap(), &mut *self.state.lock().unwrap(),
)?; )? {
self.finish_turn();
self.finish_turn(); }
} }
Self::log("leave next_game_step"); Self::log("leave next_game_step");
@ -360,7 +371,7 @@ impl MillGame {
)?; )?;
let vertex_buffer = Buffer::builder() let vertex_buffer = Buffer::builder()
.set_data(&Objects::create_cylinder(1.0, 0.25, 12)) .set_data(&Objects::create_cylinder(1.1, 0.25, 30))
.set_usage(VK_BUFFER_USAGE_VERTEX_BUFFER_BIT) .set_usage(VK_BUFFER_USAGE_VERTEX_BUFFER_BIT)
.set_memory_usage(MemoryUsage::CpuOnly) .set_memory_usage(MemoryUsage::CpuOnly)
.build(engine.device().clone())?; .build(engine.device().clone())?;
@ -394,22 +405,26 @@ impl MillGame {
pub fn place_stone( pub fn place_stone(
stone: &mut Stone, stone: &mut Stone,
slot: &mut BoardSlot, (x, y, z): (usize, usize, usize),
board: &mut [[[BoardSlot; 3]; 3]; 3],
player_color: PlayerColor, player_color: PlayerColor,
scene: &mut Scene, scene: &mut Scene,
) -> Result<()> { state: &mut GameState,
) -> Result<MillState> {
Self::log("place_stone"); Self::log("place_stone");
let slot = &board[x][y][z];
match player_color { match player_color {
PlayerColor::White => { PlayerColor::White => {
Self::log(&format!("place white stone")); Self::log(&format!("place white stone"));
*slot.state.lock().unwrap() = BoardSlotState::White(stone.stone); slot.set_state(BoardSlotState::White(stone.stone));
} }
PlayerColor::Black => { PlayerColor::Black => {
Self::log(&format!("place black stone")); Self::log(&format!("place black stone"));
*slot.state.lock().unwrap() = BoardSlotState::Black(stone.stone); slot.set_state(BoardSlotState::Black(stone.stone));
} }
} }
@ -420,13 +435,67 @@ impl MillGame {
let location = entity.get_component_mut::<Location>()?; let location = entity.get_component_mut::<Location>()?;
location.set_position(slot.position.extend(0.0)); location.set_position(slot.position.extend(0.0));
Ok(()) let millstate = Self::check_for_mill(&board[x][y][z], board);
if millstate != MillState::None {
*state = GameState::Removing;
}
Self::log(&format!("return place stone with {:?}", millstate));
Ok(millstate)
} }
pub fn remove_stone(slot: &mut BoardSlot, scene: &mut Scene) -> Result<()> { pub fn move_stone(
(x, y, z): (usize, usize, usize),
(tx, ty, tz): (usize, usize, usize),
scene: &mut Scene,
board: &mut [[[BoardSlot; 3]; 3]; 3],
state: &mut GameState,
) -> Result<MillState> {
Self::log(&format!(
"move stone ({:?}) to ({:?})",
(x, y, z),
(tx, ty, tz)
));
let slot = &board[x][y][z];
let neighbour = &board[tx][ty][tz];
if neighbour.state() != BoardSlotState::Empty {
Self::log("neighbour not empty");
return Ok(MillState::None);
}
neighbour.set_state(slot.state());
slot.set_state(BoardSlotState::Empty);
scene
.entity_mut(match neighbour.state() {
BoardSlotState::Black(e) => e,
BoardSlotState::White(e) => e,
_ => unreachable!(),
})?
.get_component_mut::<Location>()?
.set_position(neighbour.position.extend(0.0));
let millstate = Self::check_for_mill(&board[tx][ty][tz], board);
if millstate != MillState::None {
Self::log("mill found!");
*state = GameState::Removing;
}
Self::log(&format!("return move stone with {:?}", millstate));
Ok(millstate)
}
pub fn remove_stone(stone: &mut Stone, slot: &mut BoardSlot, scene: &mut Scene) -> Result<()> {
Self::log("remove_stone"); Self::log("remove_stone");
let entity = match *slot.state.lock().unwrap() { let entity = match slot.state() {
BoardSlotState::Black(e) => { BoardSlotState::Black(e) => {
Self::log(&format!("\tremove black stone")); Self::log(&format!("\tremove black stone"));
@ -441,9 +510,11 @@ impl MillGame {
}; };
scene.remove_entity(entity)?; scene.remove_entity(entity)?;
Self::log("\t -> TODO: set stone state to Dead");
*slot.state.lock().unwrap() = BoardSlotState::Empty; assert_ne!(stone.state, StoneState::Dead);
stone.state = StoneState::Dead;
slot.set_state(BoardSlotState::Empty);
Ok(()) Ok(())
} }
@ -460,19 +531,25 @@ impl MillGame {
MillState::None MillState::None
} }
pub fn check_for_mill(&self, slot: &BoardSlot, board: &[[[BoardSlot; 3]; 3]; 3]) -> MillState { pub fn check_for_mill(slot: &BoardSlot, board: &[[[BoardSlot; 3]; 3]; 3]) -> MillState {
Self::log("check for mill"); Self::log("check for mill");
let state = Self::check_mill( if !(slot.x == 0 && slot.y == 0)
&board[slot.x][slot.y][0], && !(slot.x == 2 && slot.y == 0)
&board[slot.x][slot.y][1], && !(slot.x == 0 && slot.y == 2)
&board[slot.x][slot.y][2], && !(slot.x == 2 && slot.y == 2)
); {
let state = Self::check_mill(
&board[slot.x][slot.y][0],
&board[slot.x][slot.y][1],
&board[slot.x][slot.y][2],
);
if state != MillState::None { if state != MillState::None {
Self::log(&format!("mill found {:?}", state)); Self::log(&format!("mill found {:?}", state));
return state; return state;
}
} }
let state = Self::check_mill( let state = Self::check_mill(
@ -530,6 +607,21 @@ impl MillGame {
}) })
} }
fn is_neighbour((x, y, z): (usize, usize, usize), (tx, ty, tz): (usize, usize, usize)) -> bool {
if (x == tx && y == ty)
&& ((x == 0 && y == 0)
|| (x == 2 && y == 0)
|| (x == 0 && y == 2)
|| (x == 2 && y == 2))
{
return false;
}
(x as i32 - tx as i32).abs() == 1
|| (y as i32 - ty as i32).abs() == 1
|| (z as i32 - tz as i32).abs() == 1
}
pub fn log(s: &str) { pub fn log(s: &str) {
println!("{}", s); println!("{}", s);
} }
@ -561,86 +653,141 @@ impl EngineObject for MillGame {
&mut *self.scene.lock().unwrap(), &mut *self.scene.lock().unwrap(),
)?; )?;
} }
EngineEvent::MouseButtonDown(button) => match button { EngineEvent::MouseButtonDown(button) => {
MouseButton::Left => { match button {
let mut state = self.state.lock().unwrap(); MouseButton::Left => {
Self::log(&format!("User click at state {:?}", state)); let mut state = self.state.lock().unwrap();
Self::log(&format!("User click at state {:?}", state));
match *state { match *state {
GameState::Placing => { GameState::Placing => {
let mut placed = false; let mut placed = false;
let mut mill_found = false; let mut mill_found = false;
self.check_mouse_click(|me, (x, y, z), board, scene| { self.check_mouse_click(|me, (x, y, z), board, scene| {
let current_player = *self.current_player.lock().unwrap(); let current_player = *self.current_player.lock().unwrap();
if let Some(placable) = self if let Some(placable) = self
.stones(current_player) .stones(current_player)
.iter_mut() .iter_mut()
.find(|stone| stone.state == StoneState::ReadyToBePlaced) .find(|stone| stone.state == StoneState::ReadyToBePlaced)
{
Self::place_stone(
placable,
&mut board[x][y][z],
current_player,
scene,
)?;
if me.check_for_mill(&board[x][y][z], board) != MillState::None
{ {
mill_found = true; if board[x][y][z].is_empty() {
*state = GameState::Removing; if Self::place_stone(
placable,
(x, y, z),
board,
current_player,
scene,
&mut *state,
)? != MillState::None
{
mill_found = true;
}
placed = true;
} else {
Self::log(&format!(
"slot ({:?}), not empty ({:?})",
(x, y, z),
board[x][y][z].state()
));
}
} }
placed = true; Ok(())
})?;
if placed && !mill_found {
self.finish_turn();
} }
Ok(())
})?;
if placed && !mill_found {
self.finish_turn();
} }
} GameState::Removing => {
GameState::Removing => { let mut removed = false;
let mut removed = false;
self.check_mouse_click(|me, (x, y, z), board, scene| { self.check_mouse_click(|me, (x, y, z), board, scene| {
let slot = &mut board[x][y][z]; let slot = &mut board[x][y][z];
Self::log(&format!( Self::log(&format!(
"state {:?} - player {:?}", "state {:?} - player {:?}",
slot.state, slot.state(),
*self.current_player.lock().unwrap() *self.current_player.lock().unwrap()
)); ));
if match ( if match (slot.state(), *self.current_player.lock().unwrap()) {
*slot.state.lock().unwrap(), (BoardSlotState::Black(_), PlayerColor::White) => true,
*self.current_player.lock().unwrap(), (BoardSlotState::White(_), PlayerColor::Black) => true,
) { _ => false,
(BoardSlotState::Black(_), PlayerColor::White) => true, } {
(BoardSlotState::White(_), PlayerColor::Black) => true, let mut stones = self
_ => false, .stones(self.current_player.lock().unwrap().other());
} {
removed = true; let stone = stones
Self::remove_stone(slot, scene)?; .iter_mut()
.find(|s| {
s.stone
== match slot.state() {
BoardSlotState::Black(e) => e,
BoardSlotState::White(e) => e,
_ => unreachable!(),
}
})
.unwrap();
Self::remove_stone(stone, slot, scene)?;
removed = true;
}
Ok(())
})?;
if removed {
self.finish_turn();
} }
Ok(())
})?;
if removed {
self.finish_turn();
} }
GameState::Main => {
self.check_mouse_click(|me, (tx, ty, tz), board, scene| {
let mut selected_slot = me.selected_field.lock().unwrap();
match *selected_slot {
Some((x, y, z)) => {
if Self::is_neighbour((x, y, z), (tx, ty, tz)) {
if Self::move_stone((x,y,z), (tx,ty,tz), scene, board, &mut state)? == MillState::None {
*selected_slot = None;
self.finish_turn();
}
}
}
None => {
let slot = &board[tx][ty][tz];
match (
slot.state(),
*me.current_player.lock().unwrap(),
) {
(BoardSlotState::Black(_), PlayerColor::Black)
| (BoardSlotState::White(_), PlayerColor::White) => {
*selected_slot = Some((tx, ty, tz));
}
_ => (),
}
}
}
Ok(())
})?;
}
GameState::Waiting => (),
} }
GameState::Main => todo!(),
} }
MouseButton::Middle => self.camera_controls.lock().unwrap().hold(),
MouseButton::Right => (),
MouseButton::Forward => (),
MouseButton::Backward => (),
} }
MouseButton::Middle => self.camera_controls.lock().unwrap().hold(), }
MouseButton::Right => (),
MouseButton::Forward => (),
MouseButton::Backward => (),
},
EngineEvent::MouseButtonUp(button) => match button { EngineEvent::MouseButtonUp(button) => match button {
MouseButton::Left => (), MouseButton::Left => (),
MouseButton::Middle => self.camera_controls.lock().unwrap().release(), MouseButton::Middle => self.camera_controls.lock().unwrap().release(),

View file

@ -1,6 +1,6 @@
use crate::{ use crate::{
board::{BoardSlot, BoardSlotState}, board::{Board, BoardSlot, BoardSlotState},
game::{GameState, MillGame, PlayerColor, Stone, StoneState}, game::{GameState, MillGame, MillState, PlayerColor, Stone, StoneState},
}; };
use anyhow::Result; use anyhow::Result;
@ -20,29 +20,45 @@ impl SimpleAI {
stones: &mut [Stone; 9], stones: &mut [Stone; 9],
board: &mut [[[BoardSlot; 3]; 3]; 3], board: &mut [[[BoardSlot; 3]; 3]; 3],
scene: &mut SceneHandle, scene: &mut SceneHandle,
state: GameState, state: &mut GameState,
) -> Result<()> { ) -> Result<bool> {
MillGame::log(&format!("AI at state {:?}", state)); MillGame::log(&format!("AI at state {:?}", *state));
match state { let mut found_a_mill = false;
match *state {
GameState::Placing => { GameState::Placing => {
if let Some(placable) = stones if let Some(placable) = stones
.iter_mut() .iter_mut()
.find(|stone| stone.state == StoneState::ReadyToBePlaced) .find(|stone| stone.state == StoneState::ReadyToBePlaced)
{ {
let mut free_slots: Vec<&mut BoardSlot> = board let (x, y, z) = {
.iter_mut() let free_slots: Vec<&BoardSlot> = board
.flatten() .iter()
.flatten() .flatten()
.filter(|slot| *slot.state.lock().unwrap() == BoardSlotState::Empty) .flatten()
.collect(); .filter(|slot| slot.state() == BoardSlotState::Empty)
.collect();
let len = free_slots.len(); let len = free_slots.len();
let slot = &mut free_slots[Random::range(0, len as u32) as usize]; let slot = &free_slots[Random::range(0, len as u32) as usize];
(slot.x, slot.y, slot.z)
};
scene.on_scene(|scene| { scene.on_scene(|scene| {
MillGame::place_stone(placable, slot, self.player_color, scene)?; if MillGame::place_stone(
placable,
(x, y, z),
board,
self.player_color,
scene,
state,
)? != MillState::None
{
found_a_mill = true;
}
Ok(()) Ok(())
})?; })?;
@ -53,115 +69,90 @@ impl SimpleAI {
.iter_mut() .iter_mut()
.flatten() .flatten()
.flatten() .flatten()
.filter( .filter(|slot| match (slot.state(), self.player_color) {
|slot| match (*slot.state.lock().unwrap(), self.player_color) { (BoardSlotState::Black(_), PlayerColor::White) => true,
(BoardSlotState::Black(_), PlayerColor::White) => true, (BoardSlotState::White(_), PlayerColor::Black) => true,
(BoardSlotState::White(_), PlayerColor::Black) => true, _ => false,
_ => false, })
},
)
.collect(); .collect();
let len = other_color.len(); let len = other_color.len();
let slot = &mut other_color[Random::range(0, len as u32) as usize]; let slot: &mut &mut BoardSlot =
&mut other_color[Random::range(0, len as u32) as usize];
let stone = stones
.iter_mut()
.find(|s| {
s.stone
== match slot.state() {
BoardSlotState::Black(e) => e,
BoardSlotState::White(e) => e,
_ => unreachable!(),
}
})
.unwrap();
scene.on_scene(|scene| { scene.on_scene(|scene| {
MillGame::remove_stone(slot, scene)?; MillGame::remove_stone(stone, slot, scene)?;
Ok(()) Ok(())
})?; })?;
} }
GameState::Main => { GameState::Main => {
let mut same_color: Vec<&BoardSlot> = board let same_color: Vec<&BoardSlot> = board
.iter() .iter()
.flatten() .flatten()
.flatten() .flatten()
.filter( .filter(|slot| match (slot.state(), self.player_color) {
|slot| match (*slot.state.lock().unwrap(), self.player_color) { (BoardSlotState::White(_), PlayerColor::White) => true,
(BoardSlotState::White(_), PlayerColor::White) => true, (BoardSlotState::Black(_), PlayerColor::Black) => true,
(BoardSlotState::Black(_), PlayerColor::Black) => true, _ => false,
_ => false, })
},
)
.collect(); .collect();
loop { loop {
let len = same_color.len(); let len = same_color.len();
let slot = &mut same_color[Random::range(0, len as u32) as usize]; let slot = &same_color[Random::range(0, len as u32) as usize];
let n = Board::get_neighbours(slot, board);
let neighbours: Vec<&&BoardSlot> = n
.iter()
.filter(|n| n.state() == BoardSlotState::Empty)
.collect();
// find free neighbour field if !neighbours.is_empty() {
if let Some(neighbour) = match Random::range(0, 6) as usize { let neighbour =
0 => { &neighbours[Random::range(0, neighbours.len() as u32) as usize];
if slot.x > 0 {
Some(&board[slot.x - 1][slot.y][slot.z])
} else {
None
}
}
1 => {
if slot.x < 2 {
Some(&board[slot.x + 1][slot.y][slot.z])
} else {
None
}
}
2 => {
if slot.y > 0 {
Some(&board[slot.x][slot.y - 1][slot.z])
} else {
None
}
}
3 => {
if slot.y < 2 {
Some(&board[slot.x][slot.y + 1][slot.z])
} else {
None
}
}
4 => {
if slot.z > 0 {
Some(&board[slot.x][slot.y][slot.z - 1])
} else {
None
}
}
5 => {
if slot.z < 2 {
Some(&board[slot.x][slot.y][slot.z + 1])
} else {
None
}
}
// 6 => ,
// 7 => ,
// 8 => ,
// 9 => ,
_ => unreachable!(),
} {
let mut neighbour_state = neighbour.state.lock().unwrap();
let mut slot_state = slot.state.lock().unwrap();
match (*neighbour_state, *slot_state) { if neighbour.state() == BoardSlotState::Empty {
(BoardSlotState::Empty, BoardSlotState::Black(e)) => { let (x, y, z) = (slot.x, slot.y, slot.z);
*neighbour_state = BoardSlotState::Black(e); let (tx, ty, tz) = (neighbour.x, neighbour.y, neighbour.z);
*slot_state = BoardSlotState::Empty;
}
(BoardSlotState::Empty, BoardSlotState::White(e)) => {
*neighbour_state = BoardSlotState::White(e);
*slot_state = BoardSlotState::Empty;
}
_ => (), scene.on_scene(|scene| {
if MillGame::move_stone(
(x, y, z),
(tx, ty, tz),
scene,
board,
state,
)? != MillState::None
{
found_a_mill = true;
}
Ok(())
})?;
break;
} }
} }
} }
} }
GameState::Waiting => unreachable!(),
} }
MillGame::log("finish AI"); MillGame::log("finish AI");
Ok(()) Ok(found_a_mill)
} }
} }