oox/src/main.rs

240 lines
7.7 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

/* Copyright 2023 Christophe Parent (christopheparent@thatspaceandtime.org)
*
* This file is part of OOX.
*
* OOX is free software: you can redistribute it and/or modify it under the terms of the GNU
* General Public License as published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* OOX is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even
* the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General
* Public License for more details.
*
* You should have received a copy of the GNU General Public License along with OOX. If not, see
* <https://www.gnu.org/licenses/>.
*/
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(())
}