Created
July 26, 2023 06:45
-
-
Save leoshimo/8b9c85fa71b2dae4b5c6a55eda538205 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
use std::io::{self, Write}; | |
use std::{fmt::Display, str::FromStr}; | |
type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>; | |
struct Board { | |
contents: Vec<Option<char>>, | |
width: usize, | |
height: usize, | |
} | |
#[derive(Debug, PartialEq)] | |
struct Pos { | |
x: usize, | |
y: usize, | |
} | |
#[derive(Debug, PartialEq)] | |
enum Status { | |
Pending, | |
Draw, | |
Win(char), | |
} | |
impl Pos { | |
fn new(x: usize, y: usize) -> Pos { | |
Pos { x, y } | |
} | |
} | |
#[derive(Debug, Default, PartialEq)] | |
struct ParseError; | |
impl FromStr for Pos { | |
type Err = ParseError; | |
// Parse comma-separated pair of numbers | |
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> { | |
let (x, y) = s.split_once(',').ok_or(ParseError)?; | |
let x = x.trim().parse::<usize>().map_err(|_| ParseError)?; | |
let y = y.trim().parse::<usize>().map_err(|_| ParseError)?; | |
Ok(Pos { x, y }) | |
} | |
} | |
impl From<(usize, usize)> for Pos { | |
fn from(value: (usize, usize)) -> Self { | |
Self { | |
x: value.0, | |
y: value.1, | |
} | |
} | |
} | |
impl Board { | |
pub fn new(width: usize, height: usize) -> Board { | |
Board { | |
contents: vec![None; width * height], | |
width, | |
height, | |
} | |
} | |
pub fn get(&self, pos: &Pos) -> Option<char> { | |
self.contents[pos.x + pos.y * self.width] | |
} | |
pub fn set(&mut self, pos: &Pos, val: char) { | |
self.contents[pos.x + pos.y * self.width] = Some(val); | |
} | |
pub fn is_full(&self) -> bool { | |
self.contents.iter().all(|v| v.is_some()) | |
} | |
} | |
impl Display for Board { | |
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
use std::fmt::Write; | |
for y in 0..self.height { | |
if y != 0 { | |
f.write_str("-----\n")?; | |
} | |
for x in 0..self.width { | |
if x != 0 { | |
f.write_char('|')?; | |
} | |
f.write_char(self.get(&Pos::new(x, y)).unwrap_or(' '))?; | |
} | |
f.write_char('\n')?; | |
} | |
Ok(()) | |
} | |
} | |
/// Check if given symbol won the game | |
fn check_board(players: &[char], b: &Board) -> Status { | |
assert!(b.width == 3); | |
assert!(b.height == 3); | |
let checks = vec![ | |
// rows | |
((0, 0), (0, 1), (0, 2)), | |
((1, 0), (1, 1), (1, 2)), | |
((0, 0), (0, 1), (0, 2)), | |
// cols | |
((0, 0), (1, 0), (2, 0)), | |
((0, 1), (1, 1), (2, 1)), | |
((0, 2), (1, 2), (2, 2)), | |
// diag | |
((0, 0), (1, 1), (2, 2)), | |
((0, 2), (1, 1), (2, 0)), | |
]; | |
for p in players { | |
let win = checks.iter().any(|c| { | |
let (a, b, c) = ( | |
b.get(&(c.0.into())), | |
b.get(&(c.1.into())), | |
b.get(&(c.2.into())), | |
); | |
a.is_some() && a.unwrap() == *p && a == b && b == c | |
}); | |
if win { | |
return Status::Win(*p); | |
} | |
} | |
if b.is_full() { | |
return Status::Draw; | |
} | |
Status::Pending | |
} | |
/// Read positional input from player | |
fn read_pos(player: char, b: &Board) -> Result<Pos> { | |
let mut input = String::new(); | |
loop { | |
print!("Player \"{}\" move: ", player); | |
io::stdout().flush()?; | |
let _ = io::stdin() | |
.read_line(&mut input) | |
.map_err(|_| format!("Unable to read input"))?; | |
let pos = input.parse::<Pos>(); | |
input.clear(); | |
// parse fail | |
if pos.is_err() { | |
println!("Please enter position in form \"<x>, <y>\""); | |
continue; | |
} | |
// out-of-bounds | |
let pos = pos.unwrap(); | |
if pos.x >= b.width || pos.y >= b.height { | |
println!("Position is outside {} x {} board", b.width, b.height); | |
continue; | |
} | |
// nonempty | |
if b.get(&pos).is_some() { | |
println!("Position {}, {} is not empty", pos.x, pos.y); | |
continue; | |
} | |
return Ok(pos); | |
} | |
} | |
fn main() -> Result<()> { | |
let mut b = Board::new(3, 3); | |
let players = vec!['x', 'o']; | |
let mut turn = players.iter().cycle(); | |
let mut status = check_board(&players, &b); | |
while status == Status::Pending { | |
println!("{b}"); | |
let player = turn.next().unwrap(); | |
let pos = read_pos(*player, &b)?; | |
b.set(&pos, *player); | |
status = check_board(&players, &b); | |
println!(); | |
} | |
println!("{b}"); | |
match status { | |
Status::Draw => println!("Game draw!"), | |
Status::Win(winner) => println!("Player {} wins!", winner), | |
Status::Pending => println!("Game ended"), | |
} | |
Ok(()) | |
} | |
#[cfg(test)] | |
mod test { | |
use super::*; | |
#[test] | |
fn new_board_is_empty() { | |
let b = Board::new(3, 3); | |
for x in 0..3 { | |
for y in 0..3 { | |
assert!(b.get(&Pos { x, y }).is_none()); | |
} | |
} | |
} | |
#[test] | |
fn set_updates_board() { | |
let mut b = Board::new(3, 3); | |
b.set(&Pos { x: 0, y: 0 }, 'o'); | |
b.set(&Pos { x: 1, y: 1 }, 'x'); | |
b.set(&Pos { x: 1, y: 2 }, 'x'); | |
assert_eq!(b.get(&Pos { x: 0, y: 0 }), Some('o')); | |
assert_eq!(b.get(&Pos { x: 1, y: 1 }), Some('x')); | |
assert_eq!(b.get(&Pos { x: 1, y: 2 }), Some('x')); | |
} | |
#[test] | |
fn display_empty_board() { | |
let b = Board::new(3, 3); | |
let expected = " | | \ | |
\n-----\ | |
\n | | \ | |
\n-----\ | |
\n | | \n"; | |
assert_eq!(format!("{b}"), expected); | |
} | |
#[test] | |
fn display_nonempty_board() { | |
let mut b = Board::new(3, 3); | |
b.set(&Pos { x: 1, y: 1 }, 'x'); | |
b.set(&Pos { x: 0, y: 0 }, 'o'); | |
b.set(&Pos { x: 2, y: 2 }, 'x'); | |
let expected = "o| | \ | |
\n-----\ | |
\n |x| \ | |
\n-----\ | |
\n | |x\n"; | |
assert_eq!(format!("{b}"), expected); | |
} | |
#[test] | |
fn check_board_empty() { | |
let b = Board::new(3, 3); | |
let p = vec!['x', 'o']; | |
assert_eq!(check_board(&p, &b), Status::Pending); | |
} | |
#[test] | |
fn check_board_draw() { | |
let mut b = Board::new(3, 3); | |
let p = vec!['x', 'o']; | |
b.set(&Pos::new(0, 0), 'x'); | |
b.set(&Pos::new(0, 1), 'o'); | |
b.set(&Pos::new(0, 2), 'x'); | |
b.set(&Pos::new(1, 0), 'o'); | |
b.set(&Pos::new(1, 1), 'x'); | |
b.set(&Pos::new(1, 2), 'o'); | |
b.set(&Pos::new(2, 0), 'o'); | |
b.set(&Pos::new(2, 1), 'x'); | |
b.set(&Pos::new(2, 2), 'o'); | |
assert_eq!(check_board(&p, &b), Status::Draw); | |
} | |
#[test] | |
fn check_board_win() { | |
let mut b = Board::new(3, 3); | |
let p = vec!['x', 'o']; | |
b.set(&Pos::new(0, 0), 'x'); | |
b.set(&Pos::new(1, 0), 'o'); | |
b.set(&Pos::new(0, 1), 'x'); | |
b.set(&Pos::new(1, 1), 'o'); | |
assert_eq!(check_board(&p, &b), Status::Pending); | |
b.set(&Pos::new(0, 2), 'x'); | |
assert_eq!(check_board(&p, &b), Status::Win('x')); | |
let mut b = Board::new(3, 3); | |
let p = vec!['x', 'o']; | |
b.set(&Pos::new(1, 1), 'x'); | |
b.set(&Pos::new(0, 0), 'o'); | |
b.set(&Pos::new(0, 1), 'x'); | |
b.set(&Pos::new(2, 2), 'o'); | |
b.set(&Pos::new(2, 1), 'x'); | |
assert_eq!(check_board(&p, &b), Status::Win('x')); | |
b.set(&Pos::new(1, 1), 'o'); | |
b.set(&Pos::new(0, 0), 'x'); | |
b.set(&Pos::new(0, 1), 'o'); | |
b.set(&Pos::new(2, 2), 'x'); | |
b.set(&Pos::new(2, 1), 'o'); | |
assert_eq!(check_board(&p, &b), Status::Win('o')); | |
} | |
#[test] | |
fn parse_pos_from_str() { | |
assert_eq!("0,0".parse::<Pos>(), Ok(Pos::new(0, 0))); | |
assert_eq!("1, 3".parse::<Pos>(), Ok(Pos::new(1, 3))); | |
assert_eq!( | |
" 300, 4 ".parse::<Pos>(), | |
Ok(Pos::new(300, 4)) | |
); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Sample run: