From fa1c33c65e456b1fa0e2e19aeb61e042445e48f5 Mon Sep 17 00:00:00 2001 From: Daithi Hearn Date: Sun, 28 Jan 2024 20:53:53 +0100 Subject: [PATCH] fix: more fixes based on api changes --- src/caches/GameSlice.ts | 85 +++++++++++++++--- src/caches/MyCardsSlice.ts | 90 ------------------- src/caches/caches.ts | 4 - src/components/Game/Actions/Buying.tsx | 10 +-- src/components/Game/Actions/PlayCard.tsx | 11 ++- src/components/Game/Actions/SelectSuit.tsx | 10 ++- .../Game/Actions/ThrowCardsWarningModal.tsx | 4 +- src/components/Game/GameWrapper.tsx | 14 ++- src/components/Game/MyCards.tsx | 16 ++-- src/components/Hooks/useGameActions.tsx | 11 +-- src/components/Hooks/useGameState.tsx | 22 ++--- src/model/Game.ts | 8 +- src/pages/Game/Game.tsx | 8 +- src/utils/EventUtils.ts | 32 +++---- 14 files changed, 145 insertions(+), 180 deletions(-) delete mode 100644 src/caches/MyCardsSlice.ts diff --git a/src/caches/GameSlice.ts b/src/caches/GameSlice.ts index 8cb35ce..2b108cc 100644 --- a/src/caches/GameSlice.ts +++ b/src/caches/GameSlice.ts @@ -1,9 +1,11 @@ import { createSelector, createSlice, PayloadAction } from "@reduxjs/toolkit" -import { GameState, GameStatus, PlayedCard } from "model/Game" -import { Player } from "model/Player" +import { GameState, GameStateResponse, GameStatus } from "model/Game" import { RoundStatus } from "model/Round" import { RootState } from "./caches" +import { processOrderedCardsAfterGameUpdate } from "utils/GameUtils" +import { Card, EMPTY } from "model/Cards" +import { determineEvent } from "utils/EventUtils" export const initialGameState: GameState = { revision: -1, @@ -13,6 +15,7 @@ export const initialGameState: GameState = { iamDealer: false, iamAdmin: false, cards: [], + cardsFull: [], status: GameStatus.NONE, players: [], } @@ -21,16 +24,53 @@ export const gameSlice = createSlice({ name: "game", initialState: initialGameState, reducers: { - updateGame: (_, action: PayloadAction) => action.payload, - updatePlayers: (state, action: PayloadAction) => { - state.players = action.payload + updateGame: (state, action: PayloadAction) => { + const updatedGame: GameState = { + ...action.payload, + cardsFull: processOrderedCardsAfterGameUpdate( + state.cardsFull, + action.payload.cards, + ), + } + + const event = determineEvent(state, updatedGame) + + console.log("event", event) + + return updatedGame + }, + selectCard: (state, action: PayloadAction) => { + state.cardsFull.forEach(c => { + if (c.name === action.payload.name) c.selected = true + }) }, - updatePlayedCards: (state, action: PayloadAction) => { - if (state.round) - state.round.currentHand.playedCards = action.payload + selectCards: (state, action: PayloadAction) => { + state.cardsFull.forEach(c => { + if (action.payload.some(a => a.name === c.name)) + c.selected = true + }) }, - disableActions: state => { - state.isMyGo = false + toggleSelect: (state, action: PayloadAction) => + state.cardsFull.forEach(c => { + if (c.name === action.payload.name) c.selected = !c.selected + }), + toggleUniqueSelect: (state, action: PayloadAction) => + state.cardsFull.forEach(c => { + if (c.name === action.payload.name) c.selected = !c.selected + else c.selected = false + }), + selectAll: state => { + state.cardsFull.forEach(c => { + if (c.name !== EMPTY.name) c.selected = true + }) + }, + clearSelectedCards: state => { + state.cardsFull.forEach(c => { + c.selected = false + }) + }, + replaceMyCards: (state, action: PayloadAction) => { + state.cardsFull = action.payload }, resetGame: () => initialGameState, }, @@ -38,10 +78,14 @@ export const gameSlice = createSlice({ export const { updateGame, - disableActions, - updatePlayedCards, - updatePlayers, resetGame, + toggleSelect, + toggleUniqueSelect, + selectCard, + selectCards, + selectAll, + clearSelectedCards, + replaceMyCards, } = gameSlice.actions export const getGame = (state: RootState) => state.game @@ -54,6 +98,14 @@ export const getMe = createSelector(getGame, game => game.me) export const getRound = createSelector(getGame, game => game.round) export const getCards = createSelector(getGame, game => game.cards) +export const getCardsFull = createSelector(getGame, game => game.cardsFull) +export const getCardsWithoutBlanks = createSelector(getCardsFull, cards => + cards.filter(c => c.name !== EMPTY.name), +) +export const getSelectedCards = createSelector(getCardsFull, cards => + cards.filter(c => c.selected), +) + export const getSuit = createSelector(getRound, round => round?.suit) export const getGameId = createSelector(getGame, game => game.id) @@ -68,6 +120,11 @@ export const getIsGameActive = createSelector( status => status === GameStatus.ACTIVE, ) +export const getIsGameCompleted = createSelector( + getGameStatus, + status => status === GameStatus.COMPLETED, +) + export const getRoundStatus = createSelector(getRound, round => round?.status) export const getIsRoundCalling = createSelector( getRoundStatus, @@ -120,3 +177,5 @@ export const getIsInBunker = createSelector( (isMyGo, isRoundCalling, me) => isMyGo && isRoundCalling && me && me?.score < -30, ) + +export const getRevision = createSelector(getGame, game => game.revision) diff --git a/src/caches/MyCardsSlice.ts b/src/caches/MyCardsSlice.ts deleted file mode 100644 index a55bf9b..0000000 --- a/src/caches/MyCardsSlice.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { createSelector, createSlice, PayloadAction } from "@reduxjs/toolkit" -import { CardName, EMPTY, Card } from "model/Cards" -import { processOrderedCardsAfterGameUpdate } from "utils/GameUtils" -import { RootState } from "./caches" - -export interface MyCardsState { - cards: Card[] -} - -const initialState: MyCardsState = { - cards: [], -} - -export const myCardsSlice = createSlice({ - name: "myCards", - initialState: initialState, - reducers: { - updateMyCards: (state, action: PayloadAction) => { - return { - cards: processOrderedCardsAfterGameUpdate( - state.cards, - action.payload, - ), - } - }, - replaceMyCards: (_, action: PayloadAction) => { - return { - cards: action.payload, - } - }, - removeCard: (state, action: PayloadAction) => { - const idx = state.cards.findIndex(c => c.name === action.payload) - if (idx > 0) state.cards[idx] = { ...EMPTY, selected: false } - }, - selectCard: (state, action: PayloadAction) => { - state.cards.forEach(c => { - if (c.name === action.payload.name) c.selected = true - }) - }, - selectCards: (state, action: PayloadAction) => { - state.cards.forEach(c => { - if (action.payload.some(a => a.name === c.name)) - c.selected = true - }) - }, - toggleSelect: (state, action: PayloadAction) => - state.cards.forEach(c => { - if (c.name === action.payload.name) c.selected = !c.selected - }), - toggleUniqueSelect: (state, action: PayloadAction) => - state.cards.forEach(c => { - if (c.name === action.payload.name) c.selected = !c.selected - else c.selected = false - }), - selectAll: state => { - state.cards.forEach(c => { - if (c.name !== EMPTY.name) c.selected = true - }) - }, - clearSelectedCards: state => { - state.cards.forEach(c => { - c.selected = false - }) - }, - clearMyCards: () => initialState, - }, -}) - -export const { - updateMyCards, - replaceMyCards, - removeCard, - clearSelectedCards, - selectAll, - selectCard, - selectCards, - toggleSelect, - toggleUniqueSelect, - clearMyCards, -} = myCardsSlice.actions - -export const getMyCards = (state: RootState) => state.myCards.cards - -export const getMyCardsWithoutBlanks = createSelector(getMyCards, cards => - cards.filter(c => c.name !== EMPTY.name), -) - -export const getSelectedCards = createSelector(getMyCards, cards => - cards.filter(c => c.selected), -) diff --git a/src/caches/caches.ts b/src/caches/caches.ts index 98f271e..86755b0 100644 --- a/src/caches/caches.ts +++ b/src/caches/caches.ts @@ -1,19 +1,15 @@ import { - Action, AnyAction, combineReducers, configureStore, Reducer, - ThunkDispatch, } from "@reduxjs/toolkit" import { gameSlice } from "./GameSlice" -import { myCardsSlice } from "./MyCardsSlice" import { playCardSlice } from "./PlayCardSlice" const combinedReducer = combineReducers({ game: gameSlice.reducer, - myCards: myCardsSlice.reducer, playCard: playCardSlice.reducer, }) diff --git a/src/components/Game/Actions/Buying.tsx b/src/components/Game/Actions/Buying.tsx index 3caab07..60e8bfc 100644 --- a/src/components/Game/Actions/Buying.tsx +++ b/src/components/Game/Actions/Buying.tsx @@ -1,11 +1,6 @@ import { useCallback, useEffect, useState } from "react" import { useAppDispatch, useAppSelector } from "caches/hooks" -import { - getMyCardsWithoutBlanks, - getSelectedCards, - selectAll, -} from "caches/MyCardsSlice" import { getGameId, getNumPlayers, @@ -13,6 +8,9 @@ import { getIHavePlayed, getIsMyGo, getSuit, + getCardsWithoutBlanks, + getSelectedCards, + selectAll, } from "caches/GameSlice" import { pickBestCards, riskOfMistakeBuyingCards } from "utils/GameUtils" import ThrowCardsWarningModal from "./ThrowCardsWarningModal" @@ -35,7 +33,7 @@ const Buying = () => { const numPlayers = useAppSelector(getNumPlayers) const gameId = useAppSelector(getGameId) const suit = useAppSelector(getSuit) - const myCards = useAppSelector(getMyCardsWithoutBlanks) + const myCards = useAppSelector(getCardsWithoutBlanks) const [readyToBuy, setReadyToBuy] = useState(false) const iHavePlayed = useAppSelector(getIHavePlayed) const isMyGo = useAppSelector(getIsMyGo) diff --git a/src/components/Game/Actions/PlayCard.tsx b/src/components/Game/Actions/PlayCard.tsx index b92a370..d56c7a7 100644 --- a/src/components/Game/Actions/PlayCard.tsx +++ b/src/components/Game/Actions/PlayCard.tsx @@ -1,8 +1,13 @@ import { useCallback, useEffect, useMemo, useState } from "react" import { useAppDispatch, useAppSelector } from "caches/hooks" -import { getMyCardsWithoutBlanks, getSelectedCards } from "caches/MyCardsSlice" -import { getGameId, getIsMyGo, getRound } from "caches/GameSlice" +import { + getCardsWithoutBlanks, + getGameId, + getIsMyGo, + getRound, + getSelectedCards, +} from "caches/GameSlice" import { RoundStatus } from "model/Round" import { Button, @@ -27,7 +32,7 @@ const PlayCard = () => { const { playCard } = useGameActions() const round = useAppSelector(getRound) const gameId = useAppSelector(getGameId) - const myCards = useAppSelector(getMyCardsWithoutBlanks) + const myCards = useAppSelector(getCardsWithoutBlanks) const isMyGo = useAppSelector(getIsMyGo) const [autoPlay, setAutoPlay] = useState("off") diff --git a/src/components/Game/Actions/SelectSuit.tsx b/src/components/Game/Actions/SelectSuit.tsx index 1e48d6b..ad54c3c 100644 --- a/src/components/Game/Actions/SelectSuit.tsx +++ b/src/components/Game/Actions/SelectSuit.tsx @@ -2,13 +2,17 @@ import { useCallback, useState } from "react" import { Suit } from "model/Suit" import { useSnackbar } from "notistack" -import { getMyCardsWithoutBlanks, getSelectedCards } from "caches/MyCardsSlice" import { removeAllFromHand } from "utils/GameUtils" import ThrowCardsWarningModal from "./ThrowCardsWarningModal" import { CardName, Card } from "model/Cards" import { Button } from "@mui/material" import { useAppSelector } from "caches/hooks" -import { getGameId, getIamGoer } from "caches/GameSlice" +import { + getCardsWithoutBlanks, + getGameId, + getIamGoer, + getSelectedCards, +} from "caches/GameSlice" import { useGameActions } from "components/Hooks/useGameActions" const WaitingForSuit = () => ( @@ -22,7 +26,7 @@ const SelectSuit = () => { const { selectSuit } = useGameActions() const gameId = useAppSelector(getGameId) - const myCards = useAppSelector(getMyCardsWithoutBlanks) + const myCards = useAppSelector(getCardsWithoutBlanks) const iamGoer = useAppSelector(getIamGoer) const [selectedSuit, setSelectedSuit] = useState() diff --git a/src/components/Game/Actions/ThrowCardsWarningModal.tsx b/src/components/Game/Actions/ThrowCardsWarningModal.tsx index 80e6b1e..fb04227 100644 --- a/src/components/Game/Actions/ThrowCardsWarningModal.tsx +++ b/src/components/Game/Actions/ThrowCardsWarningModal.tsx @@ -10,10 +10,10 @@ import { CardContent, } from "@mui/material" import { useAppSelector } from "caches/hooks" -import { getMyCardsWithoutBlanks, getSelectedCards } from "caches/MyCardsSlice" import { Card } from "model/Cards" import { Suit } from "model/Suit" import { getTrumpCards, removeAllFromHand } from "utils/GameUtils" +import { getCardsWithoutBlanks, getSelectedCards } from "caches/GameSlice" interface ModalOpts { modalVisible: boolean @@ -28,7 +28,7 @@ const ThrowCardsWarningModal: React.FC = ({ continueCallback, suit, }) => { - const myCards = useAppSelector(getMyCardsWithoutBlanks) + const myCards = useAppSelector(getCardsWithoutBlanks) const selectedCards = useAppSelector(getSelectedCards) const cardsToBeThrown = useMemo( diff --git a/src/components/Game/GameWrapper.tsx b/src/components/Game/GameWrapper.tsx index 234f994..cc19493 100644 --- a/src/components/Game/GameWrapper.tsx +++ b/src/components/Game/GameWrapper.tsx @@ -23,15 +23,13 @@ const GameWrapper = () => { const iamSpectator = useAppSelector(getIamSpectator) return ( - <> - - + + - {!iamSpectator ? : null} - {!iamSpectator ? : null} - {!iamSpectator ? : null} - - + {!iamSpectator ? : null} + {!iamSpectator ? : null} + {!iamSpectator ? : null} + ) } diff --git a/src/components/Game/MyCards.tsx b/src/components/Game/MyCards.tsx index ac5b92e..cd19815 100644 --- a/src/components/Game/MyCards.tsx +++ b/src/components/Game/MyCards.tsx @@ -8,14 +8,6 @@ import { import { EMPTY, Card } from "model/Cards" import { RoundStatus } from "model/Round" import { useAppDispatch, useAppSelector } from "caches/hooks" -import { - clearSelectedCards, - getMyCards, - replaceMyCards, - selectCards, - toggleSelect, - toggleUniqueSelect, -} from "caches/MyCardsSlice" import { getCardToPlay, togglePlayCard, @@ -24,11 +16,17 @@ import { import { CardContent, CardMedia, useTheme } from "@mui/material" import { pickBestCards } from "utils/GameUtils" import { + clearSelectedCards, + getCardsFull, getGameId, getIamGoer, getIsRoundCalled, getNumPlayers, getRound, + replaceMyCards, + selectCards, + toggleSelect, + toggleUniqueSelect, } from "caches/GameSlice" const EMPTY_HAND = [ @@ -55,7 +53,7 @@ const MyCards = () => { const gameId = useAppSelector(getGameId) const numPlayers = useAppSelector(getNumPlayers) const isRoundCalled = useAppSelector(getIsRoundCalled) - const myCards = useAppSelector(getMyCards) + const myCards = useAppSelector(getCardsFull) const autoPlayCard = useAppSelector(getCardToPlay) const iamGoer = useAppSelector(getIamGoer) const prevRoundStatus = usePrevious(round?.status) diff --git a/src/components/Hooks/useGameActions.tsx b/src/components/Hooks/useGameActions.tsx index b5f2591..bb5cfae 100644 --- a/src/components/Hooks/useGameActions.tsx +++ b/src/components/Hooks/useGameActions.tsx @@ -2,10 +2,9 @@ import { useMutation, useQueryClient } from "@tanstack/react-query" import useAccessToken from "auth/accessToken" import axios from "axios" import { initialGameState, updateGame } from "caches/GameSlice" -import { updateMyCards } from "caches/MyCardsSlice" import { useAppDispatch } from "caches/hooks" import { Card, CardName } from "model/Cards" -import { GameState } from "model/Game" +import { GameStateResponse } from "model/Game" import { Suit } from "model/Suit" import { useSnackbar } from "notistack" import { getDefaultConfig } from "utils/AxiosUtils" @@ -28,7 +27,7 @@ export const useGameActions = () => { if (!accessToken) { return initialGameState } - const response = await axios.put( + const response = await axios.put( `${process.env.REACT_APP_API_URL}/api/v1/game/${gameId}/call?call=${call}`, null, getDefaultConfig(accessToken), @@ -38,7 +37,6 @@ export const useGameActions = () => { onSuccess: data => { queryClient.setQueryData(["gameState"], data) dispatch(updateGame(data)) - dispatch(updateMyCards(data.cards)) }, onError: e => enqueueSnackbar(parseError(e), { variant: "error" }), }) @@ -54,7 +52,7 @@ export const useGameActions = () => { if (!accessToken) { return initialGameState } - const response = await axios.put( + const response = await axios.put( `${process.env.REACT_APP_API_URL}/api/v1/game/${gameId}/buy`, { cards }, getDefaultConfig(accessToken), @@ -64,7 +62,6 @@ export const useGameActions = () => { onSuccess: data => { queryClient.setQueryData(["gameState"], data) dispatch(updateGame(data)) - dispatch(updateMyCards(data.cards)) }, onError: e => enqueueSnackbar(parseError(e), { variant: "error" }), }) @@ -95,7 +92,6 @@ export const useGameActions = () => { onSuccess: data => { queryClient.setQueryData(["gameState"], data) dispatch(updateGame(data)) - dispatch(updateMyCards(data.cards)) }, onError: e => enqueueSnackbar(parseError(e), { variant: "error" }), }) @@ -121,7 +117,6 @@ export const useGameActions = () => { onSuccess: data => { queryClient.setQueryData(["gameState"], data) dispatch(updateGame(data)) - dispatch(updateMyCards(data.cards)) }, onError: e => enqueueSnackbar(parseError(e), { variant: "error" }), }) diff --git a/src/components/Hooks/useGameState.tsx b/src/components/Hooks/useGameState.tsx index 81bc1e9..567fd77 100644 --- a/src/components/Hooks/useGameState.tsx +++ b/src/components/Hooks/useGameState.tsx @@ -1,42 +1,38 @@ import { useQuery } from "@tanstack/react-query" import useAccessToken from "auth/accessToken" import axios from "axios" -import { initialGameState, updateGame } from "caches/GameSlice" -import { updateMyCards } from "caches/MyCardsSlice" -import { useAppDispatch } from "caches/hooks" -import { GameState } from "model/Game" -import { useState } from "react" +import { getRevision, initialGameState, updateGame } from "caches/GameSlice" +import { useAppDispatch, useAppSelector } from "caches/hooks" +import { GameStateResponse } from "model/Game" import { getDefaultConfig } from "utils/AxiosUtils" -export const useGameState = (gameId: string) => { +export const useGameState = (gameId?: string) => { const dispatch = useAppDispatch() const { accessToken } = useAccessToken() - const [revision, setRevision] = useState(-1) + const revision = useAppSelector(getRevision) // Game state query useQuery({ queryKey: ["gameState", accessToken, revision, gameId], queryFn: async () => { - if (!accessToken) { + if (!accessToken || !gameId) { // Handle the case when accessToken is undefined // For example, return a default value or throw an error return initialGameState } - const res = await axios.get( + const res = await axios.get( `${process.env.REACT_APP_API_URL}/api/v1/game/${gameId}/state?revision=${revision}`, getDefaultConfig(accessToken), ) if (res.status === 200) { - setRevision(res.data.revision) dispatch(updateGame(res.data)) - dispatch(updateMyCards(res.data.cards)) } return res.data }, // Refetch the data every 2 seconds - refetchInterval: 2_000, - enabled: !!accessToken, + refetchInterval: 10_000, + enabled: !!accessToken && !!gameId, }) } diff --git a/src/model/Game.ts b/src/model/Game.ts index 535cc26..c30fea8 100644 --- a/src/model/Game.ts +++ b/src/model/Game.ts @@ -1,4 +1,4 @@ -import { CardName } from "./Cards" +import { Card, CardName } from "./Cards" import { Player } from "./Player" import { Round } from "./Round" @@ -26,7 +26,7 @@ export interface PlayedCard { card: CardName } -export interface GameState { +export interface GameStateResponse { id?: string revision: number iamSpectator: boolean @@ -42,6 +42,10 @@ export interface GameState { players: Player[] } +export interface GameState extends GameStateResponse { + cardsFull: Card[] +} + export interface CreateGame { players: string[] name: string diff --git a/src/pages/Game/Game.tsx b/src/pages/Game/Game.tsx index 38700a9..e291c8f 100644 --- a/src/pages/Game/Game.tsx +++ b/src/pages/Game/Game.tsx @@ -8,18 +8,20 @@ import { useParams } from "react-router-dom" import { useGameState } from "components/Hooks/useGameState" import { useAppSelector } from "caches/hooks" -import { getIsGameActive } from "caches/GameSlice" +import { getIsGameActive, getIsGameCompleted } from "caches/GameSlice" const Game = () => { let { id } = useParams() - if (id) useGameState(id) + useGameState(id) const isGameActive = useAppSelector(getIsGameActive) + const isGameCompleted = useAppSelector(getIsGameCompleted) return (
- {isGameActive ? : } + {isGameActive && } + {isGameCompleted && }
diff --git a/src/utils/EventUtils.ts b/src/utils/EventUtils.ts index 96cae2d..b27b4c5 100644 --- a/src/utils/EventUtils.ts +++ b/src/utils/EventUtils.ts @@ -1,5 +1,5 @@ import { Actions } from "model/Events" -import { GameState, GameStatus } from "model/Game" +import { GameStateResponse, GameStatus } from "model/Game" import { RoundStatus } from "model/Round" import { Player } from "model/Player" @@ -17,8 +17,8 @@ const callsChanged = (prev: Player[], curr: Player[]): boolean => { } export const isCallEvent = ( - prevState: GameState, - currState: GameState, + prevState: GameStateResponse, + currState: GameStateResponse, ): boolean => { return ( prevState.round?.status === RoundStatus.CALLING && @@ -29,8 +29,8 @@ export const isCallEvent = ( } export const isPassEvent = ( - prevState: GameState, - currState: GameState, + prevState: GameStateResponse, + currState: GameStateResponse, ): boolean => { return ( prevState.round?.status === RoundStatus.CALLING && @@ -40,21 +40,21 @@ export const isPassEvent = ( ) } -export const isSelectSuitEvent = (prevState: GameState): boolean => { +export const isSelectSuitEvent = (prevState: GameStateResponse): boolean => { return prevState.round?.status === RoundStatus.CALLED } -export const isBuyCardsEvent = (prevState: GameState): boolean => { +export const isBuyCardsEvent = (prevState: GameStateResponse): boolean => { return prevState.round?.status === RoundStatus.BUYING } -export const isCardPlayedEvent = (prevState: GameState): boolean => { +export const isCardPlayedEvent = (prevState: GameStateResponse): boolean => { return prevState.round?.status === RoundStatus.PLAYING } export const isHandEndEvent = ( - prevState: GameState, - currState: GameState, + prevState: GameStateResponse, + currState: GameStateResponse, ): boolean => { return ( prevState.round?.status === RoundStatus.PLAYING && @@ -65,8 +65,8 @@ export const isHandEndEvent = ( } export const isRoundEndEvent = ( - prevState: GameState, - currState: GameState, + prevState: GameStateResponse, + currState: GameStateResponse, ): boolean => { return ( prevState.round?.status === RoundStatus.PLAYING && @@ -76,8 +76,8 @@ export const isRoundEndEvent = ( } export const isGameOverEvent = ( - prevState: GameState, - currState: GameState, + prevState: GameStateResponse, + currState: GameStateResponse, ): boolean => { return ( prevState.status === GameStatus.ACTIVE && @@ -86,8 +86,8 @@ export const isGameOverEvent = ( } export const determineEvent = ( - prevState: GameState, - currState: GameState, + prevState: GameStateResponse, + currState: GameStateResponse, ): Actions => { if (isCallEvent(prevState, currState)) { return Actions.Call