Add arrow for fast play

This commit is contained in:
Joel Therrien 2023-09-22 18:24:05 -07:00
parent a34ad8cd12
commit 2fa28ce3d4
4 changed files with 234 additions and 54 deletions

View file

@ -1,15 +1,22 @@
import * as React from "react"; import * as React from "react";
import {useEffect, useMemo, useReducer, useRef, useState} from "react";
import { import {
GameState, GameState,
GameWasm, Letter, GameWasm,
Letter as LetterData,
MyResult, MyResult,
PlayedTile, PlayedTile,
PlayerAndScore, PlayerAndScore,
ScoreResult, ScoreResult,
Tray, TurnAction, TurnAdvanceResult Tray,
TurnAction,
TurnAdvanceResult
} from "../../pkg/word_grid"; } from "../../pkg/word_grid";
import { import {
Direction,
GRID_LENGTH,
GridArrowData,
GridArrowDispatchAction,
GridArrowDispatchActionType,
HighlightableLetterData, HighlightableLetterData,
LocationType, LocationType,
matchCoordinate, matchCoordinate,
@ -19,7 +26,6 @@ import {
TileDispatchAction, TileDispatchAction,
TileDispatchActionType TileDispatchActionType
} from "./utils"; } from "./utils";
import {useEffect, useMemo, useReducer, useRef, useState} from "react";
import {TileExchangeModal} from "./TileExchange"; import {TileExchangeModal} from "./TileExchange";
import {Grid, Scores, TileTray} from "./UI"; import {Grid, Scores, TileTray} from "./UI";
@ -42,6 +48,94 @@ export function Game(props: {
const [isGameOver, setGameOver] = useState<boolean>(false); const [isGameOver, setGameOver] = useState<boolean>(false);
const [confirmedScorePoints, setConfirmedScorePoints] = useState<number>(-1); const [confirmedScorePoints, setConfirmedScorePoints] = useState<number>(-1);
const [boardLetters, setBoardLetters] = useState<HighlightableLetterData[]>(() => {
const newLetterData = [] as HighlightableLetterData[];
for(let i=0; i<GRID_LENGTH * GRID_LENGTH; i++) {
newLetterData.push(undefined);
}
return newLetterData;
});
function adjustGridArrow(existing: GridArrowData, update: GridArrowDispatchAction): GridArrowData {
console.log({update});
if(update.action == GridArrowDispatchActionType.CLEAR) {
return null;
} else if (update.action == GridArrowDispatchActionType.CYCLE) {
// if there's nothing where the user clicked, we create a right arrow.
if(existing == null || existing.position != update.position) {
return {
direction: Direction.RIGHT, position: update.position
}
// if there's a right arrow, we shift to downwards
} else if(existing.direction == Direction.RIGHT) {
return {
direction: Direction.DOWN, position: existing.position
}
// if there's a down arrow, we clear it
} else if (existing.direction == Direction.DOWN){
return null;
}
} else if (update.action == GridArrowDispatchActionType.SHIFT) {
if(existing == null) {
// no arrow to shift
return null;
} else {
let current_x = existing.position % GRID_LENGTH;
let current_y = Math.floor(existing.position / GRID_LENGTH);
// we loop because we want to skip over letters that are already set
while (current_x < GRID_LENGTH && current_y < GRID_LENGTH) {
if(existing.direction == Direction.RIGHT) {
current_x += 1;
} else {
current_y += 1;
}
const new_position = current_x + current_y * GRID_LENGTH;
if(current_x < GRID_LENGTH && current_y < GRID_LENGTH && boardLetters[new_position] == null) {
return {
direction: existing.direction,
position: new_position,
}
}
}
// if we reached this point without returning then we went off the board, remove arrow
return null;
}
}
}
function exchangeFunction(selectedArray: Array<boolean>) {
const result: MyResult<TurnAction | string> = props.wasm.exchange_tiles(selectedArray);
if(result.response_type === "ERR") {
logDispatch(<div><em>{(result.value as string)}</em></div>);
} else {
handlePlayerAction(result.value as TurnAction, props.settings.playerName);
setTurnCount(turnCount + 1);
if(result.game_state.type === "Ended") {
endGame(result.game_state);
}
}
}
function addWordFn(word: string) {
props.wasm.add_word(word);
}
const [gridArrow, gridArrowDispatch] = useReducer(adjustGridArrow, null);
const [logInfo, logDispatch] = useReducer(addLogInfo, []);
function movePlayableLetters(playerLetters: PlayableLetterData[], update: TileDispatchAction) { function movePlayableLetters(playerLetters: PlayableLetterData[], update: TileDispatchAction) {
if(update.action === TileDispatchActionType.RETRIEVE) { if(update.action === TileDispatchActionType.RETRIEVE) {
@ -79,7 +173,26 @@ export function Game(props: {
} }
return playerLetters.slice(); return playerLetters.slice();
} else if (update.action === TileDispatchActionType.RETURN) { } else if (update.action === TileDispatchActionType.RETURN) {
gridArrowDispatch({action: GridArrowDispatchActionType.CLEAR});
return mergeTrays(playerLetters, playerLetters); return mergeTrays(playerLetters, playerLetters);
} else if (update.action === TileDispatchActionType.MOVE_TO_ARROW) {
// let's verify that the arrow is defined, otherwise do nothing
if(gridArrow != null) {
const end_position = {
location: LocationType.GRID,
index: gridArrow.position,
};
gridArrowDispatch({
action: GridArrowDispatchActionType.SHIFT,
});
return movePlayableLetters(playerLetters, {
action: TileDispatchActionType.MOVE,
start: update.start,
end: end_position,
});
} else {
return playerLetters;
}
} else { } else {
console.error("Unknown tray update"); console.error("Unknown tray update");
console.error({update}); console.error({update});
@ -87,44 +200,14 @@ export function Game(props: {
} }
function exchangeFunction(selectedArray: Array<boolean>) {
const result: MyResult<TurnAction | string> = props.wasm.exchange_tiles(selectedArray);
if(result.response_type === "ERR") {
logDispatch(<div><em>{(result.value as string)}</em></div>);
} else {
handlePlayerAction(result.value as TurnAction, props.settings.playerName);
setTurnCount(turnCount + 1);
if(result.game_state.type === "Ended") {
endGame(result.game_state);
}
}
}
function addWordFn(word: string) {
props.wasm.add_word(word);
}
const [playerLetters, trayDispatch] = useReducer(movePlayableLetters, []); const [playerLetters, trayDispatch] = useReducer(movePlayableLetters, []);
const [logInfo, logDispatch] = useReducer(addLogInfo, []);
const [turnCount, setTurnCount] = useState<number>(1); const [turnCount, setTurnCount] = useState<number>(1);
const playerAndScores: PlayerAndScore[] = useMemo(() => { const playerAndScores: PlayerAndScore[] = useMemo(() => {
return props.wasm.get_scores(); return props.wasm.get_scores();
}, [turnCount, isGameOver]); }, [turnCount, isGameOver]);
const [boardLetters, setBoardLetters] = useState<HighlightableLetterData[]>(() => {
const newLetterData = [] as HighlightableLetterData[];
for(let i=0; i<15*15; i++) {
newLetterData.push(undefined);
}
return newLetterData;
});
useEffect(() => { useEffect(() => {
const newLetterData = props.wasm.get_board_letters() as HighlightableLetterData[]; const newLetterData = props.wasm.get_board_letters() as HighlightableLetterData[];
@ -188,6 +271,9 @@ export function Game(props: {
else if(action.type == "Pass"){ else if(action.type == "Pass"){
logDispatch(<div>{playerName} passed.</div>); logDispatch(<div>{playerName} passed.</div>);
} }
// Clear any on-screen arrows
gridArrowDispatch({action: GridArrowDispatchActionType.CLEAR});
} }
function endGame(state: GameState) { function endGame(state: GameState) {
@ -303,7 +389,14 @@ export function Game(props: {
exchangeFunction={exchangeFunction} exchangeFunction={exchangeFunction}
/> />
<div className="board-log"> <div className="board-log">
<Grid cellTypes={cellTypes} playerLetters={playerLetters} boardLetters={boardLetters} dispatch={trayDispatch}/> <Grid
cellTypes={cellTypes}
playerLetters={playerLetters}
boardLetters={boardLetters}
tileDispatch={trayDispatch}
arrow={gridArrow}
arrowDispatch={gridArrowDispatch}
/>
<div className="message-log"> <div className="message-log">
<button className="end-game" <button className="end-game"
onClick={() => { onClick={() => {
@ -334,7 +427,7 @@ export function Game(props: {
}}>Open Tile Exchange</button> }}>Open Tile Exchange</button>
</div> </div>
<TileTray letters={playerLetters} trayLength={props.settings.trayLength} dispatch={trayDispatch}/> <TileTray letters={playerLetters} trayLength={props.settings.trayLength} trayDispatch={trayDispatch}/>
<div className="player-controls"> <div className="player-controls">
<button className="check" onClick={() => { <button className="check" onClick={() => {
const playedTiles = playerLetters.map((i) => { const playedTiles = playerLetters.map((i) => {

View file

@ -1,16 +1,27 @@
import * as React from "react"; import * as React from "react";
import {Letter as LetterData, PlayerAndScore} from "word_grid";
import {ChangeEvent, JSX} from "react"; import {ChangeEvent, JSX} from "react";
import {PlayerAndScore} from "word_grid";
import { import {
cellTypeToDetails,
CellType, CellType,
cellTypeToDetails,
CoordinateData,
GridArrowData,
GridArrowDispatch,
GridArrowDispatchActionType,
HighlightableLetterData,
LocationType, LocationType,
PlayableLetterData, PlayableLetterData,
CoordinateData, TileDispatchActionType, TileDispatch, HighlightableLetterData, TileDispatch,
TileDispatchActionType,
} from "./utils"; } from "./utils";
export function TileSlot(props: { tile?: React.JSX.Element | undefined, location: CoordinateData, dispatch: TileDispatch }): React.JSX.Element { export function TileSlot(props: {
tile?: React.JSX.Element | undefined,
location: CoordinateData,
tileDispatch: TileDispatch,
arrowDispatch?: GridArrowDispatch,
}): React.JSX.Element {
let isDraggable = props.tile !== undefined; let isDraggable = props.tile !== undefined;
function onDragStart(e: React.DragEvent<HTMLDivElement>) { function onDragStart(e: React.DragEvent<HTMLDivElement>) {
@ -22,7 +33,7 @@ export function TileSlot(props: { tile?: React.JSX.Element | undefined, location
const startLocation: CoordinateData = JSON.parse(e.dataTransfer.getData("wordGrid/coords")); const startLocation: CoordinateData = JSON.parse(e.dataTransfer.getData("wordGrid/coords"));
const thisLocation = props.location; const thisLocation = props.location;
props.dispatch({action: TileDispatchActionType.MOVE, start: startLocation, end: thisLocation}); props.tileDispatch({action: TileDispatchActionType.MOVE, start: startLocation, end: thisLocation});
} }
let className = "tileSpot"; let className = "tileSpot";
@ -30,10 +41,24 @@ export function TileSlot(props: { tile?: React.JSX.Element | undefined, location
className += " ephemeral"; className += " ephemeral";
} }
let onClick: () => void;
if(props.arrowDispatch != null && props.location.location == LocationType.GRID) {
onClick = () => {
props.arrowDispatch({action: GridArrowDispatchActionType.CYCLE, position: props.location.index});
}
} else if(props.location.location == LocationType.TRAY && props.tile != null && props.tile.props.data.text != ' ' && props.tile.props.data.text != '') {
onClick = () => {
props.tileDispatch({
action: TileDispatchActionType.MOVE_TO_ARROW, end: undefined, start: props.location,
});
}
}
return <div className={className} return <div className={className}
draggable={isDraggable} draggable={isDraggable}
onDragStart={onDragStart} onDragStart={onDragStart}
onDrop={onDrop} onDrop={onDrop}
onClick={onClick}
onDragOver={(e) => {e.preventDefault()}} onDragOver={(e) => {e.preventDefault()}}
> >
{props.tile} {props.tile}
@ -92,7 +117,7 @@ export function Letter(props: { data: HighlightableLetterData, letterUpdater?: (
} }
export function TileTray(props: { letters: Array<PlayableLetterData>, trayLength: number, dispatch: TileDispatch }): React.JSX.Element { export function TileTray(props: { letters: Array<PlayableLetterData>, trayLength: number, trayDispatch: TileDispatch }): React.JSX.Element {
let elements: JSX.Element[] = []; let elements: JSX.Element[] = [];
for (let i=0; i<props.trayLength; i++) { for (let i=0; i<props.trayLength; i++) {
@ -100,7 +125,7 @@ export function TileTray(props: { letters: Array<PlayableLetterData>, trayLength
<TileSlot <TileSlot
key={"empty" + i} key={"empty" + i}
location={{location: LocationType.TRAY, index: i}} location={{location: LocationType.TRAY, index: i}}
dispatch={props.dispatch} /> tileDispatch={props.trayDispatch} />
); );
} }
@ -111,14 +136,14 @@ export function TileTray(props: { letters: Array<PlayableLetterData>, trayLength
elements[letter.index] = elements[letter.index] =
<TileSlot <TileSlot
tile={<Letter tile={<Letter
data={letter} data={{...letter, highlight: false}}
letterUpdater={(value) => { letterUpdater={(value) => {
props.dispatch({action: TileDispatchActionType.SET_BLANK, blankIndex: i, newBlankValue: value}) props.trayDispatch({action: TileDispatchActionType.SET_BLANK, blankIndex: i, newBlankValue: value})
}} }}
/>} />}
key={"letter" + letter.index} key={"letter" + letter.index}
location={{location: LocationType.TRAY, index: letter.index}} location={{location: LocationType.TRAY, index: letter.index}}
dispatch={props.dispatch} /> tileDispatch={props.trayDispatch} />
} }
}); });
@ -131,11 +156,27 @@ export function TileTray(props: { letters: Array<PlayableLetterData>, trayLength
} }
export function Arrow(props: {
data: GridArrowData,
dispatch: GridArrowDispatch,
}) {
return <div
className={`arrow ${props.data.direction}`}
>
</div>;
}
export function Grid(props: { export function Grid(props: {
cellTypes: CellType[], cellTypes: CellType[],
playerLetters: Array<PlayableLetterData>, playerLetters: Array<PlayableLetterData>,
boardLetters: HighlightableLetterData[], boardLetters: HighlightableLetterData[],
dispatch: TileDispatch}) { tileDispatch: TileDispatch,
arrow: GridArrowData,
arrowDispatch: GridArrowDispatch,
}) {
const elements = props.cellTypes.map((ct, i) => { const elements = props.cellTypes.map((ct, i) => {
const {className, text} = cellTypeToDetails(ct); const {className, text} = cellTypeToDetails(ct);
@ -147,13 +188,22 @@ export function Grid(props: {
tileElement = <> tileElement = <>
<span>{text}</span> <span>{text}</span>
<TileSlot <TileSlot
location={{location: LocationType.GRID, index: i}} location={{location: LocationType.GRID, index: i}}
dispatch={props.dispatch} /></>; tileDispatch={props.tileDispatch}
arrowDispatch={props.arrowDispatch}
/>
</>;
}
let arrowElement: JSX.Element;
if(props.arrow != null && props.arrow.position == i) {
arrowElement = <Arrow data={props.arrow} dispatch={props.arrowDispatch} />;
} }
return <div key={i} className={"grid-spot " + className}> return <div key={i} className={"grid-spot " + className}>
{tileElement} {tileElement}
{arrowElement}
</div>; </div>;
}); });
@ -168,13 +218,13 @@ export function Grid(props: {
<div key={"letter" + letter.index} className={"grid-spot " + className}> <div key={"letter" + letter.index} className={"grid-spot " + className}>
<TileSlot <TileSlot
tile={<Letter tile={<Letter
data={letter} data={{...letter, highlight: false}}
letterUpdater={(value) => { letterUpdater={(value) => {
props.dispatch({action: TileDispatchActionType.SET_BLANK, blankIndex: i, newBlankValue: value}); props.tileDispatch({action: TileDispatchActionType.SET_BLANK, blankIndex: i, newBlankValue: value});
}} }}
/>} />}
location={{location: LocationType.GRID, index: letter.index}} location={{location: LocationType.GRID, index: letter.index}}
dispatch={props.dispatch} /> tileDispatch={props.tileDispatch} />
</div>; </div>;

View file

@ -67,9 +67,23 @@
position: absolute; position: absolute;
top: 0; top: 0;
left: 0; left: 0;
z-index: 2;
}
.arrow {
color: red;
font-size: @tile-font-size+10;
position: absolute;
top: 0;
left: 10px;
z-index: 1; z-index: 1;
} }
.down {
transform: rotate(90deg);
}
.ephemeral { .ephemeral {
opacity: 75%; opacity: 75%;
} }
@ -108,6 +122,8 @@
.tileSpot { .tileSpot {
height: @tile-width; height: @tile-width;
width: @tile-width; width: @tile-width;
} }
.letter { .letter {

View file

@ -35,10 +35,29 @@ export enum TileDispatchActionType {
RETRIEVE, RETRIEVE,
SET_BLANK, SET_BLANK,
RETURN, RETURN,
MOVE_TO_ARROW,
} }
export type TileDispatchAction = {action: TileDispatchActionType, start?: CoordinateData, end?: CoordinateData, newBlankValue?: string, blankIndex?: number}; export type TileDispatchAction = {action: TileDispatchActionType, start?: CoordinateData, end?: CoordinateData, newBlankValue?: string, blankIndex?: number};
export type TileDispatch = React.Dispatch<TileDispatchAction>; export type TileDispatch = React.Dispatch<TileDispatchAction>;
export enum Direction {
RIGHT = "right",
DOWN = "down",
}
export interface GridArrowData {
direction: Direction,
position: number,
}
export enum GridArrowDispatchActionType {
CLEAR,
CYCLE,
SHIFT,
}
export type GridArrowDispatchAction = {action: GridArrowDispatchActionType, position?: number};
export type GridArrowDispatch = React.Dispatch<GridArrowDispatchAction>;
export function matchCoordinate(playerLetters: PlayableLetterData[], coords: CoordinateData): number { export function matchCoordinate(playerLetters: PlayableLetterData[], coords: CoordinateData): number {
for (let i=0; i<playerLetters.length; i++){ for (let i=0; i<playerLetters.length; i++){
@ -136,4 +155,6 @@ export function mergeTrays(existing: PlayableLetterData[], newer: (Letter | unde
return ld as PlayableLetterData; return ld as PlayableLetterData;
}); });
} }
export const GRID_LENGTH = 15;