diff --git a/server/src/main.rs b/server/src/main.rs index 598e120..9b8b9e4 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -13,7 +13,7 @@ use std::collections::HashMap; use std::sync::{Arc, LazyLock, Weak}; use tokio::select; 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::game::{Error, Game, PlayedTile}; use word_grid::player_interaction::ai::Difficulty; @@ -73,7 +73,6 @@ enum RoomEvent { #[serde(tag = "type")] enum GameEvent { TurnAction { state: ApiState, committed: bool }, - WordAdded { word: String, player: Player }, } #[derive(Clone, Serialize, Debug)] @@ -81,7 +80,6 @@ enum GameEvent { enum ServerToClientMessage { RoomChange { event: RoomEvent, info: PartyInfo }, GameEvent { event: GameEvent }, - WordAdded { word: String }, GameError { error: Error }, Invalid { reason: String }, } @@ -115,7 +113,7 @@ enum ClientToServerMessage { #[derive(Clone, Debug)] enum InnerRoomMessage { PassThrough(ServerToClientMessage), - GameEvent, + GameEvent(Option), } type RoomMap = HashMap>>; @@ -134,6 +132,10 @@ async fn incoming_message_handler( Some(message) => { match message { Ok(message) => { + if let Message::Ping(_) = message { + println!("Received ping from player {player:#?}"); + return false; + } let message = message.to_text().unwrap(); if message.len() == 0 { println!("Websocket closed"); @@ -173,7 +175,7 @@ async fn incoming_message_handler( println!("Received {message:#?} from client {}", player.id); match message { ClientToServerMessage::Load => { - return !game_load(player, room, stream).await + return !game_load(player, None, room, stream).await } ClientToServerMessage::StartGame => { let mut room = room.write().await; @@ -200,7 +202,7 @@ async fn incoming_message_handler( let game = APIGame::new(game); room.game = Some(game); - sender.send(InnerRoomMessage::GameEvent).unwrap(); + sender.send(InnerRoomMessage::GameEvent(None)).unwrap(); } } ClientToServerMessage::GameMove { r#move } => { @@ -242,8 +244,10 @@ async fn incoming_message_handler( } }; match result { - Ok(_) => { - sender.send(InnerRoomMessage::GameEvent).unwrap(); + Ok(event) => { + sender + .send(InnerRoomMessage::GameEvent(event.update)) + .unwrap(); } Err(error) => { let event = ServerToClientMessage::GameError { error }; @@ -292,11 +296,17 @@ async fn incoming_message_handler( false } -async fn game_load(player: &Player, room: &Arc>, stream: &mut DuplexStream) -> bool { +async fn game_load( + player: &Player, + update: Option, + room: &Arc>, + stream: &mut DuplexStream, +) -> bool { // The game object was modified; we need to trigger a load from this player's perspective 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 { event: GameEvent::TurnAction { state, @@ -325,7 +335,7 @@ async fn outgoing_message_handler( let x = stream.send(text.into()).await; x.is_err() } - InnerRoomMessage::GameEvent => !game_load(player, room, stream).await, + InnerRoomMessage::GameEvent(update) => !game_load(player, update, room, stream).await, }; } diff --git a/ui/src/Game.tsx b/ui/src/Game.tsx index 424a412..5b5cc7d 100644 --- a/ui/src/Game.tsx +++ b/ui/src/Game.tsx @@ -32,7 +32,6 @@ export function Game(props: { }) { const [api_state, setAPIState] = useState(undefined); - const [isLoading, setIsLoading] = useState(true); const [confirmedScorePoints, setConfirmedScorePoints] = useState(-1); const currentTurnNumber = useRef(-1); const historyProcessedNumber = useRef(0); @@ -44,15 +43,11 @@ export function Game(props: { function waitForUpdate() { - // setAPIState(undefined); - // setIsLoading(true); - setIsLoading(true); const result = props.api.load(true); result.then( (state) => { setAPIState(state); - setIsLoading(false); } ) .catch((error) => { @@ -136,7 +131,6 @@ export function Game(props: { .then( (api_state) => { setAPIState(api_state); - waitForUpdate(); }) .catch((error) => { console.error({error}); @@ -149,14 +143,11 @@ export function Game(props: { const [logInfo, logDispatch] = useReducer(addLogInfo, []); useEffect(() => { - props.api.register_log_dispatch(logDispatch); props.api.load(false) .then((api_state) => { setAPIState(api_state); - setIsLoading(false); }); - }, []) - + }, []); function movePlayableLetters(playerLetters: PlayableLetterData[], update: TileDispatchAction) { if (update.action === TileDispatchActionType.RETRIEVE) { @@ -255,10 +246,12 @@ export function Game(props: { their turn.); } else if (action.type == "Pass") { logDispatch(
{playerName} passed.
); + } else if (action.type == "AddToDictionary") { + logDispatch(
{playerName} added {action.word} to the dictionary.
) + } else { + console.error("Received unknown turn action: ", action); } - // Clear any on-screen arrows - gridArrowDispatch({action: GridArrowDispatchActionType.CLEAR}); } function endGame(state: GameState) { @@ -363,18 +356,24 @@ export function Game(props: { useEffect(() => { if (api_state) { + console.log("In state: ", api_state.public_information.current_turn_number); + console.log("In ref: ", currentTurnNumber.current); console.debug(api_state); - gridArrowDispatch({action: GridArrowDispatchActionType.CLEAR}); - trayDispatch({action: TileDispatchActionType.RETRIEVE}); - setConfirmedScorePoints(-1); - updateBoardLetters(api_state.public_information.board); + if(currentTurnNumber.current < api_state.public_information.current_turn_number){ + // We only clear everything if there's a chance the board changed + // We may have gotten a dictionary update event which doesn't count + 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++) { const update = api_state.public_information.history[i]; if (update.turn_number > currentTurnNumber.current) { currentTurnNumber.current = update.turn_number; - logDispatch(

