feat: Change API for multiplayer direction

This commit is contained in:
Joel Therrien 2024-05-15 17:00:49 -07:00
parent ae33218a30
commit 45cc83bfb2
26 changed files with 2903 additions and 1832 deletions

3
.gitignore vendored
View file

@ -1,4 +1,5 @@
/target target/
/Cargo.lock /Cargo.lock
.idea/ .idea/
pkg/ pkg/
.nvmrc

View file

@ -1,21 +0,0 @@
use wasm_bindgen::prelude::wasm_bindgen;
pub mod constants;
pub mod board;
pub mod dictionary;
pub mod player_interaction;
pub mod game;
pub mod wasm;
#[wasm_bindgen]
extern {
pub fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet(name: &str) {
alert(&format!("Hello, {}!", name));
}

View file

@ -1,213 +0,0 @@
use serde::{Deserialize, Serialize};
use serde_wasm_bindgen::Error;
use tsify::Tsify;
use wasm_bindgen::JsValue;
use wasm_bindgen::prelude::wasm_bindgen;
use crate::board::{CellType, Letter};
use crate::game::{Game, GameState, PlayedTile};
use crate::player_interaction::ai::Difficulty;
#[wasm_bindgen]
pub struct GameWasm(Game);
#[derive(Serialize, Deserialize, Tsify)]
#[tsify(from_wasm_abi)]
pub enum ResponseType {
OK,
ERR,
}
#[derive(Serialize, Deserialize, Tsify)]
#[tsify(from_wasm_abi)]
pub struct MyResult<E: Serialize> {
response_type: ResponseType,
value: E,
game_state: Option<GameState>,
}
#[wasm_bindgen]
impl GameWasm {
#[wasm_bindgen(constructor)]
pub fn new(seed: u64, dictionary_text: &str, ai_difficulty: JsValue) -> GameWasm {
let difficulty: Difficulty = serde_wasm_bindgen::from_value(ai_difficulty).unwrap();
GameWasm(Game::new(seed, dictionary_text, vec!["Player".to_string()], vec![difficulty]))
}
pub fn get_tray(&self, name: &str) -> Result<JsValue, Error> {
let tray = self.0.player_states.get_tray(name);
serde_wasm_bindgen::to_value(&tray)
}
pub fn get_board_cell_types(&self) -> Result<JsValue, Error> {
let board = self.0.get_board();
let cell_types: Vec<CellType> = board.cells.iter().map(|cell| -> CellType {
cell.cell_type.clone()
}).collect();
serde_wasm_bindgen::to_value(&cell_types)
}
pub fn get_board_letters(&self) -> Result<JsValue, Error> {
let board = self.0.get_board();
let letters: Vec<Option<Letter>> = board.cells.iter().map(|cell| -> Option<Letter> {
cell.value.clone()
}).collect();
serde_wasm_bindgen::to_value(&letters)
}
pub fn receive_play(&mut self, tray_tile_locations: JsValue, commit_move: bool) -> Result<JsValue, Error> {
let tray_tile_locations: Vec<Option<PlayedTile>> = serde_wasm_bindgen::from_value(tray_tile_locations)?;
let result = self.0.receive_play(tray_tile_locations, commit_move);
match result {
Ok((x, game_state)) => {
serde_wasm_bindgen::to_value(&MyResult {
response_type: ResponseType::OK,
value: x,
game_state: Some(game_state)
})
},
Err(e) => {
serde_wasm_bindgen::to_value(&MyResult {
response_type: ResponseType::ERR,
value: e,
game_state: None,
})
}
}
}
pub fn get_scores(&self) -> Result<JsValue, JsValue> {
#[derive(Serialize, Deserialize, Tsify)]
#[tsify(from_wasm_abi)]
pub struct PlayerAndScore {
name: String,
score: u32,
}
let scores: Vec<PlayerAndScore> = self.0.player_states.0.iter()
.map(|player_state| {
PlayerAndScore {
name: player_state.player.get_name().to_string(),
score: player_state.score,
}
})
.collect();
Ok(serde_wasm_bindgen::to_value(&scores)?)
}
pub fn exchange_tiles(&mut self, tray_tile_locations: JsValue) -> Result<JsValue, Error>{
let tray_tile_locations: Vec<bool> = serde_wasm_bindgen::from_value(tray_tile_locations)?;
match self.0.exchange_tiles(tray_tile_locations) {
Ok((_, turn_action, state)) => {
serde_wasm_bindgen::to_value(&MyResult {
response_type: ResponseType::OK,
value: turn_action,
game_state: Some(state),
})
},
Err(e) => {
serde_wasm_bindgen::to_value(&MyResult {
response_type: ResponseType::ERR,
value: e,
game_state: None,
})
}
}
}
pub fn add_word(&mut self, word: String) {
self.0.add_word(word);
}
pub fn skip_turn(&mut self) -> Result<JsValue, Error>{
let result = self.0.pass();
match result {
Ok(game_state) => {
Ok(serde_wasm_bindgen::to_value(&MyResult {
response_type: ResponseType::OK,
value: "Turn passed",
game_state: Some(game_state),
})?)
},
Err(e) => {
Ok(serde_wasm_bindgen::to_value(&MyResult {
response_type: ResponseType::ERR,
value: e,
game_state: None,
})?)
}
}
}
pub fn advance_turn(&mut self) -> Result<JsValue, Error> {
let result = self.0.advance_turn();
match result {
Ok((turn_advance_result, game_state)) => {
Ok(serde_wasm_bindgen::to_value(&MyResult {
response_type: ResponseType::OK,
value: turn_advance_result,
game_state: Some(game_state),
})?)
},
Err(e) => {
Ok(serde_wasm_bindgen::to_value(&MyResult {
response_type: ResponseType::ERR,
value: e,
game_state: None,
})?)
}
}
}
pub fn get_current_player(&self) -> String {
self.0.current_player_name()
}
pub fn get_remaining_tiles(&self) -> usize {
self.0.get_remaining_tiles()
}
pub fn get_player_tile_count(&self, player: &str) -> Result<JsValue, Error> {
match self.0.get_player_tile_count(player) {
Ok(count) => {
Ok(serde_wasm_bindgen::to_value(&MyResult{
response_type: ResponseType::OK,
value: count,
game_state: None,
})?)
},
Err(msg) => {
Ok(serde_wasm_bindgen::to_value(&MyResult{
response_type: ResponseType::OK,
value: msg,
game_state: None,
})?)
}
}
}
}

1919
ui/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -2,13 +2,13 @@
"dependencies": { "dependencies": {
"react": "^18.2.0", "react": "^18.2.0",
"react-dom": "^18.2.0", "react-dom": "^18.2.0",
"word_grid": "file:../pkg" "word_grid": "file:../wasm/pkg"
}, },
"devDependencies": { "devDependencies": {
"@parcel/transformer-less": "^2.9.3", "@parcel/transformer-less": "^2.9.3",
"@types/react": "^18.2.18", "@types/react": "^18.2.18",
"@types/react-dom": "^18.2.7", "@types/react-dom": "^18.2.7",
"parcel": "^2.9.3", "parcel": "^2.12.0",
"process": "^0.11.10" "process": "^0.11.10"
}, },
"scripts": { "scripts": {

View file

@ -1,16 +1,5 @@
import * as React from "react"; import * as React from "react";
import {useEffect, useMemo, useReducer, useRef, useState} from "react"; import {useEffect, useReducer, useRef, useState} from "react";
import {
GameState,
GameWasm,
MyResult,
PlayedTile,
PlayerAndScore,
ScoreResult,
Tray,
TurnAction,
TurnAdvanceResult
} from "../../pkg/word_grid";
import { import {
Direction, Direction,
GRID_LENGTH, GRID_LENGTH,
@ -28,6 +17,7 @@ import {
} from "./utils"; } from "./utils";
import {TileExchangeModal} from "./TileExchange"; import {TileExchangeModal} from "./TileExchange";
import {Grid, Scores, TileTray} from "./UI"; import {Grid, Scores, TileTray} from "./UI";
import {API, APIState, GameState, TurnAction, Tray, PlayedTile, Letter} from "./api";
function addLogInfo(existingLog: React.JSX.Element[], newItem: React.JSX.Element) { function addLogInfo(existingLog: React.JSX.Element[], newItem: React.JSX.Element) {
newItem = React.cloneElement(newItem, {key: existingLog.length}) newItem = React.cloneElement(newItem, {key: existingLog.length})
@ -36,17 +26,42 @@ function addLogInfo(existingLog: React.JSX.Element[], newItem: React.JSX.Element
} }
export function Game(props: { export function Game(props: {
wasm: GameWasm, api: API,
settings: Settings, settings: Settings,
end_game_fn: () => void, end_game_fn: () => void,
}) { }) {
const cellTypes = useMemo(() => { const [api_state, setAPIState] = useState<APIState>(undefined);
return props.wasm.get_board_cell_types(); const [isLoading, setIsLoading] = useState<boolean>(true);
}, []);
const [isGameOver, setGameOver] = useState<boolean>(false);
const [confirmedScorePoints, setConfirmedScorePoints] = useState<number>(-1); const [confirmedScorePoints, setConfirmedScorePoints] = useState<number>(-1);
const [currentTurnNumber, setCurrentTurnNumber] = useState<number>(0);
let isGameOver = false;
if (api_state !== undefined) {
isGameOver = api_state.public_information.game_state.type === "Ended";
}
function load() {
// setAPIState(undefined);
// setIsLoading(true);
const result = props.api.load();
result.then(
(state) => {
setAPIState(state);
setIsLoading(false);
}
)
.catch((error) => {
console.log("load() failed")
console.log(error);
});
}
useEffect(() => {
load();
}, [])
const [boardLetters, setBoardLetters] = useState<HighlightableLetterData[]>(() => { const [boardLetters, setBoardLetters] = useState<HighlightableLetterData[]>(() => {
const newLetterData = [] as HighlightableLetterData[]; const newLetterData = [] as HighlightableLetterData[];
@ -58,8 +73,6 @@ export function Game(props: {
}); });
function adjustGridArrow(existing: GridArrowData, update: GridArrowDispatchAction): GridArrowData { function adjustGridArrow(existing: GridArrowData, update: GridArrowDispatchAction): GridArrowData {
console.log({update});
if (update.action == GridArrowDispatchActionType.CLEAR) { if (update.action == GridArrowDispatchActionType.CLEAR) {
return null; return null;
} else if (update.action == GridArrowDispatchActionType.CYCLE) { } else if (update.action == GridArrowDispatchActionType.CYCLE) {
@ -119,36 +132,28 @@ export function Game(props: {
} }
function exchangeFunction(selectedArray: Array<boolean>) { function exchangeFunction(selectedArray: Array<boolean>) {
const result = props.api.exchange(selectedArray);
const result: MyResult<TurnAction | string> = props.wasm.exchange_tiles(selectedArray); result
.then(
if(result.response_type === "ERR") { (_api_state) => {
logDispatch(<div><em>{(result.value as string)}</em></div>); load();
} else { })
handlePlayerAction(result.value as TurnAction, props.settings.playerName); .catch((error) => {
setTurnCount(turnCount + 1); console.error({error});
logDispatch(<div>{error}</div>);
if(result.game_state.type === "Ended") { });
endGame(result.game_state);
}
}
} }
function addWordFn(word: string) {
props.wasm.add_word(word);
}
const [gridArrow, gridArrowDispatch] = useReducer(adjustGridArrow, null); const [gridArrow, gridArrowDispatch] = useReducer(adjustGridArrow, null);
const [logInfo, logDispatch] = useReducer(addLogInfo, []); const [logInfo, logDispatch] = useReducer(addLogInfo, []);
function movePlayableLetters(playerLetters: PlayableLetterData[], update: TileDispatchAction) { function movePlayableLetters(playerLetters: PlayableLetterData[], update: TileDispatchAction) {
if (update.action === TileDispatchActionType.RETRIEVE) { if (update.action === TileDispatchActionType.RETRIEVE) {
let tray: Tray = props.wasm.get_tray("Player"); let tray: Tray = api_state.tray;
if (update.override) { if (update.override) {
playerLetters = []; playerLetters = [];
@ -215,35 +220,6 @@ export function Game(props: {
} }
const [playerLetters, trayDispatch] = useReducer(movePlayableLetters, []); const [playerLetters, trayDispatch] = useReducer(movePlayableLetters, []);
const [turnCount, setTurnCount] = useState<number>(1);
const playerAndScores: PlayerAndScore[] = useMemo(() => {
return props.wasm.get_scores();
}, [turnCount, isGameOver]);
useEffect(() => {
const newLetterData = props.wasm.get_board_letters() as HighlightableLetterData[];
// we need to go through and set 'highlight' field to either true or false
// it will always be false if the player that just went was the AI
// TODO - build in support for multiple other players
for(let i=0; i<newLetterData.length; i++) {
const newLetter = newLetterData[i];
if(boardLetters[i] === undefined && newLetter !== undefined && playerTurnName == props.settings.playerName) {
newLetter.highlight = true;
} else if(newLetter !== undefined) {
newLetter.highlight = false;
}
}
setBoardLetters(newLetterData);
}, [turnCount]);
const playerTurnName = useMemo(() => {
return props.wasm.get_current_player();
}, [turnCount]);
const logDivRef = useRef(null); const logDivRef = useRef(null);
const [isTileExchangeOpen, setIsTileExchangeOpen] = useState<boolean>(false); const [isTileExchangeOpen, setIsTileExchangeOpen] = useState<boolean>(false);
@ -255,21 +231,6 @@ export function Game(props: {
} }
}, [logInfo]); }, [logInfo]);
const remainingTiles = useMemo(() => {
return props.wasm.get_remaining_tiles();
}, [turnCount, isGameOver]);
const remainingAITiles = useMemo(() => {
let result = props.wasm.get_player_tile_count(props.settings.aiName) as MyResult<number | String>;
if(result.response_type == "OK") {
return result.value as number;
} else {
console.error(result.value);
return -1;
}
}, [turnCount, isGameOver]);
function handlePlayerAction(action: TurnAction, playerName: string) { function handlePlayerAction(action: TurnAction, playerName: string) {
if (action.type == "PlayTiles") { if (action.type == "PlayTiles") {
@ -278,11 +239,13 @@ export function Game(props: {
for (let word of result.words) { for (let word of result.words) {
logDispatch(<div>{playerName} received {word.score} points for playing '{word.word}.'</div>); logDispatch(<div>{playerName} received {word.score} points for playing '{word.word}.'</div>);
} }
logDispatch(<div>{playerName} received a total of <strong>{result.total} points</strong> for their turn.</div>); logDispatch(<div>{playerName} received a total of <strong>{result.total} points</strong> for their turn.
</div>);
} else if (action.type == "ExchangeTiles") { } else if (action.type == "ExchangeTiles") {
logDispatch(<div>{playerName} exchanged {action.tiles_exchanged} tile{action.tiles_exchanged > 1 ? 's' : ''} for their turn.</div>); logDispatch(
} <div>{playerName} exchanged {action.tiles_exchanged} tile{action.tiles_exchanged > 1 ? 's' : ''} for
else if(action.type == "Pass"){ their turn.</div>);
} else if (action.type == "Pass") {
logDispatch(<div>{playerName} passed.</div>); logDispatch(<div>{playerName} passed.</div>);
} }
@ -293,11 +256,9 @@ export function Game(props: {
function endGame(state: GameState) { function endGame(state: GameState) {
if (state.type != "InProgress") { if (state.type != "InProgress") {
setGameOver(true);
logDispatch(<h4>Scoring</h4>); logDispatch(<h4>Scoring</h4>);
const scores = props.wasm.get_scores() as PlayerAndScore[]; const scores = api_state.public_information.players;
let pointsBonus = 0; let pointsBonus = 0;
for (const playerAndScore of scores) { for (const playerAndScore of scores) {
const name = playerAndScore.name; const name = playerAndScore.name;
@ -348,15 +309,14 @@ export function Game(props: {
.at(0); .at(0);
const playersAtHighest = scores.filter((score) => score.score == highestScore); const playersAtHighest = scores.filter((score) => score.score == highestScore);
let endGameMsg: string = ''; let endGameMsg: string;
if (playersAtHighest.length > 1 && state.finisher == null) { if (playersAtHighest.length > 1 && state.finisher == null) {
endGameMsg = "Tie game!"; endGameMsg = "Tie game!";
} else if (playersAtHighest.length > 1 && state.finisher != null) { } else if (playersAtHighest.length > 1 && state.finisher != null) {
// if there's a tie then the finisher gets the win // if there's a tie then the finisher gets the win
endGameMsg = `${playersAtHighest[0].name} won by finishing first!`; endGameMsg = `${playersAtHighest[0].name} won by finishing first!`;
} } else {
else {
endGameMsg = `${playersAtHighest[0].name} won!`; endGameMsg = `${playersAtHighest[0].name} won!`;
} }
logDispatch(<h4>Game over - {endGameMsg}</h4>); logDispatch(<h4>Game over - {endGameMsg}</h4>);
@ -367,34 +327,78 @@ export function Game(props: {
} }
function runAI() { function updateBoardLetters(newLetters: Array<Letter>) {
const result: MyResult<TurnAdvanceResult> = props.wasm.advance_turn(); const newLetterData = newLetters as HighlightableLetterData[];
if(result.response_type === "OK" && result.value.type == "AIMove") {
handlePlayerAction(result.value.action, props.settings.aiName); for (let i = 0; i < newLetterData.length; i++) {
if(result.game_state.type === "Ended") { const newLetter = newLetterData[i];
endGame(result.game_state); if (newLetter != null) {
newLetter.highlight = false;
}
} }
} else { // loop through the histories backwards until we reach our player
// this would be quite surprising for (let j = api_state.public_information.history.length - 1; j >= 0; j--) {
console.error({result}); const update = api_state.public_information.history[j];
if (update.player == props.settings.playerName) {
break
}
if (update.type.type === "PlayTiles") {
for (let i of update.type.locations) {
newLetterData[i].highlight = true;
}
}
} }
setTurnCount(turnCount + 1); setBoardLetters(newLetterData);
} }
useEffect(() => { useEffect(() => {
if (api_state) {
console.debug(api_state);
gridArrowDispatch({action: GridArrowDispatchActionType.CLEAR});
trayDispatch({action: TileDispatchActionType.RETRIEVE}); trayDispatch({action: TileDispatchActionType.RETRIEVE});
setConfirmedScorePoints(-1); setConfirmedScorePoints(-1);
if(!isGameOver){ updateBoardLetters(api_state.public_information.board);
logDispatch(<h4>Turn {turnCount}</h4>);
logDispatch(<div>{playerTurnName}'s turn</div>);
if(playerTurnName != props.settings.playerName && !isGameOver) {
runAI();
}
}
}, [turnCount]);
for (let i = currentTurnNumber; 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];
handlePlayerAction(update.type, update.player);
}
setCurrentTurnNumber(api_state.public_information.history.length);
if (!isGameOver) {
logDispatch(<h4>Turn {api_state.public_information.history.length + 1}</h4>);
logDispatch(<div>{api_state.public_information.current_player}'s turn</div>);
} else {
endGame(api_state.public_information.game_state);
}
}
}, [api_state]);
if (isLoading) {
return <div>Still loading</div>;
}
const playerAndScores = api_state.public_information.players;
const remainingTiles = api_state.public_information.remaining_tiles;
let remainingAITiles = null;
for (let player of playerAndScores) {
if (player.name == 'AI') {
remainingAITiles = player.tray_tiles;
break;
}
}
return <> return <>
<TileExchangeModal <TileExchangeModal
@ -405,7 +409,7 @@ export function Game(props: {
/> />
<div className="board-log"> <div className="board-log">
<Grid <Grid
cellTypes={cellTypes} cellTypes={api_state.public_information.cell_types}
playerLetters={playerLetters} playerLetters={playerLetters}
boardLetters={boardLetters} boardLetters={boardLetters}
tileDispatch={trayDispatch} tileDispatch={trayDispatch}
@ -439,15 +443,15 @@ export function Game(props: {
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);
}}>Open Tile Exchange</button> }}>Open Tile Exchange
</button>
</div> </div>
<TileTray letters={playerLetters} trayLength={props.settings.trayLength} trayDispatch={trayDispatch}/> <TileTray letters={playerLetters} trayLength={props.settings.trayLength} trayDispatch={trayDispatch}/>
<div className="player-controls"> <div className="player-controls">
<button <button
className="check" className="check"
disabled={isGameOver} disabled={isGameOver}
onClick={() => { onClick={async () => {
const playedTiles = playerLetters.map((i) => { const playedTiles = playerLetters.map((i) => {
if (i === undefined) { if (i === undefined) {
return null; return null;
@ -468,38 +472,40 @@ export function Game(props: {
return null; return null;
}); });
const result: MyResult<{ type: "PlayTiles"; result: ScoreResult } | string> = props.wasm.receive_play(playedTiles, confirmedScorePoints > -1); const committing = confirmedScorePoints > -1;
console.log({result}); const result = props.api.play(playedTiles, committing);
if(result.response_type === "ERR") { result
const message = result.value as string; .then(
if (message.endsWith("is not a valid word")) { (api_state) => {
// extract out word
const word = message.split(" ")[0]; const play_tiles: TurnAction = api_state.update.type;
logDispatch(<AddWordButton word={word} addWordFn={addWordFn} />); if (play_tiles.type == "PlayTiles") {
} else { setConfirmedScorePoints(play_tiles.result.total);
logDispatch(<div><em>{message}</em></div>);
if (committing) {
load();
} }
} else { } else {
console.error("Inaccessible branch!!!")
const score_result = (result.value as { type: "PlayTiles"; result: ScoreResult }).result;
const total_points = score_result.total;
if (confirmedScorePoints > -1) {
handlePlayerAction({type: "PlayTiles", result: score_result}, props.settings.playerName);
setTurnCount(turnCount + 1);
} }
if (result.game_state.type === "Ended") {
endGame(result.game_state);
} }
)
.catch((error) => {
console.error({error});
setConfirmedScorePoints(total_points); if (error.endsWith(" is not a valid word")) {
const word = error.split(' ')[0];
// For whatever reason I can't pass props.api.add_to_dictionary directly
logDispatch(<AddWordButton word={word}
addWordFn={(word) => props.api.add_to_dictionary(word)}/>);
} else {
logDispatch(<div>{error}</div>);
} }
});
}}>{confirmedScorePoints > -1 ? `Score ${confirmedScorePoints} points` : "Check"}</button> }}>{confirmedScorePoints > -1 ? `Score ${confirmedScorePoints} points` : "Check"}</button>
<button <button
@ -507,26 +513,31 @@ export function Game(props: {
disabled={isGameOver} disabled={isGameOver}
onClick={() => { onClick={() => {
trayDispatch({action: TileDispatchActionType.RETURN}); trayDispatch({action: TileDispatchActionType.RETURN});
}}>Return Tiles</button> }}>Return Tiles
</button>
<button <button
className="pass" className="pass"
disabled={isGameOver} disabled={isGameOver}
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.wasm.skip_turn() as MyResult<string>; const result = props.api.pass();
handlePlayerAction({type: "Pass"}, props.settings.playerName);
setTurnCount(turnCount + 1); result
.then(
(_api_state) => {
load();
})
.catch((error) => {
console.error({error});
logDispatch(<div>{error}</div>);
});
if (result.game_state.type === "Ended") {
endGame(result.game_state);
} }
} }}>Pass
}}>Pass</button> </button>
</div> </div>
</div> </div>
</>; </>;
} }

View file

@ -1,8 +1,9 @@
import * as React from "react"; import * as React from "react";
import {useState} from "react"; import {useState} from "react";
import {Difficulty, GameWasm} from '../../pkg/word_grid';
import {Settings} from "./utils"; import {Settings} from "./utils";
import {Game} from "./Game"; import {Game} from "./Game";
import {API, Difficulty} from "./api";
import {GameWasm} from "./wasm";
export function Menu(props: {settings: Settings, dictionary_text: string}) { export function Menu(props: {settings: Settings, dictionary_text: string}) {
@ -70,8 +71,8 @@ export function Menu(props: {settings: Settings, dictionary_text: string}) {
proportion: processedProportionDictionary, proportion: processedProportionDictionary,
randomness: processedAIRandomness, randomness: processedAIRandomness,
}; };
const game_wasm = new GameWasm(BigInt(seed), props.dictionary_text, difficulty); const game_wasm: API = new GameWasm(BigInt(seed), props.dictionary_text, difficulty);
const game = <Game settings={props.settings} wasm={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>
</div> </div>

View file

@ -1,8 +1,8 @@
import {addNTimes, PlayableLetterData} from "./utils"; import {addNTimes, PlayableLetterData} from "./utils";
import {useEffect, useState} from "react"; import {useEffect, useState} from "react";
import {Modal} from "./Modal"; import {Modal} from "./Modal";
import {Letter as LetterData} from "../../pkg/word_grid";
import * as React from "react"; import * as React from "react";
import {Letter} from "./api";
export function TileExchangeModal(props: { export function TileExchangeModal(props: {
playerLetters: PlayableLetterData[], playerLetters: PlayableLetterData[],
@ -105,7 +105,7 @@ function TilesExchangedTray(props: {
} }
function DummyExchangeTile(props: { function DummyExchangeTile(props: {
letter: LetterData, letter: Letter,
isSelected: boolean, isSelected: boolean,
onClick: () => void, onClick: () => void,
}){ }){

View file

@ -1,8 +1,6 @@
import * as React from "react"; import * as React from "react";
import {ChangeEvent, JSX} from "react"; import {ChangeEvent, JSX} from "react";
import {PlayerAndScore} from "../../pkg/word_grid";
import { import {
CellType,
cellTypeToDetails, cellTypeToDetails,
CoordinateData, CoordinateData,
GridArrowData, GridArrowData,
@ -14,6 +12,7 @@ import {
TileDispatch, TileDispatch,
TileDispatchActionType, TileDispatchActionType,
} from "./utils"; } from "./utils";
import {APIPlayer, CellType} from "./api";
export function TileSlot(props: { export function TileSlot(props: {
@ -234,7 +233,7 @@ export function Grid(props: {
</div> </div>
} }
export function Scores(props: {playerScores: Array<PlayerAndScore>}){ 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>

90
ui/src/api.ts Normal file
View file

@ -0,0 +1,90 @@
export interface Tray {
letters: (Letter | undefined)[];
}
export interface ScoreResult {
words: WordResult[];
total: number;
}
export interface WordResult {
word: string;
score: number;
}
export interface PlayedTile {
index: number;
character: string | undefined;
}
export interface Difficulty {
proportion: number;
randomness: number;
}
export interface Letter {
text: string;
points: number;
ephemeral: boolean;
is_blank: boolean;
}
export type TurnAction = { type: "Pass" } | { type: "ExchangeTiles"; tiles_exchanged: number } | { type: "PlayTiles"; result: ScoreResult; locations: number[] };
export enum CellType {
Normal = "Normal",
DoubleWord = "DoubleWord",
DoubleLetter = "DoubleLetter",
TripleLetter = "TripleLetter",
TripleWord = "TripleWord",
Start = "Start",
}
export type GameState = { type: "InProgress" } | { type: "Ended"; finisher: string | undefined; remaining_tiles: Map<string, Letter[]> };
export interface APIPlayer {
name: string;
tray_tiles: number;
score: number
}
export interface PublicInformation {
game_state: GameState;
board: Array<Letter>;
cell_types: Array<CellType>;
current_player: string;
players: Array<APIPlayer>;
remaining_tiles: number;
history: Array<Update>;
}
export interface Update {
type: TurnAction,
player: string;
}
export interface APIState {
public_information: PublicInformation;
tray: Tray;
update?: Update;
}
export interface Result<A, B> {
"Ok"?: A;
"Err"?: B;
}
export function is_ok<A, B>(result: Result<A, B>): boolean {
return result["Ok"] != null;
}
export interface API {
exchange: (selection: Array<boolean>) => Promise<APIState>;
pass: () => Promise<APIState>;
play: (tiles: Array<PlayedTile>, commit: boolean) => Promise<APIState>;
add_to_dictionary: (word: string) => Promise<void>;
load: () => Promise<APIState>;
}

View file

@ -1,4 +1,4 @@
import init from '../../pkg/word_grid.js'; import init from 'word_grid';
import {createRoot} from "react-dom/client"; import {createRoot} from "react-dom/client";
import * as React from "react"; import * as React from "react";
import {Menu} from "./Menu"; import {Menu} from "./Menu";

View file

@ -1,14 +1,5 @@
import {Letter as LetterData, Letter} from "../../pkg/word_grid";
import * as React from "react"; import * as React from "react";
import {CellType, Letter as LetterData} from "./api";
export enum CellType {
Normal = "Normal",
DoubleWord = "DoubleWord",
DoubleLetter = "DoubleLetter",
TripleLetter = "TripleLetter",
TripleWord = "TripleWord",
Start = "Start",
}
export interface Settings { export interface Settings {
@ -118,7 +109,7 @@ export function addNTimes<T>(array: T[], toAdd: T, times: number) {
} }
} }
export function mergeTrays(existing: PlayableLetterData[], newer: (Letter | undefined)[]): PlayableLetterData[] { export function mergeTrays(existing: PlayableLetterData[], newer: (LetterData | undefined)[]): PlayableLetterData[] {
let trayLength = Math.max(existing.length, newer.length); let trayLength = Math.max(existing.length, newer.length);

66
ui/src/wasm.ts Normal file
View file

@ -0,0 +1,66 @@
import {API, APIState, Difficulty, PlayedTile, Result, is_ok} from "./api";
import {WasmAPI} from 'word_grid';
export class GameWasm implements API{
wasm: WasmAPI;
constructor(seed: bigint, dictionary_text: string, difficulty: Difficulty) {
this.wasm = new WasmAPI(seed, dictionary_text, difficulty);
}
add_to_dictionary(word: string): Promise<void> {
return new Promise((resolve, _) => {
this.wasm.add_to_dictionary(word);
resolve()
});
}
exchange(selection: Array<boolean>): Promise<APIState> {
return new Promise((resolve, reject) => {
let api_state: Result<APIState, any> = this.wasm.exchange(selection);
if(is_ok(api_state)) {
resolve(api_state.Ok);
} else {
reject(api_state.Err);
}
});
}
load(): Promise<APIState> {
return new Promise((resolve, reject) => {
let api_state: Result<APIState, any> = this.wasm.load();
if(is_ok(api_state)) {
resolve(api_state.Ok);
} else {
reject(api_state.Err);
}
});
}
pass(): Promise<APIState> {
return new Promise((resolve, reject) => {
let api_state: Result<APIState, any> = this.wasm.pass();
if(is_ok(api_state)) {
resolve(api_state.Ok);
} else {
reject(api_state.Err);
}
});
}
play(tiles: Array<PlayedTile>, commit: boolean): Promise<APIState> {
return new Promise((resolve, reject) => {
let api_state: Result<APIState, any> = this.wasm.play(tiles, commit);
if(is_ok(api_state)) {
resolve(api_state.Ok);
} else {
reject(api_state.Err);
}
});
}
}

289
wasm/Cargo.lock generated Normal file
View file

@ -0,0 +1,289 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "csv"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe"
dependencies = [
"csv-core",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "csv-core"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70"
dependencies = [
"memchr",
]
[[package]]
name = "getrandom"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasm-bindgen",
]
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "js-sys"
version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.155"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
[[package]]
name = "log"
version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[package]]
name = "memchr"
version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-macro2"
version = "1.0.84"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "ryu"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "serde"
version = "1.0.203"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde-wasm-bindgen"
version = "0.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b"
dependencies = [
"js-sys",
"serde",
"wasm-bindgen",
]
[[package]]
name = "serde_derive"
version = "1.0.203"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "syn"
version = "2.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm"
version = "0.1.0"
dependencies = [
"serde",
"serde-wasm-bindgen",
"serde_json",
"wasm-bindgen",
"word_grid",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
[[package]]
name = "word_grid"
version = "0.1.0"
dependencies = [
"csv",
"getrandom",
"rand",
"serde",
"serde_json",
]

14
wasm/Cargo.toml Normal file
View file

@ -0,0 +1,14 @@
[package]
name = "wasm"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
serde-wasm-bindgen = "0.6.5"
wasm-bindgen = "0.2.92"
word_grid = { version = "0.1.0", path = "../wordgrid" }
serde_json = "1.0"
serde = { version = "1.0.202", features = ["derive"] }

60
wasm/src/lib.rs Normal file
View file

@ -0,0 +1,60 @@
use wasm_bindgen::prelude::wasm_bindgen;
use wasm_bindgen::JsValue;
use word_grid::api::APIGame;
use word_grid::game::{Game, PlayedTile};
use word_grid::player_interaction::ai::Difficulty;
#[wasm_bindgen]
pub struct WasmAPI(APIGame);
#[wasm_bindgen]
impl WasmAPI {
#[wasm_bindgen(constructor)]
pub fn new(seed: u64, dictionary_text: &str, ai_difficulty: JsValue) -> Self {
let difficulty: Difficulty = serde_wasm_bindgen::from_value(ai_difficulty).unwrap();
let game = Game::new(
seed,
dictionary_text,
vec!["Player".to_string()],
vec![difficulty],
);
WasmAPI(APIGame::new(game))
}
pub fn exchange(&mut self, selection: JsValue) -> JsValue {
let selection: Vec<bool> = serde_wasm_bindgen::from_value(selection).unwrap();
let result = self.0.exchange("Player".to_string(), selection);
serde_wasm_bindgen::to_value(&result).unwrap()
}
pub fn pass(&mut self) -> JsValue {
let result = self.0.pass("Player".to_string());
serde_wasm_bindgen::to_value(&result).unwrap()
}
pub fn load(&mut self) -> JsValue {
let result = self.0.load("Player".to_string());
serde_wasm_bindgen::to_value(&result).unwrap()
}
pub fn play(&mut self, tray_tile_locations: JsValue, commit_move: bool) -> JsValue {
let tray_tile_locations: Vec<Option<PlayedTile>> =
serde_wasm_bindgen::from_value(tray_tile_locations).unwrap();
let result = self
.0
.play("Player".to_string(), tray_tile_locations, commit_move);
serde_wasm_bindgen::to_value(&result).unwrap()
}
pub fn add_to_dictionary(&mut self, word: String) -> JsValue {
let result = self.0.add_to_dictionary(word);
serde_wasm_bindgen::to_value(&result).unwrap()
}
}

267
wordgrid/Cargo.lock generated Normal file
View file

@ -0,0 +1,267 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "bumpalo"
version = "3.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "csv"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac574ff4d437a7b5ad237ef331c17ccca63c46479e5b5453eb8e10bb99a759fe"
dependencies = [
"csv-core",
"itoa",
"ryu",
"serde",
]
[[package]]
name = "csv-core"
version = "0.1.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5efa2b3d7902f4b634a20cae3c9c4e6209dc4779feb6863329607560143efa70"
dependencies = [
"memchr",
]
[[package]]
name = "getrandom"
version = "0.2.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"js-sys",
"libc",
"wasi",
"wasm-bindgen",
]
[[package]]
name = "itoa"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
[[package]]
name = "js-sys"
version = "0.3.69"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d"
dependencies = [
"wasm-bindgen",
]
[[package]]
name = "libc"
version = "0.2.154"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
[[package]]
name = "log"
version = "0.4.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
[[package]]
name = "memchr"
version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
[[package]]
name = "once_cell"
version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "ppv-lite86"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-macro2"
version = "1.0.82"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rand"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404"
dependencies = [
"libc",
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
"getrandom",
]
[[package]]
name = "ryu"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[package]]
name = "serde"
version = "1.0.202"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.202"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "syn"
version = "2.0.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bf5be731623ca1a1fb7d8be6f261a3be6d3e2337b8a1f97be944d020c8fcb704"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "unicode-ident"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasm-bindgen"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8"
dependencies = [
"cfg-if",
"wasm-bindgen-macro",
]
[[package]]
name = "wasm-bindgen-backend"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da"
dependencies = [
"bumpalo",
"log",
"once_cell",
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7"
dependencies = [
"proc-macro2",
"quote",
"syn",
"wasm-bindgen-backend",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.92"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96"
[[package]]
name = "word_grid"
version = "0.1.0"
dependencies = [
"csv",
"getrandom",
"rand",
"serde",
"serde_json",
]

View file

@ -7,16 +7,9 @@ repository = "https://git.joeltherrien.ca/joel/WordGrid"
license = "AGPL-3" license = "AGPL-3"
description = "A (WIP) package for playing 'WordGrid'." description = "A (WIP) package for playing 'WordGrid'."
[lib]
crate-type = ["cdylib"]
[dependencies] [dependencies]
csv = "1.2.2" csv = "1.3.0"
rand = {version = "0.8.5", features = ["small_rng"]} rand = {version = "0.8.5", features = ["small_rng"]}
getrandom = {version = "0.2", features = ["js"]} getrandom = {version = "0.2", features = ["js"]}
wasm-bindgen = { version = "0.2.87", features = ["serde-serialize"] }
serde_json = "1.0" serde_json = "1.0"
serde = { version = "=1.0.171", features = ["derive"] } serde = { version = "1.0.202", features = ["derive"] }
serde-wasm-bindgen = "0.4.5"
tsify = { version = "0.4.5", features = ["js"] }

217
wordgrid/src/api.rs Normal file
View file

@ -0,0 +1,217 @@
use crate::board::{CellType, Letter};
use crate::game::{
Error, Game, GameState, PlayedTile, Player, PlayerState, TurnAction, TurnAdvanceResult,
};
use crate::player_interaction::Tray;
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ApiPlayer {
name: String,
tray_tiles: usize,
score: u32,
}
impl ApiPlayer {
fn from(player_state: &PlayerState) -> ApiPlayer {
ApiPlayer {
name: player_state.player.get_name().to_string(),
tray_tiles: player_state.tray.count(),
score: player_state.score,
}
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Update {
r#type: TurnAction,
player: String,
}
pub type ApiBoard = Vec<Option<Letter>>;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct PublicInformation {
game_state: GameState,
board: ApiBoard,
cell_types: Vec<CellType>,
current_player: String,
players: Vec<ApiPlayer>,
remaining_tiles: usize,
history: Vec<Update>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct ApiState {
public_information: PublicInformation,
tray: Tray,
update: Option<Update>,
}
pub struct APIGame(pub Game, Vec<Update>);
impl APIGame {
pub fn new(game: Game) -> APIGame {
APIGame(game, vec![])
}
fn is_players_turn(&self, player: &str) -> bool {
self.0.current_player_name().eq(player)
}
fn player_exists(&self, player: &str) -> bool {
self.0.player_states.get_player_state(player).is_some()
}
fn player_exists_and_turn(&self, player: &str) -> Result<(), Error> {
if !self.player_exists(&player) {
Err(Error::InvalidPlayer(player.to_string()))
} else if !self.is_players_turn(&player) {
Err(Error::WrongTurn(player.to_string()))
} else {
Ok(())
}
}
fn build_result(
&self,
tray: Tray,
game_state: Option<GameState>,
update: Option<Update>,
) -> ApiState {
ApiState {
public_information: self.build_public_information(game_state, &self.1),
tray,
update,
}
}
fn build_public_information(
&self,
game_state: Option<GameState>,
history: &Vec<Update>,
) -> PublicInformation {
let game_state = game_state.unwrap_or_else(|| self.0.get_state());
let cell_types = self
.0
.get_board()
.cells
.iter()
.map(|cell| -> CellType { cell.cell_type })
.collect::<Vec<CellType>>();
let board: Vec<Option<Letter>> = self
.0
.get_board()
.cells
.iter()
.map(|cell| -> Option<Letter> { cell.value.clone() })
.collect();
let players = self
.0
.player_states
.0
.iter()
.map(|p| ApiPlayer::from(p))
.collect::<Vec<ApiPlayer>>();
PublicInformation {
game_state,
board,
cell_types,
current_player: self.0.current_player_name(),
players,
remaining_tiles: self.0.get_remaining_tiles(),
history: history.clone(),
}
}
pub fn exchange(&mut self, player: String, tray_tiles: Vec<bool>) -> Result<ApiState, Error> {
self.player_exists_and_turn(&player)?;
let (tray, turn_action, game_state) = self.0.exchange_tiles(tray_tiles)?;
let update = Update {
r#type: turn_action,
player,
};
self.1.push(update.clone());
Ok(self.build_result(tray, Some(game_state), Some(update)))
}
pub fn pass(&mut self, player: String) -> Result<ApiState, Error> {
self.player_exists_and_turn(&player)?;
let game_state = self.0.pass()?;
let tray = self.0.player_states.get_tray(&player).unwrap().clone();
let update = Update {
r#type: TurnAction::Pass,
player,
};
self.1.push(update.clone());
Ok(self.build_result(tray, Some(game_state), Some(update)))
}
pub fn load(&mut self, player: String) -> Result<ApiState, Error> {
if !self.player_exists(&player) {
Err(Error::InvalidPlayer(player))
} else {
while self.is_ai_turn() {
let (result, _) = self.0.advance_turn()?;
if let TurnAdvanceResult::AIMove { name, action } = result {
self.1.push(Update {
r#type: action,
player: name,
});
} else {
unreachable!("We already checked that the current player is AI");
}
}
let tray = self.0.player_states.get_tray(&player).unwrap().clone();
Ok(self.build_result(tray, None, None))
}
}
pub fn play(
&mut self,
player: String,
tray_tile_locations: Vec<Option<PlayedTile>>,
commit_move: bool,
) -> Result<ApiState, Error> {
self.player_exists_and_turn(&player)?;
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 update = Update {
r#type: turn_action,
player,
};
if commit_move {
self.1.push(update.clone())
}
Ok(self.build_result(tray, Some(game_state), Some(update)))
}
pub fn add_to_dictionary(&mut self, word: String) {
self.0.add_word(word);
}
pub fn is_ai_turn(&self) -> bool {
let current_player = self.0.current_player_name();
matches!(
self.0
.player_states
.get_player_state(&current_player)
.unwrap()
.player,
Player::AI { .. }
)
}
}

View file

@ -1,23 +1,23 @@
use crate::constants::{ALL_LETTERS_BONUS, GRID_LENGTH, TRAY_LENGTH};
use crate::dictionary::DictionaryImpl;
use crate::game::Error;
use serde::{Deserialize, Serialize};
use std::borrow::BorrowMut;
use std::collections::HashSet; use std::collections::HashSet;
use std::fmt; use std::fmt;
use std::fmt::{Formatter, Write}; use std::fmt::{Formatter, Write};
use std::borrow::BorrowMut;
use serde::{Deserialize, Serialize};
use tsify::Tsify;
use crate::constants::{ALL_LETTERS_BONUS, GRID_LENGTH, TRAY_LENGTH};
use crate::dictionary::DictionaryImpl;
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
pub enum Direction { pub enum Direction {
Row, Column Row,
Column,
} }
impl Direction { impl Direction {
pub fn invert(&self) -> Self { pub fn invert(&self) -> Self {
match &self { match &self {
Direction::Row => {Direction::Column} Direction::Row => Direction::Column,
Direction::Column => {Direction::Row} Direction::Column => Direction::Row,
} }
} }
} }
@ -26,7 +26,6 @@ impl Direction {
pub struct Coordinates(pub u8, pub u8); pub struct Coordinates(pub u8, pub u8);
impl Coordinates { impl Coordinates {
pub fn new_from_index(index: usize) -> Self { pub fn new_from_index(index: usize) -> Self {
let y = index / GRID_LENGTH as usize; let y = index / GRID_LENGTH as usize;
let x = index % GRID_LENGTH as usize; let x = index % GRID_LENGTH as usize;
@ -36,11 +35,15 @@ impl Coordinates {
fn add(&self, direction: Direction, i: i8) -> Option<Self> { fn add(&self, direction: Direction, i: i8) -> Option<Self> {
let proposed = match direction { let proposed = match direction {
Direction::Column => {(self.0 as i8, self.1 as i8+i)} Direction::Column => (self.0 as i8, self.1 as i8 + i),
Direction::Row => {(self.0 as i8+i, self.1 as i8)} Direction::Row => (self.0 as i8 + i, self.1 as i8),
}; };
if proposed.0 < 0 || proposed.0 >= GRID_LENGTH as i8 || proposed.1 < 0 || proposed.1 >= GRID_LENGTH as i8 { if proposed.0 < 0
|| proposed.0 >= GRID_LENGTH as i8
|| proposed.1 < 0
|| proposed.1 >= GRID_LENGTH as i8
{
None None
} else { } else {
Some(Coordinates(proposed.0 as u8, proposed.1 as u8)) Some(Coordinates(proposed.0 as u8, proposed.1 as u8))
@ -60,8 +63,7 @@ impl Coordinates {
} }
} }
#[derive(Debug, Copy, Clone, Serialize, Deserialize, Tsify, PartialEq, Eq, Hash)] #[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[tsify(from_wasm_abi)]
pub struct Letter { pub struct Letter {
pub text: char, pub text: char,
pub points: u32, pub points: u32,
@ -81,32 +83,27 @@ impl Letter {
pub fn new(text: Option<char>, points: u32) -> Letter { pub fn new(text: Option<char>, points: u32) -> Letter {
match text { match text {
None => { None => Letter {
Letter {
text: ' ', text: ' ',
points, points,
ephemeral: true, ephemeral: true,
is_blank: true, is_blank: true,
} },
} Some(text) => Letter {
Some(text) => {
Letter {
text, text,
points, points,
ephemeral: true, ephemeral: true,
is_blank: false, is_blank: false,
} },
}
} }
} }
pub fn partial_match(&self, other: &Letter) -> bool { pub fn partial_match(&self, other: &Letter) -> bool {
self == other || (self.is_blank && other.is_blank && self.points == other.points) self == other || (self.is_blank && other.is_blank && self.points == other.points)
} }
} }
#[derive(Debug, Copy, Clone, Serialize)] #[derive(Debug, Copy, Clone, Serialize, Deserialize)]
pub enum CellType { pub enum CellType {
Normal, Normal,
DoubleWord, DoubleWord,
@ -141,12 +138,10 @@ impl<'a> ToString for Word<'a> {
} }
text text
} }
} }
impl<'a> Word<'a> { impl<'a> Word<'a> {
pub fn calculate_score(&self) -> u32 { pub fn calculate_score(&self) -> u32 {
let mut multiplier = 1; let mut multiplier = 1;
let mut unmultiplied_score = 0; let mut unmultiplied_score = 0;
@ -154,15 +149,14 @@ impl <'a> Word<'a> {
for cell in self.cells.as_slice() { for cell in self.cells.as_slice() {
let cell_value = cell.value.unwrap(); let cell_value = cell.value.unwrap();
if cell_value.ephemeral { if cell_value.ephemeral {
let cell_multiplier = let cell_multiplier = match cell.cell_type {
match cell.cell_type { CellType::Normal => 1,
CellType::Normal => {1}
CellType::DoubleWord => { CellType::DoubleWord => {
multiplier *= 2; multiplier *= 2;
1 1
} }
CellType::DoubleLetter => {2} CellType::DoubleLetter => 2,
CellType::TripleLetter => {3} CellType::TripleLetter => 3,
CellType::TripleWord => { CellType::TripleWord => {
multiplier *= 3; multiplier *= 3;
1 1
@ -187,7 +181,6 @@ impl Board {
pub fn new() -> Self { pub fn new() -> Self {
let mut cells = Vec::new(); let mut cells = Vec::new();
/// Since the board is symmetrical in both directions for the purposes of our logic we can keep our coordinates in one corner /// Since the board is symmetrical in both directions for the purposes of our logic we can keep our coordinates in one corner
/// ///
/// # Arguments /// # Arguments
@ -200,7 +193,7 @@ impl Board {
GRID_LENGTH - x - 1 GRID_LENGTH - x - 1
} else { } else {
x x
} };
} }
for i_orig in 0..GRID_LENGTH { for i_orig in 0..GRID_LENGTH {
@ -221,9 +214,7 @@ impl Board {
} }
// Double letters // Double letters
if (i % 4 == 2) && (j % 4 == 2) && !( if (i % 4 == 2) && (j % 4 == 2) && !(i == 2 && j == 2) {
i == 2 && j == 2
) {
typee = CellType::DoubleLetter; typee = CellType::DoubleLetter;
} }
if (i.min(j) == 0 && i.max(j) == 3) || (i.min(j) == 3 && i.max(j) == 7) { if (i.min(j) == 0 && i.max(j) == 3) || (i.min(j) == 3 && i.max(j) == 7) {
@ -245,7 +236,6 @@ impl Board {
value: None, value: None,
coordinates: Coordinates(j_orig, i_orig), coordinates: Coordinates(j_orig, i_orig),
}) })
} }
} }
@ -261,24 +251,28 @@ impl Board {
} }
} }
pub fn get_cell_mut(&mut self, coordinates: Coordinates) -> Result<&mut Cell, &str> { pub fn get_cell_mut(&mut self, coordinates: Coordinates) -> Result<&mut Cell, Error> {
if coordinates.0 >= GRID_LENGTH || coordinates.1 >= GRID_LENGTH { if coordinates.0 >= GRID_LENGTH || coordinates.1 >= GRID_LENGTH {
Err("x & y must be within the board's coordinates") Err(Error::Other(
"x & y must be within the board's coordinates".to_string(),
))
} else { } else {
let index = coordinates.map_to_index(); let index = coordinates.map_to_index();
Ok(self.cells.get_mut(index).unwrap()) Ok(self.cells.get_mut(index).unwrap())
} }
} }
pub fn calculate_scores(
pub fn calculate_scores(&self, dictionary: &DictionaryImpl) -> Result<(Vec<(Word, u32)>, u32), String> { &self,
dictionary: &DictionaryImpl,
) -> Result<(Vec<(Word, u32)>, u32), Error> {
let (words, tiles_played) = self.find_played_words()?; let (words, tiles_played) = self.find_played_words()?;
let mut words_and_scores = Vec::new(); let mut words_and_scores = Vec::new();
let mut total_score = 0; let mut total_score = 0;
for word in words { for word in words {
if !dictionary.contains_key(&word.to_string()) { if !dictionary.contains_key(&word.to_string()) {
return Err(format!("{} is not a valid word", word.to_string())); return Err(Error::InvalidWord(word.to_string()));
} }
let score = word.calculate_score(); let score = word.calculate_score();
@ -293,10 +287,9 @@ impl Board {
Ok((words_and_scores, total_score)) Ok((words_and_scores, total_score))
} }
pub fn find_played_words(&self) -> Result<(Vec<Word>, u8), &str> { pub fn find_played_words(&self) -> Result<(Vec<Word>, u8), Error> {
// We don't assume that the move is valid, so let's first establish that // We don't assume that the move is valid, so let's first establish that
// Let's first establish what rows and columns tiles were played in // Let's first establish what rows and columns tiles were played in
let mut rows_played = HashSet::with_capacity(15); let mut rows_played = HashSet::with_capacity(15);
let mut columns_played = HashSet::with_capacity(15); let mut columns_played = HashSet::with_capacity(15);
@ -320,9 +313,9 @@ impl Board {
} }
if rows_played.is_empty() { if rows_played.is_empty() {
return Err("Tiles need to be played") return Err(Error::NoTilesPlayed);
} else if rows_played.len() > 1 && columns_played.len() > 1 { } else if rows_played.len() > 1 && columns_played.len() > 1 {
return Err("Tiles need to be played on one row or column") return Err(Error::TilesNotStraight);
} }
let direction = if rows_played.len() > 1 { let direction = if rows_played.len() > 1 {
@ -335,7 +328,9 @@ impl Board {
let starting_column = *columns_played.iter().min().unwrap(); let starting_column = *columns_played.iter().min().unwrap();
let starting_coords = Coordinates(starting_row, starting_column); let starting_coords = Coordinates(starting_row, starting_column);
let main_word = self.find_word_at_position(starting_coords, direction).unwrap(); let main_word = self
.find_word_at_position(starting_coords, direction)
.unwrap();
let mut words = Vec::new(); let mut words = Vec::new();
let mut observed_tiles_played = 0; let mut observed_tiles_played = 0;
@ -359,25 +354,25 @@ impl Board {
// there are tiles not part of the main word // there are tiles not part of the main word
if observed_tiles_played != tiles_played { if observed_tiles_played != tiles_played {
return Err("Played tiles cannot have empty gap"); return Err(Error::TilesHaveGap);
} }
// don't want the case of a single letter word // don't want the case of a single letter word
if main_word.cells.len() > 1 { if main_word.cells.len() > 1 {
words.push(main_word); words.push(main_word);
} else if words.is_empty() { } else if words.is_empty() {
return Err("All words must be at least one letter"); return Err(Error::OneLetterWord);
} }
// need to verify that the play is 'anchored' // need to verify that the play is 'anchored'
let mut anchored = false; let mut anchored = false;
'outer: for word in words.as_slice() { 'outer: for word in words.as_slice() {
for cell in word.cells.as_slice() { for cell in word.cells.as_slice() {
// either one of the letters // either one of the letters
if !cell.value.as_ref().unwrap().ephemeral || (cell.coordinates.0 == GRID_LENGTH / 2 && cell.coordinates.1 == GRID_LENGTH / 2){ if !cell.value.as_ref().unwrap().ephemeral
|| (cell.coordinates.0 == GRID_LENGTH / 2
&& cell.coordinates.1 == GRID_LENGTH / 2)
{
anchored = true; anchored = true;
break 'outer; break 'outer;
} }
@ -387,23 +382,27 @@ impl Board {
if anchored { if anchored {
Ok((words, tiles_played)) Ok((words, tiles_played))
} else { } else {
return Err("Played tiles must be anchored to something") Err(Error::UnanchoredWord)
} }
} }
pub fn find_word_at_position(&self, mut start_coords: Coordinates, direction: Direction) -> Option<Word> { pub fn find_word_at_position(
&self,
mut start_coords: Coordinates,
direction: Direction,
) -> Option<Word> {
// let's see how far we can backtrack to the start of the word // let's see how far we can backtrack to the start of the word
let mut times_moved = 0; let mut times_moved = 0;
loop { loop {
let one_back = start_coords.add(direction, -times_moved); let one_back = start_coords.add(direction, -times_moved);
match one_back { match one_back {
None => { break } None => break,
Some(new_coords) => { Some(new_coords) => {
let cell = self.get_cell(new_coords).unwrap(); let cell = self.get_cell(new_coords).unwrap();
if cell.value.is_some() { if cell.value.is_some() {
times_moved += 1; times_moved += 1;
} else { } else {
break break;
} }
} }
} }
@ -423,11 +422,11 @@ impl Board {
loop { loop {
let position = start_coords.add(direction, cells.len() as i8); let position = start_coords.add(direction, cells.len() as i8);
match position { match position {
None => {break} None => break,
Some(x) => { Some(x) => {
let cell = self.get_cell(x).unwrap(); let cell = self.get_cell(x).unwrap();
match cell.value { match cell.value {
None => {break} None => break,
Some(_) => { Some(_) => {
cells.push(cell); cells.push(cell);
} }
@ -442,16 +441,16 @@ impl Board {
}) })
} }
pub fn receive_play(&mut self, play: Vec<(Letter, Coordinates)>) -> Result<(), String> { pub fn receive_play(&mut self, play: Vec<(Letter, Coordinates)>) -> Result<(), Error> {
for (mut letter, coords) in play { for (mut letter, coords) in play {
{ {
let cell = match self.get_cell_mut(coords) { let cell = self.get_cell_mut(coords)?;
Ok(cell) => {cell}
Err(e) => {return Err(e.to_string())}
};
if cell.value.is_some() { if cell.value.is_some() {
return Err(format!("There's already a letter at {:?}", coords)); return Err(Error::Other(format!(
"There's already a letter at {:?}",
coords
)));
} }
letter.ephemeral = true; letter.ephemeral = true;
@ -476,7 +475,6 @@ impl Board {
impl fmt::Display for Board { impl fmt::Display for Board {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let mut str = String::new(); let mut str = String::new();
let normal = "\x1b[48;5;174m\x1b[38;5;0m"; let normal = "\x1b[48;5;174m\x1b[38;5;0m";
@ -493,57 +491,82 @@ impl fmt::Display for Board {
let cell = self.get_cell(coords).unwrap(); let cell = self.get_cell(coords).unwrap();
let color = match cell.cell_type { let color = match cell.cell_type {
CellType::Normal => {normal} CellType::Normal => normal,
CellType::DoubleWord => {double_word} CellType::DoubleWord => double_word,
CellType::DoubleLetter => {double_letter} CellType::DoubleLetter => double_letter,
CellType::TripleLetter => {triple_letter} CellType::TripleLetter => triple_letter,
CellType::TripleWord => {triple_word} CellType::TripleWord => triple_word,
CellType::Start => {double_word} CellType::Start => double_word,
}; };
let content = match &cell.value { let content = match &cell.value {
None => {' '} None => ' ',
Some(letter) => {letter.text} Some(letter) => letter.text,
}; };
str.write_str(color).unwrap(); str.write_str(color).unwrap();
str.write_char(content).unwrap(); str.write_char(content).unwrap();
} }
str.write_str("\x1b[0m\n").unwrap(); str.write_str("\x1b[0m\n").unwrap();
} }
write!(f, "{}", str) write!(f, "{}", str)
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::dictionary::Dictionary;
use super::*; use super::*;
use crate::dictionary::Dictionary;
#[test] #[test]
fn test_cell_types() { fn test_cell_types() {
let board = Board::new(); let board = Board::new();
assert!(matches!(board.get_cell(Coordinates(0, 0)).unwrap().cell_type, CellType::TripleWord)); assert!(matches!(
assert!(matches!(board.get_cell(Coordinates(1, 0)).unwrap().cell_type, CellType::Normal)); board.get_cell(Coordinates(0, 0)).unwrap().cell_type,
assert!(matches!(board.get_cell(Coordinates(0, 1)).unwrap().cell_type, CellType::Normal)); CellType::TripleWord
assert!(matches!(board.get_cell(Coordinates(1, 1)).unwrap().cell_type, CellType::DoubleWord)); ));
assert!(matches!(
board.get_cell(Coordinates(1, 0)).unwrap().cell_type,
CellType::Normal
));
assert!(matches!(
board.get_cell(Coordinates(0, 1)).unwrap().cell_type,
CellType::Normal
));
assert!(matches!(
board.get_cell(Coordinates(1, 1)).unwrap().cell_type,
CellType::DoubleWord
));
assert!(matches!(board.get_cell(Coordinates(13, 13)).unwrap().cell_type, CellType::DoubleWord)); assert!(matches!(
assert!(matches!(board.get_cell(Coordinates(14, 14)).unwrap().cell_type, CellType::TripleWord)); board.get_cell(Coordinates(13, 13)).unwrap().cell_type,
assert!(matches!(board.get_cell(Coordinates(11, 14)).unwrap().cell_type, CellType::DoubleLetter)); CellType::DoubleWord
));
assert!(matches!(
board.get_cell(Coordinates(14, 14)).unwrap().cell_type,
CellType::TripleWord
));
assert!(matches!(
board.get_cell(Coordinates(11, 14)).unwrap().cell_type,
CellType::DoubleLetter
));
assert!(matches!(board.get_cell(Coordinates(7, 7)).unwrap().cell_type, CellType::Start)); assert!(matches!(
assert!(matches!(board.get_cell(Coordinates(8, 6)).unwrap().cell_type, CellType::DoubleLetter)); board.get_cell(Coordinates(7, 7)).unwrap().cell_type,
assert!(matches!(board.get_cell(Coordinates(5, 9)).unwrap().cell_type, CellType::TripleLetter)); CellType::Start
));
assert!(matches!(
board.get_cell(Coordinates(8, 6)).unwrap().cell_type,
CellType::DoubleLetter
));
assert!(matches!(
board.get_cell(Coordinates(5, 9)).unwrap().cell_type,
CellType::TripleLetter
));
} }
#[test] #[test]
fn test_cell_coordinates() { fn test_cell_coordinates() {
let board = Board::new(); let board = Board::new();
@ -576,7 +599,6 @@ mod tests {
board.get_cell_mut(Coordinates(5, 0)).unwrap().value = Some(Letter::new_fixed('O', 0)); board.get_cell_mut(Coordinates(5, 0)).unwrap().value = Some(Letter::new_fixed('O', 0));
board.get_cell_mut(Coordinates(6, 0)).unwrap().value = Some(Letter::new_fixed('L', 0)); board.get_cell_mut(Coordinates(6, 0)).unwrap().value = Some(Letter::new_fixed('L', 0));
board.get_cell_mut(Coordinates(9, 8)).unwrap().value = Some(Letter::new_fixed('G', 0)); board.get_cell_mut(Coordinates(9, 8)).unwrap().value = Some(Letter::new_fixed('G', 0));
board.get_cell_mut(Coordinates(10, 8)).unwrap().value = Some(Letter::new_fixed('G', 0)); board.get_cell_mut(Coordinates(10, 8)).unwrap().value = Some(Letter::new_fixed('G', 0));
@ -584,7 +606,9 @@ mod tests {
println!("x is {}", x); println!("x is {}", x);
let first_word = board.find_word_at_position(Coordinates(8, x), Direction::Column); let first_word = board.find_word_at_position(Coordinates(8, x), Direction::Column);
match first_word { match first_word {
None => { panic!("Expected to find word JOEL") } None => {
panic!("Expected to find word JOEL")
}
Some(x) => { Some(x) => {
assert_eq!(x.coords.0, 8); assert_eq!(x.coords.0, 8);
assert_eq!(x.coords.1, 6); assert_eq!(x.coords.1, 6);
@ -596,7 +620,9 @@ mod tests {
let single_letter_word = board.find_word_at_position(Coordinates(8, 9), Direction::Row); let single_letter_word = board.find_word_at_position(Coordinates(8, 9), Direction::Row);
match single_letter_word { match single_letter_word {
None => { panic!("Expected to find letter L") } None => {
panic!("Expected to find letter L")
}
Some(x) => { Some(x) => {
assert_eq!(x.coords.0, 8); assert_eq!(x.coords.0, 8);
assert_eq!(x.coords.1, 9); assert_eq!(x.coords.1, 9);
@ -609,7 +635,9 @@ mod tests {
println!("x is {}", x); println!("x is {}", x);
let word = board.find_word_at_position(Coordinates(x, 0), Direction::Row); let word = board.find_word_at_position(Coordinates(x, 0), Direction::Row);
match word { match word {
None => { panic!("Expected to find word IS") } None => {
panic!("Expected to find word IS")
}
Some(x) => { Some(x) => {
assert_eq!(x.coords.0, 0); assert_eq!(x.coords.0, 0);
assert_eq!(x.coords.1, 0); assert_eq!(x.coords.1, 0);
@ -623,7 +651,9 @@ mod tests {
println!("x is {}", x); println!("x is {}", x);
let word = board.find_word_at_position(Coordinates(x, 0), Direction::Row); let word = board.find_word_at_position(Coordinates(x, 0), Direction::Row);
match word { match word {
None => { panic!("Expected to find word COOL") } None => {
panic!("Expected to find word COOL")
}
Some(x) => { Some(x) => {
assert_eq!(x.coords.0, 3); assert_eq!(x.coords.0, 3);
assert_eq!(x.coords.1, 0); assert_eq!(x.coords.1, 0);
@ -638,7 +668,9 @@ mod tests {
let word = board.find_word_at_position(Coordinates(10, 8), Direction::Row); let word = board.find_word_at_position(Coordinates(10, 8), Direction::Row);
match word { match word {
None => { panic!("Expected to find word EGG") } None => {
panic!("Expected to find word EGG")
}
Some(x) => { Some(x) => {
assert_eq!(x.coords.0, 8); assert_eq!(x.coords.0, 8);
assert_eq!(x.coords.1, 8); assert_eq!(x.coords.1, 8);
@ -659,10 +691,10 @@ mod tests {
is_blank: false, is_blank: false,
}); });
match board.find_played_words() { assert!(matches!(
Ok(_) => {panic!("Expected error")} board.find_played_words(),
Err(e) => {assert_eq!(e, "All words must be at least one letter");} Err(Error::OneLetterWord)
} ));
board.get_cell_mut(Coordinates(7, 7)).unwrap().value = Some(Letter { board.get_cell_mut(Coordinates(7, 7)).unwrap().value = Some(Letter {
text: 'I', text: 'I',
@ -726,10 +758,7 @@ mod tests {
board.get_cell_mut(Coordinates(8, 9)).unwrap().value = Some(make_letter('L', true)); board.get_cell_mut(Coordinates(8, 9)).unwrap().value = Some(make_letter('L', true));
let words = board.find_played_words(); let words = board.find_played_words();
match words { assert!(matches!(words, Err(Error::UnanchoredWord)));
Ok(_) => {panic!("Expected the not-anchored error")}
Err(x) => {assert_eq!(x, "Played tiles must be anchored to something")}
}
// Adding anchor // Adding anchor
board.get_cell_mut(Coordinates(7, 6)).unwrap().value = Some(make_letter('I', false)); board.get_cell_mut(Coordinates(7, 6)).unwrap().value = Some(make_letter('I', false));
@ -746,7 +775,6 @@ mod tests {
assert!(board.find_played_words().is_ok()); assert!(board.find_played_words().is_ok());
} }
#[test] #[test]
fn test_word_finding_with_break() { fn test_word_finding_with_break() {
// Verify that if I play my tiles on one row or column but with a break in-between I get an error // Verify that if I play my tiles on one row or column but with a break in-between I get an error
@ -771,15 +799,8 @@ mod tests {
board.get_cell_mut(Coordinates(8, 12)).unwrap().value = Some(Letter::new_fixed('S', 0)); board.get_cell_mut(Coordinates(8, 12)).unwrap().value = Some(Letter::new_fixed('S', 0));
let words = board.find_played_words(); let words = board.find_played_words();
match words { assert!(matches!(words, Err(Error::TilesHaveGap)));
Ok(_) => {panic!("Expected to find an error!")}
Err(x) => {
assert_eq!(x, "Played tiles cannot have empty gap")
} }
}
}
#[test] #[test]
fn test_word_finding_whole_board() { fn test_word_finding_whole_board() {
@ -795,10 +816,7 @@ mod tests {
} }
let words = board.find_played_words(); let words = board.find_played_words();
match words { assert!(matches!(words, Err(Error::NoTilesPlayed)));
Ok(_) => {panic!("Expected to find no words")}
Err(x) => {assert_eq!(x, "Tiles need to be played")}
}
board.get_cell_mut(Coordinates(8, 6)).unwrap().value = Some(Letter::new_fixed('J', 8)); board.get_cell_mut(Coordinates(8, 6)).unwrap().value = Some(Letter::new_fixed('J', 8));
board.get_cell_mut(Coordinates(8, 7)).unwrap().value = Some(make_letter('O', true, 1)); board.get_cell_mut(Coordinates(8, 7)).unwrap().value = Some(make_letter('O', true, 1));
@ -814,7 +832,7 @@ mod tests {
board.get_cell_mut(Coordinates(6, 0)).unwrap().value = Some(Letter::new_fixed('L', 1)); board.get_cell_mut(Coordinates(6, 0)).unwrap().value = Some(Letter::new_fixed('L', 1));
fn check_board(board: &mut Board, inverted: bool) { fn check_board(board: &mut Board, inverted: bool) {
let dictionary = DictionaryImpl::create_from_path("resources/dictionary.csv"); let dictionary = DictionaryImpl::create_from_path("../resources/dictionary.csv");
println!("{}", board); println!("{}", board);
let words = board.find_played_words(); let words = board.find_played_words();
match words { match words {
@ -827,7 +845,9 @@ mod tests {
assert_eq!(word.calculate_score(), 8 + 1 + 2 + 1); assert_eq!(word.calculate_score(), 8 + 1 + 2 + 1);
} }
Err(e) => { panic!("Expected to find a word to play; found error {}", e) } Err(e) => {
panic!("Expected to find a word to play; found error {}", e)
}
} }
let maybe_invert = |coords: Coordinates| { let maybe_invert = |coords: Coordinates| {
@ -844,12 +864,21 @@ mod tests {
return direction; return direction;
}; };
board.get_cell_mut(maybe_invert(Coordinates(9, 8))).unwrap().value = Some(Letter::new_fixed('G', 2)); board
board.get_cell_mut(maybe_invert(Coordinates(10, 8))).unwrap().value = Some(Letter::new_fixed('G', 2)); .get_cell_mut(maybe_invert(Coordinates(9, 8)))
.unwrap()
.value = Some(Letter::new_fixed('G', 2));
board
.get_cell_mut(maybe_invert(Coordinates(10, 8)))
.unwrap()
.value = Some(Letter::new_fixed('G', 2));
let word = board.find_word_at_position(Coordinates(8, 8), maybe_invert_direction(Direction::Row)); let word = board
.find_word_at_position(Coordinates(8, 8), maybe_invert_direction(Direction::Row));
match word { match word {
None => {panic!("Expected to find word EGG")} None => {
panic!("Expected to find word EGG")
}
Some(x) => { Some(x) => {
assert_eq!(x.coords.0, 8); assert_eq!(x.coords.0, 8);
assert_eq!(x.coords.1, 8); assert_eq!(x.coords.1, 8);
@ -857,7 +886,6 @@ mod tests {
assert_eq!(x.to_string(), "EGG"); assert_eq!(x.to_string(), "EGG");
assert_eq!(x.calculate_score(), 2 + 2 + 2); assert_eq!(x.calculate_score(), 2 + 2 + 2);
assert!(dictionary.is_word_valid(&x)); assert!(dictionary.is_word_valid(&x));
} }
} }
@ -876,20 +904,29 @@ mod tests {
assert_eq!(word.calculate_score(), 8 + 1 + 2 + 1); assert_eq!(word.calculate_score(), 8 + 1 + 2 + 1);
assert!(!dictionary.is_word_valid(word)); assert!(!dictionary.is_word_valid(word));
} }
Err(e) => { panic!("Expected to find a word to play; found error {}", e) } Err(e) => {
panic!("Expected to find a word to play; found error {}", e)
}
} }
let scores = board.calculate_scores(&dictionary); let scores = board.calculate_scores(&dictionary);
match scores { match scores {
Ok(_) => {panic!("Expected an error")} Ok(_) => {
Err(e) => {assert_eq!(e, "JOEL is not a valid word")} panic!("Expected an error")
}
Err(e) => {
if let Error::InvalidWord(w) = e {
assert_eq!(w, "JOEL");
} else {
panic!("Expected an InvalidPlay error")
}
}
} }
let mut alt_dictionary = DictionaryImpl::new(); let mut alt_dictionary = DictionaryImpl::new();
alt_dictionary.insert("JOEL".to_string(), 0.5); alt_dictionary.insert("JOEL".to_string(), 0.5);
alt_dictionary.insert("EGG".to_string(), 0.5); alt_dictionary.insert("EGG".to_string(), 0.5);
let scores = board.calculate_scores(&alt_dictionary); let scores = board.calculate_scores(&alt_dictionary);
match scores { match scores {
Ok((words, total_score)) => { Ok((words, total_score)) => {
@ -906,18 +943,19 @@ mod tests {
assert_eq!(total_score, 18); assert_eq!(total_score, 18);
} }
Err(e) => {panic!("Wasn't expecting to encounter error {e}")} Err(e) => {
panic!("Wasn't expecting to encounter error {e}")
}
} }
// replace one of the 'G' in EGG with an ephemeral to trigger an error // replace one of the 'G' in EGG with an ephemeral to trigger an error
board.get_cell_mut(maybe_invert(Coordinates(9, 8))).unwrap().value = Some(make_letter('G', true, 2)); board
.get_cell_mut(maybe_invert(Coordinates(9, 8)))
.unwrap()
.value = Some(make_letter('G', true, 2));
let words = board.find_played_words(); let words = board.find_played_words();
match words { assert!(matches!(words, Err(Error::TilesNotStraight)));
Ok(_) => { panic!("Expected error as we played tiles in multiple rows and columns") }
Err(e) => { assert_eq!(e, "Tiles need to be played on one row or column") }
}
} }
// make a copy of the board now with x and y swapped // make a copy of the board now with x and y swapped
@ -934,7 +972,6 @@ mod tests {
cell_new.value = Some(*x); cell_new.value = Some(*x);
} }
} }
} }
} }
@ -943,8 +980,5 @@ mod tests {
println!("Checking inverted board"); println!("Checking inverted board");
check_board(&mut inverted_board, true); check_board(&mut inverted_board, true);
} }
} }

View file

@ -1,6 +1,6 @@
use rand::prelude::SliceRandom;
use rand::{Rng};
use crate::board::Letter; use crate::board::Letter;
use rand::prelude::SliceRandom;
use rand::Rng;
pub const GRID_LENGTH: u8 = 15; pub const GRID_LENGTH: u8 = 15;
pub const TRAY_LENGTH: u8 = 7; pub const TRAY_LENGTH: u8 = 7;
@ -49,5 +49,4 @@ pub fn standard_tile_pool<R: Rng>(rng: Option<&mut R>) -> Vec<Letter> {
} }
letters letters
} }

View file

@ -1,10 +1,9 @@
use crate::board::Word;
use csv::Reader;
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use std::str::FromStr; use std::str::FromStr;
use csv::Reader;
use crate::board::Word;
pub trait Dictionary { pub trait Dictionary {
fn create_from_reader<T: std::io::Read>(reader: Reader<T>) -> Self; fn create_from_reader<T: std::io::Read>(reader: Reader<T>) -> Self;
fn create_from_path(path: &str) -> Self; fn create_from_path(path: &str) -> Self;
fn create_from_str(data: &str) -> Self; fn create_from_str(data: &str) -> Self;
@ -15,7 +14,6 @@ pub trait Dictionary {
pub type DictionaryImpl = HashMap<String, f64>; pub type DictionaryImpl = HashMap<String, f64>;
impl Dictionary for DictionaryImpl { impl Dictionary for DictionaryImpl {
fn create_from_reader<T: std::io::Read>(mut reader: Reader<T>) -> Self { fn create_from_reader<T: std::io::Read>(mut reader: Reader<T>) -> Self {
let mut map = HashMap::new(); let mut map = HashMap::new();
@ -27,7 +25,6 @@ impl Dictionary for DictionaryImpl{
let score = f64::from_str(score).unwrap(); let score = f64::from_str(score).unwrap();
map.insert(word, score); map.insert(word, score);
} }
map map
@ -57,7 +54,6 @@ impl Dictionary for DictionaryImpl{
} }
map map
} }
fn substring_set(&self) -> HashSet<&str> { fn substring_set(&self) -> HashSet<&str> {
@ -69,7 +65,6 @@ impl Dictionary for DictionaryImpl{
set.insert(&word[j..k]); set.insert(&word[j..k]);
} }
} }
} }
set set
@ -85,10 +80,9 @@ impl Dictionary for DictionaryImpl{
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn test_dictionary() { fn test_dictionary() {
let dictionary = HashMap::create_from_path("resources/dictionary.csv"); let dictionary = HashMap::create_from_path("../resources/dictionary.csv");
assert_eq!(dictionary.len(), 279429); assert_eq!(dictionary.len(), 279429);
@ -96,7 +90,6 @@ mod tests {
assert!(dictionary.contains_key("AARDVARK")); assert!(dictionary.contains_key("AARDVARK"));
assert!((dictionary.get("AARDVARK").unwrap() - 0.5798372).abs() < 0.0001) assert!((dictionary.get("AARDVARK").unwrap() - 0.5798372).abs() < 0.0001)
} }
#[test] #[test]
@ -131,11 +124,8 @@ mod tests {
assert!(set.contains("JOH")); assert!(set.contains("JOH"));
assert!(set.contains("OHN")); assert!(set.contains("OHN"));
assert!(!set.contains("XY")); assert!(!set.contains("XY"));
assert!(!set.contains("JH")); assert!(!set.contains("JH"));
assert!(!set.contains("JE")); assert!(!set.contains("JE"));
} }
} }

View file

@ -1,15 +1,15 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::fmt::{Display, Formatter};
use rand::prelude::SliceRandom;
use rand::rngs::SmallRng;
use rand::SeedableRng;
use serde::{Deserialize, Serialize};
use tsify::Tsify;
use crate::board::{Board, Coordinates, Letter}; use crate::board::{Board, Coordinates, Letter};
use crate::constants::{standard_tile_pool, TRAY_LENGTH}; use crate::constants::{standard_tile_pool, TRAY_LENGTH};
use crate::dictionary::{Dictionary, DictionaryImpl}; use crate::dictionary::{Dictionary, DictionaryImpl};
use crate::player_interaction::ai::{AI, CompleteMove, Difficulty}; use crate::player_interaction::ai::{Difficulty, AI};
use crate::player_interaction::Tray; use crate::player_interaction::Tray;
use rand::prelude::SliceRandom;
use rand::rngs::SmallRng;
use rand::SeedableRng;
use serde::{Deserialize, Serialize, Serializer};
pub enum Player { pub enum Player {
Human(String), Human(String),
@ -17,15 +17,74 @@ pub enum Player {
name: String, name: String,
difficulty: Difficulty, difficulty: Difficulty,
object: AI, object: AI,
},
}
#[derive(Debug)]
pub enum Error {
InvalidPlayer(String),
WrongTurn(String),
Other(String),
InvalidWord(String),
GameFinished,
NoTilesPlayed,
TilesNotStraight,
TilesHaveGap,
OneLetterWord,
UnanchoredWord,
}
impl Display for Error {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match &self {
Error::InvalidPlayer(player) => {
write!(f, "{player} doesn't exist")
}
Error::WrongTurn(player) => {
write!(f, "It's not {player}'s turn")
}
Error::Other(msg) => {
write!(f, "Other error: {msg}")
}
Error::InvalidWord(word) => {
write!(f, "{word} is not a valid word")
}
Error::GameFinished => {
write!(f, "Moves cannot be made after a game has finished")
}
Error::NoTilesPlayed => {
write!(f, "Tiles need to be played")
}
Error::TilesNotStraight => {
write!(f, "Tiles need to be played on one row or column")
}
Error::TilesHaveGap => {
write!(f, "Played tiles cannot have empty gap")
}
Error::OneLetterWord => {
write!(f, "All words must be at least two letters")
}
Error::UnanchoredWord => {
write!(f, "Played tiles must be anchored to something")
}
}
}
}
impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
} }
} }
impl Player { impl Player {
pub fn get_name(&self) -> &str { pub fn get_name(&self) -> &str {
match &self { match &self {
Player::Human(name) => {name} Player::Human(name) => name,
Player::AI { name, .. } => {name} Player::AI { name, .. } => name,
} }
} }
} }
@ -33,18 +92,20 @@ impl Player {
pub struct PlayerState { pub struct PlayerState {
pub player: Player, pub player: Player,
pub score: u32, pub score: u32,
pub tray: Tray pub tray: Tray,
} }
#[derive(Deserialize, Tsify, Copy, Clone, Debug)] #[derive(Deserialize, Copy, Clone, Debug)]
#[tsify(from_wasm_abi)]
pub struct PlayedTile { pub struct PlayedTile {
pub index: usize, pub index: usize,
pub character: Option<char>, // we only set this if PlayedTile is a blank pub character: Option<char>, // we only set this if PlayedTile is a blank
} }
impl PlayedTile { impl PlayedTile {
pub fn convert_tray(tray_tile_locations: &Vec<Option<PlayedTile>>, tray: &Tray) -> Vec<(Letter, Coordinates)> { pub fn convert_tray(
tray_tile_locations: &Vec<Option<PlayedTile>>,
tray: &Tray,
) -> Vec<(Letter, Coordinates)> {
let mut played_letters: Vec<(Letter, Coordinates)> = Vec::new(); let mut played_letters: Vec<(Letter, Coordinates)> = Vec::new();
for (i, played_tile) in tray_tile_locations.iter().enumerate() { for (i, played_tile) in tray_tile_locations.iter().enumerate() {
if played_tile.is_some() { if played_tile.is_some() {
@ -55,7 +116,9 @@ impl PlayedTile {
if letter.is_blank { if letter.is_blank {
match played_tile.character { match played_tile.character {
None => { None => {
panic!("You can't play a blank character without providing a letter value") panic!(
"You can't play a blank character without providing a letter value"
)
} }
Some(x) => { Some(x) => {
// TODO - check that x is a valid alphabet letter // TODO - check that x is a valid alphabet letter
@ -64,7 +127,6 @@ impl PlayedTile {
} }
} }
played_letters.push((letter, coord)); played_letters.push((letter, coord));
} }
} }
@ -72,15 +134,13 @@ impl PlayedTile {
} }
} }
#[derive(Debug, Serialize, Deserialize, Tsify)] #[derive(Clone, Debug, Serialize, Deserialize)]
#[tsify(from_wasm_abi)]
pub struct WordResult { pub struct WordResult {
word: String, word: String,
score: u32, score: u32,
} }
#[derive(Debug, Serialize, Deserialize, Tsify)] #[derive(Clone, Debug, Serialize, Deserialize)]
#[tsify(from_wasm_abi)]
pub struct ScoreResult { pub struct ScoreResult {
words: Vec<WordResult>, words: Vec<WordResult>,
total: u32, total: u32,
@ -88,7 +148,6 @@ pub struct ScoreResult {
pub struct PlayerStates(pub Vec<PlayerState>); pub struct PlayerStates(pub Vec<PlayerState>);
impl PlayerStates { impl PlayerStates {
fn get_player_name_by_turn_id(&self, id: usize) -> &str { fn get_player_name_by_turn_id(&self, id: usize) -> &str {
let id_mod = id % self.0.len(); let id_mod = id % self.0.len();
let state = self.0.get(id_mod).unwrap(); let state = self.0.get(id_mod).unwrap();
@ -97,37 +156,33 @@ impl PlayerStates {
} }
pub fn get_player_state(&self, name: &str) -> Option<&PlayerState> { pub fn get_player_state(&self, name: &str) -> Option<&PlayerState> {
self.0.iter() self.0
.iter()
.filter(|state| state.player.get_name().eq(name)) .filter(|state| state.player.get_name().eq(name))
.nth(0) .nth(0)
} }
pub fn get_player_state_mut(&mut self, name: &str) -> Option<&mut PlayerState> { pub fn get_player_state_mut(&mut self, name: &str) -> Option<&mut PlayerState> {
self.0.iter_mut() self.0
.iter_mut()
.filter(|state| state.player.get_name().eq(name)) .filter(|state| state.player.get_name().eq(name))
.nth(0) .nth(0)
} }
pub fn get_tray(&self, name: &str) -> Option<&Tray> { pub fn get_tray(&self, name: &str) -> Option<&Tray> {
let player = self.get_player_state(name)?; let player = self.get_player_state(name)?;
Some(&player.tray) Some(&player.tray)
} }
pub fn get_tray_mut(&mut self, name: &str) -> Option<&mut Tray> { pub fn get_tray_mut(&mut self, name: &str) -> Option<&mut Tray> {
let player = self.get_player_state_mut(name)?; let player = self.get_player_state_mut(name)?;
Some(&mut player.tray) Some(&mut player.tray)
} }
} }
#[derive(Deserialize, Serialize, Debug, Clone)]
#[derive(Deserialize, Serialize, Tsify, Debug, Clone)]
#[tsify(from_wasm_abi)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum GameState { pub enum GameState {
InProgress, InProgress,
@ -148,15 +203,20 @@ pub struct Game{
state: GameState, state: GameState,
} }
impl Game { impl Game {
pub fn new(seed: u64, dictionary_text: &str, player_names: Vec<String>, ai_difficulties: Vec<Difficulty>) -> Self { pub fn new(
seed: u64,
dictionary_text: &str,
player_names: Vec<String>,
ai_difficulties: Vec<Difficulty>,
) -> Self {
let mut rng = SmallRng::seed_from_u64(seed); let mut rng = SmallRng::seed_from_u64(seed);
let mut letters = standard_tile_pool(Some(&mut rng)); let mut letters = standard_tile_pool(Some(&mut rng));
let dictionary = DictionaryImpl::create_from_str(dictionary_text); let dictionary = DictionaryImpl::create_from_str(dictionary_text);
let mut player_states: Vec<PlayerState> = player_names.iter() let mut player_states: Vec<PlayerState> = player_names
.iter()
.map(|name| { .map(|name| {
let mut tray = Tray::new(TRAY_LENGTH); let mut tray = Tray::new(TRAY_LENGTH);
tray.fill(&mut letters); tray.fill(&mut letters);
@ -204,9 +264,9 @@ impl Game {
} }
} }
pub fn get_board(&self) -> &Board {
&self.board
pub fn get_board(&self) -> &Board {&self.board} }
pub fn set_board(&mut self, new_board: Board) { pub fn set_board(&mut self, new_board: Board) {
self.board = new_board; self.board = new_board;
@ -223,14 +283,18 @@ impl Game {
&self.dictionary &self.dictionary
} }
fn verify_game_in_progress(&self) -> Result<(), String> { fn verify_game_in_progress(&self) -> Result<(), Error> {
if !matches!(self.state, GameState::InProgress) { if !matches!(self.state, GameState::InProgress) {
return Err("Moves cannot be made after a game has finished".to_string()); return Err(Error::GameFinished);
} }
Ok(()) Ok(())
} }
pub fn receive_play(&mut self, tray_tile_locations: Vec<Option<PlayedTile>>, commit_move: bool) -> Result<(TurnAction, GameState), String> { pub fn receive_play(
&mut self,
tray_tile_locations: Vec<Option<PlayedTile>>,
commit_move: bool,
) -> Result<(TurnAction, GameState), Error> {
self.verify_game_in_progress()?; self.verify_game_in_progress()?;
let player = self.current_player_name(); let player = self.current_player_name();
@ -238,7 +302,8 @@ impl Game {
let mut board_instance = self.get_board().clone(); let mut board_instance = self.get_board().clone();
let mut tray = self.player_states.get_tray(&player).unwrap().clone(); let mut tray = self.player_states.get_tray(&player).unwrap().clone();
let played_letters: Vec<(Letter, Coordinates)> = PlayedTile::convert_tray(&tray_tile_locations, &tray); let played_letters: Vec<(Letter, Coordinates)> =
PlayedTile::convert_tray(&tray_tile_locations, &tray);
for (i, played_tile) in tray_tile_locations.iter().enumerate() { for (i, played_tile) in tray_tile_locations.iter().enumerate() {
if played_tile.is_some() { if played_tile.is_some() {
*tray.letters.get_mut(i).unwrap() = None; *tray.letters.get_mut(i).unwrap() = None;
@ -247,17 +312,14 @@ impl Game {
board_instance.receive_play(played_letters)?; board_instance.receive_play(played_letters)?;
let x = board_instance.calculate_scores(self.get_dictionary())?; let x = board_instance.calculate_scores(self.get_dictionary())?;
let total_score = x.1; let total_score = x.1;
let words: Vec<WordResult> = x.0.iter() let words: Vec<WordResult> =
.map(|(word, score)| { x.0.iter()
WordResult { .map(|(word, score)| WordResult {
word: word.to_string(), word: word.to_string(),
score: *score score: *score,
}
}) })
.collect(); .collect();
@ -276,28 +338,42 @@ impl Game {
// game is over // game is over
self.end_game(Some(player)); self.end_game(Some(player));
} }
} }
Ok((TurnAction::PlayTiles { let locations = tray_tile_locations
.iter()
.filter_map(|x| x.as_ref())
.map(|x| x.index)
.collect::<Vec<usize>>();
Ok((
TurnAction::PlayTiles {
result: ScoreResult { result: ScoreResult {
words, words,
total: total_score, total: total_score,
}, },
}, self.state.clone())) locations,
},
self.get_state(),
))
} }
pub fn exchange_tiles(&mut self, tray_tile_locations: Vec<bool>) -> Result<(Tray, TurnAction, GameState), String> { pub fn exchange_tiles(
&mut self,
tray_tile_locations: Vec<bool>,
) -> Result<(Tray, TurnAction, GameState), Error> {
self.verify_game_in_progress()?; self.verify_game_in_progress()?;
let player = self.current_player_name(); let player = self.current_player_name();
let tray = match self.player_states.get_tray_mut(&player) { let tray = match self.player_states.get_tray_mut(&player) {
None => {return Err(format!("Player {} not found", player))} None => return Err(Error::InvalidPlayer(player)),
Some(x) => {x} Some(x) => x,
}; };
if tray.letters.len() != tray_tile_locations.len() { if tray.letters.len() != tray_tile_locations.len() {
return Err("Incoming tray and existing tray have different lengths".to_string()); return Err(Error::Other(
"Incoming tray and existing tray have different lengths".to_string(),
));
} }
let tile_pool = &mut self.tile_pool; let tile_pool = &mut self.tile_pool;
@ -321,8 +397,11 @@ impl Game {
let state = self.increment_turn(false); let state = self.increment_turn(false);
Ok((tray, TurnAction::ExchangeTiles { tiles_exchanged }, state.clone())) Ok((
tray,
TurnAction::ExchangeTiles { tiles_exchanged },
state.clone(),
))
} }
pub fn add_word(&mut self, word: String) { pub fn add_word(&mut self, word: String) {
@ -331,7 +410,7 @@ impl Game {
self.dictionary.insert(word, -1.0); self.dictionary.insert(word, -1.0);
} }
pub fn pass(&mut self) -> Result<GameState, String> { pub fn pass(&mut self) -> Result<GameState, Error> {
self.verify_game_in_progress()?; self.verify_game_in_progress()?;
Ok(self.increment_turn(false).clone()) Ok(self.increment_turn(false).clone())
} }
@ -345,7 +424,6 @@ impl Game {
if self.turns_not_played >= 2 * self.player_states.0.len() { if self.turns_not_played >= 2 * self.player_states.0.len() {
self.end_game(None); self.end_game(None);
} }
} else { } else {
self.turns_not_played = 0; self.turns_not_played = 0;
} }
@ -354,7 +432,6 @@ impl Game {
} }
fn end_game(&mut self, finisher: Option<String>) { fn end_game(&mut self, finisher: Option<String>) {
let mut finished_letters_map = HashMap::new(); let mut finished_letters_map = HashMap::new();
let mut points_forfeit = 0; let mut points_forfeit = 0;
@ -376,23 +453,28 @@ impl Game {
} }
if let Some(finisher) = &finisher { if let Some(finisher) = &finisher {
let mut state = self.player_states.get_player_state_mut(finisher).unwrap(); let state = self.player_states.get_player_state_mut(finisher).unwrap();
state.score += points_forfeit; state.score += points_forfeit;
} }
self.state = GameState::Ended { self.state = GameState::Ended {
finisher, finisher,
remaining_tiles: finished_letters_map remaining_tiles: finished_letters_map,
}; };
} }
pub fn current_player_name(&self) -> String { pub fn current_player_name(&self) -> String {
self.player_states.get_player_name_by_turn_id(self.turn_order).to_string() self.player_states
.get_player_name_by_turn_id(self.turn_order)
.to_string()
} }
pub fn advance_turn(&mut self) -> Result<(TurnAdvanceResult, GameState), String> { pub fn advance_turn(&mut self) -> Result<(TurnAdvanceResult, GameState), Error> {
let current_player = self.current_player_name(); let current_player = self.current_player_name();
let state = self.player_states.get_player_state_mut(&current_player).ok_or("There should be a player available")?; let state = self
.player_states
.get_player_state_mut(&current_player)
.ok_or(Error::InvalidPlayer(current_player.clone()))?;
if let Player::AI { object, .. } = &mut state.player { if let Player::AI { object, .. } = &mut state.player {
let tray = &mut state.tray; let tray = &mut state.tray;
@ -407,7 +489,7 @@ impl Game {
match tile_spot { match tile_spot {
None => { None => {
to_exchange.push(false); to_exchange.push(false);
}, }
Some(_) => { Some(_) => {
to_exchange.push(true); to_exchange.push(true);
} }
@ -416,34 +498,44 @@ impl Game {
if self.tile_pool.is_empty() { if self.tile_pool.is_empty() {
let game_state = self.increment_turn(false); let game_state = self.increment_turn(false);
Ok((TurnAdvanceResult::AIMove { Ok((
TurnAdvanceResult::AIMove {
name: current_player, name: current_player,
action: TurnAction::Pass, action: TurnAction::Pass,
}, game_state.clone())) },
game_state.clone(),
))
} else { } else {
let (_, action, game_state) = self.exchange_tiles(to_exchange)?; let (_, action, game_state) = self.exchange_tiles(to_exchange)?;
Ok((TurnAdvanceResult::AIMove { Ok((
TurnAdvanceResult::AIMove {
name: current_player, name: current_player,
action, action,
}, game_state)) },
game_state,
))
} }
} }
Some(best_move) => { Some(best_move) => {
let play = best_move.convert_to_play(tray); let play = best_move.convert_to_play(tray);
let (action, game_state) = self.receive_play(play, true)?; let (action, game_state) = self.receive_play(play, true)?;
Ok((TurnAdvanceResult::AIMove { Ok((
TurnAdvanceResult::AIMove {
name: current_player, name: current_player,
action, action,
}, game_state)) },
game_state,
))
} }
} }
} else { } else {
Ok((TurnAdvanceResult::HumanInputRequired{name: self.current_player_name()}, self.state.clone())) Ok((
TurnAdvanceResult::HumanInputRequired {
name: self.current_player_name(),
},
self.get_state(),
))
} }
} }
pub fn get_remaining_tiles(&self) -> usize { pub fn get_remaining_tiles(&self) -> usize {
@ -452,60 +544,61 @@ impl Game {
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)),
Some(x) => {x} Some(x) => x,
}; };
Ok( Ok(tray.count())
tray.count()
)
} }
pub fn get_state(&self) -> GameState {
self.state.clone()
}
} }
#[derive(Serialize, Deserialize, Tsify, Debug)] #[derive(Serialize, Deserialize, Debug, Clone)]
#[tsify(from_wasm_abi)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum TurnAction { pub enum TurnAction {
Pass, Pass,
ExchangeTiles { ExchangeTiles {
tiles_exchanged: usize tiles_exchanged: usize,
}, },
PlayTiles { PlayTiles {
result: ScoreResult result: ScoreResult,
locations: Vec<usize>,
}, },
} }
#[derive(Serialize, Deserialize, Tsify, Debug)] #[derive(Serialize, Deserialize, Debug)]
#[tsify(from_wasm_abi)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum TurnAdvanceResult { pub enum TurnAdvanceResult {
HumanInputRequired{ HumanInputRequired { name: String },
name: String AIMove { name: String, action: TurnAction },
},
AIMove{
name: String,
action: TurnAction,
}
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::fs;
use crate::game::Game; use crate::game::Game;
use crate::player_interaction::ai::Difficulty; use crate::player_interaction::ai::Difficulty;
use std::fs;
#[test] #[test]
fn test_game() { fn test_game() {
let seed = 124; let seed = 124;
let dictionary_path = "resources/dictionary.csv"; let dictionary_path = "../resources/dictionary.csv";
let dictionary_string = fs::read_to_string(dictionary_path).unwrap(); let dictionary_string = fs::read_to_string(dictionary_path).unwrap();
let mut game = Game::new(seed, &dictionary_string, vec!["Player".to_string()], vec![Difficulty{proportion: 0.5, randomness: 0.0}]); let mut game = Game::new(
seed,
&dictionary_string,
vec!["Player".to_string()],
vec![Difficulty {
proportion: 0.5,
randomness: 0.0,
}],
);
let current_player = game.current_player_name(); let current_player = game.current_player_name();
println!("Current player is {current_player}"); println!("Current player is {current_player}");
@ -522,7 +615,5 @@ mod tests {
assert_eq!(game.current_player_name(), "Player"); assert_eq!(game.current_player_name(), "Player");
assert_eq!(0, game.turns_not_played); assert_eq!(0, game.turns_not_played);
} }
} }

6
wordgrid/src/lib.rs Normal file
View file

@ -0,0 +1,6 @@
pub mod api;
pub mod board;
pub mod constants;
pub mod dictionary;
pub mod game;
pub mod player_interaction;

View file

@ -1,14 +1,11 @@
use serde::{Deserialize, Serialize};
use tsify::Tsify;
use crate::board::Letter; use crate::board::Letter;
use serde::{Deserialize, Serialize};
pub mod ai; pub mod ai;
#[derive(Debug, Serialize, Deserialize, Tsify, Clone)] #[derive(Debug, Serialize, Deserialize, Clone)]
#[tsify(from_wasm_abi)]
pub struct Tray { pub struct Tray {
pub letters: Vec<Option<Letter>> pub letters: Vec<Option<Letter>>,
} }
impl Tray { impl Tray {
@ -17,9 +14,7 @@ impl Tray {
for _ in 0..tray_length { for _ in 0..tray_length {
letters.push(None); letters.push(None);
} }
Tray { Tray { letters }
letters
}
} }
pub fn fill(&mut self, standard_tile_pool: &mut Vec<Letter>) { pub fn fill(&mut self, standard_tile_pool: &mut Vec<Letter>) {
@ -37,25 +32,20 @@ impl Tray {
} }
pub fn count(&self) -> usize { pub fn count(&self) -> usize {
self.letters.iter() self.letters.iter().filter(|l| l.is_some()).count()
.filter(|l| l.is_some())
.count()
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test] #[test]
fn test_tray() { fn test_tray() {
let mut letters = vec![ let mut letters = vec![
Letter::new(Some('E'), 3), Letter::new(Some('E'), 3),
Letter::new(Some('O'), 2), Letter::new(Some('O'), 2),
Letter::new(Some('J'), 1) Letter::new(Some('J'), 1),
]; ];
let mut tray = Tray::new(5); let mut tray = Tray::new(5);
@ -89,8 +79,5 @@ mod tests {
assert_eq!(tray.letters.get(2).unwrap().unwrap().text, 'E'); assert_eq!(tray.letters.get(2).unwrap().unwrap().text, 'E');
assert!(tray.letters.get(3).unwrap().is_none()); assert!(tray.letters.get(3).unwrap().is_none());
assert!(tray.letters.get(4).unwrap().is_none()); assert!(tray.letters.get(4).unwrap().is_none());
} }
} }

View file

@ -1,14 +1,16 @@
use std::collections::{HashMap, HashSet};
use rand::Rng;
use serde::{Deserialize, Serialize};
use tsify::Tsify;
use crate::board::{Board, CellType, Coordinates, Direction, Letter}; use crate::board::{Board, CellType, Coordinates, Direction, Letter};
use crate::constants::GRID_LENGTH; use crate::constants::GRID_LENGTH;
use crate::dictionary::DictionaryImpl; use crate::dictionary::DictionaryImpl;
use crate::game::PlayedTile; use crate::game::PlayedTile;
use crate::player_interaction::Tray; use crate::player_interaction::Tray;
use rand::Rng;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
const ALPHABET: [char; 26] = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z']; const ALPHABET: [char; 26] = [
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
];
struct CoordinateLineMapper { struct CoordinateLineMapper {
direction: Direction, direction: Direction,
@ -18,18 +20,13 @@ struct CoordinateLineMapper {
impl CoordinateLineMapper { impl CoordinateLineMapper {
fn line_to_coord(&self, variable: u8) -> Coordinates { fn line_to_coord(&self, variable: u8) -> Coordinates {
match self.direction { match self.direction {
Direction::Row => { Direction::Row => Coordinates(variable, self.fixed),
Coordinates(variable, self.fixed) Direction::Column => Coordinates(self.fixed, variable),
}
Direction::Column => {
Coordinates(self.fixed, variable)
}
} }
} }
} }
#[derive(Copy, Clone, Serialize, Deserialize, Tsify)] #[derive(Copy, Clone, Serialize, Deserialize)]
#[tsify(from_wasm_abi)]
pub struct Difficulty { pub struct Difficulty {
pub proportion: f64, pub proportion: f64,
pub randomness: f64, pub randomness: f64,
@ -48,7 +45,6 @@ pub struct AI {
type MoveMap = HashMap<char, u32>; type MoveMap = HashMap<char, u32>;
type CrossTiles = Vec<Option<MoveMap>>; type CrossTiles = Vec<Option<MoveMap>>;
#[derive(Debug, Eq, PartialEq, Hash)] #[derive(Debug, Eq, PartialEq, Hash)]
pub struct CompleteMove { pub struct CompleteMove {
moves: Vec<MoveComponent>, moves: Vec<MoveComponent>,
@ -56,10 +52,11 @@ pub struct CompleteMove {
} }
impl CompleteMove { impl CompleteMove {
pub fn convert_to_play(&self, tray: &Tray) -> Vec<Option<PlayedTile>> { pub fn convert_to_play(&self, tray: &Tray) -> Vec<Option<PlayedTile>> {
let mut played_tiles = Vec::with_capacity(tray.letters.len()); let mut played_tiles = Vec::with_capacity(tray.letters.len());
let mut moves = self.moves.iter() let mut moves = self
.moves
.iter()
.map(|m| Some(m.clone())) .map(|m| Some(m.clone()))
.collect::<Vec<Option<MoveComponent>>>(); .collect::<Vec<Option<MoveComponent>>>();
@ -88,7 +85,7 @@ impl CompleteMove {
found_match = true; found_match = true;
break; break;
} }
}, }
None => {} None => {}
} }
} }
@ -127,13 +124,13 @@ struct MoveComponent {
} }
impl AI { impl AI {
pub fn new(difficulty: Difficulty, dictionary: &DictionaryImpl) -> Self { pub fn new(difficulty: Difficulty, dictionary: &DictionaryImpl) -> Self {
let mut ai_dictionary = HashSet::new(); let mut ai_dictionary = HashSet::new();
let mut substrings = HashSet::new(); let mut substrings = HashSet::new();
for (word, score) in dictionary.iter() { for (word, score) in dictionary.iter() {
if *score >= difficulty.proportion { // TODO - may need to reverse if *score >= difficulty.proportion {
// TODO - may need to reverse
ai_dictionary.insert(word.clone()); ai_dictionary.insert(word.clone());
substrings.insert(word.clone()); substrings.insert(word.clone());
@ -143,13 +140,12 @@ impl AI {
substrings.insert(word[i..j].to_string()); substrings.insert(word[i..j].to_string());
} }
} }
} }
} }
let board_view = Board::new(); let board_view = Board::new();
let mut column_cross_tiles = CrossTiles::with_capacity((GRID_LENGTH * GRID_LENGTH) as usize); let mut column_cross_tiles =
CrossTiles::with_capacity((GRID_LENGTH * GRID_LENGTH) as usize);
let mut row_cross_tiles = CrossTiles::with_capacity((GRID_LENGTH * GRID_LENGTH) as usize); let mut row_cross_tiles = CrossTiles::with_capacity((GRID_LENGTH * GRID_LENGTH) as usize);
board_view.cells.iter().for_each(|_| { board_view.cells.iter().for_each(|_| {
@ -168,15 +164,20 @@ impl AI {
} }
} }
pub fn find_best_move<R: Rng>(&mut self, tray: &Tray, grid: &Board, rng: &mut R) -> Option<CompleteMove>{ pub fn find_best_move<R: Rng>(
&mut self,
tray: &Tray,
grid: &Board,
rng: &mut R,
) -> Option<CompleteMove> {
let move_set = self.find_all_moves(tray, grid); let move_set = self.find_all_moves(tray, grid);
let mut best_move: Option<(CompleteMove, f64)> = None; let mut best_move: Option<(CompleteMove, f64)> = None;
for possible_move in move_set { for possible_move in move_set {
let move_score = let move_score = if self.difficulty.randomness > 0.0 {
if self.difficulty.randomness > 0.0 { (1.0 - self.difficulty.randomness) * (possible_move.score as f64)
(1.0 - self.difficulty.randomness) * (possible_move.score as f64) + self.difficulty.randomness * rng.gen_range(0.0..1.0) + self.difficulty.randomness * rng.gen_range(0.0..1.0)
} else { } else {
possible_move.score as f64 possible_move.score as f64
}; };
@ -194,10 +195,9 @@ impl AI {
} }
return match best_move { return match best_move {
None => {None} None => None,
Some((best_move, _)) => {Some(best_move)} Some((best_move, _)) => Some(best_move),
}; };
} }
fn find_all_moves(&mut self, tray: &Tray, grid: &Board) -> HashSet<CompleteMove> { fn find_all_moves(&mut self, tray: &Tray, grid: &Board) -> HashSet<CompleteMove> {
@ -215,15 +215,17 @@ impl AI {
&self, &self,
tray: &Tray, tray: &Tray,
direction: Direction, direction: Direction,
all_moves: &mut HashSet<CompleteMove>) { all_moves: &mut HashSet<CompleteMove>,
) {
// If you're building a word in one direction, you need to form valid words in the cross-direction // If you're building a word in one direction, you need to form valid words in the cross-direction
let cross_tiles = match direction { let cross_tiles = match direction {
Direction::Column => {&self.row_cross_tiles} Direction::Column => &self.row_cross_tiles,
Direction::Row => {&self.column_cross_tiles} Direction::Row => &self.column_cross_tiles,
}; };
let tray_letters = tray.letters.iter() let tray_letters = tray
.letters
.iter()
.filter(|letter| letter.is_some()) .filter(|letter| letter.is_some())
.map(|letter| letter.unwrap()) .map(|letter| letter.unwrap())
.collect::<Vec<Letter>>(); .collect::<Vec<Letter>>();
@ -234,7 +236,7 @@ impl AI {
let coord_mapper = CoordinateLineMapper { let coord_mapper = CoordinateLineMapper {
direction, direction,
fixed: k fixed: k,
}; };
for p in 0..GRID_LENGTH { for p in 0..GRID_LENGTH {
@ -246,13 +248,12 @@ impl AI {
for l in 0..GRID_LENGTH { for l in 0..GRID_LENGTH {
let coords = coord_mapper.line_to_coord(l); let coords = coord_mapper.line_to_coord(l);
let is_anchored = check_if_anchored(&self.board_view, coords, Direction::Row) || let is_anchored = check_if_anchored(&self.board_view, coords, Direction::Row)
check_if_anchored(&self.board_view, coords, Direction::Column); || check_if_anchored(&self.board_view, coords, Direction::Column);
if is_anchored && if is_anchored && line_letters.get(l as usize).unwrap().is_none()
line_letters.get(l as usize).unwrap().is_none() // it's duplicate work to check here when we'll already check at either free side // it's duplicate work to check here when we'll already check at either free side
{ {
self.evaluate_spot_heading_left( self.evaluate_spot_heading_left(
&line_letters, &line_letters,
&line_cross_letters, &line_cross_letters,
@ -262,17 +263,11 @@ impl AI {
&Vec::new(), &Vec::new(),
1, 1,
&MoveScoring::new(), &MoveScoring::new(),
all_moves,
all_moves
); );
}
} }
} }
}
} }
fn evaluate_spot_heading_left( fn evaluate_spot_heading_left(
@ -287,10 +282,8 @@ impl AI {
min_length: usize, min_length: usize,
current_points: &MoveScoring, current_points: &MoveScoring,
all_moves: &mut HashSet<CompleteMove> all_moves: &mut HashSet<CompleteMove>,
) { ) {
if line_index < 0 || line_index >= GRID_LENGTH as i8 { if line_index < 0 || line_index >= GRID_LENGTH as i8 {
return; return;
} }
@ -300,10 +293,13 @@ impl AI {
Some(_) => { Some(_) => {
// there's a letter here; need to take a step left if we can // there's a letter here; need to take a step left if we can
if !(line_index >= 1 && if !(line_index >= 1
line_letters.get((line_index-1) as usize).unwrap().is_some() && && line_letters
min_length == 1 .get((line_index - 1) as usize)
) { .unwrap()
.is_some()
&& min_length == 1)
{
// if-statement is basically saying that if we're at the start of the process (min_length==1) and there's a word still to our left, // if-statement is basically saying that if we're at the start of the process (min_length==1) and there's a word still to our left,
// just stop. Other versions of the for-loops that call this function will have picked up that case. // just stop. Other versions of the for-loops that call this function will have picked up that case.
self.evaluate_spot_heading_left( self.evaluate_spot_heading_left(
@ -315,9 +311,8 @@ impl AI {
current_play, current_play,
min_length, min_length,
current_points, current_points,
all_moves all_moves,
); );
} }
} }
None => { None => {
@ -331,10 +326,9 @@ impl AI {
min_length, min_length,
current_points, current_points,
coord_mapper, coord_mapper,
&available_letters, &available_letters,
letter.clone(), letter.clone(),
all_moves all_moves,
); );
} }
@ -348,12 +342,10 @@ impl AI {
current_play, current_play,
min_length + 1, min_length + 1,
current_points, current_points,
all_moves all_moves,
); );
} }
} }
} }
fn evaluate_letter_at_spot( fn evaluate_letter_at_spot(
@ -366,13 +358,10 @@ impl AI {
current_points: &MoveScoring, current_points: &MoveScoring,
coord_mapper: &CoordinateLineMapper, coord_mapper: &CoordinateLineMapper,
available_letters: &Vec<Letter>, available_letters: &Vec<Letter>,
letter: Letter, letter: Letter,
all_moves: &mut HashSet<CompleteMove> all_moves: &mut HashSet<CompleteMove>,
) { ) {
if letter.is_blank { if letter.is_blank {
// need to loop through alphabet // need to loop through alphabet
for alpha in ALPHABET { for alpha in ALPHABET {
@ -389,8 +378,7 @@ impl AI {
coord_mapper, coord_mapper,
available_letters, available_letters,
letter, letter,
all_moves all_moves,
); );
} }
} else { } else {
@ -404,10 +392,9 @@ impl AI {
coord_mapper, coord_mapper,
available_letters, available_letters,
letter.clone(), letter.clone(),
all_moves all_moves,
); );
} }
} }
fn evaluate_non_blank_letter_at_spot( fn evaluate_non_blank_letter_at_spot(
@ -419,15 +406,12 @@ impl AI {
min_length: usize, min_length: usize,
current_points: &MoveScoring, current_points: &MoveScoring,
coord_mapper: &CoordinateLineMapper, coord_mapper: &CoordinateLineMapper,
available_letters: &Vec<Letter>, available_letters: &Vec<Letter>,
letter: Letter, letter: Letter,
all_moves: &mut HashSet<CompleteMove>, all_moves: &mut HashSet<CompleteMove>,
) { ) {
// let's now assign the letter to this spot // let's now assign the letter to this spot
let mut line_letters = line_letters.clone(); let mut line_letters = line_letters.clone();
*line_letters.get_mut(line_index as usize).unwrap() = Some(letter); *line_letters.get_mut(line_index as usize).unwrap() = Some(letter);
@ -458,7 +442,11 @@ impl AI {
// making copy // making copy
let mut current_points = current_points.clone(); let mut current_points = current_points.clone();
let cell_type = self.board_view.get_cell(coord_mapper.line_to_coord(line_index as u8)).unwrap().cell_type; let cell_type = self
.board_view
.get_cell(coord_mapper.line_to_coord(line_index as u8))
.unwrap()
.cell_type;
// first we score cross letters // first we score cross letters
if let Some(map) = cross_letters { if let Some(map) = cross_letters {
@ -508,7 +496,8 @@ impl AI {
current_points.main_scoring += letter.points * 2; current_points.main_scoring += letter.points * 2;
} }
CellType::TripleLetter => { CellType::TripleLetter => {
current_points.main_scoring += letter.points * 3;} current_points.main_scoring += letter.points * 3;
}
}; };
// finally, while we know that we're in a valid substring we should check if this is a valid word or not // finally, while we know that we're in a valid substring we should check if this is a valid word or not
@ -516,7 +505,8 @@ impl AI {
if word.len() >= min_length && self.dictionary.contains(&word) { if word.len() >= min_length && self.dictionary.contains(&word) {
let new_move = CompleteMove { let new_move = CompleteMove {
moves: current_play.clone(), moves: current_play.clone(),
score: current_points.cross_scoring + current_points.main_scoring*current_points.multiplier, score: current_points.cross_scoring
+ current_points.main_scoring * current_points.multiplier,
}; };
all_moves.insert(new_move); all_moves.insert(new_move);
} }
@ -525,8 +515,7 @@ impl AI {
let mut new_available_letters = Vec::with_capacity(available_letters.len() - 1); let mut new_available_letters = Vec::with_capacity(available_letters.len() - 1);
let mut skipped_one = false; let mut skipped_one = false;
available_letters.iter() available_letters.iter().for_each(|in_tray| {
.for_each(|in_tray| {
if skipped_one || !in_tray.partial_match(&letter) { if skipped_one || !in_tray.partial_match(&letter) {
new_available_letters.push(*in_tray); new_available_letters.push(*in_tray);
} else { } else {
@ -543,11 +532,9 @@ impl AI {
current_play, current_play,
min_length, min_length,
&current_points, &current_points,
coord_mapper, coord_mapper,
&new_available_letters, &new_available_letters,
all_moves,
all_moves
); );
} }
@ -560,13 +547,11 @@ impl AI {
min_length: usize, min_length: usize,
current_points: &MoveScoring, current_points: &MoveScoring,
coord_mapper: &CoordinateLineMapper, coord_mapper: &CoordinateLineMapper,
available_letters: &Vec<Letter>, available_letters: &Vec<Letter>,
all_moves: &mut HashSet<CompleteMove>, all_moves: &mut HashSet<CompleteMove>,
) { ) {
// out-of-bounds check // out-of-bounds check
if line_index < 0 || line_index >= GRID_LENGTH as i8 { if line_index < 0 || line_index >= GRID_LENGTH as i8 {
return; return;
@ -586,7 +571,7 @@ impl AI {
&current_points, &current_points,
coord_mapper, coord_mapper,
available_letters, available_letters,
all_moves all_moves,
); );
} }
None => { None => {
@ -601,10 +586,8 @@ impl AI {
current_points, current_points,
coord_mapper, coord_mapper,
available_letters, available_letters,
letter.clone(), letter.clone(),
all_moves all_moves,
) )
} }
} }
@ -625,16 +608,20 @@ impl AI {
} }
let mut check_for_valid_moves = |coords: Coordinates| { let mut check_for_valid_moves = |coords: Coordinates| {
let cell = new.get_cell(coords).unwrap(); let cell = new.get_cell(coords).unwrap();
if cell.value.is_none() { if cell.value.is_none() {
let valid_row_moves = self.get_letters_that_make_words(new, Direction::Row, coords); let valid_row_moves = self.get_letters_that_make_words(new, Direction::Row, coords);
let valid_column_moves = self.get_letters_that_make_words(new, Direction::Column, coords); let valid_column_moves =
self.get_letters_that_make_words(new, Direction::Column, coords);
let existing_row_moves = self.row_cross_tiles.get_mut(coords.map_to_index()).unwrap(); let existing_row_moves =
self.row_cross_tiles.get_mut(coords.map_to_index()).unwrap();
*existing_row_moves = valid_row_moves; *existing_row_moves = valid_row_moves;
let existing_column_moves = self.column_cross_tiles.get_mut(coords.map_to_index()).unwrap(); let existing_column_moves = self
.column_cross_tiles
.get_mut(coords.map_to_index())
.unwrap();
*existing_column_moves = valid_column_moves; *existing_column_moves = valid_column_moves;
} }
}; };
@ -652,10 +639,14 @@ impl AI {
check_for_valid_moves(coords); check_for_valid_moves(coords);
} }
}); });
} }
fn get_letters_that_make_words(&self, new: &Board, direction: Direction, coords: Coordinates) -> Option<MoveMap> { fn get_letters_that_make_words(
&self,
new: &Board,
direction: Direction,
coords: Coordinates,
) -> Option<MoveMap> {
let is_anchored = check_if_anchored(new, coords, direction); let is_anchored = check_if_anchored(new, coords, direction);
if !is_anchored { if !is_anchored {
return None; return None;
@ -687,11 +678,9 @@ impl AI {
Some(new_map) Some(new_map)
} }
} }
fn check_if_anchored(grid: &Board, coords: Coordinates, direction: Direction) -> bool { fn check_if_anchored(grid: &Board, coords: Coordinates, direction: Direction) -> bool {
// Middle tile is always considered anchored // Middle tile is always considered anchored
if coords.0 == GRID_LENGTH / 2 && coords.1 == GRID_LENGTH / 2 { if coords.0 == GRID_LENGTH / 2 && coords.1 == GRID_LENGTH / 2 {
return true; return true;
@ -699,7 +688,7 @@ fn check_if_anchored(grid: &Board, coords: Coordinates, direction: Direction) ->
let has_letter = |alt_coord: Option<Coordinates>| -> bool { let has_letter = |alt_coord: Option<Coordinates>| -> bool {
match alt_coord { match alt_coord {
None => {false} None => false,
Some(alt_coord) => { Some(alt_coord) => {
let cell = grid.get_cell(alt_coord).unwrap(); let cell = grid.get_cell(alt_coord).unwrap();
cell.value.is_some() cell.value.is_some()
@ -719,7 +708,11 @@ fn find_word_on_line(line_letters: &Vec<Option<Letter>>, line_index: i8) -> Stri
if start_word < 1 { if start_word < 1 {
break; break;
} }
if line_letters.get((start_word - 1) as usize).unwrap().is_none() { if line_letters
.get((start_word - 1) as usize)
.unwrap()
.is_none()
{
break; break;
} }
start_word -= 1; start_word -= 1;
@ -738,7 +731,8 @@ fn find_word_on_line(line_letters: &Vec<Option<Letter>>, line_index: i8) -> Stri
let mut str = String::new(); let mut str = String::new();
line_letters[(start_word as usize)..((end_word + 1) as usize)] line_letters[(start_word as usize)..((end_word + 1) as usize)]
.iter().for_each(|letter| { .iter()
.for_each(|letter| {
str.push(letter.unwrap().text); str.push(letter.unwrap().text);
}); });
@ -747,10 +741,10 @@ fn find_word_on_line(line_letters: &Vec<Option<Letter>>, line_index: i8) -> Stri
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use crate::dictionary::Dictionary;
use rand::rngs::SmallRng; use rand::rngs::SmallRng;
use rand::SeedableRng; use rand::SeedableRng;
use crate::dictionary::Dictionary;
use super::*;
fn set_cell(board: &mut Board, x: u8, y: u8, letter: char, points: u32) { fn set_cell(board: &mut Board, x: u8, y: u8, letter: char, points: u32) {
let cell = board.get_cell_mut(Coordinates(x, y)).unwrap(); let cell = board.get_cell_mut(Coordinates(x, y)).unwrap();
@ -832,14 +826,12 @@ mod tests {
assert_eq!(moves.len(), 10); assert_eq!(moves.len(), 10);
tray.letters[4] = Some( tray.letters[4] = Some(Letter {
Letter {
text: ' ', text: ' ',
points: 0, points: 0,
ephemeral: false, ephemeral: false,
is_blank: true, is_blank: true,
} });
);
let moves = ai.find_all_moves(&tray, &board); let moves = ai.find_all_moves(&tray, &board);
@ -857,8 +849,6 @@ mod tests {
set_cell(&mut board, 7, 9, 'A', 1); set_cell(&mut board, 7, 9, 'A', 1);
set_cell(&mut board, 7, 10, 'T', 1); set_cell(&mut board, 7, 10, 'T', 1);
let difficulty = Difficulty { let difficulty = Difficulty {
proportion: 0.0, // restrict yourself to words with this proportion OR HIGHER proportion: 0.0, // restrict yourself to words with this proportion OR HIGHER
randomness: 0.0, randomness: 0.0,
@ -898,7 +888,10 @@ mod tests {
ai.update_state(&board); ai.update_state(&board);
let end_of_boat = ai.column_cross_tiles.get(Coordinates(7, 11).map_to_index()).unwrap(); let end_of_boat = ai
.column_cross_tiles
.get(Coordinates(7, 11).map_to_index())
.unwrap();
assert!(end_of_boat.is_some()); assert!(end_of_boat.is_some());
assert_eq!(end_of_boat.as_ref().unwrap().len(), 1); assert_eq!(end_of_boat.as_ref().unwrap().len(), 1);
@ -906,7 +899,6 @@ mod tests {
println!("Moves are {:?}", moves); println!("Moves are {:?}", moves);
// 3 possible moves - // 3 possible moves -
// 1. put 'S' at the end of 'BOAT' and form words 'SLAM' and 'BOATS' // 1. put 'S' at the end of 'BOAT' and form words 'SLAM' and 'BOATS'
// 2. Put 'S' at end of 'BOAT' // 2. Put 'S' at end of 'BOAT'
@ -920,7 +912,6 @@ mod tests {
let play = best_move.convert_to_play(&tray); let play = best_move.convert_to_play(&tray);
println!("Play is {:?}", play); println!("Play is {:?}", play);
} }
#[test] #[test]
@ -943,15 +934,20 @@ mod tests {
let above_cell_coords = Coordinates(7, 6); let above_cell_coords = Coordinates(7, 6);
let left_cell_cords = Coordinates(6, 7); let left_cell_cords = Coordinates(6, 7);
let row_cross_tiles = ai.row_cross_tiles.get(left_cell_cords.map_to_index()).unwrap(); let row_cross_tiles = ai
let column_cross_tiles = ai.column_cross_tiles.get(above_cell_coords.map_to_index()).unwrap(); .row_cross_tiles
.get(left_cell_cords.map_to_index())
.unwrap();
let column_cross_tiles = ai
.column_cross_tiles
.get(above_cell_coords.map_to_index())
.unwrap();
assert_eq!(row_cross_tiles.as_ref().unwrap().len(), 1); assert_eq!(row_cross_tiles.as_ref().unwrap().len(), 1);
assert_eq!(column_cross_tiles.as_ref().unwrap().len(), 1); assert_eq!(column_cross_tiles.as_ref().unwrap().len(), 1);
let far_off_tiles = ai.row_cross_tiles.get(0).unwrap(); let far_off_tiles = ai.row_cross_tiles.get(0).unwrap();
assert!(far_off_tiles.is_none()); assert!(far_off_tiles.is_none());
} }
#[test] #[test]
@ -992,12 +988,14 @@ mod tests {
fn test_starting_move() { fn test_starting_move() {
let mut board = Board::new(); let mut board = Board::new();
let difficulty = Difficulty{proportion: 0.1, randomness: 0.0}; let difficulty = Difficulty {
proportion: 0.1,
randomness: 0.0,
};
let mut dictionary = DictionaryImpl::new(); let mut dictionary = DictionaryImpl::new();
dictionary.insert("TWEETED".to_string(), 0.2); dictionary.insert("TWEETED".to_string(), 0.2);
let mut tray = Tray::new(7); let mut tray = Tray::new(7);
tray.letters[0] = Some(Letter { tray.letters[0] = Some(Letter {
text: 'I', text: 'I',
@ -1048,7 +1046,5 @@ mod tests {
println!("Available moves are {:?}", all_moves); println!("Available moves are {:?}", all_moves);
assert!(all_moves.is_empty()); assert!(all_moves.is_empty());
} }
} }