mirror of
https://github.com/danbulant/cushy
synced 2026-06-16 21:11:20 +00:00
Installing a callback now returns a CallbackHandle. All map-style APIs install this handle automatically on the created dynamic, which keeps the callback installed until the dynamic is freed. All other APIs return the handle for the caller to either call persist() or store somewhere. Now, the dynamic system can be used for application-long data with almost no fear of leaking data due to how callbacks are being installed. Technically cycles are still possible by moving clones into the callbacks, so a WeakDynamic type might be worth exposing.
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 gooey::value::Dynamic;
|
|
use gooey::widget::MakeWidget;
|
|
use gooey::widgets::button::ButtonKind;
|
|
use gooey::{Run, WithClone};
|
|
use kludgine::figures::units::Lp;
|
|
|
|
fn main() -> gooey::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()
|
|
}
|