Compare commits
1 commit
main
...
multiplaye
Author | SHA1 | Date | |
---|---|---|---|
45cc83bfb2 |
26 changed files with 2903 additions and 1832 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -1,4 +1,5 @@
|
|||
/target
|
||||
target/
|
||||
/Cargo.lock
|
||||
.idea/
|
||||
pkg/
|
||||
.nvmrc
|
||||
|
|
21
src/lib.rs
21
src/lib.rs
|
@ -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));
|
||||
}
|
||||
|
213
src/wasm.rs
213
src/wasm.rs
|
@ -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
1919
ui/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -2,13 +2,13 @@
|
|||
"dependencies": {
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"word_grid": "file:../pkg"
|
||||
"word_grid": "file:../wasm/pkg"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@parcel/transformer-less": "^2.9.3",
|
||||
"@types/react": "^18.2.18",
|
||||
"@types/react-dom": "^18.2.7",
|
||||
"parcel": "^2.9.3",
|
||||
"parcel": "^2.12.0",
|
||||
"process": "^0.11.10"
|
||||
},
|
||||
"scripts": {
|
||||
|
|
305
ui/src/Game.tsx
305
ui/src/Game.tsx
|
@ -1,16 +1,5 @@
|
|||
import * as React from "react";
|
||||
import {useEffect, useMemo, useReducer, useRef, useState} from "react";
|
||||
import {
|
||||
GameState,
|
||||
GameWasm,
|
||||
MyResult,
|
||||
PlayedTile,
|
||||
PlayerAndScore,
|
||||
ScoreResult,
|
||||
Tray,
|
||||
TurnAction,
|
||||
TurnAdvanceResult
|
||||
} from "../../pkg/word_grid";
|
||||
import {useEffect, useReducer, useRef, useState} from "react";
|
||||
import {
|
||||
Direction,
|
||||
GRID_LENGTH,
|
||||
|
@ -28,6 +17,7 @@ import {
|
|||
} from "./utils";
|
||||
import {TileExchangeModal} from "./TileExchange";
|
||||
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) {
|
||||
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: {
|
||||
wasm: GameWasm,
|
||||
api: API,
|
||||
settings: Settings,
|
||||
end_game_fn: () => void,
|
||||
}) {
|
||||
|
||||
const cellTypes = useMemo(() => {
|
||||
return props.wasm.get_board_cell_types();
|
||||
}, []);
|
||||
|
||||
const [isGameOver, setGameOver] = useState<boolean>(false);
|
||||
const [api_state, setAPIState] = useState<APIState>(undefined);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||
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 newLetterData = [] as HighlightableLetterData[];
|
||||
|
@ -58,8 +73,6 @@ export function Game(props: {
|
|||
});
|
||||
|
||||
function adjustGridArrow(existing: GridArrowData, update: GridArrowDispatchAction): GridArrowData {
|
||||
console.log({update});
|
||||
|
||||
if (update.action == GridArrowDispatchActionType.CLEAR) {
|
||||
return null;
|
||||
} else if (update.action == GridArrowDispatchActionType.CYCLE) {
|
||||
|
@ -119,36 +132,28 @@ export function Game(props: {
|
|||
}
|
||||
|
||||
function exchangeFunction(selectedArray: Array<boolean>) {
|
||||
const result = props.api.exchange(selectedArray);
|
||||
|
||||
const result: MyResult<TurnAction | string> = props.wasm.exchange_tiles(selectedArray);
|
||||
|
||||
if(result.response_type === "ERR") {
|
||||
logDispatch(<div><em>{(result.value as string)}</em></div>);
|
||||
} else {
|
||||
handlePlayerAction(result.value as TurnAction, props.settings.playerName);
|
||||
setTurnCount(turnCount + 1);
|
||||
|
||||
if(result.game_state.type === "Ended") {
|
||||
endGame(result.game_state);
|
||||
}
|
||||
}
|
||||
result
|
||||
.then(
|
||||
(_api_state) => {
|
||||
load();
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error({error});
|
||||
logDispatch(<div>{error}</div>);
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
function addWordFn(word: string) {
|
||||
props.wasm.add_word(word);
|
||||
}
|
||||
|
||||
|
||||
const [gridArrow, gridArrowDispatch] = useReducer(adjustGridArrow, null);
|
||||
const [logInfo, logDispatch] = useReducer(addLogInfo, []);
|
||||
|
||||
|
||||
function movePlayableLetters(playerLetters: PlayableLetterData[], update: TileDispatchAction) {
|
||||
|
||||
if (update.action === TileDispatchActionType.RETRIEVE) {
|
||||
|
||||
let tray: Tray = props.wasm.get_tray("Player");
|
||||
let tray: Tray = api_state.tray;
|
||||
|
||||
if (update.override) {
|
||||
playerLetters = [];
|
||||
|
@ -215,35 +220,6 @@ export function Game(props: {
|
|||
}
|
||||
|
||||
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 [isTileExchangeOpen, setIsTileExchangeOpen] = useState<boolean>(false);
|
||||
|
@ -255,21 +231,6 @@ export function Game(props: {
|
|||
}
|
||||
}, [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) {
|
||||
|
||||
if (action.type == "PlayTiles") {
|
||||
|
@ -278,11 +239,13 @@ export function Game(props: {
|
|||
for (let word of result.words) {
|
||||
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") {
|
||||
logDispatch(<div>{playerName} exchanged {action.tiles_exchanged} tile{action.tiles_exchanged > 1 ? 's' : ''} for their turn.</div>);
|
||||
}
|
||||
else if(action.type == "Pass"){
|
||||
logDispatch(
|
||||
<div>{playerName} exchanged {action.tiles_exchanged} tile{action.tiles_exchanged > 1 ? 's' : ''} for
|
||||
their turn.</div>);
|
||||
} else if (action.type == "Pass") {
|
||||
logDispatch(<div>{playerName} passed.</div>);
|
||||
}
|
||||
|
||||
|
@ -293,11 +256,9 @@ export function Game(props: {
|
|||
function endGame(state: GameState) {
|
||||
|
||||
if (state.type != "InProgress") {
|
||||
|
||||
setGameOver(true);
|
||||
logDispatch(<h4>Scoring</h4>);
|
||||
|
||||
const scores = props.wasm.get_scores() as PlayerAndScore[];
|
||||
const scores = api_state.public_information.players;
|
||||
let pointsBonus = 0;
|
||||
for (const playerAndScore of scores) {
|
||||
const name = playerAndScore.name;
|
||||
|
@ -348,15 +309,14 @@ export function Game(props: {
|
|||
.at(0);
|
||||
|
||||
const playersAtHighest = scores.filter((score) => score.score == highestScore);
|
||||
let endGameMsg: string = '';
|
||||
let endGameMsg: string;
|
||||
|
||||
if (playersAtHighest.length > 1 && state.finisher == null) {
|
||||
endGameMsg = "Tie game!";
|
||||
} else if (playersAtHighest.length > 1 && state.finisher != null) {
|
||||
// if there's a tie then the finisher gets the win
|
||||
endGameMsg = `${playersAtHighest[0].name} won by finishing first!`;
|
||||
}
|
||||
else {
|
||||
} else {
|
||||
endGameMsg = `${playersAtHighest[0].name} won!`;
|
||||
}
|
||||
logDispatch(<h4>Game over - {endGameMsg}</h4>);
|
||||
|
@ -367,34 +327,78 @@ export function Game(props: {
|
|||
|
||||
}
|
||||
|
||||
function runAI() {
|
||||
const result: MyResult<TurnAdvanceResult> = props.wasm.advance_turn();
|
||||
if(result.response_type === "OK" && result.value.type == "AIMove") {
|
||||
handlePlayerAction(result.value.action, props.settings.aiName);
|
||||
if(result.game_state.type === "Ended") {
|
||||
endGame(result.game_state);
|
||||
function updateBoardLetters(newLetters: Array<Letter>) {
|
||||
const newLetterData = newLetters as HighlightableLetterData[];
|
||||
|
||||
for (let i = 0; i < newLetterData.length; i++) {
|
||||
const newLetter = newLetterData[i];
|
||||
if (newLetter != null) {
|
||||
newLetter.highlight = false;
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
// this would be quite surprising
|
||||
console.error({result});
|
||||
// loop through the histories backwards until we reach our player
|
||||
for (let j = api_state.public_information.history.length - 1; j >= 0; j--) {
|
||||
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(() => {
|
||||
if (api_state) {
|
||||
console.debug(api_state);
|
||||
gridArrowDispatch({action: GridArrowDispatchActionType.CLEAR});
|
||||
trayDispatch({action: TileDispatchActionType.RETRIEVE});
|
||||
setConfirmedScorePoints(-1);
|
||||
if(!isGameOver){
|
||||
logDispatch(<h4>Turn {turnCount}</h4>);
|
||||
logDispatch(<div>{playerTurnName}'s turn</div>);
|
||||
if(playerTurnName != props.settings.playerName && !isGameOver) {
|
||||
runAI();
|
||||
}
|
||||
}
|
||||
}, [turnCount]);
|
||||
updateBoardLetters(api_state.public_information.board);
|
||||
|
||||
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 <>
|
||||
<TileExchangeModal
|
||||
|
@ -405,7 +409,7 @@ export function Game(props: {
|
|||
/>
|
||||
<div className="board-log">
|
||||
<Grid
|
||||
cellTypes={cellTypes}
|
||||
cellTypes={api_state.public_information.cell_types}
|
||||
playerLetters={playerLetters}
|
||||
boardLetters={boardLetters}
|
||||
tileDispatch={trayDispatch}
|
||||
|
@ -439,15 +443,15 @@ export function Game(props: {
|
|||
onClick={() => {
|
||||
trayDispatch({action: TileDispatchActionType.RETURN}); // want all tiles back on tray for tile exchange
|
||||
setIsTileExchangeOpen(true);
|
||||
}}>Open Tile Exchange</button>
|
||||
}}>Open Tile Exchange
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<TileTray letters={playerLetters} trayLength={props.settings.trayLength} trayDispatch={trayDispatch}/>
|
||||
<div className="player-controls">
|
||||
<button
|
||||
className="check"
|
||||
disabled={isGameOver}
|
||||
onClick={() => {
|
||||
onClick={async () => {
|
||||
const playedTiles = playerLetters.map((i) => {
|
||||
if (i === undefined) {
|
||||
return null;
|
||||
|
@ -468,38 +472,40 @@ export function Game(props: {
|
|||
return null;
|
||||
});
|
||||
|
||||
const result: MyResult<{ type: "PlayTiles"; result: ScoreResult } | string> = props.wasm.receive_play(playedTiles, confirmedScorePoints > -1);
|
||||
console.log({result});
|
||||
const committing = confirmedScorePoints > -1;
|
||||
const result = props.api.play(playedTiles, committing);
|
||||
|
||||
if(result.response_type === "ERR") {
|
||||
const message = result.value as string;
|
||||
if (message.endsWith("is not a valid word")) {
|
||||
// extract out word
|
||||
const word = message.split(" ")[0];
|
||||
logDispatch(<AddWordButton word={word} addWordFn={addWordFn} />);
|
||||
} else {
|
||||
logDispatch(<div><em>{message}</em></div>);
|
||||
result
|
||||
.then(
|
||||
(api_state) => {
|
||||
|
||||
const play_tiles: TurnAction = api_state.update.type;
|
||||
if (play_tiles.type == "PlayTiles") {
|
||||
setConfirmedScorePoints(play_tiles.result.total);
|
||||
|
||||
if (committing) {
|
||||
load();
|
||||
}
|
||||
|
||||
} else {
|
||||
|
||||
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);
|
||||
console.error("Inaccessible branch!!!")
|
||||
}
|
||||
|
||||
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>
|
||||
<button
|
||||
|
@ -507,26 +513,31 @@ export function Game(props: {
|
|||
disabled={isGameOver}
|
||||
onClick={() => {
|
||||
trayDispatch({action: TileDispatchActionType.RETURN});
|
||||
}}>Return Tiles</button>
|
||||
}}>Return Tiles
|
||||
</button>
|
||||
<button
|
||||
className="pass"
|
||||
disabled={isGameOver}
|
||||
onClick={() => {
|
||||
if (window.confirm("Are you sure you want to pass?")) {
|
||||
const result = props.wasm.skip_turn() as MyResult<string>;
|
||||
handlePlayerAction({type: "Pass"}, props.settings.playerName);
|
||||
setTurnCount(turnCount + 1);
|
||||
const result = props.api.pass();
|
||||
|
||||
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</button>
|
||||
}}>Pass
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</>;
|
||||
|
||||
}
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
import * as React from "react";
|
||||
import {useState} from "react";
|
||||
import {Difficulty, GameWasm} from '../../pkg/word_grid';
|
||||
import {Settings} from "./utils";
|
||||
import {Game} from "./Game";
|
||||
import {API, Difficulty} from "./api";
|
||||
import {GameWasm} from "./wasm";
|
||||
|
||||
export function Menu(props: {settings: Settings, dictionary_text: string}) {
|
||||
|
||||
|
@ -70,8 +71,8 @@ export function Menu(props: {settings: Settings, dictionary_text: string}) {
|
|||
proportion: processedProportionDictionary,
|
||||
randomness: processedAIRandomness,
|
||||
};
|
||||
const game_wasm = 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_wasm: API = new GameWasm(BigInt(seed), props.dictionary_text, difficulty);
|
||||
const game = <Game settings={props.settings} api={game_wasm} key={seed} end_game_fn={() => setGame(null)}/>
|
||||
setGame(game);
|
||||
}}>New Game</button>
|
||||
</div>
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import {addNTimes, PlayableLetterData} from "./utils";
|
||||
import {useEffect, useState} from "react";
|
||||
import {Modal} from "./Modal";
|
||||
import {Letter as LetterData} from "../../pkg/word_grid";
|
||||
import * as React from "react";
|
||||
import {Letter} from "./api";
|
||||
|
||||
export function TileExchangeModal(props: {
|
||||
playerLetters: PlayableLetterData[],
|
||||
|
@ -105,7 +105,7 @@ function TilesExchangedTray(props: {
|
|||
}
|
||||
|
||||
function DummyExchangeTile(props: {
|
||||
letter: LetterData,
|
||||
letter: Letter,
|
||||
isSelected: boolean,
|
||||
onClick: () => void,
|
||||
}){
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import * as React from "react";
|
||||
import {ChangeEvent, JSX} from "react";
|
||||
import {PlayerAndScore} from "../../pkg/word_grid";
|
||||
import {
|
||||
CellType,
|
||||
cellTypeToDetails,
|
||||
CoordinateData,
|
||||
GridArrowData,
|
||||
|
@ -14,6 +12,7 @@ import {
|
|||
TileDispatch,
|
||||
TileDispatchActionType,
|
||||
} from "./utils";
|
||||
import {APIPlayer, CellType} from "./api";
|
||||
|
||||
|
||||
export function TileSlot(props: {
|
||||
|
@ -234,7 +233,7 @@ export function Grid(props: {
|
|||
</div>
|
||||
}
|
||||
|
||||
export function Scores(props: {playerScores: Array<PlayerAndScore>}){
|
||||
export function Scores(props: {playerScores: Array<APIPlayer>}){
|
||||
let elements = props.playerScores.map((ps) => {
|
||||
return <div key={ps.name}>
|
||||
<h3>{ps.name}</h3>
|
||||
|
|
90
ui/src/api.ts
Normal file
90
ui/src/api.ts
Normal 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>;
|
||||
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
import init from '../../pkg/word_grid.js';
|
||||
import init from 'word_grid';
|
||||
import {createRoot} from "react-dom/client";
|
||||
import * as React from "react";
|
||||
import {Menu} from "./Menu";
|
||||
|
|
|
@ -1,14 +1,5 @@
|
|||
import {Letter as LetterData, Letter} from "../../pkg/word_grid";
|
||||
import * as React from "react";
|
||||
|
||||
export enum CellType {
|
||||
Normal = "Normal",
|
||||
DoubleWord = "DoubleWord",
|
||||
DoubleLetter = "DoubleLetter",
|
||||
TripleLetter = "TripleLetter",
|
||||
TripleWord = "TripleWord",
|
||||
Start = "Start",
|
||||
}
|
||||
import {CellType, Letter as LetterData} from "./api";
|
||||
|
||||
|
||||
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);
|
||||
|
||||
|
|
66
ui/src/wasm.ts
Normal file
66
ui/src/wasm.ts
Normal 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
289
wasm/Cargo.lock
generated
Normal 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
14
wasm/Cargo.toml
Normal 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
60
wasm/src/lib.rs
Normal 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
267
wordgrid/Cargo.lock
generated
Normal 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",
|
||||
]
|
|
@ -7,16 +7,9 @@ repository = "https://git.joeltherrien.ca/joel/WordGrid"
|
|||
license = "AGPL-3"
|
||||
description = "A (WIP) package for playing 'WordGrid'."
|
||||
|
||||
[lib]
|
||||
crate-type = ["cdylib"]
|
||||
|
||||
[dependencies]
|
||||
csv = "1.2.2"
|
||||
csv = "1.3.0"
|
||||
rand = {version = "0.8.5", features = ["small_rng"]}
|
||||
getrandom = {version = "0.2", features = ["js"]}
|
||||
wasm-bindgen = { version = "0.2.87", features = ["serde-serialize"] }
|
||||
serde_json = "1.0"
|
||||
serde = { version = "=1.0.171", features = ["derive"] }
|
||||
serde-wasm-bindgen = "0.4.5"
|
||||
tsify = { version = "0.4.5", features = ["js"] }
|
||||
|
||||
serde = { version = "1.0.202", features = ["derive"] }
|
217
wordgrid/src/api.rs
Normal file
217
wordgrid/src/api.rs
Normal 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(¤t_player)
|
||||
.unwrap()
|
||||
.player,
|
||||
Player::AI { .. }
|
||||
)
|
||||
}
|
||||
}
|
|
@ -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::fmt;
|
||||
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)]
|
||||
pub enum Direction {
|
||||
Row, Column
|
||||
Row,
|
||||
Column,
|
||||
}
|
||||
|
||||
impl Direction {
|
||||
pub fn invert(&self) -> Self {
|
||||
match &self {
|
||||
Direction::Row => {Direction::Column}
|
||||
Direction::Column => {Direction::Row}
|
||||
Direction::Row => Direction::Column,
|
||||
Direction::Column => Direction::Row,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -26,7 +26,6 @@ impl Direction {
|
|||
pub struct Coordinates(pub u8, pub u8);
|
||||
|
||||
impl Coordinates {
|
||||
|
||||
pub fn new_from_index(index: usize) -> Self {
|
||||
let y = 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> {
|
||||
let proposed = match direction {
|
||||
Direction::Column => {(self.0 as i8, self.1 as i8+i)}
|
||||
Direction::Row => {(self.0 as i8+i, self.1 as i8)}
|
||||
Direction::Column => (self.0 as i8, self.1 as i8 + i),
|
||||
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
|
||||
} else {
|
||||
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)]
|
||||
#[tsify(from_wasm_abi)]
|
||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||
pub struct Letter {
|
||||
pub text: char,
|
||||
pub points: u32,
|
||||
|
@ -81,32 +83,27 @@ impl Letter {
|
|||
|
||||
pub fn new(text: Option<char>, points: u32) -> Letter {
|
||||
match text {
|
||||
None => {
|
||||
Letter {
|
||||
None => Letter {
|
||||
text: ' ',
|
||||
points,
|
||||
ephemeral: true,
|
||||
is_blank: true,
|
||||
}
|
||||
}
|
||||
Some(text) => {
|
||||
Letter {
|
||||
},
|
||||
Some(text) => Letter {
|
||||
text,
|
||||
points,
|
||||
ephemeral: true,
|
||||
is_blank: false,
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn partial_match(&self, other: &Letter) -> bool {
|
||||
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 {
|
||||
Normal,
|
||||
DoubleWord,
|
||||
|
@ -141,12 +138,10 @@ impl<'a> ToString for Word<'a> {
|
|||
}
|
||||
|
||||
text
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Word<'a> {
|
||||
|
||||
pub fn calculate_score(&self) -> u32 {
|
||||
let mut multiplier = 1;
|
||||
let mut unmultiplied_score = 0;
|
||||
|
@ -154,15 +149,14 @@ impl <'a> Word<'a> {
|
|||
for cell in self.cells.as_slice() {
|
||||
let cell_value = cell.value.unwrap();
|
||||
if cell_value.ephemeral {
|
||||
let cell_multiplier =
|
||||
match cell.cell_type {
|
||||
CellType::Normal => {1}
|
||||
let cell_multiplier = match cell.cell_type {
|
||||
CellType::Normal => 1,
|
||||
CellType::DoubleWord => {
|
||||
multiplier *= 2;
|
||||
1
|
||||
}
|
||||
CellType::DoubleLetter => {2}
|
||||
CellType::TripleLetter => {3}
|
||||
CellType::DoubleLetter => 2,
|
||||
CellType::TripleLetter => 3,
|
||||
CellType::TripleWord => {
|
||||
multiplier *= 3;
|
||||
1
|
||||
|
@ -187,7 +181,6 @@ impl Board {
|
|||
pub fn new() -> Self {
|
||||
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
|
||||
///
|
||||
/// # Arguments
|
||||
|
@ -200,7 +193,7 @@ impl Board {
|
|||
GRID_LENGTH - x - 1
|
||||
} else {
|
||||
x
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
for i_orig in 0..GRID_LENGTH {
|
||||
|
@ -221,9 +214,7 @@ impl Board {
|
|||
}
|
||||
|
||||
// Double letters
|
||||
if (i % 4 == 2) && (j % 4 == 2) && !(
|
||||
i == 2 && j == 2
|
||||
) {
|
||||
if (i % 4 == 2) && (j % 4 == 2) && !(i == 2 && j == 2) {
|
||||
typee = CellType::DoubleLetter;
|
||||
}
|
||||
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,
|
||||
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 {
|
||||
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 {
|
||||
let index = coordinates.map_to_index();
|
||||
Ok(self.cells.get_mut(index).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub fn calculate_scores(&self, dictionary: &DictionaryImpl) -> Result<(Vec<(Word, u32)>, u32), String> {
|
||||
pub fn calculate_scores(
|
||||
&self,
|
||||
dictionary: &DictionaryImpl,
|
||||
) -> Result<(Vec<(Word, u32)>, u32), Error> {
|
||||
let (words, tiles_played) = self.find_played_words()?;
|
||||
let mut words_and_scores = Vec::new();
|
||||
let mut total_score = 0;
|
||||
|
||||
for word in words {
|
||||
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();
|
||||
|
@ -293,10 +287,9 @@ impl Board {
|
|||
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
|
||||
|
||||
|
||||
// Let's first establish what rows and columns tiles were played in
|
||||
let mut rows_played = HashSet::with_capacity(15);
|
||||
let mut columns_played = HashSet::with_capacity(15);
|
||||
|
@ -320,9 +313,9 @@ impl Board {
|
|||
}
|
||||
|
||||
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 {
|
||||
return Err("Tiles need to be played on one row or column")
|
||||
return Err(Error::TilesNotStraight);
|
||||
}
|
||||
|
||||
let direction = if rows_played.len() > 1 {
|
||||
|
@ -335,7 +328,9 @@ impl Board {
|
|||
let starting_column = *columns_played.iter().min().unwrap();
|
||||
|
||||
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 observed_tiles_played = 0;
|
||||
|
@ -359,25 +354,25 @@ impl Board {
|
|||
|
||||
// there are tiles not part of the main word
|
||||
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
|
||||
if main_word.cells.len() > 1 {
|
||||
words.push(main_word);
|
||||
} 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'
|
||||
let mut anchored = false;
|
||||
'outer: for word in words.as_slice() {
|
||||
for cell in word.cells.as_slice() {
|
||||
// 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;
|
||||
break 'outer;
|
||||
}
|
||||
|
@ -387,23 +382,27 @@ impl Board {
|
|||
if anchored {
|
||||
Ok((words, tiles_played))
|
||||
} 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 mut times_moved = 0;
|
||||
loop {
|
||||
let one_back = start_coords.add(direction, -times_moved);
|
||||
match one_back {
|
||||
None => { break }
|
||||
None => break,
|
||||
Some(new_coords) => {
|
||||
let cell = self.get_cell(new_coords).unwrap();
|
||||
if cell.value.is_some() {
|
||||
times_moved += 1;
|
||||
} else {
|
||||
break
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -423,11 +422,11 @@ impl Board {
|
|||
loop {
|
||||
let position = start_coords.add(direction, cells.len() as i8);
|
||||
match position {
|
||||
None => {break}
|
||||
None => break,
|
||||
Some(x) => {
|
||||
let cell = self.get_cell(x).unwrap();
|
||||
match cell.value {
|
||||
None => {break}
|
||||
None => break,
|
||||
Some(_) => {
|
||||
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 {
|
||||
{
|
||||
let cell = match self.get_cell_mut(coords) {
|
||||
Ok(cell) => {cell}
|
||||
Err(e) => {return Err(e.to_string())}
|
||||
};
|
||||
let cell = self.get_cell_mut(coords)?;
|
||||
|
||||
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;
|
||||
|
@ -476,7 +475,6 @@ impl Board {
|
|||
|
||||
impl fmt::Display for Board {
|
||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||
|
||||
let mut str = String::new();
|
||||
|
||||
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 color = match cell.cell_type {
|
||||
CellType::Normal => {normal}
|
||||
CellType::DoubleWord => {double_word}
|
||||
CellType::DoubleLetter => {double_letter}
|
||||
CellType::TripleLetter => {triple_letter}
|
||||
CellType::TripleWord => {triple_word}
|
||||
CellType::Start => {double_word}
|
||||
CellType::Normal => normal,
|
||||
CellType::DoubleWord => double_word,
|
||||
CellType::DoubleLetter => double_letter,
|
||||
CellType::TripleLetter => triple_letter,
|
||||
CellType::TripleWord => triple_word,
|
||||
CellType::Start => double_word,
|
||||
};
|
||||
|
||||
let content = match &cell.value {
|
||||
None => {' '}
|
||||
Some(letter) => {letter.text}
|
||||
None => ' ',
|
||||
Some(letter) => letter.text,
|
||||
};
|
||||
|
||||
|
||||
str.write_str(color).unwrap();
|
||||
str.write_char(content).unwrap();
|
||||
|
||||
|
||||
}
|
||||
str.write_str("\x1b[0m\n").unwrap();
|
||||
}
|
||||
|
||||
write!(f, "{}", str)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::dictionary::Dictionary;
|
||||
use super::*;
|
||||
use crate::dictionary::Dictionary;
|
||||
|
||||
#[test]
|
||||
fn test_cell_types() {
|
||||
let board = Board::new();
|
||||
|
||||
assert!(matches!(board.get_cell(Coordinates(0, 0)).unwrap().cell_type, CellType::TripleWord));
|
||||
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(0, 0)).unwrap().cell_type,
|
||||
CellType::TripleWord
|
||||
));
|
||||
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!(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(13, 13)).unwrap().cell_type,
|
||||
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!(board.get_cell(Coordinates(8, 6)).unwrap().cell_type, CellType::DoubleLetter));
|
||||
assert!(matches!(board.get_cell(Coordinates(5, 9)).unwrap().cell_type, CellType::TripleLetter));
|
||||
assert!(matches!(
|
||||
board.get_cell(Coordinates(7, 7)).unwrap().cell_type,
|
||||
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]
|
||||
fn test_cell_coordinates() {
|
||||
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(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(10, 8)).unwrap().value = Some(Letter::new_fixed('G', 0));
|
||||
|
||||
|
@ -584,7 +606,9 @@ mod tests {
|
|||
println!("x is {}", x);
|
||||
let first_word = board.find_word_at_position(Coordinates(8, x), Direction::Column);
|
||||
match first_word {
|
||||
None => { panic!("Expected to find word JOEL") }
|
||||
None => {
|
||||
panic!("Expected to find word JOEL")
|
||||
}
|
||||
Some(x) => {
|
||||
assert_eq!(x.coords.0, 8);
|
||||
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);
|
||||
match single_letter_word {
|
||||
None => { panic!("Expected to find letter L") }
|
||||
None => {
|
||||
panic!("Expected to find letter L")
|
||||
}
|
||||
Some(x) => {
|
||||
assert_eq!(x.coords.0, 8);
|
||||
assert_eq!(x.coords.1, 9);
|
||||
|
@ -609,7 +635,9 @@ mod tests {
|
|||
println!("x is {}", x);
|
||||
let word = board.find_word_at_position(Coordinates(x, 0), Direction::Row);
|
||||
match word {
|
||||
None => { panic!("Expected to find word IS") }
|
||||
None => {
|
||||
panic!("Expected to find word IS")
|
||||
}
|
||||
Some(x) => {
|
||||
assert_eq!(x.coords.0, 0);
|
||||
assert_eq!(x.coords.1, 0);
|
||||
|
@ -623,7 +651,9 @@ mod tests {
|
|||
println!("x is {}", x);
|
||||
let word = board.find_word_at_position(Coordinates(x, 0), Direction::Row);
|
||||
match word {
|
||||
None => { panic!("Expected to find word COOL") }
|
||||
None => {
|
||||
panic!("Expected to find word COOL")
|
||||
}
|
||||
Some(x) => {
|
||||
assert_eq!(x.coords.0, 3);
|
||||
assert_eq!(x.coords.1, 0);
|
||||
|
@ -638,7 +668,9 @@ mod tests {
|
|||
|
||||
let word = board.find_word_at_position(Coordinates(10, 8), Direction::Row);
|
||||
match word {
|
||||
None => { panic!("Expected to find word EGG") }
|
||||
None => {
|
||||
panic!("Expected to find word EGG")
|
||||
}
|
||||
Some(x) => {
|
||||
assert_eq!(x.coords.0, 8);
|
||||
assert_eq!(x.coords.1, 8);
|
||||
|
@ -659,10 +691,10 @@ mod tests {
|
|||
is_blank: false,
|
||||
});
|
||||
|
||||
match board.find_played_words() {
|
||||
Ok(_) => {panic!("Expected error")}
|
||||
Err(e) => {assert_eq!(e, "All words must be at least one letter");}
|
||||
}
|
||||
assert!(matches!(
|
||||
board.find_played_words(),
|
||||
Err(Error::OneLetterWord)
|
||||
));
|
||||
|
||||
board.get_cell_mut(Coordinates(7, 7)).unwrap().value = Some(Letter {
|
||||
text: 'I',
|
||||
|
@ -726,10 +758,7 @@ mod tests {
|
|||
board.get_cell_mut(Coordinates(8, 9)).unwrap().value = Some(make_letter('L', true));
|
||||
|
||||
let words = board.find_played_words();
|
||||
match words {
|
||||
Ok(_) => {panic!("Expected the not-anchored error")}
|
||||
Err(x) => {assert_eq!(x, "Played tiles must be anchored to something")}
|
||||
}
|
||||
assert!(matches!(words, Err(Error::UnanchoredWord)));
|
||||
|
||||
// Adding anchor
|
||||
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());
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
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
|
||||
|
@ -771,15 +799,8 @@ mod tests {
|
|||
board.get_cell_mut(Coordinates(8, 12)).unwrap().value = Some(Letter::new_fixed('S', 0));
|
||||
|
||||
let words = board.find_played_words();
|
||||
match words {
|
||||
Ok(_) => {panic!("Expected to find an error!")}
|
||||
Err(x) => {
|
||||
assert_eq!(x, "Played tiles cannot have empty gap")
|
||||
assert!(matches!(words, Err(Error::TilesHaveGap)));
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_word_finding_whole_board() {
|
||||
|
@ -795,10 +816,7 @@ mod tests {
|
|||
}
|
||||
|
||||
let words = board.find_played_words();
|
||||
match words {
|
||||
Ok(_) => {panic!("Expected to find no words")}
|
||||
Err(x) => {assert_eq!(x, "Tiles need to be played")}
|
||||
}
|
||||
assert!(matches!(words, Err(Error::NoTilesPlayed)));
|
||||
|
||||
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));
|
||||
|
@ -814,7 +832,7 @@ mod tests {
|
|||
board.get_cell_mut(Coordinates(6, 0)).unwrap().value = Some(Letter::new_fixed('L', 1));
|
||||
|
||||
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);
|
||||
let words = board.find_played_words();
|
||||
match words {
|
||||
|
@ -827,7 +845,9 @@ mod tests {
|
|||
|
||||
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| {
|
||||
|
@ -844,12 +864,21 @@ mod tests {
|
|||
return direction;
|
||||
};
|
||||
|
||||
board.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));
|
||||
board
|
||||
.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 {
|
||||
None => {panic!("Expected to find word EGG")}
|
||||
None => {
|
||||
panic!("Expected to find word EGG")
|
||||
}
|
||||
Some(x) => {
|
||||
assert_eq!(x.coords.0, 8);
|
||||
assert_eq!(x.coords.1, 8);
|
||||
|
@ -857,7 +886,6 @@ mod tests {
|
|||
assert_eq!(x.to_string(), "EGG");
|
||||
assert_eq!(x.calculate_score(), 2 + 2 + 2);
|
||||
assert!(dictionary.is_word_valid(&x));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -876,20 +904,29 @@ mod tests {
|
|||
assert_eq!(word.calculate_score(), 8 + 1 + 2 + 1);
|
||||
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);
|
||||
match scores {
|
||||
Ok(_) => {panic!("Expected an error")}
|
||||
Err(e) => {assert_eq!(e, "JOEL is not a valid word")}
|
||||
Ok(_) => {
|
||||
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();
|
||||
alt_dictionary.insert("JOEL".to_string(), 0.5);
|
||||
alt_dictionary.insert("EGG".to_string(), 0.5);
|
||||
|
||||
|
||||
let scores = board.calculate_scores(&alt_dictionary);
|
||||
match scores {
|
||||
Ok((words, total_score)) => {
|
||||
|
@ -906,18 +943,19 @@ mod tests {
|
|||
|
||||
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
|
||||
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();
|
||||
match words {
|
||||
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") }
|
||||
}
|
||||
assert!(matches!(words, Err(Error::TilesNotStraight)));
|
||||
}
|
||||
|
||||
// make a copy of the board now with x and y swapped
|
||||
|
@ -934,7 +972,6 @@ mod tests {
|
|||
cell_new.value = Some(*x);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -943,8 +980,5 @@ mod tests {
|
|||
|
||||
println!("Checking inverted board");
|
||||
check_board(&mut inverted_board, true);
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
use rand::prelude::SliceRandom;
|
||||
use rand::{Rng};
|
||||
use crate::board::Letter;
|
||||
use rand::prelude::SliceRandom;
|
||||
use rand::Rng;
|
||||
|
||||
pub const GRID_LENGTH: u8 = 15;
|
||||
pub const TRAY_LENGTH: u8 = 7;
|
||||
|
@ -49,5 +49,4 @@ pub fn standard_tile_pool<R: Rng>(rng: Option<&mut R>) -> Vec<Letter> {
|
|||
}
|
||||
|
||||
letters
|
||||
|
||||
}
|
|
@ -1,10 +1,9 @@
|
|||
use crate::board::Word;
|
||||
use csv::Reader;
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::str::FromStr;
|
||||
use csv::Reader;
|
||||
use crate::board::Word;
|
||||
|
||||
pub trait Dictionary {
|
||||
|
||||
fn create_from_reader<T: std::io::Read>(reader: Reader<T>) -> Self;
|
||||
fn create_from_path(path: &str) -> Self;
|
||||
fn create_from_str(data: &str) -> Self;
|
||||
|
@ -15,7 +14,6 @@ pub trait Dictionary {
|
|||
pub type DictionaryImpl = HashMap<String, f64>;
|
||||
|
||||
impl Dictionary for DictionaryImpl {
|
||||
|
||||
fn create_from_reader<T: std::io::Read>(mut reader: Reader<T>) -> Self {
|
||||
let mut map = HashMap::new();
|
||||
|
||||
|
@ -27,7 +25,6 @@ impl Dictionary for DictionaryImpl{
|
|||
let score = f64::from_str(score).unwrap();
|
||||
|
||||
map.insert(word, score);
|
||||
|
||||
}
|
||||
|
||||
map
|
||||
|
@ -57,7 +54,6 @@ impl Dictionary for DictionaryImpl{
|
|||
}
|
||||
|
||||
map
|
||||
|
||||
}
|
||||
|
||||
fn substring_set(&self) -> HashSet<&str> {
|
||||
|
@ -69,7 +65,6 @@ impl Dictionary for DictionaryImpl{
|
|||
set.insert(&word[j..k]);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
set
|
||||
|
@ -85,10 +80,9 @@ impl Dictionary for DictionaryImpl{
|
|||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
#[test]
|
||||
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);
|
||||
|
||||
|
@ -96,7 +90,6 @@ mod tests {
|
|||
assert!(dictionary.contains_key("AARDVARK"));
|
||||
|
||||
assert!((dictionary.get("AARDVARK").unwrap() - 0.5798372).abs() < 0.0001)
|
||||
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -131,11 +124,8 @@ mod tests {
|
|||
assert!(set.contains("JOH"));
|
||||
assert!(set.contains("OHN"));
|
||||
|
||||
|
||||
assert!(!set.contains("XY"));
|
||||
assert!(!set.contains("JH"));
|
||||
assert!(!set.contains("JE"));
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -1,15 +1,15 @@
|
|||
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::constants::{standard_tile_pool, TRAY_LENGTH};
|
||||
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 rand::prelude::SliceRandom;
|
||||
use rand::rngs::SmallRng;
|
||||
use rand::SeedableRng;
|
||||
use serde::{Deserialize, Serialize, Serializer};
|
||||
|
||||
pub enum Player {
|
||||
Human(String),
|
||||
|
@ -17,15 +17,74 @@ pub enum Player {
|
|||
name: String,
|
||||
difficulty: Difficulty,
|
||||
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 {
|
||||
|
||||
pub fn get_name(&self) -> &str {
|
||||
match &self {
|
||||
Player::Human(name) => {name}
|
||||
Player::AI { name, .. } => {name}
|
||||
Player::Human(name) => name,
|
||||
Player::AI { name, .. } => name,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -33,18 +92,20 @@ impl Player {
|
|||
pub struct PlayerState {
|
||||
pub player: Player,
|
||||
pub score: u32,
|
||||
pub tray: Tray
|
||||
pub tray: Tray,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Tsify, Copy, Clone, Debug)]
|
||||
#[tsify(from_wasm_abi)]
|
||||
#[derive(Deserialize, Copy, Clone, Debug)]
|
||||
pub struct PlayedTile {
|
||||
pub index: usize,
|
||||
pub character: Option<char>, // we only set this if PlayedTile is a blank
|
||||
}
|
||||
|
||||
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();
|
||||
for (i, played_tile) in tray_tile_locations.iter().enumerate() {
|
||||
if played_tile.is_some() {
|
||||
|
@ -55,7 +116,9 @@ impl PlayedTile {
|
|||
if letter.is_blank {
|
||||
match played_tile.character {
|
||||
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) => {
|
||||
// TODO - check that x is a valid alphabet letter
|
||||
|
@ -64,7 +127,6 @@ impl PlayedTile {
|
|||
}
|
||||
}
|
||||
played_letters.push((letter, coord));
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -72,15 +134,13 @@ impl PlayedTile {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Tsify)]
|
||||
#[tsify(from_wasm_abi)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct WordResult {
|
||||
word: String,
|
||||
score: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Tsify)]
|
||||
#[tsify(from_wasm_abi)]
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct ScoreResult {
|
||||
words: Vec<WordResult>,
|
||||
total: u32,
|
||||
|
@ -88,7 +148,6 @@ pub struct ScoreResult {
|
|||
|
||||
pub struct PlayerStates(pub Vec<PlayerState>);
|
||||
impl PlayerStates {
|
||||
|
||||
fn get_player_name_by_turn_id(&self, id: usize) -> &str {
|
||||
let id_mod = id % self.0.len();
|
||||
let state = self.0.get(id_mod).unwrap();
|
||||
|
@ -97,37 +156,33 @@ impl PlayerStates {
|
|||
}
|
||||
|
||||
pub fn get_player_state(&self, name: &str) -> Option<&PlayerState> {
|
||||
self.0.iter()
|
||||
self.0
|
||||
.iter()
|
||||
.filter(|state| state.player.get_name().eq(name))
|
||||
.nth(0)
|
||||
|
||||
}
|
||||
|
||||
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))
|
||||
.nth(0)
|
||||
|
||||
}
|
||||
|
||||
pub fn get_tray(&self, name: &str) -> Option<&Tray> {
|
||||
let player = self.get_player_state(name)?;
|
||||
|
||||
Some(&player.tray)
|
||||
|
||||
}
|
||||
|
||||
pub fn get_tray_mut(&mut self, name: &str) -> Option<&mut Tray> {
|
||||
let player = self.get_player_state_mut(name)?;
|
||||
|
||||
Some(&mut player.tray)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[derive(Deserialize, Serialize, Tsify, Debug, Clone)]
|
||||
#[tsify(from_wasm_abi)]
|
||||
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum GameState {
|
||||
InProgress,
|
||||
|
@ -148,15 +203,20 @@ pub struct Game{
|
|||
state: GameState,
|
||||
}
|
||||
|
||||
|
||||
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 letters = standard_tile_pool(Some(&mut rng));
|
||||
|
||||
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| {
|
||||
let mut tray = Tray::new(TRAY_LENGTH);
|
||||
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) {
|
||||
self.board = new_board;
|
||||
|
@ -223,14 +283,18 @@ impl Game {
|
|||
&self.dictionary
|
||||
}
|
||||
|
||||
fn verify_game_in_progress(&self) -> Result<(), String> {
|
||||
fn verify_game_in_progress(&self) -> Result<(), Error> {
|
||||
if !matches!(self.state, GameState::InProgress) {
|
||||
return Err("Moves cannot be made after a game has finished".to_string());
|
||||
return Err(Error::GameFinished);
|
||||
}
|
||||
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()?;
|
||||
|
||||
let player = self.current_player_name();
|
||||
|
@ -238,7 +302,8 @@ impl Game {
|
|||
let mut board_instance = self.get_board().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() {
|
||||
if played_tile.is_some() {
|
||||
*tray.letters.get_mut(i).unwrap() = None;
|
||||
|
@ -247,17 +312,14 @@ impl Game {
|
|||
|
||||
board_instance.receive_play(played_letters)?;
|
||||
|
||||
|
||||
let x = board_instance.calculate_scores(self.get_dictionary())?;
|
||||
let total_score = x.1;
|
||||
|
||||
let words: Vec<WordResult> = x.0.iter()
|
||||
.map(|(word, score)| {
|
||||
WordResult {
|
||||
let words: Vec<WordResult> =
|
||||
x.0.iter()
|
||||
.map(|(word, score)| WordResult {
|
||||
word: word.to_string(),
|
||||
score: *score
|
||||
}
|
||||
|
||||
score: *score,
|
||||
})
|
||||
.collect();
|
||||
|
||||
|
@ -276,28 +338,42 @@ impl Game {
|
|||
// game is over
|
||||
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 {
|
||||
words,
|
||||
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()?;
|
||||
|
||||
let player = self.current_player_name();
|
||||
let tray = match self.player_states.get_tray_mut(&player) {
|
||||
None => {return Err(format!("Player {} not found", player))}
|
||||
Some(x) => {x}
|
||||
None => return Err(Error::InvalidPlayer(player)),
|
||||
Some(x) => x,
|
||||
};
|
||||
|
||||
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;
|
||||
|
@ -321,8 +397,11 @@ impl Game {
|
|||
|
||||
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) {
|
||||
|
@ -331,7 +410,7 @@ impl Game {
|
|||
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()?;
|
||||
Ok(self.increment_turn(false).clone())
|
||||
}
|
||||
|
@ -345,7 +424,6 @@ impl Game {
|
|||
if self.turns_not_played >= 2 * self.player_states.0.len() {
|
||||
self.end_game(None);
|
||||
}
|
||||
|
||||
} else {
|
||||
self.turns_not_played = 0;
|
||||
}
|
||||
|
@ -354,7 +432,6 @@ impl Game {
|
|||
}
|
||||
|
||||
fn end_game(&mut self, finisher: Option<String>) {
|
||||
|
||||
let mut finished_letters_map = HashMap::new();
|
||||
let mut points_forfeit = 0;
|
||||
|
||||
|
@ -376,23 +453,28 @@ impl Game {
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
self.state = GameState::Ended {
|
||||
finisher,
|
||||
remaining_tiles: finished_letters_map
|
||||
remaining_tiles: finished_letters_map,
|
||||
};
|
||||
}
|
||||
|
||||
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 state = self.player_states.get_player_state_mut(¤t_player).ok_or("There should be a player available")?;
|
||||
let state = self
|
||||
.player_states
|
||||
.get_player_state_mut(¤t_player)
|
||||
.ok_or(Error::InvalidPlayer(current_player.clone()))?;
|
||||
|
||||
if let Player::AI { object, .. } = &mut state.player {
|
||||
let tray = &mut state.tray;
|
||||
|
@ -407,7 +489,7 @@ impl Game {
|
|||
match tile_spot {
|
||||
None => {
|
||||
to_exchange.push(false);
|
||||
},
|
||||
}
|
||||
Some(_) => {
|
||||
to_exchange.push(true);
|
||||
}
|
||||
|
@ -416,34 +498,44 @@ impl Game {
|
|||
|
||||
if self.tile_pool.is_empty() {
|
||||
let game_state = self.increment_turn(false);
|
||||
Ok((TurnAdvanceResult::AIMove {
|
||||
Ok((
|
||||
TurnAdvanceResult::AIMove {
|
||||
name: current_player,
|
||||
action: TurnAction::Pass,
|
||||
}, game_state.clone()))
|
||||
},
|
||||
game_state.clone(),
|
||||
))
|
||||
} else {
|
||||
let (_, action, game_state) = self.exchange_tiles(to_exchange)?;
|
||||
Ok((TurnAdvanceResult::AIMove {
|
||||
Ok((
|
||||
TurnAdvanceResult::AIMove {
|
||||
name: current_player,
|
||||
action,
|
||||
}, game_state))
|
||||
},
|
||||
game_state,
|
||||
))
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Some(best_move) => {
|
||||
let play = best_move.convert_to_play(tray);
|
||||
let (action, game_state) = self.receive_play(play, true)?;
|
||||
Ok((TurnAdvanceResult::AIMove {
|
||||
Ok((
|
||||
TurnAdvanceResult::AIMove {
|
||||
name: current_player,
|
||||
action,
|
||||
}, game_state))
|
||||
},
|
||||
game_state,
|
||||
))
|
||||
}
|
||||
}
|
||||
} 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 {
|
||||
|
@ -452,60 +544,61 @@ impl Game {
|
|||
|
||||
pub fn get_player_tile_count(&self, player: &str) -> Result<usize, String> {
|
||||
let tray = match self.player_states.get_tray(&player) {
|
||||
None => {return Err(format!("Player {} not found", player))}
|
||||
Some(x) => {x}
|
||||
None => return Err(format!("Player {} not found", player)),
|
||||
Some(x) => x,
|
||||
};
|
||||
|
||||
Ok(
|
||||
tray.count()
|
||||
)
|
||||
Ok(tray.count())
|
||||
}
|
||||
|
||||
|
||||
|
||||
pub fn get_state(&self) -> GameState {
|
||||
self.state.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Tsify, Debug)]
|
||||
#[tsify(from_wasm_abi)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum TurnAction {
|
||||
Pass,
|
||||
ExchangeTiles {
|
||||
tiles_exchanged: usize
|
||||
tiles_exchanged: usize,
|
||||
},
|
||||
PlayTiles {
|
||||
result: ScoreResult
|
||||
result: ScoreResult,
|
||||
locations: Vec<usize>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Tsify, Debug)]
|
||||
#[tsify(from_wasm_abi)]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum TurnAdvanceResult {
|
||||
HumanInputRequired{
|
||||
name: String
|
||||
},
|
||||
AIMove{
|
||||
name: String,
|
||||
action: TurnAction,
|
||||
}
|
||||
HumanInputRequired { name: String },
|
||||
AIMove { name: String, action: TurnAction },
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use std::fs;
|
||||
use crate::game::Game;
|
||||
use crate::player_interaction::ai::Difficulty;
|
||||
use std::fs;
|
||||
|
||||
#[test]
|
||||
fn test_game() {
|
||||
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 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();
|
||||
println!("Current player is {current_player}");
|
||||
|
@ -522,7 +615,5 @@ mod tests {
|
|||
|
||||
assert_eq!(game.current_player_name(), "Player");
|
||||
assert_eq!(0, game.turns_not_played);
|
||||
|
||||
}
|
||||
|
||||
}
|
6
wordgrid/src/lib.rs
Normal file
6
wordgrid/src/lib.rs
Normal file
|
@ -0,0 +1,6 @@
|
|||
pub mod api;
|
||||
pub mod board;
|
||||
pub mod constants;
|
||||
pub mod dictionary;
|
||||
pub mod game;
|
||||
pub mod player_interaction;
|
|
@ -1,14 +1,11 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use tsify::Tsify;
|
||||
use crate::board::Letter;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod ai;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Tsify, Clone)]
|
||||
#[tsify(from_wasm_abi)]
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct Tray {
|
||||
pub letters: Vec<Option<Letter>>
|
||||
pub letters: Vec<Option<Letter>>,
|
||||
}
|
||||
|
||||
impl Tray {
|
||||
|
@ -17,9 +14,7 @@ impl Tray {
|
|||
for _ in 0..tray_length {
|
||||
letters.push(None);
|
||||
}
|
||||
Tray {
|
||||
letters
|
||||
}
|
||||
Tray { letters }
|
||||
}
|
||||
|
||||
pub fn fill(&mut self, standard_tile_pool: &mut Vec<Letter>) {
|
||||
|
@ -37,25 +32,20 @@ impl Tray {
|
|||
}
|
||||
|
||||
pub fn count(&self) -> usize {
|
||||
self.letters.iter()
|
||||
.filter(|l| l.is_some())
|
||||
.count()
|
||||
self.letters.iter().filter(|l| l.is_some()).count()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
|
||||
#[test]
|
||||
fn test_tray() {
|
||||
let mut letters = vec![
|
||||
Letter::new(Some('E'), 3),
|
||||
Letter::new(Some('O'), 2),
|
||||
Letter::new(Some('J'), 1)
|
||||
Letter::new(Some('J'), 1),
|
||||
];
|
||||
|
||||
let mut tray = Tray::new(5);
|
||||
|
@ -89,8 +79,5 @@ mod tests {
|
|||
assert_eq!(tray.letters.get(2).unwrap().unwrap().text, 'E');
|
||||
assert!(tray.letters.get(3).unwrap().is_none());
|
||||
assert!(tray.letters.get(4).unwrap().is_none());
|
||||
|
||||
|
||||
}
|
||||
|
||||
}
|
|
@ -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::constants::GRID_LENGTH;
|
||||
use crate::dictionary::DictionaryImpl;
|
||||
use crate::game::PlayedTile;
|
||||
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 {
|
||||
direction: Direction,
|
||||
|
@ -18,18 +20,13 @@ struct CoordinateLineMapper {
|
|||
impl CoordinateLineMapper {
|
||||
fn line_to_coord(&self, variable: u8) -> Coordinates {
|
||||
match self.direction {
|
||||
Direction::Row => {
|
||||
Coordinates(variable, self.fixed)
|
||||
}
|
||||
Direction::Column => {
|
||||
Coordinates(self.fixed, variable)
|
||||
}
|
||||
Direction::Row => Coordinates(variable, self.fixed),
|
||||
Direction::Column => Coordinates(self.fixed, variable),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Serialize, Deserialize, Tsify)]
|
||||
#[tsify(from_wasm_abi)]
|
||||
#[derive(Copy, Clone, Serialize, Deserialize)]
|
||||
pub struct Difficulty {
|
||||
pub proportion: f64,
|
||||
pub randomness: f64,
|
||||
|
@ -48,7 +45,6 @@ pub struct AI {
|
|||
type MoveMap = HashMap<char, u32>;
|
||||
type CrossTiles = Vec<Option<MoveMap>>;
|
||||
|
||||
|
||||
#[derive(Debug, Eq, PartialEq, Hash)]
|
||||
pub struct CompleteMove {
|
||||
moves: Vec<MoveComponent>,
|
||||
|
@ -56,10 +52,11 @@ pub struct CompleteMove {
|
|||
}
|
||||
|
||||
impl CompleteMove {
|
||||
|
||||
pub fn convert_to_play(&self, tray: &Tray) -> Vec<Option<PlayedTile>> {
|
||||
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()))
|
||||
.collect::<Vec<Option<MoveComponent>>>();
|
||||
|
||||
|
@ -88,7 +85,7 @@ impl CompleteMove {
|
|||
found_match = true;
|
||||
break;
|
||||
}
|
||||
},
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
}
|
||||
|
@ -127,13 +124,13 @@ struct MoveComponent {
|
|||
}
|
||||
|
||||
impl AI {
|
||||
|
||||
pub fn new(difficulty: Difficulty, dictionary: &DictionaryImpl) -> Self {
|
||||
let mut ai_dictionary = HashSet::new();
|
||||
let mut substrings = HashSet::new();
|
||||
|
||||
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());
|
||||
substrings.insert(word.clone());
|
||||
|
@ -143,13 +140,12 @@ impl AI {
|
|||
substrings.insert(word[i..j].to_string());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
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 mut best_move: Option<(CompleteMove, f64)> = None;
|
||||
|
||||
for possible_move in move_set {
|
||||
let move_score =
|
||||
if self.difficulty.randomness > 0.0 {
|
||||
(1.0 - self.difficulty.randomness) * (possible_move.score as f64) + self.difficulty.randomness * rng.gen_range(0.0..1.0)
|
||||
let move_score = if self.difficulty.randomness > 0.0 {
|
||||
(1.0 - self.difficulty.randomness) * (possible_move.score as f64)
|
||||
+ self.difficulty.randomness * rng.gen_range(0.0..1.0)
|
||||
} else {
|
||||
possible_move.score as f64
|
||||
};
|
||||
|
@ -194,10 +195,9 @@ impl AI {
|
|||
}
|
||||
|
||||
return match best_move {
|
||||
None => {None}
|
||||
Some((best_move, _)) => {Some(best_move)}
|
||||
None => None,
|
||||
Some((best_move, _)) => Some(best_move),
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
fn find_all_moves(&mut self, tray: &Tray, grid: &Board) -> HashSet<CompleteMove> {
|
||||
|
@ -215,15 +215,17 @@ impl AI {
|
|||
&self,
|
||||
tray: &Tray,
|
||||
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
|
||||
let cross_tiles = match direction {
|
||||
Direction::Column => {&self.row_cross_tiles}
|
||||
Direction::Row => {&self.column_cross_tiles}
|
||||
Direction::Column => &self.row_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())
|
||||
.map(|letter| letter.unwrap())
|
||||
.collect::<Vec<Letter>>();
|
||||
|
@ -234,7 +236,7 @@ impl AI {
|
|||
|
||||
let coord_mapper = CoordinateLineMapper {
|
||||
direction,
|
||||
fixed: k
|
||||
fixed: k,
|
||||
};
|
||||
|
||||
for p in 0..GRID_LENGTH {
|
||||
|
@ -246,13 +248,12 @@ impl AI {
|
|||
for l in 0..GRID_LENGTH {
|
||||
let coords = coord_mapper.line_to_coord(l);
|
||||
|
||||
let is_anchored = check_if_anchored(&self.board_view, coords, Direction::Row) ||
|
||||
check_if_anchored(&self.board_view, coords, Direction::Column);
|
||||
let is_anchored = check_if_anchored(&self.board_view, coords, Direction::Row)
|
||||
|| check_if_anchored(&self.board_view, coords, Direction::Column);
|
||||
|
||||
if is_anchored &&
|
||||
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
|
||||
if is_anchored && 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
|
||||
{
|
||||
|
||||
self.evaluate_spot_heading_left(
|
||||
&line_letters,
|
||||
&line_cross_letters,
|
||||
|
@ -262,17 +263,11 @@ impl AI {
|
|||
&Vec::new(),
|
||||
1,
|
||||
&MoveScoring::new(),
|
||||
|
||||
all_moves
|
||||
|
||||
all_moves,
|
||||
);
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
fn evaluate_spot_heading_left(
|
||||
|
@ -287,10 +282,8 @@ impl AI {
|
|||
min_length: usize,
|
||||
current_points: &MoveScoring,
|
||||
|
||||
all_moves: &mut HashSet<CompleteMove>
|
||||
|
||||
all_moves: &mut HashSet<CompleteMove>,
|
||||
) {
|
||||
|
||||
if line_index < 0 || line_index >= GRID_LENGTH as i8 {
|
||||
return;
|
||||
}
|
||||
|
@ -300,10 +293,13 @@ impl AI {
|
|||
Some(_) => {
|
||||
// there's a letter here; need to take a step left if we can
|
||||
|
||||
if !(line_index >= 1 &&
|
||||
line_letters.get((line_index-1) as usize).unwrap().is_some() &&
|
||||
min_length == 1
|
||||
) {
|
||||
if !(line_index >= 1
|
||||
&& line_letters
|
||||
.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,
|
||||
// just stop. Other versions of the for-loops that call this function will have picked up that case.
|
||||
self.evaluate_spot_heading_left(
|
||||
|
@ -315,9 +311,8 @@ impl AI {
|
|||
current_play,
|
||||
min_length,
|
||||
current_points,
|
||||
all_moves
|
||||
all_moves,
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
None => {
|
||||
|
@ -331,10 +326,9 @@ impl AI {
|
|||
min_length,
|
||||
current_points,
|
||||
coord_mapper,
|
||||
|
||||
&available_letters,
|
||||
letter.clone(),
|
||||
all_moves
|
||||
all_moves,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -348,12 +342,10 @@ impl AI {
|
|||
current_play,
|
||||
min_length + 1,
|
||||
current_points,
|
||||
all_moves
|
||||
all_moves,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
fn evaluate_letter_at_spot(
|
||||
|
@ -366,13 +358,10 @@ impl AI {
|
|||
current_points: &MoveScoring,
|
||||
coord_mapper: &CoordinateLineMapper,
|
||||
|
||||
|
||||
available_letters: &Vec<Letter>,
|
||||
letter: Letter,
|
||||
all_moves: &mut HashSet<CompleteMove>
|
||||
|
||||
all_moves: &mut HashSet<CompleteMove>,
|
||||
) {
|
||||
|
||||
if letter.is_blank {
|
||||
// need to loop through alphabet
|
||||
for alpha in ALPHABET {
|
||||
|
@ -389,8 +378,7 @@ impl AI {
|
|||
coord_mapper,
|
||||
available_letters,
|
||||
letter,
|
||||
all_moves
|
||||
|
||||
all_moves,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
|
@ -404,10 +392,9 @@ impl AI {
|
|||
coord_mapper,
|
||||
available_letters,
|
||||
letter.clone(),
|
||||
all_moves
|
||||
all_moves,
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fn evaluate_non_blank_letter_at_spot(
|
||||
|
@ -419,15 +406,12 @@ impl AI {
|
|||
min_length: usize,
|
||||
current_points: &MoveScoring,
|
||||
|
||||
|
||||
coord_mapper: &CoordinateLineMapper,
|
||||
available_letters: &Vec<Letter>,
|
||||
letter: Letter,
|
||||
|
||||
all_moves: &mut HashSet<CompleteMove>,
|
||||
|
||||
) {
|
||||
|
||||
// let's now assign the letter to this spot
|
||||
let mut line_letters = line_letters.clone();
|
||||
*line_letters.get_mut(line_index as usize).unwrap() = Some(letter);
|
||||
|
@ -458,7 +442,11 @@ impl AI {
|
|||
|
||||
// making copy
|
||||
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
|
||||
if let Some(map) = cross_letters {
|
||||
|
@ -508,7 +496,8 @@ impl AI {
|
|||
current_points.main_scoring += letter.points * 2;
|
||||
}
|
||||
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
|
||||
|
@ -516,7 +505,8 @@ impl AI {
|
|||
if word.len() >= min_length && self.dictionary.contains(&word) {
|
||||
let new_move = CompleteMove {
|
||||
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);
|
||||
}
|
||||
|
@ -525,8 +515,7 @@ impl AI {
|
|||
let mut new_available_letters = Vec::with_capacity(available_letters.len() - 1);
|
||||
let mut skipped_one = false;
|
||||
|
||||
available_letters.iter()
|
||||
.for_each(|in_tray| {
|
||||
available_letters.iter().for_each(|in_tray| {
|
||||
if skipped_one || !in_tray.partial_match(&letter) {
|
||||
new_available_letters.push(*in_tray);
|
||||
} else {
|
||||
|
@ -543,11 +532,9 @@ impl AI {
|
|||
current_play,
|
||||
min_length,
|
||||
¤t_points,
|
||||
|
||||
coord_mapper,
|
||||
&new_available_letters,
|
||||
|
||||
all_moves
|
||||
all_moves,
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -560,13 +547,11 @@ impl AI {
|
|||
min_length: usize,
|
||||
current_points: &MoveScoring,
|
||||
|
||||
|
||||
coord_mapper: &CoordinateLineMapper,
|
||||
available_letters: &Vec<Letter>,
|
||||
|
||||
all_moves: &mut HashSet<CompleteMove>,
|
||||
) {
|
||||
|
||||
// out-of-bounds check
|
||||
if line_index < 0 || line_index >= GRID_LENGTH as i8 {
|
||||
return;
|
||||
|
@ -586,7 +571,7 @@ impl AI {
|
|||
¤t_points,
|
||||
coord_mapper,
|
||||
available_letters,
|
||||
all_moves
|
||||
all_moves,
|
||||
);
|
||||
}
|
||||
None => {
|
||||
|
@ -601,10 +586,8 @@ impl AI {
|
|||
current_points,
|
||||
coord_mapper,
|
||||
available_letters,
|
||||
|
||||
letter.clone(),
|
||||
all_moves
|
||||
|
||||
all_moves,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -625,16 +608,20 @@ impl AI {
|
|||
}
|
||||
|
||||
let mut check_for_valid_moves = |coords: Coordinates| {
|
||||
|
||||
let cell = new.get_cell(coords).unwrap();
|
||||
if cell.value.is_none() {
|
||||
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;
|
||||
|
||||
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;
|
||||
}
|
||||
};
|
||||
|
@ -652,10 +639,14 @@ impl AI {
|
|||
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);
|
||||
if !is_anchored {
|
||||
return None;
|
||||
|
@ -687,11 +678,9 @@ impl AI {
|
|||
|
||||
Some(new_map)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
fn check_if_anchored(grid: &Board, coords: Coordinates, direction: Direction) -> bool {
|
||||
|
||||
// Middle tile is always considered anchored
|
||||
if coords.0 == GRID_LENGTH / 2 && coords.1 == GRID_LENGTH / 2 {
|
||||
return true;
|
||||
|
@ -699,7 +688,7 @@ fn check_if_anchored(grid: &Board, coords: Coordinates, direction: Direction) ->
|
|||
|
||||
let has_letter = |alt_coord: Option<Coordinates>| -> bool {
|
||||
match alt_coord {
|
||||
None => {false}
|
||||
None => false,
|
||||
Some(alt_coord) => {
|
||||
let cell = grid.get_cell(alt_coord).unwrap();
|
||||
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 {
|
||||
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;
|
||||
}
|
||||
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();
|
||||
line_letters[(start_word as usize)..((end_word + 1) as usize)]
|
||||
.iter().for_each(|letter| {
|
||||
.iter()
|
||||
.for_each(|letter| {
|
||||
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)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::dictionary::Dictionary;
|
||||
use rand::rngs::SmallRng;
|
||||
use rand::SeedableRng;
|
||||
use crate::dictionary::Dictionary;
|
||||
use super::*;
|
||||
|
||||
fn set_cell(board: &mut Board, x: u8, y: u8, letter: char, points: u32) {
|
||||
let cell = board.get_cell_mut(Coordinates(x, y)).unwrap();
|
||||
|
@ -832,14 +826,12 @@ mod tests {
|
|||
|
||||
assert_eq!(moves.len(), 10);
|
||||
|
||||
tray.letters[4] = Some(
|
||||
Letter {
|
||||
tray.letters[4] = Some(Letter {
|
||||
text: ' ',
|
||||
points: 0,
|
||||
ephemeral: false,
|
||||
is_blank: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
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, 10, 'T', 1);
|
||||
|
||||
|
||||
|
||||
let difficulty = Difficulty {
|
||||
proportion: 0.0, // restrict yourself to words with this proportion OR HIGHER
|
||||
randomness: 0.0,
|
||||
|
@ -898,7 +888,10 @@ mod tests {
|
|||
|
||||
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_eq!(end_of_boat.as_ref().unwrap().len(), 1);
|
||||
|
||||
|
@ -906,7 +899,6 @@ mod tests {
|
|||
|
||||
println!("Moves are {:?}", moves);
|
||||
|
||||
|
||||
// 3 possible moves -
|
||||
// 1. put 'S' at the end of 'BOAT' and form words 'SLAM' and 'BOATS'
|
||||
// 2. Put 'S' at end of 'BOAT'
|
||||
|
@ -920,7 +912,6 @@ mod tests {
|
|||
|
||||
let play = best_move.convert_to_play(&tray);
|
||||
println!("Play is {:?}", play);
|
||||
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -943,15 +934,20 @@ mod tests {
|
|||
let above_cell_coords = Coordinates(7, 6);
|
||||
let left_cell_cords = Coordinates(6, 7);
|
||||
|
||||
let row_cross_tiles = ai.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();
|
||||
let row_cross_tiles = ai
|
||||
.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!(column_cross_tiles.as_ref().unwrap().len(), 1);
|
||||
|
||||
let far_off_tiles = ai.row_cross_tiles.get(0).unwrap();
|
||||
assert!(far_off_tiles.is_none());
|
||||
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -992,12 +988,14 @@ mod tests {
|
|||
fn test_starting_move() {
|
||||
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();
|
||||
dictionary.insert("TWEETED".to_string(), 0.2);
|
||||
|
||||
|
||||
let mut tray = Tray::new(7);
|
||||
tray.letters[0] = Some(Letter {
|
||||
text: 'I',
|
||||
|
@ -1048,7 +1046,5 @@ mod tests {
|
|||
println!("Available moves are {:?}", all_moves);
|
||||
|
||||
assert!(all_moves.is_empty());
|
||||
|
||||
}
|
||||
|
||||
}
|
Loading…
Reference in a new issue