master
Mark 2024-03-04 17:46:05 -08:00
parent 17a42356be
commit 39fbf31af7
Signed by: Mark
GPG Key ID: C6D63995FE72FD80
7 changed files with 230 additions and 213 deletions

View File

@ -1,3 +1,5 @@
use anyhow::Result;
use super::{MinimizerAgent, RandomAgent}; use super::{MinimizerAgent, RandomAgent};
use crate::{ use crate::{
board::{Board, PlayerAction}, board::{Board, PlayerAction},
@ -67,7 +69,7 @@ impl DiffuseAgent {
} }
impl MinimizerAgent for DiffuseAgent { impl MinimizerAgent for DiffuseAgent {
fn step_min(&mut self, board: &Board) -> PlayerAction { fn step_min(&mut self, board: &Board) -> Result<PlayerAction> {
let symb = [Symb::Minus, Symb::Times, Symb::Plus, Symb::Div] let symb = [Symb::Minus, Symb::Times, Symb::Plus, Symb::Div]
.iter() .iter()
.filter(|x| !board.contains(**x)) .filter(|x| !board.contains(**x))
@ -77,7 +79,7 @@ impl MinimizerAgent for DiffuseAgent {
// No symbols available, play a random number // No symbols available, play a random number
RandomAgent {}.step_min(board) RandomAgent {}.step_min(board)
} else { } else {
self.step_symb(board, *symb.unwrap()) Ok(self.step_symb(board, *symb.unwrap()))
} }
} }
} }

View File

@ -1,19 +1,22 @@
mod diffuse; mod diffuse;
mod minmaxtree; mod minmaxtree;
mod player;
mod random; mod random;
pub use diffuse::DiffuseAgent; pub use diffuse::DiffuseAgent;
pub use minmaxtree::MinMaxTree; pub use minmaxtree::MinMaxTree;
pub use player::PlayerAgent;
pub use random::RandomAgent; pub use random::RandomAgent;
use crate::board::{Board, PlayerAction}; use crate::board::{Board, PlayerAction};
use anyhow::Result;
/// An agent that tries to minimize the value of a board. /// An agent that tries to minimize the value of a board.
pub trait MinimizerAgent { pub trait MinimizerAgent {
fn step_min(&mut self, board: &Board) -> PlayerAction; fn step_min(&mut self, board: &Board) -> Result<PlayerAction>;
} }
/// An agent that tries to maximize the value of a board. /// An agent that tries to maximize the value of a board.
pub trait MaximizerAgent { pub trait MaximizerAgent {
fn step_max(&mut self, board: &Board) -> PlayerAction; fn step_max(&mut self, board: &Board) -> Result<PlayerAction>;
} }

172
src/agents/player.rs Normal file
View File

@ -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<char>,
cursor: usize,
}
impl SymbolSelector {
fn new(symbols: Vec<char>) -> 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<PlayerAction> {
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<PlayerAction> {
self.step(board, true)
}
}
impl MaximizerAgent for PlayerAgent {
fn step_max(&mut self, board: &Board) -> Result<PlayerAction> {
self.step(board, false)
}
}

View File

@ -2,6 +2,7 @@ use crate::{
board::{Board, PlayerAction}, board::{Board, PlayerAction},
util::Symb, util::Symb,
}; };
use anyhow::Result;
use rand::Rng; use rand::Rng;
use std::num::NonZeroU8; use std::num::NonZeroU8;
@ -36,21 +37,21 @@ impl RandomAgent {
} }
impl MinimizerAgent for RandomAgent { impl MinimizerAgent for RandomAgent {
fn step_min(&mut self, board: &Board) -> PlayerAction { fn step_min(&mut self, board: &Board) -> Result<PlayerAction> {
let mut action = self.random_action(board); let mut action = self.random_action(board);
while !board.can_play(&action) { while !board.can_play(&action) {
action = self.random_action(board); action = self.random_action(board);
} }
action Ok(action)
} }
} }
impl MaximizerAgent for RandomAgent { impl MaximizerAgent for RandomAgent {
fn step_max(&mut self, board: &Board) -> PlayerAction { fn step_max(&mut self, board: &Board) -> Result<PlayerAction> {
let mut action = self.random_action(board); let mut action = self.random_action(board);
while !board.can_play(&action) { while !board.can_play(&action) {
action = self.random_action(board); action = self.random_action(board);
} }
action Ok(action)
} }
} }

View File

