cushy/examples/tic-tac-toe.rs
2024-07-26 10:34:30 -07:00

203 lines
5.4 KiB
Rust

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<Player>),
}
#[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<AppState>,
current_player: Player,
cells: Vec<Option<Player>>,
}
impl GameState {
fn new_game(app: &Dynamic<AppState>) -> 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<Player> {
// 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<Player> {
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<Player> {
self.cells[row * 3 + column]
}
}
fn game_end(winner: Option<Player>, app: &Dynamic<AppState>) -> 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<AppState>) -> 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<GameState>) -> 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<GameState>) -> 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<GameState>) -> 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()
}