Multiplayer #1

Merged
joel merged 19 commits from multiplayer into main 2024-12-26 18:38:24 +00:00
13 changed files with 115 additions and 148 deletions
Showing only changes of commit f0552fee32 - Show all commits

View file

@ -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,
}; };
} }

View file

@ -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});

View file

@ -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>

View file

@ -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>;
}); });

View file

@ -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;
} }

View file

@ -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>

View file

@ -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);
}); });

View file

@ -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',
}}/>); }}/>);
} }

View file

@ -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;
} }
} }
} }

View file

@ -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 {

View file

@ -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;
}
} }

View file

@ -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;
}
} }

View file

@ -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>);