Add arrow for fast play
This commit is contained in:
parent
a34ad8cd12
commit
2fa28ce3d4
4 changed files with 234 additions and 54 deletions
165
ui/src/Game.tsx
165
ui/src/Game.tsx
|
@ -1,15 +1,22 @@
|
|||
import * as React from "react";
|
||||
import {useEffect, useMemo, useReducer, useRef, useState} from "react";
|
||||
import {
|
||||
GameState,
|
||||
GameWasm, Letter,
|
||||
Letter as LetterData,
|
||||
GameWasm,
|
||||
MyResult,
|
||||
PlayedTile,
|
||||
PlayerAndScore,
|
||||
ScoreResult,
|
||||
Tray, TurnAction, TurnAdvanceResult
|
||||
Tray,
|
||||
TurnAction,
|
||||
TurnAdvanceResult
|
||||
} from "../../pkg/word_grid";
|
||||
import {
|
||||
Direction,
|
||||
GRID_LENGTH,
|
||||
GridArrowData,
|
||||
GridArrowDispatchAction,
|
||||
GridArrowDispatchActionType,
|
||||
HighlightableLetterData,
|
||||
LocationType,
|
||||
matchCoordinate,
|
||||
|
@ -19,7 +26,6 @@ import {
|
|||
TileDispatchAction,
|
||||
TileDispatchActionType
|
||||
} from "./utils";
|
||||
import {useEffect, useMemo, useReducer, useRef, useState} from "react";
|
||||
import {TileExchangeModal} from "./TileExchange";
|
||||
import {Grid, Scores, TileTray} from "./UI";
|
||||
|
||||
|
@ -42,6 +48,94 @@ export function Game(props: {
|
|||
const [isGameOver, setGameOver] = useState<boolean>(false);
|
||||
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) {
|
||||
|
||||
if(update.action === TileDispatchActionType.RETRIEVE) {
|
||||
|
@ -79,7 +173,26 @@ export function Game(props: {
|
|||
}
|
||||
return playerLetters.slice();
|
||||
} else if (update.action === TileDispatchActionType.RETURN) {
|
||||
gridArrowDispatch({action: GridArrowDispatchActionType.CLEAR});
|
||||
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 {
|
||||
console.error("Unknown tray 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 [logInfo, logDispatch] = useReducer(addLogInfo, []);
|
||||
|
||||
const [turnCount, setTurnCount] = useState<number>(1);
|
||||
const playerAndScores: PlayerAndScore[] = useMemo(() => {
|
||||
return props.wasm.get_scores();
|
||||
}, [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(() => {
|
||||
const newLetterData = props.wasm.get_board_letters() as HighlightableLetterData[];
|
||||
|
@ -188,6 +271,9 @@ export function Game(props: {
|
|||
else if(action.type == "Pass"){
|
||||
logDispatch(<div>{playerName} passed.</div>);
|
||||
}
|
||||
|
||||
// Clear any on-screen arrows
|
||||
gridArrowDispatch({action: GridArrowDispatchActionType.CLEAR});
|
||||
}
|
||||
|
||||
function endGame(state: GameState) {
|
||||
|
@ -303,7 +389,14 @@ export function Game(props: {
|
|||
exchangeFunction={exchangeFunction}
|
||||
/>
|
||||
<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">
|
||||
<button className="end-game"
|
||||
onClick={() => {
|
||||
|
@ -334,7 +427,7 @@ export function Game(props: {
|
|||
}}>Open Tile Exchange</button>
|
||||
</div>
|
||||
|
||||
<TileTray letters={playerLetters} trayLength={props.settings.trayLength} dispatch={trayDispatch}/>
|
||||
<TileTray letters={playerLetters} trayLength={props.settings.trayLength} trayDispatch={trayDispatch}/>
|
||||
<div className="player-controls">
|
||||
<button className="check" onClick={() => {
|
||||
const playedTiles = playerLetters.map((i) => {
|
||||
|
|
|
@ -1,16 +1,27 @@
|
|||
import * as React from "react";
|
||||
import {Letter as LetterData, PlayerAndScore} from "word_grid";
|
||||
import {ChangeEvent, JSX} from "react";
|
||||
import {PlayerAndScore} from "word_grid";
|
||||
import {
|
||||
cellTypeToDetails,
|
||||
CellType,
|
||||
cellTypeToDetails,
|
||||
CoordinateData,
|
||||
GridArrowData,
|
||||
GridArrowDispatch,
|
||||
GridArrowDispatchActionType,
|
||||
HighlightableLetterData,
|
||||
LocationType,
|
||||
PlayableLetterData,
|
||||
CoordinateData, TileDispatchActionType, TileDispatch, HighlightableLetterData,
|
||||
TileDispatch,
|
||||
TileDispatchActionType,
|
||||
} 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;
|
||||
|
||||
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 thisLocation = props.location;
|
||||
|
||||
props.dispatch({action: TileDispatchActionType.MOVE, start: startLocation, end: thisLocation});
|
||||
props.tileDispatch({action: TileDispatchActionType.MOVE, start: startLocation, end: thisLocation});
|
||||
}
|
||||
|
||||
let className = "tileSpot";
|
||||
|
@ -30,10 +41,24 @@ export function TileSlot(props: { tile?: React.JSX.Element | undefined, location
|
|||
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}
|
||||
draggable={isDraggable}
|
||||
onDragStart={onDragStart}
|
||||
onDrop={onDrop}
|
||||
onClick={onClick}
|
||||
onDragOver={(e) => {e.preventDefault()}}
|
||||
>
|
||||
{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[] = [];
|
||||
for (let i=0; i<props.trayLength; i++) {
|
||||
|
@ -100,7 +125,7 @@ export function TileTray(props: { letters: Array<PlayableLetterData>, trayLength
|
|||
<TileSlot
|
||||
key={"empty" + 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] =
|
||||
<TileSlot
|
||||
tile={<Letter
|
||||
data={letter}
|
||||
data={{...letter, highlight: false}}
|
||||
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}
|
||||
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: {
|
||||
cellTypes: CellType[],
|
||||
playerLetters: Array<PlayableLetterData>,
|
||||
boardLetters: HighlightableLetterData[],
|
||||
dispatch: TileDispatch}) {
|
||||
tileDispatch: TileDispatch,
|
||||
arrow: GridArrowData,
|
||||
arrowDispatch: GridArrowDispatch,
|
||||
}) {
|
||||
|
||||
const elements = props.cellTypes.map((ct, i) => {
|
||||
const {className, text} = cellTypeToDetails(ct);
|
||||
|
@ -147,13 +188,22 @@ export function Grid(props: {
|
|||
tileElement = <>
|
||||
<span>{text}</span>
|
||||
<TileSlot
|
||||
location={{location: LocationType.GRID, index: i}}
|
||||
dispatch={props.dispatch} /></>;
|
||||
location={{location: LocationType.GRID, index: i}}
|
||||
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}>
|
||||
|
||||
{tileElement}
|
||||
{arrowElement}
|
||||
</div>;
|
||||
});
|
||||
|
||||
|
@ -168,13 +218,13 @@ export function Grid(props: {
|
|||
<div key={"letter" + letter.index} className={"grid-spot " + className}>
|
||||
<TileSlot
|
||||
tile={<Letter
|
||||
data={letter}
|
||||
data={{...letter, highlight: false}}
|
||||
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}}
|
||||
dispatch={props.dispatch} />
|
||||
tileDispatch={props.tileDispatch} />
|
||||
</div>;
|
||||
|
||||
|
||||
|
|
|
@ -67,9 +67,23 @@
|
|||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 2;
|
||||
|
||||
}
|
||||
|
||||
.arrow {
|
||||
color: red;
|
||||
font-size: @tile-font-size+10;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 10px;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.down {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
|
||||
.ephemeral {
|
||||
opacity: 75%;
|
||||
}
|
||||
|
@ -108,6 +122,8 @@
|
|||
.tileSpot {
|
||||
height: @tile-width;
|
||||
width: @tile-width;
|
||||
|
||||
|
||||
}
|
||||
|
||||
.letter {
|
||||
|
|
|
@ -35,10 +35,29 @@ export enum TileDispatchActionType {
|
|||
RETRIEVE,
|
||||
SET_BLANK,
|
||||
RETURN,
|
||||
MOVE_TO_ARROW,
|
||||
}
|
||||
export type TileDispatchAction = {action: TileDispatchActionType, start?: CoordinateData, end?: CoordinateData, newBlankValue?: string, blankIndex?: number};
|
||||
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 {
|
||||
|
||||
for (let i=0; i<playerLetters.length; i++){
|
||||
|
@ -136,4 +155,6 @@ export function mergeTrays(existing: PlayableLetterData[], newer: (Letter | unde
|
|||
return ld as PlayableLetterData;
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
export const GRID_LENGTH = 15;
|
Loading…
Reference in a new issue