Split UI code into separate files
This commit is contained in:
parent
de2605af67
commit
974751bda0
6 changed files with 616 additions and 594 deletions
206
ui/src/Game.tsx
Normal file
206
ui/src/Game.tsx
Normal file
|
@ -0,0 +1,206 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import {GameWasm, Letter as LetterData, MyResult, PlayedTile, PlayerAndScore, Tray, WordResult} from "word_grid";
|
||||||
|
import {
|
||||||
|
LocationType,
|
||||||
|
matchCoordinate,
|
||||||
|
mergeTrays,
|
||||||
|
PlayableLetterData,
|
||||||
|
Settings,
|
||||||
|
TileDispatchAction,
|
||||||
|
TileDispatchActionType
|
||||||
|
} from "./utils";
|
||||||
|
import {useEffect, useMemo, useReducer, useRef, useState} from "react";
|
||||||
|
import {TileExchangeModal} from "./TileExchange";
|
||||||
|
import {Grid, Scores, TileTray} from "./UI";
|
||||||
|
|
||||||
|
function addLogInfo(existingLog: React.JSX.Element[], newItem: React.JSX.Element) {
|
||||||
|
newItem = React.cloneElement(newItem, { key: existingLog.length })
|
||||||
|
existingLog.push(newItem);
|
||||||
|
return existingLog.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Game(props: {wasm: GameWasm, settings: Settings}) {
|
||||||
|
|
||||||
|
const cellTypes = useMemo(() => {
|
||||||
|
return props.wasm.get_board_cell_types();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [confirmedScorePoints, setConfirmedScorePoints] = useState<number>(-1);
|
||||||
|
|
||||||
|
function movePlayableLetters(playerLetters: PlayableLetterData[], update: TileDispatchAction) {
|
||||||
|
|
||||||
|
if(update.action === TileDispatchActionType.RETRIEVE) {
|
||||||
|
|
||||||
|
let tray: Tray = props.wasm.get_tray("Player");
|
||||||
|
|
||||||
|
return mergeTrays(playerLetters, tray.letters);
|
||||||
|
} else if (update.action === TileDispatchActionType.MOVE) {
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
setConfirmedScorePoints(-1);
|
||||||
|
|
||||||
|
return playerLetters.slice();
|
||||||
|
} if (update.action === TileDispatchActionType.SET_BLANK) {
|
||||||
|
const blankLetter = playerLetters[update.blankIndex];
|
||||||
|
if(blankLetter.text !== update.newBlankValue) {
|
||||||
|
blankLetter.text = update.newBlankValue;
|
||||||
|
if (blankLetter.location == LocationType.GRID) {
|
||||||
|
setConfirmedScorePoints(-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return playerLetters.slice();
|
||||||
|
} else if (update.action === TileDispatchActionType.RETURN) {
|
||||||
|
return mergeTrays(playerLetters, playerLetters);
|
||||||
|
} else {
|
||||||
|
console.error("Unknown tray update");
|
||||||
|
console.error({update});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
const [playerLetters, trayDispatch] = useReducer(movePlayableLetters, []);
|
||||||
|
const [logInfo, logDispatch] = useReducer(addLogInfo, []);
|
||||||
|
|
||||||
|
const [turnCount, setTurnCount] = useState<number>(1);
|
||||||
|
const playerAndScores: PlayerAndScore[] = useMemo(() => {
|
||||||
|
return props.wasm.get_scores();
|
||||||
|
}, [turnCount]);
|
||||||
|
const boardLetters: LetterData[] = useMemo(() => {
|
||||||
|
return props.wasm.get_board_letters();
|
||||||
|
}, [turnCount]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
logDispatch(<h4>Turn {turnCount}</h4>);
|
||||||
|
setConfirmedScorePoints(-1);
|
||||||
|
trayDispatch({action: TileDispatchActionType.RETRIEVE});
|
||||||
|
|
||||||
|
}, [turnCount]);
|
||||||
|
|
||||||
|
const logDivRef = useRef(null);
|
||||||
|
|
||||||
|
const [isTileExchangeOpen, setIsTileExchangeOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Effect is to keep the log window scrolled down
|
||||||
|
if (logDivRef.current != null) {
|
||||||
|
logDivRef.current.scrollTo(0, logDivRef.current.scrollHeight); // scroll down
|
||||||
|
}
|
||||||
|
}, [logInfo])
|
||||||
|
|
||||||
|
function exchangeFunction(selectedArray: Array<boolean>) {
|
||||||
|
|
||||||
|
let numSelected = 0;
|
||||||
|
selectedArray.forEach((x) => {
|
||||||
|
if (x){
|
||||||
|
numSelected++;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const result: MyResult<Tray | string> = props.wasm.exchange_tiles("Player", selectedArray);
|
||||||
|
console.log({result});
|
||||||
|
|
||||||
|
if(result.response_type === "ERR") {
|
||||||
|
logDispatch(<div><em>{(result.value as string)}</em></div>);
|
||||||
|
} else {
|
||||||
|
logDispatch(<div><em>You exchanged {numSelected} tiles.</em></div>);
|
||||||
|
setTurnCount(turnCount + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return <>
|
||||||
|
<TileExchangeModal
|
||||||
|
playerLetters={playerLetters}
|
||||||
|
isOpen={isTileExchangeOpen}
|
||||||
|
setOpen={setIsTileExchangeOpen}
|
||||||
|
exchangeFunction={exchangeFunction}
|
||||||
|
/>
|
||||||
|
<div className="board-log">
|
||||||
|
<Grid cellTypes={cellTypes} playerLetters={playerLetters} boardLetters={boardLetters} dispatch={trayDispatch}/>
|
||||||
|
<div className="message-log">
|
||||||
|
<Scores playerScores={playerAndScores}/>
|
||||||
|
<div className="log" ref={logDivRef}>
|
||||||
|
{logInfo}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TileTray letters={playerLetters} trayLength={props.settings.trayLength} dispatch={trayDispatch}/>
|
||||||
|
<button onClick={(e) => {
|
||||||
|
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: MyResult<Array<WordResult> | string> = props.wasm.receive_play("Player", playedTiles, confirmedScorePoints > -1);
|
||||||
|
console.log({result});
|
||||||
|
|
||||||
|
if(result.response_type === "ERR") {
|
||||||
|
logDispatch(<div><em>{(result.value as string)}</em></div>);
|
||||||
|
} else {
|
||||||
|
|
||||||
|
let total_points = 0;
|
||||||
|
for (let word_result of (result.value as Array<WordResult>)) {
|
||||||
|
total_points += word_result.score;
|
||||||
|
}
|
||||||
|
|
||||||
|
let msg: string;
|
||||||
|
if (confirmedScorePoints > -1) {
|
||||||
|
msg = `You scored ${total_points} points.`;
|
||||||
|
setTurnCount(turnCount + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
logDispatch(<div><em>{msg}</em></div>);
|
||||||
|
|
||||||
|
setConfirmedScorePoints(total_points);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}}>{confirmedScorePoints > -1 ? `Score ${confirmedScorePoints} points ✅` : "Check"}</button>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
trayDispatch({action: TileDispatchActionType.RETURN}); // want all tiles back on tray for tile exchange
|
||||||
|
setIsTileExchangeOpen(true);
|
||||||
|
}}
|
||||||
|
>Open Tile Exchange</button>
|
||||||
|
<button onClick={(e) => {
|
||||||
|
trayDispatch({action: TileDispatchActionType.RETURN});
|
||||||
|
}}>Return Tiles</button>
|
||||||
|
</>;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
126
ui/src/TileExchange.tsx
Normal file
126
ui/src/TileExchange.tsx
Normal file
|
@ -0,0 +1,126 @@
|
||||||
|
import {addNTimes, PlayableLetterData} from "./utils";
|
||||||
|
import {useEffect, useState} from "react";
|
||||||
|
import {Modal} from "./Modal";
|
||||||
|
import {Letter as LetterData} from "word_grid";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export function TileExchangeModal(props: {
|
||||||
|
playerLetters: PlayableLetterData[],
|
||||||
|
isOpen: boolean,
|
||||||
|
setOpen: (isOpen: boolean) => void,
|
||||||
|
exchangeFunction: (selectedArray: Array<boolean>) => void
|
||||||
|
}) {
|
||||||
|
|
||||||
|
function clearExchangeTiles() {
|
||||||
|
const array: boolean[] = [];
|
||||||
|
addNTimes(array, false, props.playerLetters.length);
|
||||||
|
return array;
|
||||||
|
}
|
||||||
|
|
||||||
|
const [tilesToExchange, setTilesToExchange] = useState<boolean[]>(clearExchangeTiles);
|
||||||
|
useEffect(() => {
|
||||||
|
setTilesToExchange(clearExchangeTiles());
|
||||||
|
}, [props.playerLetters])
|
||||||
|
|
||||||
|
let tilesExchangedSelected = 0;
|
||||||
|
for (let i of tilesToExchange) {
|
||||||
|
if(i) {
|
||||||
|
tilesExchangedSelected++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Modal isOpen={props.isOpen} setOpen={(isOpen) => {
|
||||||
|
setTilesToExchange(clearExchangeTiles());
|
||||||
|
props.setOpen(isOpen);
|
||||||
|
}}>
|
||||||
|
<div className="tile-exchange-dialog">
|
||||||
|
<div className="instructions">
|
||||||
|
Click on each tile you'd like to exchange. You currently have {tilesExchangedSelected} tiles selected.
|
||||||
|
</div>
|
||||||
|
<div className="selection-buttons">
|
||||||
|
<button onClick={(e) => {
|
||||||
|
const array: boolean[] = [];
|
||||||
|
addNTimes(array, true, props.playerLetters.length);
|
||||||
|
setTilesToExchange(array);
|
||||||
|
}}>Select All</button>
|
||||||
|
<button onClick={(e) => {
|
||||||
|
setTilesToExchange(clearExchangeTiles());
|
||||||
|
}}>Select None</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<TilesExchangedTray
|
||||||
|
tray={props.playerLetters}
|
||||||
|
selectedArray={tilesToExchange}
|
||||||
|
setSelectedArray={setTilesToExchange}
|
||||||
|
trayLength={props.playerLetters.length}
|
||||||
|
/>
|
||||||
|
<div className="finish-buttons">
|
||||||
|
<button onClick={() => {
|
||||||
|
setTilesToExchange(clearExchangeTiles());
|
||||||
|
props.setOpen(false);
|
||||||
|
}}>Cancel</button>
|
||||||
|
<button disabled = {tilesExchangedSelected == 0} onClick={(e) => {
|
||||||
|
props.exchangeFunction(tilesToExchange);
|
||||||
|
props.setOpen(false);
|
||||||
|
}}>Exchange</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function TilesExchangedTray(props: {
|
||||||
|
tray: Array<PlayableLetterData>,
|
||||||
|
trayLength: number,
|
||||||
|
selectedArray: Array<boolean>,
|
||||||
|
setSelectedArray: (x: Array<boolean>) => void,
|
||||||
|
}){
|
||||||
|
|
||||||
|
const divContent = [];
|
||||||
|
for(let i=0; i<props.trayLength; i++) {
|
||||||
|
divContent.push(<span key={i} />); // empty tile elements
|
||||||
|
}
|
||||||
|
|
||||||
|
for(let i = 0; i<props.trayLength; i++){
|
||||||
|
const tileData = props.tray[i];
|
||||||
|
|
||||||
|
const toggleFunction = () => {
|
||||||
|
props.selectedArray[i] = !props.selectedArray[i];
|
||||||
|
props.setSelectedArray(props.selectedArray.slice());
|
||||||
|
}
|
||||||
|
|
||||||
|
if(tileData != null){
|
||||||
|
divContent[tileData.index] =
|
||||||
|
<DummyExchangeTile key={tileData.index} letter={tileData} isSelected={props.selectedArray[i]} onClick={toggleFunction}/>;
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className="tray">
|
||||||
|
{divContent}
|
||||||
|
</div>;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function DummyExchangeTile(props: {
|
||||||
|
letter: LetterData,
|
||||||
|
isSelected: boolean,
|
||||||
|
onClick: () => void,
|
||||||
|
}){
|
||||||
|
|
||||||
|
let textClassName = "text";
|
||||||
|
if(props.letter.is_blank) {
|
||||||
|
textClassName += " prev-blank";
|
||||||
|
}
|
||||||
|
|
||||||
|
let letterClassName = "letter";
|
||||||
|
if(props.isSelected){
|
||||||
|
letterClassName += ' selected-tile';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return <div className={letterClassName} onClick={props.onClick}>
|
||||||
|
<div className={textClassName}>{props.letter.text}</div>
|
||||||
|
<div className="letter-points">{props.letter.points}</div>
|
||||||
|
</div>;
|
||||||
|
}
|
196
ui/src/UI.tsx
Normal file
196
ui/src/UI.tsx
Normal file
|
@ -0,0 +1,196 @@
|
||||||
|
import * as React from "react";
|
||||||
|
import {Letter as LetterData, PlayerAndScore} from "word_grid";
|
||||||
|
import {ChangeEvent, JSX} from "react";
|
||||||
|
import {
|
||||||
|
cellTypeToDetails,
|
||||||
|
CellType,
|
||||||
|
LocationType,
|
||||||
|
PlayableLetterData,
|
||||||
|
CoordinateData, TileDispatchActionType, TileDispatch,
|
||||||
|
} from "./utils";
|
||||||
|
|
||||||
|
|
||||||
|
export function TileSlot(props: { tile?: React.JSX.Element | undefined, location: CoordinateData, dispatch: TileDispatch }): React.JSX.Element {
|
||||||
|
let isDraggable = props.tile !== undefined;
|
||||||
|
|
||||||
|
function onDragStart(e: React.DragEvent<HTMLDivElement>) {
|
||||||
|
e.dataTransfer.effectAllowed = "move";
|
||||||
|
e.dataTransfer.setData("wordGrid/coords", JSON.stringify(props.location));
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDrop(e: React.DragEvent<HTMLDivElement>) {
|
||||||
|
const startLocation: CoordinateData = JSON.parse(e.dataTransfer.getData("wordGrid/coords"));
|
||||||
|
const thisLocation = props.location;
|
||||||
|
|
||||||
|
props.dispatch({action: TileDispatchActionType.MOVE, start: startLocation, end: thisLocation});
|
||||||
|
}
|
||||||
|
|
||||||
|
let className = "tileSpot";
|
||||||
|
if (props.location.location === LocationType.GRID) {
|
||||||
|
className += " ephemeral";
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div className={className}
|
||||||
|
draggable={isDraggable}
|
||||||
|
onDragStart={onDragStart}
|
||||||
|
onDrop={onDrop}
|
||||||
|
onDragOver={(e) => {e.preventDefault()}}
|
||||||
|
>
|
||||||
|
{props.tile}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Letter(props: { data: LetterData, letterUpdater?: (value: string) => void }): React.JSX.Element {
|
||||||
|
|
||||||
|
function modifyThisLetter(value:string){
|
||||||
|
props.letterUpdater(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function onBlankInput(e: ChangeEvent<HTMLInputElement>){
|
||||||
|
let value = e.target.value.toUpperCase();
|
||||||
|
if(value.length > 1){
|
||||||
|
value = value[value.length - 1];
|
||||||
|
} else if(value.length == 0){
|
||||||
|
modifyThisLetter(value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now check that it's a letter
|
||||||
|
let is_letter = value.match("[A-Z]") != null;
|
||||||
|
if(is_letter){
|
||||||
|
modifyThisLetter(value);
|
||||||
|
} else {
|
||||||
|
// Cancel and do nothing
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
if(props.data.is_blank && props.data.ephemeral) {
|
||||||
|
return <div className="letter">
|
||||||
|
<input className="blank-input" type="text" onChange={onBlankInput} value={props.data.text} />
|
||||||
|
<div className="letter-points">{props.data.points}</div>
|
||||||
|
</div>
|
||||||
|
} else {
|
||||||
|
let className = "text";
|
||||||
|
if (props.data.is_blank) { // not ephemeral
|
||||||
|
className += " prev-blank";
|
||||||
|
}
|
||||||
|
return <div className="letter">
|
||||||
|
<div className={className}>{props.data.text}</div>
|
||||||
|
<div className="letter-points">{props.data.points}</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export function TileTray(props: { letters: Array<PlayableLetterData>, trayLength: number, dispatch: TileDispatch }): React.JSX.Element {
|
||||||
|
|
||||||
|
let elements: JSX.Element[] = [];
|
||||||
|
for (let i=0; i<props.trayLength; i++) {
|
||||||
|
elements.push(
|
||||||
|
<TileSlot
|
||||||
|
key={"empty" + i}
|
||||||
|
location={{location: LocationType.TRAY, index: i}}
|
||||||
|
dispatch={props.dispatch} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
props.letters
|
||||||
|
.filter((x) => {return x !== undefined;})
|
||||||
|
.forEach((letter, i) => {
|
||||||
|
if (letter.location === LocationType.TRAY) {
|
||||||
|
elements[letter.index] =
|
||||||
|
<TileSlot
|
||||||
|
tile={<Letter
|
||||||
|
data={letter}
|
||||||
|
letterUpdater={(value) => {
|
||||||
|
props.dispatch({action: TileDispatchActionType.SET_BLANK, blankIndex: i, newBlankValue: value})
|
||||||
|
}}
|
||||||
|
/>}
|
||||||
|
key={"letter" + letter.index}
|
||||||
|
location={{location: LocationType.TRAY, index: letter.index}}
|
||||||
|
dispatch={props.dispatch} />
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="tray">
|
||||||
|
{elements}
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export function Grid(props: {
|
||||||
|
cellTypes: CellType[],
|
||||||
|
playerLetters: Array<PlayableLetterData>,
|
||||||
|
boardLetters: LetterData[],
|
||||||
|
dispatch: TileDispatch}) {
|
||||||
|
|
||||||
|
const elements = props.cellTypes.map((ct, i) => {
|
||||||
|
const {className, text} = cellTypeToDetails(ct);
|
||||||
|
|
||||||
|
let tileElement: JSX.Element;
|
||||||
|
if (props.boardLetters[i] !== undefined) {
|
||||||
|
tileElement = <Letter data={props.boardLetters[i]} />;
|
||||||
|
} else {
|
||||||
|
tileElement = <>
|
||||||
|
<span>{text}</span>
|
||||||
|
<TileSlot
|
||||||
|
location={{location: LocationType.GRID, index: i}}
|
||||||
|
dispatch={props.dispatch} /></>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <div key={i} className={"grid-spot " + className}>
|
||||||
|
|
||||||
|
{tileElement}
|
||||||
|
</div>;
|
||||||
|
});
|
||||||
|
|
||||||
|
props.playerLetters
|
||||||
|
.filter((letter) => {return letter !== undefined})
|
||||||
|
.forEach((letter, i) => {
|
||||||
|
if (letter.location === LocationType.GRID) {
|
||||||
|
const ct = props.cellTypes[letter.index];
|
||||||
|
const {className, text} = cellTypeToDetails(ct);
|
||||||
|
|
||||||
|
elements[letter.index] =
|
||||||
|
<div key={"letter" + letter.index} className={"grid-spot " + className}>
|
||||||
|
<TileSlot
|
||||||
|
tile={<Letter
|
||||||
|
data={letter}
|
||||||
|
letterUpdater={(value) => {
|
||||||
|
props.dispatch({action: TileDispatchActionType.SET_BLANK, blankIndex: i, newBlankValue: value});
|
||||||
|
}}
|
||||||
|
/>}
|
||||||
|
location={{location: LocationType.GRID, index: letter.index}}
|
||||||
|
dispatch={props.dispatch} />
|
||||||
|
</div>;
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return <div className="board-grid">
|
||||||
|
{elements}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export 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>
|
||||||
|
}
|
|
@ -1,590 +0,0 @@
|
||||||
import * as React from "react";
|
|
||||||
import {GameWasm, Letter as LetterData, MyResult, PlayedTile, PlayerAndScore, Tray, WordResult} from "word_grid";
|
|
||||||
import {ChangeEvent, JSX, useEffect, useMemo, useReducer, useRef, useState} from "react";
|
|
||||||
import {Modal} from "./Modal";
|
|
||||||
import {addNTimes, mergeTrays} from "./utils";
|
|
||||||
|
|
||||||
export interface Settings {
|
|
||||||
trayLength: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum LocationType {
|
|
||||||
GRID,
|
|
||||||
TRAY
|
|
||||||
}
|
|
||||||
|
|
||||||
enum CellType {
|
|
||||||
Normal = "Normal",
|
|
||||||
DoubleWord = "DoubleWord",
|
|
||||||
DoubleLetter = "DoubleLetter",
|
|
||||||
TripleLetter = "TripleLetter",
|
|
||||||
TripleWord = "TripleWord",
|
|
||||||
Start = "Start",
|
|
||||||
}
|
|
||||||
|
|
||||||
function cell_type_to_details(cell_type: CellType): {className: string, text: string} {
|
|
||||||
let className: string;
|
|
||||||
let text: string;
|
|
||||||
|
|
||||||
switch (cell_type) {
|
|
||||||
case CellType.Normal:
|
|
||||||
className = "grid-spot-normal";
|
|
||||||
text = "";
|
|
||||||
break;
|
|
||||||
case CellType.DoubleWord:
|
|
||||||
className = "grid-spot-double-word";
|
|
||||||
text = "Double Word Score";
|
|
||||||
break;
|
|
||||||
case CellType.DoubleLetter:
|
|
||||||
className = "grid-spot-double-letter";
|
|
||||||
text = "Double Letter Score";
|
|
||||||
break;
|
|
||||||
case CellType.TripleLetter:
|
|
||||||
className = "grid-spot-triple-letter";
|
|
||||||
text = "Triple Letter Score";
|
|
||||||
break;
|
|
||||||
case CellType.TripleWord:
|
|
||||||
className = "grid-spot-triple-word";
|
|
||||||
text = "Triple Word Score";
|
|
||||||
break;
|
|
||||||
case CellType.Start:
|
|
||||||
className = "grid-spot-start";
|
|
||||||
text = "★";
|
|
||||||
break;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
return {className: className, text: text};
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CoordinateData {
|
|
||||||
location: LocationType;
|
|
||||||
index: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type PlayableLetterData = LetterData & CoordinateData;
|
|
||||||
|
|
||||||
function matchCoordinate(playerLetters: PlayableLetterData[], coords: CoordinateData): number {
|
|
||||||
|
|
||||||
for (let i=0; i<playerLetters.length; i++){
|
|
||||||
let letter = playerLetters[i];
|
|
||||||
|
|
||||||
if (letter !== undefined && letter.location === coords.location && letter.index === coords.index) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return null; // no match
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
enum TileDispatchActionType {
|
|
||||||
MOVE,
|
|
||||||
RETRIEVE,
|
|
||||||
SET_BLANK,
|
|
||||||
RETURN,
|
|
||||||
}
|
|
||||||
type TileDispatchAction = {action: TileDispatchActionType, start?: CoordinateData, end?: CoordinateData, newBlankValue?: string, blankIndex?: number};
|
|
||||||
type TileDispatch = React.Dispatch<TileDispatchAction>;
|
|
||||||
|
|
||||||
function addLogInfo(existingLog: React.JSX.Element[], newItem: React.JSX.Element) {
|
|
||||||
newItem = React.cloneElement(newItem, { key: existingLog.length })
|
|
||||||
existingLog.push(newItem);
|
|
||||||
return existingLog.slice();
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Game(props: {wasm: GameWasm, settings: Settings}) {
|
|
||||||
|
|
||||||
const cellTypes = useMemo(() => {
|
|
||||||
return props.wasm.get_board_cell_types();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [confirmedScorePoints, setConfirmedScorePoints] = useState<number>(-1);
|
|
||||||
|
|
||||||
function movePlayableLetters(playerLetters: PlayableLetterData[], update: TileDispatchAction) {
|
|
||||||
|
|
||||||
if(update.action === TileDispatchActionType.RETRIEVE) {
|
|
||||||
|
|
||||||
let tray: Tray = props.wasm.get_tray("Player");
|
|
||||||
|
|
||||||
return mergeTrays(playerLetters, tray.letters);
|
|
||||||
} else if (update.action === TileDispatchActionType.MOVE) {
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
setConfirmedScorePoints(-1);
|
|
||||||
|
|
||||||
return playerLetters.slice();
|
|
||||||
} if (update.action === TileDispatchActionType.SET_BLANK) {
|
|
||||||
const blankLetter = playerLetters[update.blankIndex];
|
|
||||||
if(blankLetter.text !== update.newBlankValue) {
|
|
||||||
blankLetter.text = update.newBlankValue;
|
|
||||||
if (blankLetter.location == LocationType.GRID) {
|
|
||||||
setConfirmedScorePoints(-1);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return playerLetters.slice();
|
|
||||||
} else if (update.action === TileDispatchActionType.RETURN) {
|
|
||||||
return mergeTrays(playerLetters, playerLetters);
|
|
||||||
} else {
|
|
||||||
console.error("Unknown tray update");
|
|
||||||
console.error({update});
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
const [playerLetters, trayDispatch] = useReducer(movePlayableLetters, []);
|
|
||||||
const [logInfo, logDispatch] = useReducer(addLogInfo, []);
|
|
||||||
|
|
||||||
const [turnCount, setTurnCount] = useState<number>(1);
|
|
||||||
const playerAndScores: PlayerAndScore[] = useMemo(() => {
|
|
||||||
return props.wasm.get_scores();
|
|
||||||
}, [turnCount]);
|
|
||||||
const boardLetters: LetterData[] = useMemo(() => {
|
|
||||||
return props.wasm.get_board_letters();
|
|
||||||
}, [turnCount]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
logDispatch(<h4>Turn {turnCount}</h4>);
|
|
||||||
setConfirmedScorePoints(-1);
|
|
||||||
trayDispatch({action: TileDispatchActionType.RETRIEVE});
|
|
||||||
|
|
||||||
}, [turnCount]);
|
|
||||||
|
|
||||||
const logDivRef = useRef(null);
|
|
||||||
|
|
||||||
const [isTileExchangeOpen, setIsTileExchangeOpen] = useState<boolean>(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Effect is to keep the log window scrolled down
|
|
||||||
if (logDivRef.current != null) {
|
|
||||||
logDivRef.current.scrollTo(0, logDivRef.current.scrollHeight); // scroll down
|
|
||||||
}
|
|
||||||
}, [logInfo])
|
|
||||||
|
|
||||||
function exchangeFunction(selectedArray: Array<boolean>) {
|
|
||||||
|
|
||||||
let numSelected = 0;
|
|
||||||
selectedArray.forEach((x) => {
|
|
||||||
if (x){
|
|
||||||
numSelected++;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const result: MyResult<Tray | string> = props.wasm.exchange_tiles("Player", selectedArray);
|
|
||||||
console.log({result});
|
|
||||||
|
|
||||||
if(result.response_type === "ERR") {
|
|
||||||
logDispatch(<div><em>{(result.value as string)}</em></div>);
|
|
||||||
} else {
|
|
||||||
logDispatch(<div><em>You exchanged {numSelected} tiles.</em></div>);
|
|
||||||
setTurnCount(turnCount + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return <>
|
|
||||||
<TileExchangeModal
|
|
||||||
playerLetters={playerLetters}
|
|
||||||
isOpen={isTileExchangeOpen}
|
|
||||||
setOpen={setIsTileExchangeOpen}
|
|
||||||
exchangeFunction={exchangeFunction}
|
|
||||||
/>
|
|
||||||
<div className="board-log">
|
|
||||||
<Grid cellTypes={cellTypes} playerLetters={playerLetters} boardLetters={boardLetters} dispatch={trayDispatch}/>
|
|
||||||
<div className="message-log">
|
|
||||||
<Scores playerScores={playerAndScores}/>
|
|
||||||
<div className="log" ref={logDivRef}>
|
|
||||||
{logInfo}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TileTray letters={playerLetters} trayLength={props.settings.trayLength} dispatch={trayDispatch}/>
|
|
||||||
<button onClick={(e) => {
|
|
||||||
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: MyResult<Array<WordResult> | string> = props.wasm.receive_play("Player", playedTiles, confirmedScorePoints > -1);
|
|
||||||
console.log({result});
|
|
||||||
|
|
||||||
if(result.response_type === "ERR") {
|
|
||||||
logDispatch(<div><em>{(result.value as string)}</em></div>);
|
|
||||||
} else {
|
|
||||||
|
|
||||||
let total_points = 0;
|
|
||||||
for (let word_result of (result.value as Array<WordResult>)) {
|
|
||||||
total_points += word_result.score;
|
|
||||||
}
|
|
||||||
|
|
||||||
let msg: string;
|
|
||||||
if (confirmedScorePoints > -1) {
|
|
||||||
msg = `You scored ${total_points} points.`;
|
|
||||||
setTurnCount(turnCount + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
logDispatch(<div><em>{msg}</em></div>);
|
|
||||||
|
|
||||||
setConfirmedScorePoints(total_points);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}}>{confirmedScorePoints > -1 ? `Score ${confirmedScorePoints} points ✅` : "Check"}</button>
|
|
||||||
<button
|
|
||||||
onClick={(e) => {
|
|
||||||
trayDispatch({action: TileDispatchActionType.RETURN}); // want all tiles back on tray for tile exchange
|
|
||||||
setIsTileExchangeOpen(true);
|
|
||||||
}}
|
|
||||||
>Open Tile Exchange</button>
|
|
||||||
<button onClick={(e) => {
|
|
||||||
trayDispatch({action: TileDispatchActionType.RETURN});
|
|
||||||
}}>Return Tiles</button>
|
|
||||||
</>;
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export function TileSlot(props: { tile?: React.JSX.Element | undefined, location: CoordinateData, dispatch: TileDispatch }): React.JSX.Element {
|
|
||||||
let isDraggable = props.tile !== undefined;
|
|
||||||
|
|
||||||
function onDragStart(e: React.DragEvent<HTMLDivElement>) {
|
|
||||||
e.dataTransfer.effectAllowed = "move";
|
|
||||||
e.dataTransfer.setData("wordGrid/coords", JSON.stringify(props.location));
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDrop(e: React.DragEvent<HTMLDivElement>) {
|
|
||||||
const startLocation: CoordinateData = JSON.parse(e.dataTransfer.getData("wordGrid/coords"));
|
|
||||||
const thisLocation = props.location;
|
|
||||||
|
|
||||||
props.dispatch({action: TileDispatchActionType.MOVE, start: startLocation, end: thisLocation});
|
|
||||||
}
|
|
||||||
|
|
||||||
let className = "tileSpot";
|
|
||||||
if (props.location.location === LocationType.GRID) {
|
|
||||||
className += " ephemeral";
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className={className}
|
|
||||||
draggable={isDraggable}
|
|
||||||
onDragStart={onDragStart}
|
|
||||||
onDrop={onDrop}
|
|
||||||
onDragOver={(e) => {e.preventDefault()}}
|
|
||||||
>
|
|
||||||
{props.tile}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export function Letter(props: { data: LetterData, letterUpdater?: (value: string) => void }): React.JSX.Element {
|
|
||||||
|
|
||||||
function modifyThisLetter(value:string){
|
|
||||||
props.letterUpdater(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
function onBlankInput(e: ChangeEvent<HTMLInputElement>){
|
|
||||||
let value = e.target.value.toUpperCase();
|
|
||||||
if(value.length > 1){
|
|
||||||
value = value[value.length - 1];
|
|
||||||
} else if(value.length == 0){
|
|
||||||
modifyThisLetter(value);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now check that it's a letter
|
|
||||||
let is_letter = value.match("[A-Z]") != null;
|
|
||||||
if(is_letter){
|
|
||||||
modifyThisLetter(value);
|
|
||||||
} else {
|
|
||||||
// Cancel and do nothing
|
|
||||||
e.preventDefault();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
if(props.data.is_blank && props.data.ephemeral) {
|
|
||||||
return <div className="letter">
|
|
||||||
<input className="blank-input" type="text" onChange={onBlankInput} value={props.data.text} />
|
|
||||||
<div className="letter-points">{props.data.points}</div>
|
|
||||||
</div>
|
|
||||||
} else {
|
|
||||||
let className = "text";
|
|
||||||
if (props.data.is_blank) { // not ephemeral
|
|
||||||
className += " prev-blank";
|
|
||||||
}
|
|
||||||
return <div className="letter">
|
|
||||||
<div className={className}>{props.data.text}</div>
|
|
||||||
<div className="letter-points">{props.data.points}</div>
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
export function TileTray(props: { letters: Array<PlayableLetterData>, trayLength: number, dispatch: TileDispatch }): React.JSX.Element {
|
|
||||||
|
|
||||||
let elements: JSX.Element[] = [];
|
|
||||||
for (let i=0; i<props.trayLength; i++) {
|
|
||||||
elements.push(
|
|
||||||
<TileSlot
|
|
||||||
key={"empty" + i}
|
|
||||||
location={{location: LocationType.TRAY, index: i}}
|
|
||||||
dispatch={props.dispatch} />
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
props.letters
|
|
||||||
.filter((x) => {return x !== undefined;})
|
|
||||||
.forEach((letter, i) => {
|
|
||||||
if (letter.location === LocationType.TRAY) {
|
|
||||||
elements[letter.index] =
|
|
||||||
<TileSlot
|
|
||||||
tile={<Letter
|
|
||||||
data={letter}
|
|
||||||
letterUpdater={(value) => {
|
|
||||||
props.dispatch({action: TileDispatchActionType.SET_BLANK, blankIndex: i, newBlankValue: value})
|
|
||||||
}}
|
|
||||||
/>}
|
|
||||||
key={"letter" + letter.index}
|
|
||||||
location={{location: LocationType.TRAY, index: letter.index}}
|
|
||||||
dispatch={props.dispatch} />
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="tray">
|
|
||||||
{elements}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function Grid(props: {
|
|
||||||
cellTypes: CellType[],
|
|
||||||
playerLetters: Array<PlayableLetterData>,
|
|
||||||
boardLetters: LetterData[],
|
|
||||||
dispatch: TileDispatch}) {
|
|
||||||
|
|
||||||
const elements = props.cellTypes.map((ct, i) => {
|
|
||||||
const {className, text} = cell_type_to_details(ct);
|
|
||||||
|
|
||||||
let tileElement: JSX.Element;
|
|
||||||
if (props.boardLetters[i] !== undefined) {
|
|
||||||
tileElement = <Letter data={props.boardLetters[i]} />;
|
|
||||||
} else {
|
|
||||||
tileElement = <>
|
|
||||||
<span>{text}</span>
|
|
||||||
<TileSlot
|
|
||||||
location={{location: LocationType.GRID, index: i}}
|
|
||||||
dispatch={props.dispatch} /></>;
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div key={i} className={"grid-spot " + className}>
|
|
||||||
|
|
||||||
{tileElement}
|
|
||||||
</div>;
|
|
||||||
});
|
|
||||||
|
|
||||||
props.playerLetters
|
|
||||||
.filter((letter) => {return letter !== undefined})
|
|
||||||
.forEach((letter, i) => {
|
|
||||||
if (letter.location === LocationType.GRID) {
|
|
||||||
const ct = props.cellTypes[letter.index];
|
|
||||||
const {className, text} = cell_type_to_details(ct);
|
|
||||||
|
|
||||||
elements[letter.index] =
|
|
||||||
<div key={"letter" + letter.index} className={"grid-spot " + className}>
|
|
||||||
<TileSlot
|
|
||||||
tile={<Letter
|
|
||||||
data={letter}
|
|
||||||
letterUpdater={(value) => {
|
|
||||||
props.dispatch({action: TileDispatchActionType.SET_BLANK, blankIndex: i, newBlankValue: value});
|
|
||||||
}}
|
|
||||||
/>}
|
|
||||||
location={{location: LocationType.GRID, index: letter.index}}
|
|
||||||
dispatch={props.dispatch} />
|
|
||||||
</div>;
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return <div className="board-grid">
|
|
||||||
{elements}
|
|
||||||
</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>
|
|
||||||
}
|
|
||||||
|
|
||||||
function TileExchangeModal(props: {
|
|
||||||
playerLetters: PlayableLetterData[],
|
|
||||||
isOpen: boolean,
|
|
||||||
setOpen: (isOpen: boolean) => void,
|
|
||||||
exchangeFunction: (selectedArray: Array<boolean>) => void
|
|
||||||
}) {
|
|
||||||
|
|
||||||
function clearExchangeTiles() {
|
|
||||||
const array: boolean[] = [];
|
|
||||||
addNTimes(array, false, props.playerLetters.length);
|
|
||||||
return array;
|
|
||||||
}
|
|
||||||
|
|
||||||
const [tilesToExchange, setTilesToExchange] = useState<boolean[]>(clearExchangeTiles);
|
|
||||||
useEffect(() => {
|
|
||||||
setTilesToExchange(clearExchangeTiles());
|
|
||||||
}, [props.playerLetters])
|
|
||||||
|
|
||||||
let tilesExchangedSelected = 0;
|
|
||||||
for (let i of tilesToExchange) {
|
|
||||||
if(i) {
|
|
||||||
tilesExchangedSelected++;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return <Modal isOpen={props.isOpen} setOpen={(isOpen) => {
|
|
||||||
setTilesToExchange(clearExchangeTiles());
|
|
||||||
props.setOpen(isOpen);
|
|
||||||
}}>
|
|
||||||
<div className="tile-exchange-dialog">
|
|
||||||
<div className="instructions">
|
|
||||||
Click on each tile you'd like to exchange. You currently have {tilesExchangedSelected} tiles selected.
|
|
||||||
</div>
|
|
||||||
<div className="selection-buttons">
|
|
||||||
<button onClick={(e) => {
|
|
||||||
const array: boolean[] = [];
|
|
||||||
addNTimes(array, true, props.playerLetters.length);
|
|
||||||
setTilesToExchange(array);
|
|
||||||
}}>Select All</button>
|
|
||||||
<button onClick={(e) => {
|
|
||||||
setTilesToExchange(clearExchangeTiles());
|
|
||||||
}}>Select None</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<TilesExchangedTray
|
|
||||||
tray={props.playerLetters}
|
|
||||||
selectedArray={tilesToExchange}
|
|
||||||
setSelectedArray={setTilesToExchange}
|
|
||||||
trayLength={props.playerLetters.length}
|
|
||||||
/>
|
|
||||||
<div className="finish-buttons">
|
|
||||||
<button onClick={() => {
|
|
||||||
setTilesToExchange(clearExchangeTiles());
|
|
||||||
props.setOpen(false);
|
|
||||||
}}>Cancel</button>
|
|
||||||
<button disabled = {tilesExchangedSelected == 0} onClick={(e) => {
|
|
||||||
props.exchangeFunction(tilesToExchange);
|
|
||||||
props.setOpen(false);
|
|
||||||
}}>Exchange</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function TilesExchangedTray(props: {
|
|
||||||
tray: Array<PlayableLetterData>,
|
|
||||||
trayLength: number,
|
|
||||||
selectedArray: Array<boolean>,
|
|
||||||
setSelectedArray: (x: Array<boolean>) => void,
|
|
||||||
}){
|
|
||||||
|
|
||||||
const divContent = [];
|
|
||||||
for(let i=0; i<props.trayLength; i++) {
|
|
||||||
divContent.push(<span key={i} />); // empty tile elements
|
|
||||||
}
|
|
||||||
|
|
||||||
for(let i = 0; i<props.trayLength; i++){
|
|
||||||
const tileData = props.tray[i];
|
|
||||||
|
|
||||||
const toggleFunction = () => {
|
|
||||||
props.selectedArray[i] = !props.selectedArray[i];
|
|
||||||
props.setSelectedArray(props.selectedArray.slice());
|
|
||||||
}
|
|
||||||
|
|
||||||
if(tileData != null){
|
|
||||||
divContent[tileData.index] =
|
|
||||||
<DummyExchangeTile key={tileData.index} letter={tileData} isSelected={props.selectedArray[i]} onClick={toggleFunction}/>;
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return <div className="tray">
|
|
||||||
{divContent}
|
|
||||||
</div>;
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
function DummyExchangeTile(props: {
|
|
||||||
letter: LetterData,
|
|
||||||
isSelected: boolean,
|
|
||||||
onClick: () => void,
|
|
||||||
}){
|
|
||||||
|
|
||||||
let textClassName = "text";
|
|
||||||
if(props.letter.is_blank) {
|
|
||||||
textClassName += " prev-blank";
|
|
||||||
}
|
|
||||||
|
|
||||||
let letterClassName = "letter";
|
|
||||||
if(props.isSelected){
|
|
||||||
letterClassName += ' selected-tile';
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
return <div className={letterClassName} onClick={props.onClick}>
|
|
||||||
<div className={textClassName}>{props.letter.text}</div>
|
|
||||||
<div className="letter-points">{props.letter.points}</div>
|
|
||||||
</div>;
|
|
||||||
}
|
|
|
@ -1,5 +1,5 @@
|
||||||
import init, {greet, GameWasm} from '../node_modules/word_grid/word_grid.js';
|
import init, {GameWasm} from '../node_modules/word_grid/word_grid.js';
|
||||||
import {Game} from "./elements";
|
import {Game} from "./Game";
|
||||||
import {createRoot} from "react-dom/client";
|
import {createRoot} from "react-dom/client";
|
||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
|
|
||||||
|
|
|
@ -1,5 +1,89 @@
|
||||||
import {Letter} from "word_grid";
|
import {Letter as LetterData, Letter} from "word_grid";
|
||||||
import {LocationType, PlayableLetterData} from "./elements";
|
import * as React from "react";
|
||||||
|
|
||||||
|
export enum CellType {
|
||||||
|
Normal = "Normal",
|
||||||
|
DoubleWord = "DoubleWord",
|
||||||
|
DoubleLetter = "DoubleLetter",
|
||||||
|
TripleLetter = "TripleLetter",
|
||||||
|
TripleWord = "TripleWord",
|
||||||
|
Start = "Start",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface Settings {
|
||||||
|
trayLength: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum LocationType {
|
||||||
|
GRID,
|
||||||
|
TRAY
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CoordinateData {
|
||||||
|
location: LocationType;
|
||||||
|
index: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PlayableLetterData = LetterData & CoordinateData;
|
||||||
|
|
||||||
|
export enum TileDispatchActionType {
|
||||||
|
MOVE,
|
||||||
|
RETRIEVE,
|
||||||
|
SET_BLANK,
|
||||||
|
RETURN,
|
||||||
|
}
|
||||||
|
export type TileDispatchAction = {action: TileDispatchActionType, start?: CoordinateData, end?: CoordinateData, newBlankValue?: string, blankIndex?: number};
|
||||||
|
export type TileDispatch = React.Dispatch<TileDispatchAction>;
|
||||||
|
|
||||||
|
export function matchCoordinate(playerLetters: PlayableLetterData[], coords: CoordinateData): number {
|
||||||
|
|
||||||
|
for (let i=0; i<playerLetters.length; i++){
|
||||||
|
let letter = playerLetters[i];
|
||||||
|
|
||||||
|
if (letter !== undefined && letter.location === coords.location && letter.index === coords.index) {
|
||||||
|
return i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null; // no match
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export function cellTypeToDetails(cell_type: CellType): {className: string, text: string} {
|
||||||
|
let className: string;
|
||||||
|
let text: string;
|
||||||
|
|
||||||
|
switch (cell_type) {
|
||||||
|
case CellType.Normal:
|
||||||
|
className = "grid-spot-normal";
|
||||||
|
text = "";
|
||||||
|
break;
|
||||||
|
case CellType.DoubleWord:
|
||||||
|
className = "grid-spot-double-word";
|
||||||
|
text = "Double Word Score";
|
||||||
|
break;
|
||||||
|
case CellType.DoubleLetter:
|
||||||
|
className = "grid-spot-double-letter";
|
||||||
|
text = "Double Letter Score";
|
||||||
|
break;
|
||||||
|
case CellType.TripleLetter:
|
||||||
|
className = "grid-spot-triple-letter";
|
||||||
|
text = "Triple Letter Score";
|
||||||
|
break;
|
||||||
|
case CellType.TripleWord:
|
||||||
|
className = "grid-spot-triple-word";
|
||||||
|
text = "Triple Word Score";
|
||||||
|
break;
|
||||||
|
case CellType.Start:
|
||||||
|
className = "grid-spot-start";
|
||||||
|
text = "★";
|
||||||
|
break;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return {className: className, text: text};
|
||||||
|
}
|
||||||
|
|
||||||
export function addNTimes<T>(array: T[], toAdd: T, times: number) {
|
export function addNTimes<T>(array: T[], toAdd: T, times: number) {
|
||||||
for (let i=0; i<times; i++) {
|
for (let i=0; i<times; i++) {
|
||||||
|
|
Loading…
Reference in a new issue