From 39fbf31af7b701a68959c47475198719602e7e84 Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 4 Mar 2024 17:46:05 -0800 Subject: [PATCH] Refactor --- src/agents/diffuse.rs | 6 +- src/agents/mod.rs | 7 +- src/agents/player.rs | 172 +++++++++++++++++++++++++++++++++ src/agents/random.rs | 9 +- src/board.rs | 21 +--- src/main.rs | 219 ++++++++---------------------------------- src/util.rs | 9 +- 7 files changed, 230 insertions(+), 213 deletions(-) create mode 100644 src/agents/player.rs diff --git a/src/agents/diffuse.rs b/src/agents/diffuse.rs index 7a4e4a8..b637dbe 100644 --- a/src/agents/diffuse.rs +++ b/src/agents/diffuse.rs @@ -1,3 +1,5 @@ +use anyhow::Result; + use super::{MinimizerAgent, RandomAgent}; use crate::{ board::{Board, PlayerAction}, @@ -67,7 +69,7 @@ impl DiffuseAgent { } impl MinimizerAgent for DiffuseAgent { - fn step_min(&mut self, board: &Board) -> PlayerAction { + fn step_min(&mut self, board: &Board) -> Result { let symb = [Symb::Minus, Symb::Times, Symb::Plus, Symb::Div] .iter() .filter(|x| !board.contains(**x)) @@ -77,7 +79,7 @@ impl MinimizerAgent for DiffuseAgent { // No symbols available, play a random number RandomAgent {}.step_min(board) } else { - self.step_symb(board, *symb.unwrap()) + Ok(self.step_symb(board, *symb.unwrap())) } } } diff --git a/src/agents/mod.rs b/src/agents/mod.rs index 6dc3951..5c20652 100644 --- a/src/agents/mod.rs +++ b/src/agents/mod.rs @@ -1,19 +1,22 @@ mod diffuse; mod minmaxtree; +mod player; mod random; pub use diffuse::DiffuseAgent; pub use minmaxtree::MinMaxTree; +pub use player::PlayerAgent; pub use random::RandomAgent; use crate::board::{Board, PlayerAction}; +use anyhow::Result; /// An agent that tries to minimize the value of a board. pub trait MinimizerAgent { - fn step_min(&mut self, board: &Board) -> PlayerAction; + fn step_min(&mut self, board: &Board) -> Result; } /// An agent that tries to maximize the value of a board. pub trait MaximizerAgent { - fn step_max(&mut self, board: &Board) -> PlayerAction; + fn step_max(&mut self, board: &Board) -> Result; } diff --git a/src/agents/player.rs b/src/agents/player.rs new file mode 100644 index 0000000..39cff2d --- /dev/null +++ b/src/agents/player.rs @@ -0,0 +1,172 @@ +use std::io::{stdin, stdout, Write}; + +use anyhow::{bail, Result}; +use termion::{color, cursor::HideCursor, event::Key, input::TermRead, raw::IntoRawMode}; + +use crate::{ + board::{Board, PlayerAction}, + util::{Player, Symb}, +}; + +use super::{MaximizerAgent, MinimizerAgent}; + +struct SymbolSelector { + symbols: Vec, + cursor: usize, +} + +impl SymbolSelector { + fn new(symbols: Vec) -> Self { + Self { symbols, cursor: 0 } + } + + fn current(&self) -> char { + self.symbols[self.cursor] + } + + fn check(&mut self, board: &Board) { + while board.contains(Symb::from_char(self.current()).unwrap()) { + if self.cursor == 0 { + self.cursor = self.symbols.len() - 1; + } else { + self.cursor -= 1; + } + } + } + + fn up(&mut self, board: &Board) { + if self.cursor == 0 { + self.cursor = self.symbols.len() - 1; + } else { + self.cursor -= 1; + } + + while board.contains(Symb::from_char(self.current()).unwrap()) { + if self.cursor == 0 { + self.cursor = self.symbols.len() - 1; + } else { + self.cursor -= 1; + } + } + } + + fn down(&mut self, board: &Board) { + if self.cursor == self.symbols.len() - 1 { + self.cursor = 0; + } else { + self.cursor += 1; + } + + while board.contains(Symb::from_char(self.current()).unwrap()) { + if self.cursor == self.symbols.len() - 1 { + self.cursor = 0; + } else { + self.cursor += 1; + } + } + } +} + +pub struct PlayerAgent { + player: Player, + cursor: usize, + symbol_selector: SymbolSelector, +} + +impl PlayerAgent { + pub fn new(player: Player) -> Self { + Self { + player, + cursor: 0, + symbol_selector: SymbolSelector::new(vec![ + '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '+', '-', '×', '÷', + ]), + } + } + + fn step(&mut self, board: &Board, minimize: bool) -> Result { + let stdout = HideCursor::from(stdout().into_raw_mode().unwrap()); + let mut stdout = stdout.lock(); + let cursor_max = board.size() - 1; + + self.symbol_selector.check(board); + + // Ask for input until we get a valid move + 'outer: loop { + print!( + "\r{}{}{} ╙{}{}{}{}{}╜", + // Goal + color::Fg(self.player.color()), + if minimize { "Min" } else { "Max" }, + color::Fg(color::Reset), + // Cursor + " ".repeat(self.cursor), + color::Fg(self.player.color()), + if board.is_done() { + ' ' + } else { + self.symbol_selector.current() + }, + color::Fg(color::Reset), + " ".repeat(cursor_max - self.cursor), + ); + stdout.flush()?; + + // Player turn + let stdin = stdin(); + for c in stdin.keys() { + match c.unwrap() { + Key::Char('q') => bail!("player ended game"), + Key::Right => { + self.cursor = cursor_max.min(self.cursor + 1); + } + Key::Left => { + if self.cursor != 0 { + self.cursor -= 1; + } + } + Key::Up => self.symbol_selector.up(board), + Key::Down => self.symbol_selector.down(board), + Key::Char('\n') => { + let symb = Symb::from_char(self.symbol_selector.current()); + if let Some(symb) = symb { + let action = PlayerAction { + symb, + pos: self.cursor, + }; + if board.can_play(&action) { + return Ok(action); + } + } + } + Key::Char(c) => { + let symb = Symb::from_char(c); + if let Some(symb) = symb { + let action = PlayerAction { + symb, + pos: self.cursor, + }; + if board.can_play(&action) { + return Ok(action); + } + } + } + _ => {} + }; + continue 'outer; + } + } + } +} + +impl MinimizerAgent for PlayerAgent { + fn step_min(&mut self, board: &Board) -> Result { + self.step(board, true) + } +} + +impl MaximizerAgent for PlayerAgent { + fn step_max(&mut self, board: &Board) -> Result { + self.step(board, false) + } +} diff --git a/src/agents/random.rs b/src/agents/random.rs index 949161f..bc3370b 100644 --- a/src/agents/random.rs +++ b/src/agents/random.rs @@ -2,6 +2,7 @@ use crate::{ board::{Board, PlayerAction}, util::Symb, }; +use anyhow::Result; use rand::Rng; use std::num::NonZeroU8; @@ -36,21 +37,21 @@ impl RandomAgent { } impl MinimizerAgent for RandomAgent { - fn step_min(&mut self, board: &Board) -> PlayerAction { + fn step_min(&mut self, board: &Board) -> Result { let mut action = self.random_action(board); while !board.can_play(&action) { action = self.random_action(board); } - action + Ok(action) } } impl MaximizerAgent for RandomAgent { - fn step_max(&mut self, board: &Board) -> PlayerAction { + fn step_max(&mut self, board: &Board) -> Result { let mut action = self.random_action(board); while !board.can_play(&action) { action = self.random_action(board); } - action + Ok(action) } } diff --git a/src/board.rs b/src/board.rs index d533ba0..dc75c35 100644 --- a/src/board.rs +++ b/src/board.rs @@ -170,7 +170,6 @@ impl TreeElement { pub struct Board { board: [Option<(Symb, Player)>; 11], free_spots: usize, - current_player: Player, } impl Display for Board { @@ -195,11 +194,10 @@ impl Display for Board { #[allow(dead_code)] impl Board { - pub fn new(current_player: Player) -> Self { + pub fn new() -> Self { Self { free_spots: 11, board: Default::default(), - current_player, } } @@ -211,10 +209,6 @@ impl Board { self.board.get(idx) } - pub fn current_player(&self) -> Player { - self.current_player - } - pub fn is_done(&self) -> bool { self.free_spots == 0 } @@ -295,13 +289,12 @@ impl Board { /// Place the marked symbol at the given position. /// Returns true for valid moves and false otherwise. - pub fn play(&mut self, action: PlayerAction) -> bool { + pub fn play(&mut self, action: PlayerAction, player: Player) -> bool { if !self.can_play(&action) { return false; } - self.board[action.pos] = Some((action.symb, self.current_player)); - self.current_player.invert(); + self.board[action.pos] = Some((action.symb, player)); self.free_spots -= 1; true } @@ -489,7 +482,7 @@ impl Board { .filter_map(|c| { if c == '_' { Some(None) - } else if let Some(symb) = Symb::from_char(&c) { + } else if let Some(symb) = Symb::from_char(c) { Some(Some((symb, current_player))) } else { None @@ -510,10 +503,6 @@ impl Board { } } - Some(Self { - board, - free_spots, - current_player, - }) + Some(Self { board, free_spots }) } } diff --git a/src/main.rs b/src/main.rs index 5e506e2..59b0158 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,5 @@ -use anyhow::Result; -use std::io::{stdin, stdout, StdoutLock, Write}; -use termion::{ - color::{self}, - cursor::HideCursor, - event::Key, - input::TermRead, - raw::IntoRawMode, -}; +use anyhow::{bail, Result}; +use termion::color::{self}; mod agents; mod board; @@ -14,172 +7,55 @@ mod util; use board::Board; use util::{Player, Symb}; -use crate::board::PlayerAction; - fn play( - stdout: &mut StdoutLock, - player_max: bool, - computer: &mut dyn agents::MinimizerAgent, + maxi: &mut dyn agents::MaximizerAgent, + mini: &mut dyn agents::MinimizerAgent, ) -> Result { - let mut cursor = 0usize; - let cursor_offset = 10usize - 1; - let cursor_max = 10usize; + let mut board = Board::new(); + let mut is_first_turn = true; + let mut is_maxi_turn = true; - let mut board = Board::new(if player_max { - Player::Human - } else { - Player::Computer - }); - - let mut is_first = true; - let mut print_board = true; - - // For human player UI - let symbols = [ - '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', '+', '-', '×', '÷', - ]; - let mut selected_symbol = 0; - - 'outer: loop { - // Computer turn - if board.current_player() == Player::Computer && !board.is_done() { - computer.step_min(&mut board); - } - - let min_color = if !player_max { - Player::Human.color() - } else { - Player::Computer.color() - }; - - let max_color = if player_max { - Player::Human.color() - } else { - Player::Computer.color() - }; - - if print_board { - println!( - "\r{}\r{}Min{}/{}Max{} {}{}{}", - " ".repeat(cursor_max + cursor_offset), - color::Fg(min_color), - color::Fg(color::Reset), - color::Fg(max_color), - color::Fg(color::Reset), - if is_first { '╓' } else { '║' }, - board, - if is_first { '╖' } else { '║' }, - ); - is_first = false; - } - - while board.contains(Symb::from_char(&symbols[selected_symbol]).unwrap()) { - if selected_symbol == symbols.len() - 1 { - selected_symbol = 0; - } else { - selected_symbol += 1; - } - } - - print!( - "\r{}╙{}{}{}{}{}╜", - " ".repeat(cursor_offset), - " ".repeat(cursor), - color::Fg(board.current_player().color()), - if board.is_done() { - ' ' - } else { - symbols[selected_symbol] - }, - color::Fg(color::Reset), - " ".repeat(cursor_max - cursor), + while !board.is_done() { + // Print board + println!( + "\r{}{}{}{}", + " ".repeat(6), + if is_first_turn { '╓' } else { '║' }, + board, + if is_first_turn { '╖' } else { '║' }, ); - stdout.flush()?; + is_first_turn = false; - if board.is_done() { - break; - } + // Take action + let action = if is_maxi_turn { + maxi.step_max(&board)? + } else { + mini.step_min(&board)? + }; + is_maxi_turn = !is_maxi_turn; - // Player turn - let stdin = stdin(); - for c in stdin.keys() { - print_board = match c.unwrap() { - Key::Char('q') => break 'outer, - Key::Right => { - cursor = cursor_max.min(cursor + 1); - false - } - Key::Left => { - if cursor != 0 { - cursor -= 1 - } - false - } - Key::Up => { - if selected_symbol == 0 { - selected_symbol = symbols.len() - 1; - } else { - selected_symbol -= 1; - } - - while board.contains(Symb::from_char(&symbols[selected_symbol]).unwrap()) { - if selected_symbol == 0 { - selected_symbol = symbols.len() - 1; - } else { - selected_symbol -= 1; - } - } - false - } - Key::Down => { - if selected_symbol == symbols.len() - 1 { - selected_symbol = 0; - } else { - selected_symbol += 1; - } - - while board.contains(Symb::from_char(&symbols[selected_symbol]).unwrap()) { - if selected_symbol == symbols.len() - 1 { - selected_symbol = 0; - } else { - selected_symbol += 1; - } - } - false - } - Key::Char('\n') => { - let symb = Symb::from_char(&symbols[selected_symbol]); - if let Some(symb) = symb { - let action = PlayerAction { symb, pos: cursor }; - board.play(action) - } else { - false - } - } - Key::Char(c) => { - let symb = Symb::from_char(&c); - if let Some(symb) = symb { - let action = PlayerAction { symb, pos: cursor }; - board.play(action) - } else { - false - } - } - _ => false, - }; - continue 'outer; + if !board.play( + action, + if is_maxi_turn { + Player::Human + } else { + Player::Computer + }, + ) { + bail!("agent made invalid move") } } - return Ok(board); + println!("\r{}║{}║", " ".repeat(6), board,); + println!("\r{}╙{}╜", " ".repeat(6), " ".repeat(board.size())); + Ok(board) } fn main() -> Result<()> { - let stdout = HideCursor::from(stdout().into_raw_mode().unwrap()); - let mut stdout = stdout.lock(); + let mut maxi = agents::PlayerAgent::new(Player::Human); + let mut mini = agents::MinMaxTree {}; - let mut agent = agents::DiffuseAgent {}; - let a = play(&mut stdout, true, &mut agent)?; + let a = play(&mut maxi, &mut mini)?; if a.is_done() { println!( "\r\n{}Your score:{} {:.2}\n\n", @@ -195,24 +71,5 @@ fn main() -> Result<()> { ); return Ok(()); } - - let mut agent = agents::RandomAgent {}; - let b = play(&mut stdout, false, &mut agent)?; - if b.is_done() { - println!( - "\r\n{}Computer score:{} {:.2}\n\n", - color::Fg(Player::Computer.color()), - color::Fg(color::Reset), - b.evaluate().unwrap() - ); - } else { - println!( - "\r\n{}Quitting{}\r\n", - color::Fg(color::Red), - color::Fg(color::Reset), - ); - return Ok(()); - } - Ok(()) } diff --git a/src/util.rs b/src/util.rs index 8ec3537..9256a43 100644 --- a/src/util.rs +++ b/src/util.rs @@ -12,13 +12,6 @@ pub enum Player { } impl Player { - pub fn invert(&mut self) { - match self { - Self::Human => *self = Self::Computer, - Self::Computer => *self = Self::Human, - } - } - pub fn color(&self) -> &dyn Color { match self { Player::Computer => &color::LightBlack, @@ -79,7 +72,7 @@ impl Symb { self == &Self::Minus } - pub fn from_char(c: &char) -> Option { + pub fn from_char(c: char) -> Option { match c { '1' => Some(Self::Number(NonZeroU8::new(1).unwrap())), '2' => Some(Self::Number(NonZeroU8::new(2).unwrap())),