Source: server/util.js

import { SUITES, WORLD_EVENTS, MULTIPLIER } from './types.js';
import * as Types from './types.js';

const NUM_CARDS = 14;

/**
 * Creates and returns random unique game code for each game instance
 * @param { number } rangeLo lowest allowed game code (inclusive)
 * @param { number } rangeHi highest allowed game code (inclusive)
 * @param { Array<number> } currentGameCodes games currently in use (i.e., to avoid)
 * @returns { number } newly generated unique game code
 */
export function generateUniqueGameCode(rangeLo, rangeHi, currentGameCodes) {
  let gameCode;

  do {
    gameCode = Math.floor(Math.random() * (rangeHi - rangeLo + 1)) + rangeLo;
  } while (currentGameCodes.includes(gameCode));

  return gameCode;
} /* generateUniqueGameCode */

/**
 * Determines if two arrays are equal as sets
 * @param { any[] } arr1 array of objects to compare
 * @param { any[] } arr2 array of objects to compare
 * @returns { boolean } true if arrays are equal as sets; false otherwise
 */
export function areUnorderedArrsEqual(arr1, arr2) {
  return (
    arr1.every((el) => arr2.includes(el)) &&
    arr2.every((el) => arr1.includes(el))
  );
} /* areUnorderedArrsEqual */

/**
 * Ensures that card object is well-formed and valid
 * @param { Types.Card } card card object to test
 * @returns { boolean } true if card is valid; false if invalid
 */
export function isCardValid(card) {
  return (
    areUnorderedArrsEqual(Object.keys(card), ['number', 'suite']) &&
    typeof card.number === 'number' &&
    typeof card.suite === 'string'
  );
} /* isCardValid */

/**
 * Determines if two different card objects represent the same card
 * @param { Types.Card } card1 card to compare
 * @param { Types.Card } card2 card to compare
 * @returns { boolean } true if the two cards represent the same
 * card (i.e., same values); false otherwise
 */
export function areCardsEqual(card1, card2) {
  return card1.number === card2.number && card1.suite === card2.suite;
} /* areCardsEqual */

/**
 * Determines the opponent of a given player in a game instance
 * @param { Types.GameInstance } gameInstance game instance to target
 * @param { Types.WSConnection } webSocketConnection non-target player of game instance
 * @returns { Types.WSConnection } other player in game instance
 */
export function getOtherPlayer(gameInstance, webSocketConnection) {
  return gameInstance.webSocketConnections.find(
    (conn) => conn !== webSocketConnection,
  );
} /* getOtherPlayer */

/**
 * Returns the last entry in list of round states from a game instance
 * @param { Types.GameInstance } gameInstance game instance to target
 * @returns { Types.RoundState } most recent (current) round attached to gameInstance
 */
export function getCurrentRoundState(gameInstance) {
  return gameInstance.gameState.byRound[
    gameInstance.gameState.byRound.length - 1
  ];
} /* getCurrentRoundState */

/**
 *
 * @returns { string } random suite from SUITES object
 */
function getRandomSuite() {
  const suiteList = Object.values(SUITES);

  return suiteList[Math.floor(Math.random() * suiteList.length)];
}

/**
 * Determines the multiplier for a given matchup between two cards
 * (Positive multiplier implies that card1 has a suite-based advantage over card2)
 * @param { Types.Card } card1 card to compare, target of multiplier
 * @param { Types.Card } card2 card to compare
 * @param {string} worldEvent current worldEvent for the round
 * @returns { Types.Card } matchup multiplier for card1 with reference to card2
 */
