Set up a new game screen #45
6
Makefile
6
Makefile
|
@ -36,13 +36,13 @@ ifc-test:
|
||||||
cd ifc && make test
|
cd ifc && make test
|
||||||
|
|
||||||
kifu-core/dev:
|
kifu-core/dev:
|
||||||
cd kifu/kifu-core && make test
|
cd kifu/core && make test
|
||||||
|
|
||||||
kifu-core/test:
|
kifu-core/test:
|
||||||
cd kifu/kifu-core && make test
|
cd kifu/core && make test
|
||||||
|
|
||||||
kifu-core/test-oneshot:
|
kifu-core/test-oneshot:
|
||||||
cd kifu/kifu-core && make test-oneshot
|
cd kifu/core && make test-oneshot
|
||||||
|
|
||||||
kifu-gtk:
|
kifu-gtk:
|
||||||
cd kifu/gtk && make release
|
cd kifu/gtk && make release
|
||||||
|
|
|
@ -1,15 +1,21 @@
|
||||||
use crate::types::AppState;
|
use crate::{
|
||||||
use crate::ui::{playing_field, PlayingFieldView};
|
types::{AppState, GameState, Player, Rank},
|
||||||
|
ui::{new_game, playing_field, NewGameView, PlayingFieldView},
|
||||||
|
};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
use typeshare::typeshare;
|
use typeshare::typeshare;
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
#[serde(tag = "type", content = "content")]
|
#[serde(tag = "type", content = "content")]
|
||||||
pub enum CoreRequest {
|
pub enum CoreRequest {
|
||||||
|
CreateGame(CreateGameRequest),
|
||||||
|
LaunchScreen,
|
||||||
|
NewGame,
|
||||||
PlayingField,
|
PlayingField,
|
||||||
PlayStoneRequest(PlayStoneRequest),
|
PlayStone(PlayStoneRequest),
|
||||||
|
StartGame,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
@ -19,10 +25,40 @@ pub struct PlayStoneRequest {
|
||||||
pub row: u8,
|
pub row: u8,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[typeshare]
|
||||||
|
pub struct CreateGameRequest {
|
||||||
|
pub black_player: PlayerInfoRequest,
|
||||||
|
pub white_player: PlayerInfoRequest,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[typeshare]
|
||||||
|
pub enum PlayerInfoRequest {
|
||||||
|
Hotseat(HotseatPlayerRequest),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
|
#[typeshare]
|
||||||
|
pub struct HotseatPlayerRequest {
|
||||||
|
pub name: String,
|
||||||
|
pub rank: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<HotseatPlayerRequest> for Player {
|
||||||
|
fn from(p: HotseatPlayerRequest) -> Self {
|
||||||
|
Self {
|
||||||
|
name: p.name,
|
||||||
|
rank: p.rank.and_then(|r| Rank::try_from(r.as_ref()).ok()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
#[serde(tag = "type", content = "content")]
|
#[serde(tag = "type", content = "content")]
|
||||||
pub enum CoreResponse {
|
pub enum CoreResponse {
|
||||||
|
NewGameView(NewGameView),
|
||||||
PlayingFieldView(PlayingFieldView),
|
PlayingFieldView(PlayingFieldView),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -40,18 +76,57 @@ impl CoreApp {
|
||||||
|
|
||||||
pub async fn dispatch(&self, request: CoreRequest) -> CoreResponse {
|
pub async fn dispatch(&self, request: CoreRequest) -> CoreResponse {
|
||||||
match request {
|
match request {
|
||||||
|
/*
|
||||||
|
CoreRequest::LaunchScreen => {
|
||||||
|
let app_state = self.state.read().unwrap();
|
||||||
|
|
||||||
|
At launch, I want to either show a list of games in progress, the current game, or the game creation screen.
|
||||||
|
- if a live game is in progress, immmediately go to that game. Such a game will be classified at game creation, so it should be persisted to the state.
|
||||||
|
- if no live games are in progress, but there are slow games in progress, show a list of the slow games and let the player choose which one to jump into.
|
||||||
|
- if no games are in progress, show only the game creation screen
|
||||||
|
- game creation menu should be present both when there are only slow games and when there are no games
|
||||||
|
- the UI returned here will always be available in other places, such as when the user is viewing a game and wants to return to this page
|
||||||
|
|
||||||
|
For the initial version, I want only to show the game creation screen. Then I will backtrack record application state so that the only decisions can be made.
|
||||||
|
}
|
||||||
|
*/
|
||||||
|
CoreRequest::CreateGame(create_request) => {
|
||||||
|
let mut app_state = self.state.write().unwrap();
|
||||||
|
let white_player = {
|
||||||
|
match create_request.white_player {
|
||||||
|
PlayerInfoRequest::Hotseat(request) => Player::from(request),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let black_player = {
|
||||||
|
match create_request.black_player {
|
||||||
|
PlayerInfoRequest::Hotseat(request) => Player::from(request),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
app_state.game = Some(GameState {
|
||||||
|
white_player,
|
||||||
|
black_player,
|
||||||
|
..GameState::new()
|
||||||
|
});
|
||||||
|
let game_state = app_state.game.as_ref().unwrap();
|
||||||
|
CoreResponse::PlayingFieldView(playing_field(game_state))
|
||||||
|
}
|
||||||
|
CoreRequest::LaunchScreen => CoreResponse::NewGameView(new_game()),
|
||||||
|
CoreRequest::NewGame => CoreResponse::NewGameView(new_game()),
|
||||||
CoreRequest::PlayingField => {
|
CoreRequest::PlayingField => {
|
||||||
let app_state = self.state.read().unwrap();
|
let app_state = self.state.read().unwrap();
|
||||||
let game = app_state.game.as_ref().unwrap();
|
let game = app_state.game.as_ref().unwrap();
|
||||||
CoreResponse::PlayingFieldView(playing_field(game))
|
CoreResponse::PlayingFieldView(playing_field(game))
|
||||||
}
|
}
|
||||||
CoreRequest::PlayStoneRequest(request) => {
|
CoreRequest::PlayStone(request) => {
|
||||||
let mut app_state = self.state.write().unwrap();
|
let mut app_state = self.state.write().unwrap();
|
||||||
app_state.place_stone(request);
|
app_state.place_stone(request);
|
||||||
|
|
||||||
let game = app_state.game.as_ref().unwrap();
|
let game = app_state.game.as_ref().unwrap();
|
||||||
CoreResponse::PlayingFieldView(playing_field(game))
|
CoreResponse::PlayingFieldView(playing_field(game))
|
||||||
}
|
}
|
||||||
|
CoreRequest::StartGame => {
|
||||||
|
unimplemented!()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
mod api;
|
mod api;
|
||||||
pub use api::{CoreApp, CoreRequest, CoreResponse};
|
pub use api::{
|
||||||
|
CoreApp, CoreRequest, CoreResponse, CreateGameRequest, HotseatPlayerRequest, PlayerInfoRequest,
|
||||||
|
};
|
||||||
|
|
||||||
mod types;
|
mod types;
|
||||||
pub use types::{BoardError, Color, Size};
|
pub use types::{BoardError, Color, Rank, Size};
|
||||||
pub mod ui;
|
pub mod ui;
|
||||||
|
|
||||||
mod board;
|
mod board;
|
||||||
|
|
|
@ -55,7 +55,7 @@ impl AppState {
|
||||||
pub fn place_stone(&mut self, req: PlayStoneRequest) {
|
pub fn place_stone(&mut self, req: PlayStoneRequest) {
|
||||||
match self.game {
|
match self.game {
|
||||||
Some(ref mut game) => {
|
Some(ref mut game) => {
|
||||||
game.place_stone(Coordinate {
|
let _ = game.place_stone(Coordinate {
|
||||||
column: req.column,
|
column: req.column,
|
||||||
row: req.row,
|
row: req.row,
|
||||||
});
|
});
|
||||||
|
@ -65,8 +65,9 @@ impl AppState {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
enum Rank {
|
#[typeshare]
|
||||||
|
pub enum Rank {
|
||||||
Kyu(u8),
|
Kyu(u8),
|
||||||
Dan(u8),
|
Dan(u8),
|
||||||
Pro(u8),
|
Pro(u8),
|
||||||
|
@ -92,8 +93,8 @@ impl From<Rank> for String {
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Player {
|
pub struct Player {
|
||||||
name: String,
|
pub name: String,
|
||||||
rank: Rank,
|
pub rank: Option<Rank>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
|
@ -112,19 +113,19 @@ pub struct GameState {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl GameState {
|
impl GameState {
|
||||||
fn new() -> GameState {
|
pub fn new() -> GameState {
|
||||||
GameState {
|
GameState {
|
||||||
board: Board::new(),
|
board: Board::new(),
|
||||||
past_positions: vec![],
|
past_positions: vec![],
|
||||||
conversation: vec![],
|
conversation: vec![],
|
||||||
current_player: Color::Black,
|
current_player: Color::Black,
|
||||||
white_player: Player {
|
white_player: Player {
|
||||||
name: "Savanni".to_owned(),
|
name: "".to_owned(),
|
||||||
rank: Rank::Kyu(10),
|
rank: None,
|
||||||
},
|
},
|
||||||
black_player: Player {
|
black_player: Player {
|
||||||
name: "Opal".to_owned(),
|
name: "".to_owned(),
|
||||||
rank: Rank::Kyu(10),
|
rank: None,
|
||||||
},
|
},
|
||||||
white_clock: Duration::from_secs(600),
|
white_clock: Duration::from_secs(600),
|
||||||
black_clock: Duration::from_secs(600),
|
black_clock: Duration::from_secs(600),
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use typeshare::typeshare;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[typeshare]
|
||||||
|
pub struct Action<A> {
|
||||||
|
pub id: String,
|
||||||
|
pub label: String,
|
||||||
|
pub action: A,
|
||||||
|
}
|
|
@ -0,0 +1,18 @@
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use typeshare::typeshare;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[typeshare]
|
||||||
|
pub struct Menu<T: PartialEq + Eq> {
|
||||||
|
current: Option<T>,
|
||||||
|
options: Vec<T>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T: PartialEq + Eq> Menu<T> {
|
||||||
|
pub fn new(options: Vec<T>, current: Option<T>) -> Menu<T> {
|
||||||
|
if let Some(ref current) = current {
|
||||||
|
assert!(options.contains(current));
|
||||||
|
}
|
||||||
|
Menu { current, options }
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,2 @@
|
||||||
|
pub mod action;
|
||||||
|
pub mod menu;
|
|
@ -0,0 +1,14 @@
|
||||||
|
use crate::{
|
||||||
|
ui::types;
|
||||||
|
};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use typeshare::typeshare;
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum LaunchScreenView {
|
||||||
|
CreateGame(CreateGameView)
|
||||||
|
}
|
||||||
|
|
||||||
|
// This will be called when the Kifu application starts.
|
||||||
|
pub fn launch_screen() -> LaunchScreenView {
|
||||||
|
}
|
|
@ -1,6 +1,15 @@
|
||||||
|
mod elements;
|
||||||
|
pub use elements::{action::Action, menu::Menu};
|
||||||
|
|
||||||
mod playing_field;
|
mod playing_field;
|
||||||
pub use playing_field::{playing_field, PlayingFieldView};
|
pub use playing_field::{playing_field, PlayingFieldView};
|
||||||
|
|
||||||
|
// mod launch_screen;
|
||||||
|
// pub use launch_screen::{launch_screen, LaunchScreenView};
|
||||||
|
|
||||||
|
mod new_game;
|
||||||
|
pub use new_game::{new_game, HotseatPlayerElement, NewGameView, PlayerElement};
|
||||||
|
|
||||||
mod types;
|
mod types;
|
||||||
pub use types::{
|
pub use types::{
|
||||||
BoardElement, ChatElement, IntersectionElement, PlayerCardElement, StoneElement,
|
BoardElement, ChatElement, IntersectionElement, PlayerCardElement, StoneElement,
|
||||||
|
|
|
@ -0,0 +1,77 @@
|
||||||
|
use crate::ui::Action;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use typeshare::typeshare;
|
||||||
|
|
||||||
|
fn rank_strings() -> Vec<String> {
|
||||||
|
vec![
|
||||||
|
"", "30 kyu", "29 kyu", "28 kyu", "27 kyu", "26 kyu", "25 kyu", "24 kyu", "23 kyu",
|
||||||
|
"22 kyu", "21 kyu", "20 kyu", "19 kyu", "18 kyu", "17 kyu", "16 kyu", "15 kyu", "14 kyu",
|
||||||
|
"13 kyu", "12 kyu", "11 kyu", "10 kyu", "9 kyu", "8 kyu", "7 kyu", "6 kyu", "5 kyu",
|
||||||
|
"4 kyu", "3 kyu", "2 kyu", "1 kyu", "1 dan", "2 dan", "3 dan", "4 dan", "5 dan", "6 dan",
|
||||||
|
"7 dan", "8 dan", "9 dan", "1 pro", "2 pro", "3 pro", "4 pro", "5 pro", "6 pro", "7 pro",
|
||||||
|
"8 pro", "9 pro",
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.map(|s| s.to_owned())
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[typeshare]
|
||||||
|
pub enum PlayerElement {
|
||||||
|
Hotseat(HotseatPlayerElement),
|
||||||
|
// Remote(RemotePlayerElement),
|
||||||
|
// Bot(BotPlayerElement),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PlayerElement {
|
||||||
|
fn default() -> PlayerElement {
|
||||||
|
PlayerElement::Hotseat(HotseatPlayerElement::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize, Default)]
|
||||||
|
#[typeshare]
|
||||||
|
pub struct HotseatPlayerElement {
|
||||||
|
pub placeholder: Option<String>,
|
||||||
|
pub default_rank: Option<String>,
|
||||||
|
pub ranks: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[typeshare]
|
||||||
|
pub struct RemotePlayerElement {}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[typeshare]
|
||||||
|
pub struct BotPlayerElement {}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
#[typeshare]
|
||||||
|
pub struct NewGameView {
|
||||||
|
pub black_player: PlayerElement,
|
||||||
|
pub white_player: PlayerElement,
|
||||||
|
pub start_game: Action<()>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn new_game() -> NewGameView {
|
||||||
|
let black_player = PlayerElement::Hotseat(HotseatPlayerElement {
|
||||||
|
placeholder: Some("black player".to_owned()),
|
||||||
|
default_rank: None,
|
||||||
|
ranks: rank_strings(),
|
||||||
|
});
|
||||||
|
let white_player = PlayerElement::Hotseat(HotseatPlayerElement {
|
||||||
|
placeholder: Some("white player".to_owned()),
|
||||||
|
default_rank: None,
|
||||||
|
ranks: rank_strings(),
|
||||||
|
});
|
||||||
|
NewGameView {
|
||||||
|
black_player,
|
||||||
|
white_player,
|
||||||
|
start_game: Action {
|
||||||
|
id: "start-game-action".to_owned(),
|
||||||
|
label: "New Game".to_owned(),
|
||||||
|
action: (),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
|
@ -21,16 +21,24 @@ pub fn playing_field(game: &GameState) -> PlayingFieldView {
|
||||||
|
|
||||||
let player_card_black = types::PlayerCardElement {
|
let player_card_black = types::PlayerCardElement {
|
||||||
color: Color::Black,
|
color: Color::Black,
|
||||||
name: "Savanni".to_owned(),
|
name: game.black_player.name.clone(),
|
||||||
rank: "10k".to_owned(),
|
rank: game
|
||||||
clock: "24:53".to_owned(),
|
.black_player
|
||||||
|
.rank
|
||||||
|
.map(String::from)
|
||||||
|
.unwrap_or("".to_owned()),
|
||||||
|
clock: "".to_owned(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let player_card_white = types::PlayerCardElement {
|
let player_card_white = types::PlayerCardElement {
|
||||||
color: Color::White,
|
color: Color::White,
|
||||||
name: "Opal".to_owned(),
|
name: game.white_player.name.clone(),
|
||||||
rank: "10k".to_owned(),
|
rank: game
|
||||||
clock: "25:00".to_owned(),
|
.black_player
|
||||||
|
.rank
|
||||||
|
.map(String::from)
|
||||||
|
.unwrap_or("".to_owned()),
|
||||||
|
clock: "".to_owned(),
|
||||||
};
|
};
|
||||||
|
|
||||||
let chat = types::ChatElement {
|
let chat = types::ChatElement {
|
||||||
|
|
|
@ -31,7 +31,7 @@ impl StoneElement {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
|
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
|
||||||
#[typeshare]
|
#[typeshare]
|
||||||
#[serde(tag = "type", content = "content")]
|
#[serde(tag = "type", content = "content")]
|
||||||
pub enum IntersectionElement {
|
pub enum IntersectionElement {
|
||||||
|
@ -53,9 +53,10 @@ impl BoardElement {
|
||||||
.map(|row| {
|
.map(|row| {
|
||||||
(0..size.width)
|
(0..size.width)
|
||||||
.map(|column| {
|
.map(|column| {
|
||||||
IntersectionElement::Empty(CoreRequest::PlayStoneRequest(
|
IntersectionElement::Empty(CoreRequest::PlayStone(PlayStoneRequest {
|
||||||
PlayStoneRequest { column, row },
|
column,
|
||||||
))
|
row,
|
||||||
|
}))
|
||||||
})
|
})
|
||||||
.collect::<Vec<IntersectionElement>>()
|
.collect::<Vec<IntersectionElement>>()
|
||||||
})
|
})
|
||||||
|
@ -66,7 +67,12 @@ impl BoardElement {
|
||||||
|
|
||||||
pub fn stone(&self, column: u8, row: u8) -> IntersectionElement {
|
pub fn stone(&self, column: u8, row: u8) -> IntersectionElement {
|
||||||
let addr = self.addr(column, row);
|
let addr = self.addr(column, row);
|
||||||
self.spaces[addr]
|
self.spaces[addr].clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn stone_mut(&mut self, column: u8, row: u8) -> &mut IntersectionElement {
|
||||||
|
let addr = self.addr(column, row);
|
||||||
|
&mut self.spaces[addr]
|
||||||
}
|
}
|
||||||
|
|
||||||
fn addr(&self, column: u8, row: u8) -> usize {
|
fn addr(&self, column: u8, row: u8) -> usize {
|
||||||
|
@ -76,7 +82,8 @@ impl BoardElement {
|
||||||
|
|
||||||
impl From<&Board> for BoardElement {
|
impl From<&Board> for BoardElement {
|
||||||
fn from(board: &Board) -> Self {
|
fn from(board: &Board) -> Self {
|
||||||
let spaces: Vec<IntersectionElement> = (0..board.size.height)
|
let spaces: Vec<IntersectionElement> =
|
||||||
|
(0..board.size.height)
|
||||||
.map(|row| {
|
.map(|row| {
|
||||||
(0..board.size.width)
|
(0..board.size.width)
|
||||||
.map(|column| match board.stone(&Coordinate { column, row }) {
|
.map(|column| match board.stone(&Coordinate { column, row }) {
|
||||||
|
@ -85,7 +92,7 @@ impl From<&Board> for BoardElement {
|
||||||
liberties: None,
|
liberties: None,
|
||||||
color,
|
color,
|
||||||
}),
|
}),
|
||||||
None => IntersectionElement::Empty(CoreRequest::PlayStoneRequest(
|
None => IntersectionElement::Empty(CoreRequest::PlayStone(
|
||||||
PlayStoneRequest { column, row },
|
PlayStoneRequest { column, row },
|
||||||
)),
|
)),
|
||||||
})
|
})
|
||||||
|
|
|
@ -3,3 +3,7 @@ release:
|
||||||
|
|
||||||
dev:
|
dev:
|
||||||
cargo watch -x 'run --bin kifu-gtk'
|
cargo watch -x 'run --bin kifu-gtk'
|
||||||
|
|
||||||
|
screenplay:
|
||||||
|
cargo watch -x 'run --bin screenplay --features="screenplay"'
|
||||||
|
|
||||||
|
|
|
@ -19,3 +19,14 @@ impl CoreApi {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn perftrace<F, A>(trace_name: &str, f: F) -> A
|
||||||
|
where
|
||||||
|
F: FnOnce() -> A,
|
||||||
|
{
|
||||||
|
let start = std::time::Instant::now();
|
||||||
|
let result = f();
|
||||||
|
let end = std::time::Instant::now();
|
||||||
|
println!("[Trace: {}] {:?}", trace_name, end - start);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
|
@ -1,8 +1,38 @@
|
||||||
use gtk::prelude::*;
|
use gtk::prelude::*;
|
||||||
use kifu_core::{CoreApp, CoreRequest, CoreResponse};
|
use kifu_core::{CoreApp, CoreRequest, CoreResponse};
|
||||||
use kifu_gtk::{ui::PlayingField, CoreApi};
|
use kifu_gtk::{
|
||||||
|
perftrace,
|
||||||
|
ui::{NewGame, PlayingField},
|
||||||
|
CoreApi,
|
||||||
|
};
|
||||||
use std::sync::{Arc, RwLock};
|
use std::sync::{Arc, RwLock};
|
||||||
|
|
||||||
|
fn handle_response(api: CoreApi, window: gtk::ApplicationWindow, message: CoreResponse) {
|
||||||
|
let playing_field = Arc::new(RwLock::new(None));
|
||||||
|
match message {
|
||||||
|
CoreResponse::NewGameView(view) => perftrace("NewGameView", || {
|
||||||
|
let api = api.clone();
|
||||||
|
|
||||||
|
let new_game = NewGame::new(api, view);
|
||||||
|
window.set_child(Some(&new_game));
|
||||||
|
}),
|
||||||
|
CoreResponse::PlayingFieldView(view) => perftrace("PlayingFieldView", || {
|
||||||
|
let api = api.clone();
|
||||||
|
|
||||||
|
let mut playing_field = playing_field.write().unwrap();
|
||||||
|
if playing_field.is_none() {
|
||||||
|
perftrace("creating a new playing field", || {
|
||||||
|
let field = PlayingField::new(api, view);
|
||||||
|
window.set_child(Some(&field));
|
||||||
|
*playing_field = Some(field);
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
playing_field.as_ref().map(|field| field.update_view(view));
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
gio::resources_register_include!("com.luminescent-dreams.kifu-gtk.gresource")
|
gio::resources_register_include!("com.luminescent-dreams.kifu-gtk.gresource")
|
||||||
.expect("Failed to register resources");
|
.expect("Failed to register resources");
|
||||||
|
@ -44,30 +74,15 @@ fn main() {
|
||||||
|
|
||||||
gtk_rx.attach(None, {
|
gtk_rx.attach(None, {
|
||||||
let api = api.clone();
|
let api = api.clone();
|
||||||
let playing_field = Arc::new(RwLock::new(None));
|
|
||||||
move |message| {
|
move |message| {
|
||||||
match message {
|
perftrace("handle_response", || {
|
||||||
CoreResponse::PlayingFieldView(view) => {
|
handle_response(api.clone(), window.clone(), message)
|
||||||
let api = api.clone();
|
});
|
||||||
|
|
||||||
let start = std::time::Instant::now();
|
|
||||||
let mut playing_field = playing_field.write().unwrap();
|
|
||||||
if playing_field.is_none() {
|
|
||||||
let field = PlayingField::new(api, view);
|
|
||||||
window.set_child(Some(&field));
|
|
||||||
*playing_field = Some(field);
|
|
||||||
} else {
|
|
||||||
playing_field.as_ref().map(|field| field.update_view(view));
|
|
||||||
}
|
|
||||||
let end = std::time::Instant::now();
|
|
||||||
println!("Time to render the playing field: {:?}", end - start);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Continue(true)
|
Continue(true)
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
api.dispatch(CoreRequest::PlayingField);
|
api.dispatch(CoreRequest::NewGame);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::CoreApi;
|
use crate::{perftrace, CoreApi};
|
||||||
use gio::resources_lookup_data;
|
use gio::resources_lookup_data;
|
||||||
use glib::Object;
|
use glib::Object;
|
||||||
use gtk::{
|
use gtk::{
|
||||||
|
@ -83,6 +83,7 @@ impl ObjectImpl for BoardPrivate {
|
||||||
|
|
||||||
self.drawing_area
|
self.drawing_area
|
||||||
.set_draw_func(move |_, context, width, height| {
|
.set_draw_func(move |_, context, width, height| {
|
||||||
|
perftrace("render drawing area", || {
|
||||||
let render_start = std::time::Instant::now();
|
let render_start = std::time::Instant::now();
|
||||||
let board = board.borrow();
|
let board = board.borrow();
|
||||||
|
|
||||||
|
@ -163,6 +164,7 @@ impl ObjectImpl for BoardPrivate {
|
||||||
}
|
}
|
||||||
let render_end = std::time::Instant::now();
|
let render_end = std::time::Instant::now();
|
||||||
println!("board rendering time: {:?}", render_end - render_start);
|
println!("board rendering time: {:?}", render_end - render_start);
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
let motion_controller = gtk::EventControllerMotion::new();
|
let motion_controller = gtk::EventControllerMotion::new();
|
||||||
|
|
|
@ -7,6 +7,9 @@ pub use chat::Chat;
|
||||||
mod playing_field;
|
mod playing_field;
|
||||||
pub use playing_field::PlayingField;
|
pub use playing_field::PlayingField;
|
||||||
|
|
||||||
|
mod new_game;
|
||||||
|
pub use new_game::NewGame;
|
||||||
|
|
||||||
mod board;
|
mod board;
|
||||||
pub use board::Board;
|
pub use board::Board;
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,150 @@
|
||||||
|
use crate::CoreApi;
|
||||||
|
use glib::Object;
|
||||||
|
use gtk::{glib, prelude::*, subclass::prelude::*};
|
||||||
|
use kifu_core::{
|
||||||
|
ui::{NewGameView, PlayerElement},
|
||||||
|
CoreRequest, CreateGameRequest, HotseatPlayerRequest, PlayerInfoRequest,
|
||||||
|
};
|
||||||
|
use std::{cell::RefCell, rc::Rc};
|
||||||
|
|
||||||
|
struct PlayerDataEntryPrivate {
|
||||||
|
name: gtk::Text,
|
||||||
|
rank: gtk::DropDown,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for PlayerDataEntryPrivate {
|
||||||
|
fn default() -> Self {
|
||||||
|
let rank = gtk::DropDown::builder().build();
|
||||||
|
Self {
|
||||||
|
name: gtk::Text::builder().build(),
|
||||||
|
rank,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for PlayerDataEntryPrivate {
|
||||||
|
const NAME: &'static str = "PlayerDataEntry";
|
||||||
|
type Type = PlayerDataEntry;
|
||||||
|
type ParentType = gtk::Box;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for PlayerDataEntryPrivate {}
|
||||||
|
impl WidgetImpl for PlayerDataEntryPrivate {}
|
||||||
|
impl BoxImpl for PlayerDataEntryPrivate {}
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
struct PlayerDataEntry(ObjectSubclass<PlayerDataEntryPrivate>) @extends gtk::Box, gtk::Widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PlayerDataEntry {
|
||||||
|
pub fn new(element: PlayerElement) -> PlayerDataEntry {
|
||||||
|
let s: Self = Object::builder().build();
|
||||||
|
|
||||||
|
let rank_model = gio::ListStore::new(gtk::StringObject::static_type());
|
||||||
|
s.imp().rank.set_model(Some(&rank_model));
|
||||||
|
|
||||||
|
match element {
|
||||||
|
PlayerElement::Hotseat(player) => {
|
||||||
|
if let Some(placeholder) = player.placeholder {
|
||||||
|
s.imp().name.set_placeholder_text(Some(&placeholder));
|
||||||
|
}
|
||||||
|
player.ranks.iter().for_each(|rank| rank_model.append(>k::StringObject::new(rank)));
|
||||||
|
}
|
||||||
|
// PlayerElement::Remote(_) => s.imp().placeholder.set_text("remote player"),
|
||||||
|
// PlayerElement::Bot(_) => s.imp().placeholder.set_text("bot player"),
|
||||||
|
}
|
||||||
|
|
||||||
|
s.append(&s.imp().name);
|
||||||
|
s.append(&s.imp().rank);
|
||||||
|
|
||||||
|
s
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn text(&self) -> String {
|
||||||
|
let name = self.imp().name.buffer().text().to_string();
|
||||||
|
if name.is_empty() {
|
||||||
|
self.imp()
|
||||||
|
.name
|
||||||
|
.placeholder_text()
|
||||||
|
.map(|s| s.to_string())
|
||||||
|
.unwrap_or("".to_owned())
|
||||||
|
} else {
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn rank(&self) -> Option<String> {
|
||||||
|
self.imp().rank.selected_item().and_then(|obj| {
|
||||||
|
let str_obj = obj.downcast::<gtk::StringObject>().ok()?;
|
||||||
|
Some(str_obj.string().clone().to_string())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct NewGamePrivate {
|
||||||
|
black_player: Rc<RefCell<Option<PlayerDataEntry>>>,
|
||||||
|
white_player: Rc<RefCell<Option<PlayerDataEntry>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for NewGamePrivate {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
black_player: Rc::new(RefCell::new(None)),
|
||||||
|
white_player: Rc::new(RefCell::new(None)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[glib::object_subclass]
|
||||||
|
impl ObjectSubclass for NewGamePrivate {
|
||||||
|
const NAME: &'static str = "NewGame";
|
||||||
|
type Type = NewGame;
|
||||||
|
type ParentType = gtk::Grid;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ObjectImpl for NewGamePrivate {}
|
||||||
|
impl WidgetImpl for NewGamePrivate {}
|
||||||
|
impl GridImpl for NewGamePrivate {}
|
||||||
|
|
||||||
|
glib::wrapper! {
|
||||||
|
pub struct NewGame(ObjectSubclass<NewGamePrivate>) @extends gtk::Grid, gtk::Widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NewGame {
|
||||||
|
pub fn new(api: CoreApi, view: NewGameView) -> NewGame {
|
||||||
|
let s: Self = Object::builder().build();
|
||||||
|
|
||||||
|
let black_player = PlayerDataEntry::new(view.black_player);
|
||||||
|
s.attach(&black_player, 1, 1, 1, 1);
|
||||||
|
*s.imp().black_player.borrow_mut() = Some(black_player.clone());
|
||||||
|
|
||||||
|
let white_player = PlayerDataEntry::new(view.white_player);
|
||||||
|
s.attach(&white_player, 2, 1, 1, 1);
|
||||||
|
*s.imp().white_player.borrow_mut() = Some(white_player.clone());
|
||||||
|
|
||||||
|
let new_game_button = gtk::Button::builder().label(&view.start_game.label).build();
|
||||||
|
s.attach(&new_game_button, 2, 2, 1, 1);
|
||||||
|
|
||||||
|
new_game_button.connect_clicked({
|
||||||
|
move |_| {
|
||||||
|
let black_player = black_player.clone();
|
||||||
|
let white_player = white_player.clone();
|
||||||
|
|
||||||
|
api.dispatch(CoreRequest::CreateGame(CreateGameRequest {
|
||||||
|
black_player: player_info(black_player.clone()),
|
||||||
|
white_player: player_info(white_player.clone()),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
s
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn player_info(player: PlayerDataEntry) -> PlayerInfoRequest {
|
||||||
|
PlayerInfoRequest::Hotseat(HotseatPlayerRequest {
|
||||||
|
name: player.text(),
|
||||||
|
rank: player.rank(),
|
||||||
|
})
|
||||||
|
}
|
|
@ -1,5 +1,8 @@
|
||||||
use crate::ui::{Board, Chat, PlayerCard};
|
use crate::{
|
||||||
use crate::CoreApi;
|
perftrace,
|
||||||
|
ui::{Board, Chat, PlayerCard},
|
||||||
|
CoreApi,
|
||||||
|
};
|
||||||
use glib::Object;
|
use glib::Object;
|
||||||
use gtk::{prelude::*, subclass::prelude::*};
|
use gtk::{prelude::*, subclass::prelude::*};
|
||||||
use kifu_core::ui::PlayingFieldView;
|
use kifu_core::ui::PlayingFieldView;
|
||||||
|
@ -69,10 +72,12 @@ impl PlayingField {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_view(&self, view: PlayingFieldView) {
|
pub fn update_view(&self, view: PlayingFieldView) {
|
||||||
|
perftrace("update_view", || {
|
||||||
self.imp().board.borrow().as_ref().map(|board| {
|
self.imp().board.borrow().as_ref().map(|board| {
|
||||||
board.set_board(view.board);
|
board.set_board(view.board);
|
||||||
board.set_current_player(view.current_player);
|
board.set_current_player(view.current_player);
|
||||||
});
|
});
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -82,8 +87,8 @@ pub fn playing_field_view() -> PlayingFieldView {
|
||||||
width: 19,
|
width: 19,
|
||||||
height: 19,
|
height: 19,
|
||||||
});
|
});
|
||||||
*board.stone_mut(4, 4) = IntersectionElement::Filled(StoneElement::new(Color::White));
|
*board.stone_mut(4, 4) = IntersectionElement::Filled(StoneElement::new(Color::White, None));
|
||||||
*board.stone_mut(15, 15) = IntersectionElement::Filled(StoneElement::new(Color::Black));
|
*board.stone_mut(15, 15) = IntersectionElement::Filled(StoneElement::new(Color::Black, None));
|
||||||
*board.stone_mut(18, 18) = IntersectionElement::Unplayable;
|
*board.stone_mut(18, 18) = IntersectionElement::Unplayable;
|
||||||
let player_card_black = PlayerCardElement {
|
let player_card_black = PlayerCardElement {
|
||||||
color: Color::Black,
|
color: Color::Black,
|
||||||
|
|
Loading…
Reference in New Issue