mavend/octoboard

View on GitHub
src/games/splendid/Board/index.js

Summary

Maintainability
B
6 hrs
Test Coverage
F
52%
import React, { useState, useEffect, useCallback } from "react";
import { Segment } from "semantic-ui-react";
import { toast } from "react-toastify";
import { sum } from "lodash";
import Confetti from "react-dom-confetti";

import { useBoardGame } from "contexts/BoardGameContext";
import GameLayout from "components/layout/GameLayout";
import { WIDE_CONFETTI } from "config/confetti";
import { canBuyCard, canTakeBonus } from "../utils";
import WaitingBoard from "./WaitingBoard";
import BonusCards from "./BonusCards";
import TokensShop from "./TokensShop";
import PlayerInfo from "./PlayerInfo";
import CardsTable from "./CardsTable";
import ReservedCards from "./ReservedCards";
import BonusCardsModal from "./BonusCardsModal";

import styles from "./Board.module.css";

const Board = () => {
  const { G, ctx, moves, players, player, playerID } = useBoardGame();
  const [selectedCard, setSelectedCard] = useState(null);
  const [availableBonuses, setAvailableBonuses] = useState(null);
  const [loading, setLoading] = useState(null);
  const [confetti, setConfetti] = useState(false);

  const hasGameStarted = ctx.phase !== "wait";
  const isActivePlayer = ctx.currentPlayer === playerID && !ctx.gameover;

  const lastRoundPlayer = G.lastPlayer && players.find((p) => p.id.toString() === G.lastPlayer);
  const lastRoundPlayerName = lastRoundPlayer && lastRoundPlayer.profile.displayName;

  const winnerID = ctx.gameover && ctx.gameover.winner.toString();
  const winner = winnerID && players.find((p) => p.id.toString() === winnerID);
  const winnerName = winner && winner.profile.displayName;
  const isWinner = winnerID && winnerID.toString() === playerID.toString();

  useEffect(() => {
    setSelectedCard(null);
    setLoading(false);
    if (isActivePlayer && hasGameStarted) {
      toast.success("🎲 Your turn!", { autoClose: 2000 });
    }
  }, [isActivePlayer, hasGameStarted, setSelectedCard, setLoading]);

  useEffect(() => {
    if (lastRoundPlayerName && !winnerName) {
      toast.warn(`This is a last round. ${lastRoundPlayerName} scored at least 15 points.`);
    }
  }, [lastRoundPlayerName, winnerName]);

  useEffect(() => {
    if (winnerName) {
      toast.success(isWinner ? "🏅You won! 🎉" : `🏅${winnerName} won the game!`, {
        autoClose: false,
        closeButton: false,
        draggable: false,
      });
      if (isWinner) {
        setConfetti(true);
      }
    }
  }, [winnerName, isWinner, setConfetti]);

  useEffect(() => {
    const bonuses = G.bonuses.filter((bonus) => canTakeBonus(player.cards, bonus));
    setAvailableBonuses(bonuses);
  }, [player.cards, G.bonuses, setAvailableBonuses]);

  const selectCard = useCallback(
    (cardId) => {
      if (loading) return;
      setSelectedCard((id) => (id === cardId ? null : cardId));
    },
    [loading, setSelectedCard]
  );

  const canBuy = useCallback(
    (card) => {
      const { tokens, cards } = player;
      return canBuyCard(tokens, cards, card);
    },
    [player]
  );

  const buyCard = useCallback(
    (card) => {
      if (canBuy(card)) {
        const { level, id } = card;
        setLoading(true);
        moves.BuyCard(level, id);
      } else {
        toast.error("⛔️Not enough resources");
      }
    },
    [moves, canBuy]
  );

  const buyReservedCard = useCallback(
    (card) => {
      if (canBuy(card)) {
        const { id } = card;
        setLoading(true);
        moves.BuyReserved(id);
      } else {
        toast.error("⛔️Not enough resources");
      }
    },
    [moves, canBuy]
  );

  const reserveCard = useCallback(
    (card) => {
      const playerTokens = sum(Object.values(player.tokens));
      if (playerTokens < 10) {
        const { level, id } = card;
        setLoading(true);
        moves.ReserveCard(level, id);
      } else {
        toast.error("⛔️You can't have more than 10 tokens!");
      }
    },
    [player, moves, setLoading]
  );

  const takeTokens = useCallback(
    (tokens) => {
      const takenTokens = sum(Object.values(tokens));
      const playerTokens = sum(Object.values(player.tokens));
      if (playerTokens + takenTokens <= 10) {
        setLoading(true);
        moves.TakeTokens(tokens);
      } else {
        toast.error("⛔️You can't have more than 10 tokens!");
      }
    },
    [player, moves, setLoading]
  );

  const discardToken = useCallback(
    (resource) => {
      if (player.tokens[resource] > 0) {
        setLoading(true);
        moves.DiscardToken(resource);
      } else {
        toast.error("⛔️Not enough resources");
      }
    },
    [player, moves]
  );

  const takeBonus = useCallback(
    (id) => {
      moves.TakeBonus(id);
    },
    [moves]
  );

  const extraPlayerContent = useCallback(
    ({ isYou, isCurrentPlayer, tokens, cards, reservedCards }) => (
      <PlayerInfo
        tokens={tokens}
        cards={cards}
        onDiscardToken={isYou && isCurrentPlayer ? discardToken : undefined}
      >
        <ReservedCards
          cards={reservedCards}
          selectedCard={selectedCard}
          onSelect={selectCard}
          loading={loading}
          active={isCurrentPlayer}
          onBuy={buyReservedCard}
          canBuy={canBuy}
        />
      </PlayerInfo>
    ),
    [selectedCard, loading, buyReservedCard, selectCard, canBuy, discardToken]
  );

  if (!hasGameStarted) {
    return (
      <WaitingBoard
        canManageGame={player.canManageGame}
        currentPlayers={players.filter((p) => p.uid).length}
        totalPlayers={ctx.numPlayers}
        onStartGame={() => moves.StartGame()}
      />
    );
  }

  return (
    <GameLayout
      gameName={"Splendid"}
      privateMatch={G.privateMatch}
      header={
        <Segment className={styles.topBar}>
          <BonusCards bonuses={G.bonuses || []} />
          <TokensShop
            tokens={G.tokens}
            active={isActivePlayer}
            loading={loading}
            onTakeTokens={takeTokens}
            playerTokensCount={sum(Object.values(player.tokens || {}))}
          />
        </Segment>
      }
      sidebarHeader={<></>}
      sidebarSize={5}
      extraPlayerContent={extraPlayerContent}
    >
      <Segment className={styles.mainBoard}>
        {G.table && (
          <CardsTable
            table={G.table}
            active={isActivePlayer}
            selectedCard={selectedCard}
            onSelect={selectCard}
            loading={loading}
            onBuy={buyCard}
            canBuy={canBuy}
            onReserve={reserveCard}
            canReserve={(player.reservedCards || []).length < 3}
          />
        )}
      </Segment>
      <BonusCardsModal
        open={player.stage === "bonus"}
        bonuses={availableBonuses}
        onTake={takeBonus}
      />
      <Confetti active={confetti} config={WIDE_CONFETTI} className="confetti" />
    </GameLayout>
  );
};

export default Board;