Multiplayer #1
10 changed files with 402 additions and 65 deletions
|
@ -63,22 +63,31 @@ impl Room {
|
||||||
#[derive(Clone, Serialize, Debug)]
|
#[derive(Clone, Serialize, Debug)]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
enum RoomEvent {
|
enum RoomEvent {
|
||||||
PlayerJoined(Player),
|
PlayerJoined { player: Player },
|
||||||
PlayerLeft(Player),
|
PlayerLeft { player: Player },
|
||||||
AIJoined(Difficulty),
|
AIJoined { difficulty: Difficulty },
|
||||||
AILeft { index: usize },
|
AILeft { index: usize },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Serialize, Debug)]
|
||||||
|
#[serde(tag = "type")]
|
||||||
|
enum GameEvent {
|
||||||
|
TurnAction { state: ApiState, committed: bool },
|
||||||
|
WordAdded { word: String, player: Player },
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Clone, Serialize, Debug)]
|
#[derive(Clone, Serialize, Debug)]
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
enum ServerToClientMessage {
|
enum ServerToClientMessage {
|
||||||
RoomChange { event: RoomEvent, info: PartyInfo },
|
RoomChange { event: RoomEvent, info: PartyInfo },
|
||||||
GameEvent { state: ApiState, committed: bool },
|
GameEvent { event: GameEvent },
|
||||||
|
WordAdded { word: String },
|
||||||
GameError { error: Error },
|
GameError { error: Error },
|
||||||
Invalid { reason: String },
|
Invalid { reason: String },
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
|
#[serde(tag = "type")]
|
||||||
enum GameMove {
|
enum GameMove {
|
||||||
Pass,
|
Pass,
|
||||||
Exchange {
|
Exchange {
|
||||||
|
@ -88,6 +97,9 @@ enum GameMove {
|
||||||
played_tiles: Vec<Option<PlayedTile>>,
|
played_tiles: Vec<Option<PlayedTile>>,
|
||||||
commit_move: bool,
|
commit_move: bool,
|
||||||
},
|
},
|
||||||
|
AddToDictionary {
|
||||||
|
word: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
|
@ -142,7 +154,9 @@ async fn incoming_message_handler<E: std::fmt::Display>(
|
||||||
room.party_info.players = new_vec;
|
room.party_info.players = new_vec;
|
||||||
|
|
||||||
let event = ServerToClientMessage::RoomChange {
|
let event = ServerToClientMessage::RoomChange {
|
||||||
event: RoomEvent::PlayerLeft(player.clone()),
|
event: RoomEvent::PlayerLeft {
|
||||||
|
player: player.clone(),
|
||||||
|
},
|
||||||
info: room.party_info.clone(),
|
info: room.party_info.clone(),
|
||||||
};
|
};
|
||||||
sender.send(InnerRoomMessage::PassThrough(event)).unwrap();
|
sender.send(InnerRoomMessage::PassThrough(event)).unwrap();
|
||||||
|
@ -208,15 +222,24 @@ async fn incoming_message_handler<E: std::fmt::Display>(
|
||||||
played_tiles,
|
played_tiles,
|
||||||
commit_move,
|
commit_move,
|
||||||
} => {
|
} => {
|
||||||
let result = game.play(&player.name, played_tiles, commit_move);
|
let result =
|
||||||
|
game.play(&player.name, played_tiles, commit_move);
|
||||||
if result.is_ok() & !commit_move {
|
if result.is_ok() & !commit_move {
|
||||||
let event = ServerToClientMessage::GameEvent {state: result.unwrap(), committed: false};
|
let event = ServerToClientMessage::GameEvent {
|
||||||
|
event: GameEvent::TurnAction {
|
||||||
|
state: result.unwrap(),
|
||||||
|
committed: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
let event = serde_json::to_string(&event).unwrap();
|
let event = serde_json::to_string(&event).unwrap();
|
||||||
let _ = stream.send(event.into()).await;
|
let _ = stream.send(event.into()).await;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
result
|
result
|
||||||
},
|
}
|
||||||
|
GameMove::AddToDictionary { word } => {
|
||||||
|
game.add_to_dictionary(&player.name, &word)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
match result {
|
match result {
|
||||||
Ok(_) => {
|
Ok(_) => {
|
||||||
|
@ -235,7 +258,7 @@ async fn incoming_message_handler<E: std::fmt::Display>(
|
||||||
room.party_info.ais.push(difficulty.clone());
|
room.party_info.ais.push(difficulty.clone());
|
||||||
|
|
||||||
let event = ServerToClientMessage::RoomChange {
|
let event = ServerToClientMessage::RoomChange {
|
||||||
event: RoomEvent::AIJoined(difficulty),
|
event: RoomEvent::AIJoined { difficulty },
|
||||||
info: room.party_info.clone(),
|
info: room.party_info.clone(),
|
||||||
};
|
};
|
||||||
sender.send(InnerRoomMessage::PassThrough(event)).unwrap();
|
sender.send(InnerRoomMessage::PassThrough(event)).unwrap();
|
||||||
|
@ -274,7 +297,12 @@ async fn game_load(player: &Player, room: &Arc<RwLock<Room>>, stream: &mut Duple
|
||||||
let mut room = room.write().await;
|
let mut room = room.write().await;
|
||||||
|
|
||||||
let state = room.game.as_mut().unwrap().load(&player.name).unwrap();
|
let state = room.game.as_mut().unwrap().load(&player.name).unwrap();
|
||||||
let event = ServerToClientMessage::GameEvent { state, committed: true };
|
let event = ServerToClientMessage::GameEvent {
|
||||||
|
event: GameEvent::TurnAction {
|
||||||
|
state,
|
||||||
|
committed: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
let text = serde_json::to_string(&event).unwrap();
|
let text = serde_json::to_string(&event).unwrap();
|
||||||
let x = stream.send(text.into()).await;
|
let x = stream.send(text.into()).await;
|
||||||
|
@ -319,7 +347,9 @@ async fn chat(
|
||||||
|
|
||||||
fn make_join_event(room: &Room, player: &Player) -> ServerToClientMessage {
|
fn make_join_event(room: &Room, player: &Player) -> ServerToClientMessage {
|
||||||
ServerToClientMessage::RoomChange {
|
ServerToClientMessage::RoomChange {
|
||||||
event: RoomEvent::PlayerJoined(player.clone()),
|
event: RoomEvent::PlayerJoined {
|
||||||
|
player: player.clone(),
|
||||||
|
},
|
||||||
info: room.party_info.clone(),
|
info: room.party_info.clone(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,8 @@ 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 [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
const [confirmedScorePoints, setConfirmedScorePoints] = useState<number>(-1);
|
const [confirmedScorePoints, setConfirmedScorePoints] = useState<number>(-1);
|
||||||
const [currentTurnNumber, setCurrentTurnNumber] = useState<number>(0);
|
const currentTurnNumber = useRef<number>(-1);
|
||||||
|
const historyProcessedNumber = useRef<number>(0);
|
||||||
|
|
||||||
let isGameOver = false;
|
let isGameOver = false;
|
||||||
if (api_state !== undefined) {
|
if (api_state !== undefined) {
|
||||||
|
@ -42,10 +43,11 @@ export function Game(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function load() {
|
function waitForUpdate() {
|
||||||
// setAPIState(undefined);
|
// setAPIState(undefined);
|
||||||
// setIsLoading(true);
|
// setIsLoading(true);
|
||||||
const result = props.api.load();
|
setIsLoading(true);
|
||||||
|
const result = props.api.load(true);
|
||||||
|
|
||||||
result.then(
|
result.then(
|
||||||
(state) => {
|
(state) => {
|
||||||
|
@ -54,15 +56,11 @@ export function Game(props: {
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.log("load() failed")
|
console.log("waitForUpdate() failed")
|
||||||
console.log(error);
|
console.log(error);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
load();
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const [boardLetters, setBoardLetters] = useState<HighlightableLetterData[]>(() => {
|
const [boardLetters, setBoardLetters] = useState<HighlightableLetterData[]>(() => {
|
||||||
const newLetterData = [] as HighlightableLetterData[];
|
const newLetterData = [] as HighlightableLetterData[];
|
||||||
for (let i = 0; i < GRID_LENGTH * GRID_LENGTH; i++) {
|
for (let i = 0; i < GRID_LENGTH * GRID_LENGTH; i++) {
|
||||||
|
@ -136,8 +134,9 @@ export function Game(props: {
|
||||||
|
|
||||||
result
|
result
|
||||||
.then(
|
.then(
|
||||||
(_api_state) => {
|
(api_state) => {
|
||||||
load();
|
setAPIState(api_state);
|
||||||
|
waitForUpdate();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error({error});
|
console.error({error});
|
||||||
|
@ -149,6 +148,15 @@ export function Game(props: {
|
||||||
const [gridArrow, gridArrowDispatch] = useReducer(adjustGridArrow, null);
|
const [gridArrow, gridArrowDispatch] = useReducer(adjustGridArrow, null);
|
||||||
const [logInfo, logDispatch] = useReducer(addLogInfo, []);
|
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) {
|
function movePlayableLetters(playerLetters: PlayableLetterData[], update: TileDispatchAction) {
|
||||||
if (update.action === TileDispatchActionType.RETRIEVE) {
|
if (update.action === TileDispatchActionType.RETRIEVE) {
|
||||||
|
@ -361,22 +369,28 @@ export function Game(props: {
|
||||||
setConfirmedScorePoints(-1);
|
setConfirmedScorePoints(-1);
|
||||||
updateBoardLetters(api_state.public_information.board);
|
updateBoardLetters(api_state.public_information.board);
|
||||||
|
|
||||||
for (let i = currentTurnNumber; i < api_state.public_information.history.length; i++) {
|
for (let i = historyProcessedNumber.current; i < api_state.public_information.history.length; i++) {
|
||||||
if (i > currentTurnNumber) {
|
|
||||||
logDispatch(<h4>Turn {i + 1}</h4>);
|
|
||||||
const playerAtTurn = api_state.public_information.players[i % api_state.public_information.players.length].name;
|
|
||||||
logDispatch(<div>{playerAtTurn}'s turn</div>);
|
|
||||||
}
|
|
||||||
const update = api_state.public_information.history[i];
|
const update = api_state.public_information.history[i];
|
||||||
|
if (update.turn_number > currentTurnNumber.current) {
|
||||||
|
currentTurnNumber.current = update.turn_number;
|
||||||
|
logDispatch(<h4>TTurn {update.turn_number}</h4>);
|
||||||
|
const playerAtTurn = api_state.public_information.players[(update.turn_number-1) % api_state.public_information.players.length].name;
|
||||||
|
logDispatch(<div>{playerAtTurn}'s turn</div>);
|
||||||
|
|
||||||
|
}
|
||||||
handlePlayerAction(update.type, update.player);
|
handlePlayerAction(update.type, update.player);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
historyProcessedNumber.current = api_state.public_information.history.length;
|
||||||
setCurrentTurnNumber(api_state.public_information.history.length);
|
|
||||||
|
|
||||||
if (!isGameOver) {
|
if (!isGameOver) {
|
||||||
logDispatch(<h4>Turn {api_state.public_information.history.length + 1}</h4>);
|
console.log("In state: ", api_state.public_information.current_turn_number);
|
||||||
logDispatch(<div>{api_state.public_information.current_player}'s turn</div>);
|
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(<div>{api_state.public_information.current_player}'s turn</div>);
|
||||||
|
currentTurnNumber.current = api_state.public_information.current_turn_number;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
endGame(api_state.public_information.game_state);
|
endGame(api_state.public_information.game_state);
|
||||||
}
|
}
|
||||||
|
@ -484,7 +498,8 @@ export function Game(props: {
|
||||||
setConfirmedScorePoints(play_tiles.result.total);
|
setConfirmedScorePoints(play_tiles.result.total);
|
||||||
|
|
||||||
if (committing) {
|
if (committing) {
|
||||||
load();
|
setAPIState(api_state);
|
||||||
|
waitForUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
@ -524,8 +539,9 @@ export function Game(props: {
|
||||||
|
|
||||||
result
|
result
|
||||||
.then(
|
.then(
|
||||||
(_api_state) => {
|
(api_state) => {
|
||||||
load();
|
setAPIState(api_state);
|
||||||
|
waitForUpdate();
|
||||||
})
|
})
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
console.error({error});
|
console.error({error});
|
||||||
|
@ -560,7 +576,7 @@ function AddWordButton(props: { word: string, addWordFn: (x: string) => void })
|
||||||
</div>;
|
</div>;
|
||||||
} else {
|
} else {
|
||||||
return <div>
|
return <div>
|
||||||
<em>{props.word} was added to dictionary.</em>
|
<em>Adding {props.word} to dictionary.</em>
|
||||||
</div>;
|
</div>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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";
|
import {GameWasm} 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}) {
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
|
|
||||||
export interface Tray {
|
export interface Tray {
|
||||||
letters: (Letter | undefined)[];
|
letters: (Letter | undefined)[];
|
||||||
}
|
}
|
||||||
|
@ -58,11 +57,13 @@ export interface PublicInformation {
|
||||||
players: Array<APIPlayer>;
|
players: Array<APIPlayer>;
|
||||||
remaining_tiles: number;
|
remaining_tiles: number;
|
||||||
history: Array<Update>;
|
history: Array<Update>;
|
||||||
|
current_turn_number: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface Update {
|
export interface Update {
|
||||||
type: TurnAction,
|
type: TurnAction,
|
||||||
player: string;
|
player: string;
|
||||||
|
turn_number: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface APIState {
|
export interface APIState {
|
||||||
|
@ -85,6 +86,7 @@ export interface API {
|
||||||
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<void>;
|
||||||
load: () => Promise<APIState>;
|
load: (wait: boolean) => Promise<APIState>;
|
||||||
|
register_log_dispatch: (fn: (x: any) => void) => void;
|
||||||
|
|
||||||
}
|
}
|
|
@ -2,21 +2,9 @@ import * as React from "react";
|
||||||
import {useState} from "react";
|
import {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";
|
||||||
|
|
||||||
interface Player {
|
|
||||||
name: string
|
|
||||||
id: string
|
|
||||||
}
|
|
||||||
|
|
||||||
interface AI {
|
|
||||||
proportion: number
|
|
||||||
randomness: number
|
|
||||||
}
|
|
||||||
|
|
||||||
interface PartyInfo {
|
|
||||||
ais: AI[]
|
|
||||||
players: Player[]
|
|
||||||
}
|
|
||||||
|
|
||||||
const LOGBASE = 10000;
|
const LOGBASE = 10000;
|
||||||
function unprocessAIRandomness(processedAIRandomness: number): string {
|
function unprocessAIRandomness(processedAIRandomness: number): string {
|
||||||
|
@ -117,8 +105,10 @@ export function Menu(): React.JSX.Element {
|
||||||
<button onClick={(e) => {
|
<button onClick={(e) => {
|
||||||
let socket = new WebSocket(`ws://localhost:8000/room/${roomName}?player_name=${playerName}`)
|
let socket = new WebSocket(`ws://localhost:8000/room/${roomName}?player_name=${playerName}`)
|
||||||
socket.addEventListener("message", (event) => {
|
socket.addEventListener("message", (event) => {
|
||||||
const input: { info: PartyInfo } = JSON.parse(event.data);
|
const input: ServerToClientMessage = JSON.parse(event.data);
|
||||||
setPartyInfo(input.info);
|
if(input.type == "RoomChange"){
|
||||||
|
setPartyInfo(input.info);
|
||||||
|
}
|
||||||
console.log("Message from server ", event.data);
|
console.log("Message from server ", event.data);
|
||||||
});
|
});
|
||||||
setSocket(socket);
|
setSocket(socket);
|
||||||
|
|
|
@ -2,15 +2,22 @@ import {API, APIState, Difficulty, PlayedTile, Result, is_ok} from "./api";
|
||||||
import {WasmAPI} from 'word_grid';
|
import {WasmAPI} from 'word_grid';
|
||||||
|
|
||||||
export class GameWasm implements API{
|
export class GameWasm implements API{
|
||||||
wasm: WasmAPI;
|
private wasm: WasmAPI;
|
||||||
|
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 WasmAPI(seed, dictionary_text, difficulty);
|
||||||
|
this.log_dispatch = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
add_to_dictionary(word: string): Promise<void> {
|
add_to_dictionary(word: string): Promise<void> {
|
||||||
return new Promise((resolve, _) => {
|
return new Promise((resolve, _) => {
|
||||||
this.wasm.add_to_dictionary(word);
|
this.wasm.add_to_dictionary(word);
|
||||||
|
if(this.log_dispatch != null) {
|
||||||
|
this.log_dispatch(<div>{word} was added to dictionary</div>);
|
||||||
|
} else {
|
||||||
|
console.error("log_dispatch was unexpectedly null");
|
||||||
|
}
|
||||||
resolve()
|
resolve()
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -27,7 +34,7 @@ export class GameWasm implements API{
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
load(): Promise<APIState> {
|
load(_wait: boolean): Promise<APIState> {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let api_state: Result<APIState, any> = this.wasm.load();
|
let api_state: Result<APIState, any> = this.wasm.load();
|
||||||
|
|
||||||
|
@ -63,4 +70,8 @@ export class GameWasm implements API{
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
register_log_dispatch(fn: (x: any) => void): void {
|
||||||
|
this.log_dispatch = fn;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
255
ui/src/ws_api.tsx
Normal file
255
ui/src/ws_api.tsx
Normal file
|
@ -0,0 +1,255 @@
|
||||||
|
import {API, APIState, Difficulty, PlayedTile} from "./api";
|
||||||
|
|
||||||
|
export interface Player {
|
||||||
|
name: string
|
||||||
|
id: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AI {
|
||||||
|
proportion: number
|
||||||
|
randomness: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PartyInfo {
|
||||||
|
ais: AI[]
|
||||||
|
players: Player[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RoomEvent = {
|
||||||
|
type: "PlayerJoined" | "PlayerLeft"
|
||||||
|
player: Player
|
||||||
|
} | {
|
||||||
|
type: "AIJoined",
|
||||||
|
difficulty: Difficulty
|
||||||
|
} | {
|
||||||
|
type: "AILeft"
|
||||||
|
index: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export type GameEvent = {
|
||||||
|
type: "TurnAction"
|
||||||
|
state: APIState
|
||||||
|
committed: boolean
|
||||||
|
} | {
|
||||||
|
type: "WordAdded"
|
||||||
|
word: string
|
||||||
|
player: Player
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ServerToClientMessage = {
|
||||||
|
type: "RoomChange"
|
||||||
|
event: RoomEvent
|
||||||
|
info: PartyInfo
|
||||||
|
|
||||||
|
} | {
|
||||||
|
type: "GameEvent"
|
||||||
|
event: GameEvent
|
||||||
|
} | {
|
||||||
|
type: "GameError"
|
||||||
|
error: any
|
||||||
|
} | {
|
||||||
|
type: "Invalid"
|
||||||
|
reason: string
|
||||||
|
} | {
|
||||||
|
type: "WordAdded"
|
||||||
|
word: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type GameMove = {
|
||||||
|
type: "Pass"
|
||||||
|
} | {
|
||||||
|
type: "Exchange"
|
||||||
|
tiles: Array<boolean>
|
||||||
|
} | {
|
||||||
|
type: "Play"
|
||||||
|
played_tiles: Array<PlayedTile>
|
||||||
|
commit_move: boolean
|
||||||
|
} | {
|
||||||
|
type: "AddToDictionary"
|
||||||
|
word: string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClientToServerMessage = {
|
||||||
|
type: "Load" | "StartGame"
|
||||||
|
} | {
|
||||||
|
type: "GameMove"
|
||||||
|
move: GameMove
|
||||||
|
} | {
|
||||||
|
type: "AddAI"
|
||||||
|
difficulty: Difficulty
|
||||||
|
} | {
|
||||||
|
type: "RemoveAI"
|
||||||
|
index: number
|
||||||
|
}
|
||||||
|
|
||||||
|
interface PromiseInput {
|
||||||
|
resolve: (value: GameEvent) => void
|
||||||
|
reject: (error: any) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export class GameWS 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);
|
||||||
|
console.log("Message from server ", event.data);
|
||||||
|
if(data.type == "GameEvent") {
|
||||||
|
if(this.currentPromiseInput != null) {
|
||||||
|
this.currentPromiseInput.resolve(data.event);
|
||||||
|
this.currentPromiseInput = null;
|
||||||
|
} else {
|
||||||
|
console.error("Received game data but no promise is queued");
|
||||||
|
console.error({data});
|
||||||
|
}
|
||||||
|
} else if(data.type == "GameError") {
|
||||||
|
if(this.currentPromiseInput != null) {
|
||||||
|
this.currentPromiseInput.reject(data.error);
|
||||||
|
this.currentPromiseInput = null;
|
||||||
|
} else {
|
||||||
|
console.error("Received error game data but no promise is queued");
|
||||||
|
console.error({data});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
if(this.currentPromiseInput != null) {
|
||||||
|
console.error("We are setting a new promise before the current one has resolved")
|
||||||
|
this.currentPromiseInput.reject("New promise was registered");
|
||||||
|
}
|
||||||
|
this.currentPromiseInput = {
|
||||||
|
resolve: resolve,
|
||||||
|
reject: reject
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
add_to_dictionary(word: string): Promise<void> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.register_promise(resolve, reject);
|
||||||
|
let event: ClientToServerMessage = {
|
||||||
|
type: "GameMove",
|
||||||
|
move: {
|
||||||
|
type: "AddToDictionary",
|
||||||
|
word
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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!");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
exchange(selection: Array<boolean>): Promise<APIState> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.register_promise(resolve, reject);
|
||||||
|
let event: ClientToServerMessage = {
|
||||||
|
type: "GameMove",
|
||||||
|
move: {
|
||||||
|
type: "Exchange",
|
||||||
|
tiles: selection
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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!");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
load(wait: boolean): Promise<APIState> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.register_promise(resolve, reject);
|
||||||
|
if(!wait) {
|
||||||
|
let event: ClientToServerMessage = {
|
||||||
|
type: "Load"
|
||||||
|
}
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
pass(): Promise<APIState> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.register_promise(resolve, reject);
|
||||||
|
let event: ClientToServerMessage = {
|
||||||
|
type: "GameMove",
|
||||||
|
move: {
|
||||||
|
type: "Pass",
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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!");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
play(tiles: Array<PlayedTile>, commit: boolean): Promise<APIState> {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
this.register_promise(resolve, reject);
|
||||||
|
let event: ClientToServerMessage = {
|
||||||
|
type: "GameMove",
|
||||||
|
move: {
|
||||||
|
type: "Play",
|
||||||
|
played_tiles: tiles,
|
||||||
|
commit_move: commit,
|
||||||
|
}
|
||||||
|
};
|
||||||
|
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!");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
register_log_dispatch(fn: (x: any) => void): void {
|
||||||
|
this.log_dispatch = fn;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -4,6 +4,8 @@ use word_grid::api::APIGame;
|
||||||
use word_grid::game::{Game, PlayedTile};
|
use word_grid::game::{Game, PlayedTile};
|
||||||
use word_grid::player_interaction::ai::Difficulty;
|
use word_grid::player_interaction::ai::Difficulty;
|
||||||
|
|
||||||
|
const PLAYER_NAME: &str = "Player";
|
||||||
|
|
||||||
#[wasm_bindgen]
|
#[wasm_bindgen]
|
||||||
pub struct WasmAPI(APIGame);
|
pub struct WasmAPI(APIGame);
|
||||||
|
|
||||||
|
@ -16,7 +18,7 @@ impl WasmAPI {
|
||||||
let game = Game::new(
|
let game = Game::new(
|
||||||
seed,
|
seed,
|
||||||
dictionary_text,
|
dictionary_text,
|
||||||
vec!["Player".to_string()],
|
vec![PLAYER_NAME.to_string()],
|
||||||
vec![difficulty],
|
vec![difficulty],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -26,19 +28,19 @@ impl WasmAPI {
|
||||||
pub fn exchange(&mut self, selection: JsValue) -> JsValue {
|
pub fn exchange(&mut self, selection: JsValue) -> JsValue {
|
||||||
let selection: Vec<bool> = serde_wasm_bindgen::from_value(selection).unwrap();
|
let selection: Vec<bool> = serde_wasm_bindgen::from_value(selection).unwrap();
|
||||||
|
|
||||||
let result = self.0.exchange("Player", selection);
|
let result = self.0.exchange(PLAYER_NAME, selection);
|
||||||
|
|
||||||
serde_wasm_bindgen::to_value(&result).unwrap()
|
serde_wasm_bindgen::to_value(&result).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pass(&mut self) -> JsValue {
|
pub fn pass(&mut self) -> JsValue {
|
||||||
let result = self.0.pass("Player");
|
let result = self.0.pass(PLAYER_NAME);
|
||||||
|
|
||||||
serde_wasm_bindgen::to_value(&result).unwrap()
|
serde_wasm_bindgen::to_value(&result).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn load(&mut self) -> JsValue {
|
pub fn load(&mut self) -> JsValue {
|
||||||
let result = self.0.load("Player");
|
let result = self.0.load(PLAYER_NAME);
|
||||||
serde_wasm_bindgen::to_value(&result).unwrap()
|
serde_wasm_bindgen::to_value(&result).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -46,12 +48,12 @@ impl WasmAPI {
|
||||||
let tray_tile_locations: Vec<Option<PlayedTile>> =
|
let tray_tile_locations: Vec<Option<PlayedTile>> =
|
||||||
serde_wasm_bindgen::from_value(tray_tile_locations).unwrap();
|
serde_wasm_bindgen::from_value(tray_tile_locations).unwrap();
|
||||||
|
|
||||||
let result = self.0.play("Player", tray_tile_locations, commit_move);
|
let result = self.0.play(PLAYER_NAME, tray_tile_locations, commit_move);
|
||||||
serde_wasm_bindgen::to_value(&result).unwrap()
|
serde_wasm_bindgen::to_value(&result).unwrap()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_to_dictionary(&mut self, word: String) -> JsValue {
|
pub fn add_to_dictionary(&mut self, word: &str) -> JsValue {
|
||||||
let result = self.0.add_to_dictionary(word);
|
let result = self.0.add_to_dictionary(PLAYER_NAME, word);
|
||||||
|
|
||||||
serde_wasm_bindgen::to_value(&result).unwrap()
|
serde_wasm_bindgen::to_value(&result).unwrap()
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,6 +26,7 @@ impl ApiPlayer {
|
||||||
pub struct Update {
|
pub struct Update {
|
||||||
r#type: TurnAction,
|
r#type: TurnAction,
|
||||||
player: String,
|
player: String,
|
||||||
|
turn_number: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub type ApiBoard = Vec<Option<Letter>>;
|
pub type ApiBoard = Vec<Option<Letter>>;
|
||||||
|
@ -39,6 +40,7 @@ pub struct PublicInformation {
|
||||||
players: Vec<ApiPlayer>,
|
players: Vec<ApiPlayer>,
|
||||||
remaining_tiles: usize,
|
remaining_tiles: usize,
|
||||||
history: Vec<Update>,
|
history: Vec<Update>,
|
||||||
|
current_turn_number: usize,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
|
@ -125,16 +127,20 @@ impl APIGame {
|
||||||
players,
|
players,
|
||||||
remaining_tiles: self.0.get_remaining_tiles(),
|
remaining_tiles: self.0.get_remaining_tiles(),
|
||||||
history: history.clone(),
|
history: history.clone(),
|
||||||
|
current_turn_number: self.0.get_number_turns(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn exchange(&mut self, player: &str, tray_tiles: Vec<bool>) -> Result<ApiState, Error> {
|
pub fn exchange(&mut self, player: &str, tray_tiles: Vec<bool>) -> Result<ApiState, Error> {
|
||||||
self.player_exists_and_turn(player)?;
|
self.player_exists_and_turn(player)?;
|
||||||
|
|
||||||
|
let turn_number = self.0.get_number_turns();
|
||||||
|
|
||||||
let (tray, turn_action, game_state) = self.0.exchange_tiles(tray_tiles)?;
|
let (tray, turn_action, game_state) = self.0.exchange_tiles(tray_tiles)?;
|
||||||
let update = Update {
|
let update = Update {
|
||||||
r#type: turn_action,
|
r#type: turn_action,
|
||||||
player: player.to_string(),
|
player: player.to_string(),
|
||||||
|
turn_number,
|
||||||
};
|
};
|
||||||
|
|
||||||
self.1.push(update.clone());
|
self.1.push(update.clone());
|
||||||
|
@ -145,11 +151,14 @@ impl APIGame {
|
||||||
pub fn pass(&mut self, player: &str) -> Result<ApiState, Error> {
|
pub fn pass(&mut self, player: &str) -> Result<ApiState, Error> {
|
||||||
self.player_exists_and_turn(player)?;
|
self.player_exists_and_turn(player)?;
|
||||||
|
|
||||||
|
let turn_number = self.0.get_number_turns();
|
||||||
|
|
||||||
let game_state = self.0.pass()?;
|
let game_state = self.0.pass()?;
|
||||||
let tray = self.0.player_states.get_tray(player).unwrap().clone();
|
let tray = self.0.player_states.get_tray(player).unwrap().clone();
|
||||||
let update = Update {
|
let update = Update {
|
||||||
r#type: TurnAction::Pass,
|
r#type: TurnAction::Pass,
|
||||||
player: player.to_string(),
|
player: player.to_string(),
|
||||||
|
turn_number,
|
||||||
};
|
};
|
||||||
self.1.push(update.clone());
|
self.1.push(update.clone());
|
||||||
|
|
||||||
|
@ -161,12 +170,14 @@ impl APIGame {
|
||||||
Err(Error::InvalidPlayer(player.to_string()))
|
Err(Error::InvalidPlayer(player.to_string()))
|
||||||
} else {
|
} else {
|
||||||
while self.is_ai_turn() {
|
while self.is_ai_turn() {
|
||||||
|
let turn_number = self.0.get_number_turns();
|
||||||
let (result, _) = self.0.advance_turn()?;
|
let (result, _) = self.0.advance_turn()?;
|
||||||
|
|
||||||
if let TurnAdvanceResult::AIMove { name, action } = result {
|
if let TurnAdvanceResult::AIMove { name, action } = result {
|
||||||
self.1.push(Update {
|
self.1.push(Update {
|
||||||
r#type: action,
|
r#type: action,
|
||||||
player: name,
|
player: name,
|
||||||
|
turn_number,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
unreachable!("We already checked that the current player is AI");
|
unreachable!("We already checked that the current player is AI");
|
||||||
|
@ -186,11 +197,14 @@ impl APIGame {
|
||||||
) -> Result<ApiState, Error> {
|
) -> Result<ApiState, Error> {
|
||||||
self.player_exists_and_turn(&player)?;
|
self.player_exists_and_turn(&player)?;
|
||||||
|
|
||||||
|
let turn_number = self.0.get_number_turns();
|
||||||
|
|
||||||
let (turn_action, game_state) = self.0.receive_play(tray_tile_locations, commit_move)?;
|
let (turn_action, game_state) = self.0.receive_play(tray_tile_locations, commit_move)?;
|
||||||
let tray = self.0.player_states.get_tray(&player).unwrap().clone();
|
let tray = self.0.player_states.get_tray(&player).unwrap().clone();
|
||||||
let update = Update {
|
let update = Update {
|
||||||
r#type: turn_action,
|
r#type: turn_action,
|
||||||
player: player.to_string(),
|
player: player.to_string(),
|
||||||
|
turn_number,
|
||||||
};
|
};
|
||||||
if commit_move {
|
if commit_move {
|
||||||
self.1.push(update.clone())
|
self.1.push(update.clone())
|
||||||
|
@ -199,8 +213,18 @@ impl APIGame {
|
||||||
Ok(self.build_result(tray, Some(game_state), Some(update)))
|
Ok(self.build_result(tray, Some(game_state), Some(update)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_to_dictionary(&mut self, word: String) {
|
pub fn add_to_dictionary(&mut self, player: &str, word: &str) -> Result<ApiState, Error> {
|
||||||
self.0.add_word(word);
|
let word = word.to_uppercase();
|
||||||
|
self.0.add_word(&word);
|
||||||
|
|
||||||
|
let update = Update {
|
||||||
|
r#type: TurnAction::AddToDictionary { word },
|
||||||
|
player: player.to_string(),
|
||||||
|
turn_number: self.0.get_number_turns(),
|
||||||
|
};
|
||||||
|
self.1.push(update.clone());
|
||||||
|
let tray = self.0.player_states.get_tray(&player).unwrap().clone();
|
||||||
|
Ok(self.build_result(tray, None, Some(update)))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn is_ai_turn(&self) -> bool {
|
pub fn is_ai_turn(&self) -> bool {
|
||||||
|
|
|
@ -413,7 +413,7 @@ impl Game {
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_word(&mut self, word: String) {
|
pub fn add_word(&mut self, word: &str) {
|
||||||
let word = word.to_uppercase();
|
let word = word.to_uppercase();
|
||||||
|
|
||||||
self.dictionary.insert(word, -1.0);
|
self.dictionary.insert(word, -1.0);
|
||||||
|
@ -551,6 +551,10 @@ impl Game {
|
||||||
self.tile_pool.len()
|
self.tile_pool.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_number_turns(&self) -> usize {
|
||||||
|
self.turn_order
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_player_tile_count(&self, player: &str) -> Result<usize, String> {
|
pub fn get_player_tile_count(&self, player: &str) -> Result<usize, String> {
|
||||||
let tray = match self.player_states.get_tray(&player) {
|
let tray = match self.player_states.get_tray(&player) {
|
||||||
None => return Err(format!("Player {} not found", player)),
|
None => return Err(format!("Player {} not found", player)),
|
||||||
|
@ -576,6 +580,9 @@ pub enum TurnAction {
|
||||||
result: ScoreResult,
|
result: ScoreResult,
|
||||||
locations: Vec<usize>,
|
locations: Vec<usize>,
|
||||||
},
|
},
|
||||||
|
AddToDictionary {
|
||||||
|
word: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
|
|
Loading…
Reference in a new issue