TTurn {update.turn_number}

); - const playerAtTurn = api_state.public_information.players[(update.turn_number-1) % api_state.public_information.players.length].name; + logDispatch(

Turn {update.turn_number + 1}

); + const playerAtTurn = api_state.public_information.players[(update.turn_number) % api_state.public_information.players.length].name; logDispatch(
{playerAtTurn}'s turn
); } @@ -384,9 +383,7 @@ export function Game(props: { historyProcessedNumber.current = api_state.public_information.history.length; if (!isGameOver) { - console.log("In state: ", api_state.public_information.current_turn_number); - console.log("In ref: ", currentTurnNumber.current); - if(api_state.public_information.current_turn_number >= currentTurnNumber.current){ + if(api_state.public_information.current_turn_number > currentTurnNumber.current){ logDispatch(

Turn {api_state.public_information.current_turn_number + 1}

); logDispatch(
{api_state.public_information.current_player}'s turn
); currentTurnNumber.current = api_state.public_information.current_turn_number; @@ -395,24 +392,19 @@ export function Game(props: { endGame(api_state.public_information.game_state); } - + if(api_state.public_information.current_player != props.settings.playerName) { + waitForUpdate(); + } } }, [api_state]); - - if (isLoading) { + if(api_state == null){ return
Still loading
; } const playerAndScores = api_state.public_information.players; const remainingTiles = api_state.public_information.remaining_tiles; - let remainingAITiles = null; - for (let player of playerAndScores) { - if (player.name == 'AI') { - remainingAITiles = player.tray_tiles; - break; - } - } + const isPlayersTurn = api_state.public_information.current_player == props.settings.playerName; return <> {remainingTiles} letters remaining -
- {props.settings.aiName} has {remainingAITiles} tiles -
diff --git a/ui/src/UI.tsx b/ui/src/UI.tsx index 8224f23..74b699b 100644 --- a/ui/src/UI.tsx +++ b/ui/src/UI.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import {ChangeEvent, JSX, useState} from "react"; +import {ChangeEvent, JSX} from "react"; import { cellTypeToDetails, CoordinateData, @@ -12,9 +12,7 @@ import { TileDispatch, TileDispatchActionType, } from "./utils"; -import {API, APIPlayer, CellType, Difficulty} from "./api"; -import {GameWasm} from "./wasm"; -import {Game} from "./Game"; +import {APIPlayer, CellType} from "./api"; export function TileSlot(props: { @@ -182,7 +180,7 @@ export function Grid(props: { const {className, text} = cellTypeToDetails(ct); let tileElement: JSX.Element; - if (props.boardLetters[i] !== undefined) { + if (props.boardLetters[i] != null) { tileElement = ; } else { tileElement = <> @@ -239,7 +237,8 @@ export function Scores(props: {playerScores: Array}){ let elements = props.playerScores.map((ps) => { return

{ps.name}

- {ps.score} +
{ps.score}
+
({ps.tray_tiles} tiles remaining)
; }); diff --git a/ui/src/api.ts b/ui/src/api.ts index 713c88b..7e85070 100644 --- a/ui/src/api.ts +++ b/ui/src/api.ts @@ -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 { Normal = "Normal", @@ -85,8 +85,7 @@ export interface API { exchange: (selection: Array) => Promise; pass: () => Promise; play: (tiles: Array, commit: boolean) => Promise; - add_to_dictionary: (word: string) => Promise; + add_to_dictionary: (word: string) => Promise; load: (wait: boolean) => Promise; - register_log_dispatch: (fn: (x: any) => void) => void; } \ No newline at end of file diff --git a/ui/src/multiplayer.html b/ui/src/multiplayer.html index 0356cc6..1c03960 100644 --- a/ui/src/multiplayer.html +++ b/ui/src/multiplayer.html @@ -2,6 +2,7 @@ + Word Grid diff --git a/ui/src/multiplayer.tsx b/ui/src/multiplayer.tsx index a750841..455a289 100644 --- a/ui/src/multiplayer.tsx +++ b/ui/src/multiplayer.tsx @@ -1,8 +1,10 @@ import * as React from "react"; -import {useState} from "react"; +import {useRef, useState} from "react"; import {createRoot} from "react-dom/client"; 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(6); const [proportionDictionary, setProportionDictionary] = useState(7); + const [game, setGame] = useState(null); + let button_or_game = ; + if(game){ + button_or_game = game; + } + // 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 const processedAIRandomness = Math.log(1 + (LOGBASE - 1)*aiRandomness/100) / Math.log(LOGBASE); @@ -53,6 +66,7 @@ export function Menu(): React.JSX.Element { }); + return

Connected to {roomName}

Players:
    @@ -82,7 +96,9 @@ export function Menu(): React.JSX.Element { setSocket(null); setPartyInfo(null); }}>Disconnect + {button_or_game}
+ } else { return
@@ -108,6 +124,16 @@ export function Menu(): React.JSX.Element { const input: ServerToClientMessage = JSON.parse(event.data); if(input.type == "RoomChange"){ setPartyInfo(input.info); + } else if(input.type == "GameEvent" && game == null){ + // start game + setGame(); } console.log("Message from server ", event.data); }); diff --git a/ui/src/singleplayer.tsx b/ui/src/singleplayer.tsx index 167de2e..6b82ccc 100644 --- a/ui/src/singleplayer.tsx +++ b/ui/src/singleplayer.tsx @@ -50,8 +50,6 @@ async function run() { root.render(); } diff --git a/ui/src/style.less b/ui/src/style.less index 663ec4e..03b6f46 100644 --- a/ui/src/style.less +++ b/ui/src/style.less @@ -204,9 +204,7 @@ .scoring { text-align: center; - display: grid; - grid-template-columns: 1fr 1fr; - grid-template-rows: none; + display: flex; span { font-size: 20px; @@ -215,6 +213,7 @@ div { margin-left: 10px; margin-right: 10px; + flex: 1; } } } diff --git a/ui/src/utils.ts b/ui/src/utils.ts index 7ad4c2c..11962ef 100644 --- a/ui/src/utils.ts +++ b/ui/src/utils.ts @@ -5,7 +5,6 @@ import {CellType, Letter as LetterData} from "./api"; export interface Settings { trayLength: number; playerName: string; - aiName: string; } export enum LocationType { diff --git a/ui/src/wasm_api.tsx b/ui/src/wasm_api.tsx index 9b04c4a..7ccbd52 100644 --- a/ui/src/wasm_api.tsx +++ b/ui/src/wasm_api.tsx @@ -1,24 +1,22 @@ 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{ - private wasm: WasmAPI; - private log_dispatch: (x: any) => void | null; +export class WasmAPI implements API{ + private wasm: RawAPI; constructor(seed: bigint, dictionary_text: string, difficulty: Difficulty) { - this.wasm = new WasmAPI(seed, dictionary_text, difficulty); - this.log_dispatch = null; + this.wasm = new RawAPI(seed, dictionary_text, difficulty); } - add_to_dictionary(word: string): Promise { - return new Promise((resolve, _) => { - this.wasm.add_to_dictionary(word); - if(this.log_dispatch != null) { - this.log_dispatch(
{word} was added to dictionary
); + add_to_dictionary(word: string): Promise { + return new Promise((resolve, reject) => { + let api_state: Result = this.wasm.add_to_dictionary(word); + + if(is_ok(api_state)) { + resolve(api_state.Ok); } 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; - } - } \ No newline at end of file diff --git a/ui/src/ws_api.tsx b/ui/src/ws_api.tsx index 87ddd19..d3721dd 100644 --- a/ui/src/ws_api.tsx +++ b/ui/src/ws_api.tsx @@ -2,7 +2,6 @@ import {API, APIState, Difficulty, PlayedTile} from "./api"; export interface Player { name: string - id: string } export interface AI { @@ -30,10 +29,6 @@ export type GameEvent = { type: "TurnAction" state: APIState committed: boolean -} | { - type: "WordAdded" - word: string - player: Player } export type ServerToClientMessage = { @@ -50,9 +45,6 @@ export type ServerToClientMessage = { } | { type: "Invalid" reason: string -} | { - type: "WordAdded" - word: string } type GameMove = { @@ -69,7 +61,7 @@ type GameMove = { word: string } -type ClientToServerMessage = { +export type ClientToServerMessage = { type: "Load" | "StartGame" } | { type: "GameMove" @@ -87,14 +79,12 @@ interface PromiseInput { reject: (error: any) => void } -export class GameWS implements API{ +export class WSAPI implements API{ private socket: WebSocket; private currentPromiseInput: PromiseInput | null; - private log_dispatch: (x: any) => void | null; constructor(socket: WebSocket) { this.socket = socket; - this.log_dispatch = null; this.currentPromiseInput = null; this.socket.addEventListener("message", (event) => { 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(
Player {player} added {word} to the dictionary
); - } else { - console.error("Unable to log new word ", word, " from player ", player); - } - } - private register_promise(resolve: (value: GameEvent) => void, reject: (value: any) => void) { if(this.currentPromiseInput != null) { 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 { + add_to_dictionary(word: string): Promise { return new Promise((resolve, reject) => { this.register_promise(resolve, reject); let event: ClientToServerMessage = { @@ -152,13 +134,7 @@ export class GameWS implements API{ this.socket.send(JSON.stringify(event)); }).then((game_event: GameEvent) => { - if(game_event.type == "WordAdded"){ - 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!"); - } + return game_event.state; }); } @@ -174,13 +150,7 @@ export class GameWS implements API{ }; this.socket.send(JSON.stringify(event)); }).then((game_event: GameEvent) => { - if(game_event.type == "TurnAction") { - 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!"); - } + return game_event.state; }); } @@ -194,13 +164,7 @@ export class GameWS implements API{ this.socket.send(JSON.stringify(event)); } }).then((game_event: GameEvent) => { - if(game_event.type == "TurnAction") { - 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); - } + return game_event.state; }); } @@ -215,13 +179,7 @@ export class GameWS implements API{ }; this.socket.send(JSON.stringify(event)); }).then((game_event: GameEvent) => { - if(game_event.type == "TurnAction") { - 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!"); - } + return game_event.state; }); } @@ -238,18 +196,8 @@ export class GameWS implements API{ }; this.socket.send(JSON.stringify(event)); }).then((game_event: GameEvent) => { - if(game_event.type == "TurnAction") { - 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!"); - } + return game_event.state; }); } - register_log_dispatch(fn: (x: any) => void): void { - this.log_dispatch = fn; - } - } \ No newline at end of file diff --git a/wordgrid/src/api.rs b/wordgrid/src/api.rs index c6ac54b..86f7ccd 100644 --- a/wordgrid/src/api.rs +++ b/wordgrid/src/api.rs @@ -47,7 +47,7 @@ pub struct PublicInformation { pub struct ApiState { public_information: PublicInformation, tray: Tray, - update: Option, + pub update: Option, } pub struct APIGame(pub Game, Vec);