Integrate score keeping and individual trays

This commit is contained in:
Joel Therrien 2023-08-15 19:35:23 -07:00
parent deedc1aee4
commit c7399b2b99
5 changed files with 172 additions and 51 deletions

View file

@ -1,3 +1,4 @@
use rand::prelude::SliceRandom;
use rand::rngs::SmallRng; use rand::rngs::SmallRng;
use rand::SeedableRng; use rand::SeedableRng;
use crate::board::{Board, Letter}; use crate::board::{Board, Letter};
@ -14,16 +15,26 @@ pub enum Player {
} }
} }
impl Player {
pub fn get_name(&self) -> &str {
match &self {
Player::Human(name) => {name}
Player::AI { name, .. } => {name}
}
}
}
pub struct PlayerState { pub struct PlayerState {
player: Player, pub player: Player,
score: u32, pub score: u32,
tray: Tray tray: Tray
} }
pub struct Game{ pub struct Game{
tile_pool: Vec<Letter>, tile_pool: Vec<Letter>,
board: Board, board: Board,
player: PlayerState, pub player_states: Vec<PlayerState>,
dictionary: DictionaryImpl, dictionary: DictionaryImpl,
} }
@ -38,34 +49,48 @@ pub struct Game{
// This will later work out well when I build it out as an API for multiplayer. // This will later work out well when I build it out as an API for multiplayer.
impl Game { impl Game {
pub fn new(seed: u64, dictionary_text: &str) -> Self { pub fn new(seed: u64, dictionary_text: &str, mut player_names: Vec<String>) -> 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 mut tray = Tray::new(TRAY_LENGTH);
tray.fill(&mut letters);
player_names.shuffle(&mut rng);
let player = PlayerState { let player_states: Vec<PlayerState> = player_names.iter()
player: Player::Human("Joel".to_string()), .map(|name| {
score: 0, let mut tray = Tray::new(TRAY_LENGTH);
tray, tray.fill(&mut letters);
}; PlayerState {
player: Player::Human(name.clone()),
score: 0,
tray,
}
})
.collect();
Game { Game {
tile_pool: letters, tile_pool: letters,
board: Board::new(), board: Board::new(),
player, player_states,
dictionary: DictionaryImpl::create_from_str(dictionary_text) dictionary: DictionaryImpl::create_from_str(dictionary_text)
} }
} }
pub fn get_tray(&self) -> &Tray { pub fn get_tray(&self, name: &str) -> Option<&Tray> {
&self.player.tray let player = self.player_states.iter()
.filter(|state| state.player.get_name().eq(name))
.nth(0)?;
Some(&player.tray)
} }
pub fn get_tray_mut(&mut self) -> &mut Tray { pub fn get_tray_mut(&mut self, name: &str) -> Option<&mut Tray> {
&mut self.player.tray let player = self.player_states.iter_mut()
.filter(|state| state.player.get_name().eq(name))
.nth(0)?;
Some(&mut player.tray)
} }
pub fn get_board(&self) -> &Board {&self.board} pub fn get_board(&self) -> &Board {&self.board}

View file

@ -1,4 +1,5 @@
pub struct Difficulty { pub struct Difficulty {
proportion: f64, proportion: f64,
randomness: f64, randomness: f64,

View file

@ -35,13 +35,13 @@ impl GameWasm {
#[wasm_bindgen(constructor)] #[wasm_bindgen(constructor)]
pub fn new(seed: u64, dictionary_text: &str) -> GameWasm { pub fn new(seed: u64, dictionary_text: &str) -> GameWasm {
GameWasm(Game::new(seed, dictionary_text)) GameWasm(Game::new(seed, dictionary_text, vec!["Player".to_string(), "AI".to_string()]))
} }
pub fn get_tray(&self) -> Result<JsValue, Error> { pub fn get_tray(&self, name: &str) -> Result<JsValue, Error> {
let tray = self.0.get_tray(); let tray = self.0.get_tray(name);
serde_wasm_bindgen::to_value(tray) serde_wasm_bindgen::to_value(&tray)
} }
pub fn get_board_cell_types(&self) -> Result<JsValue, Error> { pub fn get_board_cell_types(&self) -> Result<JsValue, Error> {
@ -54,11 +54,11 @@ impl GameWasm {
serde_wasm_bindgen::to_value(&cell_types) serde_wasm_bindgen::to_value(&cell_types)
} }
pub fn receive_play(&mut self, tray_tile_locations: JsValue, commit_move: bool) -> Result<JsValue, JsValue> { pub fn receive_play(&mut self, player: &str, tray_tile_locations: JsValue, commit_move: bool) -> Result<JsValue, JsValue> {
let tray_tile_locations: Vec<Option<PlayedTile>> = serde_wasm_bindgen::from_value(tray_tile_locations)?; let tray_tile_locations: Vec<Option<PlayedTile>> = serde_wasm_bindgen::from_value(tray_tile_locations)?;
let mut board_instance = self.0.get_board().clone(); let mut board_instance = self.0.get_board().clone();
let tray = self.0.get_tray().clone(); let tray = self.0.get_tray(player).unwrap().clone();
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() {
@ -134,4 +134,26 @@ impl GameWasm {
Ok(result) Ok(result)
} }
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.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)?)
}
} }

View file

@ -1,7 +1,6 @@
import * as React from "react"; import * as React from "react";
import {GameWasm, Letter as LetterData, MyResult, PlayedTile, Tray, WordResult} from "word_grid"; import {GameWasm, Letter as LetterData, MyResult, PlayedTile, PlayerAndScore, Tray, WordResult} from "word_grid";
import {createRoot} from "react-dom/client"; import {useMemo, useReducer, useState} from "react";
import {Children, useMemo, useReducer, useState} from "react";
export enum LocationType { export enum LocationType {
GRID, GRID,
@ -75,24 +74,10 @@ function matchCoordinate(playerLetters: PlayableLetterData[], coords: Coordinate
type TileDispatch = React.Dispatch<{start: CoordinateData, end: CoordinateData}>; type TileDispatch = React.Dispatch<{start: CoordinateData, end: CoordinateData}>;
function movePlayableLetters(playerLetters: PlayableLetterData[], update: {start: CoordinateData, end: CoordinateData}) { function addLogInfo(existingLog: React.JSX.Element[], newItem: React.JSX.Element) {
newItem.key = existingLog.length;
let startIndex = matchCoordinate(playerLetters, update.start); existingLog.push(newItem);
let endIndex = matchCoordinate(playerLetters, update.end); return existingLog.slice();
if(startIndex != null) {
let startLetter = playerLetters[startIndex];
startLetter.location = update.end.location;
startLetter.index = update.end.index;
}
if(endIndex != null) {
let endLetter = playerLetters[endIndex];
endLetter.location = update.start.location;
endLetter.index = update.start.index;
}
return playerLetters.slice();
} }
export function Game(props: {wasm: GameWasm}) { export function Game(props: {wasm: GameWasm}) {
@ -101,8 +86,32 @@ export function Game(props: {wasm: GameWasm}) {
return props.wasm.get_board_cell_types(); return props.wasm.get_board_cell_types();
}, []); }, []);
const [checkPerformed, setCheckPerformed] = useState<boolean>(false);
function movePlayableLetters(playerLetters: PlayableLetterData[], update: {start: CoordinateData, end: CoordinateData}) {
let startIndex = matchCoordinate(playerLetters, update.start);
let endIndex = matchCoordinate(playerLetters, update.end);
if(startIndex != null) {
let startLetter = playerLetters[startIndex];
startLetter.location = update.end.location;
startLetter.index = update.end.index;
}
if(endIndex != null) {
let endLetter = playerLetters[endIndex];
endLetter.location = update.start.location;
endLetter.index = update.start.index;
}
setCheckPerformed(false);
return playerLetters.slice();
}
const [playerLetters, dispatch] = useReducer(movePlayableLetters, null, (_) => { const [playerLetters, dispatch] = useReducer(movePlayableLetters, null, (_) => {
let tray: Tray = props.wasm.get_tray(); let tray: Tray = props.wasm.get_tray("Player");
// initial state // initial state
let letters: PlayableLetterData[] = tray.letters.map((ld, i) => { let letters: PlayableLetterData[] = tray.letters.map((ld, i) => {
@ -112,13 +121,28 @@ export function Game(props: {wasm: GameWasm}) {
}) })
return letters; return letters;
}) });
const [logInfo, logDispatch] = useReducer(addLogInfo, []);
const [turnCount, setTurnCount] = useState<number>(0);
const playerAndScores: PlayerAndScore[] = useMemo(() => {
return props.wasm.get_scores();
}, [turnCount])
return <> return <>
<Grid cellTypes={cellTypes} playerLetters={playerLetters} dispatch={dispatch}/> <div className="board-log">
<Grid cellTypes={cellTypes} playerLetters={playerLetters} dispatch={dispatch}/>
<div className="message-log">
<Scores playerScores={playerAndScores}/>
<div className="log">
{logInfo}
</div>
</div>
</div>
<TileTray letters={playerLetters} trayLength={7} dispatch={dispatch}/> <TileTray letters={playerLetters} trayLength={7} dispatch={dispatch}/>
<button onClick={(e) => { <button onClick={(e) => {
const playedTiles = playerLetters.map((i) => { const playedTiles = playerLetters.map((i) => {
@ -141,11 +165,11 @@ export function Game(props: {wasm: GameWasm}) {
return null; return null;
}); });
const result: MyResult<Array<WordResult> | string> = props.wasm.receive_play(playedTiles, false); const result: MyResult<Array<WordResult> | string> = props.wasm.receive_play("Player", playedTiles, false);
console.log({result}); console.log({result});
if(result.response_type === "ERR") { if(result.response_type === "ERR") {
alert(result.value); logDispatch(<div><em>{(result.value as string)}</em></div>);
} else { } else {
let total_points = 0; let total_points = 0;
@ -153,12 +177,15 @@ export function Game(props: {wasm: GameWasm}) {
total_points += word_result.score; total_points += word_result.score;
} }
alert(`You would score ${total_points} points.`); const msg = `You would score ${total_points} points.`;
logDispatch(<div><em>{msg}</em></div>);
setCheckPerformed(true);
} }
}}>Check Submission</button> }}>{checkPerformed ? "Submit ✅" : "Check"}</button>
</>; </>;
@ -271,4 +298,18 @@ function Grid(props: {cellTypes: CellType[], playerLetters: Array<PlayableLetter
return <div className="board-grid"> return <div className="board-grid">
{elements} {elements}
</div> </div>
}
function Scores(props: {playerScores: Array<PlayerAndScore>}){
let elements = props.playerScores.map((ps, i) => {
return <div key={ps.name}>
<h3>{ps.name}</h3>
<span>{ps.score}</span>
</div>;
});
return <div className="scoring">
{elements}
</div>
} }

View file

@ -21,6 +21,7 @@
justify-content: center; justify-content: center;
grid-gap: 1px; grid-gap: 1px;
user-select: none; user-select: none;
flex: initial;
} }
.grid-spot { .grid-spot {
@ -104,4 +105,35 @@
padding-right: 5px; padding-right: 5px;
} }
}
.board-log {
display: flex;
justify-content: start;
flex-flow: row wrap;
align-items: stretch;
}
.message-log {
border-color: black;
border-style: solid;
border-width: 2px;
width: max-content;
margin: 5px
}
.scoring {
text-align: center;
display: flex;
grid-template-columns: 1fr 1fr;
grid-template-rows: none;
span {
font-size: 20px;
}
div {
margin-left: 10px;
margin-right: 10px;
}
} }