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 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<PlayerAction> {
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()))
}
}
}

View File

@ -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<PlayerAction>;
}
/// 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<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},
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<PlayerAction> {
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<PlayerAction> {
let mut action = self.random_action(board);
while !board.can_play(&action) {
action = self.random_action(board);
}
action
Ok(action)
}
}

View File

@ -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 })
}
}

View File

@ -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<Board> {
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(())
}

View File

@ -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<Self> {
pub fn from_char(c: char) -> Option<Self> {
match c {
'1' => Some(Self::Number(NonZeroU8::new(1).unwrap())),
'2' => Some(Self::Number(NonZeroU8::new(2).unwrap())),