Source: client/scripts/versus/store.js

/** @module store */

import {
  UPDATE_USERNAME_LISTENER_NAME,
  UPDATE_AVATAR_LISTENER_NAME,
  WORLD_EVENTS,
} from './types.js';
import * as Types from './types.js';
import { getPlayerUUID } from './../profile.js';
import { areCardsEqual, getCurrentRoundState } from './util.js';

/** @type { Types.ServerToClientProfile } */
const userProfilesByUUID = {};

/** @type { Types.GameState } */
let gameState = {
  byPlayer: {},
  byRound: [],
  isStarted: false,
  gameWinner: null,
};

/**
 * Sets the winner of the current/most recent round
 * @param { Types.UUID } playerUUID unique identifier of round winner
 */
export function setRoundWinnerUUID(playerUUID) {
  getCurrentRoundState(gameState).roundWinner = playerUUID;
  gameState.byPlayer[playerUUID].score += 1;
} /* setRoundWinner */

/**
 * Fetches UUID of user that won the current round
 * @returns { Types.UUID } unique ID corresponding to round winner
 */
export function getRoundWinnerUUID() {
  return getCurrentRoundState(gameState).roundWinner;
} /* getRoundWinnerUUID */

/**
 * Adds blank new round to the current game state
 */
export function createNewRoundState() {
  gameState.byRound.push({
    selectedCard: {},
    roundWinner: null,
  });
} /* createNewRoundState */

/**
 * For the current round, sets the client user's selected card
 * @param { Types.Card } selectedCard card selected by the client user
 */
export function setSelfSelectedCard(selectedCard) {
  const playerUUID = getPlayerUUID();

  const playerGameState = gameState.byPlayer[playerUUID];
  playerGameState.remainingCards = playerGameState.remainingCards.filter(
    (card) => !areCardsEqual(card, selectedCard),
  );

  getCurrentRoundState(gameState).selectedCard[playerUUID] = selectedCard;
} /* setSelfSelectedCard */

/**
 * Fetches world event associated to current round
 * @returns { string } value of WORLD_EVENT enum
 */
export function getCurrentWorldEvent() {
  return getCurrentRoundState(gameState).worldEvent ?? WORLD_EVENTS.NONE;
} /* getCurrentWorldEvent */

/**
 * Fetches card selected by the user during the current round
 * @returns { Types.Card|null } card selected by the user in the current round,
 * or null if no card has been selected
 */
export function getSelfSelectedCard() {
  return getCurrentRoundState(gameState).selectedCard[getPlayerUUID()] ?? null;
} /* getSelfSelectedCard */

/**
 * Determines the UUID of the opponent player by finding the first (only)
 * UUID that doesn't match the client user
 * @returns { Types.UUID } UUID of opponent user
 */
export function getOpponentUUID() {
  const playerUUID = getPlayerUUID();
  return Object.keys(gameState.byPlayer).find((UUID) => UUID !== playerUUID);
} /* getOpponentUUID */

/**
 * For the current round, sets the opponent's selected card and tosses out
 * a random card from the opponent's remaining cards (b/c they are anonymized anyways)
 * @param { Types.IndeterminateCard } selectedCard card selected by the
 * opponent user, passed from the server
 */
export function setOppSelectedCard(selectedCard) {
  const opponentUUID = getOpponentUUID();

  getCurrentRoundState(gameState).selectedCard[opponentUUID] = selectedCard;
  gameState.byPlayer[opponentUUID].remainingCards.pop();
} /* setOppSelectedCard */

/**
 * For the current round, returns whether or not the opponent player has already
 * played a card
 * @returns { boolean } true if opponent has played this round; false otherwise
 */
export function getOppHasPlayedRound() {
  return !!getCurrentRoundState(gameState).selectedCard[getOpponentUUID()];
} /* getOppHasPlayedRound */

/**
 * For the current round, returns whether or not the client user has already
 * played a card
 * @returns { boolean } true if client user has played this round; false otherwise
 */
export function getSelfHasPlayedRound() {
  return !!getCurrentRoundState(gameState).selectedCard[getPlayerUUID()];
} /* getOppHasPlayedRound */

/**
 * Updates profile to store by UUID and emits appropriate event listener
 * for downstream users
 * @param { Types.ServerToClientProfile } profile most-recent profile sent from
 * the server for a given user
 */
