From 2d7432c228b3b62170eecf364fd8e33955b627db Mon Sep 17 00:00:00 2001 From: Mark Date: Mon, 26 Feb 2024 08:54:35 -0800 Subject: [PATCH] Added basic game with random agent --- Cargo.lock | 139 ++++++++++++++++++++++++++++++++ Cargo.toml | 9 +++ src/board.rs | 219 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 169 ++++++++++++++++++++++++++++++++++++++ src/random.rs | 50 ++++++++++++ src/util.rs | 72 +++++++++++++++++ 6 files changed, 658 insertions(+) create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 src/board.rs create mode 100644 src/main.rs create mode 100644 src/random.rs create mode 100644 src/util.rs diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..0b16be1 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,139 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "anyhow" +version = "1.0.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "getrandom" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "libc" +version = "0.2.153" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" + +[[package]] +name = "libredox" +version = "0.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3af92c55d7d839293953fcd0fda5ecfe93297cfde6ffbdec13b41d99c0ba6607" +dependencies = [ + "bitflags 2.4.2", + "libc", + "redox_syscall", +] + +[[package]] +name = "numtoa" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8f8bdf33df195859076e54ab11ee78a1b208382d3a26ec40d142ffc1ecc49ef" + +[[package]] +name = "ops" +version = "0.1.0" +dependencies = [ + "anyhow", + "rand", + "termion", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha", + "rand_core", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_termios" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20145670ba436b55d91fc92d25e71160fbfbdd57831631c8d7d36377a476f1cb" + +[[package]] +name = "termion" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "417813675a504dfbbf21bfde32c03e5bf9f2413999962b479023c02848c1c7a5" +dependencies = [ + "libc", + "libredox", + "numtoa", + "redox_termios", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..4f51f1c --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "ops" +version = "0.1.0" +edition = "2021" + +[dependencies] +anyhow = "1.0.80" +rand = "0.8.5" +termion = "3.0.0" diff --git a/src/board.rs b/src/board.rs new file mode 100644 index 0000000..90b226d --- /dev/null +++ b/src/board.rs @@ -0,0 +1,219 @@ +use std::fmt::Display; +use termion::color; + +use crate::{Player, Symb}; + +#[derive(Debug, PartialEq)] +enum Token { + Number(f32), + OpAdd, + OpSub, + OpMult, + OpDiv, +} + +impl Token { + fn val(&self) -> f32 { + match self { + Self::Number(x) => *x, + _ => unreachable!(), + } + } +} + +pub struct Board { + board: [Option<(Symb, Player)>; 11], + free_spots: usize, + current_player: Player, +} + +impl Display for Board { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + // Print board + for i in &self.board { + match i { + Some((symb, player)) => write!( + f, + "{}{}{}", + color::Fg(player.color()), + symb, + color::Fg(color::Reset) + )?, + + None => write!(f, "_")?, + } + } + Ok(()) + } +} + +impl Board { + pub fn new(current_player: Player) -> Self { + Self { + free_spots: 11, + board: Default::default(), + current_player, + } + } + + pub fn current_player(&self) -> Player { + self.current_player + } + + pub fn is_done(&self) -> bool { + self.free_spots == 0 + } + + pub fn size(&self) -> usize { + self.board.len() + } + + // Place the marked symbol at the given position. + // Returns true for valid moves and false otherwise. + pub fn play(&mut self, cursor: usize, symb: Symb) -> bool { + match &self.board[cursor] { + Some(_) => return false, + None => { + // Check for duplicate symbols + for i in &self.board { + if let Some(i) = i { + if i.0 == symb { + return false; + } + } + } + + // Check syntax + match symb { + Symb::Minus => { + if cursor == self.board.len() - 1 { + return false; + } + + let r = &self.board[cursor + 1]; + if r.is_some_and(|(s, _)| s.is_binop()) { + return false; + } + } + + Symb::Zero => { + if cursor != 0 { + let l = &self.board[cursor - 1].map(|x| x.0); + if l == &Some(Symb::Div) { + return false; + } + } + } + + Symb::Div | Symb::Plus | Symb::Times => { + if cursor == 0 || cursor == self.board.len() - 1 { + return false; + } + + let l = &self.board[cursor - 1].map(|x| x.0); + let r = &self.board[cursor + 1].map(|x| x.0); + + if symb == Symb::Div && r == &Some(Symb::Zero) { + return false; + } + + if l.is_some_and(|s| s.is_binop() || s.is_minus()) + || r.is_some_and(|s| s.is_binop()) + { + return false; + } + } + _ => {} + } + + self.board[cursor] = Some((symb, self.current_player)); + } + } + + self.current_player.invert(); + self.free_spots -= 1; + true + } + + pub fn evaluate(&self) -> Option { + if !self.is_done() { + return None; + } + + let mut tokens = Vec::new(); + let mut is_neg = true; // if true, - is negative. if false, subtract. + let mut current_num = 0f32; + let mut current_sgn = 1f32; + + for (s, _) in self.board.iter().map(|x| x.unwrap()) { + match s { + Symb::Div => { + tokens.push(Token::Number(current_num * current_sgn)); + current_num = 0.0; + current_sgn = 1.0; + tokens.push(Token::OpDiv); + is_neg = true; + } + Symb::Minus => { + if is_neg { + current_sgn = -1.0; + } else { + tokens.push(Token::Number(current_num * current_sgn)); + current_num = 0.0; + current_sgn = 1.0; + tokens.push(Token::OpSub); + is_neg = true; + } + } + Symb::Plus => { + tokens.push(Token::Number(current_num * current_sgn)); + current_num = 0.0; + current_sgn = 1.0; + tokens.push(Token::OpAdd); + is_neg = true; + } + Symb::Times => { + tokens.push(Token::Number(current_num * current_sgn)); + current_num = 0.0; + current_sgn = 1.0; + tokens.push(Token::OpMult); + is_neg = true; + } + Symb::Zero => { + current_num = current_num * 10.0; + is_neg = false; + } + Symb::Number(x) => { + current_num = current_num * 10.0 + (x.get() as f32); + is_neg = false; + } + } + } + + tokens.push(Token::Number(current_num * current_sgn)); + + for op in [Token::OpMult, Token::OpDiv, Token::OpAdd, Token::OpSub] { + for i in 0..tokens.len() { + if tokens[i] == op { + let l = &tokens[i - 1]; + let r = &tokens[i + 1]; + + let v = match op { + Token::OpAdd => l.val() + r.val(), + Token::OpDiv => l.val() / r.val(), + Token::OpSub => l.val() - r.val(), + Token::OpMult => l.val() * r.val(), + _ => unreachable!(), + }; + + tokens.remove(i - 1); + tokens.remove(i - 1); + tokens[i - 1] = Token::Number(v); + break; + } + } + } + + return Some(tokens[0].val()); + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..b1ec09c --- /dev/null +++ b/src/main.rs @@ -0,0 +1,169 @@ +use anyhow::Result; +use std::{ + io::{stdin, stdout, StdoutLock, Write}, + num::NonZeroU8, +}; +use termion::{ + color::{self}, + cursor::HideCursor, + event::Key, + input::TermRead, + raw::IntoRawMode, +}; + +mod board; +mod random; +mod util; +use board::Board; +use util::{Player, Symb}; + +use crate::random::RandomAgent; + +pub trait PlayerAgent { + fn step(&mut self, board: &mut Board); +} + +fn play( + stdout: &mut StdoutLock, + player_max: bool, + computer: &mut dyn PlayerAgent, +) -> Result { + let mut cursor = 0usize; + let cursor_offset = 10usize - 1; + let cursor_max = 10usize; + + let mut board = Board::new(if player_max { + Player::Human + } else { + Player::Computer + }); + + let mut is_first = true; + let mut print_board = true; + 'outer: loop { + // Computer turn + if board.current_player() == Player::Computer && !board.is_done() { + computer.step(&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; + } + + print!( + "\r{}╙{}{}{}{}{}╜", + " ".repeat(cursor_offset), + " ".repeat(cursor), + color::Fg(board.current_player().color()), + if board.is_done() { ' ' } else { '^' }, + color::Fg(color::Reset), + " ".repeat(cursor_max - cursor), + ); + stdout.flush()?; + + if board.is_done() { + break; + } + + let stdin = stdin(); + for c in stdin.keys() { + print_board = match c.unwrap() { + Key::Right => { + cursor = cursor_max.min(cursor + 1); + false + } + Key::Left => { + if cursor != 0 { + cursor -= 1 + } + false + } + Key::Char('q') => break 'outer, + Key::Char('1') => board.play(cursor, Symb::Number(NonZeroU8::new(1).unwrap())), + Key::Char('2') => board.play(cursor, Symb::Number(NonZeroU8::new(2).unwrap())), + Key::Char('3') => board.play(cursor, Symb::Number(NonZeroU8::new(3).unwrap())), + Key::Char('4') => board.play(cursor, Symb::Number(NonZeroU8::new(4).unwrap())), + Key::Char('5') => board.play(cursor, Symb::Number(NonZeroU8::new(5).unwrap())), + Key::Char('6') => board.play(cursor, Symb::Number(NonZeroU8::new(6).unwrap())), + Key::Char('7') => board.play(cursor, Symb::Number(NonZeroU8::new(7).unwrap())), + Key::Char('8') => board.play(cursor, Symb::Number(NonZeroU8::new(8).unwrap())), + Key::Char('9') => board.play(cursor, Symb::Number(NonZeroU8::new(9).unwrap())), + Key::Char('0') => board.play(cursor, Symb::Zero), + Key::Char('a') => board.play(cursor, Symb::Plus), + Key::Char('s') => board.play(cursor, Symb::Minus), + Key::Char('m') => board.play(cursor, Symb::Times), + Key::Char('d') => board.play(cursor, Symb::Div), + _ => false, + }; + continue 'outer; + } + } + + return Ok(board); +} + +fn main() -> Result<()> { + let stdout = HideCursor::from(stdout().into_raw_mode().unwrap()); + let mut stdout = stdout.lock(); + + let mut agent = RandomAgent {}; + let a = play(&mut stdout, true, &mut agent)?; + if a.is_done() { + println!( + "\r\n{}Your score:{} {:.2}\n\n", + color::Fg(Player::Human.color()), + color::Fg(color::Reset), + a.evaluate().unwrap() + ); + } else { + println!( + "\r\n{}Quitting{}\r\n", + color::Fg(color::Red), + color::Fg(color::Reset), + ); + return Ok(()); + } + + let mut agent = 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/random.rs b/src/random.rs new file mode 100644 index 0000000..ed7ede6 --- /dev/null +++ b/src/random.rs @@ -0,0 +1,50 @@ +use std::num::NonZeroU8; + +use rand::Rng; + +use crate::{util::Symb, PlayerAgent}; + +pub struct RandomAgent {} + +impl PlayerAgent for RandomAgent { + fn step(&mut self, board: &mut crate::board::Board) { + let mut rng = rand::thread_rng(); + let n = board.size(); + + let mut c = rng.gen_range(0..n); + let mut s = match rng.gen_range(0..4) { + 0 => { + let n = rng.gen_range(0..=9); + if n == 0 { + Symb::Zero + } else { + Symb::Number(NonZeroU8::new(n).unwrap()) + } + } + 1 => Symb::Div, + 2 => Symb::Minus, + 3 => Symb::Plus, + 4 => Symb::Times, + _ => unreachable!(), + }; + + while !board.play(c, s) { + c = rng.gen_range(0..n); + s = match rng.gen_range(0..4) { + 0 => { + let n = rng.gen_range(0..=9); + if n == 0 { + Symb::Zero + } else { + Symb::Number(NonZeroU8::new(n).unwrap()) + } + } + 1 => Symb::Div, + 2 => Symb::Minus, + 3 => Symb::Plus, + 4 => Symb::Times, + _ => unreachable!(), + }; + } + } +} diff --git a/src/util.rs b/src/util.rs new file mode 100644 index 0000000..d6f0a50 --- /dev/null +++ b/src/util.rs @@ -0,0 +1,72 @@ +use std::{fmt::Display, num::NonZeroU8}; + +use termion::color::{self, Color}; + +#[derive(PartialEq, Eq, Clone, Copy)] +pub enum Player { + Human, + Computer, +} + +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, + Player::Human => &color::Magenta, + } + } +} + +impl Display for Player { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let s = match self { + Self::Computer => "Max", + Self::Human => "Min", + }; + write!(f, "{s}") + } +} + +#[derive(PartialEq, Eq, Clone, Copy)] +pub enum Symb { + Number(NonZeroU8), + Zero, + Plus, + Minus, + Times, + Div, +} + +impl Display for Symb { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Number(x) => x.fmt(f), + Self::Zero => '0'.fmt(f), + Self::Plus => '+'.fmt(f), + Self::Minus => '-'.fmt(f), + Self::Div => '÷'.fmt(f), + Self::Times => '×'.fmt(f), + } + } +} + +impl Symb { + /// Is this symbol a plain binary operator? + pub fn is_binop(&self) -> bool { + match self { + Symb::Div | Symb::Plus | Symb::Times => true, + _ => false, + } + } + + pub fn is_minus(&self) -> bool { + self == &Self::Minus + } +}