mavend/octoboard

View on GitHub
src/games/splendid/Game.js

Summary

Maintainability
A
0 mins
Test Coverage
F
2%
import { INVALID_MOVE, TurnOrder } from "boardgame.io/core";
import { mapValues, sum } from "lodash";
import cards from "./data/cards.json";
import bonuses from "./data/bonuses.json";
import { canBuyCard, canTakeBonus, fromEntries } from "./utils";
import { RESOURCES, RESOURCES_CONFIG, WINNING_POINTS } from "./config";
import { PluginActions } from "plugins/actions";

const REGULAR_RESOURCES = RESOURCES.filter((res) => res !== "gold");
const CARDS_PER_LEVEL = 4;

function setupGame(ctx, setupData) {
  const G = {
    privateMatch: setupData && setupData.private,
    actionsCount: 0,
    players: {},
    points: Array(ctx.numPlayers).fill(0),
    actions: [],
    deck: mapValues(cards, ctx.random.Shuffle),
    table: mapValues(cards, () => Array(CARDS_PER_LEVEL).fill(null)),
    tokens: mapValues(RESOURCES_CONFIG, (res) => res.tokensCount[ctx.numPlayers - 1]),
    bonuses: ctx.random.Shuffle(bonuses).slice(0, ctx.numPlayers + 1),
  };

  for (let i = 0; i < ctx.numPlayers; i++) {
    G.players[i] = {
      cards: fromEntries(REGULAR_RESOURCES.map((res) => [res, 0])),
      tokens: fromEntries(RESOURCES.map((res) => [res, 0])),
      reservedCards: [],
      bonuses: [],
    };
  }

  return G;
}

function StartGame(G, ctx) {
  G.actions = [];
  ctx.events.setPhase("play", { next: "0" });
}

function canTakeTokens(availableTokens, requestedTokens, playerTokens) {
  const playerTokensCount = sum(Object.values(playerTokens));
  const requestedCount = sum(Object.values(requestedTokens));
  const resources = Object.keys(requestedTokens).filter((res) => requestedTokens[res] > 0);

  const allowedResources = resources.every((res) => REGULAR_RESOURCES.includes(res));

  const withinLimit = playerTokensCount + requestedCount <= 10;

  const upToThreeDifferent =
    requestedCount <= 3 &&
    requestedCount === resources.length &&
    resources.every((res) => availableTokens[res] >= 1);

  const twoSame =
    requestedCount === 2 && resources.length === 1 && availableTokens[resources[0]] >= 4;

  return allowedResources && withinLimit && (upToThreeDifferent || twoSame);
}

function TakeTokens(G, ctx, requestedTokens) {
  const player = G.players[ctx.currentPlayer];
  if (!canTakeTokens(G.tokens, requestedTokens, player.tokens)) {
    return INVALID_MOVE;
  }

  for (const [res, count] of Object.entries(requestedTokens)) {
    G.tokens[res] -= count;
    player.tokens[res] += count;
  }

  checkBonusAndEndTurn(G, ctx);
}

function findCardOnTheTable(G, level, cardId) {
  return G.table[level].find((card) => card && card.id === cardId);
}

function removeCardFromTheTable(G, level, cardId) {
  G.table[level] = G.table[level].map((card) => (card && card.id === cardId ? null : card));
}

function payForCard(player, card, publicTokens) {
  for (const [res, cost] of Object.entries(card.cost)) {
    const tokens = player.tokens[res];
    const cards = player.cards[res];
    let tokensPaid = 0;
    let goldPaid = 0;
    if (tokens + cards >= cost) {
      tokensPaid = Math.max(cost - cards, 0);
    } else {
      tokensPaid = player.tokens[res];
      goldPaid = cost - (tokens + cards);
    }
    player.tokens[res] -= tokensPaid;
    player.tokens.gold -= goldPaid;
    publicTokens[res] += tokensPaid;
    publicTokens.gold += goldPaid;
  }
}

function BuyCard(G, ctx, level, cardId) {
  const player = G.players[ctx.currentPlayer];
  const { tokens, cards } = player;
  const card = findCardOnTheTable(G, level, cardId);

  if (!canBuyCard(tokens, cards, card)) {
    return INVALID_MOVE;
  }

  payForCard(player, card, G.tokens);

  player.cards[card.resource] += 1;
  G.points[ctx.currentPlayer] += card.points;
  removeCardFromTheTable(G, level, cardId);

  checkBonusAndEndTurn(G, ctx);
}