export function updateProfile(profile) {
  userProfilesByUUID[profile.uuid] = profile;
  window.dispatchEvent(new Event(UPDATE_USERNAME_LISTENER_NAME));
  window.dispatchEvent(new Event(UPDATE_AVATAR_LISTENER_NAME));
} /* updateProfile */

/**
 * Fetches last-read profile associated to a player sent from the server
 * @param { Types.UUID } playerUUID unique identifer whose profile is targeted
 * @returns { Types.ServerToClientProfile } profile of user
 */
export function getProfile(playerUUID) {
  return userProfilesByUUID[playerUUID];
} /* getProfile */

/**
 * Fetches game score corresponding to user profile with a given UUID
 * @param { Types.UUID} playerUUID unique identifier whose score is targeted
 * @returns { number } number of rounds won by player
 */
export function getScore(playerUUID) {
  return gameState.byPlayer[playerUUID].score;
} /* getScore */

/**
 * For the current game state, returns the current number associated
 * with the round (i.e., at the start of the game, the client is partaking
 * in round 1)
 * @returns { number } current round number of game state for client user
 */
export function getRoundNumber() {
  return gameState.byRound.length;
} /* getRoundNumber */

/**
 * For a given list of members of the lobby, initializes player game state for
 * each of them under the gameState object
 * @param { Types.UUID[] } playerUUIDs unique identifiers for each member of game
 */
export function initializePlayers(playerUUIDs) {
  gameState.byPlayer = Object.fromEntries(
    playerUUIDs.map((UUID) => [
      UUID,
      {
        score: 0,
        remainingCards: [],
        ...gameState.byPlayer[UUID],
      },
    ]),
  );
} /* initializePlayers */

/**
 * Sets list of remaining cards that can be played by client user
 * @param { Types.Card[] } remainingCards list of remaining card names for user
 */
export function setRemainingCards(remainingCards) {
  gameState.byPlayer[getPlayerUUID()].remainingCards = remainingCards;
} /* setRemainingCards */

/**
 * Fetches all cards that are still available in the hand of the client user
 * @returns { Types.Card[] } list of remaining card names for user
 */
export function getRemainingCards() {
  return gameState.byPlayer[getPlayerUUID()].remainingCards;
} /* getRemainingCards */

/**
 * Fetches the number of cards that the opponent still has (cards themselves
 * are still unknown)
 * @returns { number } number of cards that opponent has in hand
 */
export function getNumOpponentCards() {
  return gameState.byPlayer[getOpponentUUID()].remainingCards.length;
} /* getNumOpponentCards */

/**
 * Sets an anonymized list of cards of same length as numOpponentCards in the data store
 * for future access
 * @param { number } numOpponentCards number of remaining cards that the opponent has
 */
export function setNumOpponentCards(numOpponentCards) {
  gameState.byPlayer[getOpponentUUID()].remainingCards = Array.from({
    length: numOpponentCards,
  });
} /* setNumOpponentCards */

/**
 * Returns the UUIDs member to current game
 * @returns { Types.UUID[] } list of UUIDs active in game (length 2)
 */
export function getPlayerUUIDs() {
  return Object.keys(gameState.byPlayer);
} /* getPlayerUUIDs */

/**
 * Sets game-start status to true
 */
export function setGameIsStarted() {
  gameState.isStarted = true;
} /* setGameIsStarted */

/**
 * Pulls game-start status from the current game state
 * @returns { boolean } true if game is started; false otherwise
 */
export function getGameIsStarted() {
  return gameState.isStarted;
} /* getGameIsStarted */

/**
 * Sets unique ID of user that won the game
 * @param { Types.UUID } gameWinnerUUID unique ID of game winner
 */
export function setGameWinnerUUID(gameWinnerUUID) {
  gameState.gameWinner = gameWinnerUUID;
} /* setGameWinner */

/**
 * Fetches UUID of user that won the game
 * @returns { Types.UUID } unique ID of game winner
 */
export function getGameWinnerUUID() {
  return gameState.gameWinner;
} /* getGameWinner */

/**
 * Attempts to update game state by forcing a new game state object
 * on top of the existing game state
 * @param { Types.GameState } newGameState game state that takes priority
 */
export function setGameState(newGameState) {
  gameState = { ...gameState, ...newGameState };
} /* setGameState */

/**
 *
 */
export function clearGameState() {
  Object.assign(gameState, {
    byPlayer: {},
    byRound: [],
    isStarted: false,
    gameWinner: null,
  });
} /* clearGameState */