/* 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 * . */ 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(()) }