Skip to content

Commit

Permalink
Merge pull request #179 from daithihearn/full-auto-play
Browse files Browse the repository at this point in the history
Auto play
  • Loading branch information
daithihearn authored Jul 8, 2023
2 parents eb510b3 + ab221d4 commit 20d20e2
Show file tree
Hide file tree
Showing 11 changed files with 117 additions and 92 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.2.5",
"version": "7.3.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.2.5",
"version": "7.3.0",
"icons": [
{
"src": "./assets/favicon.png",
Expand Down
14 changes: 7 additions & 7 deletions src/caches/AutoPlaySlice.ts → src/caches/PlayCardSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,16 @@ export interface AutoPlayState {

const initialState: AutoPlayState = {}

export const autoPlaySlice = createSlice({
name: "autoPlay",
export const playCardSlice = createSlice({
name: "playCard",
initialState: initialState,
reducers: {
updateAutoPlay: (_, action: PayloadAction<Card>) => {
updateCardToPlay: (_, action: PayloadAction<Card>) => {
return {
card: action.payload.name,
}
},
toggleAutoPlay: (state, action: PayloadAction<Card>) => {
togglePlayCard: (state, action: PayloadAction<Card>) => {
if (state.card === action.payload.name) return initialState
return {
card: action.payload.name,
Expand All @@ -27,7 +27,7 @@ export const autoPlaySlice = createSlice({
},
})

export const { updateAutoPlay, toggleAutoPlay, clearAutoPlay } =
autoPlaySlice.actions
export const { updateCardToPlay, togglePlayCard, clearAutoPlay } =
playCardSlice.actions

export const getAutoPlayCard = (state: RootState) => state.autoPlay.card
export const getCardToPlay = (state: RootState) => state.playCard.card
4 changes: 2 additions & 2 deletions src/caches/caches.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { myProfileSlice } from "./MyProfileSlice"
import { gameSlice } from "./GameSlice"
import { myGamesSlice } from "./MyGamesSlice"
import { myCardsSlice } from "./MyCardsSlice"
import { autoPlaySlice } from "./AutoPlaySlice"
import { playCardSlice } from "./PlayCardSlice"
import { playerProfilesSlice } from "./PlayerProfilesSlice"
import { settingsSlice } from "./SettingsSlice"

Expand All @@ -21,7 +21,7 @@ const combinedReducer = combineReducers({
myGames: myGamesSlice.reducer,
playerProfiles: playerProfilesSlice.reducer,
myCards: myCardsSlice.reducer,
autoPlay: autoPlaySlice.reducer,
playCard: playCardSlice.reducer,
settings: settingsSlice.reducer,
})

Expand Down
95 changes: 74 additions & 21 deletions src/components/Game/Actions/PlayCard.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import { useCallback, useMemo } from "react"
import { useCallback, useEffect, useMemo, useState } from "react"

import GameService from "services/GameService"
import { useAppDispatch, useAppSelector } from "caches/hooks"
import { useSnackbar } from "notistack"
import { getMyCardsWithoutBlanks, getSelectedCards } from "caches/MyCardsSlice"
import { getGameId, getIsMyGo, getRound } from "caches/GameSlice"
import { BLANK_CARD } from "model/Cards"
import parseError from "utils/ErrorUtils"
import { RoundStatus } from "model/Round"
import { Button } from "@mui/material"
import { getCardToPlay, updateCardToPlay } from "caches/PlayCardSlice"
import { bestCardLead, getBestCard, getWorstCard } from "utils/GameUtils"

const WaitingForYourTurn = () => (
<Button variant="contained" disableRipple color="secondary">
Expand All @@ -19,11 +20,14 @@ const WaitingForYourTurn = () => (
const PlayCard = () => {
const dispatch = useAppDispatch()
const round = useAppSelector(getRound)
const { enqueueSnackbar } = useSnackbar()
const [autoPlay, setAutoPlay] = useState(false)
const gameId = useAppSelector(getGameId)
const myCards = useAppSelector(getMyCardsWithoutBlanks)
const isMyGo = useAppSelector(getIsMyGo)
const selectedCards = useAppSelector(getSelectedCards)
const cardToPlay = useAppSelector(getCardToPlay)

const togglePlayCard = useCallback(() => setAutoPlay(!autoPlay), [autoPlay])

const playButtonEnabled = useMemo(
() =>
Expand All @@ -37,27 +41,76 @@ const PlayCard = () => {
[isMyGo, round, myCards],
)

const playCard = useCallback(() => {
if (selectedCards.length !== 1) {
enqueueSnackbar("Please select exactly one card to play", {
variant: "warning",
})
} else {
dispatch(
GameService.playCard(gameId!, selectedCards[0].name),
).catch(e => enqueueSnackbar(parseError(e), { variant: "error" }))
const playCard = useCallback(
(card: string) =>
dispatch(GameService.playCard(gameId!, card)).catch(e => {
console.error(parseError(e))
}),
[gameId],
)

const selectCardToPlay = useCallback(() => {
if (selectedCards.length === 1)
dispatch(updateCardToPlay(selectedCards[0]))
}, [selectedCards])

// 1. Play card when you've pre-selected a card
// 2. If auto play is enabled, play best card
// 3. Play worst card if best card lead out
useEffect(() => {
if (round?.suit && isMyGo) {
if (cardToPlay) playCard(cardToPlay)
else if (autoPlay) {
const bestCard = getBestCard(myCards, round.suit)
playCard(bestCard.name)
} else if (bestCardLead(round)) {
const worstCard = getWorstCard(myCards, round.suit)
playCard(worstCard.name)
}
}
}, [gameId, selectedCards])
}, [playCard, autoPlay, round, isMyGo, myCards, cardToPlay])

if (!playButtonEnabled) return <WaitingForYourTurn />
if (autoPlay) {
return (
<Button
id="autoPlayCardButton"
type="button"
onClick={togglePlayCard}
color="error">
<b>Disable Auto Play</b>
</Button>
)
} else if (!playButtonEnabled)
return (
<>
<WaitingForYourTurn />
<Button
id="autoPlayCardButton"
type="button"
onClick={togglePlayCard}
color="warning">
<b>Enable Auto Play</b>
</Button>
</>
)
return (
<Button
id="playCardButton"
type="button"
onClick={playCard}
color="primary">
<b>Play Card</b>
</Button>
<>
<Button
id="playCardButton"
type="button"
onClick={selectCardToPlay}
color="primary">
<b>Play Card</b>
</Button>

<Button
id="autoPlayCardButton"
type="button"
onClick={togglePlayCard}
color="warning">
<b>Enable Auto Play</b>
</Button>
</>
)
}

Expand Down
44 changes: 1 addition & 43 deletions src/components/Game/AutoActionManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,14 @@ import { useEffect } from "react"
import GameService from "services/GameService"

import { useAppDispatch, useAppSelector } from "caches/hooks"
import {
getCards,
getGameId,
getIsInBunker,
getIsMyGo,
getRound,
} from "caches/GameSlice"
import { RoundStatus } from "model/Round"
import { getAutoPlayCard } from "caches/AutoPlaySlice"
import { bestCardLead, getWorstCard } from "utils/GameUtils"
import { useSnackbar } from "notistack"
import parseError from "utils/ErrorUtils"
import { getGameId, getIsInBunker } from "caches/GameSlice"

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

const gameId = useAppSelector(getGameId)
const round = useAppSelector(getRound)
const cards = useAppSelector(getCards)

const autoPlayCard = useAppSelector(getAutoPlayCard)

const isMyGo = useAppSelector(getIsMyGo)
const isInBunker = useAppSelector(getIsInBunker)

const playCard = (id: string, card: string) =>
dispatch(GameService.playCard(id, card)).catch(e => {
enqueueSnackbar(parseError(e), {
variant: "error",
})
})

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

Expand All @@ -43,23 +18,6 @@ const AutoActionManager = () => {
if (gameId && isInBunker) call(gameId, 0)
}, [gameId, isInBunker])

// 1. Play card when you've pre-selected a card
// 2. Play worst card if best card lead out
useEffect(() => {
if (
gameId &&
isMyGo &&
round?.suit &&
round.status === RoundStatus.PLAYING
) {
if (autoPlayCard) playCard(gameId, autoPlayCard)
else if (bestCardLead(round)) {
const cardToPlay = getWorstCard(cards, round.suit)
if (cardToPlay) playCard(gameId, cardToPlay.name)
}
}
}, [gameId, round, isMyGo, cards, autoPlayCard])

return null
}

Expand Down
10 changes: 5 additions & 5 deletions src/components/Game/MyCards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,10 @@ import {
toggleUniqueSelect,
} from "caches/MyCardsSlice"
import {
getAutoPlayCard,
toggleAutoPlay,
getCardToPlay,
togglePlayCard,
clearAutoPlay,
} from "caches/AutoPlaySlice"
} from "caches/PlayCardSlice"
import { CardContent, CardMedia, useTheme } from "@mui/material"
import { pickBestCards } from "utils/GameUtils"

Expand Down Expand Up @@ -55,7 +55,7 @@ const MyCards: React.FC = () => {
const numPlayers = useAppSelector(getNumPlayers)
const isRoundCalled = useAppSelector(getIsRoundCalled)
const myCards = useAppSelector(getMyCards)
const autoPlayCard = useAppSelector(getAutoPlayCard)
const autoPlayCard = useAppSelector(getCardToPlay)
const iamGoer = useAppSelector(getIamGoer)
const prevRoundStatus = usePrevious(round?.status)

Expand Down Expand Up @@ -104,7 +104,7 @@ const MyCards: React.FC = () => {
dispatch(clearAutoPlay())
dispatch(clearSelectedCards())
} else if (event.detail === 2) {
dispatch(toggleAutoPlay(card))
dispatch(togglePlayCard(card))
} else {
dispatch(toggleUniqueSelect(card))
dispatch(clearAutoPlay())
Expand Down
2 changes: 1 addition & 1 deletion src/components/Game/WebsocketManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import { GameState } from "model/Game"
import { Actions, BuyCardsEvent } from "model/Events"
import { useSnackbar } from "notistack"
import { clearSelectedCards, updateMyCards } from "caches/MyCardsSlice"
import { clearAutoPlay } from "caches/AutoPlaySlice"
import { clearAutoPlay } from "caches/PlayCardSlice"

import shuffleAudioFile from "assets/sounds/shuffle.ogg"
import playCardAudioFile from "assets/sounds/play_card.ogg"
Expand Down
2 changes: 1 addition & 1 deletion src/pages/Game/Game.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import { useParams } from "react-router-dom"
import { useAppDispatch, useAppSelector } from "caches/hooks"
import { useSnackbar } from "notistack"
import { getIamSpectator, getIsGameActive, resetGame } from "caches/GameSlice"
import { clearAutoPlay } from "caches/AutoPlaySlice"
import { clearAutoPlay } from "caches/PlayCardSlice"
import { clearMyCards } from "caches/MyCardsSlice"
import parseError from "utils/ErrorUtils"

Expand Down
2 changes: 1 addition & 1 deletion src/services/GameService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import {
removeCard,
updateMyCards,
} from "caches/MyCardsSlice"
import { clearAutoPlay } from "caches/AutoPlaySlice"
import { clearAutoPlay } from "caches/PlayCardSlice"

const getGame =
(gameId: string): AppThunk<Promise<Game>> =>
Expand Down
32 changes: 23 additions & 9 deletions src/utils/GameUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -147,8 +147,10 @@ export const bestCardLead = (round: Round) => {
return round.currentHand.leadOut === trumpCards[0].name
}

export const getWorstCard = (cards: string[], suit: Suit) => {
const myCardsRich = CARDS.filter(card => cards.some(c => c === card.name))
export const getWorstCard = (cards: SelectableCard[], suit: Suit) => {
const myCardsRich = CARDS.filter(card =>
cards.some(c => c.name === card.name),
)
const myTrumpCards = myCardsRich.filter(
card => card.suit === suit || card.suit === Suit.WILD,
)
Expand All @@ -161,13 +163,25 @@ export const getWorstCard = (cards: string[], suit: Suit) => {
// Sort ascending by cold value
myCardsRich.sort((a, b) => a.coldValue - b.coldValue)

// if we can't find a cold card that is clearly the worst card then do nothing
if (
myCardsRich.length > 1 &&
myCardsRich[0].coldValue === myCardsRich[1].coldValue
) {
return
}
return myCardsRich[0]
}
}

export const getBestCard = (cards: SelectableCard[], suit: Suit) => {
const myCardsRich = CARDS.filter(card =>
cards.some(c => c.name === card.name),
)
const myTrumpCards = myCardsRich.filter(
card => card.suit === suit || card.suit === Suit.WILD,
)

if (myTrumpCards.length > 0) {
// Sort descending by value
myTrumpCards.sort((a, b) => b.value - a.value)
return myTrumpCards[0]
} else {
// Sort descending by cold value
myCardsRich.sort((a, b) => b.coldValue - a.coldValue)

return myCardsRich[0]
}
Expand Down

0 comments on commit 20d20e2

Please sign in to comment.