Skip to content

Instantly share code, notes, and snippets.

@leoshimo
Created July 26, 2023 06:45
Show Gist options
  • Save leoshimo/8b9c85fa71b2dae4b5c6a55eda538205 to your computer and use it in GitHub Desktop.
Save leoshimo/8b9c85fa71b2dae4b5c6a55eda538205 to your computer and use it in GitHub Desktop.
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))
);
}
}
@leoshimo
Copy link
Author

leoshimo commented Jul 26, 2023

Sample run:

$ rustc --version
rustc 1.71.0 (8ede3aae2 2023-07-12)

$ rustc tictactoe.rs && ./tictactoe
 | |
-----
 | |
-----
 | |

Player "x" move: 1, 1

 | |
-----
 |x|
-----
 | |

Player "o" move: 0, 0

o| |
-----
 |x|
-----
 | |

Player "x" move: 0, 2

o| |
-----
 |x|
-----
x| |

Player "o" move: 2, 0

o| |o
-----
 |x|
-----
x| |

Player "x" move: 1, 0

o|x|o
-----
 |x|
-----
x| |

Player "o" move: 0, 1

o|x|o
-----
o|x|
-----
x| |

Player "x" move: 1, 2

o|x|o
-----
o|x|
-----
x|x|

Player x wins!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment