giant_tictactoe/
lib.rs

1//! # Giant Tic Tac Toe
2//!
3//! This is an implentation of Giant TicTacToe.
4//!
5//! To play, run
6//!
7//! ```bash
8//! cargo run
9//! ```
10use itertools::iproduct;
11use std::fmt;
12use std::io;
13
14/// what can be put in space.
15#[derive(Debug, Default, Clone, PartialEq)]
16pub enum Space {
17    #[default]
18    None,
19    Cross,
20    Circle,
21}
22
23impl fmt::Display for Space {
24    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25        let d = match &self {
26            Space::None => ' ',
27            Space::Cross => 'X',
28            Space::Circle => 'O',
29        };
30        write!(f, "{}", d)
31    }
32}
33
34impl From<Space> for char {
35    fn from(s: Space) -> Self {
36        format!("{}", s).chars().next().unwrap()
37    }
38}
39
40/// A grid of Tic Tac Toe
41#[derive(Debug, Default, Clone)]
42struct TicTacToe {
43    grid: [[Space; 3]; 3],
44    victory: Space,
45}
46
47impl fmt::Display for TicTacToe {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        for (line_nb, line) in self.grid.clone().into_iter().enumerate() {
50            writeln!(f, "{} | {} | {}", line[0], line[1], line[2])?;
51            if line_nb < 2 {
52                writeln!(f, "--+---+--")?
53            }
54        }
55        Ok(())
56    }
57}
58
59impl TicTacToe {
60    /// Place mark of player in place
61    ///
62    /// Place is numbered as follow:
63    ///
64    /// ```text
65    ///
66    ///      1 | 2 | 3
67    ///     ---+---+---
68    ///      4 | 5 | 6
69    ///     ---+---+---
70    ///      7 | 8 | 9
71    /// ```
72    ///
73    fn play(&mut self, player: Space, place: usize) -> Result<(), String> {
74        if place == 0 || place > 9 {
75            // TODO: create error type
76            return Err("Invalid place".into());
77        }
78        let place = place - 1;
79        let coords = (place / 3, place % 3);
80        if self.grid[coords.0][coords.1] != Space::None {
81            // TODO: create error type
82            return Err("Already occupied".into());
83        }
84        self.grid[coords.0][coords.1] = player.clone();
85        Ok(())
86    }
87
88    fn victory(&mut self) -> Space {
89        if self.victory != Space::None {
90            return self.victory.clone();
91        }
92        self.victory = self.compute_victory();
93        self.victory.clone()
94    }
95
96    fn compute_victory(&self) -> Space {
97        for p in [Space::Cross, Space::Circle] {
98            for c in 0..3 {
99                if (0..3).map(|u| &self.grid[c][u]).cloned().all(|x| x == p) {
100                    // line
101                    return p;
102                }
103                if (0..3).map(|u| &self.grid[u][c]).cloned().all(|x| x == p) {
104                    // column
105                    return p;
106                }
107            }
108            if (0..3).map(|u| &self.grid[u][u]).cloned().all(|x| x == p) {
109                // first diag
110                return p;
111            }
112            if (0..3)
113                .map(|u| &self.grid[u][2 - u])
114                .cloned()
115                .all(|x| x == p)
116            {
117                // second diag
118                return p;
119            }
120        }
121        Space::None
122    }
123}
124
125#[derive(Debug, Default)]
126struct GiantTicTacToe {
127    grid: [[TicTacToe; 3]; 3],
128}
129
130impl fmt::Display for GiantTicTacToe {
131    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132        write!(f, "{}", &self.to_grid(None).unwrap())
133    }
134}
135
136impl GiantTicTacToe {
137    /// Returns the grid to display
138    ///
139    /// playable: if provided, display possibilities on grid, numbered as follow:
140    ///
141    /// ```text
142    ///      1 | 2 | 3
143    ///     ---+---+---
144    ///      4 | 5 | 6
145    ///     ---+---+---
146    ///      7 | 8 | 9
147    /// ```
148    ///
149    fn to_grid(&self, playable: Option<usize>) -> Result<String, String> {
150        // grids
151        let mut grid = empty_giant_grid();
152
153        // data
154        for (l, x) in self.grid.iter().enumerate() {
155            for (c, g) in x.iter().enumerate() {
156                let grid_offset = (l * 12 + 1, c * 12 + 1);
157                for (line, column) in iproduct!(0..3, 0..3) {
158                    let coords = (4 * line + grid_offset.0, 4 * column + grid_offset.1);
159                    grid[coords.0][coords.1] = g.grid[line][column].clone().into();
160                }
161            }
162        }
163
164        // numbers
165        if let Some(g) = playable {
166            if g < 10 && g > 0 {
167                let g = g - 1;
168                let offset = (g / 3 * 12, g % 3 * 12);
169                for n in 0..9 {
170                    let c = ((n / 3) * 4, (n % 3) * 4);
171                    grid[offset.0 + c.0][offset.1 + c.1] =
172                        format!("{}", n + 1).chars().next().unwrap();
173                }
174            }
175        }
176
177        // collect
178        let mut lines: Vec<String> = Vec::new();
179        for line in grid.iter() {
180            lines.push(line.iter().collect())
181        }
182        let result = lines.join("\n");
183        Ok(result)
184    }
185
186    fn play(&mut self, player: Space, grid: usize, cell: usize) -> Result<(), String> {
187        if grid == 0 || grid > 9 {
188            return Err(format!(
189                "invalid grid number, expected a number between 1 and 9 included, got {grid}"
190            ));
191        }
192        if cell == 0 || cell > 9 {
193            return Err(format!(
194                "invalid cell number, expected a number between 1 and 9 included, got {cell}"
195            ));
196        }
197        let grid = grid - 1;
198        self.grid[grid / 3][grid % 3].play(player, cell)
199    }
200
201    fn victories(&self) -> TicTacToe {
202        let mut t = TicTacToe::default();
203        for (a, b) in iproduct!(0..3, 0..3) {
204            t.grid[a][b] = self.grid[a][b].clone().victory();
205        }
206        t
207    }
208
209    fn victory(&self) -> Space {
210        self.victories().victory()
211    }
212}
213
214fn empty_giant_grid() -> [[char; 3 * 12]; 3 * 12] {
215    let mut grid = [[' '; 3 * 12]; 3 * 12];
216    for (x, y) in iproduct!(0..3, 0..3) {
217        let offset = (x * 12, y * 12);
218
219        // lines
220        for l in 0..2 {
221            for c in 0..11 {
222                grid[3 + l * 4 + offset.0][c + offset.1] = '-'
223            }
224        }
225
226        // columns
227        for c in 0..2 {
228            for l in 0..11 {
229                grid[l + offset.0][3 + c * 4 + offset.1] = '|'
230            }
231        }
232
233        // crosses
234        grid[offset.0 + 3][offset.1 + 3] = '+';
235        grid[offset.0 + 7][offset.1 + 3] = '+';
236        grid[offset.0 + 3][offset.1 + 7] = '+';
237        grid[offset.0 + 7][offset.1 + 7] = '+';
238    }
239    grid
240}
241
242fn read_num() -> usize {
243    let mut read = String::new();
244    println!("choice:");
245    loop {
246        let _ = io::stdin()
247            .read_line(&mut read)
248            .expect("unable to read line");
249
250        if let Ok(res) = read.trim().parse::<usize>() {
251            if res > 0 && res < 10 {
252                return res;
253            } else {
254                println!("expected a number between 0 and 9, got {res}");
255            }
256        } else {
257            println!("expected a number between 0 and 9");
258        }
259        read.clear()
260    }
261}
262
263/// Play a game of giant TicTacToe
264///
265/// Retruns the winner (Cross or Circle)
266pub fn run_game() -> Space {
267    let mut board = GiantTicTacToe::default();
268    let mut grid = 0;
269    let mut cell;
270    let players = [Space::Cross, Space::Circle].into_iter().cycle();
271    println!("{board}");
272    for player in players {
273        println!("next player: {player}");
274        if grid == 0 || grid > 9 {
275            println!("choose a grid:");
276            grid = read_num();
277        }
278        println!("{}", board.to_grid(Some(grid)).unwrap());
279        println!("giant victory");
280        println!("{}", board.victories());
281        println!("{player}: where to play?");
282        cell = read_num();
283        match board.play(player, grid, cell) {
284            Ok(_) => grid = cell,
285            Err(s) => {
286                println!("error while playing: {s}")
287            }
288        };
289
290        if board.victory() != Space::None {
291            break;
292        }
293    }
294    println!("{board}");
295    println!("{}", board.victories());
296    println!("victory: {}", board.victory());
297    board.victory()
298}