import * as React from "react"; import { GameState, GameWasm, Letter, Letter as LetterData, MyResult, PlayedTile, PlayerAndScore, ScoreResult, Tray, TurnAction, TurnAdvanceResult } from "../../pkg/word_grid"; import { HighlightableLetterData, 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, end_game_fn: () => void, }) { const cellTypes = useMemo(() => { return props.wasm.get_board_cell_types(); }, []); const [isGameOver, setGameOver] = useState(false); const [confirmedScorePoints, setConfirmedScorePoints] = useState(-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}); } } function exchangeFunction(selectedArray: Array) { const result: MyResult = props.wasm.exchange_tiles(selectedArray); if(result.response_type === "ERR") { logDispatch(
{(result.value as string)}
); } 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); logDispatch(
{word} was added to dictionary.
); } const [playerLetters, trayDispatch] = useReducer(movePlayableLetters, []); const [logInfo, logDispatch] = useReducer(addLogInfo, []); const [turnCount, setTurnCount] = useState(1); const playerAndScores: PlayerAndScore[] = useMemo(() => { return props.wasm.get_scores(); }, [turnCount, isGameOver]); const [boardLetters, setBoardLetters] = useState(() => { 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[]; // we need to go through and set 'highlight' field to either true or false // it will always be false if the player that just went was the AI // TODO - build in support for multiple other players for(let i=0; i { return props.wasm.get_current_player(); }, [turnCount]); const logDivRef = useRef(null); const [isTileExchangeOpen, setIsTileExchangeOpen] = useState(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]); const remainingTiles = useMemo(() => { return props.wasm.get_remaining_tiles(); }, [turnCount, isGameOver]); const remainingAITiles = useMemo(() => { let result = props.wasm.get_player_tile_count(props.settings.aiName) as MyResult; if(result.response_type == "OK") { return result.value as number; } else { console.error(result.value); return -1; } }, [turnCount, isGameOver]); function handlePlayerAction(action: TurnAction, playerName: string) { if (action.type == "PlayTiles"){ const result = action.result; result.words.sort((a, b) => b.score - a.score); for(let word of result.words) { logDispatch(
{playerName} received {word.score} points for playing '{word.word}.'
); } logDispatch(
{playerName} received a total of {result.total} points for their turn.
); } else if(action.type == "ExchangeTiles") { logDispatch(
{playerName} exchanged {action.tiles_exchanged} tile{action.tiles_exchanged > 1 ? 's' : ''} for their turn.
); } else if(action.type == "Pass"){ logDispatch(
{playerName} passed.
); } } function endGame(state: GameState) { if(state.type != "InProgress") { setGameOver(true); logDispatch(

Scoring

); const scores = props.wasm.get_scores() as PlayerAndScore[]; let pointsBonus = 0; for(const playerAndScore of scores) { const name = playerAndScore.name; if(name == state.finisher) { // we'll do the finisher last continue } const letters = state.remaining_tiles.get(name); if(letters.length == 0) { logDispatch(
{name} has no remaining tiles.
); } else { let pointsLost = 0; let letterListStr = ''; for(let i=0; i 2) { if(i == letters.length - 2) { letterListStr += ', and '; } else if (i < letters.length - 2) { letterListStr += ', '; } } else if (i == 0 && letters.length == 2){ // list of 2 letterListStr += ' and '; } } logDispatch(
{name} penalized {pointsLost} points for not using {letterListStr}.
); pointsBonus += pointsLost; } } if(state.finisher != null) { logDispatch(
{state.finisher} receives {pointsBonus} bonus for completing first.
); } const highestScore = scores .map((score) => score.score) .sort((a, b) => b - a) .at(0); const playersAtHighest = scores.filter((score) => score.score == highestScore); let endGameMsg: string = ''; if(playersAtHighest.length > 1 && state.finisher == null) { endGameMsg = "Tie game!"; } else if (playersAtHighest.length > 1 && state.finisher != null) { // if there's a tie then the finisher gets the win endGameMsg = `${playersAtHighest[0].name} won by finishing first!`; } else { endGameMsg = `${playersAtHighest[0].name} won!`; } logDispatch(

Game over - {endGameMsg}

); } else { // what are we doing in this function?! console.error("endGame was called despite the state being InProgress!"); } } function runAI() { const result: MyResult = props.wasm.advance_turn(); if(result.response_type === "OK" && result.value.type == "AIMove") { handlePlayerAction(result.value.action, props.settings.aiName); if(result.game_state.type === "Ended") { endGame(result.game_state); } } else { // this would be quite surprising console.error({result}); } setTurnCount(turnCount + 1); } useEffect(() => { logDispatch(

Turn {turnCount}

); logDispatch(
{playerTurnName}'s turn
) setConfirmedScorePoints(-1); trayDispatch({action: TileDispatchActionType.RETRIEVE}); if(playerTurnName != props.settings.playerName && !isGameOver) { runAI(); } }, [turnCount]); return <>
{logInfo}
{remainingTiles} letters remaining
{props.settings.aiName} has {remainingAITiles} tiles
; } function AddWordButton(props: {word: string, addWordFn: (x: string) => void}) { const [isClicked, setIsClicked] = useState(false); return }