function BuyReserved(G, ctx, cardId) {
  const player = G.players[ctx.currentPlayer];
  const { tokens, cards } = player;
  const card = player.reservedCards.find((card) => card.id === cardId);

  if (!canBuyCard(tokens, cards, card)) {
    return INVALID_MOVE;
  }

  payForCard(player, card, G.tokens);

  player.cards[card.resource] += 1;
  G.points[ctx.currentPlayer] += card.points;
  player.reservedCards = player.reservedCards.filter((card) => card.id !== cardId);

  checkBonusAndEndTurn(G, ctx);
}

function ReserveCard(G, ctx, level, cardId) {
  const player = G.players[ctx.currentPlayer];
  const card = findCardOnTheTable(G, level, cardId);

  if (!card || player.reservedCards.length >= 3) {
    return INVALID_MOVE;
  }

  if (sum(Object.values(player.tokens)) < 10 && G.tokens.gold > 0) {
    player.tokens.gold += 1;
    G.tokens.gold -= 1;
  }
  player.reservedCards.push(card);
  removeCardFromTheTable(G, level, cardId);

  checkBonusAndEndTurn(G, ctx);
}

function DiscardToken(G, ctx, resource) {
  const player = G.players[ctx.currentPlayer];
  if (player.tokens[resource] <= 0) {
    return INVALID_MOVE;
  }

  player.tokens[resource] -= 1;
  G.tokens[resource] += 1;
}

function TakeBonus(G, ctx, id) {
  const player = G.players[ctx.currentPlayer];
  const bonus = G.bonuses.find((bonus) => bonus.id === id);
  if (!bonus || !canTakeBonus(player.cards, bonus)) {
    return INVALID_MOVE;
  }

  G.points[ctx.currentPlayer] += bonus.points;
  player.bonuses.push(bonus);
  G.bonuses = G.bonuses.filter((bonus) => bonus.id !== id);
  ctx.events.endStage();
  ctx.events.endTurn();
}

function checkBonusAndEndTurn(G, ctx) {
  const player = G.players[ctx.currentPlayer];
  if (G.bonuses.some((bonus) => canTakeBonus(player.cards, bonus))) {
    ctx.events.setStage("bonus");
  } else {
    ctx.events.endTurn();
  }
}

function addCardsToTheTable(G) {
  Object.keys(G.table).forEach((level) => {
    G.table[level] = G.table[level].map((card) => card || G.deck[level].pop() || null);
  });
}

export const Splendid = {
  name: "Splendid",
  image: "/images/games/splendid/icon.png",
  minPlayers: 2,
  maxPlayers: 5,

  seed: process.env.NODE_ENV === "production" ? undefined : "test",
  setup: setupGame,
  plugins: [PluginActions()],

  phases: {
    wait: {
      start: true,
      next: "play",
      turn: {
        onBegin: (G, ctx) => {
          ctx.actions.log(ctx.currentPlayer, "manage");
          ctx.events.setActivePlayers({ currentPlayer: "manage", others: "wait" });
        },
        stages: {
          manage: {
            moves: {
              StartGame,
            },
          },
          wait: {
            moves: {},
          },
        },
      },
    },
    play: {
      moves: { TakeTokens, BuyCard, ReserveCard, DiscardToken, BuyReserved },
      turn: {
        onBegin: (G, ctx) => {
          addCardsToTheTable(G);
          if (G.lastPlayer === ctx.currentPlayer) {
            G.calculateWinner = true;
          }
        },
        onEnd: (G, ctx) => {
          const points = G.points[ctx.currentPlayer];
          if (points >= WINNING_POINTS && !G.lastPlayer) {
            G.lastPlayer = ctx.currentPlayer;
          }
        },
        order: TurnOrder.RESET,
        events: {
          endGame: false,
          endTurn: false,
        },
        stages: {
          bonus: {
            moves: { TakeBonus },
          },
        },
      },
    },
  },
  endIf: (G, ctx) => {
    if (G.calculateWinner) {
      const cardsCount = (playerID) => sum(Object.values(G.players[playerID].cards));
      const bonusesCount = (playerID) => G.player[playerID].bonuses.length;
      const winner = Object.keys(G.players).sort(
        (a, b) =>
          G.points[b] - G.points[a] ||
          cardsCount(a) - cardsCount(b) ||
          bonusesCount(b) - bonusesCount(a)
      )[0];
      return { winners: [winner] };
    }
  },
};