oox/src/main.rs

240 lines
7.7 KiB
Rust
Raw Normal View History

2023-11-21 05:35:15 +00:00
/* 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/>.
*/
2023-10-28 05:41:25 +00:00
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(())
}