mirror of
https://github.com/danbulant/cushy
synced 2026-06-19 22:41:10 +00:00
Refs #98 This refactor overhauls the reactive system to move all the reactive methods to traits. The side effect of this change is that now DynamicReader's API is the same as Dynamic's API, but because it only implements Source<T>, DynamicReader does not offer any mutation functions. While it's unfortunate to have more traits to include to use Cushy, this seems like the best option, and it offers a path to try to integrate this into the tuple ForEach/MapEach traits. Unfortunately, my attempt at doing those in this set of changes led to issues specifying generic associated lifetimes for the DynamicGuard. But, I was also in the middle of this larger refactoring, so it might be that a fresh attempt will succeed.
204 lines
5.4 KiB
Rust
204 lines
5.4 KiB
Rust
use std::fmt::Display;
|
|
use std::iter;
|
|
use std::ops::Not;
|
|
use std::time::SystemTime;
|
|
|
|
use cushy::value::{Destination, Dynamic, Source};
|
|
use cushy::widget::MakeWidget;
|
|
use cushy::widgets::button::ButtonKind;
|
|
use cushy::{Run, WithClone};
|
|
use figures::units::Lp;
|
|
|
|
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
|
|
.clone()
|
|
.into_button()
|
|
.kind(ButtonKind::Outline)
|
|
.on_click(move |_| game.lock().play(row, column))
|
|
.with_enabled(enabled)
|
|
.pad()
|
|
.expand()
|
|
}
|