Compare commits
1 commit
main
...
multiplaye
Author | SHA1 | Date | |
---|---|---|---|
45cc83bfb2 |
26 changed files with 2903 additions and 1832 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -1,4 +1,5 @@
|
||||||
/target
|
target/
|
||||||
/Cargo.lock
|
/Cargo.lock
|
||||||
.idea/
|
.idea/
|
||||||
pkg/
|
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": {
|
"dependencies": {
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"word_grid": "file:../pkg"
|
"word_grid": "file:../wasm/pkg"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@parcel/transformer-less": "^2.9.3",
|
"@parcel/transformer-less": "^2.9.3",
|
||||||
"@types/react": "^18.2.18",
|
"@types/react": "^18.2.18",
|
||||||
"@types/react-dom": "^18.2.7",
|
"@types/react-dom": "^18.2.7",
|
||||||
"parcel": "^2.9.3",
|
"parcel": "^2.12.0",
|
||||||
"process": "^0.11.10"
|
"process": "^0.11.10"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|
433
ui/src/Game.tsx
433
ui/src/Game.tsx
|
@ -1,16 +1,5 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {useEffect, useMemo, useReducer, useRef, useState} from "react";
|
import {useEffect, useReducer, useRef, useState} from "react";
|
||||||
import {
|
|
||||||
GameState,
|
|
||||||
GameWasm,
|
|
||||||
MyResult,
|
|
||||||
PlayedTile,
|
|
||||||
PlayerAndScore,
|
|
||||||
ScoreResult,
|
|
||||||
Tray,
|
|
||||||
TurnAction,
|
|
||||||
TurnAdvanceResult
|
|
||||||
} from "../../pkg/word_grid";
|
|
||||||
import {
|
import {
|
||||||
Direction,
|
Direction,
|
||||||
GRID_LENGTH,
|
GRID_LENGTH,
|
||||||
|
@ -28,29 +17,55 @@ import {
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
import {TileExchangeModal} from "./TileExchange";
|
import {TileExchangeModal} from "./TileExchange";
|
||||||
import {Grid, Scores, TileTray} from "./UI";
|
import {Grid, Scores, TileTray} from "./UI";
|
||||||
|
import {API, APIState, GameState, TurnAction, Tray, PlayedTile, Letter} from "./api";
|
||||||
|
|
||||||
function addLogInfo(existingLog: React.JSX.Element[], newItem: React.JSX.Element) {
|
function addLogInfo(existingLog: React.JSX.Element[], newItem: React.JSX.Element) {
|
||||||
newItem = React.cloneElement(newItem, { key: existingLog.length })
|
newItem = React.cloneElement(newItem, {key: existingLog.length})
|
||||||
existingLog.push(newItem);
|
existingLog.push(newItem);
|
||||||
return existingLog.slice();
|
return existingLog.slice();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Game(props: {
|
export function Game(props: {
|
||||||
wasm: GameWasm,
|
api: API,
|
||||||
settings: Settings,
|
settings: Settings,
|
||||||
end_game_fn: () => void,
|
end_game_fn: () => void,
|
||||||
}) {
|
}) {
|
||||||
|
|
||||||
const cellTypes = useMemo(() => {
|
const [api_state, setAPIState] = useState<APIState>(undefined);
|
||||||
return props.wasm.get_board_cell_types();
|
const [isLoading, setIsLoading] = useState<boolean>(true);
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [isGameOver, setGameOver] = useState<boolean>(false);
|
|
||||||
const [confirmedScorePoints, setConfirmedScorePoints] = useState<number>(-1);
|
const [confirmedScorePoints, setConfirmedScorePoints] = useState<number>(-1);
|
||||||
|
const [currentTurnNumber, setCurrentTurnNumber] = useState<number>(0);
|
||||||
|
|
||||||
|
let isGameOver = false;
|
||||||
|
if (api_state !== undefined) {
|
||||||
|
isGameOver = api_state.public_information.game_state.type === "Ended";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function load() {
|
||||||
|
// setAPIState(undefined);
|
||||||
|
// setIsLoading(true);
|
||||||
|
const result = props.api.load();
|
||||||
|
|
||||||
|
result.then(
|
||||||
|
(state) => {
|
||||||
|
setAPIState(state);
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.catch((error) => {
|
||||||
|
console.log("load() failed")
|
||||||
|
console.log(error);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
load();
|
||||||
|
}, [])
|
||||||
|
|
||||||
const [boardLetters, setBoardLetters] = useState<HighlightableLetterData[]>(() => {
|
const [boardLetters, setBoardLetters] = useState<HighlightableLetterData[]>(() => {
|
||||||
const newLetterData = [] as HighlightableLetterData[];
|
const newLetterData = [] as HighlightableLetterData[];
|
||||||
for(let i=0; i<GRID_LENGTH * GRID_LENGTH; i++) {
|
for (let i = 0; i < GRID_LENGTH * GRID_LENGTH; i++) {
|
||||||
newLetterData.push(undefined);
|
newLetterData.push(undefined);
|
||||||
}
|
}
|
||||||
return newLetterData;
|
return newLetterData;
|
||||||
|
@ -58,28 +73,26 @@ export function Game(props: {
|
||||||
});
|
});
|
||||||
|
|
||||||
function adjustGridArrow(existing: GridArrowData, update: GridArrowDispatchAction): GridArrowData {
|
function adjustGridArrow(existing: GridArrowData, update: GridArrowDispatchAction): GridArrowData {
|
||||||
console.log({update});
|
if (update.action == GridArrowDispatchActionType.CLEAR) {
|
||||||
|
|
||||||
if(update.action == GridArrowDispatchActionType.CLEAR) {
|
|
||||||
return null;
|
return null;
|
||||||
} else if (update.action == GridArrowDispatchActionType.CYCLE) {
|
} else if (update.action == GridArrowDispatchActionType.CYCLE) {
|
||||||
// if there's nothing where the user clicked, we create a right arrow.
|
// if there's nothing where the user clicked, we create a right arrow.
|
||||||
if(existing == null || existing.position != update.position) {
|
if (existing == null || existing.position != update.position) {
|
||||||
return {
|
return {
|
||||||
direction: Direction.RIGHT, position: update.position
|
direction: Direction.RIGHT, position: update.position
|
||||||
}
|
}
|
||||||
// if there's a right arrow, we shift to downwards
|
// if there's a right arrow, we shift to downwards
|
||||||
} else if(existing.direction == Direction.RIGHT) {
|
} else if (existing.direction == Direction.RIGHT) {
|
||||||
return {
|
return {
|
||||||
direction: Direction.DOWN, position: existing.position
|
direction: Direction.DOWN, position: existing.position
|
||||||
}
|
}
|
||||||
// if there's a down arrow, we clear it
|
// if there's a down arrow, we clear it
|
||||||
} else if (existing.direction == Direction.DOWN){
|
} else if (existing.direction == Direction.DOWN) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
} else if (update.action == GridArrowDispatchActionType.SHIFT) {
|
} else if (update.action == GridArrowDispatchActionType.SHIFT) {
|
||||||
if(existing == null) {
|
if (existing == null) {
|
||||||
// no arrow to shift
|
// no arrow to shift
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
|
@ -88,7 +101,7 @@ export function Game(props: {
|
||||||
|
|
||||||
// we loop because we want to skip over letters that are already set
|
// we loop because we want to skip over letters that are already set
|
||||||
while (current_x < GRID_LENGTH && current_y < GRID_LENGTH) {
|
while (current_x < GRID_LENGTH && current_y < GRID_LENGTH) {
|
||||||
if(existing.direction == Direction.RIGHT) {
|
if (existing.direction == Direction.RIGHT) {
|
||||||
current_x += 1;
|
current_x += 1;
|
||||||
} else {
|
} else {
|
||||||
current_y += 1;
|
current_y += 1;
|
||||||
|
@ -103,7 +116,7 @@ export function Game(props: {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(current_x < GRID_LENGTH && current_y < GRID_LENGTH && boardLetters[new_position] == null && !tray_letter_at_position) {
|
if (current_x < GRID_LENGTH && current_y < GRID_LENGTH && boardLetters[new_position] == null && !tray_letter_at_position) {
|
||||||
return {
|
return {
|
||||||
direction: existing.direction,
|
direction: existing.direction,
|
||||||
position: new_position,
|
position: new_position,
|
||||||
|
@ -119,38 +132,30 @@ export function Game(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
function exchangeFunction(selectedArray: Array<boolean>) {
|
function exchangeFunction(selectedArray: Array<boolean>) {
|
||||||
|
const result = props.api.exchange(selectedArray);
|
||||||
|
|
||||||
const result: MyResult<TurnAction | string> = props.wasm.exchange_tiles(selectedArray);
|
result
|
||||||
|
.then(
|
||||||
if(result.response_type === "ERR") {
|
(_api_state) => {
|
||||||
logDispatch(<div><em>{(result.value as string)}</em></div>);
|
load();
|
||||||
} else {
|
})
|
||||||
handlePlayerAction(result.value as TurnAction, props.settings.playerName);
|
.catch((error) => {
|
||||||
setTurnCount(turnCount + 1);
|
console.error({error});
|
||||||
|
logDispatch(<div>{error}</div>);
|
||||||
if(result.game_state.type === "Ended") {
|
});
|
||||||
endGame(result.game_state);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function addWordFn(word: string) {
|
|
||||||
props.wasm.add_word(word);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const [gridArrow, gridArrowDispatch] = useReducer(adjustGridArrow, null);
|
const [gridArrow, gridArrowDispatch] = useReducer(adjustGridArrow, null);
|
||||||
const [logInfo, logDispatch] = useReducer(addLogInfo, []);
|
const [logInfo, logDispatch] = useReducer(addLogInfo, []);
|
||||||
|
|
||||||
|
|
||||||
function movePlayableLetters(playerLetters: PlayableLetterData[], update: TileDispatchAction) {
|
function movePlayableLetters(playerLetters: PlayableLetterData[], update: TileDispatchAction) {
|
||||||
|
if (update.action === TileDispatchActionType.RETRIEVE) {
|
||||||
|
|
||||||
if(update.action === TileDispatchActionType.RETRIEVE) {
|
let tray: Tray = api_state.tray;
|
||||||
|
|
||||||
let tray: Tray = props.wasm.get_tray("Player");
|
if (update.override) {
|
||||||
|
|
||||||
if(update.override) {
|
|
||||||
playerLetters = [];
|
playerLetters = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -160,13 +165,13 @@ export function Game(props: {
|
||||||
let startIndex = matchCoordinate(playerLetters, update.start);
|
let startIndex = matchCoordinate(playerLetters, update.start);
|
||||||
let endIndex = matchCoordinate(playerLetters, update.end);
|
let endIndex = matchCoordinate(playerLetters, update.end);
|
||||||
|
|
||||||
if(startIndex != null) {
|
if (startIndex != null) {
|
||||||
let startLetter = playerLetters[startIndex];
|
let startLetter = playerLetters[startIndex];
|
||||||
startLetter.location = update.end.location;
|
startLetter.location = update.end.location;
|
||||||
startLetter.index = update.end.index;
|
startLetter.index = update.end.index;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(endIndex != null) {
|
if (endIndex != null) {
|
||||||
let endLetter = playerLetters[endIndex];
|
let endLetter = playerLetters[endIndex];
|
||||||
endLetter.location = update.start.location;
|
endLetter.location = update.start.location;
|
||||||
endLetter.index = update.start.index;
|
endLetter.index = update.start.index;
|
||||||
|
@ -178,7 +183,7 @@ export function Game(props: {
|
||||||
} else if (update.action === TileDispatchActionType.SET_BLANK) {
|
} else if (update.action === TileDispatchActionType.SET_BLANK) {
|
||||||
const blankLetter = playerLetters[update.blankIndex];
|
const blankLetter = playerLetters[update.blankIndex];
|
||||||
|
|
||||||
if(blankLetter.text !== update.newBlankValue) {
|
if (blankLetter.text !== update.newBlankValue) {
|
||||||
blankLetter.text = update.newBlankValue;
|
blankLetter.text = update.newBlankValue;
|
||||||
if (blankLetter.location == LocationType.GRID) {
|
if (blankLetter.location == LocationType.GRID) {
|
||||||
setConfirmedScorePoints(-1);
|
setConfirmedScorePoints(-1);
|
||||||
|
@ -190,7 +195,7 @@ export function Game(props: {
|
||||||
return mergeTrays(playerLetters, playerLetters);
|
return mergeTrays(playerLetters, playerLetters);
|
||||||
} else if (update.action === TileDispatchActionType.MOVE_TO_ARROW) {
|
} else if (update.action === TileDispatchActionType.MOVE_TO_ARROW) {
|
||||||
// let's verify that the arrow is defined, otherwise do nothing
|
// let's verify that the arrow is defined, otherwise do nothing
|
||||||
if(gridArrow != null) {
|
if (gridArrow != null) {
|
||||||
const end_position = {
|
const end_position = {
|
||||||
location: LocationType.GRID,
|
location: LocationType.GRID,
|
||||||
index: gridArrow.position,
|
index: gridArrow.position,
|
||||||
|
@ -215,35 +220,6 @@ export function Game(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
const [playerLetters, trayDispatch] = useReducer(movePlayableLetters, []);
|
const [playerLetters, trayDispatch] = useReducer(movePlayableLetters, []);
|
||||||
|
|
||||||
const [turnCount, setTurnCount] = useState<number>(1);
|
|
||||||
const playerAndScores: PlayerAndScore[] = useMemo(() => {
|
|
||||||
return props.wasm.get_scores();
|
|
||||||
}, [turnCount, isGameOver]);
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const newLetterData = props.wasm.get_board_letters() as HighlightableLetterData[];
|
|
||||||
|
|
||||||
// we need to go through and set 'highlight' field to either true or false
|
|
||||||
// it will always be false if the player that just went was the AI
|
|
||||||
// TODO - build in support for multiple other players
|
|
||||||
for(let i=0; i<newLetterData.length; i++) {
|
|
||||||
const newLetter = newLetterData[i];
|
|
||||||
if(boardLetters[i] === undefined && newLetter !== undefined && playerTurnName == props.settings.playerName) {
|
|
||||||
newLetter.highlight = true;
|
|
||||||
} else if(newLetter !== undefined) {
|
|
||||||
newLetter.highlight = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
setBoardLetters(newLetterData);
|
|
||||||
}, [turnCount]);
|
|
||||||
|
|
||||||
const playerTurnName = useMemo(() => {
|
|
||||||
return props.wasm.get_current_player();
|
|
||||||
}, [turnCount]);
|
|
||||||
|
|
||||||
const logDivRef = useRef(null);
|
const logDivRef = useRef(null);
|
||||||
|
|
||||||
const [isTileExchangeOpen, setIsTileExchangeOpen] = useState<boolean>(false);
|
const [isTileExchangeOpen, setIsTileExchangeOpen] = useState<boolean>(false);
|
||||||
|
@ -255,34 +231,21 @@ export function Game(props: {
|
||||||
}
|
}
|
||||||
}, [logInfo]);
|
}, [logInfo]);
|
||||||
|
|
||||||
const remainingTiles = useMemo(() => {
|
|
||||||
return props.wasm.get_remaining_tiles();
|
|
||||||
}, [turnCount, isGameOver]);
|
|
||||||
|
|
||||||
const remainingAITiles = useMemo(() => {
|
|
||||||
let result = props.wasm.get_player_tile_count(props.settings.aiName) as MyResult<number | String>;
|
|
||||||
if(result.response_type == "OK") {
|
|
||||||
return result.value as number;
|
|
||||||
} else {
|
|
||||||
console.error(result.value);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
}, [turnCount, isGameOver]);
|
|
||||||
|
|
||||||
function handlePlayerAction(action: TurnAction, playerName: string) {
|
function handlePlayerAction(action: TurnAction, playerName: string) {
|
||||||
|
|
||||||
if (action.type == "PlayTiles"){
|
if (action.type == "PlayTiles") {
|
||||||
const result = action.result;
|
const result = action.result;
|
||||||
result.words.sort((a, b) => b.score - a.score);
|
result.words.sort((a, b) => b.score - a.score);
|
||||||
for(let word of result.words) {
|
for (let word of result.words) {
|
||||||
logDispatch(<div>{playerName} received {word.score} points for playing '{word.word}.'</div>);
|
logDispatch(<div>{playerName} received {word.score} points for playing '{word.word}.'</div>);
|
||||||
}
|
}
|
||||||
logDispatch(<div>{playerName} received a total of <strong>{result.total} points</strong> for their turn.</div>);
|
logDispatch(<div>{playerName} received a total of <strong>{result.total} points</strong> for their turn.
|
||||||
} else if(action.type == "ExchangeTiles") {
|
</div>);
|
||||||
logDispatch(<div>{playerName} exchanged {action.tiles_exchanged} tile{action.tiles_exchanged > 1 ? 's' : ''} for their turn.</div>);
|
} else if (action.type == "ExchangeTiles") {
|
||||||
}
|
logDispatch(
|
||||||
else if(action.type == "Pass"){
|
<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>);
|
logDispatch(<div>{playerName} passed.</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -292,28 +255,26 @@ export function Game(props: {
|
||||||
|
|
||||||
function endGame(state: GameState) {
|
function endGame(state: GameState) {
|
||||||
|
|
||||||
if(state.type != "InProgress") {
|
if (state.type != "InProgress") {
|
||||||
|
|
||||||
setGameOver(true);
|
|
||||||
logDispatch(<h4>Scoring</h4>);
|
logDispatch(<h4>Scoring</h4>);
|
||||||
|
|
||||||
const scores = props.wasm.get_scores() as PlayerAndScore[];
|
const scores = api_state.public_information.players;
|
||||||
let pointsBonus = 0;
|
let pointsBonus = 0;
|
||||||
for(const playerAndScore of scores) {
|
for (const playerAndScore of scores) {
|
||||||
const name = playerAndScore.name;
|
const name = playerAndScore.name;
|
||||||
|
|
||||||
if(name == state.finisher) {
|
if (name == state.finisher) {
|
||||||
// we'll do the finisher last
|
// we'll do the finisher last
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const letters = state.remaining_tiles.get(name);
|
const letters = state.remaining_tiles.get(name);
|
||||||
if(letters.length == 0) {
|
if (letters.length == 0) {
|
||||||
logDispatch(<div>{name} has no remaining tiles.</div>);
|
logDispatch(<div>{name} has no remaining tiles.</div>);
|
||||||
} else {
|
} else {
|
||||||
let pointsLost = 0;
|
let pointsLost = 0;
|
||||||
let letterListStr = '';
|
let letterListStr = '';
|
||||||
for(let i=0; i<letters.length; i++) {
|
for (let i = 0; i < letters.length; i++) {
|
||||||
const letter = letters[i];
|
const letter = letters[i];
|
||||||
const letterText = letter.is_blank ? 'a blank' : letter.text;
|
const letterText = letter.is_blank ? 'a blank' : letter.text;
|
||||||
pointsLost += letter.points;
|
pointsLost += letter.points;
|
||||||
|
@ -321,13 +282,13 @@ export function Game(props: {
|
||||||
letterListStr += letterText;
|
letterListStr += letterText;
|
||||||
|
|
||||||
// we're doing a list of 3 or more so add commas
|
// we're doing a list of 3 or more so add commas
|
||||||
if(letters.length > 2) {
|
if (letters.length > 2) {
|
||||||
if(i == letters.length - 2) {
|
if (i == letters.length - 2) {
|
||||||
letterListStr += ', and ';
|
letterListStr += ', and ';
|
||||||
} else if (i < letters.length - 2) {
|
} else if (i < letters.length - 2) {
|
||||||
letterListStr += ', ';
|
letterListStr += ', ';
|
||||||
}
|
}
|
||||||
} else if (i == 0 && letters.length == 2){
|
} else if (i == 0 && letters.length == 2) {
|
||||||
// list of 2
|
// list of 2
|
||||||
letterListStr += ' and ';
|
letterListStr += ' and ';
|
||||||
}
|
}
|
||||||
|
@ -338,7 +299,7 @@ export function Game(props: {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(state.finisher != null) {
|
if (state.finisher != null) {
|
||||||
logDispatch(<div>{state.finisher} receives {pointsBonus} bonus for completing first.</div>);
|
logDispatch(<div>{state.finisher} receives {pointsBonus} bonus for completing first.</div>);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -348,15 +309,14 @@ export function Game(props: {
|
||||||
.at(0);
|
.at(0);
|
||||||
|
|
||||||
const playersAtHighest = scores.filter((score) => score.score == highestScore);
|
const playersAtHighest = scores.filter((score) => score.score == highestScore);
|
||||||
let endGameMsg: string = '';
|
let endGameMsg: string;
|
||||||
|
|
||||||
if(playersAtHighest.length > 1 && state.finisher == null) {
|
if (playersAtHighest.length > 1 && state.finisher == null) {
|
||||||
endGameMsg = "Tie game!";
|
endGameMsg = "Tie game!";
|
||||||
} else if (playersAtHighest.length > 1 && state.finisher != null) {
|
} else if (playersAtHighest.length > 1 && state.finisher != null) {
|
||||||
// if there's a tie then the finisher gets the win
|
// if there's a tie then the finisher gets the win
|
||||||
endGameMsg = `${playersAtHighest[0].name} won by finishing first!`;
|
endGameMsg = `${playersAtHighest[0].name} won by finishing first!`;
|
||||||
}
|
} else {
|
||||||
else {
|
|
||||||
endGameMsg = `${playersAtHighest[0].name} won!`;
|
endGameMsg = `${playersAtHighest[0].name} won!`;
|
||||||
}
|
}
|
||||||
logDispatch(<h4>Game over - {endGameMsg}</h4>);
|
logDispatch(<h4>Game over - {endGameMsg}</h4>);
|
||||||
|
@ -367,34 +327,78 @@ export function Game(props: {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function runAI() {
|
function updateBoardLetters(newLetters: Array<Letter>) {
|
||||||
const result: MyResult<TurnAdvanceResult> = props.wasm.advance_turn();
|
const newLetterData = newLetters as HighlightableLetterData[];
|
||||||
if(result.response_type === "OK" && result.value.type == "AIMove") {
|
|
||||||
handlePlayerAction(result.value.action, props.settings.aiName);
|
|
||||||
if(result.game_state.type === "Ended") {
|
|
||||||
endGame(result.game_state);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
for (let i = 0; i < newLetterData.length; i++) {
|
||||||
// this would be quite surprising
|
const newLetter = newLetterData[i];
|
||||||
console.error({result});
|
if (newLetter != null) {
|
||||||
|
newLetter.highlight = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
setTurnCount(turnCount + 1);
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
setBoardLetters(newLetterData);
|
||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
trayDispatch({action: TileDispatchActionType.RETRIEVE});
|
if (api_state) {
|
||||||
setConfirmedScorePoints(-1);
|
console.debug(api_state);
|
||||||
if(!isGameOver){
|
gridArrowDispatch({action: GridArrowDispatchActionType.CLEAR});
|
||||||
logDispatch(<h4>Turn {turnCount}</h4>);
|
trayDispatch({action: TileDispatchActionType.RETRIEVE});
|
||||||
logDispatch(<div>{playerTurnName}'s turn</div>);
|
setConfirmedScorePoints(-1);
|
||||||
if(playerTurnName != props.settings.playerName && !isGameOver) {
|
updateBoardLetters(api_state.public_information.board);
|
||||||
runAI();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [turnCount]);
|
|
||||||
|
|
||||||
|
for (let i = currentTurnNumber; i < api_state.public_information.history.length; i++) {
|
||||||
|
if (i > currentTurnNumber) {
|
||||||
|
logDispatch(<h4>Turn {i + 1}</h4>);
|
||||||
|
const playerAtTurn = api_state.public_information.players[i % api_state.public_information.players.length].name;
|
||||||
|
logDispatch(<div>{playerAtTurn}'s turn</div>);
|
||||||
|
}
|
||||||
|
const update = api_state.public_information.history[i];
|
||||||
|
handlePlayerAction(update.type, update.player);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
setCurrentTurnNumber(api_state.public_information.history.length);
|
||||||
|
|
||||||
|
if (!isGameOver) {
|
||||||
|
logDispatch(<h4>Turn {api_state.public_information.history.length + 1}</h4>);
|
||||||
|
logDispatch(<div>{api_state.public_information.current_player}'s turn</div>);
|
||||||
|
} else {
|
||||||
|
endGame(api_state.public_information.game_state);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}, [api_state]);
|
||||||
|
|
||||||
|
|
||||||
|
if (isLoading) {
|
||||||
|
return <div>Still loading</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const playerAndScores = api_state.public_information.players;
|
||||||
|
const remainingTiles = api_state.public_information.remaining_tiles;
|
||||||
|
let remainingAITiles = null;
|
||||||
|
for (let player of playerAndScores) {
|
||||||
|
if (player.name == 'AI') {
|
||||||
|
remainingAITiles = player.tray_tiles;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return <>
|
return <>
|
||||||
<TileExchangeModal
|
<TileExchangeModal
|
||||||
|
@ -405,7 +409,7 @@ export function Game(props: {
|
||||||
/>
|
/>
|
||||||
<div className="board-log">
|
<div className="board-log">
|
||||||
<Grid
|
<Grid
|
||||||
cellTypes={cellTypes}
|
cellTypes={api_state.public_information.cell_types}
|
||||||
playerLetters={playerLetters}
|
playerLetters={playerLetters}
|
||||||
boardLetters={boardLetters}
|
boardLetters={boardLetters}
|
||||||
tileDispatch={trayDispatch}
|
tileDispatch={trayDispatch}
|
||||||
|
@ -414,9 +418,9 @@ export function Game(props: {
|
||||||
/>
|
/>
|
||||||
<div className="message-log">
|
<div className="message-log">
|
||||||
<button className="end-game"
|
<button className="end-game"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
props.end_game_fn();
|
props.end_game_fn();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
End Game
|
End Game
|
||||||
</button>
|
</button>
|
||||||
|
@ -437,101 +441,108 @@ export function Game(props: {
|
||||||
<button
|
<button
|
||||||
disabled={remainingTiles == 0 || isGameOver}
|
disabled={remainingTiles == 0 || isGameOver}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
trayDispatch({action: TileDispatchActionType.RETURN}); // want all tiles back on tray for tile exchange
|
trayDispatch({action: TileDispatchActionType.RETURN}); // want all tiles back on tray for tile exchange
|
||||||
setIsTileExchangeOpen(true);
|
setIsTileExchangeOpen(true);
|
||||||
}}>Open Tile Exchange</button>
|
}}>Open Tile Exchange
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<TileTray letters={playerLetters} trayLength={props.settings.trayLength} trayDispatch={trayDispatch}/>
|
<TileTray letters={playerLetters} trayLength={props.settings.trayLength} trayDispatch={trayDispatch}/>
|
||||||
<div className="player-controls">
|
<div className="player-controls">
|
||||||
<button
|
<button
|
||||||
className="check"
|
className="check"
|
||||||
disabled={isGameOver}
|
disabled={isGameOver}
|
||||||
onClick={() => {
|
onClick={async () => {
|
||||||
const playedTiles = playerLetters.map((i) => {
|
const playedTiles = playerLetters.map((i) => {
|
||||||
if (i === undefined) {
|
if (i === undefined) {
|
||||||
return null;
|
return null;
|
||||||
}
|
|
||||||
|
|
||||||
if (i.location === LocationType.GRID) {
|
|
||||||
let result: PlayedTile = {
|
|
||||||
index: i.index,
|
|
||||||
character: undefined
|
|
||||||
};
|
|
||||||
if (i.is_blank) {
|
|
||||||
result.character = i.text;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
if (i.location === LocationType.GRID) {
|
||||||
}
|
let result: PlayedTile = {
|
||||||
|
index: i.index,
|
||||||
|
character: undefined
|
||||||
|
};
|
||||||
|
if (i.is_blank) {
|
||||||
|
result.character = i.text;
|
||||||
|
}
|
||||||
|
|
||||||
return null;
|
return result;
|
||||||
});
|
}
|
||||||
|
|
||||||
const result: MyResult<{ type: "PlayTiles"; result: ScoreResult } | string> = props.wasm.receive_play(playedTiles, confirmedScorePoints > -1);
|
return null;
|
||||||
console.log({result});
|
});
|
||||||
|
|
||||||
if(result.response_type === "ERR") {
|
const committing = confirmedScorePoints > -1;
|
||||||
const message = result.value as string;
|
const result = props.api.play(playedTiles, committing);
|
||||||
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>);
|
|
||||||
}
|
|
||||||
|
|
||||||
} else {
|
result
|
||||||
|
.then(
|
||||||
|
(api_state) => {
|
||||||
|
|
||||||
const score_result = (result.value as { type: "PlayTiles"; result: ScoreResult }).result;
|
const play_tiles: TurnAction = api_state.update.type;
|
||||||
const total_points = score_result.total;
|
if (play_tiles.type == "PlayTiles") {
|
||||||
|
setConfirmedScorePoints(play_tiles.result.total);
|
||||||
|
|
||||||
|
if (committing) {
|
||||||
|
load();
|
||||||
|
}
|
||||||
|
|
||||||
|
} else {
|
||||||
|
console.error("Inaccessible branch!!!")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
.catch((error) => {
|
||||||
|
console.error({error});
|
||||||
|
|
||||||
|
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>);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
if (confirmedScorePoints > -1) {
|
});
|
||||||
handlePlayerAction({type: "PlayTiles", result: score_result}, props.settings.playerName);
|
|
||||||
setTurnCount(turnCount + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (result.game_state.type === "Ended") {
|
}}>{confirmedScorePoints > -1 ? `Score ${confirmedScorePoints} points` : "Check"}</button>
|
||||||
endGame(result.game_state);
|
|
||||||
}
|
|
||||||
|
|
||||||
setConfirmedScorePoints(total_points);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}}>{confirmedScorePoints > -1 ? `Score ${confirmedScorePoints} points` : "Check"}</button>
|
|
||||||
<button
|
<button
|
||||||
className="return"
|
className="return"
|
||||||
disabled={isGameOver}
|
disabled={isGameOver}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
trayDispatch({action: TileDispatchActionType.RETURN});
|
trayDispatch({action: TileDispatchActionType.RETURN});
|
||||||
}}>Return Tiles</button>
|
}}>Return Tiles
|
||||||
|
</button>
|
||||||
<button
|
<button
|
||||||
className="pass"
|
className="pass"
|
||||||
disabled={isGameOver}
|
disabled={isGameOver}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (window.confirm("Are you sure you want to pass?")) {
|
if (window.confirm("Are you sure you want to pass?")) {
|
||||||
const result = props.wasm.skip_turn() as MyResult<string>;
|
const result = props.api.pass();
|
||||||
handlePlayerAction({type: "Pass"}, props.settings.playerName);
|
|
||||||
setTurnCount(turnCount + 1);
|
result
|
||||||
|
.then(
|
||||||
|
(_api_state) => {
|
||||||
|
load();
|
||||||
|
})
|
||||||
|
.catch((error) => {
|
||||||
|
console.error({error});
|
||||||
|
logDispatch(<div>{error}</div>);
|
||||||
|
});
|
||||||
|
|
||||||
if (result.game_state.type === "Ended") {
|
|
||||||
endGame(result.game_state);
|
|
||||||
}
|
}
|
||||||
}
|
}}>Pass
|
||||||
}}>Pass</button>
|
</button>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</>;
|
</>;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function AddWordButton(props: {word: string, addWordFn: (x: string) => void}) {
|
function AddWordButton(props: { word: string, addWordFn: (x: string) => void }) {
|
||||||
const [isClicked, setIsClicked] = useState<boolean>(false);
|
const [isClicked, setIsClicked] = useState<boolean>(false);
|
||||||
|
|
||||||
if (!isClicked) {
|
if (!isClicked) {
|
||||||
|
|
|
@ -1,8 +1,9 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {useState} from "react";
|
import {useState} from "react";
|
||||||
import {Difficulty, GameWasm} from '../../pkg/word_grid';
|
|
||||||
import {Settings} from "./utils";
|
import {Settings} from "./utils";
|
||||||
import {Game} from "./Game";
|
import {Game} from "./Game";
|
||||||
|
import {API, Difficulty} from "./api";
|
||||||
|
import {GameWasm} from "./wasm";
|
||||||
|
|
||||||
export function Menu(props: {settings: Settings, dictionary_text: string}) {
|
export function Menu(props: {settings: Settings, dictionary_text: string}) {
|
||||||
|
|
||||||
|
@ -70,8 +71,8 @@ export function Menu(props: {settings: Settings, dictionary_text: string}) {
|
||||||
proportion: processedProportionDictionary,
|
proportion: processedProportionDictionary,
|
||||||
randomness: processedAIRandomness,
|
randomness: processedAIRandomness,
|
||||||
};
|
};
|
||||||
const game_wasm = new GameWasm(BigInt(seed), props.dictionary_text, difficulty);
|
const game_wasm: API = new GameWasm(BigInt(seed), props.dictionary_text, difficulty);
|
||||||
const game = <Game settings={props.settings} wasm={game_wasm} key={seed} end_game_fn={() => setGame(null)}/>
|
const game = <Game settings={props.settings} api={game_wasm} key={seed} end_game_fn={() => setGame(null)}/>
|
||||||
setGame(game);
|
setGame(game);
|
||||||
}}>New Game</button>
|
}}>New Game</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import {addNTimes, PlayableLetterData} from "./utils";
|
import {addNTimes, PlayableLetterData} from "./utils";
|
||||||
import {useEffect, useState} from "react";
|
import {useEffect, useState} from "react";
|
||||||
import {Modal} from "./Modal";
|
import {Modal} from "./Modal";
|
||||||
import {Letter as LetterData} from "../../pkg/word_grid";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import {Letter} from "./api";
|
||||||
|
|
||||||
export function TileExchangeModal(props: {
|
export function TileExchangeModal(props: {
|
||||||
playerLetters: PlayableLetterData[],
|
playerLetters: PlayableLetterData[],
|
||||||
|
@ -105,7 +105,7 @@ function TilesExchangedTray(props: {
|
||||||
}
|
}
|
||||||
|
|
||||||
function DummyExchangeTile(props: {
|
function DummyExchangeTile(props: {
|
||||||
letter: LetterData,
|
letter: Letter,
|
||||||
isSelected: boolean,
|
isSelected: boolean,
|
||||||
onClick: () => void,
|
onClick: () => void,
|
||||||
}){
|
}){
|
||||||
|
|
|
@ -1,8 +1,6 @@
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {ChangeEvent, JSX} from "react";
|
import {ChangeEvent, JSX} from "react";
|
||||||
import {PlayerAndScore} from "../../pkg/word_grid";
|
|
||||||
import {
|
import {
|
||||||
CellType,
|
|
||||||
cellTypeToDetails,
|
cellTypeToDetails,
|
||||||
CoordinateData,
|
CoordinateData,
|
||||||
GridArrowData,
|
GridArrowData,
|
||||||
|
@ -14,6 +12,7 @@ import {
|
||||||
TileDispatch,
|
TileDispatch,
|
||||||
TileDispatchActionType,
|
TileDispatchActionType,
|
||||||
} from "./utils";
|
} from "./utils";
|
||||||
|
import {APIPlayer, CellType} from "./api";
|
||||||
|
|
||||||
|
|
||||||
export function TileSlot(props: {
|
export function TileSlot(props: {
|
||||||
|
@ -234,7 +233,7 @@ export function Grid(props: {
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Scores(props: {playerScores: Array<PlayerAndScore>}){
|
export function Scores(props: {playerScores: Array<APIPlayer>}){
|
||||||
let elements = props.playerScores.map((ps) => {
|
let elements = props.playerScores.map((ps) => {
|
||||||
return <div key={ps.name}>
|
return <div key={ps.name}>
|
||||||
<h3>{ps.name}</h3>
|
<h3>{ps.name}</h3>
|
||||||
|
|
90
ui/src/api.ts
Normal file
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 {createRoot} from "react-dom/client";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import {Menu} from "./Menu";
|
import {Menu} from "./Menu";
|
||||||
|
|
|
@ -1,14 +1,5 @@
|
||||||
import {Letter as LetterData, Letter} from "../../pkg/word_grid";
|
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
import {CellType, Letter as LetterData} from "./api";
|
||||||
export enum CellType {
|
|
||||||
Normal = "Normal",
|
|
||||||
DoubleWord = "DoubleWord",
|
|
||||||
DoubleLetter = "DoubleLetter",
|
|
||||||
TripleLetter = "TripleLetter",
|
|
||||||
TripleWord = "TripleWord",
|
|
||||||
Start = "Start",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export interface Settings {
|
export interface Settings {
|
||||||
|
@ -118,7 +109,7 @@ export function addNTimes<T>(array: T[], toAdd: T, times: number) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function mergeTrays(existing: PlayableLetterData[], newer: (Letter | undefined)[]): PlayableLetterData[] {
|
export function mergeTrays(existing: PlayableLetterData[], newer: (LetterData | undefined)[]): PlayableLetterData[] {
|
||||||
|
|
||||||
let trayLength = Math.max(existing.length, newer.length);
|
let trayLength = Math.max(existing.length, newer.length);
|
||||||
|
|
||||||
|
|
66
ui/src/wasm.ts
Normal file
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"
|
license = "AGPL-3"
|
||||||
description = "A (WIP) package for playing 'WordGrid'."
|
description = "A (WIP) package for playing 'WordGrid'."
|
||||||
|
|
||||||
[lib]
|
|
||||||
crate-type = ["cdylib"]
|
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
csv = "1.2.2"
|
csv = "1.3.0"
|
||||||
rand = {version = "0.8.5", features = ["small_rng"]}
|
rand = {version = "0.8.5", features = ["small_rng"]}
|
||||||
getrandom = {version = "0.2", features = ["js"]}
|
getrandom = {version = "0.2", features = ["js"]}
|
||||||
wasm-bindgen = { version = "0.2.87", features = ["serde-serialize"] }
|
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde = { version = "=1.0.171", features = ["derive"] }
|
serde = { version = "1.0.202", features = ["derive"] }
|
||||||
serde-wasm-bindgen = "0.4.5"
|
|
||||||
tsify = { version = "0.4.5", features = ["js"] }
|
|
||||||
|
|
217
wordgrid/src/api.rs
Normal file
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,32 +1,31 @@
|
||||||
|
use crate::constants::{ALL_LETTERS_BONUS, GRID_LENGTH, TRAY_LENGTH};
|
||||||
|
use crate::dictionary::DictionaryImpl;
|
||||||
|
use crate::game::Error;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::borrow::BorrowMut;
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::fmt::{Formatter, Write};
|
use std::fmt::{Formatter, Write};
|
||||||
use std::borrow::BorrowMut;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use tsify::Tsify;
|
|
||||||
use crate::constants::{ALL_LETTERS_BONUS, GRID_LENGTH, TRAY_LENGTH};
|
|
||||||
use crate::dictionary::DictionaryImpl;
|
|
||||||
|
|
||||||
|
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone, Copy)]
|
||||||
pub enum Direction {
|
pub enum Direction {
|
||||||
Row, Column
|
Row,
|
||||||
|
Column,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Direction {
|
impl Direction {
|
||||||
pub fn invert(&self) -> Self {
|
pub fn invert(&self) -> Self {
|
||||||
match &self {
|
match &self {
|
||||||
Direction::Row => {Direction::Column}
|
Direction::Row => Direction::Column,
|
||||||
Direction::Column => {Direction::Row}
|
Direction::Column => Direction::Row,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
|
#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
|
||||||
pub struct Coordinates (pub u8, pub u8);
|
pub struct Coordinates(pub u8, pub u8);
|
||||||
|
|
||||||
impl Coordinates {
|
impl Coordinates {
|
||||||
|
|
||||||
pub fn new_from_index(index: usize) -> Self {
|
pub fn new_from_index(index: usize) -> Self {
|
||||||
let y = index / GRID_LENGTH as usize;
|
let y = index / GRID_LENGTH as usize;
|
||||||
let x = index % GRID_LENGTH as usize;
|
let x = index % GRID_LENGTH as usize;
|
||||||
|
@ -36,32 +35,35 @@ impl Coordinates {
|
||||||
|
|
||||||
fn add(&self, direction: Direction, i: i8) -> Option<Self> {
|
fn add(&self, direction: Direction, i: i8) -> Option<Self> {
|
||||||
let proposed = match direction {
|
let proposed = match direction {
|
||||||
Direction::Column => {(self.0 as i8, self.1 as i8+i)}
|
Direction::Column => (self.0 as i8, self.1 as i8 + i),
|
||||||
Direction::Row => {(self.0 as i8+i, self.1 as i8)}
|
Direction::Row => (self.0 as i8 + i, self.1 as i8),
|
||||||
};
|
};
|
||||||
|
|
||||||
if proposed.0 < 0 || proposed.0 >= GRID_LENGTH as i8 || proposed.1 < 0 || proposed.1 >= GRID_LENGTH as i8 {
|
if proposed.0 < 0
|
||||||
|
|| proposed.0 >= GRID_LENGTH as i8
|
||||||
|
|| proposed.1 < 0
|
||||||
|
|| proposed.1 >= GRID_LENGTH as i8
|
||||||
|
{
|
||||||
None
|
None
|
||||||
} else{
|
} else {
|
||||||
Some(Coordinates(proposed.0 as u8, proposed.1 as u8))
|
Some(Coordinates(proposed.0 as u8, proposed.1 as u8))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn increment(&self, direction: Direction) -> Option<Self>{
|
pub fn increment(&self, direction: Direction) -> Option<Self> {
|
||||||
self.add(direction, 1)
|
self.add(direction, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn decrement(&self, direction: Direction) -> Option<Self>{
|
pub fn decrement(&self, direction: Direction) -> Option<Self> {
|
||||||
self.add(direction, -1)
|
self.add(direction, -1)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn map_to_index(&self) -> usize {
|
pub fn map_to_index(&self) -> usize {
|
||||||
(self.0 + GRID_LENGTH*self.1) as usize
|
(self.0 + GRID_LENGTH * self.1) as usize
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, Serialize, Deserialize, Tsify, PartialEq, Eq, Hash)]
|
#[derive(Debug, Copy, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
|
||||||
#[tsify(from_wasm_abi)]
|
|
||||||
pub struct Letter {
|
pub struct Letter {
|
||||||
pub text: char,
|
pub text: char,
|
||||||
pub points: u32,
|
pub points: u32,
|
||||||
|
@ -81,32 +83,27 @@ impl Letter {
|
||||||
|
|
||||||
pub fn new(text: Option<char>, points: u32) -> Letter {
|
pub fn new(text: Option<char>, points: u32) -> Letter {
|
||||||
match text {
|
match text {
|
||||||
None => {
|
None => Letter {
|
||||||
Letter {
|
text: ' ',
|
||||||
text: ' ',
|
points,
|
||||||
points,
|
ephemeral: true,
|
||||||
ephemeral: true,
|
is_blank: true,
|
||||||
is_blank: true,
|
},
|
||||||
}
|
Some(text) => Letter {
|
||||||
}
|
text,
|
||||||
Some(text) => {
|
points,
|
||||||
Letter {
|
ephemeral: true,
|
||||||
text,
|
is_blank: false,
|
||||||
points,
|
},
|
||||||
ephemeral: true,
|
|
||||||
is_blank: false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn partial_match(&self, other: &Letter) -> bool {
|
pub fn partial_match(&self, other: &Letter) -> bool {
|
||||||
self == other || (self.is_blank && other.is_blank && self.points == other.points)
|
self == other || (self.is_blank && other.is_blank && self.points == other.points)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, Serialize)]
|
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
|
||||||
pub enum CellType {
|
pub enum CellType {
|
||||||
Normal,
|
Normal,
|
||||||
DoubleWord,
|
DoubleWord,
|
||||||
|
@ -141,37 +138,34 @@ impl<'a> ToString for Word<'a> {
|
||||||
}
|
}
|
||||||
|
|
||||||
text
|
text
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl <'a> Word<'a> {
|
impl<'a> Word<'a> {
|
||||||
|
pub fn calculate_score(&self) -> u32 {
|
||||||
pub fn calculate_score(&self) -> u32{
|
|
||||||
let mut multiplier = 1;
|
let mut multiplier = 1;
|
||||||
let mut unmultiplied_score = 0;
|
let mut unmultiplied_score = 0;
|
||||||
|
|
||||||
for cell in self.cells.as_slice() {
|
for cell in self.cells.as_slice() {
|
||||||
let cell_value = cell.value.unwrap();
|
let cell_value = cell.value.unwrap();
|
||||||
if cell_value.ephemeral {
|
if cell_value.ephemeral {
|
||||||
let cell_multiplier =
|
let cell_multiplier = match cell.cell_type {
|
||||||
match cell.cell_type {
|
CellType::Normal => 1,
|
||||||
CellType::Normal => {1}
|
CellType::DoubleWord => {
|
||||||
CellType::DoubleWord => {
|
multiplier *= 2;
|
||||||
multiplier *= 2;
|
1
|
||||||
1
|
}
|
||||||
}
|
CellType::DoubleLetter => 2,
|
||||||
CellType::DoubleLetter => {2}
|
CellType::TripleLetter => 3,
|
||||||
CellType::TripleLetter => {3}
|
CellType::TripleWord => {
|
||||||
CellType::TripleWord => {
|
multiplier *= 3;
|
||||||
multiplier *= 3;
|
1
|
||||||
1
|
}
|
||||||
}
|
CellType::Start => {
|
||||||
CellType::Start => {
|
multiplier *= 2;
|
||||||
multiplier *= 2;
|
1
|
||||||
1
|
}
|
||||||
}
|
};
|
||||||
};
|
|
||||||
unmultiplied_score += cell_value.points * cell_multiplier;
|
unmultiplied_score += cell_value.points * cell_multiplier;
|
||||||
} else {
|
} else {
|
||||||
// no cell multiplier unfortunately
|
// no cell multiplier unfortunately
|
||||||
|
@ -187,7 +181,6 @@ impl Board {
|
||||||
pub fn new() -> Self {
|
pub fn new() -> Self {
|
||||||
let mut cells = Vec::new();
|
let mut cells = Vec::new();
|
||||||
|
|
||||||
|
|
||||||
/// Since the board is symmetrical in both directions for the purposes of our logic we can keep our coordinates in one corner
|
/// Since the board is symmetrical in both directions for the purposes of our logic we can keep our coordinates in one corner
|
||||||
///
|
///
|
||||||
/// # Arguments
|
/// # Arguments
|
||||||
|
@ -200,7 +193,7 @@ impl Board {
|
||||||
GRID_LENGTH - x - 1
|
GRID_LENGTH - x - 1
|
||||||
} else {
|
} else {
|
||||||
x
|
x
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
for i_orig in 0..GRID_LENGTH {
|
for i_orig in 0..GRID_LENGTH {
|
||||||
|
@ -221,12 +214,10 @@ impl Board {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Double letters
|
// Double letters
|
||||||
if (i % 4 == 2) && (j % 4 == 2) && !(
|
if (i % 4 == 2) && (j % 4 == 2) && !(i == 2 && j == 2) {
|
||||||
i == 2 && j == 2
|
|
||||||
) {
|
|
||||||
typee = CellType::DoubleLetter;
|
typee = CellType::DoubleLetter;
|
||||||
}
|
}
|
||||||
if (i.min(j) == 0 && i.max(j) == 3) || (i.min(j)==3 && i.max(j) == 7) {
|
if (i.min(j) == 0 && i.max(j) == 3) || (i.min(j) == 3 && i.max(j) == 7) {
|
||||||
typee = CellType::DoubleLetter;
|
typee = CellType::DoubleLetter;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -245,11 +236,10 @@ impl Board {
|
||||||
value: None,
|
value: None,
|
||||||
coordinates: Coordinates(j_orig, i_orig),
|
coordinates: Coordinates(j_orig, i_orig),
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Board {cells}
|
Board { cells }
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_cell(&self, coordinates: Coordinates) -> Result<&Cell, &str> {
|
pub fn get_cell(&self, coordinates: Coordinates) -> Result<&Cell, &str> {
|
||||||
|
@ -261,24 +251,28 @@ impl Board {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_cell_mut(&mut self, coordinates: Coordinates) -> Result<&mut Cell, &str> {
|
pub fn get_cell_mut(&mut self, coordinates: Coordinates) -> Result<&mut Cell, Error> {
|
||||||
if coordinates.0 >= GRID_LENGTH || coordinates.1 >= GRID_LENGTH {
|
if coordinates.0 >= GRID_LENGTH || coordinates.1 >= GRID_LENGTH {
|
||||||
Err("x & y must be within the board's coordinates")
|
Err(Error::Other(
|
||||||
|
"x & y must be within the board's coordinates".to_string(),
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
let index = coordinates.map_to_index();
|
let index = coordinates.map_to_index();
|
||||||
Ok(self.cells.get_mut(index).unwrap())
|
Ok(self.cells.get_mut(index).unwrap())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn calculate_scores(
|
||||||
pub fn calculate_scores(&self, dictionary: &DictionaryImpl) -> Result<(Vec<(Word, u32)>, u32), String> {
|
&self,
|
||||||
|
dictionary: &DictionaryImpl,
|
||||||
|
) -> Result<(Vec<(Word, u32)>, u32), Error> {
|
||||||
let (words, tiles_played) = self.find_played_words()?;
|
let (words, tiles_played) = self.find_played_words()?;
|
||||||
let mut words_and_scores = Vec::new();
|
let mut words_and_scores = Vec::new();
|
||||||
let mut total_score = 0;
|
let mut total_score = 0;
|
||||||
|
|
||||||
for word in words {
|
for word in words {
|
||||||
if !dictionary.contains_key(&word.to_string()) {
|
if !dictionary.contains_key(&word.to_string()) {
|
||||||
return Err(format!("{} is not a valid word", word.to_string()));
|
return Err(Error::InvalidWord(word.to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
let score = word.calculate_score();
|
let score = word.calculate_score();
|
||||||
|
@ -293,10 +287,9 @@ impl Board {
|
||||||
Ok((words_and_scores, total_score))
|
Ok((words_and_scores, total_score))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_played_words(&self) -> Result<(Vec<Word>, u8), &str> {
|
pub fn find_played_words(&self) -> Result<(Vec<Word>, u8), Error> {
|
||||||
// We don't assume that the move is valid, so let's first establish that
|
// We don't assume that the move is valid, so let's first establish that
|
||||||
|
|
||||||
|
|
||||||
// Let's first establish what rows and columns tiles were played in
|
// Let's first establish what rows and columns tiles were played in
|
||||||
let mut rows_played = HashSet::with_capacity(15);
|
let mut rows_played = HashSet::with_capacity(15);
|
||||||
let mut columns_played = HashSet::with_capacity(15);
|
let mut columns_played = HashSet::with_capacity(15);
|
||||||
|
@ -320,9 +313,9 @@ impl Board {
|
||||||
}
|
}
|
||||||
|
|
||||||
if rows_played.is_empty() {
|
if rows_played.is_empty() {
|
||||||
return Err("Tiles need to be played")
|
return Err(Error::NoTilesPlayed);
|
||||||
} else if rows_played.len() > 1 && columns_played.len() > 1 {
|
} else if rows_played.len() > 1 && columns_played.len() > 1 {
|
||||||
return Err("Tiles need to be played on one row or column")
|
return Err(Error::TilesNotStraight);
|
||||||
}
|
}
|
||||||
|
|
||||||
let direction = if rows_played.len() > 1 {
|
let direction = if rows_played.len() > 1 {
|
||||||
|
@ -335,7 +328,9 @@ impl Board {
|
||||||
let starting_column = *columns_played.iter().min().unwrap();
|
let starting_column = *columns_played.iter().min().unwrap();
|
||||||
|
|
||||||
let starting_coords = Coordinates(starting_row, starting_column);
|
let starting_coords = Coordinates(starting_row, starting_column);
|
||||||
let main_word = self.find_word_at_position(starting_coords, direction).unwrap();
|
let main_word = self
|
||||||
|
.find_word_at_position(starting_coords, direction)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
let mut words = Vec::new();
|
let mut words = Vec::new();
|
||||||
let mut observed_tiles_played = 0;
|
let mut observed_tiles_played = 0;
|
||||||
|
@ -359,25 +354,25 @@ impl Board {
|
||||||
|
|
||||||
// there are tiles not part of the main word
|
// there are tiles not part of the main word
|
||||||
if observed_tiles_played != tiles_played {
|
if observed_tiles_played != tiles_played {
|
||||||
return Err("Played tiles cannot have empty gap");
|
return Err(Error::TilesHaveGap);
|
||||||
}
|
}
|
||||||
|
|
||||||
// don't want the case of a single letter word
|
// don't want the case of a single letter word
|
||||||
if main_word.cells.len() > 1 {
|
if main_word.cells.len() > 1 {
|
||||||
words.push(main_word);
|
words.push(main_word);
|
||||||
} else if words.is_empty() {
|
} else if words.is_empty() {
|
||||||
return Err("All words must be at least one letter");
|
return Err(Error::OneLetterWord);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// need to verify that the play is 'anchored'
|
// need to verify that the play is 'anchored'
|
||||||
let mut anchored = false;
|
let mut anchored = false;
|
||||||
'outer: for word in words.as_slice() {
|
'outer: for word in words.as_slice() {
|
||||||
for cell in word.cells.as_slice() {
|
for cell in word.cells.as_slice() {
|
||||||
// either one of the letters
|
// either one of the letters
|
||||||
if !cell.value.as_ref().unwrap().ephemeral || (cell.coordinates.0 == GRID_LENGTH / 2 && cell.coordinates.1 == GRID_LENGTH / 2){
|
if !cell.value.as_ref().unwrap().ephemeral
|
||||||
|
|| (cell.coordinates.0 == GRID_LENGTH / 2
|
||||||
|
&& cell.coordinates.1 == GRID_LENGTH / 2)
|
||||||
|
{
|
||||||
anchored = true;
|
anchored = true;
|
||||||
break 'outer;
|
break 'outer;
|
||||||
}
|
}
|
||||||
|
@ -387,23 +382,27 @@ impl Board {
|
||||||
if anchored {
|
if anchored {
|
||||||
Ok((words, tiles_played))
|
Ok((words, tiles_played))
|
||||||
} else {
|
} else {
|
||||||
return Err("Played tiles must be anchored to something")
|
Err(Error::UnanchoredWord)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_word_at_position(&self, mut start_coords: Coordinates, direction: Direction) -> Option<Word> {
|
pub fn find_word_at_position(
|
||||||
|
&self,
|
||||||
|
mut start_coords: Coordinates,
|
||||||
|
direction: Direction,
|
||||||
|
) -> Option<Word> {
|
||||||
// let's see how far we can backtrack to the start of the word
|
// let's see how far we can backtrack to the start of the word
|
||||||
let mut times_moved = 0;
|
let mut times_moved = 0;
|
||||||
loop {
|
loop {
|
||||||
let one_back = start_coords.add(direction, -times_moved);
|
let one_back = start_coords.add(direction, -times_moved);
|
||||||
match one_back {
|
match one_back {
|
||||||
None => { break }
|
None => break,
|
||||||
Some(new_coords) => {
|
Some(new_coords) => {
|
||||||
let cell = self.get_cell(new_coords).unwrap();
|
let cell = self.get_cell(new_coords).unwrap();
|
||||||
if cell.value.is_some(){
|
if cell.value.is_some() {
|
||||||
times_moved += 1;
|
times_moved += 1;
|
||||||
} else {
|
} else {
|
||||||
break
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -423,11 +422,11 @@ impl Board {
|
||||||
loop {
|
loop {
|
||||||
let position = start_coords.add(direction, cells.len() as i8);
|
let position = start_coords.add(direction, cells.len() as i8);
|
||||||
match position {
|
match position {
|
||||||
None => {break}
|
None => break,
|
||||||
Some(x) => {
|
Some(x) => {
|
||||||
let cell = self.get_cell(x).unwrap();
|
let cell = self.get_cell(x).unwrap();
|
||||||
match cell.value {
|
match cell.value {
|
||||||
None => {break}
|
None => break,
|
||||||
Some(_) => {
|
Some(_) => {
|
||||||
cells.push(cell);
|
cells.push(cell);
|
||||||
}
|
}
|
||||||
|
@ -442,16 +441,16 @@ impl Board {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn receive_play(&mut self, play: Vec<(Letter, Coordinates)>) -> Result<(), String> {
|
pub fn receive_play(&mut self, play: Vec<(Letter, Coordinates)>) -> Result<(), Error> {
|
||||||
for (mut letter, coords) in play {
|
for (mut letter, coords) in play {
|
||||||
{
|
{
|
||||||
let cell = match self.get_cell_mut(coords) {
|
let cell = self.get_cell_mut(coords)?;
|
||||||
Ok(cell) => {cell}
|
|
||||||
Err(e) => {return Err(e.to_string())}
|
|
||||||
};
|
|
||||||
|
|
||||||
if cell.value.is_some() {
|
if cell.value.is_some() {
|
||||||
return Err(format!("There's already a letter at {:?}", coords));
|
return Err(Error::Other(format!(
|
||||||
|
"There's already a letter at {:?}",
|
||||||
|
coords
|
||||||
|
)));
|
||||||
}
|
}
|
||||||
|
|
||||||
letter.ephemeral = true;
|
letter.ephemeral = true;
|
||||||
|
@ -476,7 +475,6 @@ impl Board {
|
||||||
|
|
||||||
impl fmt::Display for Board {
|
impl fmt::Display for Board {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
|
||||||
|
|
||||||
let mut str = String::new();
|
let mut str = String::new();
|
||||||
|
|
||||||
let normal = "\x1b[48;5;174m\x1b[38;5;0m";
|
let normal = "\x1b[48;5;174m\x1b[38;5;0m";
|
||||||
|
@ -493,57 +491,82 @@ impl fmt::Display for Board {
|
||||||
let cell = self.get_cell(coords).unwrap();
|
let cell = self.get_cell(coords).unwrap();
|
||||||
|
|
||||||
let color = match cell.cell_type {
|
let color = match cell.cell_type {
|
||||||
CellType::Normal => {normal}
|
CellType::Normal => normal,
|
||||||
CellType::DoubleWord => {double_word}
|
CellType::DoubleWord => double_word,
|
||||||
CellType::DoubleLetter => {double_letter}
|
CellType::DoubleLetter => double_letter,
|
||||||
CellType::TripleLetter => {triple_letter}
|
CellType::TripleLetter => triple_letter,
|
||||||
CellType::TripleWord => {triple_word}
|
CellType::TripleWord => triple_word,
|
||||||
CellType::Start => {double_word}
|
CellType::Start => double_word,
|
||||||
};
|
};
|
||||||
|
|
||||||
let content = match &cell.value {
|
let content = match &cell.value {
|
||||||
None => {' '}
|
None => ' ',
|
||||||
Some(letter) => {letter.text}
|
Some(letter) => letter.text,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
str.write_str(color).unwrap();
|
str.write_str(color).unwrap();
|
||||||
str.write_char(content).unwrap();
|
str.write_char(content).unwrap();
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
str.write_str("\x1b[0m\n").unwrap();
|
str.write_str("\x1b[0m\n").unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
write!(f, "{}", str)
|
write!(f, "{}", str)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::dictionary::Dictionary;
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::dictionary::Dictionary;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cell_types() {
|
fn test_cell_types() {
|
||||||
let board = Board::new();
|
let board = Board::new();
|
||||||
|
|
||||||
assert!(matches!(board.get_cell(Coordinates(0, 0)).unwrap().cell_type, CellType::TripleWord));
|
assert!(matches!(
|
||||||
assert!(matches!(board.get_cell(Coordinates(1, 0)).unwrap().cell_type, CellType::Normal));
|
board.get_cell(Coordinates(0, 0)).unwrap().cell_type,
|
||||||
assert!(matches!(board.get_cell(Coordinates(0, 1)).unwrap().cell_type, CellType::Normal));
|
CellType::TripleWord
|
||||||
assert!(matches!(board.get_cell(Coordinates(1, 1)).unwrap().cell_type, CellType::DoubleWord));
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
board.get_cell(Coordinates(1, 0)).unwrap().cell_type,
|
||||||
|
CellType::Normal
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
board.get_cell(Coordinates(0, 1)).unwrap().cell_type,
|
||||||
|
CellType::Normal
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
board.get_cell(Coordinates(1, 1)).unwrap().cell_type,
|
||||||
|
CellType::DoubleWord
|
||||||
|
));
|
||||||
|
|
||||||
assert!(matches!(board.get_cell(Coordinates(13, 13)).unwrap().cell_type, CellType::DoubleWord));
|
assert!(matches!(
|
||||||
assert!(matches!(board.get_cell(Coordinates(14, 14)).unwrap().cell_type, CellType::TripleWord));
|
board.get_cell(Coordinates(13, 13)).unwrap().cell_type,
|
||||||
assert!(matches!(board.get_cell(Coordinates(11, 14)).unwrap().cell_type, CellType::DoubleLetter));
|
CellType::DoubleWord
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
board.get_cell(Coordinates(14, 14)).unwrap().cell_type,
|
||||||
|
CellType::TripleWord
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
board.get_cell(Coordinates(11, 14)).unwrap().cell_type,
|
||||||
|
CellType::DoubleLetter
|
||||||
|
));
|
||||||
|
|
||||||
assert!(matches!(board.get_cell(Coordinates(7, 7)).unwrap().cell_type, CellType::Start));
|
assert!(matches!(
|
||||||
assert!(matches!(board.get_cell(Coordinates(8, 6)).unwrap().cell_type, CellType::DoubleLetter));
|
board.get_cell(Coordinates(7, 7)).unwrap().cell_type,
|
||||||
assert!(matches!(board.get_cell(Coordinates(5, 9)).unwrap().cell_type, CellType::TripleLetter));
|
CellType::Start
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
board.get_cell(Coordinates(8, 6)).unwrap().cell_type,
|
||||||
|
CellType::DoubleLetter
|
||||||
|
));
|
||||||
|
assert!(matches!(
|
||||||
|
board.get_cell(Coordinates(5, 9)).unwrap().cell_type,
|
||||||
|
CellType::TripleLetter
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_cell_coordinates() {
|
fn test_cell_coordinates() {
|
||||||
let board = Board::new();
|
let board = Board::new();
|
||||||
|
@ -576,7 +599,6 @@ mod tests {
|
||||||
board.get_cell_mut(Coordinates(5, 0)).unwrap().value = Some(Letter::new_fixed('O', 0));
|
board.get_cell_mut(Coordinates(5, 0)).unwrap().value = Some(Letter::new_fixed('O', 0));
|
||||||
board.get_cell_mut(Coordinates(6, 0)).unwrap().value = Some(Letter::new_fixed('L', 0));
|
board.get_cell_mut(Coordinates(6, 0)).unwrap().value = Some(Letter::new_fixed('L', 0));
|
||||||
|
|
||||||
|
|
||||||
board.get_cell_mut(Coordinates(9, 8)).unwrap().value = Some(Letter::new_fixed('G', 0));
|
board.get_cell_mut(Coordinates(9, 8)).unwrap().value = Some(Letter::new_fixed('G', 0));
|
||||||
board.get_cell_mut(Coordinates(10, 8)).unwrap().value = Some(Letter::new_fixed('G', 0));
|
board.get_cell_mut(Coordinates(10, 8)).unwrap().value = Some(Letter::new_fixed('G', 0));
|
||||||
|
|
||||||
|
@ -584,7 +606,9 @@ mod tests {
|
||||||
println!("x is {}", x);
|
println!("x is {}", x);
|
||||||
let first_word = board.find_word_at_position(Coordinates(8, x), Direction::Column);
|
let first_word = board.find_word_at_position(Coordinates(8, x), Direction::Column);
|
||||||
match first_word {
|
match first_word {
|
||||||
None => { panic!("Expected to find word JOEL") }
|
None => {
|
||||||
|
panic!("Expected to find word JOEL")
|
||||||
|
}
|
||||||
Some(x) => {
|
Some(x) => {
|
||||||
assert_eq!(x.coords.0, 8);
|
assert_eq!(x.coords.0, 8);
|
||||||
assert_eq!(x.coords.1, 6);
|
assert_eq!(x.coords.1, 6);
|
||||||
|
@ -596,7 +620,9 @@ mod tests {
|
||||||
|
|
||||||
let single_letter_word = board.find_word_at_position(Coordinates(8, 9), Direction::Row);
|
let single_letter_word = board.find_word_at_position(Coordinates(8, 9), Direction::Row);
|
||||||
match single_letter_word {
|
match single_letter_word {
|
||||||
None => { panic!("Expected to find letter L") }
|
None => {
|
||||||
|
panic!("Expected to find letter L")
|
||||||
|
}
|
||||||
Some(x) => {
|
Some(x) => {
|
||||||
assert_eq!(x.coords.0, 8);
|
assert_eq!(x.coords.0, 8);
|
||||||
assert_eq!(x.coords.1, 9);
|
assert_eq!(x.coords.1, 9);
|
||||||
|
@ -609,7 +635,9 @@ mod tests {
|
||||||
println!("x is {}", x);
|
println!("x is {}", x);
|
||||||
let word = board.find_word_at_position(Coordinates(x, 0), Direction::Row);
|
let word = board.find_word_at_position(Coordinates(x, 0), Direction::Row);
|
||||||
match word {
|
match word {
|
||||||
None => { panic!("Expected to find word IS") }
|
None => {
|
||||||
|
panic!("Expected to find word IS")
|
||||||
|
}
|
||||||
Some(x) => {
|
Some(x) => {
|
||||||
assert_eq!(x.coords.0, 0);
|
assert_eq!(x.coords.0, 0);
|
||||||
assert_eq!(x.coords.1, 0);
|
assert_eq!(x.coords.1, 0);
|
||||||
|
@ -623,7 +651,9 @@ mod tests {
|
||||||
println!("x is {}", x);
|
println!("x is {}", x);
|
||||||
let word = board.find_word_at_position(Coordinates(x, 0), Direction::Row);
|
let word = board.find_word_at_position(Coordinates(x, 0), Direction::Row);
|
||||||
match word {
|
match word {
|
||||||
None => { panic!("Expected to find word COOL") }
|
None => {
|
||||||
|
panic!("Expected to find word COOL")
|
||||||
|
}
|
||||||
Some(x) => {
|
Some(x) => {
|
||||||
assert_eq!(x.coords.0, 3);
|
assert_eq!(x.coords.0, 3);
|
||||||
assert_eq!(x.coords.1, 0);
|
assert_eq!(x.coords.1, 0);
|
||||||
|
@ -638,7 +668,9 @@ mod tests {
|
||||||
|
|
||||||
let word = board.find_word_at_position(Coordinates(10, 8), Direction::Row);
|
let word = board.find_word_at_position(Coordinates(10, 8), Direction::Row);
|
||||||
match word {
|
match word {
|
||||||
None => { panic!("Expected to find word EGG") }
|
None => {
|
||||||
|
panic!("Expected to find word EGG")
|
||||||
|
}
|
||||||
Some(x) => {
|
Some(x) => {
|
||||||
assert_eq!(x.coords.0, 8);
|
assert_eq!(x.coords.0, 8);
|
||||||
assert_eq!(x.coords.1, 8);
|
assert_eq!(x.coords.1, 8);
|
||||||
|
@ -659,10 +691,10 @@ mod tests {
|
||||||
is_blank: false,
|
is_blank: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
match board.find_played_words() {
|
assert!(matches!(
|
||||||
Ok(_) => {panic!("Expected error")}
|
board.find_played_words(),
|
||||||
Err(e) => {assert_eq!(e, "All words must be at least one letter");}
|
Err(Error::OneLetterWord)
|
||||||
}
|
));
|
||||||
|
|
||||||
board.get_cell_mut(Coordinates(7, 7)).unwrap().value = Some(Letter {
|
board.get_cell_mut(Coordinates(7, 7)).unwrap().value = Some(Letter {
|
||||||
text: 'I',
|
text: 'I',
|
||||||
|
@ -726,10 +758,7 @@ mod tests {
|
||||||
board.get_cell_mut(Coordinates(8, 9)).unwrap().value = Some(make_letter('L', true));
|
board.get_cell_mut(Coordinates(8, 9)).unwrap().value = Some(make_letter('L', true));
|
||||||
|
|
||||||
let words = board.find_played_words();
|
let words = board.find_played_words();
|
||||||
match words {
|
assert!(matches!(words, Err(Error::UnanchoredWord)));
|
||||||
Ok(_) => {panic!("Expected the not-anchored error")}
|
|
||||||
Err(x) => {assert_eq!(x, "Played tiles must be anchored to something")}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Adding anchor
|
// Adding anchor
|
||||||
board.get_cell_mut(Coordinates(7, 6)).unwrap().value = Some(make_letter('I', false));
|
board.get_cell_mut(Coordinates(7, 6)).unwrap().value = Some(make_letter('I', false));
|
||||||
|
@ -746,7 +775,6 @@ mod tests {
|
||||||
assert!(board.find_played_words().is_ok());
|
assert!(board.find_played_words().is_ok());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_word_finding_with_break() {
|
fn test_word_finding_with_break() {
|
||||||
// Verify that if I play my tiles on one row or column but with a break in-between I get an error
|
// Verify that if I play my tiles on one row or column but with a break in-between I get an error
|
||||||
|
@ -765,22 +793,15 @@ mod tests {
|
||||||
board.get_cell_mut(Coordinates(8, 6)).unwrap().value = Some(Letter::new_fixed('J', 0));
|
board.get_cell_mut(Coordinates(8, 6)).unwrap().value = Some(Letter::new_fixed('J', 0));
|
||||||
board.get_cell_mut(Coordinates(8, 7)).unwrap().value = Some(make_letter('O', true));
|
board.get_cell_mut(Coordinates(8, 7)).unwrap().value = Some(make_letter('O', true));
|
||||||
board.get_cell_mut(Coordinates(8, 8)).unwrap().value = Some(make_letter('E', true));
|
board.get_cell_mut(Coordinates(8, 8)).unwrap().value = Some(make_letter('E', true));
|
||||||
board.get_cell_mut(Coordinates(8, 9)).unwrap().value = Some(Letter::new_fixed( 'L', 0));
|
board.get_cell_mut(Coordinates(8, 9)).unwrap().value = Some(Letter::new_fixed('L', 0));
|
||||||
|
|
||||||
board.get_cell_mut(Coordinates(8, 11)).unwrap().value = Some(make_letter('I', true));
|
board.get_cell_mut(Coordinates(8, 11)).unwrap().value = Some(make_letter('I', true));
|
||||||
board.get_cell_mut(Coordinates(8, 12)).unwrap().value = Some(Letter::new_fixed('S', 0));
|
board.get_cell_mut(Coordinates(8, 12)).unwrap().value = Some(Letter::new_fixed('S', 0));
|
||||||
|
|
||||||
let words = board.find_played_words();
|
let words = board.find_played_words();
|
||||||
match words {
|
assert!(matches!(words, Err(Error::TilesHaveGap)));
|
||||||
Ok(_) => {panic!("Expected to find an error!")}
|
|
||||||
Err(x) => {
|
|
||||||
assert_eq!(x, "Played tiles cannot have empty gap")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_word_finding_whole_board() {
|
fn test_word_finding_whole_board() {
|
||||||
let mut board = Board::new();
|
let mut board = Board::new();
|
||||||
|
@ -795,15 +816,12 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
let words = board.find_played_words();
|
let words = board.find_played_words();
|
||||||
match words {
|
assert!(matches!(words, Err(Error::NoTilesPlayed)));
|
||||||
Ok(_) => {panic!("Expected to find no words")}
|
|
||||||
Err(x) => {assert_eq!(x, "Tiles need to be played")}
|
|
||||||
}
|
|
||||||
|
|
||||||
board.get_cell_mut(Coordinates(8, 6)).unwrap().value = Some(Letter::new_fixed('J', 8));
|
board.get_cell_mut(Coordinates(8, 6)).unwrap().value = Some(Letter::new_fixed('J', 8));
|
||||||
board.get_cell_mut(Coordinates(8, 7)).unwrap().value = Some(make_letter('O', true, 1));
|
board.get_cell_mut(Coordinates(8, 7)).unwrap().value = Some(make_letter('O', true, 1));
|
||||||
board.get_cell_mut(Coordinates(8, 8)).unwrap().value = Some(make_letter('E', true, 1));
|
board.get_cell_mut(Coordinates(8, 8)).unwrap().value = Some(make_letter('E', true, 1));
|
||||||
board.get_cell_mut(Coordinates(8, 9)).unwrap().value = Some(Letter::new_fixed( 'L', 1));
|
board.get_cell_mut(Coordinates(8, 9)).unwrap().value = Some(Letter::new_fixed('L', 1));
|
||||||
|
|
||||||
board.get_cell_mut(Coordinates(0, 0)).unwrap().value = Some(Letter::new_fixed('I', 1));
|
board.get_cell_mut(Coordinates(0, 0)).unwrap().value = Some(Letter::new_fixed('I', 1));
|
||||||
board.get_cell_mut(Coordinates(1, 0)).unwrap().value = Some(Letter::new_fixed('S', 1));
|
board.get_cell_mut(Coordinates(1, 0)).unwrap().value = Some(Letter::new_fixed('S', 1));
|
||||||
|
@ -814,7 +832,7 @@ mod tests {
|
||||||
board.get_cell_mut(Coordinates(6, 0)).unwrap().value = Some(Letter::new_fixed('L', 1));
|
board.get_cell_mut(Coordinates(6, 0)).unwrap().value = Some(Letter::new_fixed('L', 1));
|
||||||
|
|
||||||
fn check_board(board: &mut Board, inverted: bool) {
|
fn check_board(board: &mut Board, inverted: bool) {
|
||||||
let dictionary = DictionaryImpl::create_from_path("resources/dictionary.csv");
|
let dictionary = DictionaryImpl::create_from_path("../resources/dictionary.csv");
|
||||||
println!("{}", board);
|
println!("{}", board);
|
||||||
let words = board.find_played_words();
|
let words = board.find_played_words();
|
||||||
match words {
|
match words {
|
||||||
|
@ -827,7 +845,9 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(word.calculate_score(), 8 + 1 + 2 + 1);
|
assert_eq!(word.calculate_score(), 8 + 1 + 2 + 1);
|
||||||
}
|
}
|
||||||
Err(e) => { panic!("Expected to find a word to play; found error {}", e) }
|
Err(e) => {
|
||||||
|
panic!("Expected to find a word to play; found error {}", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let maybe_invert = |coords: Coordinates| {
|
let maybe_invert = |coords: Coordinates| {
|
||||||
|
@ -844,12 +864,21 @@ mod tests {
|
||||||
return direction;
|
return direction;
|
||||||
};
|
};
|
||||||
|
|
||||||
board.get_cell_mut(maybe_invert(Coordinates(9, 8))).unwrap().value = Some(Letter::new_fixed('G', 2));
|
board
|
||||||
board.get_cell_mut(maybe_invert(Coordinates(10, 8))).unwrap().value = Some(Letter::new_fixed('G', 2));
|
.get_cell_mut(maybe_invert(Coordinates(9, 8)))
|
||||||
|
.unwrap()
|
||||||
|
.value = Some(Letter::new_fixed('G', 2));
|
||||||
|
board
|
||||||
|
.get_cell_mut(maybe_invert(Coordinates(10, 8)))
|
||||||
|
.unwrap()
|
||||||
|
.value = Some(Letter::new_fixed('G', 2));
|
||||||
|
|
||||||
let word = board.find_word_at_position(Coordinates(8, 8), maybe_invert_direction(Direction::Row));
|
let word = board
|
||||||
|
.find_word_at_position(Coordinates(8, 8), maybe_invert_direction(Direction::Row));
|
||||||
match word {
|
match word {
|
||||||
None => {panic!("Expected to find word EGG")}
|
None => {
|
||||||
|
panic!("Expected to find word EGG")
|
||||||
|
}
|
||||||
Some(x) => {
|
Some(x) => {
|
||||||
assert_eq!(x.coords.0, 8);
|
assert_eq!(x.coords.0, 8);
|
||||||
assert_eq!(x.coords.1, 8);
|
assert_eq!(x.coords.1, 8);
|
||||||
|
@ -857,7 +886,6 @@ mod tests {
|
||||||
assert_eq!(x.to_string(), "EGG");
|
assert_eq!(x.to_string(), "EGG");
|
||||||
assert_eq!(x.calculate_score(), 2 + 2 + 2);
|
assert_eq!(x.calculate_score(), 2 + 2 + 2);
|
||||||
assert!(dictionary.is_word_valid(&x));
|
assert!(dictionary.is_word_valid(&x));
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -876,20 +904,29 @@ mod tests {
|
||||||
assert_eq!(word.calculate_score(), 8 + 1 + 2 + 1);
|
assert_eq!(word.calculate_score(), 8 + 1 + 2 + 1);
|
||||||
assert!(!dictionary.is_word_valid(word));
|
assert!(!dictionary.is_word_valid(word));
|
||||||
}
|
}
|
||||||
Err(e) => { panic!("Expected to find a word to play; found error {}", e) }
|
Err(e) => {
|
||||||
|
panic!("Expected to find a word to play; found error {}", e)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let scores = board.calculate_scores(&dictionary);
|
let scores = board.calculate_scores(&dictionary);
|
||||||
match scores {
|
match scores {
|
||||||
Ok(_) => {panic!("Expected an error")}
|
Ok(_) => {
|
||||||
Err(e) => {assert_eq!(e, "JOEL is not a valid word")}
|
panic!("Expected an error")
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
if let Error::InvalidWord(w) = e {
|
||||||
|
assert_eq!(w, "JOEL");
|
||||||
|
} else {
|
||||||
|
panic!("Expected an InvalidPlay error")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut alt_dictionary = DictionaryImpl::new();
|
let mut alt_dictionary = DictionaryImpl::new();
|
||||||
alt_dictionary.insert("JOEL".to_string(), 0.5);
|
alt_dictionary.insert("JOEL".to_string(), 0.5);
|
||||||
alt_dictionary.insert("EGG".to_string(), 0.5);
|
alt_dictionary.insert("EGG".to_string(), 0.5);
|
||||||
|
|
||||||
|
|
||||||
let scores = board.calculate_scores(&alt_dictionary);
|
let scores = board.calculate_scores(&alt_dictionary);
|
||||||
match scores {
|
match scores {
|
||||||
Ok((words, total_score)) => {
|
Ok((words, total_score)) => {
|
||||||
|
@ -906,18 +943,19 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(total_score, 18);
|
assert_eq!(total_score, 18);
|
||||||
}
|
}
|
||||||
Err(e) => {panic!("Wasn't expecting to encounter error {e}")}
|
Err(e) => {
|
||||||
|
panic!("Wasn't expecting to encounter error {e}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// replace one of the 'G' in EGG with an ephemeral to trigger an error
|
// replace one of the 'G' in EGG with an ephemeral to trigger an error
|
||||||
board.get_cell_mut(maybe_invert(Coordinates(9, 8))).unwrap().value = Some(make_letter('G', true, 2));
|
board
|
||||||
|
.get_cell_mut(maybe_invert(Coordinates(9, 8)))
|
||||||
|
.unwrap()
|
||||||
|
.value = Some(make_letter('G', true, 2));
|
||||||
|
|
||||||
let words = board.find_played_words();
|
let words = board.find_played_words();
|
||||||
match words {
|
assert!(matches!(words, Err(Error::TilesNotStraight)));
|
||||||
Ok(_) => { panic!("Expected error as we played tiles in multiple rows and columns") }
|
|
||||||
Err(e) => { assert_eq!(e, "Tiles need to be played on one row or column") }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// make a copy of the board now with x and y swapped
|
// make a copy of the board now with x and y swapped
|
||||||
|
@ -934,7 +972,6 @@ mod tests {
|
||||||
cell_new.value = Some(*x);
|
cell_new.value = Some(*x);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -943,8 +980,5 @@ mod tests {
|
||||||
|
|
||||||
println!("Checking inverted board");
|
println!("Checking inverted board");
|
||||||
check_board(&mut inverted_board, true);
|
check_board(&mut inverted_board, true);
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
|
@ -1,6 +1,6 @@
|
||||||
use rand::prelude::SliceRandom;
|
|
||||||
use rand::{Rng};
|
|
||||||
use crate::board::Letter;
|
use crate::board::Letter;
|
||||||
|
use rand::prelude::SliceRandom;
|
||||||
|
use rand::Rng;
|
||||||
|
|
||||||
pub const GRID_LENGTH: u8 = 15;
|
pub const GRID_LENGTH: u8 = 15;
|
||||||
pub const TRAY_LENGTH: u8 = 7;
|
pub const TRAY_LENGTH: u8 = 7;
|
||||||
|
@ -49,5 +49,4 @@ pub fn standard_tile_pool<R: Rng>(rng: Option<&mut R>) -> Vec<Letter> {
|
||||||
}
|
}
|
||||||
|
|
||||||
letters
|
letters
|
||||||
|
}
|
||||||
}
|
|
|
@ -1,10 +1,9 @@
|
||||||
|
use crate::board::Word;
|
||||||
|
use csv::Reader;
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
use csv::Reader;
|
|
||||||
use crate::board::Word;
|
|
||||||
|
|
||||||
pub trait Dictionary {
|
pub trait Dictionary {
|
||||||
|
|
||||||
fn create_from_reader<T: std::io::Read>(reader: Reader<T>) -> Self;
|
fn create_from_reader<T: std::io::Read>(reader: Reader<T>) -> Self;
|
||||||
fn create_from_path(path: &str) -> Self;
|
fn create_from_path(path: &str) -> Self;
|
||||||
fn create_from_str(data: &str) -> Self;
|
fn create_from_str(data: &str) -> Self;
|
||||||
|
@ -14,8 +13,7 @@ pub trait Dictionary {
|
||||||
}
|
}
|
||||||
pub type DictionaryImpl = HashMap<String, f64>;
|
pub type DictionaryImpl = HashMap<String, f64>;
|
||||||
|
|
||||||
impl Dictionary for DictionaryImpl{
|
impl Dictionary for DictionaryImpl {
|
||||||
|
|
||||||
fn create_from_reader<T: std::io::Read>(mut reader: Reader<T>) -> Self {
|
fn create_from_reader<T: std::io::Read>(mut reader: Reader<T>) -> Self {
|
||||||
let mut map = HashMap::new();
|
let mut map = HashMap::new();
|
||||||
|
|
||||||
|
@ -27,7 +25,6 @@ impl Dictionary for DictionaryImpl{
|
||||||
let score = f64::from_str(score).unwrap();
|
let score = f64::from_str(score).unwrap();
|
||||||
|
|
||||||
map.insert(word, score);
|
map.insert(word, score);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
map
|
map
|
||||||
|
@ -57,7 +54,6 @@ impl Dictionary for DictionaryImpl{
|
||||||
}
|
}
|
||||||
|
|
||||||
map
|
map
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn substring_set(&self) -> HashSet<&str> {
|
fn substring_set(&self) -> HashSet<&str> {
|
||||||
|
@ -65,11 +61,10 @@ impl Dictionary for DictionaryImpl{
|
||||||
|
|
||||||
for (word, _score) in self.iter() {
|
for (word, _score) in self.iter() {
|
||||||
for j in 0..word.len() {
|
for j in 0..word.len() {
|
||||||
for k in (j+1)..(word.len()+1) {
|
for k in (j + 1)..(word.len() + 1) {
|
||||||
set.insert(&word[j..k]);
|
set.insert(&word[j..k]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
set
|
set
|
||||||
|
@ -85,10 +80,9 @@ impl Dictionary for DictionaryImpl{
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_dictionary() {
|
fn test_dictionary() {
|
||||||
let dictionary = HashMap::create_from_path("resources/dictionary.csv");
|
let dictionary = HashMap::create_from_path("../resources/dictionary.csv");
|
||||||
|
|
||||||
assert_eq!(dictionary.len(), 279429);
|
assert_eq!(dictionary.len(), 279429);
|
||||||
|
|
||||||
|
@ -96,7 +90,6 @@ mod tests {
|
||||||
assert!(dictionary.contains_key("AARDVARK"));
|
assert!(dictionary.contains_key("AARDVARK"));
|
||||||
|
|
||||||
assert!((dictionary.get("AARDVARK").unwrap() - 0.5798372).abs() < 0.0001)
|
assert!((dictionary.get("AARDVARK").unwrap() - 0.5798372).abs() < 0.0001)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -131,11 +124,8 @@ mod tests {
|
||||||
assert!(set.contains("JOH"));
|
assert!(set.contains("JOH"));
|
||||||
assert!(set.contains("OHN"));
|
assert!(set.contains("OHN"));
|
||||||
|
|
||||||
|
|
||||||
assert!(!set.contains("XY"));
|
assert!(!set.contains("XY"));
|
||||||
assert!(!set.contains("JH"));
|
assert!(!set.contains("JH"));
|
||||||
assert!(!set.contains("JE"));
|
assert!(!set.contains("JE"));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
|
@ -1,31 +1,90 @@
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use std::fmt::{Display, Formatter};
|
||||||
|
|
||||||
use rand::prelude::SliceRandom;
|
|
||||||
use rand::rngs::SmallRng;
|
|
||||||
use rand::SeedableRng;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use tsify::Tsify;
|
|
||||||
use crate::board::{Board, Coordinates, Letter};
|
use crate::board::{Board, Coordinates, Letter};
|
||||||
use crate::constants::{standard_tile_pool, TRAY_LENGTH};
|
use crate::constants::{standard_tile_pool, TRAY_LENGTH};
|
||||||
use crate::dictionary::{Dictionary, DictionaryImpl};
|
use crate::dictionary::{Dictionary, DictionaryImpl};
|
||||||
use crate::player_interaction::ai::{AI, CompleteMove, Difficulty};
|
use crate::player_interaction::ai::{Difficulty, AI};
|
||||||
use crate::player_interaction::Tray;
|
use crate::player_interaction::Tray;
|
||||||
|
use rand::prelude::SliceRandom;
|
||||||
|
use rand::rngs::SmallRng;
|
||||||
|
use rand::SeedableRng;
|
||||||
|
use serde::{Deserialize, Serialize, Serializer};
|
||||||
|
|
||||||
pub enum Player {
|
pub enum Player {
|
||||||
Human(String),
|
Human(String),
|
||||||
AI{
|
AI {
|
||||||
name: String,
|
name: String,
|
||||||
difficulty: Difficulty,
|
difficulty: Difficulty,
|
||||||
object: AI,
|
object: AI,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub enum Error {
|
||||||
|
InvalidPlayer(String),
|
||||||
|
WrongTurn(String),
|
||||||
|
Other(String),
|
||||||
|
InvalidWord(String),
|
||||||
|
GameFinished,
|
||||||
|
NoTilesPlayed,
|
||||||
|
TilesNotStraight,
|
||||||
|
TilesHaveGap,
|
||||||
|
OneLetterWord,
|
||||||
|
UnanchoredWord,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Error {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match &self {
|
||||||
|
Error::InvalidPlayer(player) => {
|
||||||
|
write!(f, "{player} doesn't exist")
|
||||||
|
}
|
||||||
|
Error::WrongTurn(player) => {
|
||||||
|
write!(f, "It's not {player}'s turn")
|
||||||
|
}
|
||||||
|
Error::Other(msg) => {
|
||||||
|
write!(f, "Other error: {msg}")
|
||||||
|
}
|
||||||
|
Error::InvalidWord(word) => {
|
||||||
|
write!(f, "{word} is not a valid word")
|
||||||
|
}
|
||||||
|
Error::GameFinished => {
|
||||||
|
write!(f, "Moves cannot be made after a game has finished")
|
||||||
|
}
|
||||||
|
Error::NoTilesPlayed => {
|
||||||
|
write!(f, "Tiles need to be played")
|
||||||
|
}
|
||||||
|
Error::TilesNotStraight => {
|
||||||
|
write!(f, "Tiles need to be played on one row or column")
|
||||||
|
}
|
||||||
|
Error::TilesHaveGap => {
|
||||||
|
write!(f, "Played tiles cannot have empty gap")
|
||||||
|
}
|
||||||
|
Error::OneLetterWord => {
|
||||||
|
write!(f, "All words must be at least two letters")
|
||||||
|
}
|
||||||
|
Error::UnanchoredWord => {
|
||||||
|
write!(f, "Played tiles must be anchored to something")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Serialize for Error {
|
||||||
|
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
|
||||||
|
where
|
||||||
|
S: Serializer,
|
||||||
|
{
|
||||||
|
serializer.serialize_str(&self.to_string())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Player {
|
impl Player {
|
||||||
|
|
||||||
pub fn get_name(&self) -> &str {
|
pub fn get_name(&self) -> &str {
|
||||||
match &self {
|
match &self {
|
||||||
Player::Human(name) => {name}
|
Player::Human(name) => name,
|
||||||
Player::AI { name, .. } => {name}
|
Player::AI { name, .. } => name,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -33,18 +92,20 @@ impl Player {
|
||||||
pub struct PlayerState {
|
pub struct PlayerState {
|
||||||
pub player: Player,
|
pub player: Player,
|
||||||
pub score: u32,
|
pub score: u32,
|
||||||
pub tray: Tray
|
pub tray: Tray,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Tsify, Copy, Clone, Debug)]
|
#[derive(Deserialize, Copy, Clone, Debug)]
|
||||||
#[tsify(from_wasm_abi)]
|
|
||||||
pub struct PlayedTile {
|
pub struct PlayedTile {
|
||||||
pub index: usize,
|
pub index: usize,
|
||||||
pub character: Option<char>, // we only set this if PlayedTile is a blank
|
pub character: Option<char>, // we only set this if PlayedTile is a blank
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PlayedTile {
|
impl PlayedTile {
|
||||||
pub fn convert_tray(tray_tile_locations: &Vec<Option<PlayedTile>>, tray: &Tray) -> Vec<(Letter, Coordinates)> {
|
pub fn convert_tray(
|
||||||
|
tray_tile_locations: &Vec<Option<PlayedTile>>,
|
||||||
|
tray: &Tray,
|
||||||
|
) -> Vec<(Letter, Coordinates)> {
|
||||||
let mut played_letters: Vec<(Letter, Coordinates)> = Vec::new();
|
let mut played_letters: Vec<(Letter, Coordinates)> = Vec::new();
|
||||||
for (i, played_tile) in tray_tile_locations.iter().enumerate() {
|
for (i, played_tile) in tray_tile_locations.iter().enumerate() {
|
||||||
if played_tile.is_some() {
|
if played_tile.is_some() {
|
||||||
|
@ -55,7 +116,9 @@ impl PlayedTile {
|
||||||
if letter.is_blank {
|
if letter.is_blank {
|
||||||
match played_tile.character {
|
match played_tile.character {
|
||||||
None => {
|
None => {
|
||||||
panic!("You can't play a blank character without providing a letter value")
|
panic!(
|
||||||
|
"You can't play a blank character without providing a letter value"
|
||||||
|
)
|
||||||
}
|
}
|
||||||
Some(x) => {
|
Some(x) => {
|
||||||
// TODO - check that x is a valid alphabet letter
|
// TODO - check that x is a valid alphabet letter
|
||||||
|
@ -64,7 +127,6 @@ impl PlayedTile {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
played_letters.push((letter, coord));
|
played_letters.push((letter, coord));
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,15 +134,13 @@ impl PlayedTile {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Tsify)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[tsify(from_wasm_abi)]
|
|
||||||
pub struct WordResult {
|
pub struct WordResult {
|
||||||
word: String,
|
word: String,
|
||||||
score: u32,
|
score: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Tsify)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
#[tsify(from_wasm_abi)]
|
|
||||||
pub struct ScoreResult {
|
pub struct ScoreResult {
|
||||||
words: Vec<WordResult>,
|
words: Vec<WordResult>,
|
||||||
total: u32,
|
total: u32,
|
||||||
|
@ -88,7 +148,6 @@ pub struct ScoreResult {
|
||||||
|
|
||||||
pub struct PlayerStates(pub Vec<PlayerState>);
|
pub struct PlayerStates(pub Vec<PlayerState>);
|
||||||
impl PlayerStates {
|
impl PlayerStates {
|
||||||
|
|
||||||
fn get_player_name_by_turn_id(&self, id: usize) -> &str {
|
fn get_player_name_by_turn_id(&self, id: usize) -> &str {
|
||||||
let id_mod = id % self.0.len();
|
let id_mod = id % self.0.len();
|
||||||
let state = self.0.get(id_mod).unwrap();
|
let state = self.0.get(id_mod).unwrap();
|
||||||
|
@ -97,37 +156,33 @@ impl PlayerStates {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_player_state(&self, name: &str) -> Option<&PlayerState> {
|
pub fn get_player_state(&self, name: &str) -> Option<&PlayerState> {
|
||||||
self.0.iter()
|
self.0
|
||||||
|
.iter()
|
||||||
.filter(|state| state.player.get_name().eq(name))
|
.filter(|state| state.player.get_name().eq(name))
|
||||||
.nth(0)
|
.nth(0)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_player_state_mut(&mut self, name: &str) -> Option<&mut PlayerState> {
|
pub fn get_player_state_mut(&mut self, name: &str) -> Option<&mut PlayerState> {
|
||||||
self.0.iter_mut()
|
self.0
|
||||||
|
.iter_mut()
|
||||||
.filter(|state| state.player.get_name().eq(name))
|
.filter(|state| state.player.get_name().eq(name))
|
||||||
.nth(0)
|
.nth(0)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_tray(&self, name: &str) -> Option<&Tray> {
|
pub fn get_tray(&self, name: &str) -> Option<&Tray> {
|
||||||
let player = self.get_player_state(name)?;
|
let player = self.get_player_state(name)?;
|
||||||
|
|
||||||
Some(&player.tray)
|
Some(&player.tray)
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_tray_mut(&mut self, name: &str) -> Option<&mut Tray> {
|
pub fn get_tray_mut(&mut self, name: &str) -> Option<&mut Tray> {
|
||||||
let player = self.get_player_state_mut(name)?;
|
let player = self.get_player_state_mut(name)?;
|
||||||
|
|
||||||
Some(&mut player.tray)
|
Some(&mut player.tray)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Serialize, Debug, Clone)]
|
||||||
#[derive(Deserialize, Serialize, Tsify, Debug, Clone)]
|
|
||||||
#[tsify(from_wasm_abi)]
|
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
pub enum GameState {
|
pub enum GameState {
|
||||||
InProgress,
|
InProgress,
|
||||||
|
@ -137,7 +192,7 @@ pub enum GameState {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct Game{
|
pub struct Game {
|
||||||
pub tile_pool: Vec<Letter>,
|
pub tile_pool: Vec<Letter>,
|
||||||
rng: SmallRng,
|
rng: SmallRng,
|
||||||
board: Board,
|
board: Board,
|
||||||
|
@ -148,15 +203,20 @@ pub struct Game{
|
||||||
state: GameState,
|
state: GameState,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
impl Game {
|
impl Game {
|
||||||
pub fn new(seed: u64, dictionary_text: &str, player_names: Vec<String>, ai_difficulties: Vec<Difficulty>) -> Self {
|
pub fn new(
|
||||||
|
seed: u64,
|
||||||
|
dictionary_text: &str,
|
||||||
|
player_names: Vec<String>,
|
||||||
|
ai_difficulties: Vec<Difficulty>,
|
||||||
|
) -> Self {
|
||||||
let mut rng = SmallRng::seed_from_u64(seed);
|
let mut rng = SmallRng::seed_from_u64(seed);
|
||||||
let mut letters = standard_tile_pool(Some(&mut rng));
|
let mut letters = standard_tile_pool(Some(&mut rng));
|
||||||
|
|
||||||
let dictionary = DictionaryImpl::create_from_str(dictionary_text);
|
let dictionary = DictionaryImpl::create_from_str(dictionary_text);
|
||||||
|
|
||||||
let mut player_states: Vec<PlayerState> = player_names.iter()
|
let mut player_states: Vec<PlayerState> = player_names
|
||||||
|
.iter()
|
||||||
.map(|name| {
|
.map(|name| {
|
||||||
let mut tray = Tray::new(TRAY_LENGTH);
|
let mut tray = Tray::new(TRAY_LENGTH);
|
||||||
tray.fill(&mut letters);
|
tray.fill(&mut letters);
|
||||||
|
@ -174,7 +234,7 @@ impl Game {
|
||||||
let mut tray = Tray::new(TRAY_LENGTH);
|
let mut tray = Tray::new(TRAY_LENGTH);
|
||||||
tray.fill(&mut letters);
|
tray.fill(&mut letters);
|
||||||
let ai_player_name = if ai_length > 1 {
|
let ai_player_name = if ai_length > 1 {
|
||||||
format!("AI {}", i+1)
|
format!("AI {}", i + 1)
|
||||||
} else {
|
} else {
|
||||||
"AI".to_string()
|
"AI".to_string()
|
||||||
};
|
};
|
||||||
|
@ -204,15 +264,15 @@ impl Game {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_board(&self) -> &Board {
|
||||||
|
&self.board
|
||||||
pub fn get_board(&self) -> &Board {&self.board}
|
}
|
||||||
|
|
||||||
pub fn set_board(&mut self, new_board: Board) {
|
pub fn set_board(&mut self, new_board: Board) {
|
||||||
self.board = new_board;
|
self.board = new_board;
|
||||||
}
|
}
|
||||||
|
|
||||||
fn fill_trays(&mut self){
|
fn fill_trays(&mut self) {
|
||||||
for state in self.player_states.0.iter_mut() {
|
for state in self.player_states.0.iter_mut() {
|
||||||
let tray = &mut state.tray;
|
let tray = &mut state.tray;
|
||||||
tray.fill(&mut self.tile_pool);
|
tray.fill(&mut self.tile_pool);
|
||||||
|
@ -223,14 +283,18 @@ impl Game {
|
||||||
&self.dictionary
|
&self.dictionary
|
||||||
}
|
}
|
||||||
|
|
||||||
fn verify_game_in_progress(&self) -> Result<(), String> {
|
fn verify_game_in_progress(&self) -> Result<(), Error> {
|
||||||
if !matches!(self.state, GameState::InProgress) {
|
if !matches!(self.state, GameState::InProgress) {
|
||||||
return Err("Moves cannot be made after a game has finished".to_string());
|
return Err(Error::GameFinished);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn receive_play(&mut self, tray_tile_locations: Vec<Option<PlayedTile>>, commit_move: bool) -> Result<(TurnAction, GameState), String> {
|
pub fn receive_play(
|
||||||
|
&mut self,
|
||||||
|
tray_tile_locations: Vec<Option<PlayedTile>>,
|
||||||
|
commit_move: bool,
|
||||||
|
) -> Result<(TurnAction, GameState), Error> {
|
||||||
self.verify_game_in_progress()?;
|
self.verify_game_in_progress()?;
|
||||||
|
|
||||||
let player = self.current_player_name();
|
let player = self.current_player_name();
|
||||||
|
@ -238,7 +302,8 @@ impl Game {
|
||||||
let mut board_instance = self.get_board().clone();
|
let mut board_instance = self.get_board().clone();
|
||||||
let mut tray = self.player_states.get_tray(&player).unwrap().clone();
|
let mut tray = self.player_states.get_tray(&player).unwrap().clone();
|
||||||
|
|
||||||
let played_letters: Vec<(Letter, Coordinates)> = PlayedTile::convert_tray(&tray_tile_locations, &tray);
|
let played_letters: Vec<(Letter, Coordinates)> =
|
||||||
|
PlayedTile::convert_tray(&tray_tile_locations, &tray);
|
||||||
for (i, played_tile) in tray_tile_locations.iter().enumerate() {
|
for (i, played_tile) in tray_tile_locations.iter().enumerate() {
|
||||||
if played_tile.is_some() {
|
if played_tile.is_some() {
|
||||||
*tray.letters.get_mut(i).unwrap() = None;
|
*tray.letters.get_mut(i).unwrap() = None;
|
||||||
|
@ -247,19 +312,16 @@ impl Game {
|
||||||
|
|
||||||
board_instance.receive_play(played_letters)?;
|
board_instance.receive_play(played_letters)?;
|
||||||
|
|
||||||
|
|
||||||
let x = board_instance.calculate_scores(self.get_dictionary())?;
|
let x = board_instance.calculate_scores(self.get_dictionary())?;
|
||||||
let total_score = x.1;
|
let total_score = x.1;
|
||||||
|
|
||||||
let words: Vec<WordResult> = x.0.iter()
|
let words: Vec<WordResult> =
|
||||||
.map(|(word, score)| {
|
x.0.iter()
|
||||||
WordResult {
|
.map(|(word, score)| WordResult {
|
||||||
word: word.to_string(),
|
word: word.to_string(),
|
||||||
score: *score
|
score: *score,
|
||||||
}
|
})
|
||||||
|
.collect();
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
if commit_move {
|
if commit_move {
|
||||||
let player_state = self.player_states.get_player_state_mut(&player).unwrap();
|
let player_state = self.player_states.get_player_state_mut(&player).unwrap();
|
||||||
|
@ -276,28 +338,42 @@ impl Game {
|
||||||
// game is over
|
// game is over
|
||||||
self.end_game(Some(player));
|
self.end_game(Some(player));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok((TurnAction::PlayTiles {
|
let locations = tray_tile_locations
|
||||||
result: ScoreResult {
|
.iter()
|
||||||
words,
|
.filter_map(|x| x.as_ref())
|
||||||
total: total_score,
|
.map(|x| x.index)
|
||||||
|
.collect::<Vec<usize>>();
|
||||||
|
|
||||||
|
Ok((
|
||||||
|
TurnAction::PlayTiles {
|
||||||
|
result: ScoreResult {
|
||||||
|
words,
|
||||||
|
total: total_score,
|
||||||
|
},
|
||||||
|
locations,
|
||||||
},
|
},
|
||||||
}, self.state.clone()))
|
self.get_state(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn exchange_tiles(&mut self, tray_tile_locations: Vec<bool>) -> Result<(Tray, TurnAction, GameState), String> {
|
pub fn exchange_tiles(
|
||||||
|
&mut self,
|
||||||
|
tray_tile_locations: Vec<bool>,
|
||||||
|
) -> Result<(Tray, TurnAction, GameState), Error> {
|
||||||
self.verify_game_in_progress()?;
|
self.verify_game_in_progress()?;
|
||||||
|
|
||||||
let player = self.current_player_name();
|
let player = self.current_player_name();
|
||||||
let tray = match self.player_states.get_tray_mut(&player) {
|
let tray = match self.player_states.get_tray_mut(&player) {
|
||||||
None => {return Err(format!("Player {} not found", player))}
|
None => return Err(Error::InvalidPlayer(player)),
|
||||||
Some(x) => {x}
|
Some(x) => x,
|
||||||
};
|
};
|
||||||
|
|
||||||
if tray.letters.len() != tray_tile_locations.len() {
|
if tray.letters.len() != tray_tile_locations.len() {
|
||||||
return Err("Incoming tray and existing tray have different lengths".to_string());
|
return Err(Error::Other(
|
||||||
|
"Incoming tray and existing tray have different lengths".to_string(),
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let tile_pool = &mut self.tile_pool;
|
let tile_pool = &mut self.tile_pool;
|
||||||
|
@ -321,8 +397,11 @@ impl Game {
|
||||||
|
|
||||||
let state = self.increment_turn(false);
|
let state = self.increment_turn(false);
|
||||||
|
|
||||||
Ok((tray, TurnAction::ExchangeTiles { tiles_exchanged }, state.clone()))
|
Ok((
|
||||||
|
tray,
|
||||||
|
TurnAction::ExchangeTiles { tiles_exchanged },
|
||||||
|
state.clone(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn add_word(&mut self, word: String) {
|
pub fn add_word(&mut self, word: String) {
|
||||||
|
@ -331,21 +410,20 @@ impl Game {
|
||||||
self.dictionary.insert(word, -1.0);
|
self.dictionary.insert(word, -1.0);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pass(&mut self) -> Result<GameState, String> {
|
pub fn pass(&mut self) -> Result<GameState, Error> {
|
||||||
self.verify_game_in_progress()?;
|
self.verify_game_in_progress()?;
|
||||||
Ok(self.increment_turn(false).clone())
|
Ok(self.increment_turn(false).clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn increment_turn(&mut self, played: bool) -> &GameState{
|
fn increment_turn(&mut self, played: bool) -> &GameState {
|
||||||
self.turn_order += 1;
|
self.turn_order += 1;
|
||||||
if !played {
|
if !played {
|
||||||
self.turns_not_played += 1;
|
self.turns_not_played += 1;
|
||||||
|
|
||||||
// check if game has ended due to passing
|
// check if game has ended due to passing
|
||||||
if self.turns_not_played >= 2*self.player_states.0.len() {
|
if self.turns_not_played >= 2 * self.player_states.0.len() {
|
||||||
self.end_game(None);
|
self.end_game(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
self.turns_not_played = 0;
|
self.turns_not_played = 0;
|
||||||
}
|
}
|
||||||
|
@ -354,7 +432,6 @@ impl Game {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn end_game(&mut self, finisher: Option<String>) {
|
fn end_game(&mut self, finisher: Option<String>) {
|
||||||
|
|
||||||
let mut finished_letters_map = HashMap::new();
|
let mut finished_letters_map = HashMap::new();
|
||||||
let mut points_forfeit = 0;
|
let mut points_forfeit = 0;
|
||||||
|
|
||||||
|
@ -376,25 +453,30 @@ impl Game {
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(finisher) = &finisher {
|
if let Some(finisher) = &finisher {
|
||||||
let mut state = self.player_states.get_player_state_mut(finisher).unwrap();
|
let state = self.player_states.get_player_state_mut(finisher).unwrap();
|
||||||
state.score += points_forfeit;
|
state.score += points_forfeit;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.state = GameState::Ended {
|
self.state = GameState::Ended {
|
||||||
finisher,
|
finisher,
|
||||||
remaining_tiles: finished_letters_map
|
remaining_tiles: finished_letters_map,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn current_player_name(&self) -> String {
|
pub fn current_player_name(&self) -> String {
|
||||||
self.player_states.get_player_name_by_turn_id(self.turn_order).to_string()
|
self.player_states
|
||||||
|
.get_player_name_by_turn_id(self.turn_order)
|
||||||
|
.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn advance_turn(&mut self) -> Result<(TurnAdvanceResult, GameState), String> {
|
pub fn advance_turn(&mut self) -> Result<(TurnAdvanceResult, GameState), Error> {
|
||||||
let current_player = self.current_player_name();
|
let current_player = self.current_player_name();
|
||||||
let state = self.player_states.get_player_state_mut(¤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 {
|
if let Player::AI { object, .. } = &mut state.player {
|
||||||
let tray = &mut state.tray;
|
let tray = &mut state.tray;
|
||||||
|
|
||||||
let best_move = object.find_best_move(tray, &self.board, &mut self.rng);
|
let best_move = object.find_best_move(tray, &self.board, &mut self.rng);
|
||||||
|
@ -407,43 +489,53 @@ impl Game {
|
||||||
match tile_spot {
|
match tile_spot {
|
||||||
None => {
|
None => {
|
||||||
to_exchange.push(false);
|
to_exchange.push(false);
|
||||||
},
|
}
|
||||||
Some(_) => {
|
Some(_) => {
|
||||||
to_exchange.push(true);
|
to_exchange.push(true);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.tile_pool.is_empty(){
|
if self.tile_pool.is_empty() {
|
||||||
let game_state = self.increment_turn(false);
|
let game_state = self.increment_turn(false);
|
||||||
Ok((TurnAdvanceResult::AIMove {
|
Ok((
|
||||||
name: current_player,
|
TurnAdvanceResult::AIMove {
|
||||||
action: TurnAction::Pass,
|
name: current_player,
|
||||||
}, game_state.clone()))
|
action: TurnAction::Pass,
|
||||||
|
},
|
||||||
|
game_state.clone(),
|
||||||
|
))
|
||||||
} else {
|
} else {
|
||||||
let (_, action, game_state) = self.exchange_tiles(to_exchange)?;
|
let (_, action, game_state) = self.exchange_tiles(to_exchange)?;
|
||||||
Ok((TurnAdvanceResult::AIMove {
|
Ok((
|
||||||
name: current_player,
|
TurnAdvanceResult::AIMove {
|
||||||
action,
|
name: current_player,
|
||||||
}, game_state))
|
action,
|
||||||
|
},
|
||||||
|
game_state,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
Some(best_move) => {
|
Some(best_move) => {
|
||||||
let play = best_move.convert_to_play(tray);
|
let play = best_move.convert_to_play(tray);
|
||||||
let (action, game_state) = self.receive_play(play, true)?;
|
let (action, game_state) = self.receive_play(play, true)?;
|
||||||
Ok((TurnAdvanceResult::AIMove {
|
Ok((
|
||||||
name: current_player,
|
TurnAdvanceResult::AIMove {
|
||||||
action,
|
name: current_player,
|
||||||
}, game_state))
|
action,
|
||||||
|
},
|
||||||
|
game_state,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Ok((TurnAdvanceResult::HumanInputRequired{name: self.current_player_name()}, self.state.clone()))
|
Ok((
|
||||||
|
TurnAdvanceResult::HumanInputRequired {
|
||||||
|
name: self.current_player_name(),
|
||||||
|
},
|
||||||
|
self.get_state(),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_remaining_tiles(&self) -> usize {
|
pub fn get_remaining_tiles(&self) -> usize {
|
||||||
|
@ -452,60 +544,61 @@ impl Game {
|
||||||
|
|
||||||
pub fn get_player_tile_count(&self, player: &str) -> Result<usize, String> {
|
pub fn get_player_tile_count(&self, player: &str) -> Result<usize, String> {
|
||||||
let tray = match self.player_states.get_tray(&player) {
|
let tray = match self.player_states.get_tray(&player) {
|
||||||
None => {return Err(format!("Player {} not found", player))}
|
None => return Err(format!("Player {} not found", player)),
|
||||||
Some(x) => {x}
|
Some(x) => x,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(
|
Ok(tray.count())
|
||||||
tray.count()
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_state(&self) -> GameState {
|
||||||
|
self.state.clone()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Tsify, Debug)]
|
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||||
#[tsify(from_wasm_abi)]
|
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
pub enum TurnAction {
|
pub enum TurnAction {
|
||||||
Pass,
|
Pass,
|
||||||
ExchangeTiles{
|
ExchangeTiles {
|
||||||
tiles_exchanged: usize
|
tiles_exchanged: usize,
|
||||||
},
|
},
|
||||||
PlayTiles{
|
PlayTiles {
|
||||||
result: ScoreResult
|
result: ScoreResult,
|
||||||
|
locations: Vec<usize>,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Tsify, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
#[tsify(from_wasm_abi)]
|
|
||||||
#[serde(tag = "type")]
|
#[serde(tag = "type")]
|
||||||
pub enum TurnAdvanceResult {
|
pub enum TurnAdvanceResult {
|
||||||
HumanInputRequired{
|
HumanInputRequired { name: String },
|
||||||
name: String
|
AIMove { name: String, action: TurnAction },
|
||||||
},
|
|
||||||
AIMove{
|
|
||||||
name: String,
|
|
||||||
action: TurnAction,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
|
||||||
use std::fs;
|
|
||||||
use crate::game::Game;
|
use crate::game::Game;
|
||||||
use crate::player_interaction::ai::Difficulty;
|
use crate::player_interaction::ai::Difficulty;
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_game() {
|
fn test_game() {
|
||||||
let seed = 124;
|
let seed = 124;
|
||||||
|
|
||||||
let dictionary_path = "resources/dictionary.csv";
|
let dictionary_path = "../resources/dictionary.csv";
|
||||||
let dictionary_string = fs::read_to_string(dictionary_path).unwrap();
|
let dictionary_string = fs::read_to_string(dictionary_path).unwrap();
|
||||||
|
|
||||||
let mut game = Game::new(seed, &dictionary_string, vec!["Player".to_string()], vec![Difficulty{proportion: 0.5, randomness: 0.0}]);
|
let mut game = Game::new(
|
||||||
|
seed,
|
||||||
|
&dictionary_string,
|
||||||
|
vec!["Player".to_string()],
|
||||||
|
vec![Difficulty {
|
||||||
|
proportion: 0.5,
|
||||||
|
randomness: 0.0,
|
||||||
|
}],
|
||||||
|
);
|
||||||
|
|
||||||
let current_player = game.current_player_name();
|
let current_player = game.current_player_name();
|
||||||
println!("Current player is {current_player}");
|
println!("Current player is {current_player}");
|
||||||
|
@ -522,7 +615,5 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(game.current_player_name(), "Player");
|
assert_eq!(game.current_player_name(), "Player");
|
||||||
assert_eq!(0, game.turns_not_played);
|
assert_eq!(0, game.turns_not_played);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
6
wordgrid/src/lib.rs
Normal file
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 crate::board::Letter;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
pub mod ai;
|
pub mod ai;
|
||||||
|
|
||||||
#[derive(Debug, Serialize, Deserialize, Tsify, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
#[tsify(from_wasm_abi)]
|
|
||||||
pub struct Tray {
|
pub struct Tray {
|
||||||
pub letters: Vec<Option<Letter>>
|
pub letters: Vec<Option<Letter>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Tray {
|
impl Tray {
|
||||||
|
@ -17,9 +14,7 @@ impl Tray {
|
||||||
for _ in 0..tray_length {
|
for _ in 0..tray_length {
|
||||||
letters.push(None);
|
letters.push(None);
|
||||||
}
|
}
|
||||||
Tray {
|
Tray { letters }
|
||||||
letters
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn fill(&mut self, standard_tile_pool: &mut Vec<Letter>) {
|
pub fn fill(&mut self, standard_tile_pool: &mut Vec<Letter>) {
|
||||||
|
@ -37,25 +32,20 @@ impl Tray {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn count(&self) -> usize {
|
pub fn count(&self) -> usize {
|
||||||
self.letters.iter()
|
self.letters.iter().filter(|l| l.is_some()).count()
|
||||||
.filter(|l| l.is_some())
|
|
||||||
.count()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_tray() {
|
fn test_tray() {
|
||||||
let mut letters = vec![
|
let mut letters = vec![
|
||||||
Letter::new(Some('E'), 3),
|
Letter::new(Some('E'), 3),
|
||||||
Letter::new(Some('O'), 2),
|
Letter::new(Some('O'), 2),
|
||||||
Letter::new(Some('J'), 1)
|
Letter::new(Some('J'), 1),
|
||||||
];
|
];
|
||||||
|
|
||||||
let mut tray = Tray::new(5);
|
let mut tray = Tray::new(5);
|
||||||
|
@ -89,8 +79,5 @@ mod tests {
|
||||||
assert_eq!(tray.letters.get(2).unwrap().unwrap().text, 'E');
|
assert_eq!(tray.letters.get(2).unwrap().unwrap().text, 'E');
|
||||||
assert!(tray.letters.get(3).unwrap().is_none());
|
assert!(tray.letters.get(3).unwrap().is_none());
|
||||||
assert!(tray.letters.get(4).unwrap().is_none());
|
assert!(tray.letters.get(4).unwrap().is_none());
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
|
@ -1,14 +1,16 @@
|
||||||
use std::collections::{HashMap, HashSet};
|
|
||||||
use rand::Rng;
|
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use tsify::Tsify;
|
|
||||||
use crate::board::{Board, CellType, Coordinates, Direction, Letter};
|
use crate::board::{Board, CellType, Coordinates, Direction, Letter};
|
||||||
use crate::constants::GRID_LENGTH;
|
use crate::constants::GRID_LENGTH;
|
||||||
use crate::dictionary::DictionaryImpl;
|
use crate::dictionary::DictionaryImpl;
|
||||||
use crate::game::PlayedTile;
|
use crate::game::PlayedTile;
|
||||||
use crate::player_interaction::Tray;
|
use crate::player_interaction::Tray;
|
||||||
|
use rand::Rng;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::{HashMap, HashSet};
|
||||||
|
|
||||||
const ALPHABET: [char; 26] = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'];
|
const ALPHABET: [char; 26] = [
|
||||||
|
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
|
||||||
|
'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
|
||||||
|
];
|
||||||
|
|
||||||
struct CoordinateLineMapper {
|
struct CoordinateLineMapper {
|
||||||
direction: Direction,
|
direction: Direction,
|
||||||
|
@ -18,18 +20,13 @@ struct CoordinateLineMapper {
|
||||||
impl CoordinateLineMapper {
|
impl CoordinateLineMapper {
|
||||||
fn line_to_coord(&self, variable: u8) -> Coordinates {
|
fn line_to_coord(&self, variable: u8) -> Coordinates {
|
||||||
match self.direction {
|
match self.direction {
|
||||||
Direction::Row => {
|
Direction::Row => Coordinates(variable, self.fixed),
|
||||||
Coordinates(variable, self.fixed)
|
Direction::Column => Coordinates(self.fixed, variable),
|
||||||
}
|
|
||||||
Direction::Column => {
|
|
||||||
Coordinates(self.fixed, variable)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Serialize, Deserialize, Tsify)]
|
#[derive(Copy, Clone, Serialize, Deserialize)]
|
||||||
#[tsify(from_wasm_abi)]
|
|
||||||
pub struct Difficulty {
|
pub struct Difficulty {
|
||||||
pub proportion: f64,
|
pub proportion: f64,
|
||||||
pub randomness: f64,
|
pub randomness: f64,
|
||||||
|
@ -48,7 +45,6 @@ pub struct AI {
|
||||||
type MoveMap = HashMap<char, u32>;
|
type MoveMap = HashMap<char, u32>;
|
||||||
type CrossTiles = Vec<Option<MoveMap>>;
|
type CrossTiles = Vec<Option<MoveMap>>;
|
||||||
|
|
||||||
|
|
||||||
#[derive(Debug, Eq, PartialEq, Hash)]
|
#[derive(Debug, Eq, PartialEq, Hash)]
|
||||||
pub struct CompleteMove {
|
pub struct CompleteMove {
|
||||||
moves: Vec<MoveComponent>,
|
moves: Vec<MoveComponent>,
|
||||||
|
@ -56,10 +52,11 @@ pub struct CompleteMove {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CompleteMove {
|
impl CompleteMove {
|
||||||
|
|
||||||
pub fn convert_to_play(&self, tray: &Tray) -> Vec<Option<PlayedTile>> {
|
pub fn convert_to_play(&self, tray: &Tray) -> Vec<Option<PlayedTile>> {
|
||||||
let mut played_tiles = Vec::with_capacity(tray.letters.len());
|
let mut played_tiles = Vec::with_capacity(tray.letters.len());
|
||||||
let mut moves = self.moves.iter()
|
let mut moves = self
|
||||||
|
.moves
|
||||||
|
.iter()
|
||||||
.map(|m| Some(m.clone()))
|
.map(|m| Some(m.clone()))
|
||||||
.collect::<Vec<Option<MoveComponent>>>();
|
.collect::<Vec<Option<MoveComponent>>>();
|
||||||
|
|
||||||
|
@ -88,7 +85,7 @@ impl CompleteMove {
|
||||||
found_match = true;
|
found_match = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
None => {}
|
None => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -127,29 +124,28 @@ struct MoveComponent {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AI {
|
impl AI {
|
||||||
|
pub fn new(difficulty: Difficulty, dictionary: &DictionaryImpl) -> Self {
|
||||||
pub fn new(difficulty: Difficulty, dictionary: &DictionaryImpl) -> Self{
|
|
||||||
let mut ai_dictionary = HashSet::new();
|
let mut ai_dictionary = HashSet::new();
|
||||||
let mut substrings = HashSet::new();
|
let mut substrings = HashSet::new();
|
||||||
|
|
||||||
for (word, score) in dictionary.iter() {
|
for (word, score) in dictionary.iter() {
|
||||||
if *score >= difficulty.proportion { // TODO - may need to reverse
|
if *score >= difficulty.proportion {
|
||||||
|
// TODO - may need to reverse
|
||||||
|
|
||||||
ai_dictionary.insert(word.clone());
|
ai_dictionary.insert(word.clone());
|
||||||
substrings.insert(word.clone());
|
substrings.insert(word.clone());
|
||||||
|
|
||||||
for i in 0..word.len() {
|
for i in 0..word.len() {
|
||||||
for j in i+1..word.len() {
|
for j in i + 1..word.len() {
|
||||||
substrings.insert(word[i..j].to_string());
|
substrings.insert(word[i..j].to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let board_view = Board::new();
|
let board_view = Board::new();
|
||||||
let mut column_cross_tiles = CrossTiles::with_capacity((GRID_LENGTH * GRID_LENGTH) as usize);
|
let mut column_cross_tiles =
|
||||||
|
CrossTiles::with_capacity((GRID_LENGTH * GRID_LENGTH) as usize);
|
||||||
let mut row_cross_tiles = CrossTiles::with_capacity((GRID_LENGTH * GRID_LENGTH) as usize);
|
let mut row_cross_tiles = CrossTiles::with_capacity((GRID_LENGTH * GRID_LENGTH) as usize);
|
||||||
|
|
||||||
board_view.cells.iter().for_each(|_| {
|
board_view.cells.iter().for_each(|_| {
|
||||||
|
@ -168,15 +164,20 @@ impl AI {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn find_best_move<R: Rng>(&mut self, tray: &Tray, grid: &Board, rng: &mut R) -> Option<CompleteMove>{
|
pub fn find_best_move<R: Rng>(
|
||||||
|
&mut self,
|
||||||
|
tray: &Tray,
|
||||||
|
grid: &Board,
|
||||||
|
rng: &mut R,
|
||||||
|
) -> Option<CompleteMove> {
|
||||||
let move_set = self.find_all_moves(tray, grid);
|
let move_set = self.find_all_moves(tray, grid);
|
||||||
|
|
||||||
let mut best_move: Option<(CompleteMove, f64)> = None;
|
let mut best_move: Option<(CompleteMove, f64)> = None;
|
||||||
|
|
||||||
for possible_move in move_set {
|
for possible_move in move_set {
|
||||||
let move_score =
|
let move_score = if self.difficulty.randomness > 0.0 {
|
||||||
if self.difficulty.randomness > 0.0 {
|
(1.0 - self.difficulty.randomness) * (possible_move.score as f64)
|
||||||
(1.0 - self.difficulty.randomness) * (possible_move.score as f64) + self.difficulty.randomness * rng.gen_range(0.0..1.0)
|
+ self.difficulty.randomness * rng.gen_range(0.0..1.0)
|
||||||
} else {
|
} else {
|
||||||
possible_move.score as f64
|
possible_move.score as f64
|
||||||
};
|
};
|
||||||
|
@ -194,10 +195,9 @@ impl AI {
|
||||||
}
|
}
|
||||||
|
|
||||||
return match best_move {
|
return match best_move {
|
||||||
None => {None}
|
None => None,
|
||||||
Some((best_move, _)) => {Some(best_move)}
|
Some((best_move, _)) => Some(best_move),
|
||||||
};
|
};
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn find_all_moves(&mut self, tray: &Tray, grid: &Board) -> HashSet<CompleteMove> {
|
fn find_all_moves(&mut self, tray: &Tray, grid: &Board) -> HashSet<CompleteMove> {
|
||||||
|
@ -215,15 +215,17 @@ impl AI {
|
||||||
&self,
|
&self,
|
||||||
tray: &Tray,
|
tray: &Tray,
|
||||||
direction: Direction,
|
direction: Direction,
|
||||||
all_moves: &mut HashSet<CompleteMove>) {
|
all_moves: &mut HashSet<CompleteMove>,
|
||||||
|
) {
|
||||||
// If you're building a word in one direction, you need to form valid words in the cross-direction
|
// If you're building a word in one direction, you need to form valid words in the cross-direction
|
||||||
let cross_tiles = match direction {
|
let cross_tiles = match direction {
|
||||||
Direction::Column => {&self.row_cross_tiles}
|
Direction::Column => &self.row_cross_tiles,
|
||||||
Direction::Row => {&self.column_cross_tiles}
|
Direction::Row => &self.column_cross_tiles,
|
||||||
};
|
};
|
||||||
|
|
||||||
let tray_letters = tray.letters.iter()
|
let tray_letters = tray
|
||||||
|
.letters
|
||||||
|
.iter()
|
||||||
.filter(|letter| letter.is_some())
|
.filter(|letter| letter.is_some())
|
||||||
.map(|letter| letter.unwrap())
|
.map(|letter| letter.unwrap())
|
||||||
.collect::<Vec<Letter>>();
|
.collect::<Vec<Letter>>();
|
||||||
|
@ -234,7 +236,7 @@ impl AI {
|
||||||
|
|
||||||
let coord_mapper = CoordinateLineMapper {
|
let coord_mapper = CoordinateLineMapper {
|
||||||
direction,
|
direction,
|
||||||
fixed: k
|
fixed: k,
|
||||||
};
|
};
|
||||||
|
|
||||||
for p in 0..GRID_LENGTH {
|
for p in 0..GRID_LENGTH {
|
||||||
|
@ -246,13 +248,12 @@ impl AI {
|
||||||
for l in 0..GRID_LENGTH {
|
for l in 0..GRID_LENGTH {
|
||||||
let coords = coord_mapper.line_to_coord(l);
|
let coords = coord_mapper.line_to_coord(l);
|
||||||
|
|
||||||
let is_anchored = check_if_anchored(&self.board_view, coords, Direction::Row) ||
|
let is_anchored = check_if_anchored(&self.board_view, coords, Direction::Row)
|
||||||
check_if_anchored(&self.board_view, coords, Direction::Column);
|
|| check_if_anchored(&self.board_view, coords, Direction::Column);
|
||||||
|
|
||||||
if is_anchored &&
|
if is_anchored && line_letters.get(l as usize).unwrap().is_none()
|
||||||
line_letters.get(l as usize).unwrap().is_none() // it's duplicate work to check here when we'll already check at either free side
|
// it's duplicate work to check here when we'll already check at either free side
|
||||||
{
|
{
|
||||||
|
|
||||||
self.evaluate_spot_heading_left(
|
self.evaluate_spot_heading_left(
|
||||||
&line_letters,
|
&line_letters,
|
||||||
&line_cross_letters,
|
&line_cross_letters,
|
||||||
|
@ -262,17 +263,11 @@ impl AI {
|
||||||
&Vec::new(),
|
&Vec::new(),
|
||||||
1,
|
1,
|
||||||
&MoveScoring::new(),
|
&MoveScoring::new(),
|
||||||
|
all_moves,
|
||||||
all_moves
|
);
|
||||||
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn evaluate_spot_heading_left(
|
fn evaluate_spot_heading_left(
|
||||||
|
@ -287,10 +282,8 @@ impl AI {
|
||||||
min_length: usize,
|
min_length: usize,
|
||||||
current_points: &MoveScoring,
|
current_points: &MoveScoring,
|
||||||
|
|
||||||
all_moves: &mut HashSet<CompleteMove>
|
all_moves: &mut HashSet<CompleteMove>,
|
||||||
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
if line_index < 0 || line_index >= GRID_LENGTH as i8 {
|
if line_index < 0 || line_index >= GRID_LENGTH as i8 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -300,10 +293,13 @@ impl AI {
|
||||||
Some(_) => {
|
Some(_) => {
|
||||||
// there's a letter here; need to take a step left if we can
|
// there's a letter here; need to take a step left if we can
|
||||||
|
|
||||||
if !(line_index >= 1 &&
|
if !(line_index >= 1
|
||||||
line_letters.get((line_index-1) as usize).unwrap().is_some() &&
|
&& line_letters
|
||||||
min_length == 1
|
.get((line_index - 1) as usize)
|
||||||
) {
|
.unwrap()
|
||||||
|
.is_some()
|
||||||
|
&& min_length == 1)
|
||||||
|
{
|
||||||
// if-statement is basically saying that if we're at the start of the process (min_length==1) and there's a word still to our left,
|
// if-statement is basically saying that if we're at the start of the process (min_length==1) and there's a word still to our left,
|
||||||
// just stop. Other versions of the for-loops that call this function will have picked up that case.
|
// just stop. Other versions of the for-loops that call this function will have picked up that case.
|
||||||
self.evaluate_spot_heading_left(
|
self.evaluate_spot_heading_left(
|
||||||
|
@ -315,9 +311,8 @@ impl AI {
|
||||||
current_play,
|
current_play,
|
||||||
min_length,
|
min_length,
|
||||||
current_points,
|
current_points,
|
||||||
all_moves
|
all_moves,
|
||||||
);
|
);
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
@ -331,10 +326,9 @@ impl AI {
|
||||||
min_length,
|
min_length,
|
||||||
current_points,
|
current_points,
|
||||||
coord_mapper,
|
coord_mapper,
|
||||||
|
|
||||||
&available_letters,
|
&available_letters,
|
||||||
letter.clone(),
|
letter.clone(),
|
||||||
all_moves
|
all_moves,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -348,12 +342,10 @@ impl AI {
|
||||||
current_play,
|
current_play,
|
||||||
min_length + 1,
|
min_length + 1,
|
||||||
current_points,
|
current_points,
|
||||||
all_moves
|
all_moves,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn evaluate_letter_at_spot(
|
fn evaluate_letter_at_spot(
|
||||||
|
@ -366,13 +358,10 @@ impl AI {
|
||||||
current_points: &MoveScoring,
|
current_points: &MoveScoring,
|
||||||
coord_mapper: &CoordinateLineMapper,
|
coord_mapper: &CoordinateLineMapper,
|
||||||
|
|
||||||
|
|
||||||
available_letters: &Vec<Letter>,
|
available_letters: &Vec<Letter>,
|
||||||
letter: Letter,
|
letter: Letter,
|
||||||
all_moves: &mut HashSet<CompleteMove>
|
all_moves: &mut HashSet<CompleteMove>,
|
||||||
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
if letter.is_blank {
|
if letter.is_blank {
|
||||||
// need to loop through alphabet
|
// need to loop through alphabet
|
||||||
for alpha in ALPHABET {
|
for alpha in ALPHABET {
|
||||||
|
@ -389,8 +378,7 @@ impl AI {
|
||||||
coord_mapper,
|
coord_mapper,
|
||||||
available_letters,
|
available_letters,
|
||||||
letter,
|
letter,
|
||||||
all_moves
|
all_moves,
|
||||||
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -404,10 +392,9 @@ impl AI {
|
||||||
coord_mapper,
|
coord_mapper,
|
||||||
available_letters,
|
available_letters,
|
||||||
letter.clone(),
|
letter.clone(),
|
||||||
all_moves
|
all_moves,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn evaluate_non_blank_letter_at_spot(
|
fn evaluate_non_blank_letter_at_spot(
|
||||||
|
@ -419,15 +406,12 @@ impl AI {
|
||||||
min_length: usize,
|
min_length: usize,
|
||||||
current_points: &MoveScoring,
|
current_points: &MoveScoring,
|
||||||
|
|
||||||
|
|
||||||
coord_mapper: &CoordinateLineMapper,
|
coord_mapper: &CoordinateLineMapper,
|
||||||
available_letters: &Vec<Letter>,
|
available_letters: &Vec<Letter>,
|
||||||
letter: Letter,
|
letter: Letter,
|
||||||
|
|
||||||
all_moves: &mut HashSet<CompleteMove>,
|
all_moves: &mut HashSet<CompleteMove>,
|
||||||
|
|
||||||
) {
|
) {
|
||||||
|
|
||||||
// let's now assign the letter to this spot
|
// let's now assign the letter to this spot
|
||||||
let mut line_letters = line_letters.clone();
|
let mut line_letters = line_letters.clone();
|
||||||
*line_letters.get_mut(line_index as usize).unwrap() = Some(letter);
|
*line_letters.get_mut(line_index as usize).unwrap() = Some(letter);
|
||||||
|
@ -458,7 +442,11 @@ impl AI {
|
||||||
|
|
||||||
// making copy
|
// making copy
|
||||||
let mut current_points = current_points.clone();
|
let mut current_points = current_points.clone();
|
||||||
let cell_type = self.board_view.get_cell(coord_mapper.line_to_coord(line_index as u8)).unwrap().cell_type;
|
let cell_type = self
|
||||||
|
.board_view
|
||||||
|
.get_cell(coord_mapper.line_to_coord(line_index as u8))
|
||||||
|
.unwrap()
|
||||||
|
.cell_type;
|
||||||
|
|
||||||
// first we score cross letters
|
// first we score cross letters
|
||||||
if let Some(map) = cross_letters {
|
if let Some(map) = cross_letters {
|
||||||
|
@ -508,7 +496,8 @@ impl AI {
|
||||||
current_points.main_scoring += letter.points * 2;
|
current_points.main_scoring += letter.points * 2;
|
||||||
}
|
}
|
||||||
CellType::TripleLetter => {
|
CellType::TripleLetter => {
|
||||||
current_points.main_scoring += letter.points * 3;}
|
current_points.main_scoring += letter.points * 3;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// finally, while we know that we're in a valid substring we should check if this is a valid word or not
|
// finally, while we know that we're in a valid substring we should check if this is a valid word or not
|
||||||
|
@ -516,7 +505,8 @@ impl AI {
|
||||||
if word.len() >= min_length && self.dictionary.contains(&word) {
|
if word.len() >= min_length && self.dictionary.contains(&word) {
|
||||||
let new_move = CompleteMove {
|
let new_move = CompleteMove {
|
||||||
moves: current_play.clone(),
|
moves: current_play.clone(),
|
||||||
score: current_points.cross_scoring + current_points.main_scoring*current_points.multiplier,
|
score: current_points.cross_scoring
|
||||||
|
+ current_points.main_scoring * current_points.multiplier,
|
||||||
};
|
};
|
||||||
all_moves.insert(new_move);
|
all_moves.insert(new_move);
|
||||||
}
|
}
|
||||||
|
@ -525,29 +515,26 @@ impl AI {
|
||||||
let mut new_available_letters = Vec::with_capacity(available_letters.len() - 1);
|
let mut new_available_letters = Vec::with_capacity(available_letters.len() - 1);
|
||||||
let mut skipped_one = false;
|
let mut skipped_one = false;
|
||||||
|
|
||||||
available_letters.iter()
|
available_letters.iter().for_each(|in_tray| {
|
||||||
.for_each(|in_tray| {
|
if skipped_one || !in_tray.partial_match(&letter) {
|
||||||
if skipped_one || !in_tray.partial_match(&letter) {
|
new_available_letters.push(*in_tray);
|
||||||
new_available_letters.push(*in_tray);
|
} else {
|
||||||
} else {
|
skipped_one = true;
|
||||||
skipped_one = true;
|
}
|
||||||
}
|
});
|
||||||
});
|
|
||||||
|
|
||||||
assert!(skipped_one); // at least one should have been removed
|
assert!(skipped_one); // at least one should have been removed
|
||||||
|
|
||||||
self.evaluate_spot_heading_right(
|
self.evaluate_spot_heading_right(
|
||||||
&line_letters,
|
&line_letters,
|
||||||
line_cross_letters,
|
line_cross_letters,
|
||||||
line_index+1,
|
line_index + 1,
|
||||||
current_play,
|
current_play,
|
||||||
min_length,
|
min_length,
|
||||||
¤t_points,
|
¤t_points,
|
||||||
|
|
||||||
coord_mapper,
|
coord_mapper,
|
||||||
&new_available_letters,
|
&new_available_letters,
|
||||||
|
all_moves,
|
||||||
all_moves
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -560,13 +547,11 @@ impl AI {
|
||||||
min_length: usize,
|
min_length: usize,
|
||||||
current_points: &MoveScoring,
|
current_points: &MoveScoring,
|
||||||
|
|
||||||
|
|
||||||
coord_mapper: &CoordinateLineMapper,
|
coord_mapper: &CoordinateLineMapper,
|
||||||
available_letters: &Vec<Letter>,
|
available_letters: &Vec<Letter>,
|
||||||
|
|
||||||
all_moves: &mut HashSet<CompleteMove>,
|
all_moves: &mut HashSet<CompleteMove>,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
// out-of-bounds check
|
// out-of-bounds check
|
||||||
if line_index < 0 || line_index >= GRID_LENGTH as i8 {
|
if line_index < 0 || line_index >= GRID_LENGTH as i8 {
|
||||||
return;
|
return;
|
||||||
|
@ -586,11 +571,11 @@ impl AI {
|
||||||
¤t_points,
|
¤t_points,
|
||||||
coord_mapper,
|
coord_mapper,
|
||||||
available_letters,
|
available_letters,
|
||||||
all_moves
|
all_moves,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
// there's a blank spot, so let's loop through every available letter and evaluate at this spot
|
// there's a blank spot, so let's loop through every available letter and evaluate at this spot
|
||||||
for letter in available_letters {
|
for letter in available_letters {
|
||||||
self.evaluate_letter_at_spot(
|
self.evaluate_letter_at_spot(
|
||||||
line_letters,
|
line_letters,
|
||||||
|
@ -601,10 +586,8 @@ impl AI {
|
||||||
current_points,
|
current_points,
|
||||||
coord_mapper,
|
coord_mapper,
|
||||||
available_letters,
|
available_letters,
|
||||||
|
|
||||||
letter.clone(),
|
letter.clone(),
|
||||||
all_moves
|
all_moves,
|
||||||
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -625,25 +608,29 @@ impl AI {
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut check_for_valid_moves = |coords: Coordinates| {
|
let mut check_for_valid_moves = |coords: Coordinates| {
|
||||||
|
|
||||||
let cell = new.get_cell(coords).unwrap();
|
let cell = new.get_cell(coords).unwrap();
|
||||||
if cell.value.is_none() {
|
if cell.value.is_none() {
|
||||||
let valid_row_moves = self.get_letters_that_make_words(new, Direction::Row, coords);
|
let valid_row_moves = self.get_letters_that_make_words(new, Direction::Row, coords);
|
||||||
let valid_column_moves = self.get_letters_that_make_words(new, Direction::Column, coords);
|
let valid_column_moves =
|
||||||
|
self.get_letters_that_make_words(new, Direction::Column, coords);
|
||||||
|
|
||||||
let existing_row_moves = self.row_cross_tiles.get_mut(coords.map_to_index()).unwrap();
|
let existing_row_moves =
|
||||||
|
self.row_cross_tiles.get_mut(coords.map_to_index()).unwrap();
|
||||||
*existing_row_moves = valid_row_moves;
|
*existing_row_moves = valid_row_moves;
|
||||||
|
|
||||||
let existing_column_moves = self.column_cross_tiles.get_mut(coords.map_to_index()).unwrap();
|
let existing_column_moves = self
|
||||||
|
.column_cross_tiles
|
||||||
|
.get_mut(coords.map_to_index())
|
||||||
|
.unwrap();
|
||||||
*existing_column_moves = valid_column_moves;
|
*existing_column_moves = valid_column_moves;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
updated_rows.iter().for_each(|&j| {
|
updated_rows.iter().for_each(|&j| {
|
||||||
for i in 0..GRID_LENGTH {
|
for i in 0..GRID_LENGTH {
|
||||||
let coords = Coordinates(i, j);
|
let coords = Coordinates(i, j);
|
||||||
check_for_valid_moves(coords);
|
check_for_valid_moves(coords);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
updated_columns.iter().for_each(|&i| {
|
updated_columns.iter().for_each(|&i| {
|
||||||
|
@ -652,10 +639,14 @@ impl AI {
|
||||||
check_for_valid_moves(coords);
|
check_for_valid_moves(coords);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_letters_that_make_words(&self, new: &Board, direction: Direction, coords: Coordinates) -> Option<MoveMap> {
|
fn get_letters_that_make_words(
|
||||||
|
&self,
|
||||||
|
new: &Board,
|
||||||
|
direction: Direction,
|
||||||
|
coords: Coordinates,
|
||||||
|
) -> Option<MoveMap> {
|
||||||
let is_anchored = check_if_anchored(new, coords, direction);
|
let is_anchored = check_if_anchored(new, coords, direction);
|
||||||
if !is_anchored {
|
if !is_anchored {
|
||||||
return None;
|
return None;
|
||||||
|
@ -668,7 +659,7 @@ impl AI {
|
||||||
let cell = copy_grid.get_cell_mut(coords).unwrap();
|
let cell = copy_grid.get_cell_mut(coords).unwrap();
|
||||||
cell.value = Some(Letter {
|
cell.value = Some(Letter {
|
||||||
text: letter,
|
text: letter,
|
||||||
points: 0, // points are accounted for later
|
points: 0, // points are accounted for later
|
||||||
ephemeral: false, // so that tile bonuses are ignored
|
ephemeral: false, // so that tile bonuses are ignored
|
||||||
is_blank: false,
|
is_blank: false,
|
||||||
});
|
});
|
||||||
|
@ -687,11 +678,9 @@ impl AI {
|
||||||
|
|
||||||
Some(new_map)
|
Some(new_map)
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn check_if_anchored(grid: &Board, coords: Coordinates, direction: Direction) -> bool{
|
fn check_if_anchored(grid: &Board, coords: Coordinates, direction: Direction) -> bool {
|
||||||
|
|
||||||
// Middle tile is always considered anchored
|
// Middle tile is always considered anchored
|
||||||
if coords.0 == GRID_LENGTH / 2 && coords.1 == GRID_LENGTH / 2 {
|
if coords.0 == GRID_LENGTH / 2 && coords.1 == GRID_LENGTH / 2 {
|
||||||
return true;
|
return true;
|
||||||
|
@ -699,7 +688,7 @@ fn check_if_anchored(grid: &Board, coords: Coordinates, direction: Direction) ->
|
||||||
|
|
||||||
let has_letter = |alt_coord: Option<Coordinates>| -> bool {
|
let has_letter = |alt_coord: Option<Coordinates>| -> bool {
|
||||||
match alt_coord {
|
match alt_coord {
|
||||||
None => {false}
|
None => false,
|
||||||
Some(alt_coord) => {
|
Some(alt_coord) => {
|
||||||
let cell = grid.get_cell(alt_coord).unwrap();
|
let cell = grid.get_cell(alt_coord).unwrap();
|
||||||
cell.value.is_some()
|
cell.value.is_some()
|
||||||
|
@ -719,7 +708,11 @@ fn find_word_on_line(line_letters: &Vec<Option<Letter>>, line_index: i8) -> Stri
|
||||||
if start_word < 1 {
|
if start_word < 1 {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
if line_letters.get((start_word - 1) as usize).unwrap().is_none() {
|
if line_letters
|
||||||
|
.get((start_word - 1) as usize)
|
||||||
|
.unwrap()
|
||||||
|
.is_none()
|
||||||
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
start_word -= 1;
|
start_word -= 1;
|
||||||
|
@ -738,19 +731,20 @@ fn find_word_on_line(line_letters: &Vec<Option<Letter>>, line_index: i8) -> Stri
|
||||||
|
|
||||||
let mut str = String::new();
|
let mut str = String::new();
|
||||||
line_letters[(start_word as usize)..((end_word + 1) as usize)]
|
line_letters[(start_word as usize)..((end_word + 1) as usize)]
|
||||||
.iter().for_each(|letter| {
|
.iter()
|
||||||
str.push(letter.unwrap().text);
|
.for_each(|letter| {
|
||||||
});
|
str.push(letter.unwrap().text);
|
||||||
|
});
|
||||||
|
|
||||||
str
|
str
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::dictionary::Dictionary;
|
||||||
use rand::rngs::SmallRng;
|
use rand::rngs::SmallRng;
|
||||||
use rand::SeedableRng;
|
use rand::SeedableRng;
|
||||||
use crate::dictionary::Dictionary;
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
fn set_cell(board: &mut Board, x: u8, y: u8, letter: char, points: u32) {
|
fn set_cell(board: &mut Board, x: u8, y: u8, letter: char, points: u32) {
|
||||||
let cell = board.get_cell_mut(Coordinates(x, y)).unwrap();
|
let cell = board.get_cell_mut(Coordinates(x, y)).unwrap();
|
||||||
|
@ -793,31 +787,31 @@ mod tests {
|
||||||
dictionary.insert("APPLE".to_string(), 0.9);
|
dictionary.insert("APPLE".to_string(), 0.9);
|
||||||
|
|
||||||
let mut tray = Tray::new(7);
|
let mut tray = Tray::new(7);
|
||||||
tray.letters[0] = Some(Letter{
|
tray.letters[0] = Some(Letter {
|
||||||
text: 'A',
|
text: 'A',
|
||||||
points: 5,
|
points: 5,
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
is_blank: false,
|
is_blank: false,
|
||||||
});
|
});
|
||||||
tray.letters[1] = Some(Letter{
|
tray.letters[1] = Some(Letter {
|
||||||
text: 'P',
|
text: 'P',
|
||||||
points: 4,
|
points: 4,
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
is_blank: false,
|
is_blank: false,
|
||||||
});
|
});
|
||||||
tray.letters[2] = Some(Letter{
|
tray.letters[2] = Some(Letter {
|
||||||
text: 'P',
|
text: 'P',
|
||||||
points: 4,
|
points: 4,
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
is_blank: false,
|
is_blank: false,
|
||||||
});
|
});
|
||||||
tray.letters[3] = Some(Letter{
|
tray.letters[3] = Some(Letter {
|
||||||
text: 'L',
|
text: 'L',
|
||||||
points: 3,
|
points: 3,
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
is_blank: false,
|
is_blank: false,
|
||||||
});
|
});
|
||||||
tray.letters[4] = Some(Letter{
|
tray.letters[4] = Some(Letter {
|
||||||
text: 'E',
|
text: 'E',
|
||||||
points: 4,
|
points: 4,
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
|
@ -832,14 +826,12 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(moves.len(), 10);
|
assert_eq!(moves.len(), 10);
|
||||||
|
|
||||||
tray.letters[4] = Some(
|
tray.letters[4] = Some(Letter {
|
||||||
Letter {
|
text: ' ',
|
||||||
text: ' ',
|
points: 0,
|
||||||
points: 0,
|
ephemeral: false,
|
||||||
ephemeral: false,
|
is_blank: true,
|
||||||
is_blank: true,
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
let moves = ai.find_all_moves(&tray, &board);
|
let moves = ai.find_all_moves(&tray, &board);
|
||||||
|
|
||||||
|
@ -857,8 +849,6 @@ mod tests {
|
||||||
set_cell(&mut board, 7, 9, 'A', 1);
|
set_cell(&mut board, 7, 9, 'A', 1);
|
||||||
set_cell(&mut board, 7, 10, 'T', 1);
|
set_cell(&mut board, 7, 10, 'T', 1);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
let difficulty = Difficulty {
|
let difficulty = Difficulty {
|
||||||
proportion: 0.0, // restrict yourself to words with this proportion OR HIGHER
|
proportion: 0.0, // restrict yourself to words with this proportion OR HIGHER
|
||||||
randomness: 0.0,
|
randomness: 0.0,
|
||||||
|
@ -869,25 +859,25 @@ mod tests {
|
||||||
dictionary.insert("SLAM".to_string(), 0.9);
|
dictionary.insert("SLAM".to_string(), 0.9);
|
||||||
|
|
||||||
let mut tray = Tray::new(7);
|
let mut tray = Tray::new(7);
|
||||||
tray.letters[0] = Some(Letter{
|
tray.letters[0] = Some(Letter {
|
||||||
text: 'S',
|
text: 'S',
|
||||||
points: 1,
|
points: 1,
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
is_blank: false,
|
is_blank: false,
|
||||||
});
|
});
|
||||||
tray.letters[1] = Some(Letter{
|
tray.letters[1] = Some(Letter {
|
||||||
text: 'L',
|
text: 'L',
|
||||||
points: 1,
|
points: 1,
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
is_blank: false,
|
is_blank: false,
|
||||||
});
|
});
|
||||||
tray.letters[6] = Some(Letter{
|
tray.letters[6] = Some(Letter {
|
||||||
text: 'A',
|
text: 'A',
|
||||||
points: 1,
|
points: 1,
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
is_blank: false,
|
is_blank: false,
|
||||||
});
|
});
|
||||||
tray.letters[3] = Some(Letter{
|
tray.letters[3] = Some(Letter {
|
||||||
text: 'M',
|
text: 'M',
|
||||||
points: 3,
|
points: 3,
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
|
@ -898,7 +888,10 @@ mod tests {
|
||||||
|
|
||||||
ai.update_state(&board);
|
ai.update_state(&board);
|
||||||
|
|
||||||
let end_of_boat = ai.column_cross_tiles.get(Coordinates(7, 11).map_to_index()).unwrap();
|
let end_of_boat = ai
|
||||||
|
.column_cross_tiles
|
||||||
|
.get(Coordinates(7, 11).map_to_index())
|
||||||
|
.unwrap();
|
||||||
assert!(end_of_boat.is_some());
|
assert!(end_of_boat.is_some());
|
||||||
assert_eq!(end_of_boat.as_ref().unwrap().len(), 1);
|
assert_eq!(end_of_boat.as_ref().unwrap().len(), 1);
|
||||||
|
|
||||||
|
@ -906,7 +899,6 @@ mod tests {
|
||||||
|
|
||||||
println!("Moves are {:?}", moves);
|
println!("Moves are {:?}", moves);
|
||||||
|
|
||||||
|
|
||||||
// 3 possible moves -
|
// 3 possible moves -
|
||||||
// 1. put 'S' at the end of 'BOAT' and form words 'SLAM' and 'BOATS'
|
// 1. put 'S' at the end of 'BOAT' and form words 'SLAM' and 'BOATS'
|
||||||
// 2. Put 'S' at end of 'BOAT'
|
// 2. Put 'S' at end of 'BOAT'
|
||||||
|
@ -920,7 +912,6 @@ mod tests {
|
||||||
|
|
||||||
let play = best_move.convert_to_play(&tray);
|
let play = best_move.convert_to_play(&tray);
|
||||||
println!("Play is {:?}", play);
|
println!("Play is {:?}", play);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -943,25 +934,30 @@ mod tests {
|
||||||
let above_cell_coords = Coordinates(7, 6);
|
let above_cell_coords = Coordinates(7, 6);
|
||||||
let left_cell_cords = Coordinates(6, 7);
|
let left_cell_cords = Coordinates(6, 7);
|
||||||
|
|
||||||
let row_cross_tiles = ai.row_cross_tiles.get(left_cell_cords.map_to_index()).unwrap();
|
let row_cross_tiles = ai
|
||||||
let column_cross_tiles = ai.column_cross_tiles.get(above_cell_coords.map_to_index()).unwrap();
|
.row_cross_tiles
|
||||||
|
.get(left_cell_cords.map_to_index())
|
||||||
|
.unwrap();
|
||||||
|
let column_cross_tiles = ai
|
||||||
|
.column_cross_tiles
|
||||||
|
.get(above_cell_coords.map_to_index())
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(row_cross_tiles.as_ref().unwrap().len(), 1);
|
assert_eq!(row_cross_tiles.as_ref().unwrap().len(), 1);
|
||||||
assert_eq!(column_cross_tiles.as_ref().unwrap().len(), 1);
|
assert_eq!(column_cross_tiles.as_ref().unwrap().len(), 1);
|
||||||
|
|
||||||
let far_off_tiles = ai.row_cross_tiles.get(0).unwrap();
|
let far_off_tiles = ai.row_cross_tiles.get(0).unwrap();
|
||||||
assert!(far_off_tiles.is_none());
|
assert!(far_off_tiles.is_none());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_valid_moves() {
|
fn test_valid_moves() {
|
||||||
let mut board = Board::new();
|
let mut board = Board::new();
|
||||||
|
|
||||||
set_cell(&mut board, 7-3, 7+3, 'Z', 1);
|
set_cell(&mut board, 7 - 3, 7 + 3, 'Z', 1);
|
||||||
set_cell(&mut board, 6-3, 8+3, 'A', 1);
|
set_cell(&mut board, 6 - 3, 8 + 3, 'A', 1);
|
||||||
set_cell(&mut board, 7-3, 8+3, 'A', 1);
|
set_cell(&mut board, 7 - 3, 8 + 3, 'A', 1);
|
||||||
set_cell(&mut board, 7-3, 9+3, 'Z', 1);
|
set_cell(&mut board, 7 - 3, 9 + 3, 'Z', 1);
|
||||||
|
|
||||||
let difficulty = Difficulty {
|
let difficulty = Difficulty {
|
||||||
proportion: 0.0, // restrict yourself to words with this proportion OR HIGHER
|
proportion: 0.0, // restrict yourself to words with this proportion OR HIGHER
|
||||||
|
@ -972,7 +968,7 @@ mod tests {
|
||||||
dictionary.insert("AA".to_string(), 0.5);
|
dictionary.insert("AA".to_string(), 0.5);
|
||||||
|
|
||||||
let mut tray = Tray::new(7);
|
let mut tray = Tray::new(7);
|
||||||
tray.letters[0] = Some(Letter{
|
tray.letters[0] = Some(Letter {
|
||||||
text: 'A',
|
text: 'A',
|
||||||
points: 1,
|
points: 1,
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
|
@ -992,50 +988,52 @@ mod tests {
|
||||||
fn test_starting_move() {
|
fn test_starting_move() {
|
||||||
let mut board = Board::new();
|
let mut board = Board::new();
|
||||||
|
|
||||||
let difficulty = Difficulty{proportion: 0.1, randomness: 0.0};
|
let difficulty = Difficulty {
|
||||||
|
proportion: 0.1,
|
||||||
|
randomness: 0.0,
|
||||||
|
};
|
||||||
|
|
||||||
let mut dictionary = DictionaryImpl::new();
|
let mut dictionary = DictionaryImpl::new();
|
||||||
dictionary.insert("TWEETED".to_string(), 0.2);
|
dictionary.insert("TWEETED".to_string(), 0.2);
|
||||||
|
|
||||||
|
|
||||||
let mut tray = Tray::new(7);
|
let mut tray = Tray::new(7);
|
||||||
tray.letters[0] = Some(Letter{
|
tray.letters[0] = Some(Letter {
|
||||||
text: 'I',
|
text: 'I',
|
||||||
points: 1,
|
points: 1,
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
is_blank: false,
|
is_blank: false,
|
||||||
});
|
});
|
||||||
tray.letters[1] = Some(Letter{
|
tray.letters[1] = Some(Letter {
|
||||||
text: 'R',
|
text: 'R',
|
||||||
points: 1,
|
points: 1,
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
is_blank: false,
|
is_blank: false,
|
||||||
});
|
});
|
||||||
tray.letters[2] = Some(Letter{
|
tray.letters[2] = Some(Letter {
|
||||||
text: 'W',
|
text: 'W',
|
||||||
points: 1,
|
points: 1,
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
is_blank: false,
|
is_blank: false,
|
||||||
});
|
});
|
||||||
tray.letters[3] = Some(Letter{
|
tray.letters[3] = Some(Letter {
|
||||||
text: 'I',
|
text: 'I',
|
||||||
points: 1,
|
points: 1,
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
is_blank: false,
|
is_blank: false,
|
||||||
});
|
});
|
||||||
tray.letters[4] = Some(Letter{
|
tray.letters[4] = Some(Letter {
|
||||||
text: 'T',
|
text: 'T',
|
||||||
points: 1,
|
points: 1,
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
is_blank: false,
|
is_blank: false,
|
||||||
});
|
});
|
||||||
tray.letters[5] = Some(Letter{
|
tray.letters[5] = Some(Letter {
|
||||||
text: 'E',
|
text: 'E',
|
||||||
points: 1,
|
points: 1,
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
is_blank: false,
|
is_blank: false,
|
||||||
});
|
});
|
||||||
tray.letters[6] = Some(Letter{
|
tray.letters[6] = Some(Letter {
|
||||||
text: 'D',
|
text: 'D',
|
||||||
points: 1,
|
points: 1,
|
||||||
ephemeral: false,
|
ephemeral: false,
|
||||||
|
@ -1048,7 +1046,5 @@ mod tests {
|
||||||
println!("Available moves are {:?}", all_moves);
|
println!("Available moves are {:?}", all_moves);
|
||||||
|
|
||||||
assert!(all_moves.is_empty());
|
assert!(all_moves.is_empty());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
|
Loading…
Reference in a new issue