Skip to content

Commit

Permalink
Adding support for auto buying of cards
Browse files Browse the repository at this point in the history
  • Loading branch information
daithihearn committed Jul 3, 2023
1 parent f21fce3 commit 140eb89
Show file tree
Hide file tree
Showing 15 changed files with 222 additions and 70 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "frontend",
"version": "7.0.8",
"version": "7.1.0",
"description": "React frontend for the Cards 110",
"author": "Daithi Hearn",
"license": "MIT",
Expand Down
2 changes: 1 addition & 1 deletion public/manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"short_name": "Cards 110",
"name": "Cards 110",
"version": "7.0.8",
"version": "7.1.0",
"icons": [
{
"src": "./assets/favicon.png",
Expand Down
5 changes: 5 additions & 0 deletions src/caches/GameSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { GameState, GameStatus, PlayedCard } from "model/Game"
import { Player } from "model/Player"
import { RoundStatus } from "model/Round"
import { RootState } from "./caches"
import { get } from "http"

const initialState: GameState = {
iamSpectator: true,
Expand Down Expand Up @@ -45,6 +46,10 @@ export const {

export const getGame = (state: RootState) => state.game
export const getGamePlayers = createSelector(getGame, game => game.players)
export const getNumPlayers = createSelector(
getGamePlayers,
players => players.length,
)
export const getMe = createSelector(getGame, game => game.me)

export const getRound = createSelector(getGame, game => game.round)
Expand Down
7 changes: 7 additions & 0 deletions src/caches/MyCardsSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ export const myCardsSlice = createSlice({
if (c.name === action.payload.name) c.selected = true
})
},
selectCards: (state, action: PayloadAction<SelectableCard[]>) => {
state.cards.forEach(c => {
if (action.payload.some(a => a.name === c.name))
c.selected = true
})
},
toggleSelect: (state, action: PayloadAction<SelectableCard>) =>
state.cards.forEach(c => {
if (c.name === action.payload.name) c.selected = !c.selected
Expand Down Expand Up @@ -67,6 +73,7 @@ export const {
clearSelectedCards,
selectAll,
selectCard,
selectCards,
toggleSelect,
toggleUniqueSelect,
clearMyCards,
Expand Down
20 changes: 20 additions & 0 deletions src/caches/SettingsSlice.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { createSlice, PayloadAction } from "@reduxjs/toolkit"
import { RootState } from "./caches"
import { PlayerSettings } from "model/PlayerSettings"

const initialState: PlayerSettings = {
autoBuyCards: false,
}

export const settingsSlice = createSlice({
name: "settings",
initialState: initialState,
reducers: {
updateSettings: (_, action: PayloadAction<PlayerSettings>) =>
action.payload,
},
})

export const { updateSettings } = settingsSlice.actions

export const getSettings = (state: RootState) => state.settings
2 changes: 2 additions & 0 deletions src/caches/caches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { myGamesSlice } from "./MyGamesSlice"
import { myCardsSlice } from "./MyCardsSlice"
import { autoPlaySlice } from "./AutoPlaySlice"
import { playerProfilesSlice } from "./PlayerProfilesSlice"
import { settingsSlice } from "./SettingsSlice"

const combinedReducer = combineReducers({
myProfile: myProfileSlice.reducer,
Expand All @@ -21,6 +22,7 @@ const combinedReducer = combineReducers({
playerProfiles: playerProfilesSlice.reducer,
myCards: myCardsSlice.reducer,
autoPlay: autoPlaySlice.reducer,
settings: settingsSlice.reducer,
})

export type RootState = ReturnType<typeof combinedReducer>
Expand Down
56 changes: 27 additions & 29 deletions src/components/Game/Actions/Buying.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,18 @@ import {
} from "caches/MyCardsSlice"
import {
getGameId,
getNumPlayers,
getIamGoer,
getIHavePlayed,
getIsMyGo,
getSuit,
} from "caches/GameSlice"
import { riskOfMistakeBuyingCards } from "utils/GameUtils"
import { pickBestCards, riskOfMistakeBuyingCards } from "utils/GameUtils"
import ThrowCardsWarningModal from "./ThrowCardsWarningModal"
import { SelectableCard } from "model/Cards"
import parseError from "utils/ErrorUtils"
import { Button } from "@mui/material"
import { getSettings } from "caches/SettingsSlice"

const WaitingForRoundToStart = () => (
<Button variant="contained" disableRipple color="primary">
Expand All @@ -30,6 +32,7 @@ const WaitingForRoundToStart = () => (
const Buying = () => {
const dispatch = useAppDispatch()
const { enqueueSnackbar } = useSnackbar()
const settings = useAppSelector(getSettings)
const gameId = useAppSelector(getGameId)
const suit = useAppSelector(getSuit)
const myCards = useAppSelector(getMyCardsWithoutBlanks)
Expand All @@ -46,14 +49,19 @@ const Buying = () => {
setReadyToBuy(!readyToBuy)
}, [readyToBuy])

const buyCards = (id: string, sel: SelectableCard[]) => {
dispatch(GameService.buyCards(id, sel)).catch(e =>
enqueueSnackbar(parseError(e), { variant: "error" }),
)
}
const buyCards = useCallback(
(sel: SelectableCard[]) => {
if (!gameId) return
dispatch(GameService.buyCards(gameId, sel)).catch((e: Error) =>
enqueueSnackbar(parseError(e), { variant: "error" }),
)
},
[gameId],
)

const hideCancelDeleteCardsDialog = useCallback(() => {
updateDeleteCardsDialog(false)
setReadyToBuy(false)
}, [])

useEffect(() => {
Expand All @@ -64,34 +72,24 @@ const Buying = () => {
}, [iamGoer])

useEffect(() => {
if (isMyGo && readyToBuy) {
if (!gameId) throw Error("No game id set")

if (riskOfMistakeBuyingCards(suit!, selectedCards, myCards)) {
setReadyToBuy(false)
if (!isMyGo || !suit) return
if (readyToBuy) {
if (riskOfMistakeBuyingCards(suit, selectedCards, myCards)) {
updateDeleteCardsDialog(true)
} else buyCards(gameId, selectedCards)
} else buyCards(selectedCards)
}
}, [gameId, suit, selectedCards, myCards, isMyGo, readyToBuy])
}, [iamGoer, suit, selectedCards, myCards, isMyGo, readyToBuy])

if (iHavePlayed) return <WaitingForRoundToStart />
if (iHavePlayed || settings.autoBuyCards) return <WaitingForRoundToStart />
return (
<>
{isMyGo || !readyToBuy ? (
<Button
type="button"
onClick={toggleReadyToBuy}
color="primary">
<b>Keep Cards</b>
</Button>
) : (
<Button
type="button"
onClick={toggleReadyToBuy}
color="primary">
<b>Waiting to buy cards...</b>
</Button>
)}
<Button type="button" onClick={toggleReadyToBuy} color="primary">
<b>
{isMyGo || !readyToBuy
? "Keep Cards"
: "Waiting to buy cards..."}
</b>
</Button>

<ThrowCardsWarningModal
modalVisible={deleteCardsDialog}
Expand Down
21 changes: 11 additions & 10 deletions src/components/Game/Actions/SelectSuit.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -58,16 +58,17 @@ const SelectSuit = () => {
[gameId, selectedCards],
)

const selectFromDummyCallback = (
id: string,
sel: SelectableCard[],
suit?: Suit,
) => {
if (!suit) throw Error("Must provide a suit")
dispatch(GameService.chooseFromDummy(id, sel, suit)).catch((e: Error) =>
enqueueSnackbar(parseError(e), { variant: "error" }),
)
}
const selectFromDummyCallback = useCallback(
(sel: SelectableCard[], suit?: Suit) => {
if (!gameId) throw Error("Must be in a game")
if (!suit) throw Error("Must provide a suit")
dispatch(GameService.chooseFromDummy(gameId, sel, suit)).catch(
(e: Error) =>
enqueueSnackbar(parseError(e), { variant: "error" }),
)
},
[gameId],
)

const hideCancelSelectFromDummyDialog = useCallback(() => {
setSelectedSuit(undefined)
Expand Down
12 changes: 5 additions & 7 deletions src/components/Game/Actions/ThrowCardsWarningModal.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ import { useAppSelector } from "caches/hooks"
import { getMyCardsWithoutBlanks, getSelectedCards } from "caches/MyCardsSlice"
import { SelectableCard } from "model/Cards"
import { Suit } from "model/Suit"
import { removeAllFromHand } from "utils/GameUtils"
import { getTrumpCards, removeAllFromHand } from "utils/GameUtils"

interface ModalOpts {
modalVisible: boolean
cancelCallback: () => void
continueCallback: (id: string, sel: SelectableCard[], suit?: Suit) => void
continueCallback: (sel: SelectableCard[], suit?: Suit) => void
suit: Suit
}

Expand All @@ -29,19 +29,17 @@ const ThrowCardsWarningModal: React.FC<ModalOpts> = ({
continueCallback,
suit,
}) => {
const gameId = useAppSelector(getGameId)
const myCards = useAppSelector(getMyCardsWithoutBlanks)
const selectedCards = useAppSelector(getSelectedCards)

const cardsToBeThrown = useMemo(
() => removeAllFromHand(selectedCards, myCards),
() => getTrumpCards(removeAllFromHand(selectedCards, myCards), suit),
[myCards, selectedCards],
)

const callContinue = useCallback(() => {
if (!gameId) throw Error("GameId not set")
continueCallback(gameId, selectedCards, suit)
}, [gameId, selectedCards])
continueCallback(selectedCards, suit)
}, [selectedCards])

return (
<Dialog onClose={() => cancelCallback()} open={modalVisible}>
Expand Down
38 changes: 36 additions & 2 deletions src/components/Game/AutoActionManager.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,38 @@
import { useEffect } from "react"
import { useCallback, useEffect } from "react"
import GameService from "services/GameService"

import { useAppDispatch, useAppSelector } from "caches/hooks"
import {
getCards,
getGameId,
getIamGoer,
getIsInBunker,
getIsMyGo,
getNumPlayers,
getRound,
getSuit,
} from "caches/GameSlice"
import { RoundStatus } from "model/Round"
import { getAutoPlayCard } from "caches/AutoPlaySlice"
import { bestCardLead, getWorstCard } from "utils/GameUtils"
import { bestCardLead, getWorstCard, pickBestCards } from "utils/GameUtils"
import { useSnackbar } from "notistack"
import parseError from "utils/ErrorUtils"
import { getSettings } from "caches/SettingsSlice"
import { SelectableCard } from "model/Cards"
import { getMyCardsWithoutBlanks } from "caches/MyCardsSlice"

const AutoActionManager = () => {
const dispatch = useAppDispatch()
const { enqueueSnackbar } = useSnackbar()

const settings = useAppSelector(getSettings)
const numPlayers = useAppSelector(getNumPlayers)
const suit = useAppSelector(getSuit)
const myCards = useAppSelector(getMyCardsWithoutBlanks)
const gameId = useAppSelector(getGameId)
const round = useAppSelector(getRound)
const cards = useAppSelector(getCards)
const iamGoer = useAppSelector(getIamGoer)

const autoPlayCard = useAppSelector(getAutoPlayCard)

Expand All @@ -35,6 +46,16 @@ const AutoActionManager = () => {
})
})

const buyCards = useCallback(
(sel: SelectableCard[]) => {
if (!gameId) return
dispatch(GameService.buyCards(gameId, sel)).catch((e: Error) =>
enqueueSnackbar(parseError(e), { variant: "error" }),
)
},
[gameId],
)

const call = (id: string, callAmount: number) =>
dispatch(GameService.call(id, callAmount)).catch(console.error)

Expand All @@ -60,6 +81,19 @@ const AutoActionManager = () => {
}
}, [gameId, round, isMyGo, cards, autoPlayCard])

// Auto buy cards
useEffect(() => {
if (
settings.autoBuyCards &&
suit &&
isMyGo &&
round?.status === RoundStatus.BUYING &&
!iamGoer
) {
buyCards(pickBestCards(myCards, suit, numPlayers))
}
}, [settings, isMyGo, iamGoer, round, myCards, suit, numPlayers])

return null
}

Expand Down
26 changes: 14 additions & 12 deletions src/components/Game/MyCards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,19 @@ import {
} from "react-beautiful-dnd"
import { BLANK_CARD, SelectableCard } from "model/Cards"
import { RoundStatus } from "model/Round"
import { getIamGoer, getIsRoundCalled, getRound } from "caches/GameSlice"
import {
getGameId,
getIamGoer,
getIsRoundCalled,
getRound,
getNumPlayers,
} from "caches/GameSlice"
import { useAppDispatch, useAppSelector } from "caches/hooks"
import {
clearSelectedCards,
getMyCards,
replaceMyCards,
selectCard,
selectCards,
toggleSelect,
toggleUniqueSelect,
} from "caches/MyCardsSlice"
Expand All @@ -23,6 +29,7 @@ import {
clearAutoPlay,
} from "caches/AutoPlaySlice"
import { CardContent, CardMedia, useTheme } from "@mui/material"
import { pickBestCards } from "utils/GameUtils"

const EMPTY_HAND = [
{ ...BLANK_CARD, selected: false },
Expand All @@ -44,6 +51,8 @@ const MyCards: React.FC = () => {
const theme = useTheme()
const dispatch = useAppDispatch()
const round = useAppSelector(getRound)
const gameId = useAppSelector(getGameId)
const numPlayers = useAppSelector(getNumPlayers)
const isRoundCalled = useAppSelector(getIsRoundCalled)
const myCards = useAppSelector(getMyCards)
const autoPlayCard = useAppSelector(getAutoPlayCard)
Expand All @@ -58,18 +67,11 @@ const MyCards: React.FC = () => {
round.suit
) {
// Auto select all cards of a specific suit when the round status is BUYING
const bestCards = pickBestCards(myCards, round.suit, numPlayers)

const cardsOfSuit = myCards.filter(
card =>
card.suit === round.suit ||
card.name === "JOKER" ||
card.name === "ACE_HEARTS",
)
cardsOfSuit.forEach(card => {
dispatch(selectCard(card))
})
dispatch(selectCards(bestCards))
}
}, [round, myCards, prevRoundStatus, iamGoer])
}, [round, numPlayers, gameId, myCards, prevRoundStatus, iamGoer])

const cardsSelectable = useMemo(
() =>
Expand Down
Loading

0 comments on commit 140eb89

Please sign in to comment.