240 lines
7.7 KiB
Rust
240 lines
7.7 KiB
Rust
/* 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 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::<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(¤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(())
|
||
}
|