function getMultiplierWorldEvent(card1, card2, worldEvent) {
  let multiplier = MULTIPLIER.NEUTRAL;
  let suite1 =
    worldEvent === WORLD_EVENTS.RANDOM_SUITE ? getRandomSuite() : card1.suite;
  let suite2 =
    worldEvent === WORLD_EVENTS.RANDOM_SUITE ? getRandomSuite() : card2.suite;

  switch (suite1) {
    case SUITES.WANDS:
      if (suite2 === SUITES.CUPS)
        multiplier =
          worldEvent === WORLD_EVENTS.SUITE_BOOST_WANDS
            ? MULTIPLIER.GREATER + MULTIPLIER.BOOST
            : MULTIPLIER.GREATER;
      else if (suite2 == SUITES.SWORDS)
        multiplier =
          worldEvent === WORLD_EVENTS.SUITE_BOOST_WANDS
            ? MULTIPLIER.LESS + MULTIPLIER.BOOST
            : MULTIPLIER.LESS;
      break;
    case SUITES.CUPS:
      if (suite2 === SUITES.SWORDS)
        multiplier =
          worldEvent === WORLD_EVENTS.SUITE_BOOST_CUPS
            ? MULTIPLIER.GREATER + MULTIPLIER.BOOST
            : MULTIPLIER.GREATER;
      else if (suite2 == SUITES.WANDS)
        multiplier =
          worldEvent === WORLD_EVENTS.SUITE_BOOST_CUPS
            ? MULTIPLIER.LESS + MULTIPLIER.BOOST
            : MULTIPLIER.LESS;
      break;
    case SUITES.SWORDS:
      if (suite2 === SUITES.WANDS)
        multiplier =
          worldEvent === WORLD_EVENTS.SUITE_BOOST_SWORDS
            ? MULTIPLIER.GREATER + MULTIPLIER.BOOST
            : MULTIPLIER.GREATER;
      else if (suite2 == SUITES.CUPS)
        multiplier =
          worldEvent === WORLD_EVENTS.SUITE_BOOST_SWORDS
            ? MULTIPLIER.LESS + MULTIPLIER.BOOST
            : MULTIPLIER.LESS;
      break;
    case SUITES.PENTACLES:
      multiplier =
        worldEvent === WORLD_EVENTS.SUITE_BOOST_PENTACLES
          ? MULTIPLIER.NEUTRAL + MULTIPLIER.BOOST
          : MULTIPLIER.NEUTRAL;
      break;
    default:
  }

  return worldEvent === WORLD_EVENTS.SUITE_REVERSED
    ? 1 / multiplier
    : multiplier;
} /* getMultiplier */

/**
 * Returns a random number in a range
 * @param { number } lo integer low value (inclusive)
 * @param { number } hi integer high value(inclusive)
 * @returns { number } random integer between lo and hi, inclusive
 */
function getRandomValue(lo, hi) {
  return Math.floor(Math.random() * (hi - lo + 1)) + lo;
}

/**
 * Determines the winner between 2 cards given a world event
 * @param { Types.Card } card1 the first card to compare
 * @param { Types.Card } card2 the second card to compare
 * @param { string } worldEvent the current round's world event
 * @returns { Types.Card } winning card
 */
export function getWinningCard(card1, card2, worldEvent) {
  switch (worldEvent) {
    case WORLD_EVENTS.LOWER_WINS:
      return card1.number < card2.number ? card1 : card2;
    case WORLD_EVENTS.RANDOM_VALUE:
      return getRandomValue(1, NUM_CARDS) *
        getMultiplierWorldEvent(card1, card2, worldEvent) >
        getRandomValue(1, NUM_CARDS)
        ? card1
        : card2;
    /*fallthrough because other world events influence the multiplier method not winning card*/
    default:
      return card1.number * getMultiplierWorldEvent(card1, card2, worldEvent) >
        card2.number
        ? card1
        : card2;
  }
}

/**
 * Determines which of two users wins a round
 * @param { Types.GameInstance } gameInstance the round's game instance
 * @returns { Types.UUID } winning user UUID1 and UUID2
 */
export function getRoundWinnerUUID(gameInstance) {
  const currentRoundState = getCurrentRoundState(gameInstance);
  const worldEvent = currentRoundState.worldEvent;
  const [[uuid1, card1], [uuid2, card2]] = Object.entries(
    currentRoundState.selectedCard,
  );

  return getWinningCard(card1, card2, worldEvent) == card1 ? uuid1 : uuid2;
}

