From a496b15710cee9a6142adaaa90f70eec122ec045 Mon Sep 17 00:00:00 2001 From: Joel Therrien Date: Sun, 13 Aug 2023 13:08:37 -0700 Subject: [PATCH] Integrate very WIP move-checking onto front-end --- src/board.rs | 36 +++++++++++++++++++++++++----- src/dictionary.rs | 27 ++++++++++++++++++----- src/game.rs | 18 ++++++++++++--- src/wasm.rs | 53 ++++++++++++++++++++++++++++++++++++++++++--- ui/src/elements.tsx | 36 ++++++++++++++++++++---------- ui/src/index.tsx | 20 ++++++++++++++--- ui/src/style.less | 4 ++++ 7 files changed, 163 insertions(+), 31 deletions(-) diff --git a/src/board.rs b/src/board.rs index b75eaf3..f038ac9 100644 --- a/src/board.rs +++ b/src/board.rs @@ -24,6 +24,14 @@ impl Direction { pub struct Coordinates (pub u8, pub u8); impl Coordinates { + + pub fn new_from_index(index: usize) -> Self { + let y = index / GRID_LENGTH as usize; + let x = index % GRID_LENGTH as usize; + + Coordinates(x as u8, y as u8) + } + fn add(&self, direction: Direction, i: i8) -> Option { let proposed = match direction { Direction::Column => {(self.0 as i8, self.1 as i8+i)} @@ -89,6 +97,7 @@ impl Letter { } } } + } #[derive(Debug, Copy, Clone, Serialize)] @@ -101,14 +110,14 @@ pub enum CellType { Start, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Cell { pub value: Option, pub cell_type: CellType, pub coordinates: Coordinates, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct Board { pub cells: Vec, } @@ -376,8 +385,6 @@ impl Board { } else { return Err("Played tiles must be anchored to something") } - - } fn find_word_at_position(&self, mut start_coords: Coordinates, direction: Direction) -> Option { @@ -431,6 +438,25 @@ impl Board { }) } + pub fn receive_play(&mut self, play: Vec<(Letter, Coordinates)>) -> Result<(), String> { + for (mut letter, coords) in play { + { + let mut cell = match self.get_cell_mut(coords) { + Ok(cell) => {cell} + Err(e) => {return Err(e.to_string())} + }; + + if cell.value.is_some() { + return Err(format!("There's already a letter at {:?}", coords)); + } + + letter.ephemeral = true; + cell.value = Some(letter); + } + } + + Ok(()) + } } @@ -774,7 +800,7 @@ mod tests { board.get_cell_mut(Coordinates(6, 0)).unwrap().value = Some(Letter::new_fixed('L', 1)); fn check_board(board: &mut Board, inverted: bool) { - let dictionary = DictionaryImpl::create("resources/dictionary.csv"); + let dictionary = DictionaryImpl::create_from_path("resources/dictionary.csv"); println!("{}", board); let words = board.find_played_words(); match words { diff --git a/src/dictionary.rs b/src/dictionary.rs index 4a63e7c..82708df 100644 --- a/src/dictionary.rs +++ b/src/dictionary.rs @@ -1,19 +1,22 @@ use std::collections::{HashMap, HashSet}; use std::str::FromStr; +use csv::Reader; use crate::board::Word; pub trait Dictionary { - fn create(path: &str) -> Self; + + fn create_from_reader(reader: Reader) -> Self; + fn create_from_path(path: &str) -> Self; + fn create_from_str(data: &str) -> Self; fn filter_to_sub_dictionary(&self, proportion: f64) -> Self; fn substring_set(&self) -> HashSet<&str>; fn is_word_valid(&self, word: &Word) -> bool; } pub type DictionaryImpl = HashMap; -impl Dictionary for DictionaryImpl{ +impl Dictionary for DictionaryImpl{ - fn create(path: &str) -> Self { - let mut reader = csv::Reader::from_path(path).unwrap(); + fn create_from_reader(mut reader: Reader) -> Self { let mut map = HashMap::new(); for result in reader.records() { @@ -30,6 +33,20 @@ impl Dictionary for DictionaryImpl{ map } + fn create_from_path(path: &str) -> Self { + let reader = csv::Reader::from_path(path).unwrap(); + + DictionaryImpl::create_from_reader(reader) + } + + fn create_from_str(data: &str) -> Self { + let reader = csv::ReaderBuilder::new() + .has_headers(true) + .from_reader(data.as_bytes()); + + DictionaryImpl::create_from_reader(reader) + } + fn filter_to_sub_dictionary(&self, proportion: f64) -> Self { let mut map = HashMap::new(); @@ -71,7 +88,7 @@ mod tests { #[test] fn test_dictionary() { - let dictionary = HashMap::create("resources/dictionary.csv"); + let dictionary = HashMap::create_from_path("resources/dictionary.csv"); assert_eq!(dictionary.len(), 279429); diff --git a/src/game.rs b/src/game.rs index 108cd71..8ab8814 100644 --- a/src/game.rs +++ b/src/game.rs @@ -1,8 +1,8 @@ use rand::rngs::SmallRng; use rand::SeedableRng; -use wasm_bindgen::prelude::wasm_bindgen; use crate::board::{Board, Letter}; use crate::constants::{standard_tile_pool, TRAY_LENGTH}; +use crate::dictionary::{Dictionary, DictionaryImpl}; use crate::player_interaction::ai::Difficulty; use crate::player_interaction::Tray; @@ -20,10 +20,11 @@ pub struct PlayerState { tray: Tray } -pub struct Game { +pub struct Game{ tile_pool: Vec, board: Board, player: PlayerState, + dictionary: DictionaryImpl, } // Problem - I want to provide a UI to the player @@ -37,7 +38,7 @@ pub struct Game { // This will later work out well when I build it out as an API for multiplayer. impl Game { - pub fn new(seed: u64) -> Self { + pub fn new(seed: u64, dictionary_text: &str) -> Self { let mut rng = SmallRng::seed_from_u64(seed); let mut letters = standard_tile_pool(Some(&mut rng)); @@ -55,6 +56,7 @@ impl Game { tile_pool: letters, board: Board::new(), player, + dictionary: DictionaryImpl::create_from_str(dictionary_text) } } @@ -68,4 +70,14 @@ impl Game { pub fn get_board(&self) -> &Board {&self.board} + pub fn get_board_mut(&mut self) -> &mut Board { + &mut self.board + } + + pub fn get_dictionary(&self) -> &DictionaryImpl { + &self.dictionary + } + + + } \ No newline at end of file diff --git a/src/wasm.rs b/src/wasm.rs index 4682e2a..1614613 100644 --- a/src/wasm.rs +++ b/src/wasm.rs @@ -1,18 +1,28 @@ +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; +use crate::board::{Board, CellType, Coordinates, Letter}; use crate::game::Game; #[wasm_bindgen] pub struct GameWasm(Game); + +#[derive(Deserialize, Tsify, Copy, Clone)] +#[tsify(from_wasm_abi)] +pub struct PlayedTile { + index: usize, + character: Option, +} + #[wasm_bindgen] impl GameWasm { #[wasm_bindgen(constructor)] - pub fn new(seed: u64) -> GameWasm { - GameWasm(Game::new(seed)) + pub fn new(seed: u64, dictionary_text: &str) -> GameWasm { + GameWasm(Game::new(seed, dictionary_text)) } pub fn get_tray(&self) -> Result { @@ -30,4 +40,41 @@ impl GameWasm { serde_wasm_bindgen::to_value(&cell_types) } + + pub fn receive_play(&mut self, tray_tile_locations: JsValue, commit_move: bool) -> Result { + let tray_tile_locations: Vec> = serde_wasm_bindgen::from_value(tray_tile_locations)?; + + let mut board_instance = self.0.get_board().clone(); + let tray = self.0.get_tray().clone(); + + let mut played_letters: Vec<(Letter, Coordinates)> = Vec::new(); + for (i, played_tile) in tray_tile_locations.iter().enumerate() { + if played_tile.is_some() { + let played_tile = played_tile.unwrap(); + let mut letter: Letter = tray.letters.get(i).unwrap().unwrap(); + + let coord = Coordinates::new_from_index(played_tile.index); + if letter.is_blank { + match played_tile.character { + None => { + panic!("You can't play a blank character without providing a letter value") + } + Some(x) => { + // TODO - I should check that the character is a valid letter + letter.text = x; + } + } + } + played_letters.push((letter, coord)); + + } + } + + board_instance.receive_play(played_letters)?; + + let result = board_instance.calculate_scores(self.0.get_dictionary())?; + + Ok(serde_wasm_bindgen::to_value(&&result.1).unwrap()) + + } } \ No newline at end of file diff --git a/ui/src/elements.tsx b/ui/src/elements.tsx index 21e1000..1a8ad90 100644 --- a/ui/src/elements.tsx +++ b/ui/src/elements.tsx @@ -1,5 +1,5 @@ import * as React from "react"; -import {GameWasm, Letter as LetterData, Tray} from "word_grid"; +import {GameWasm, Letter as LetterData, PlayedTile, Tray} from "word_grid"; import {createRoot} from "react-dom/client"; import {Children, useMemo, useReducer, useState} from "react"; @@ -121,17 +121,29 @@ export function Game(props: {wasm: GameWasm}) { + const playedTiles = playerLetters.map((i) => { + if (i === undefined) { + 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; + } + + return null; + }); + + const result = props.wasm.receive_play(playedTiles, false); + console.log({result}); + }}>Check Submission ; diff --git a/ui/src/index.tsx b/ui/src/index.tsx index a1c7a19..5fb3b2c 100644 --- a/ui/src/index.tsx +++ b/ui/src/index.tsx @@ -1,9 +1,13 @@ import init, {greet, GameWasm} from '../node_modules/word_grid/word_grid.js'; -import {Game, LocationType, PlayableLetterData, TileTray} from "./elements"; +import {Game} from "./elements"; import {createRoot} from "react-dom/client"; -import {Tray} from "word_grid"; import * as React from "react"; +// @ts-ignore +const dictionary_url = new URL("../../resources/dictionary.csv", import.meta.url); + +let dictionary_text: string = null; + async function run() { // First up we need to actually load the wasm file, so we use the // default export to inform it where the wasm file is located on the @@ -33,7 +37,17 @@ async function run() { // modes await init(); - let game = new GameWasm(BigInt(1234)); + // need to retrieve the dictionary.csv file as text + if(dictionary_text == null){ + + dictionary_text = await fetch(dictionary_url).then((response) => { + return response.text() + }) + } + + let game = new GameWasm(BigInt(1234), dictionary_text); + + const cellTypes = game.get_board_cell_types(); console.log({cellTypes}); diff --git a/ui/src/style.less b/ui/src/style.less index 8805762..ed43bee 100644 --- a/ui/src/style.less +++ b/ui/src/style.less @@ -40,6 +40,10 @@ left: 0; z-index: 1; } + + .ephemeral { + opacity: 75%; + } } .grid-spot-normal{