From c18b06af2b5e68d3c84f0d7121115aef4606b5cf Mon Sep 17 00:00:00 2001 From: Daithi Hearn Date: Sat, 14 Jan 2023 11:20:37 +0100 Subject: [PATCH 1/3] Improving AutoPlay and select functionality --- src/components/Game/MyCards.tsx | 25 +++++++++++++++++-------- 1 file changed, 17 insertions(+), 8 deletions(-) diff --git a/src/components/Game/MyCards.tsx b/src/components/Game/MyCards.tsx index 8c5793f..beaccba 100644 --- a/src/components/Game/MyCards.tsx +++ b/src/components/Game/MyCards.tsx @@ -14,12 +14,17 @@ import { getGameId, getIsMyGo, getRound } from "../../caches/GameSlice" import { useAppDispatch, useAppSelector } from "../../caches/hooks" import { useSnackbar } from "notistack" import { + clearSelectedCards, getMyCards, replaceMyCards, toggleSelect, toggleUniqueSelect, } from "../../caches/MyCardsSlice" -import { getAutoPlayCard, toggleAutoPlay } from "../../caches/AutoPlaySlice" +import { + getAutoPlayCard, + toggleAutoPlay, + clearAutoPlay, +} from "../../caches/AutoPlaySlice" interface DoubleClickTracker { time: number @@ -73,20 +78,24 @@ const MyCards: React.FC = () => { // If the round status is PLAYING then only allow one card to be selected if (round && round.status === RoundStatus.PLAYING) { - if ( - doubleClickTracker && - doubleClickTracker.card === card.name && + if (autoPlayCard === card.name) { + dispatch(clearAutoPlay()) + dispatch(clearSelectedCards()) + } else if ( + doubleClickTracker?.card === card.name && Date.now() - doubleClickTracker.time < 500 ) { dispatch(toggleAutoPlay(card)) - } else dispatch(toggleUniqueSelect(card)) - - updateDoubleClickTracker({ card: card.name, time: Date.now() }) + } else { + dispatch(toggleUniqueSelect(card)) + dispatch(clearAutoPlay()) + updateDoubleClickTracker({ card: card.name, time: Date.now() }) + } } else { dispatch(toggleSelect(card)) } }, - [round, myCards, doubleClickTracker] + [round, myCards, autoPlayCard, doubleClickTracker] ) const handleOnDragEnd = useCallback( From 80e32cbbdc41f14fadc6b627e4380b3edde539ea Mon Sep 17 00:00:00 2001 From: Daithi Hearn Date: Sat, 14 Jan 2023 11:27:23 +0100 Subject: [PATCH 2/3] Retaining card order and selections --- src/caches/GameSlice.ts | 7 +- src/components/Game/AutoActionManager.tsx | 38 ++------ src/components/Game/WebsocketManager.tsx | 102 +++++++++++----------- src/components/Header/NavBar.tsx | 8 +- src/pages/Game/Game.tsx | 6 +- src/utils/GameUtils.ts | 41 ++++++--- 6 files changed, 99 insertions(+), 103 deletions(-) diff --git a/src/caches/GameSlice.ts b/src/caches/GameSlice.ts index a1f3fa1..26b168b 100644 --- a/src/caches/GameSlice.ts +++ b/src/caches/GameSlice.ts @@ -36,11 +36,12 @@ export const getRound = (state: RootState) => state.game.round export const getCards = (state: RootState) => state.game.cards export const getSuit = (state: RootState) => state.game.round?.suit export const getGameId = (state: RootState) => state.game.id -export const getHasGame = (state: RootState) => !!state.game.id -export const getGameStatus = (state: RootState) => state.game.status -export const isGameActive = (state: RootState) => +export const getHasGame = (state: RootState) => state.game.status === GameStatus.ACTIVE || state.game.status === GameStatus.NONE +export const getGameStatus = (state: RootState) => state.game.status +export const isGameActive = (state: RootState) => + state.game.status === GameStatus.ACTIVE export const getIsRoundCalling = (state: RootState) => state.game.round?.status === RoundStatus.CALLING export const getIsRoundCalled = (state: RootState) => diff --git a/src/components/Game/AutoActionManager.tsx b/src/components/Game/AutoActionManager.tsx index 1ec09ab..1c714ec 100644 --- a/src/components/Game/AutoActionManager.tsx +++ b/src/components/Game/AutoActionManager.tsx @@ -15,7 +15,6 @@ import { } from "../../caches/GameSlice" import { Round, RoundStatus } from "../../model/Round" import { Suit } from "../../model/Suit" -import { useSnackbar } from "notistack" import { getAutoPlayCard } from "../../caches/AutoPlaySlice" const bestCardLead = (round: Round) => { @@ -71,7 +70,6 @@ const getWorstCard = (cards: string[], suit: Suit) => { const AutoActionManager = () => { const dispatch = useAppDispatch() - const { enqueueSnackbar } = useSnackbar() const gameId = useAppSelector(getGameId) const round = useAppSelector(getRound) @@ -83,43 +81,25 @@ const AutoActionManager = () => { const isMyGo = useAppSelector(getIsMyGo) const isInBunker = useAppSelector(getIsInBunker) - const deal = (id: string) => { - console.info(`AutoAction -> deal `) - dispatch(GameService.deal(id)).catch((e: Error) => - enqueueSnackbar(e.message, { variant: "error" }) - ) - } + const deal = (id: string) => + dispatch(GameService.deal(id)).catch(console.error) - const playCard = (id: string, card: string) => { - console.info(`AutoAction -> playCard`) - dispatch(GameService.playCard(id, card)).catch((e: Error) => - enqueueSnackbar(e.message, { variant: "error" }) - ) - } + const playCard = (id: string, card: string) => + dispatch(GameService.playCard(id, card)).catch(console.error) - const call = (id: string, callAmount: number) => { - console.info(`AutoAction -> call ${callAmount}`) - dispatch(GameService.call(id, callAmount)).catch((e: Error) => - enqueueSnackbar(e.message, { variant: "error" }) - ) - } + const call = (id: string, callAmount: number) => + dispatch(GameService.call(id, callAmount)).catch(console.error) - const buyCards = (gameId: string, cardsToBuy: string[]) => { - console.info(`AutoAction -> buy cards`) - dispatch(GameService.buyCards(gameId, cardsToBuy)).catch((e: Error) => - enqueueSnackbar(e.message, { variant: "error" }) - ) - } + const buyCards = (gameId: string, cardsToBuy: string[]) => + dispatch(GameService.buyCards(gameId, cardsToBuy)).catch(console.error) // Deal when it's your turn useEffect(() => { - console.info(`Rule -> Deal`) if (gameId && canDeal) deal(gameId) }, [gameId, canDeal]) // If in the bunker, Pass useEffect(() => { - console.info(`Rule -> Bunker`) if (gameId && isInBunker) call(gameId, 0) }, [gameId, isInBunker]) @@ -127,7 +107,6 @@ const AutoActionManager = () => { // 2. Play card when you only have one left // 3. Play worst card if best card lead out useEffect(() => { - console.info(`Rule -> play card`) if ( gameId && isMyGo && @@ -145,7 +124,6 @@ const AutoActionManager = () => { // Buy cards in if you are the goer useEffect(() => { - console.info(`Rule -> buy cards`) if (gameId && canBuyCards) buyCards(gameId, cards) }, [gameId, cards, canBuyCards]) diff --git a/src/components/Game/WebsocketManager.tsx b/src/components/Game/WebsocketManager.tsx index 43d64be..45c780c 100644 --- a/src/components/Game/WebsocketManager.tsx +++ b/src/components/Game/WebsocketManager.tsx @@ -2,12 +2,7 @@ import React, { useCallback, useState } from "react" import { StompSessionProvider, useSubscription } from "react-stomp-hooks" import { useAppDispatch, useAppSelector } from "../../caches/hooks" -import { - getGame, - getGameId, - getIsMyGo, - updateGame, -} from "../../caches/GameSlice" +import { getGameId, updateGame } from "../../caches/GameSlice" import { getAccessToken } from "../../caches/MyProfileSlice" import { getPlayerProfiles } from "../../caches/PlayerProfilesSlice" import { GameState } from "../../model/Game" @@ -52,7 +47,6 @@ interface ActionEvent { const WebsocketHandler = () => { const dispatch = useAppDispatch() - const isMyGo = useAppSelector(getIsMyGo) const playerProfiles = useAppSelector(getPlayerProfiles) const { enqueueSnackbar } = useSnackbar() @@ -83,50 +77,60 @@ const WebsocketHandler = () => { } } - const processActons = (type: Actions, payload: unknown) => { - switch (type) { - case "DEAL": - // playShuffleSound() - break - case "CHOOSE_FROM_DUMMY": - case "BUY_CARDS": - case "LAST_CARD_PLAYED": - case "CARD_PLAYED": - // playPlayCardSound() - if (isMyGo) { - dispatch(clearSelectedCards()) + const reloadCards = (payload: unknown) => { + dispatch(clearSelectedCards()) + dispatch(clearAutoPlay()) + const gameState = payload as GameState + dispatch(updateMyCards(gameState.cards)) + } + + const processActons = useCallback( + (type: Actions, payload: unknown) => { + switch (type) { + case "DEAL": + // playShuffleSound() + reloadCards(payload) + break + case "CHOOSE_FROM_DUMMY": + case "BUY_CARDS": + case "LAST_CARD_PLAYED": + case "CARD_PLAYED": + // playPlayCardSound() + reloadCards(payload) + break + case "REPLAY": + break + case "GAME_OVER": + break + case "BUY_CARDS_NOTIFICATION": + const buyCardsEvt = payload as BuyCardsEvent + const player = playerProfiles.find( + (p) => p.id === buyCardsEvt.playerId + ) + if (!player) { + break + } + + enqueueSnackbar(`${player.name} bought ${buyCardsEvt.bought}`) + + break + case "HAND_COMPLETED": + break + case "ROUND_COMPLETED": dispatch(clearAutoPlay()) - const gameState = payload as GameState - dispatch(updateMyCards(gameState.cards)) - } - break - case "REPLAY": - break - case "GAME_OVER": - break - case "BUY_CARDS_NOTIFICATION": - const buyCardsEvt = payload as BuyCardsEvent - const player = playerProfiles.find((p) => p.id === buyCardsEvt.playerId) - if (!player) { break - } - - enqueueSnackbar(`${player.name} bought ${buyCardsEvt.bought}`) - - break - case "HAND_COMPLETED": - break - case "ROUND_COMPLETED": - dispatch(clearAutoPlay()) - break - case "CALL": - // playCallSound() - break - case "PASS": - // playPassSound() - break - } - } + case "CALL": + // playCallSound() + reloadCards(payload) + break + case "PASS": + // playPassSound() + reloadCards(payload) + break + } + }, + [playerProfiles] + ) useSubscription(["/game", "/user/game"], (message) => handleWebsocketMessage(message.body) diff --git a/src/components/Header/NavBar.tsx b/src/components/Header/NavBar.tsx index 1dd2fd6..33bac0f 100644 --- a/src/components/Header/NavBar.tsx +++ b/src/components/Header/NavBar.tsx @@ -10,12 +10,12 @@ import ProfilePictureEditor from "../Avatar/ProfilePictureEditor" import GameHeader from "../Game/GameHeader" import { Col, Container, Row } from "reactstrap" import LeaderboardModal from "../Leaderboard/LeaderboardModal" -import { getHasGame } from "../../caches/GameSlice" +import { isGameActive } from "../../caches/GameSlice" const NavBar = () => { const { logout } = useAuth0() - const hasGame = useAppSelector(getHasGame) + const gameActive = useAppSelector(isGameActive) const [showEditAvatar, setShowEditAvatar] = useState(false) const myProfile = useAppSelector(getMyProfile) @@ -39,8 +39,8 @@ const NavBar = () => {
Cards
- {hasGame && } - {hasGame && } + {gameActive && } + {gameActive && }
diff --git a/src/utils/GameUtils.ts b/src/utils/GameUtils.ts index 860ca0a..e5e0f99 100644 --- a/src/utils/GameUtils.ts +++ b/src/utils/GameUtils.ts @@ -9,15 +9,15 @@ export const compareCards = ( return false } - const arr1 = [...hand1].filter((ca) => ca !== BLANK_CARD).sort() - const arr2 = [...hand2].filter((ca) => ca !== BLANK_CARD).sort() + const arr1 = [...hand1].filter((ca) => ca.name !== BLANK_CARD.name).sort() + const arr2 = [...hand2].filter((ca) => ca.name !== BLANK_CARD.name).sort() if (arr1.length !== arr2.length) { return false } for (let i = 0; i < arr1.length; i++) { - if (arr1[i] !== arr2[i]) { + if (arr1[i].name !== arr2[i].name) { return false } } @@ -39,20 +39,30 @@ export const processOrderedCardsAfterGameUpdate = ( currentCards: SelectableCard[], updatedCardNames: string[] ): SelectableCard[] => { - // Find the delta between the existing cards and the updated cards we got from the api - const delta = currentCards.filter((x) => !updatedCardNames.includes(x.name)) + // Remove blanks + const currentCardsNoBlanks = currentCards.filter( + (c) => c.name !== BLANK_CARD.name + ) - const updatedCards = updatedCardNames.map( - (name) => CARDS.find((c) => c.name === name)! + // Find the delta between the existing cards and the updated cards we got from the api + const delta = currentCardsNoBlanks.filter( + (x) => !updatedCardNames.includes(x.name) ) // 1. If cards in payload match ordered cards then don't change orderedCards - if (compareCards(currentCards, updatedCards)) return currentCards + if ( + delta.length === 0 && + currentCardsNoBlanks.length === updatedCardNames.length + ) { + console.log("1. returning cards as they are") + return currentCards + } // 2. If a card was removed then replace it with a Blank card in orderedCards else if ( - currentCards.length === updatedCards.length + 1 && + currentCardsNoBlanks.length === updatedCardNames.length + 1 && delta.length === 1 ) { + console.log("2. a card was removed, so replacing it with a blank") const updatedCurrentCards = [...currentCards] const idx = updatedCurrentCards.findIndex((c) => c.name === delta[0].name) updatedCurrentCards[idx] = { ...BLANK_CARD, selected: false } @@ -61,11 +71,14 @@ export const processOrderedCardsAfterGameUpdate = ( // 3. Else send back a fresh hand constructed from the API data } else { - const updatedCurrentCards: SelectableCard[] = [] - updatedCards.forEach((c) => - updatedCurrentCards.push({ ...c, selected: false }) - ) - return padMyHand(updatedCurrentCards) + console.log(`3. refreshing cards entirely currentCards`) + + const updatedCards = updatedCardNames.map((name) => { + const card = CARDS.find((c) => c.name === name)! + return { ...card, selected: false } + }) + + return padMyHand(updatedCards) } } From 52caabc08e8d93f6bb8055318be8e4bf2d4ffbd5 Mon Sep 17 00:00:00 2001 From: Daithi Hearn Date: Sat, 14 Jan 2023 12:08:22 +0100 Subject: [PATCH 3/3] Fixing throw cards modal --- src/components/Game/Buying.tsx | 74 +++------------ src/components/Game/SelectSuit.tsx | 87 ++++-------------- .../Game/ThrowCardsWarningModal.tsx | 91 +++++++++++++++++++ 3 files changed, 121 insertions(+), 131 deletions(-) create mode 100644 src/components/Game/ThrowCardsWarningModal.tsx diff --git a/src/components/Game/Buying.tsx b/src/components/Game/Buying.tsx index ae774d6..a760221 100644 --- a/src/components/Game/Buying.tsx +++ b/src/components/Game/Buying.tsx @@ -1,15 +1,4 @@ -import { - Modal, - ModalBody, - ModalHeader, - Button, - ButtonGroup, - Form, - CardImg, - CardBody, - CardGroup, - Card, -} from "reactstrap" +import { Button, ButtonGroup, Form, CardBody } from "reactstrap" import { useCallback, useMemo, useState } from "react" @@ -27,6 +16,7 @@ import { removeAllFromHand, riskOfMistakeBuyingCards, } from "../../utils/GameUtils" +import ThrowCardsWarningModal from "./ThrowCardsWarningModal" const Buying = () => { const dispatch = useAppDispatch() @@ -45,8 +35,8 @@ const Buying = () => { ) const buyCards = useCallback( - (e: React.FormEvent) => { - e.preventDefault() + (e?: React.FormEvent) => { + if (e) e.preventDefault() if (riskOfMistakeBuyingCards(suit!, selectedCards, myCards)) { showCancelDeleteCardsDialog() } else { @@ -80,55 +70,13 @@ const Buying = () => { - - - {" "} - Are you sure you want to throw these cards away? - - - - - - {removeAllFromHand(selectedCards, myCards).map((card) => ( - {card.name} - ))} - - - - - - - - - - - - + ) : null} diff --git a/src/components/Game/SelectSuit.tsx b/src/components/Game/SelectSuit.tsx index 3c0316a..ef9bd6e 100644 --- a/src/components/Game/SelectSuit.tsx +++ b/src/components/Game/SelectSuit.tsx @@ -1,14 +1,4 @@ -import { - Modal, - ModalBody, - ModalHeader, - Button, - ButtonGroup, - Card, - CardImg, - CardBody, - CardGroup, -} from "reactstrap" +import { Button, ButtonGroup, CardBody } from "reactstrap" import { useCallback, useEffect, useState } from "react" @@ -20,6 +10,7 @@ import { Suit } from "../../model/Suit" import { useSnackbar } from "notistack" import { getMyCardsWithoutBlanks } from "../../caches/MyCardsSlice" import { removeAllFromHand } from "../../utils/GameUtils" +import ThrowCardsWarningModal from "./ThrowCardsWarningModal" const SelectSuit = () => { const dispatch = useAppDispatch() @@ -63,6 +54,14 @@ const SelectSuit = () => { [gameId, selectedCards] ) + const selectFromDummyCallback = useCallback(() => { + if (selectedSuit) { + dispatch( + GameService.chooseFromDummy(gameId!, selectedCards, selectedSuit) + ).catch((e: Error) => enqueueSnackbar(e.message, { variant: "error" })) + } + }, [gameId, selectedCards, selectedSuit]) + const hideCancelSelectFromDummyDialog = useCallback(() => { setSelectedSuit(undefined) setPossibleIssues(false) @@ -140,63 +139,15 @@ const SelectSuit = () => { - {possibleIssue && selectedSuit ? ( - - - {" "} - Are you sure you want to throw these cards away? - - - - - - {removeAllFromHand(selectedCards, myCards).map( - (card) => ( - {card.name} - ) - )} - - - - - - - - - - - - - ) : null} + {selectedSuit && ( + + )} ) : ( diff --git a/src/components/Game/ThrowCardsWarningModal.tsx b/src/components/Game/ThrowCardsWarningModal.tsx new file mode 100644 index 0000000..4a72053 --- /dev/null +++ b/src/components/Game/ThrowCardsWarningModal.tsx @@ -0,0 +1,91 @@ +import React, { useCallback } from "react" +import { + Modal, + ModalBody, + ModalHeader, + Button, + ButtonGroup, + CardImg, + CardBody, + CardGroup, + Card as CardComponent, +} from "reactstrap" +import { Card } from "../../model/Cards" +import { Suit } from "../../model/Suit" + +interface ModalOpts { + modalVisible: boolean + cancelCallback: () => void + continueCallback: () => void + suit: Suit + cards: Card[] +} + +const ThrowCardsWarningModal: React.FC = ({ + modalVisible, + cancelCallback, + continueCallback, + suit, + cards, +}) => { + const callContinue = useCallback( + (event: React.SyntheticEvent) => { + event.preventDefault() + continueCallback() + }, + [] + ) + return ( + cancelCallback()} + isOpen={modalVisible} + > + + {" "} + Are you sure you want to throw these cards away? + + + + + + {cards.map((card) => ( + {card.name} + ))} + + + + + + + + + + + + + ) +} + +export default ThrowCardsWarningModal