Multiplayer #1
13 changed files with 115 additions and 148 deletions
|
@ -13,7 +13,7 @@ use std::collections::HashMap;
|
||||||
use std::sync::{Arc, LazyLock, Weak};
|
use std::sync::{Arc, LazyLock, Weak};
|
||||||
use tokio::select;
|
use tokio::select;
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
use word_grid::api::{APIGame, ApiState};
|
use word_grid::api::{APIGame, ApiState, Update};
|
||||||
use word_grid::dictionary::{Dictionary, DictionaryImpl};
|
use word_grid::dictionary::{Dictionary, DictionaryImpl};
|
||||||
use word_grid::game::{Error, Game, PlayedTile};
|
use word_grid::game::{Error, Game, PlayedTile};
|
||||||
use word_grid::player_interaction::ai::Difficulty;
|
use word_grid::player_interaction::ai::Difficulty;
|
||||||
|
@ -73,7 +73,6 @@ enum RoomEvent {
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
enum GameEvent {
|
enum GameEvent {
|
||||||
TurnAction { state: ApiState, committed: bool },
|
TurnAction { state: ApiState, committed: bool },
|
||||||
WordAdded { word: String, player: Player },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Debug)]
|
#[derive(Clone, Serialize, Debug)]
|
||||||
|
@ -81,7 +80,6 @@ enum GameEvent {
|
||||||
enum ServerToClientMessage {
|
enum ServerToClientMessage {
|
||||||
RoomChange { event: RoomEvent, info: PartyInfo },
|
RoomChange { event: RoomEvent, info: PartyInfo },
|
||||||
GameEvent { event: GameEvent },
|
GameEvent { event: GameEvent },
|
||||||
WordAdded { word: String },
|
|
||||||
GameError { error: Error },
|
GameError { error: Error },
|
||||||
Invalid { reason: String },
|
Invalid { reason: String },
|
||||||
}
|
}
|
||||||
|
@ -115,7 +113,7 @@ enum ClientToServerMessage {
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
enum InnerRoomMessage {
|
enum InnerRoomMessage {
|
||||||
PassThrough(ServerToClientMessage),
|
PassThrough(ServerToClientMessage),
|
||||||
GameEvent,
|
GameEvent(Option<Update>),
|
||||||
}
|
}
|
||||||
|
|
||||||
type RoomMap = HashMap<String, Weak<RwLock<Room>>>;
|
type RoomMap = HashMap<String, Weak<RwLock<Room>>>;
|
||||||
|
@ -134,6 +132,10 @@ async fn incoming_message_handler<E: std::fmt::Display>(
|
||||||
Some(message) => {
|
Some(message) => {
|
||||||
match message {
|
match message {
|
||||||
Ok(message) => {
|
Ok(message) => {
|
||||||
|
if let Message::Ping(_) = message {
|
||||||
|
println!("Received ping from player {player:#?}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
let message = message.to_text().unwrap();
|
let message = message.to_text().unwrap();
|
||||||
if message.len() == 0 {
|
if message.len() == 0 {
|
||||||
println!("Websocket closed");
|
println!("Websocket closed");
|
||||||
|
@ -173,7 +175,7 @@ async fn incoming_message_handler<E: std::fmt::Display>(
|
||||||
println!("Received {message:#?} from client {}", player.id);
|
println!("Received {message:#?} from client {}", player.id);
|
||||||
match message {
|
match message {
|
||||||
ClientToServerMessage::Load => {
|
ClientToServerMessage::Load => {
|
||||||
return !game_load(player, room, stream).await
|
return !game_load(player, None, room, stream).await
|
||||||
}
|
}
|
||||||
ClientToServerMessage::StartGame => {
|
ClientToServerMessage::StartGame => {
|
||||||
let mut room = room.write().await;
|
let mut room = room.write().await;
|
||||||
|
@ -200,7 +202,7 @@ async fn incoming_message_handler<E: std::fmt::Display>(
|
||||||
let game = APIGame::new(game);
|
let game = APIGame::new(game);
|
||||||
room.game = Some(game);
|
room.game = Some(game);
|
||||||
|
|
||||||
sender.send(InnerRoomMessage::GameEvent).unwrap();
|
sender.send(InnerRoomMessage::GameEvent(None)).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ClientToServerMessage::GameMove { r#move } => {
|
ClientToServerMessage::GameMove { r#move } => {
|
||||||
|
@ -242,8 +244,10 @@ async fn incoming_message_handler<E: std::fmt::Display>(
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {
|
Ok(event) => {
|
||||||
sender.send(InnerRoomMessage::GameEvent).unwrap();
|
sender
|
||||||
|
.send(InnerRoomMessage::GameEvent(event.update))
|
||||||
|
.unwrap();
|
||||||
}
|
}
|
||||||
Err(error) => {
|
Err(error) => {
|
||||||
let event = ServerToClientMessage::GameError { error };
|
let event = ServerToClientMessage::GameError { error };
|
||||||
|
@ -292,11 +296,17 @@ async fn incoming_message_handler<E: std::fmt::Display>(
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn game_load(player: &Player, room: &Arc<RwLock<Room>>, stream: &mut DuplexStream) -> bool {
|
async fn game_load(
|
||||||
|
player: &Player,
|
||||||
|
update: Option<Update>,
|
||||||
|
room: &Arc<RwLock<Room>>,
|
||||||
|
stream: &mut DuplexStream,
|
||||||
|
) -> bool {
|
||||||
// The game object was modified; we need to trigger a load from this player's perspective
|
// The game object was modified; we need to trigger a load from this player's perspective
|
||||||
let mut room = room.write().await;
|
let mut room = room.write().await;
|
||||||
|
|
||||||
let state = room.game.as_mut().unwrap().load(&player.name).unwrap();
|
let mut state = room.game.as_mut().unwrap().load(&player.name).unwrap();
|
||||||
|
state.update = update;
|
||||||
let event = ServerToClientMessage::GameEvent {
|
let event = ServerToClientMessage::GameEvent {
|
||||||
event: GameEvent::TurnAction {
|
event: GameEvent::TurnAction {
|
||||||
state,
|
state,
|
||||||
|
@ -325,7 +335,7 @@ async fn outgoing_message_handler<E: std::fmt::Debug>(
|
||||||
let x = stream.send(text.into()).await;
|
let x = stream.send(text.into()).await;
|
||||||
x.is_err()
|
x.is_err()
|
||||||
}
|
}
|
||||||
InnerRoomMessage::GameEvent => !game_load(player, room, stream).await,
|
InnerRoomMessage::GameEvent(update) => !game_load(player, update, room, stream).await,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -32,7 +32,6 @@ export function Game(props: {
|
||||||
}) {
|
}) {
|
||||||
|
|
||||||
const [api_state, setAPIState] = useState<APIState>(undefined);
|
const [api_state, setAPIState] = useState<APIState>(undefined);
|
||||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
|
||||||
const [confirmedScorePoints, setConfirmedScorePoints] = useState<number>(-1);
|
const [confirmedScorePoints, setConfirmedScorePoints] = useState<number>(-1);
|
||||||
const currentTurnNumber = useRef<number>(-1);
|
const currentTurnNumber = useRef<number>(-1);
|
||||||
const historyProcessedNumber = useRef<number>(0);
|
const historyProcessedNumber = useRef<number>(0);
|
||||||
|
@ -44,15 +43,11 @@ export function Game(props: {
|
||||||
|
|
||||||
|
|
||||||
function waitForUpdate() {
|
function waitForUpdate() {
|
||||||
// setAPIState(undefined);
|
|
||||||
// setIsLoading(true);
|
|
||||||
setIsLoading(true);
|
|
||||||
const result = props.api.load(true);
|
const result = props.api.load(true);
|
||||||
|
|
||||||
result.then(
|
result.then(
|
||||||
(state) => {
|
(state) => {
|
||||||
setAPIState(state);
|
setAPIState(state);
|
||||||
setIsLoading(false);
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
|
@ -136,7 +131,6 @@ export function Game(props: {
|
||||||
.then(
|
.then(
|
||||||
(api_state) => {
|
(api_state) => {
|
||||||
setAPIState(api_state);
|
setAPIState(api_state);
|
||||||
waitForUpdate();
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error({error});
|
console.error({error});
|
||||||
|
@ -149,14 +143,11 @@ export function Game(props: {
|
||||||
const [logInfo, logDispatch] = useReducer(addLogInfo, []);
|
const [logInfo, logDispatch] = useReducer(addLogInfo, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
props.api.register_log_dispatch(logDispatch);
|
|
||||||
props.api.load(false)
|
props.api.load(false)
|
||||||
.then((api_state) => {
|
.then((api_state) => {
|
||||||
setAPIState(api_state);
|
setAPIState(api_state);
|
||||||
setIsLoading(false);
|
|
||||||
});
|
});
|
||||||
}, [])
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
function movePlayableLetters(playerLetters: PlayableLetterData[], update: TileDispatchAction) {
|
function movePlayableLetters(playerLetters: PlayableLetterData[], update: TileDispatchAction) {
|
||||||
if (update.action === TileDispatchActionType.RETRIEVE) {
|
if (update.action === TileDispatchActionType.RETRIEVE) {
|
||||||
|
@ -255,10 +246,12 @@ export function Game(props: {
|
||||||
their turn.</div>);
|
their turn.</div>);
|
||||||
} else if (action.type == "Pass") {
|
} else if (action.type == "Pass") {
|
||||||
logDispatch(<div>{playerName} passed.</div>);
|
logDispatch(<div>{playerName} passed.</div>);
|
||||||
|
} else if (action.type == "AddToDictionary") {
|
||||||
|
logDispatch(<div>{playerName} added {action.word} to the dictionary.</div>)
|
||||||
|
} else {
|
||||||
|
console.error("Received unknown turn action: ", action);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Clear any on-screen arrows
|
|
||||||
gridArrowDispatch({action: GridArrowDispatchActionType.CLEAR});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function endGame(state: GameState) {
|
function endGame(state: GameState) {
|
||||||
|
@ -363,18 +356,24 @@ export function Game(props: {
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (api_state) {
|
if (api_state) {
|
||||||
|
console.log("In state: ", api_state.public_information.current_turn_number);
|
||||||
|
console.log("In ref: ", currentTurnNumber.current);
|
||||||
console.debug(api_state);
|
console.debug(api_state);
|
||||||
gridArrowDispatch({action: GridArrowDispatchActionType.CLEAR});
|
if(currentTurnNumber.current < api_state.public_information.current_turn_number){
|
||||||
trayDispatch({action: TileDispatchActionType.RETRIEVE});
|
// We only clear everything if there's a chance the board changed
|
||||||
setConfirmedScorePoints(-1);
|
// We may have gotten a dictionary update event which doesn't count
|
||||||
updateBoardLetters(api_state.public_information.board);
|
gridArrowDispatch({action: GridArrowDispatchActionType.CLEAR});
|
||||||
|
trayDispatch({action: TileDispatchActionType.RETRIEVE});
|
||||||
|
setConfirmedScorePoints(-1);
|
||||||
|
updateBoardLetters(api_state.public_information.board);
|
||||||
|
}
|
||||||
|
|
||||||
for (let i = historyProcessedNumber.current; i < api_state.public_information.history.length; i++) {
|
for (let i = historyProcessedNumber.current; i < api_state.public_information.history.length; i++) {
|
||||||
const update = api_state.public_information.history[i];
|
const update = api_state.public_information.history[i];
|
||||||
if (update.turn_number > currentTurnNumber.current) {
|
if (update.turn_number > currentTurnNumber.current) {
|
||||||
currentTurnNumber.current = update.turn_number;
|
currentTurnNumber.current = update.turn_number;
|
||||||
logDispatch(<h4>TTurn {update.turn_number}</h4>);
|
logDispatch(<h4>Turn {update.turn_number + 1}</h4>);
|
||||||
const playerAtTurn = api_state.public_information.players[(update.turn_number-1) % api_state.public_information.players.length].name;
|
const playerAtTurn = api_state.public_information.players[(update.turn_number) % api_state.public_information.players.length].name;
|
||||||
logDispatch(<div>{playerAtTurn}'s turn</div>);
|
logDispatch(<div>{playerAtTurn}'s turn</div>);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -384,9 +383,7 @@ export function Game(props: {
|
||||||
historyProcessedNumber.current = api_state.public_information.history.length;
|
historyProcessedNumber.current = api_state.public_information.history.length;
|
||||||
|
|
||||||
if (!isGameOver) {
|
if (!isGameOver) {
|
||||||
console.log("In state: ", api_state.public_information.current_turn_number);
|
if(api_state.public_information.current_turn_number > currentTurnNumber.current){
|
||||||
console.log("In ref: ", currentTurnNumber.current);
|
|
||||||
if(api_state.public_information.current_turn_number >= currentTurnNumber.current){
|
|
||||||
logDispatch(<h4>Turn {api_state.public_information.current_turn_number + 1}</h4>);
|
logDispatch(<h4>Turn {api_state.public_information.current_turn_number + 1}</h4>);
|
||||||
logDispatch(<div>{api_state.public_information.current_player}'s turn</div>);
|
logDispatch(<div>{api_state.public_information.current_player}'s turn</div>);
|
||||||
currentTurnNumber.current = api_state.public_information.current_turn_number;
|
currentTurnNumber.current = api_state.public_information.current_turn_number;
|
||||||
|
@ -395,24 +392,19 @@ export function Game(props: {
|
||||||
endGame(api_state.public_information.game_state);
|
endGame(api_state.public_information.game_state);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(api_state.public_information.current_player != props.settings.playerName) {
|
||||||
|
waitForUpdate();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [api_state]);
|
}, [api_state]);
|
||||||
|
|
||||||
|
if(api_state == null){
|
||||||
if (isLoading) {
|
|
||||||
return <div>Still loading</div>;
|
return <div>Still loading</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
const playerAndScores = api_state.public_information.players;
|
const playerAndScores = api_state.public_information.players;
|
||||||
const remainingTiles = api_state.public_information.remaining_tiles;
|
const remainingTiles = api_state.public_information.remaining_tiles;
|
||||||
let remainingAITiles = null;
|
const isPlayersTurn = api_state.public_information.current_player == props.settings.playerName;
|
||||||
for (let player of playerAndScores) {
|
|
||||||
if (player.name == 'AI') {
|
|
||||||
remainingAITiles = player.tray_tiles;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<TileExchangeModal
|
<TileExchangeModal
|
||||||
|
@ -449,11 +441,8 @@ export function Game(props: {
|
||||||
<div>
|
<div>
|
||||||
{remainingTiles} letters remaining
|
{remainingTiles} letters remaining
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
{props.settings.aiName} has {remainingAITiles} tiles
|
|
||||||
</div>
|
|
||||||
<button
|
<button
|
||||||
disabled={remainingTiles == 0 || isGameOver}
|
disabled={remainingTiles == 0 || isGameOver || !isPlayersTurn}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
trayDispatch({action: TileDispatchActionType.RETURN}); // want all tiles back on tray for tile exchange
|
trayDispatch({action: TileDispatchActionType.RETURN}); // want all tiles back on tray for tile exchange
|
||||||
setIsTileExchangeOpen(true);
|
setIsTileExchangeOpen(true);
|
||||||
|
@ -464,7 +453,7 @@ export function Game(props: {
|
||||||
<div className="player-controls">
|
<div className="player-controls">
|
||||||
<button
|
<button
|
||||||
className="check"
|
className="check"
|
||||||
disabled={isGameOver}
|
disabled={isGameOver || !isPlayersTurn}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
const playedTiles = playerLetters.map((i) => {
|
const playedTiles = playerLetters.map((i) => {
|
||||||
if (i === undefined) {
|
if (i === undefined) {
|
||||||
|
@ -492,6 +481,8 @@ export function Game(props: {
|
||||||
result
|
result
|
||||||
.then(
|
.then(
|
||||||
(api_state) => {
|
(api_state) => {
|
||||||
|
console.log("Testing45")
|
||||||
|
console.log({api_state});
|
||||||
|
|
||||||
const play_tiles: TurnAction = api_state.update.type;
|
const play_tiles: TurnAction = api_state.update.type;
|
||||||
if (play_tiles.type == "PlayTiles") {
|
if (play_tiles.type == "PlayTiles") {
|
||||||
|
@ -499,7 +490,6 @@ export function Game(props: {
|
||||||
|
|
||||||
if (committing) {
|
if (committing) {
|
||||||
setAPIState(api_state);
|
setAPIState(api_state);
|
||||||
waitForUpdate();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
@ -514,7 +504,12 @@ export function Game(props: {
|
||||||
const word = error.split(' ')[0];
|
const word = error.split(' ')[0];
|
||||||
// For whatever reason I can't pass props.api.add_to_dictionary directly
|
// For whatever reason I can't pass props.api.add_to_dictionary directly
|
||||||
logDispatch(<AddWordButton word={word}
|
logDispatch(<AddWordButton word={word}
|
||||||
addWordFn={(word) => props.api.add_to_dictionary(word)}/>);
|
addWordFn={(word) => {
|
||||||
|
props.api.add_to_dictionary(word)
|
||||||
|
.then((api_state) => {
|
||||||
|
setAPIState(api_state);
|
||||||
|
})
|
||||||
|
}}/>);
|
||||||
} else {
|
} else {
|
||||||
logDispatch(<div>{error}</div>);
|
logDispatch(<div>{error}</div>);
|
||||||
}
|
}
|
||||||
|
@ -532,7 +527,7 @@ export function Game(props: {
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
className="pass"
|
className="pass"
|
||||||
disabled={isGameOver}
|
disabled={isGameOver || !isPlayersTurn}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (window.confirm("Are you sure you want to pass?")) {
|
if (window.confirm("Are you sure you want to pass?")) {
|
||||||
const result = props.api.pass();
|
const result = props.api.pass();
|
||||||
|
@ -541,7 +536,6 @@ export function Game(props: {
|
||||||
.then(
|
.then(
|
||||||
(api_state) => {
|
(api_state) => {
|
||||||
setAPIState(api_state);
|
setAPIState(api_state);
|
||||||
waitForUpdate();
|
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error({error});
|
console.error({error});
|
||||||
|
|
|
@ -3,7 +3,7 @@ import {useState} from "react";
|
||||||
import {Settings} from "./utils";
|
import {Settings} from "./utils";
|
||||||
import {Game} from "./Game";
|
import {Game} from "./Game";
|
||||||
import {API, Difficulty} from "./api";
|
import {API, Difficulty} from "./api";
|
||||||
import {GameWasm} from "./wasm_api";
|
import {WasmAPI} from "./wasm_api";
|
||||||
import {AISelection} from "./UI";
|
import {AISelection} from "./UI";
|
||||||
|
|
||||||
export function Menu(props: {settings: Settings, dictionary_text: string}) {
|
export function Menu(props: {settings: Settings, dictionary_text: string}) {
|
||||||
|
@ -38,7 +38,7 @@ export function Menu(props: {settings: Settings, dictionary_text: string}) {
|
||||||
proportion: processedProportionDictionary,
|
proportion: processedProportionDictionary,
|
||||||
randomness: processedAIRandomness,
|
randomness: processedAIRandomness,
|
||||||
};
|
};
|
||||||
const game_wasm: API = new GameWasm(BigInt(seed), props.dictionary_text, difficulty);
|
const game_wasm: API = new WasmAPI(BigInt(seed), props.dictionary_text, difficulty);
|
||||||
const game = <Game settings={props.settings} api={game_wasm} key={seed} end_game_fn={() => setGame(null)}/>
|
const game = <Game settings={props.settings} api={game_wasm} key={seed} end_game_fn={() => setGame(null)}/>
|
||||||
setGame(game);
|
setGame(game);
|
||||||
}}>New Game</button>
|
}}>New Game</button>
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {ChangeEvent, JSX, useState} from "react";
|
import {ChangeEvent, JSX} from "react";
|
||||||
import {
|
import {
|
||||||
cellTypeToDetails,
|
cellTypeToDetails,
|
||||||
CoordinateData,
|
CoordinateData,
|
||||||
|
@ -12,9 +12,7 @@ import {
|
||||||
TileDispatch,
|
TileDispatch,
|
||||||
TileDispatchActionType,
|
TileDispatchActionType,
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
import {API, APIPlayer, CellType, Difficulty} from "./api";
|
import {APIPlayer, CellType} from "./api";
|
||||||
import {GameWasm} from "./wasm";
|
|
||||||
import {Game} from "./Game";
|
|
||||||
|
|
||||||
|
|
||||||
export function TileSlot(props: {
|
export function TileSlot(props: {
|
||||||
|
@ -182,7 +180,7 @@ export function Grid(props: {
|
||||||
const {className, text} = cellTypeToDetails(ct);
|
const {className, text} = cellTypeToDetails(ct);
|
||||||
|
|
||||||
let tileElement: JSX.Element;
|
let tileElement: JSX.Element;
|
||||||
if (props.boardLetters[i] !== undefined) {
|
if (props.boardLetters[i] != null) {
|
||||||
tileElement = <Letter data={props.boardLetters[i]} />;
|
tileElement = <Letter data={props.boardLetters[i]} />;
|
||||||
} else {
|
} else {
|
||||||
tileElement = <>
|
tileElement = <>
|
||||||
|
@ -239,7 +237,8 @@ export function Scores(props: {playerScores: Array<APIPlayer>}){
|
||||||
let elements = props.playerScores.map((ps) => {
|
let elements = props.playerScores.map((ps) => {
|
||||||
return <div key={ps.name}>
|
return <div key={ps.name}>
|
||||||
<h3>{ps.name}</h3>
|
<h3>{ps.name}</h3>
|
||||||
<span>{ps.score}</span>
|
<div>{ps.score}</div>
|
||||||
|
<div>({ps.tray_tiles} tiles remaining)</div>
|
||||||
</div>;
|
</div>;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -30,7 +30,7 @@ export interface Letter {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export type TurnAction = { type: "Pass" } | { type: "ExchangeTiles"; tiles_exchanged: number } | { type: "PlayTiles"; result: ScoreResult; locations: number[] };
|
export type TurnAction = { type: "Pass" } | { type: "ExchangeTiles"; tiles_exchanged: number } | { type: "PlayTiles"; result: ScoreResult; locations: number[] } | {type: "AddToDictionary"; word: string;};
|
||||||
|
|
||||||
export enum CellType {
|
export enum CellType {
|
||||||
Normal = "Normal",
|
Normal = "Normal",
|
||||||
|
@ -85,8 +85,7 @@ export interface API {
|
||||||
exchange: (selection: Array<boolean>) => Promise<APIState>;
|
exchange: (selection: Array<boolean>) => Promise<APIState>;
|
||||||
pass: () => Promise<APIState>;
|
pass: () => Promise<APIState>;
|
||||||
play: (tiles: Array<PlayedTile>, commit: boolean) => Promise<APIState>;
|
play: (tiles: Array<PlayedTile>, commit: boolean) => Promise<APIState>;
|
||||||
add_to_dictionary: (word: string) => Promise<void>;
|
add_to_dictionary: (word: string) => Promise<APIState>;
|
||||||
load: (wait: boolean) => Promise<APIState>;
|
load: (wait: boolean) => Promise<APIState>;
|
||||||
register_log_dispatch: (fn: (x: any) => void) => void;
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -2,6 +2,7 @@
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
|
<link rel="stylesheet" href="style.less" />
|
||||||
<title>Word Grid</title>
|
<title>Word Grid</title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {useState} from "react";
|
import {useRef, useState} from "react";
|
||||||
import {createRoot} from "react-dom/client";
|
import {createRoot} from "react-dom/client";
|
||||||
import {AISelection} from "./UI";
|
import {AISelection} from "./UI";
|
||||||
import {PartyInfo, ServerToClientMessage} from "./ws_api";
|
import {ClientToServerMessage, WSAPI, PartyInfo, ServerToClientMessage} from "./ws_api";
|
||||||
|
import {Game} from "./Game";
|
||||||
|
import {Settings} from "./utils";
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -25,8 +27,19 @@ export function Menu(): React.JSX.Element {
|
||||||
|
|
||||||
const [aiRandomness, setAIRandomness] = useState<number>(6);
|
const [aiRandomness, setAIRandomness] = useState<number>(6);
|
||||||
const [proportionDictionary, setProportionDictionary] = useState<number>(7);
|
const [proportionDictionary, setProportionDictionary] = useState<number>(7);
|
||||||
|
const [game, setGame] = useState<React.JSX.Element>(null);
|
||||||
|
|
||||||
|
|
||||||
|
let button_or_game = <button onClick={() => {
|
||||||
|
const event: ClientToServerMessage = {
|
||||||
|
type: "StartGame"
|
||||||
|
};
|
||||||
|
socket.send(JSON.stringify(event));
|
||||||
|
}}>Start Game</button>;
|
||||||
|
if(game){
|
||||||
|
button_or_game = game;
|
||||||
|
}
|
||||||
|
|
||||||
// Can change log scale to control shape of curve using following equation:
|
// Can change log scale to control shape of curve using following equation:
|
||||||
// aiRandomness = log(1 + x*(n-1))/log(n) when x, the user input, ranges between 0 and 1
|
// aiRandomness = log(1 + x*(n-1))/log(n) when x, the user input, ranges between 0 and 1
|
||||||
const processedAIRandomness = Math.log(1 + (LOGBASE - 1)*aiRandomness/100) / Math.log(LOGBASE);
|
const processedAIRandomness = Math.log(1 + (LOGBASE - 1)*aiRandomness/100) / Math.log(LOGBASE);
|
||||||
|
@ -53,6 +66,7 @@ export function Menu(): React.JSX.Element {
|
||||||
</li>
|
</li>
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
return <div>
|
return <div>
|
||||||
<p>Connected to {roomName}</p>
|
<p>Connected to {roomName}</p>
|
||||||
Players: <ol>
|
Players: <ol>
|
||||||
|
@ -82,7 +96,9 @@ export function Menu(): React.JSX.Element {
|
||||||
setSocket(null);
|
setSocket(null);
|
||||||
setPartyInfo(null);
|
setPartyInfo(null);
|
||||||
}}>Disconnect</button>
|
}}>Disconnect</button>
|
||||||
|
{button_or_game}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
return <div>
|
return <div>
|
||||||
<div>
|
<div>
|
||||||
|
@ -108,6 +124,16 @@ export function Menu(): React.JSX.Element {
|
||||||
const input: ServerToClientMessage = JSON.parse(event.data);
|
const input: ServerToClientMessage = JSON.parse(event.data);
|
||||||
if(input.type == "RoomChange"){
|
if(input.type == "RoomChange"){
|
||||||
setPartyInfo(input.info);
|
setPartyInfo(input.info);
|
||||||
|
} else if(input.type == "GameEvent" && game == null){
|
||||||
|
// start game
|
||||||
|
setGame(<Game api={new WSAPI(socket)} settings={{
|
||||||
|
playerName: playerName,
|
||||||
|
trayLength: 7,
|
||||||
|
}} end_game_fn={function(): void {
|
||||||
|
socket.close();
|
||||||
|
setSocket(null);
|
||||||
|
setGame(null);
|
||||||
|
} } />);
|
||||||
}
|
}
|
||||||
console.log("Message from server ", event.data);
|
console.log("Message from server ", event.data);
|
||||||
});
|
});
|
||||||
|
|
|
@ -50,8 +50,6 @@ async function run() {
|
||||||
root.render(<Menu dictionary_text={dictionary_text} settings={{
|
root.render(<Menu dictionary_text={dictionary_text} settings={{
|
||||||
trayLength: 7,
|
trayLength: 7,
|
||||||
playerName: 'Player',
|
playerName: 'Player',
|
||||||
aiName: 'AI',
|
|
||||||
|
|
||||||
}}/>);
|
}}/>);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -204,9 +204,7 @@
|
||||||
|
|
||||||
.scoring {
|
.scoring {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
display: grid;
|
display: flex;
|
||||||
grid-template-columns: 1fr 1fr;
|
|
||||||
grid-template-rows: none;
|
|
||||||
|
|
||||||
span {
|
span {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
|
@ -215,6 +213,7 @@
|
||||||
div {
|
div {
|
||||||
margin-left: 10px;
|
margin-left: 10px;
|
||||||
margin-right: 10px;
|
margin-right: 10px;
|
||||||
|
flex: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -5,7 +5,6 @@ import {CellType, Letter as LetterData} from "./api";
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
trayLength: number;
|
trayLength: number;
|
||||||
playerName: string;
|
playerName: string;
|
||||||
aiName: string;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum LocationType {
|
export enum LocationType {
|
||||||
|
|
|
@ -1,24 +1,22 @@
|
||||||
import {API, APIState, Difficulty, PlayedTile, Result, is_ok} from "./api";
|
import {API, APIState, Difficulty, PlayedTile, Result, is_ok} from "./api";
|
||||||
import {WasmAPI} from 'word_grid';
|
import {WasmAPI as RawAPI} from 'word_grid';
|
||||||
|
|
||||||
export class GameWasm implements API{
|
export class WasmAPI implements API{
|
||||||
private wasm: WasmAPI;
|
private wasm: RawAPI;
|
||||||
private log_dispatch: (x: any) => void | null;
|
|
||||||
|
|
||||||
constructor(seed: bigint, dictionary_text: string, difficulty: Difficulty) {
|
constructor(seed: bigint, dictionary_text: string, difficulty: Difficulty) {
|
||||||
this.wasm = new WasmAPI(seed, dictionary_text, difficulty);
|
this.wasm = new RawAPI(seed, dictionary_text, difficulty);
|
||||||
this.log_dispatch = null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
add_to_dictionary(word: string): Promise<void> {
|
add_to_dictionary(word: string): Promise<APIState> {
|
||||||
return new Promise((resolve, _) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.wasm.add_to_dictionary(word);
|
let api_state: Result<APIState, any> = this.wasm.add_to_dictionary(word);
|
||||||
if(this.log_dispatch != null) {
|
|
||||||
this.log_dispatch(<div>{word} was added to dictionary</div>);
|
if(is_ok(api_state)) {
|
||||||
|
resolve(api_state.Ok);
|
||||||
} else {
|
} else {
|
||||||
console.error("log_dispatch was unexpectedly null");
|
reject(api_state.Err);
|
||||||
}
|
}
|
||||||
resolve()
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -70,8 +68,4 @@ export class GameWasm implements API{
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
register_log_dispatch(fn: (x: any) => void): void {
|
|
||||||
this.log_dispatch = fn;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -2,7 +2,6 @@ import {API, APIState, Difficulty, PlayedTile} from "./api";
|
||||||
|
|
||||||
export interface Player {
|
export interface Player {
|
||||||
name: string
|
name: string
|
||||||
id: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AI {
|
export interface AI {
|
||||||
|
@ -30,10 +29,6 @@ export type GameEvent = {
|
||||||
type: "TurnAction"
|
type: "TurnAction"
|
||||||
state: APIState
|
state: APIState
|
||||||
committed: boolean
|
committed: boolean
|
||||||
} | {
|
|
||||||
type: "WordAdded"
|
|
||||||
word: string
|
|
||||||
player: Player
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type ServerToClientMessage = {
|
export type ServerToClientMessage = {
|
||||||
|
@ -50,9 +45,6 @@ export type ServerToClientMessage = {
|
||||||
} | {
|
} | {
|
||||||
type: "Invalid"
|
type: "Invalid"
|
||||||
reason: string
|
reason: string
|
||||||
} | {
|
|
||||||
type: "WordAdded"
|
|
||||||
word: string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type GameMove = {
|
type GameMove = {
|
||||||
|
@ -69,7 +61,7 @@ type GameMove = {
|
||||||
word: string
|
word: string
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClientToServerMessage = {
|
export type ClientToServerMessage = {
|
||||||
type: "Load" | "StartGame"
|
type: "Load" | "StartGame"
|
||||||
} | {
|
} | {
|
||||||
type: "GameMove"
|
type: "GameMove"
|
||||||
|
@ -87,14 +79,12 @@ interface PromiseInput {
|
||||||
reject: (error: any) => void
|
reject: (error: any) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export class GameWS implements API{
|
export class WSAPI implements API{
|
||||||
private socket: WebSocket;
|
private socket: WebSocket;
|
||||||
private currentPromiseInput: PromiseInput | null;
|
private currentPromiseInput: PromiseInput | null;
|
||||||
private log_dispatch: (x: any) => void | null;
|
|
||||||
|
|
||||||
constructor(socket: WebSocket) {
|
constructor(socket: WebSocket) {
|
||||||
this.socket = socket;
|
this.socket = socket;
|
||||||
this.log_dispatch = null;
|
|
||||||
this.currentPromiseInput = null;
|
this.currentPromiseInput = null;
|
||||||
this.socket.addEventListener("message", (event) => {
|
this.socket.addEventListener("message", (event) => {
|
||||||
let data: ServerToClientMessage = JSON.parse(event.data);
|
let data: ServerToClientMessage = JSON.parse(event.data);
|
||||||
|
@ -120,14 +110,6 @@ export class GameWS implements API{
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private log_new_word(word: string, player: string) {
|
|
||||||
if(this.log_dispatch != null) {
|
|
||||||
this.log_dispatch(<div>Player {player} added {word} to the dictionary</div>);
|
|
||||||
} else {
|
|
||||||
console.error("Unable to log new word ", word, " from player ", player);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private register_promise(resolve: (value: GameEvent) => void, reject: (value: any) => void) {
|
private register_promise(resolve: (value: GameEvent) => void, reject: (value: any) => void) {
|
||||||
if(this.currentPromiseInput != null) {
|
if(this.currentPromiseInput != null) {
|
||||||
console.error("We are setting a new promise before the current one has resolved")
|
console.error("We are setting a new promise before the current one has resolved")
|
||||||
|
@ -139,7 +121,7 @@ export class GameWS implements API{
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
add_to_dictionary(word: string): Promise<void> {
|
add_to_dictionary(word: string): Promise<APIState> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
this.register_promise(resolve, reject);
|
this.register_promise(resolve, reject);
|
||||||
let event: ClientToServerMessage = {
|
let event: ClientToServerMessage = {
|
||||||
|
@ -152,13 +134,7 @@ export class GameWS implements API{
|
||||||
this.socket.send(JSON.stringify(event));
|
this.socket.send(JSON.stringify(event));
|
||||||
|
|
||||||
}).then((game_event: GameEvent) => {
|
}).then((game_event: GameEvent) => {
|
||||||
if(game_event.type == "WordAdded"){
|
return game_event.state;
|
||||||
this.log_new_word(game_event.word, game_event.player.name);
|
|
||||||
} else {
|
|
||||||
console.error("We received the wrong kind of response back!")
|
|
||||||
console.error({game_event});
|
|
||||||
return Promise.reject("We received the wrong kind of response back!");
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -174,13 +150,7 @@ export class GameWS implements API{
|
||||||
};
|
};
|
||||||
this.socket.send(JSON.stringify(event));
|
this.socket.send(JSON.stringify(event));
|
||||||
}).then((game_event: GameEvent) => {
|
}).then((game_event: GameEvent) => {
|
||||||
if(game_event.type == "TurnAction") {
|
return game_event.state;
|
||||||
return game_event.state;
|
|
||||||
} else {
|
|
||||||
console.error("We received the wrong kind of response back!")
|
|
||||||
console.error({game_event});
|
|
||||||
return Promise.reject("We received the wrong kind of response back!");
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -194,13 +164,7 @@ export class GameWS implements API{
|
||||||
this.socket.send(JSON.stringify(event));
|
this.socket.send(JSON.stringify(event));
|
||||||
}
|
}
|
||||||
}).then((game_event: GameEvent) => {
|
}).then((game_event: GameEvent) => {
|
||||||
if(game_event.type == "TurnAction") {
|
return game_event.state;
|
||||||
return game_event.state;
|
|
||||||
} else {
|
|
||||||
// need to handle this case; we'll deal with it by returning a new promise again
|
|
||||||
this.log_new_word(game_event.word, game_event.player.name);
|
|
||||||
return this.load(wait);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -215,13 +179,7 @@ export class GameWS implements API{
|
||||||
};
|
};
|
||||||
this.socket.send(JSON.stringify(event));
|
this.socket.send(JSON.stringify(event));
|
||||||
}).then((game_event: GameEvent) => {
|
}).then((game_event: GameEvent) => {
|
||||||
if(game_event.type == "TurnAction") {
|
return game_event.state;
|
||||||
return game_event.state;
|
|
||||||
} else {
|
|
||||||
console.error("We received the wrong kind of response back!")
|
|
||||||
console.error({game_event});
|
|
||||||
return Promise.reject("We received the wrong kind of response back!");
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -238,18 +196,8 @@ export class GameWS implements API{
|
||||||
};
|
};
|
||||||
this.socket.send(JSON.stringify(event));
|
this.socket.send(JSON.stringify(event));
|
||||||
}).then((game_event: GameEvent) => {
|
}).then((game_event: GameEvent) => {
|
||||||
if(game_event.type == "TurnAction") {
|
return game_event.state;
|
||||||
return game_event.state;
|
|
||||||
} else {
|
|
||||||
console.error("We received the wrong kind of response back!")
|
|
||||||
console.error({game_event});
|
|
||||||
return Promise.reject("We received the wrong kind of response back!");
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
register_log_dispatch(fn: (x: any) => void): void {
|
|
||||||
this.log_dispatch = fn;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
|
@ -47,7 +47,7 @@ pub struct PublicInformation {
|
||||||
pub struct ApiState {
|
pub struct ApiState {
|
||||||
public_information: PublicInformation,
|
public_information: PublicInformation,
|
||||||
tray: Tray,
|
tray: Tray,
|
||||||
update: Option<Update>,
|
pub update: Option<Update>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct APIGame(pub Game, Vec<Update>);
|
pub struct APIGame(pub Game, Vec<Update>);
|
||||||
|
|
Loading…
Reference in a new issue