@ -170,7 +170,6 @@ impl TreeElement {
pub struct Board { pub struct Board {
board: [Option<(Symb, Player)>; 11], board: [Option<(Symb, Player)>; 11],
free_spots: usize, free_spots: usize,
current_player: Player,
} }
impl Display for Board { impl Display for Board {
@ -195,11 +194,10 @@ impl Display for Board {
#[allow(dead_code)] #[allow(dead_code)]
impl Board { impl Board {
pub fn new(current_player: Player) -> Self { pub fn new() -> Self {
Self { Self {
free_spots: 11, free_spots: 11,
board: Default::default(), board: Default::default(),
current_player,
} }
} }
@ -211,10 +209,6 @@ impl Board {
self.board.get(idx) self.board.get(idx)
} }
pub fn current_player(&self) -> Player {
self.current_player
}
pub fn is_done(&self) -> bool { pub fn is_done(&self) -> bool {
self.free_spots == 0 self.free_spots == 0
} }
@ -295,13 +289,12 @@ impl Board {
/// Place the marked symbol at the given position. /// Place the marked symbol at the given position.
/// Returns true for valid moves and false otherwise. /// 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) { if !self.can_play(&action) {
return false; return false;
} }
self.board[action.pos] = Some((action.symb, self.current_player)); self.board[action.pos] = Some((action.symb, player));
self.current_player.invert();
self.free_spots -= 1; self.free_spots -= 1;
true true
} }
@ -489,7 +482,7 @@ impl Board {
.filter_map(|c| { .filter_map(|c| {
if c == '_' { if c == '_' {
Some(None) 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))) Some(Some((symb, current_player)))
} else { } else {
None None
@ -510,10 +503,6 @@ impl Board {
} }
} }
Some(Self { Some(Self { board, free_spots })
board,
free_spots,
current_player,
})
} }
} }

View File

@ -1,12 +1,5 @@
use anyhow::Result; use anyhow::{bail, Result};
use std::io::{stdin, stdout, StdoutLock, Write}; use termion::color::{self};
use termion::{
color::{self},
cursor::HideCursor,
event::Key,
input::TermRead,
raw::IntoRawMode,
};
mod agents; mod agents;
mod board; mod board;
@ -14,172 +7,55 @@ mod util;
use board::Board; use board::Board;
use util::{Player, Symb}; use util::{Player, Symb};
use crate::board::PlayerAction;
fn play( fn play(
stdout: &mut StdoutLock, maxi: &mut dyn agents::MaximizerAgent,
player_max: bool, mini: &mut dyn agents::MinimizerAgent,
computer: &mut dyn agents::MinimizerAgent,
) -> Result<Board> { ) -> Result<Board> {
let mut cursor = 0usize; let mut board = Board::new();
let cursor_offset = 10usize - 1; let mut is_first_turn = true;
let cursor_max = 10usize; let mut is_maxi_turn = true;
let mut board = Board::new(if player_max { while !board.is_done() {
Player::Human // Print board
} else { println!(
Player::Computer "\r{}{}{}{}",
}); " ".repeat(6),
if is_first_turn { '╓' } else { '║' },
let mut is_first = true; board,
let mut print_board = true; if is_first_turn { '╖' } else { '║' },
// 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),
); );
stdout.flush()?; is_first_turn = false;
if board.is_done() { // Take action
break; let action = if is_maxi_turn {
} maxi.step_max(&board)?
} else {
mini.step_min(&board)?
};
is_maxi_turn = !is_maxi_turn;
// Player turn if !board.play(
let stdin = stdin(); action,
for c in stdin.keys() { if is_maxi_turn {
print_board = match c.unwrap() { Player::Human
Key::Char('q') => break 'outer, } else {
Key::Right => { Player::Computer
cursor = cursor_max.min(cursor + 1); },
false ) {
} bail!("agent made invalid move")
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;
} }
} }
return Ok(board); println!("\r{}{}", " ".repeat(6), board,);
println!("\r{}{}", " ".repeat(6), " ".repeat(board.size()));
Ok(board)
} }
fn main() -> Result<()> { fn main() -> Result<()> {
let stdout = HideCursor::from(stdout().into_raw_mode().unwrap()); let mut maxi = agents::PlayerAgent::new(Player::Human);
let mut stdout = stdout.lock(); let mut mini = agents::MinMaxTree {};
let mut agent = agents::DiffuseAgent {}; let a = play(&mut maxi, &mut mini)?;
let a = play(&mut stdout, true, &mut agent)?;
if a.is_done() { if a.is_done() {
println!( println!(
"\r\n{}Your score:{} {:.2}\n\n", "\r\n{}Your score:{} {:.2}\n\n",
@ -195,24 +71,5 @@ fn main() -> Result<()> {
); );
return Ok(()); 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(()) Ok(())
} }

View File

@ -12,13 +12,6 @@ pub enum Player {
} }
impl 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 { pub fn color(&self) -> &dyn Color {
match self { match self {
Player::Computer => &color::LightBlack, Player::Computer => &color::LightBlack,
@ -79,7 +72,7 @@ impl Symb {
self == &Self::Minus self == &Self::Minus
} }
pub fn from_char(c: &char) -> Option<Self> { pub fn from_char(c: char) -> Option<Self> {
match c { match c {
'1' => Some(Self::Number(NonZeroU8::new(1).unwrap())), '1' => Some(Self::Number(NonZeroU8::new(1).unwrap())),
'2' => Some(Self::Number(NonZeroU8::new(2).unwrap())), '2' => Some(Self::Number(NonZeroU8::new(2).unwrap())),