Integrate a remote playtesting front-end
This commit is contained in:
@@ -1,28 +1,29 @@
|
|||||||
use crate::hand_view::HandView;
|
use crate::hand_view::HandView;
|
||||||
|
use crate::remote_view::RemoteGameView;
|
||||||
use crate::tile_view::PlacedTileView;
|
use crate::tile_view::PlacedTileView;
|
||||||
|
use crate::types::SelectedTile;
|
||||||
use board_shared::board::Board;
|
use board_shared::board::Board;
|
||||||
use board_shared::deck::RngDeck;
|
use board_shared::deck::RngDeck;
|
||||||
use board_shared::expr::is_valid_guess;
|
use board_shared::expr::is_valid_guess;
|
||||||
use board_shared::game::Game;
|
use board_shared::game::Game;
|
||||||
use board_shared::position::Grid2d;
|
use board_shared::position::Grid2d;
|
||||||
use board_shared::tile::Tile;
|
use board_shared::tile::Tile;
|
||||||
|
use futures::StreamExt;
|
||||||
use gloo_dialogs::alert;
|
use gloo_dialogs::alert;
|
||||||
|
use gloo_net::websocket::futures::WebSocket;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use web_sys::HtmlInputElement;
|
||||||
use yew::prelude::*;
|
use yew::prelude::*;
|
||||||
|
|
||||||
enum SelectedTile {
|
|
||||||
InHand(usize),
|
|
||||||
Equals,
|
|
||||||
None,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Properties, PartialEq)]
|
#[derive(Properties, PartialEq)]
|
||||||
struct BoardViewProps {
|
pub struct BoardViewProps {
|
||||||
board: Board,
|
pub board: Board,
|
||||||
on_click: Callback<(usize, usize)>,
|
pub on_click: Callback<(usize, usize)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component(BoardView)]
|
#[function_component(BoardView)]
|
||||||
fn board_view(BoardViewProps { board, on_click }: &BoardViewProps) -> Html {
|
pub fn board_view(BoardViewProps { board, on_click }: &BoardViewProps) -> Html {
|
||||||
html! {
|
html! {
|
||||||
<table class="board">
|
<table class="board">
|
||||||
{ (0..board.height()).map(|y| html! {
|
{ (0..board.height()).map(|y| html! {
|
||||||
@@ -36,8 +37,8 @@ fn board_view(BoardViewProps { board, on_click }: &BoardViewProps) -> Html {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[function_component(App)]
|
#[function_component(GameView)]
|
||||||
pub fn app() -> Html {
|
pub fn game_view() -> Html {
|
||||||
let game = use_state(Game::default);
|
let game = use_state(Game::default);
|
||||||
let current_game = use_state(Game::default);
|
let current_game = use_state(Game::default);
|
||||||
|
|
||||||
@@ -128,3 +129,86 @@ pub fn app() -> Html {
|
|||||||
</main>
|
</main>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[function_component(App)]
|
||||||
|
pub fn app() -> Html {
|
||||||
|
let remote = use_state(|| "ws://localhost:8081/ws".to_string());
|
||||||
|
let player_name = use_state(|| None);
|
||||||
|
let room_name = use_state(|| None);
|
||||||
|
|
||||||
|
let player_name_ref = use_node_ref();
|
||||||
|
let room_name_ref = use_node_ref();
|
||||||
|
let on_create_click = {
|
||||||
|
let player_name = player_name.clone();
|
||||||
|
let player_name_ref = player_name_ref.clone();
|
||||||
|
Callback::from(move |_| {
|
||||||
|
player_name.set(Some(
|
||||||
|
player_name_ref
|
||||||
|
.cast::<HtmlInputElement>()
|
||||||
|
.expect("player_name_ref is not attached to a input element")
|
||||||
|
.value(),
|
||||||
|
));
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let on_join_click = {
|
||||||
|
let player_name = player_name.clone();
|
||||||
|
let player_name_ref = player_name_ref.clone();
|
||||||
|
let room_name = room_name.clone();
|
||||||
|
let room_name_ref = room_name_ref.clone();
|
||||||
|
Callback::from(move |_| {
|
||||||
|
player_name.set(Some(
|
||||||
|
player_name_ref
|
||||||
|
.cast::<HtmlInputElement>()
|
||||||
|
.expect("player_name_ref is not attached to a input element")
|
||||||
|
.value(),
|
||||||
|
));
|
||||||
|
room_name.set(Some(
|
||||||
|
room_name_ref
|
||||||
|
.cast::<HtmlInputElement>()
|
||||||
|
.expect("room_name_ref is not attached to a input element")
|
||||||
|
.value(),
|
||||||
|
));
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(player_name) = (*player_name).as_ref() {
|
||||||
|
let ws = WebSocket::open(remote.as_ref()).unwrap();
|
||||||
|
let (write, read) = ws.split();
|
||||||
|
|
||||||
|
return html! {
|
||||||
|
<div class="app">
|
||||||
|
<RemoteGameView
|
||||||
|
write={Rc::new(RefCell::new(write))}
|
||||||
|
read={Rc::new(RefCell::new(read))}
|
||||||
|
player_name={player_name.clone()}
|
||||||
|
room_name={(*room_name).clone()} />
|
||||||
|
</div>
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<main>
|
||||||
|
<div>
|
||||||
|
<label>{"Server: "}</label>
|
||||||
|
<input type="text" required={true} value={"ws://localhost:8081"} />
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label>{"Player name: "}</label>
|
||||||
|
<input type="text" required={true} value={"test"} ref={player_name_ref} />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="display: grid; grid-template-columns: 2fr 2fr;">
|
||||||
|
<div>
|
||||||
|
<button onclick={on_create_click}>{"Create"}</button>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<div>
|
||||||
|
<label>{"Room name: "}</label>
|
||||||
|
<input type="text" ref={room_name_ref} />
|
||||||
|
</div>
|
||||||
|
<button onclick={on_join_click}>{"Join"}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
mod app;
|
mod app;
|
||||||
mod hand_view;
|
mod hand_view;
|
||||||
|
mod remote_view;
|
||||||
mod tile_view;
|
mod tile_view;
|
||||||
|
mod types;
|
||||||
|
|
||||||
use app::App;
|
use app::App;
|
||||||
|
|
||||||
|
149
board-frontend/src/remote_view.rs
Normal file
149
board-frontend/src/remote_view.rs
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
use crate::app::BoardView;
|
||||||
|
use crate::types::SelectedTile;
|
||||||
|
use board_network::protocol::{ClientMessage, ServerMessage};
|
||||||
|
use board_shared::game::Game;
|
||||||
|
use futures::stream::{SplitSink, SplitStream};
|
||||||
|
use futures::{SinkExt, StreamExt};
|
||||||
|
use gloo_dialogs::alert;
|
||||||
|
use gloo_net::websocket::futures::WebSocket;
|
||||||
|
use gloo_net::websocket::Message;
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use yew::platform::spawn_local;
|
||||||
|
use yew::prelude::*;
|
||||||
|
|
||||||
|
#[derive(Properties)]
|
||||||
|
pub struct RemoteGameViewProps {
|
||||||
|
pub write: Rc<RefCell<SplitSink<WebSocket, Message>>>,
|
||||||
|
pub read: Rc<RefCell<SplitStream<WebSocket>>>,
|
||||||
|
pub player_name: String,
|
||||||
|
pub room_name: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for RemoteGameViewProps {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
Rc::ptr_eq(&self.write, &other.write)
|
||||||
|
&& Rc::ptr_eq(&self.read, &other.read)
|
||||||
|
&& self.player_name == other.player_name
|
||||||
|
&& self.room_name == other.room_name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[function_component(RemoteGameView)]
|
||||||
|
pub fn remote_game_view(
|
||||||
|
RemoteGameViewProps {
|
||||||
|
write,
|
||||||
|
read,
|
||||||
|
player_name,
|
||||||
|
room_name,
|
||||||
|
}: &RemoteGameViewProps,
|
||||||
|
) -> Html {
|
||||||
|
macro_rules! send_client_message {
|
||||||
|
($write:expr, $message:expr) => {{
|
||||||
|
let write = $write.clone();
|
||||||
|
spawn_local(async move {
|
||||||
|
write
|
||||||
|
.borrow_mut()
|
||||||
|
.send(Message::Text(
|
||||||
|
serde_json::to_string(&$message).expect("Cannot serialize"),
|
||||||
|
))
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
});
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
let selected_tile = use_state(|| SelectedTile::None);
|
||||||
|
let is_started = use_state(|| false);
|
||||||
|
let current_player_turn = use_state(|| 0);
|
||||||
|
let game = use_state(Game::default);
|
||||||
|
{
|
||||||
|
let player_name = player_name.clone();
|
||||||
|
let room_name = room_name.clone();
|
||||||
|
let write = write.clone();
|
||||||
|
use_effect_with_deps(
|
||||||
|
move |_| {
|
||||||
|
send_client_message!(
|
||||||
|
write,
|
||||||
|
if let Some(room_name) = room_name {
|
||||||
|
ClientMessage::JoinRoom(room_name, player_name)
|
||||||
|
} else {
|
||||||
|
ClientMessage::CreateRoom(player_name)
|
||||||
|
}
|
||||||
|
);
|
||||||
|
},
|
||||||
|
(),
|
||||||
|
);
|
||||||
|
|
||||||
|
let is_started = is_started.clone();
|
||||||
|
let current_player_turn = current_player_turn.clone();
|
||||||
|
let read = read.clone();
|
||||||
|
use_effect_with_deps(
|
||||||
|
move |_| {
|
||||||
|
spawn_local(async move {
|
||||||
|
while let Some(event) = read.borrow_mut().next().await {
|
||||||
|
if let Message::Text(msg) = event.unwrap() {
|
||||||
|
match serde_json::from_str::<ServerMessage>(&msg) {
|
||||||
|
Ok(ServerMessage::JoinedRoom {
|
||||||
|
room_name,
|
||||||
|
has_started,
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
alert(&format!("Joined room {}", room_name));
|
||||||
|
is_started.set(has_started);
|
||||||
|
}
|
||||||
|
Ok(ServerMessage::PlayerTurn(player_id)) => {
|
||||||
|
current_player_turn.set(player_id);
|
||||||
|
is_started.set(true);
|
||||||
|
}
|
||||||
|
r => {
|
||||||
|
alert(&format!("{:?}", r));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|| {}
|
||||||
|
},
|
||||||
|
(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
let on_validate_click = {
|
||||||
|
let write = write.clone();
|
||||||
|
Callback::from(move |_| {
|
||||||
|
send_client_message!(write, ClientMessage::Validate);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let on_start_game_click = {
|
||||||
|
let write = write.clone();
|
||||||
|
Callback::from(move |_| {
|
||||||
|
send_client_message!(write, ClientMessage::StartGame);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
let on_equals_select = {
|
||||||
|
Callback::from(move |_| {
|
||||||
|
selected_tile.set(SelectedTile::Equals);
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
html! {
|
||||||
|
<main>
|
||||||
|
<h1>{"Remote Game"}</h1>
|
||||||
|
<BoardView board={game.board.clone()} on_click={Callback::from(|_| {})} />
|
||||||
|
<div class="row">
|
||||||
|
<button onclick={on_equals_select} class="button">{"="}</button>
|
||||||
|
if *is_started {
|
||||||
|
<button onclick={on_validate_click} class="button">{"Validate"}</button>
|
||||||
|
} else {
|
||||||
|
<button onclick={on_start_game_click} class="button">{"Start"}</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
if *is_started {
|
||||||
|
<div class="row">
|
||||||
|
<p>{format!("Player {}'s turn", *current_player_turn)}</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</main>
|
||||||
|
}
|
||||||
|
}
|
5
board-frontend/src/types.rs
Normal file
5
board-frontend/src/types.rs
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
pub enum SelectedTile {
|
||||||
|
InHand(usize),
|
||||||
|
Equals,
|
||||||
|
None,
|
||||||
|
}
|
@@ -35,6 +35,7 @@ pub enum ServerMessage {
|
|||||||
room_name: String,
|
room_name: String,
|
||||||
players: Vec<(String, u32, bool)>,
|
players: Vec<(String, u32, bool)>,
|
||||||
active_player: usize,
|
active_player: usize,
|
||||||
|
has_started: bool,
|
||||||
},
|
},
|
||||||
/// Notify that the room cannot be joined.
|
/// Notify that the room cannot be joined.
|
||||||
JoinFailed(String),
|
JoinFailed(String),
|
||||||
|
@@ -72,6 +72,7 @@ impl Room {
|
|||||||
.map(|p| (p.name.clone(), p.score, p.ws.is_some()))
|
.map(|p| (p.name.clone(), p.score, p.ws.is_some()))
|
||||||
.collect(),
|
.collect(),
|
||||||
active_player: self.active_player,
|
active_player: self.active_player,
|
||||||
|
has_started: self.has_started,
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
Reference in New Issue
Block a user