-
Notifications
You must be signed in to change notification settings - Fork 27
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Tic-tac-toe, Buttons labels now stretch to fill
- Loading branch information
Showing
2 changed files
with
219 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,198 @@ | ||
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::new(AppState::Winner(None)); | ||
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(), | ||
} | ||
})) | ||
.switcher() | ||
.contain() | ||
.width(Lp::inches(2)..Lp::inches(6)) | ||
.height(Lp::inches(2)..Lp::inches(6)) | ||
.centered() | ||
.expand() | ||
.run() | ||
} | ||
|
||
#[derive(Default, Debug)] | ||
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 { | ||
// TODO we need typography styles | ||
let app = app.clone(); | ||
let label = if let Some(winner) = winner { | ||
format!("{winner:?} wins!") | ||
} else { | ||
String::from("No winner") | ||
}; | ||
|
||
label | ||
.and("Play Again".into_button().on_click(move |_| { | ||
app.set(AppState::Playing); | ||
})) | ||
.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.update(false) { | ||
label.update(player.to_string()); | ||
} | ||
}); | ||
}); | ||
|
||
label | ||
.clone() | ||
.into_button() | ||
.enabled(enabled) | ||
.kind(ButtonKind::Outline) | ||
.on_click(move |_| game.lock().play(row, column)) | ||
.expand() | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters