WIP API work

This commit is contained in:
Joel Therrien 2024-12-23 13:02:12 -08:00
parent 8c2714b0dd
commit 3dbba11eb1
10 changed files with 402 additions and 65 deletions

View file

@ -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(),
} }
} }

View file

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

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"; 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}) {

View file

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

View file

@ -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);
if(input.type == "RoomChange"){
setPartyInfo(input.info); setPartyInfo(input.info);
}
console.log("Message from server ", event.data); console.log("Message from server ", event.data);
}); });
setSocket(socket); setSocket(socket);

View file

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

View file

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

View file

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

View file

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