use std::fmt::Display; use std::iter; use std::ops::Not; use std::time::SystemTime; use cushy::figures::units::Lp; use cushy::value::{Destination, Dynamic, Source}; use cushy::widget::MakeWidget; use cushy::widgets::button::ButtonKind; use cushy::{Run, WithClone}; fn main() -> cushy::Result { let app = Dynamic::default(); app.map_each(app.with_clone(|app| { move |state: &AppState| match state { AppState::Playing => play_screen(&app).make_widget(), AppState::Winner(winner) => game_end(*winner, &app).make_widget(), } })) .into_switcher() .contain() .width(Lp::inches(2)..Lp::inches(6)) .height(Lp::inches(2)..Lp::inches(6)) .centered() .run() } #[derive(Default, Debug, Eq, PartialEq)] enum AppState { #[default] Playing, Winner(Option), } #[derive(Clone, Copy, Debug, Eq, PartialEq)] enum Player { X, O, } impl Display for Player { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Player::X => f.write_str("X"), Player::O => f.write_str("O"), } } } impl Not for Player { type Output = Self; fn not(self) -> Self::Output { match self { Self::X => Self::O, Self::O => Self::X, } } } struct GameState { app: Dynamic, current_player: Player, cells: Vec>, } impl GameState { fn new_game(app: &Dynamic) -> Self { Self { app: app.clone(), // Bad RNG: if we have an even milliseconds in the current // timestamp, it's O's turn first. current_player: if SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .expect("invalid system time") .as_millis() % 2 == 0 { Player::O } else { Player::X }, cells: iter::repeat(None).take(9).collect(), } } fn play(&mut self, row: usize, column: usize) { let player = self.current_player; self.current_player = !player; self.cells[row * 3 + column] = Some(player); if let Some(winner) = self.check_for_winner() { self.app.set(AppState::Winner(Some(winner))); } else if self.cells.iter().all(Option::is_some) { self.app.set(AppState::Winner(None)); } } fn check_for_winner(&self) -> Option { // Rows and columns for i in 0..3 { if let Some(winner) = self .winner_in_cells([[i, 0], [i, 1], [i, 2]]) .or_else(|| self.winner_in_cells([[0, i], [1, i], [2, i]])) { return Some(winner); } } // Diagonals self.winner_in_cells([[0, 0], [1, 1], [2, 2]]) .or_else(|| self.winner_in_cells([[2, 0], [1, 1], [0, 2]])) } fn winner_in_cells(&self, cells: [[usize; 2]; 3]) -> Option { match ( self.cell(cells[0][0], cells[0][1]), self.cell(cells[1][0], cells[1][1]), self.cell(cells[2][0], cells[2][1]), ) { (Some(a), Some(b), Some(c)) if a == b && b == c => Some(a), _ => None, } } fn cell(&self, row: usize, column: usize) -> Option { self.cells[row * 3 + column] } } fn game_end(winner: Option, app: &Dynamic) -> impl MakeWidget { let app = app.clone(); let label = if let Some(winner) = winner { format!("{winner:?} wins!") } else { String::from("No winner") }; label .h1() .and( "Play Again" .into_button() .on_click(move |_| { app.set(AppState::Playing); }) .into_default(), ) .into_rows() .centered() .expand() } fn play_screen(app: &Dynamic) -> impl MakeWidget { let game = Dynamic::new(GameState::new_game(app)); let current_player_label = game.map_each(|state| format!("{}'s Turn", state.current_player)); current_player_label.and(play_grid(&game)).into_rows() } fn play_grid(game: &Dynamic) -> impl MakeWidget { row_of_squares(0, game) .expand() .and(row_of_squares(1, game).expand()) .and(row_of_squares(2, game).expand()) .into_rows() } fn row_of_squares(row: usize, game: &Dynamic) -> impl MakeWidget { square(row, 0, game) .expand() .and(square(row, 1, game).expand()) .and(square(row, 2, game).expand()) .into_columns() } fn square(row: usize, column: usize, game: &Dynamic) -> impl MakeWidget { let game = game.clone(); let enabled = Dynamic::new(true); let label = Dynamic::default(); (&enabled, &label).with_clone(|(enabled, label)| { game.for_each(move |state| { let Some(player) = state.cell(row, column) else { return; }; if enabled.replace(false).is_some() { label.set(player.to_string()); } }) .persist(); }); label .into_button() .kind(ButtonKind::Outline) .on_click(move |_| game.lock().play(row, column)) .with_enabled(enabled) .pad() .expand() }