From f2a63cf3c329cd19dfdb293fd3920a6095e38ea4 Mon Sep 17 00:00:00 2001 From: Savanni D'Gerinel Date: Tue, 27 Feb 2024 10:00:32 -0500 Subject: [PATCH] Set up a placeholder for the GameViewModel --- kifu/core/src/board.rs | 30 +++---- kifu/core/src/lib.rs | 6 +- kifu/core/src/types.rs | 12 +-- kifu/core/src/ui/types.rs | 6 +- kifu/gtk/src/view_models/game_view_model.rs | 86 +++++++++++++++++++++ kifu/gtk/src/view_models/mod.rs | 6 +- 6 files changed, 117 insertions(+), 29 deletions(-) create mode 100644 kifu/gtk/src/view_models/game_view_model.rs diff --git a/kifu/core/src/board.rs b/kifu/core/src/board.rs index d94d5c9..c42fc68 100644 --- a/kifu/core/src/board.rs +++ b/kifu/core/src/board.rs @@ -2,12 +2,12 @@ use crate::{BoardError, Color, Size}; use std::collections::HashSet; #[derive(Clone, Debug, Default)] -pub struct Board { +pub struct Goban { pub size: Size, pub groups: Vec, } -impl std::fmt::Display for Board { +impl std::fmt::Display for Goban { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> { write!(f, " ")?; // for c in 'A'..'U' { @@ -31,7 +31,7 @@ impl std::fmt::Display for Board { } } -impl PartialEq for Board { +impl PartialEq for Goban { fn eq(&self, other: &Self) -> bool { if self.size != other.size { return false; @@ -51,7 +51,7 @@ impl PartialEq for Board { } } -impl Board { +impl Goban { pub fn new() -> Self { Self { size: Size { @@ -77,7 +77,7 @@ pub struct Coordinate { pub row: u8, } -impl Board { +impl Goban { pub fn place_stone(mut self, coordinate: Coordinate, color: Color) -> Result { if self.stone(&coordinate).is_some() { return Err(BoardError::InvalidPosition); @@ -224,8 +224,8 @@ mod test { * A stone placed in a suicidal position is legal if it captures other stones first. */ - fn with_example_board(test: impl FnOnce(Board)) { - let board = Board::from_coordinates( + fn with_example_board(test: impl FnOnce(Goban)) { + let board = Goban::from_coordinates( vec![ (Coordinate { column: 3, row: 3 }, Color::White), (Coordinate { column: 3, row: 4 }, Color::White), @@ -284,7 +284,7 @@ mod test { #[test] fn it_gets_adjacencies_for_coordinate() { - let board = Board::new(); + let board = Goban::new(); for column in 0..19 { for row in 0..19 { for coordinate in board.adjacencies(&Coordinate { column, row }) { @@ -302,7 +302,7 @@ mod test { #[test] fn it_counts_individual_liberties() { - let board = Board::from_coordinates( + let board = Goban::from_coordinates( vec![ (Coordinate { column: 3, row: 3 }, Color::White), (Coordinate { column: 0, row: 3 }, Color::White), @@ -357,7 +357,7 @@ mod test { #[test] fn stones_share_liberties() { - with_example_board(|board: Board| { + with_example_board(|board: Goban| { let test_cases = vec![ ( board.clone(), @@ -567,11 +567,11 @@ mod test { #[test] fn validate_group_comparisons() { { - let b1 = Board::from_coordinates( + let b1 = Goban::from_coordinates( vec![(Coordinate { column: 7, row: 9 }, Color::White)].into_iter(), ) .unwrap(); - let b2 = Board::from_coordinates( + let b2 = Goban::from_coordinates( vec![(Coordinate { column: 7, row: 9 }, Color::White)].into_iter(), ) .unwrap(); @@ -580,7 +580,7 @@ mod test { } { - let b1 = Board::from_coordinates( + let b1 = Goban::from_coordinates( vec![ (Coordinate { column: 7, row: 9 }, Color::White), (Coordinate { column: 8, row: 10 }, Color::White), @@ -588,7 +588,7 @@ mod test { .into_iter(), ) .unwrap(); - let b2 = Board::from_coordinates( + let b2 = Goban::from_coordinates( vec![ (Coordinate { column: 8, row: 10 }, Color::White), (Coordinate { column: 7, row: 9 }, Color::White), @@ -603,7 +603,7 @@ mod test { #[test] fn two_boards_can_be_compared() { - let board = Board::from_coordinates( + let board = Goban::from_coordinates( vec![ (Coordinate { column: 7, row: 9 }, Color::White), (Coordinate { column: 8, row: 8 }, Color::White), diff --git a/kifu/core/src/lib.rs b/kifu/core/src/lib.rs index aa31343..2a62b0b 100644 --- a/kifu/core/src/lib.rs +++ b/kifu/core/src/lib.rs @@ -2,8 +2,8 @@ extern crate config_derive; mod api; pub use api::{ - ChangeSettingRequest, Core, CoreRequest, CoreResponse, CreateGameRequest, - HotseatPlayerRequest, PlayerInfoRequest, CoreNotification, + ChangeSettingRequest, Core, CoreNotification, CoreRequest, CoreResponse, CreateGameRequest, + HotseatPlayerRequest, PlayerInfoRequest, }; mod board; @@ -12,6 +12,6 @@ pub use board::*; mod database; mod types; -pub use types::{BoardError, Color, Rank, Size, DatabasePath, Config, ConfigOption}; +pub use types::{BoardError, Color, Config, ConfigOption, DatabasePath, Player, Rank, Size}; pub mod ui; diff --git a/kifu/core/src/types.rs b/kifu/core/src/types.rs index 99bcec2..cec63b6 100644 --- a/kifu/core/src/types.rs +++ b/kifu/core/src/types.rs @@ -1,6 +1,6 @@ use crate::{ api::PlayStoneRequest, - board::{Board, Coordinate}, + board::{Goban, Coordinate}, database::Database, }; use config::define_config; @@ -132,8 +132,8 @@ pub struct Player { #[derive(Debug)] pub struct GameState { - pub board: Board, - pub past_positions: Vec, + pub board: Goban, + pub past_positions: Vec, pub conversation: Vec, pub current_player: Color, @@ -148,7 +148,7 @@ pub struct GameState { impl Default for GameState { fn default() -> Self { Self { - board: Board::new(), + board: Goban::new(), past_positions: vec![], conversation: vec![], current_player: Color::Black, @@ -200,7 +200,7 @@ mod test { #[test] fn current_player_remains_the_same_after_self_capture() { let mut state = GameState::default(); - state.board = Board::from_coordinates( + state.board = Goban::from_coordinates( vec![ (Coordinate { column: 17, row: 0 }, Color::White), (Coordinate { column: 17, row: 1 }, Color::White), @@ -221,7 +221,7 @@ mod test { #[test] fn ko_rules_are_enforced() { let mut state = GameState::default(); - state.board = Board::from_coordinates( + state.board = Goban::from_coordinates( vec![ (Coordinate { column: 7, row: 9 }, Color::White), (Coordinate { column: 8, row: 8 }, Color::White), diff --git a/kifu/core/src/ui/types.rs b/kifu/core/src/ui/types.rs index 9bfa6cf..ab855a3 100644 --- a/kifu/core/src/ui/types.rs +++ b/kifu/core/src/ui/types.rs @@ -1,7 +1,7 @@ use crate::types::{Color, Size}; use crate::{ api::{CoreRequest, PlayStoneRequest}, - Board, Coordinate, + Goban, Coordinate, }; use serde::{Deserialize, Serialize}; use typeshare::typeshare; @@ -80,8 +80,8 @@ impl BoardElement { } } -impl From<&Board> for BoardElement { - fn from(board: &Board) -> Self { +impl From<&Goban> for BoardElement { + fn from(board: &Goban) -> Self { let spaces: Vec = (0..board.size.height) .map(|row| { diff --git a/kifu/gtk/src/view_models/game_view_model.rs b/kifu/gtk/src/view_models/game_view_model.rs new file mode 100644 index 0000000..bd1cde5 --- /dev/null +++ b/kifu/gtk/src/view_models/game_view_model.rs @@ -0,0 +1,86 @@ +/* +Copyright 2024, Savanni D'Gerinel + +This file is part of Kifu. + +Kifu is free software: you can redistribute it and/or modify it under the terms of the GNU +General Public License as published by the Free Software Foundation, either version 3 of the +License, or (at your option) any later version. + +Kifu is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without +even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License along with Kifu. If not, see . +*/ + +use async_channel::Receiver; +use async_std::task::{spawn, yield_now, JoinHandle}; +use kifu_core::{Color, Core, CoreNotification, Goban, Player}; +use std::{ + sync::{Arc, RwLock}, + time::Duration, +}; + +struct GameViewModelPrivate { + white: Player, /* Maybe this should be PlayerState, instead, combining the player info, current clock, and current captures. */ + black: Player, + current: Color, + goban: Goban, /* Or perhaps clocks, captures, and the board should be bound into GameState. */ + white_clock: Duration, + black_clock: Duration, +} + +/// The Game View Model manages the current state of the game. It shows the two player cards, the board, the current capture count, the current player, and it maintains the UI for the clock (bearing in mind that the real clock is managed in the core). This view model should only be created once the details of the game, whether a game in progress or a new game (this view model won't know the difference) is known. +pub struct GameViewModel { + core: Core, + notification_handler: JoinHandle<()>, + widget: gtk::Box, + data: Arc>, +} + +impl GameViewModelPrivate { + fn handle(&mut self, message: CoreNotification) {} +} + +impl GameViewModel { + pub fn new( + white: Player, + black: Player, + current: Color, + goban: Goban, + white_clock: Duration, + black_clock: Duration, + core: Core, + notifications: Receiver, + ) -> Self { + let data = Arc::new(RwLock::new(GameViewModelPrivate { + white, + black, + current, + goban, + white_clock, + black_clock, + })); + + let handler = spawn({ + let data = data.clone(); + async move { + loop { + match notifications.recv().await { + Ok(msg) => data.write().unwrap().handle(msg), + Err(err) => unimplemented!("Should display an error message in the UI"), + } + yield_now().await; + } + } + }); + + Self { + core, + notification_handler: handler, + widget: gtk::Box::new(gtk::Orientation::Horizontal, 0), + data, + } + } +} diff --git a/kifu/gtk/src/view_models/mod.rs b/kifu/gtk/src/view_models/mod.rs index 0082a78..e41396c 100644 --- a/kifu/gtk/src/view_models/mod.rs +++ b/kifu/gtk/src/view_models/mod.rs @@ -14,7 +14,6 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with Kifu. If not, see . */ - /* Every view model requires a reference to the app so that it can call functions on the core, and a notification receiver so that it can receive messages from the core. @@ -22,4 +21,7 @@ The view model is primary over the view. It will construct the view, it can make */ mod database_view_model; -pub use database_view_model::DatabaseViewModel; \ No newline at end of file +pub use database_view_model::DatabaseViewModel; + +mod game_view_model; +pub use game_view_model::GameViewModel;