diff --git a/.gitignore b/.gitignore index 3ca43ae..2f7896d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,16 +1 @@ -# ---> Rust -# Generated by Cargo -# will have compiled files and executables -debug/ target/ - -# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries -# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html -Cargo.lock - -# These are backup files generated by rustfmt -**/*.rs.bk - -# MSVC Windows builds of rustc generate these, which store debugging information -*.pdb - diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..fb57717 --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "oox" +version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..cc896ad --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,4 @@ +[package] +name = "oox" +version = "0.1.0" +edition = "2021" diff --git a/README.md b/README.md index cef1eea..2661a89 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,56 @@ -# oox +# OOX -A Tic‐tac‐toe game \ No newline at end of file +The name ”OOX” is a mutually recursive acronym: “OOX” stands for “**O**OX **O**r **X**OO”, while “XOO” stands for “**X**OO **O**r **O**OX”. Other than that this is your typical [Tic‐tac‐toe game](https://en.wikipedia.org/wiki/Tic-tac-toe), used as a training ground to explore the development of games in Rust. + +![OOX in action](/demo.png) + +## Usage + +### Requirements + +Rust must be installed on your system; the oldest supported version is `1.63.0`. No other dependencies are required. + +### Installing + +Clone this repository locally: + +```shell +$ git clone https://forge.thatspaceandtime.org/ooxie/oox.git +$ cd oox +``` + +### Building + +To build the binary, execute: + +```shell +$ cargo build --release +``` + +### Running + +To run the game, either execute (this will also build if need be): + +```shell +$ cargo run --release +``` + +or: + +```shell +$ ./target/release/oox +``` + +### Playing + +SETUP: This game is for two players. Player 1 plays with marks noted ”X” and Player 2 with marks noted ”O”. The board consists of 3 rows and 3 columns, and is empty at the start of the game. + +GOAL: Each player tries to align three of their marks on the board. + +GAMEPLAY: Each player takes their turn alternatively, starting with Player 1. Each turn a player must place one mark on one of the free spaces of the board; they do this by entering the coordinates of the desired space (for example ”a1”). + +END OF GAME: If one player succeeds in aligning three of their marks, either horizontally, vertically, or diagonally, they win the game. If no player is able to do so by the time the board is full, the game is declared a draw. + +## License + +The source code of this project is licensed under a [GNU General Public License v3.0](/LICENSE). diff --git a/demo.png b/demo.png new file mode 100644 index 0000000..5c4784e Binary files /dev/null and b/demo.png differ diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..326d540 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,223 @@ +use std::collections::HashMap; +use std::error::Error; +use std::fmt::Display; +use std::fmt::Formatter; +use std::fmt::Result as FmtResult; +use std::fmt::Write; +use std::io::stdin; +use std::process; + +const BOARD_SIZE: u32 = 3; +const FIRST_COLUMN_LABEL: u32 = 0x61; // LATIN SMALL LETTER A in UTF-8 +const LAST_COLUMN_LABEL: u32 = FIRST_COLUMN_LABEL + BOARD_SIZE - 1; +const FIRST_ROW_LABEL: u32 = 0x31; // DIGIT ONE in UTF-8 +const LAST_ROW_LABEL: u32 = FIRST_ROW_LABEL + BOARD_SIZE - 1; + +enum Player { + P1, + P2, +} + +impl Player { + fn next(&mut self) { + *self = match self { + Player::P1 => Player::P2, + Player::P2 => Player::P1, + }; + } +} + +impl Display for Player { + fn fmt(&self, formatter: &mut Formatter) -> FmtResult { + write!( + formatter, + "{}", + match self { + Player::P1 => "1", + Player::P2 => "2", + } + ) + } +} + +struct Coordinates { + row: u32, + column: u32, +} + +#[derive(Clone, Copy, PartialEq)] +enum Mark { + X, + O, +} + +enum Status { + SpaceAlreadyTaken, + InProgress, + Won, + Draw, +} + +fn draw(board: &[Option]) -> FmtResult { + println!(); + for row_index in 0..BOARD_SIZE as usize { + let mut row = (BOARD_SIZE as usize - row_index).to_string(); + for column_index in 0..BOARD_SIZE as usize { + write!( + row, + " {}", + match board[row_index * BOARD_SIZE as usize + column_index] { + Some(mark) => match mark { + Mark::X => "X", + Mark::O => "O", + }, + None => "•", + } + )?; + } + println!("{}", row); + } + let mut footer_row = String::from(" "); + for x in FIRST_COLUMN_LABEL..=LAST_COLUMN_LABEL { + write!(footer_row, " {}", char::from_u32(x).unwrap_or(' '))?; + } + println!("{}\n", footer_row); + Ok(()) +} + +fn get_input() -> Result { + let mut input = String::new(); + if let Err(error) = stdin().read_line(&mut input) { + return Err(error.to_string()); + } + let mut chars = input.trim().chars(); + let coordinates = Coordinates { + column: match chars.next() { + Some(x) if x as u32 >= FIRST_COLUMN_LABEL && x as u32 <= LAST_COLUMN_LABEL => { + x as u32 - FIRST_COLUMN_LABEL + } + _ => return Err("That input is invalid.".to_string()), + }, + row: match chars.next() { + Some(x) if x as u32 >= FIRST_ROW_LABEL && x as u32 <= LAST_ROW_LABEL => { + BOARD_SIZE as u32 - 1 - (x as u32 - FIRST_ROW_LABEL) + } + _ => return Err("That input is invalid.".to_string()), + }, + }; + match chars.next() { + Some(_) => Err("That input is invalid.".to_string()), + _ => Ok(coordinates), + } +} + +fn process_logic( + current_player: &Player, + board: &mut [Option], + lines: &mut HashMap)>, + input_coordinates: Coordinates, +) -> Result { + let current_index = + match usize::try_from(input_coordinates.row * BOARD_SIZE + input_coordinates.column) { + Ok(x) => x, + Err(error) => return Err(error.to_string()), + }; + if board[current_index] != None { + return Ok(Status::SpaceAlreadyTaken); + } + let current_mark = match current_player { + Player::P1 => Mark::X, + Player::P2 => Mark::O, + }; + board[current_index] = Some(current_mark); + + // The matching lines are the lines where the current space is located. + let mut matching_lines = vec![input_coordinates.row, BOARD_SIZE + input_coordinates.column]; + if input_coordinates.row == input_coordinates.column { + matching_lines.push(BOARD_SIZE * 2); + } + if input_coordinates.row + input_coordinates.column == BOARD_SIZE - 1 { + matching_lines.push(BOARD_SIZE * 2 + 1); + } + for line in matching_lines { + if let Some((number_free_spaces, mark_option)) = lines.get(&line) { + // A line with two different marks is removed from the hash map. + if mark_option != &None && mark_option != &Some(current_mark) { + lines.remove(&line); + // Otherwise, a line with more than one free space is filled with the current mark. + } else if number_free_spaces > &1 { + lines.insert(line, (number_free_spaces - 1, Some(current_mark))); + // Otherwise, ie. when a line only has one free space, + // that last free space is filled and the game is won. + } else { + return Ok(Status::Won); + } + }; + } + // The game is a draw if the board is entirely filled up and there is no winner. + if board.iter().all(|x| x.is_some()) { + return Ok(Status::Draw); + } + Ok(Status::InProgress) +} + +fn main() -> Result<(), Box> { + println!("OOX"); + println!("A Tic‐tac‐toe game"); + + let mut current_player = Player::P1; + let mut board = [None; (BOARD_SIZE * BOARD_SIZE) as usize]; + // Lines are rows, columns, and diagonals that can be a win. + // A hash map is used to keep track of what lines are still up for grabs and worth checking. + // The keys are 0-indexed integers, starting with the rows in order, followed by the columns + // in order, followed by the top-left-to-bottom-right and top-right-to-bottom-left diagonals. + let mut lines = (0..BOARD_SIZE * 2 + 2) + .map(|i| (i, (BOARD_SIZE, None))) + .collect::)>>(); + + loop { + if let Err(error) = draw(&board) { + println!("{}", error); + process::exit(1); + } + + println!("Waiting for Player {}...", current_player); + let input_coordinates = match get_input() { + Ok(x) => x, + Err(error) => { + println!("{}", error); + continue; + } + }; + + match process_logic(¤t_player, &mut board, &mut lines, input_coordinates) { + Ok(status) => match status { + Status::SpaceAlreadyTaken => println!("That space is already taken."), + Status::InProgress => current_player.next(), + Status::Won => { + if let Err(error) = draw(&board) { + println!("{}", error); + process::exit(1); + } + println!("Player {} won the game!", current_player); + break; + } + Status::Draw => { + if let Err(error) = draw(&board) { + println!("{}", error); + process::exit(1); + } + println!("The game ended in a draw!"); + break; + } + }, + Err(error) => { + println!("{}", error); + process::exit(1); + } + }; + } + + println!("\nGAME OVER"); + Ok(()) +}