Skip to content

Instantly share code, notes, and snippets.

@jryio
Last active August 21, 2023 18:05
Show Gist options
  • Save jryio/9a48703d227f40da2eceb8930bad5a58 to your computer and use it in GitHub Desktop.
Save jryio/9a48703d227f40da2eceb8930bad5a58 to your computer and use it in GitHub Desktop.
Recurse - Tic Tac Toe - Jacob Young

Install

This game is wrriten in Rust, using version 1.67.0.

Rust can be installed here if necessary.

Create an empty crate: cargo new recurse-tic-tac-toe

Replace main.rs and Cargo.toml in the newly created crate with the files in this gist.

Running

rustup install 1.67.0
rustup default 1.67.0

cargo run
[package]
name = "recurse-tic-tac-toe"
version = "0.1.0"
edition = "2021"
authors = ["Jacob Young <crates@jry.io>"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[[bin]]
name = "tictactoe"
path = "src/main.rs"
[dependencies]
rand = "0.8.5"
use std::error::Error;
use std::fmt::Display;
use std::io::{stdin, stdout, Write};
type Board = [Pos; 9];
type Role = (Player, Mark);
type Roles = (Role, Role);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Pos {
Mark(Mark),
None,
}
impl Display for Pos {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Mark(m) => Ok(write!(f, "{m:?}")?),
Self::None => Ok(write!(f, " ")?),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Mark {
X,
O,
}
impl Mark {
fn opposite(&self) -> Self {
match self {
Self::X => Self::O,
Self::O => Self::X,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Player {
One,
Two,
}
#[derive(Debug, PartialEq, Eq)]
enum Status {
InProgress,
Winner(Player),
Draw,
}
struct Turn(Role);
impl Turn {
fn next(&mut self) {
self.0 = match &self.0 {
(Player::One, m) => (Player::Two, m.opposite()),
(Player::Two, m) => (Player::One, m.opposite()),
}
}
}
struct Game {
status: Status,
roles: Roles,
turn: Turn,
board: Board,
}
impl Game {
fn new() -> Self {
let status = Status::InProgress;
let roles = Self::rand_roles();
let turn = Self::rand_turn(&roles);
let board: Board = [Pos::None; 9];
Self {
status,
roles,
turn,
board,
}
}
fn handle_input(&mut self, index: usize) -> Result<(), Box<dyn Error>> {
let index = index - 1;
let mut pos = self.board[index];
if pos != Pos::None {
return Err(
format!("Position {index} is already occupied, choose an empty position",).into(),
);
}
pos = match self.turn.0 {
(_, Mark::X) => Pos::Mark(Mark::X),
(_, Mark::O) => Pos::Mark(Mark::O),
};
self.board[index] = pos;
Ok(())
}
fn check_win(&mut self) {
let board = &self.board;
let (player, _) = self.turn.0;
// Diagonal win
if (board[0] != Pos::None && (board[0] == board[4] && board[0] == board[8]))
|| (board[2] != Pos::None && (board[2] == board[4] && board[2] == board[6]))
{
return self.status = Status::Winner(player);
}
for pos in 0..=2 {
if board[pos] == Pos::None {
continue;
}
// Column win
if board[pos] == board[pos + 3] && board[pos] == board[pos + 6] {
return self.status = Status::Winner(player);
}
let pos = pos * 3;
if board[pos] == Pos::None {
continue;
}
// Row win
if board[pos] == board[pos + 1] && board[pos] == board[pos + 2] {
return self.status = Status::Winner(player);
}
}
// Draw
if board.iter().all(|p| *p != Pos::None) {
self.status = Status::Draw;
}
}
fn switch_player(&mut self) {
self.turn.next();
}
fn rand_roles() -> Roles {
match rand::random::<bool>() {
true => ((Player::One, Mark::X), (Player::Two, Mark::O)),
false => ((Player::One, Mark::O), (Player::Two, Mark::X)),
}
}
fn rand_turn(roles: &Roles) -> Turn {
match rand::random::<bool>() {
true => Turn(roles.0),
false => Turn(roles.1),
}
}
}
impl Display for Game {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// Display the grid
for pos in 0..=2 {
let index = pos * 3;
write!(
f,
"\
-------------\n\
| {} | {} | {} |\n\
-------------\n",
self.board[index],
self.board[index + 1],
self.board[index + 2]
)?;
}
writeln!(f, "-------------\n")?;
writeln!(f, "{:?} is Player {:?}", self.roles.0 .1, self.roles.0 .0)?;
writeln!(f, "{:?} is Player {:?}", self.roles.1 .1, self.roles.1 .0)?;
Ok(())
}
}
fn strip_trailing_newline(input: &str) -> &str {
input
.strip_suffix("\r\n")
.or(input.strip_suffix('\n'))
.unwrap_or(input)
}
fn main() -> Result<(), Box<dyn Error>> {
let mut game = Game::new();
while game.status == Status::InProgress {
// Clear screen
print!("\x1B[2J\x1B[1;1H");
print!("{game}");
let mut try_input = || -> Result<(), Box<dyn Error>> {
println!("Enter a position 1...9");
print!(
"Player {:?}'s Turn ( {:?} ):> ",
game.turn.0 .0, game.turn.0 .1
);
stdout().flush().unwrap();
let mut input = String::new();
stdin()
.read_line(&mut input)
.map_err(|_| "Oops! Couldn't read that input. Try again...")?;
let input: &str = strip_trailing_newline(input.as_str());
let pos = input
.parse::<usize>()
.map_err(|_| "Enter a valid position between 1..9")?;
if !(1..=9).contains(&pos) {
return Err("Enter a valid position between 1..9".into());
}
game.handle_input(pos)?;
Ok(())
};
loop {
match try_input() {
// Valid input
Ok(()) => break,
// Try input again
Err(err) => {
println!("{err}");
continue;
}
}
}
game.check_win();
game.switch_player();
}
print!("{game}");
println!("Game Over => {:?}", game.status);
Ok(())
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment