Split UI code into separate files

This commit is contained in:
Joel Therrien 2023-08-23 20:46:43 -07:00
parent de2605af67
commit 974751bda0
6 changed files with 616 additions and 594 deletions

206
ui/src/Game.tsx Normal file
View 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
View 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
View 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>
}

View file

@ -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>;
}

View file

@ -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";

View file

@ -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++) {