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/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 = () => { </ButtonGroup> </Form> - <Modal - fade={true} - size="lg" - toggle={hideCancelDeleteCardsDialog} - isOpen={deleteCardsDialog} - > - <ModalHeader> - <CardImg - alt="Suit" - src={`/cards/originals/${suit}_ICON.svg`} - className="thumbnail_size_extra_small left-padding" - />{" "} - Are you sure you want to throw these cards away? - </ModalHeader> - <ModalBody className="called-modal"> - <CardGroup className="gameModalCardGroup"> - <Card - className="p-6 tableCloth" - style={{ backgroundColor: "#333", borderColor: "#333" }} - > - <CardBody className="cardArea"> - {removeAllFromHand(selectedCards, myCards).map((card) => ( - <img - key={"deleteCardModal_" + card} - alt={card.name} - src={"/cards/thumbnails/" + card + ".png"} - className="thumbnail_size" - /> - ))} - </CardBody> - - <CardBody className="buttonArea"> - <ButtonGroup size="lg"> - <Button - type="button" - color="primary" - onClick={hideCancelDeleteCardsDialog} - > - Cancel - </Button> - <Button type="button" color="warning" onClick={buyCards}> - Throw Cards - </Button> - </ButtonGroup> - </CardBody> - </Card> - </CardGroup> - </ModalBody> - </Modal> + <ThrowCardsWarningModal + modalVisible={deleteCardsDialog} + cancelCallback={hideCancelDeleteCardsDialog} + continueCallback={buyCards} + suit={suit!} + cards={removeAllFromHand(selectedCards, myCards)} + /> </CardBody> ) : null} </div> 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( 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 = () => { </Button> </ButtonGroup> - {possibleIssue && selectedSuit ? ( - <Modal - fade={true} - size="lg" - toggle={hideCancelSelectFromDummyDialog} - isOpen={possibleIssue} - > - <ModalHeader> - <CardImg - alt="Suit" - src={`/cards/originals/${selectedSuit}_ICON.svg`} - className="thumbnail_size_extra_small left-padding" - />{" "} - Are you sure you want to throw these cards away? - </ModalHeader> - <ModalBody className="called-modal"> - <CardGroup className="gameModalCardGroup"> - <Card - className="p-6 tableCloth" - style={{ backgroundColor: "#333", borderColor: "#333" }} - > - <CardBody className="cardArea"> - {removeAllFromHand(selectedCards, myCards).map( - (card) => ( - <img - key={`cancelSelectFromDummyModal_${card.name}`} - alt={card.name} - src={`/cards/thumbnails/${card.name}.png`} - className="thumbnail_size" - /> - ) - )} - </CardBody> - - <CardBody className="buttonArea"> - <ButtonGroup size="lg"> - <Button - type="button" - color="primary" - onClick={hideCancelSelectFromDummyDialog} - > - Cancel - </Button> - <Button - type="button" - color="warning" - onClick={() => selectFromDummy(selectedSuit)} - > - Throw Cards - </Button> - </ButtonGroup> - </CardBody> - </Card> - </CardGroup> - </ModalBody> - </Modal> - ) : null} + {selectedSuit && ( + <ThrowCardsWarningModal + modalVisible={possibleIssue} + cancelCallback={hideCancelSelectFromDummyDialog} + continueCallback={selectFromDummyCallback} + suit={selectedSuit} + cards={removeAllFromHand(selectedCards, myCards)} + /> + )} </div> ) : ( <ButtonGroup size="lg"> 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<ModalOpts> = ({ + modalVisible, + cancelCallback, + continueCallback, + suit, + cards, +}) => { + const callContinue = useCallback( + (event: React.SyntheticEvent<HTMLButtonElement>) => { + event.preventDefault() + continueCallback() + }, + [] + ) + return ( + <Modal + fade={true} + size="lg" + toggle={() => cancelCallback()} + isOpen={modalVisible} + > + <ModalHeader> + <CardImg + alt="Suit" + src={`/cards/originals/${suit}_ICON.svg`} + className="thumbnail_size_extra_small left-padding" + />{" "} + Are you sure you want to throw these cards away? + </ModalHeader> + <ModalBody className="called-modal"> + <CardGroup className="gameModalCardGroup"> + <CardComponent + className="p-6 tableCloth" + style={{ backgroundColor: "#333", borderColor: "#333" }} + > + <CardBody className="cardArea"> + {cards.map((card) => ( + <img + key={`deleteCardModal_${card.name}`} + alt={card.name} + src={`/cards/thumbnails/${card.name}.png`} + className="thumbnail_size" + /> + ))} + </CardBody> + + <CardBody className="buttonArea"> + <ButtonGroup size="lg"> + <Button + type="button" + color="primary" + onClick={() => cancelCallback()} + > + Cancel + </Button> + <Button type="button" color="primary" onClick={callContinue}> + Throw Cards + </Button> + </ButtonGroup> + </CardBody> + </CardComponent> + </CardGroup> + </ModalBody> + </Modal> + ) +} + +export default ThrowCardsWarningModal 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 = () => { <div className="linknavbar">Cards</div> </Link> </Col> - <Col className="nav-col">{hasGame && <LeaderboardModal />}</Col> - <Col className="nav-col">{hasGame && <GameHeader />}</Col> + <Col className="nav-col">{gameActive && <LeaderboardModal />}</Col> + <Col className="nav-col">{gameActive && <GameHeader />}</Col> <Col className="nav-col"> <div> <label className="mr-2 text-white"> diff --git a/src/pages/Game/Game.tsx b/src/pages/Game/Game.tsx index 2a16788..12995f0 100644 --- a/src/pages/Game/Game.tsx +++ b/src/pages/Game/Game.tsx @@ -9,7 +9,7 @@ import { useParams } from "react-router-dom" import { useAppDispatch, useAppSelector } from "../../caches/hooks" import { useSnackbar } from "notistack" -import { isGameActive, resetGame } from "../../caches/GameSlice" +import { getHasGame, resetGame } from "../../caches/GameSlice" import DataLoader from "../../components/DataLoader/DataLoader" import { clearAutoPlay } from "../../caches/AutoPlaySlice" import { clearMyCards } from "../../caches/MyCardsSlice" @@ -18,7 +18,7 @@ const Game = () => { const dispatch = useAppDispatch() let { id } = useParams<string>() const { enqueueSnackbar } = useSnackbar() - const gameActive = useAppSelector(isGameActive) + const hasGame = useAppSelector(getHasGame) useEffect(() => { if (id) @@ -40,7 +40,7 @@ const Game = () => { <div className="game_wrap"> <div className="game_container"> <DataLoader /> - {gameActive ? <GameWrapper /> : <GameOver />} + {hasGame ? <GameWrapper /> : <GameOver />} </div> </div> </div> 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<Card>( - (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<SelectableCard>((name) => { + const card = CARDS.find((c) => c.name === name)! + return { ...card, selected: false } + }) + + return padMyHand(updatedCards) } }