/**
 * Gives a random World Event
 * @param { Types.GameInstance } gameInstance the game instance getting the world event
 * @returns { Types.worldEvent } World Event
 */
function getRandomWorldEvent(gameInstance) {
  const currentRoundNumber = gameInstance.gameState.byRound.length + 1;
  let worldEvents = Object.values(WORLD_EVENTS).filter((e) => {
    return e != WORLD_EVENTS.NONE;
  });

  if (currentRoundNumber == 2 || currentRoundNumber == 4) {
    return worldEvents[Math.floor(Math.random() * worldEvents.length)];
  } else {
    return WORLD_EVENTS.NONE;
  }
}

/**
 * Returns profile of user who has a higher score
 * @param { Types.GameInstance } gameInstance target game instance
 * @returns { Types.ServerToClientProfile } profile of game winner
 */
export function calculateGameWinnerProfile(gameInstance) {
  const [[pOneUUID, { score: pOneScore }], [pTwoUUID, { score: pTwoScore }]] =
    Object.entries(gameInstance.gameState.byPlayer);

  const winnerUUID = pOneScore > pTwoScore ? pOneUUID : pTwoUUID;

  return gameInstance.webSocketConnections.find(
    (webSocketConnection) => webSocketConnection.profile.uuid === winnerUUID,
  ).profile;
} /* getGameWinner */

/**
 * Creates and returns a new round state
 * @param { Types.GameInstance } gameInstance game instance to determine if world event should be used
 * @returns { Types.RoundState } blank round state
 */
export function createNewRound(gameInstance) {
  return {
    selectedCard: {},
    roundWinner: null,
    worldEvent: getRandomWorldEvent(gameInstance),
  };
} /* createNewRound */

/**
 * Generates two sets of n mutually-unique cards
 * @param { Types.Card[] } cardList list of cards to pull from
 * @param { number } n integer size of each set
 * @returns { Types.Card[][] }  two lists of n mutually-unique cards
 */
export function generateUniqueCards(cardList, n) {
  /* shuffle cards by sorting according to random weights */
  const shuffledCardList = cardList
    .map((card) => [card, Math.random()])
    .sort(([, weight1], [, weight2]) => weight1 - weight2)
    .map(([card]) => card);

  /* choose first n and second n elements for each list */
  return [shuffledCardList.slice(0, n), shuffledCardList.slice(n, n + n)];
} /* generateUniqueCards */

/**
 * Strips sensitive data (i.e., currently-selected card) from a specific game state
 * object so that it can be sent downstream to the specified user without fear that
 * it exposes behind-the-screens info
 * @param { Types.UUID } playerUUID user who the game state is destined for
 * @param { Types.GameState } gameState initial (dirty) game state
 * @returns { Types.GameState } game state cleaned of any data that should be
 * private to the given user
 */
export function cleanGameState(playerUUID, gameState) {
  const copiedState = structuredClone(gameState);
  const currentRound = copiedState.byRound[copiedState.byRound.length - 1];

  Object.keys(gameState.byPlayer).forEach((UUID) => {
    if (UUID === playerUUID) return;

    if (!currentRound.roundWinner && currentRound.selectedCard[UUID])
      currentRound.selectedCard[UUID] = 'played';

    const playerState = copiedState.byPlayer[UUID];
    playerState.remainingCards = Array.from({
      length: playerState.remainingCards.length,
    });
  });

  return copiedState;
} /* cleanGameState */

/**
 * For a given game, returns the number of connections (i.e., players)
 * that are currently live connections
 * @param { Types.GameInstance } gameInstance queried game instance
 * @returns { number } number of live connections in the moment
 */
export function getNumActivePlayers(gameInstance) {
  return gameInstance?.webSocketConnections.filter(
    (webSocketConnection) => webSocketConnection.connected,
  ).length;
} /* getNumActivePlayers*/