tic_tac_toe/
tic_tac_toe.rs

1/*
2 * Copyright (c) 2025 Jasmine Tai. All rights reserved.
3 */
4
5//! An interactive tic-tac-toe game.
6
7use std::io;
8
9use line_ui::element::{Cursor, Element, IntoElement};
10use line_ui::{Renderer, Style};
11use termion::event::{Event, Key};
12use termion::input::TermRead;
13use termion::raw::IntoRawMode;
14
15#[derive(Debug, Clone, Copy, Default)]
16enum Player {
17    #[default]
18    X,
19    O,
20}
21
22#[derive(Debug, Default)]
23struct TicTacToe {
24    pub grid: [[Option<Player>; 3]; 3],
25    pub current: Player,
26}
27
28impl TicTacToe {
29    fn place(&mut self, row: usize, col: usize) {
30        if self.grid[row][col].is_some() {
31            // Square already occupied
32            return;
33        }
34        self.grid[row][col] = Some(self.current);
35        self.current = match self.current {
36            Player::X => Player::O,
37            Player::O => Player::X,
38        };
39    }
40
41    fn check_win(&self) -> Option<Option<Player>> {
42        for i in 0..3 {
43            for line in [
44                self.grid[i],                           // row
45                [0, 1, 2].map(|j| self.grid[j][i]),     // column
46                [0, 1, 2].map(|j| self.grid[j][j]),     // diagonal
47                [0, 1, 2].map(|j| self.grid[2 - j][j]), // other diagonal
48            ] {
49                match line {
50                    [Some(Player::X), Some(Player::X), Some(Player::X)] => {
51                        return Some(Some(Player::X));
52                    }
53                    [Some(Player::O), Some(Player::O), Some(Player::O)] => {
54                        return Some(Some(Player::O));
55                    }
56                    _ => {}
57                }
58            }
59        }
60
61        if self.grid.as_flattened().iter().all(Option::is_some) {
62            Some(None) // draw; all cells filled
63        } else {
64            None
65        }
66    }
67}
68
69fn main() -> io::Result<()> {
70    let stdout = io::stdout().into_raw_mode()?;
71    let mut r = Renderer::new(stdout);
72    let mut events = std::io::stdin().events();
73
74    let mut game = TicTacToe::default();
75    let (mut row, mut col): (usize, usize) = (1, 1);
76    loop {
77        // Render the grid
78        r.reset()?;
79        for (i, line) in game.grid.iter().enumerate() {
80            if i != 0 {
81                r.render("--+---+--".into_element())?;
82            }
83            r.render((
84                ((row, col) == (i, 0)).then_some(Cursor),
85                render_player(line[0]),
86                " | ".into_element(),
87                ((row, col) == (i, 1)).then_some(Cursor),
88                render_player(line[1]),
89                " | ".into_element(),
90                ((row, col) == (i, 2)).then_some(Cursor),
91                render_player(line[2]),
92            ))?;
93        }
94
95        // Check if someone won
96        let result = game.check_win();
97        match result {
98            Some(Some(winner)) => {
99                r.render((
100                    "The winner is ".into_element(),
101                    render_player(Some(winner)),
102                    "!".into_element(),
103                ))?;
104            }
105            Some(None) => {
106                r.render("The game is a draw.".into_element())?;
107            }
108            _ => {}
109        }
110
111        r.finish()?;
112        if result.is_some() {
113            r.leave()?;
114            break;
115        }
116
117        // Poll an input
118        let Some(event) = events.next() else {
119            break;
120        };
121        match event? {
122            Event::Key(Key::Up) => row = row.checked_sub(1).unwrap_or(2),
123            Event::Key(Key::Down) => row = (row + 1) % 3,
124            Event::Key(Key::Left) => col = col.checked_sub(1).unwrap_or(2),
125            Event::Key(Key::Right) => col = (col + 1) % 3,
126            Event::Key(Key::Char(' ' | '\n' | '\r')) => game.place(row, col),
127            _ => {}
128        }
129    }
130
131    Ok(())
132}
133
134fn render_player(player: Option<Player>) -> impl Element {
135    match player {
136        None => "-".with_style(Style::fg(245)),
137        Some(Player::X) => "X".with_style(Style::BOLD + Style::fg(33)),
138        Some(Player::O) => "O".with_style(Style::BOLD + Style::fg(203)),
139    }
140}