From 0cf10fdc16226bcf1f0e6a2eb38a324df36e313f Mon Sep 17 00:00:00 2001 From: Henri Bourcereau Date: Tue, 30 Dec 2025 17:39:06 +0100 Subject: [PATCH] basic tic-tac-toe rust implementation --- Cargo.lock | 7 ++ src/game.rs | 198 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 12 +++- 3 files changed, 216 insertions(+), 1 deletion(-) create mode 100644 Cargo.lock create mode 100644 src/game.rs diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..41776ed --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "tictactoe-rust-foropenspiel" +version = "0.1.0" diff --git a/src/game.rs b/src/game.rs new file mode 100644 index 0000000..a23a996 --- /dev/null +++ b/src/game.rs @@ -0,0 +1,198 @@ +use std::{fmt, str}; + +// ------------- Player + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Player { + Player0, + Player1, +} + +impl From for Player { + fn from(item: u8) -> Self { + match item { + 0 => Player::Player0, + 1 => Player::Player1, + _ => Player::Player0, + } + } +} + +impl From for u8 { + fn from(player: Player) -> u8 { + match player { + Player::Player0 => 0, + Player::Player1 => 1, + } + } +} + +impl fmt::Display for Player { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let repr = match self { + Player::Player0 => "x", + Player::Player1 => "o", + }; + let mut s = String::new(); + s.push_str(repr); + write!(f, "{s}") + } +} + +// ------------- CellState + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum CellState { + Cross, + Nought, + Empty, +} + +impl From for CellState { + fn from(player: Player) -> CellState { + match player { + Player::Player0 => CellState::Cross, + Player::Player1 => CellState::Nought, + } + } +} + +impl fmt::Display for CellState { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let repr = match self { + CellState::Cross => "x", + CellState::Nought => "o", + CellState::Empty => ".", + }; + let mut s = String::new(); + s.push_str(repr); + write!(f, "{s}") + } +} + +// ------------ Board + +const NUM_CELLS: usize = 9; +const NUM_ROWS: usize = 3; +const NUM_COLS: usize = 3; + +struct Board { + cells: Vec, +} + +impl Default for Board { + fn default() -> Self { + Board { + cells: vec![CellState::Empty; NUM_CELLS], + } + } +} + +impl Board { + fn has_line(&self, player: Player) -> bool { + let c: CellState = player.into(); + (self.cells[0] == c && self.cells[1] == c && self.cells[2] == c) + || (self.cells[3] == c && self.cells[4] == c && self.cells[5] == c) + || (self.cells[6] == c && self.cells[7] == c && self.cells[8] == c) + || (self.cells[0] == c && self.cells[3] == c && self.cells[6] == c) + || (self.cells[1] == c && self.cells[4] == c && self.cells[7] == c) + || (self.cells[2] == c && self.cells[5] == c && self.cells[8] == c) + || (self.cells[0] == c && self.cells[4] == c && self.cells[8] == c) + || (self.cells[2] == c && self.cells[4] == c && self.cells[6] == c) + } + + fn set_cell_state(&mut self, position: usize, state: CellState) { + self.cells[position] = state; + } + + + fn cell_at(&self, row: usize, col: usize) -> CellState { + self.cells[row * NUM_COLS + col] + } +} + +// -------------- GameState + +type Action = usize; + +pub struct GameState { + pub current_player: Option, + pub outcome: Option, + pub board: Board, + pub num_moves: usize, +} + +impl Default for GameState { + fn default() -> Self { + GameState { + current_player: Some(Player::Player0), + outcome: None, + board: Board::default(), + num_moves: 0, + } + } +} + +impl GameState { + fn is_terminal(&self) -> bool { + return self.outcome != None || self.is_full(); + } + + fn is_full(&self) -> bool { + self.num_moves == NUM_CELLS + } + + pub fn do_action(&mut self, action: Action) { + if self.board.cells[action] != CellState::Empty { + return; + } + if let Some(player) = self.current_player { + self.board.cells[action] = player.into(); + if self.board.has_line(player) { + self.outcome = self.current_player; + } + self.change_player(); + self.num_moves += 1; + } + } + + fn undo_action(&mut self, player: Player, action: Action) { + self.board.set_cell_state(action.into(), CellState::Empty); + self.current_player = Some(player); + self.outcome = None; + self.num_moves -= 1; + } + + pub fn legal_actions(&self) -> Vec { + // if (self.is_terminal()) return vec![]; + // Can move in any empty cell. + self.board + .cells + .iter() + .enumerate() + .filter(|(_, cs)| **cs == CellState::Empty) + .map(|(pos, _)| pos) + .collect() + } + + fn change_player(&mut self) { + self.current_player = if self.current_player == Some(Player::Player0) { + Some(Player::Player1) + } else { + Some(Player::Player0) + }; + } +} + +impl fmt::Display for GameState { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + let mut s = String::new(); + for row in 0..NUM_ROWS { + for col in 0..NUM_COLS { + s.push_str(self.board.cell_at(row, col).to_string().as_str()); + } + s.push('\n'); + } + write!(f, "{s}") + } +} diff --git a/src/main.rs b/src/main.rs index e7a11a9..06394c2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,13 @@ +mod game; + +use game::GameState; + fn main() { - println!("Hello, world!"); + let mut game = GameState::default(); + let mut actions = game.legal_actions(); + while !actions.is_empty() { + game.do_action(actions[0]); + actions = game.legal_actions(); + } + println!("Result: \n{game}"); }