Merge branch 'release-0.1.0'

master v0.1.0
Christophe Parent 2023-10-27 23:09:42 -07:00
commit ffa582b533
6 changed files with 289 additions and 17 deletions

15
.gitignore vendored
View File

@ -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

7
Cargo.lock generated Normal file
View File

@ -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"

4
Cargo.toml Normal file
View File

@ -0,0 +1,4 @@
[package]
name = "oox"
version = "0.1.0"
edition = "2021"

View File

@ -1,3 +1,56 @@
# oox
# OOX
A Tictactoe game
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 [Tictactoe 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).

BIN
demo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.5 KiB

223
src/main.rs Normal file
View File

@ -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<Mark>]) -> 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<Coordinates, String> {
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<Mark>],
lines: &mut HashMap<u32, (u32, Option<Mark>)>,
input_coordinates: Coordinates,
) -> Result<Status, String> {
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<dyn Error>> {
println!("OOX");
println!("A Tictactoe 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::<HashMap<u32, (u32, Option<Mark>)>>();
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(&current_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(())
}