Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 65 additions & 0 deletions topics/tic-tac-toe/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 7 additions & 0 deletions topics/tic-tac-toe/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[package]
name = "tic-tac-toe"
version = "0.1.0"
edition = "2024"

[dependencies]
thiserror = "2.0.17"
16 changes: 16 additions & 0 deletions topics/tic-tac-toe/docs/architecture.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Architecture

## File and folder structure
- `src/logic/`: Logic code of tic-tac-toe. It contains the player iteration logic in `game.rs`, and tic-tac-toe grid logic in `grid.rs`
- `src/player`: Different implementations of players that can play the tic-tac-toe game. Currently, it contains a terminal player, which will be controlled by a player by their terminal, and an AI player, using the MinMax algorithm.
- `src/types.rs`: Contains pure data types used by the library

# Objects
The main objects that will interact together in this library are the `Game` object, and `PlayerBehavior` objects. a Game will call methods on PlayerBehaviors, which simulate method. PlayerBehaviors may act on these methods to run diverse actions (e.g. printing information to the terminal), or communicate back to the `Game` (e.g. `play()` return value).

An interesting property of Rust I've tried to use here is passing-by-movement. You will notice that the `Game` constructor takes ownership of players, and that `play()` takes ownership of `Game`. This allows me to enforce that players or games won't be re-used, and allow me to skip creating state checks to ensure these objects would behave correctly when misused this way.

## Error handling
This project uses `thiserror` to help with error handling. This crate has been chosen because it allows us to create semantic error types, while not leaking itself into the interface provided by this library.

Error handling is very limited due to the few cases in which returning an error would be acceptable. In a more complex library, we could use an associated object in `PlayerBehavior`, and make `Game::play` return either an associated object error from Player1 or Player2 using generics.
11 changes: 11 additions & 0 deletions topics/tic-tac-toe/docs/usage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Integrating this into your application

Note for Mr. Ortiz: this project wasn't decoupled into a separate library and binary crate because doing so would create an big commit with lots of files moving around, but clear contracts have been established. I am going to assume below that all files but main.rs are part of a library.

You can use this library into your application by creating two players, (e.g. a `player::ai_minmax::AIMinMax` and a `player::ai_minmax::TerminalPlayer`), creating a game with `logic::game::Game::new()`, anv invoking `Game::play()` on your `Game` instance. A sample code is available [here](../src/main.rs)

# Using the sample application provided
You can run a sample application by building the project with `cargo build --release`, and running the binary at `target/release/tic-tac-toe`
When run in this way, you will run against an AI using the MinMax algorithm to win.

In the grid presented to you, numbers represent free cells that you can play in. Cells with X or O (which will be colored) represent cells controlled by you or the AI. You can play by entering the number corresponding to the cell you want to play in.
77 changes: 77 additions & 0 deletions topics/tic-tac-toe/src/logic/game.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
use crate::{
logic::grid,
player::PlayerBehavior,
types::{Grid, PlayerID},
};

pub struct Game<T1: PlayerBehavior, T2: PlayerBehavior> {
pub grid: Grid,
pub player1: T1,
pub player2: T2,
}

impl<T1: PlayerBehavior, T2: PlayerBehavior> Game<T1, T2> {
pub fn new(player1: T1, player2: T2) -> Self {
Game {
grid: [None; 9],
player1,
player2,
}
}

/// Call handlers, and run tic-tac-toe logic
pub fn play(mut self) -> crate::Result<Option<PlayerID>> {
self.player1.game_start(crate::types::PlayerID::Player1);
self.player2.game_start(crate::types::PlayerID::Player2);

let winner = self.play_inner()?;

self.player1.game_ended(self.grid, winner);
self.player2.game_ended(self.grid, winner);

Ok(winner)
}

/// Actual play logic, without calling handlers
pub fn play_inner(&mut self) -> crate::Result<Option<PlayerID>> {
let mut current_player: &mut dyn PlayerBehavior = &mut self.player1;
let mut current_player_id = crate::types::PlayerID::Player1;

loop {
// Make current player play
match current_player.play(self.grid) {
Ok(position) => {
// Validate move
if self.grid[position as usize].is_none() {
self.grid[position as usize] = Some(current_player_id);

// Win
if let Some(winner) = grid::is_there_a_win(self.grid) {
return Ok(Some(winner));
}

// Tie
if !crate::logic::grid::are_there_moves_left(self.grid) {
return Ok(None);
}

// Switch players
if current_player_id == crate::types::PlayerID::Player1 {
current_player = &mut self.player2;
current_player_id = crate::types::PlayerID::Player2;
} else {
current_player = &mut self.player1;
current_player_id = crate::types::PlayerID::Player1;
}
} else {
// If move is invalid, ask the same player to play again
continue;
}
}
Err(err) => {
return Err(err);
}
}
}
}
}
36 changes: 36 additions & 0 deletions topics/tic-tac-toe/src/logic/grid.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
use crate::types::{Grid, PlayerID};

pub fn is_there_a_win(grid: Grid) -> Option<PlayerID> {
// Check rows
for row in 0..3 {
if grid[row * 3].is_some()
&& grid[row * 3] == grid[row * 3 + 1]
&& grid[row * 3 + 1] == grid[row * 3 + 2]
{
return grid[row * 3];
}
}

// Check columns
for col in 0..3 {
if grid[col].is_some() && grid[col] == grid[col + 3] && grid[col + 3] == grid[col + 6] {
return grid[col];
}
}

// Check diagonals
if grid[0].is_some() && grid[0] == grid[4] && grid[4] == grid[8] {
return grid[0];
}

if grid[2].is_some() && grid[2] == grid[4] && grid[4] == grid[6] {
return grid[2];
}

None // No winner
}

/// Check if there are any moves left on the board
pub fn are_there_moves_left(grid: Grid) -> bool {
grid.iter().any(|cell| cell.is_none())
}
2 changes: 2 additions & 0 deletions topics/tic-tac-toe/src/logic/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
pub mod game;
pub mod grid;
22 changes: 22 additions & 0 deletions topics/tic-tac-toe/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
pub mod logic;
pub mod player;
pub mod types;

pub use types::Result;

fn main() {
let p1 = player::terminal::TerminalPlayer::new();
let p2 = player::ai_minmax::AIMinMax::new();
let game = logic::game::Game::new(p1, p2);
match game.play() {
Ok(Some(winner)) => {
println!("Player {:?} wins!", winner);
}
Ok(None) => {
println!("It's a tie!");
}
Err(_e) => {
eprintln!("An error occurred: {:?}", _e);
}
}
}
Loading