Added basic game with random agent

master
Mark 2024-02-26 08:54:35 -08:00
parent 7239eb15d7
commit 2d7432c228
Signed by: Mark
GPG Key ID: C6D63995FE72FD80
6 changed files with 658 additions and 0 deletions

139
Cargo.lock generated Normal file
View File

@ -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"

9
Cargo.toml Normal file
View File

@ -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"

219
src/board.rs Normal file
View File

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

169
src/main.rs Normal file
View File

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

50
src/random.rs Normal file
View File

@ -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!(),
};
}
}
}

72
src/util.rs Normal file
View File

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