Skip to content

Commit

Permalink
Merge pull request #101 from daithihearn/auto-play-last-hand
Browse files Browse the repository at this point in the history
Autoplaying last hand on the server
  • Loading branch information
daithihearn authored Jan 23, 2023
2 parents 23502e3 + db7555d commit 5d00047
Show file tree
Hide file tree
Showing 9 changed files with 148 additions and 85 deletions.
18 changes: 15 additions & 3 deletions src/caches/GameSlice.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createSelector, createSlice, PayloadAction } from "@reduxjs/toolkit"

import { GameState, GameStatus } from "../model/Game"
import { GameState, GameStatus, PlayedCard } from "../model/Game"
import { Player } from "../model/Player"
import { RoundStatus } from "../model/Round"
import { RootState } from "./caches"
Expand All @@ -13,7 +13,6 @@ const initialState: GameState = {
cards: [],
status: GameStatus.NONE,
players: [],
playedCards: [],
}

export const gameSlice = createSlice({
Expand All @@ -24,11 +23,24 @@ export const gameSlice = createSlice({
updatePlayers: (state, action: PayloadAction<Player[]>) => {
state.players = action.payload
},
updatePlayedCards: (state, action: PayloadAction<PlayedCard[]>) => {
if (state.round)
state.round.currentHand.playedCards = action.payload
},
disableActions: state => {
state.isMyGo = false
},
resetGame: () => initialState,
},
})

export const { updateGame, updatePlayers, resetGame } = gameSlice.actions
export const {
updateGame,
disableActions,
updatePlayedCards,
updatePlayers,
resetGame,
} = gameSlice.actions

export const getGame = (state: RootState) => state.game
export const getGamePlayers = createSelector(getGame, game => game.players)
Expand Down
5 changes: 5 additions & 0 deletions src/caches/MyCardsSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ export const myCardsSlice = createSlice({
cards: action.payload,
}
},
removeCard: (state, action: PayloadAction<string>) => {
const idx = state.cards.findIndex(c => c.name === action.payload)
if (idx > 0) state.cards[idx] = { ...BLANK_CARD, selected: false }
},
toggleSelect: (state, action: PayloadAction<SelectableCard>) =>
state.cards.forEach(c => {
if (c.name === action.payload.name) c.selected = !c.selected
Expand All @@ -49,6 +53,7 @@ export const myCardsSlice = createSlice({
export const {
updateMyCards,
replaceMyCards,
removeCard,
clearSelectedCards,
toggleSelect,
toggleUniqueSelect,
Expand Down
4 changes: 1 addition & 3 deletions src/components/Game/AutoActionManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,7 @@ const AutoActionManager = () => {
}, [gameId, isInBunker])

// 1. Play card when you've pre-selected a card
// 2. Play card when you only have one left
// 3. Play worst card if best card lead out
// 2. Play worst card if best card lead out
useEffect(() => {
if (
gameId &&
Expand All @@ -61,7 +60,6 @@ const AutoActionManager = () => {
round.status === RoundStatus.PLAYING
) {
if (autoPlayCard) playCard(gameId, autoPlayCard, true)
else if (cards.length === 1) playCard(gameId, cards[0], true)
else if (bestCardLead(round)) {
const cardToPlay = getWorstCard(cards, round.suit)
if (cardToPlay) playCard(gameId, cardToPlay.name, true)
Expand Down
4 changes: 3 additions & 1 deletion src/components/Game/MyCards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,9 @@ const MyCards: React.FC = () => {

const playCard = useCallback(() => {
if (selectedCards.length !== 1) {
enqueueSnackbar("Please select exactly one card to play")
enqueueSnackbar("Please select exactly one card to play", {
variant: "warning",
})
} else {
dispatch(
GameService.playCard(gameId!, selectedCards[0].name),
Expand Down
7 changes: 6 additions & 1 deletion src/components/Game/PlayersAndCards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,12 @@ const PlayersAndCards = () => {
return (
<CardBody className="cardArea">
<Container>
<Row xs="6" sm="6" md="6" lg="6" xl="6">
<Row
xs={sortedPlayers.length}
sm={sortedPlayers.length}
md={sortedPlayers.length}
lg={sortedPlayers.length}
xl={sortedPlayers.length}>
{sortedPlayers.map(player => (
<PlayerCard
key={`playercard_${player.id}`}
Expand Down
163 changes: 101 additions & 62 deletions src/components/Game/WebsocketManager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,13 @@ import React, { useCallback, useEffect, useState } from "react"

import { StompSessionProvider, useSubscription } from "react-stomp-hooks"
import { useAppDispatch, useAppSelector } from "../../caches/hooks"
import { getGameId, getIsMyGo, updateGame } from "../../caches/GameSlice"
import {
disableActions,
getGameId,
getIsMyGo,
updateGame,
updatePlayedCards,
} from "../../caches/GameSlice"
import { getAccessToken } from "../../caches/MyProfileSlice"
import { getPlayerProfiles } from "../../caches/PlayerProfilesSlice"
import { GameState } from "../../model/Game"
Expand All @@ -16,6 +22,7 @@ import playCardAudioFile from "../../assets/sounds/play_card.ogg"
import callAudioFile from "../../assets/sounds/call.ogg"
import passAudioFile from "../../assets/sounds/pass.ogg"
import AutoActionManager from "./AutoActionManager"
import { Round } from "../../model/Round"

const shuffleAudio = new Audio(shuffleAudioFile)
const playCardAudio = new Audio(playCardAudioFile)
Expand Down Expand Up @@ -44,7 +51,8 @@ const passSound = () => {

interface ActionEvent {
type: Actions
content: unknown
gameState: GameState
transitionData: unknown
}

const WebsocketHandler = () => {
Expand All @@ -55,100 +63,131 @@ const WebsocketHandler = () => {
const playerProfiles = useAppSelector(getPlayerProfiles)
const { enqueueSnackbar } = useSnackbar()

const [previousAction, updatePreviousAction] = useState<Actions>()

// Enable the auto action manager after a delay if it isn't already active
useEffect(() => {
setTimeout(() => {
if (!autoActionEnabled) setAutoActionEnabled(true)
}, 2000)
}, [autoActionEnabled])

const handleWebsocketMessage = useCallback(
(message: string) => {
if (previousAction === "LAST_CARD_PLAYED") {
console.info(
"Waiting on last card to allow time to view cards...",
)
setTimeout(() => processWebsocketMessage(message), 4000)
} else {
processWebsocketMessage(message)
}
},
[previousAction],
)

const processWebsocketMessage = (message: string) => {
const handleWebsocketMessage = (message: string) => {
const payload = JSON.parse(message)
const actionEvent = JSON.parse(payload.payload) as ActionEvent

updatePreviousAction(actionEvent.type)
processActons(actionEvent.type, actionEvent.content)

if (actionEvent.type !== Actions.BuyCardsNotification) {
const gameState = actionEvent.content as GameState
dispatch(updateGame(gameState))
}
processAction(actionEvent)

// Only enable the auto action manager when we have successfully processed a message
if (!autoActionEnabled) setAutoActionEnabled(true)
}

const reloadCards = (payload: unknown, clearSelected = false) => {
const gameState = payload as GameState
const sendCardsBoughtNotification = useCallback(
(buyCardsEvt: BuyCardsEvent) => {
const player = playerProfiles.find(
p => p.id === buyCardsEvt.playerId,
)
if (player)
enqueueSnackbar(`${player.name} bought ${buyCardsEvt.bought}`, {
variant: "info",
})
},
[playerProfiles],
)

const reloadCards = (cards: string[], clearSelected = false) => {
if (clearSelected) {
dispatch(clearSelectedCards())
dispatch(clearAutoPlay())
}
dispatch(updateMyCards(gameState.cards))
dispatch(updateMyCards(cards))
}

const processActons = useCallback(
(type: Actions, payload: unknown) => {
switch (type) {
case "DEAL":
shuffleSound()
reloadCards(payload, true)
break
case "CHOOSE_FROM_DUMMY":
case "BUY_CARDS":
case "LAST_CARD_PLAYED":
case "CARD_PLAYED":
playCardSound()
reloadCards(payload, isMyGo)
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
}
// On hand completion we need to display the last card to the user
const processHandCompleted = async (
game: GameState,
previousRound: Round,
) => {
// Disable actions by setting isMyGo to false
dispatch(disableActions())

// Show the last card of the last roung being played
playCardSound()
dispatch(updatePlayedCards(previousRound.currentHand.playedCards))
await new Promise(r => setTimeout(r, 4000))

// Finally update the game with the latest state
dispatch(updateGame(game))
dispatch(updateMyCards(game.cards))
}

enqueueSnackbar(
`${player.name} bought ${buyCardsEvt.bought}`,
)
// On round completion we need to display the last round to the user
const processRoundCompleted = async (
game: GameState,
previousRound: Round,
) => {
// Disable actions by setting isMyGo to false
dispatch(disableActions())

// Show the last card of the penultimate round being played
playCardSound()
const penultimateHand = previousRound.completedHands.pop()
if (!penultimateHand) throw Error("Failed to get the penultimate round")
dispatch(updatePlayedCards(penultimateHand.playedCards))
await new Promise(r => setTimeout(r, 4000))

// Next show the final round being played
playCardSound()
dispatch(updatePlayedCards(previousRound.currentHand.playedCards))
dispatch(updateMyCards([]))
await new Promise(r => setTimeout(r, 6000))

// Finally update the game with the latest state
shuffleSound()
dispatch(updateGame(game))
dispatch(updateMyCards(game.cards))
}

break
const processAction = useCallback(
async (action: ActionEvent) => {
console.log(action.type)
switch (action.type) {
case "HAND_COMPLETED":
await processHandCompleted(
action.gameState,
action.transitionData as Round,
)
break
case "ROUND_COMPLETED":
reloadCards(payload)
await processRoundCompleted(
action.gameState,
action.transitionData as Round,
)
break
case "BUY_CARDS":
const buyCardsEvt = action.transitionData as BuyCardsEvent
sendCardsBoughtNotification(buyCardsEvt)
reloadCards(action.gameState.cards, isMyGo)
dispatch(updateGame(action.gameState))
break
case "CHOOSE_FROM_DUMMY":
case "CARD_PLAYED":
playCardSound()
reloadCards(action.gameState.cards, isMyGo)
dispatch(updateGame(action.gameState))
break
case "CALL":
callSound()
reloadCards(payload, true)
reloadCards(action.gameState.cards, true)
dispatch(updateGame(action.gameState))
break
case "PASS":
passSound()
reloadCards(payload, true)
reloadCards(action.gameState.cards, true)
dispatch(updateGame(action.gameState))
break

case "REPLAY":
case "GAME_OVER":
dispatch(updateGame(action.gameState))
}
},
[playerProfiles, isMyGo],
Expand Down
21 changes: 9 additions & 12 deletions src/model/Events.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,13 @@
export enum Actions {
Deal = "DEAL",
ChooseFromDummy = "CHOOSE_FROM_DUMMY",
BuyCards = "BUY_CARDS",
LastCardPlayed = "LAST_CARD_PLAYED",
CardPlayed = "CARD_PLAYED",
Replay = "REPLAY",
GameOver = "GAME_OVER",
BuyCardsNotification = "BUY_CARDS_NOTIFICATION",
HandCompleted = "HAND_COMPLETED",
RoundCompleted = "ROUND_COMPLETED",
Call = "CALL",
Pass = "PASS",
REPLAY = "REPLAY",
CALL = "CALL",
PASS = "PASS",
CHOOSE_FROM_DUMMY = "CHOOSE_FROM_DUMMY",
BUY_CARDS = "BUY_CARDS",
CARD_PLAYED = "CARD_PLAYED",
GAME_OVER = "GAME_OVER",
ROUND_COMPLETED = "ROUND_COMPLETED",
HAND_COMPLETED = "HAND_COMPLETED",
}

export interface BuyCardsEvent {
Expand Down
1 change: 0 additions & 1 deletion src/model/Game.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ export interface GameState {
maxCall?: number
me?: Player
players: Player[]
playedCards: PlayedCard[]
}

export interface CreateGame {
Expand Down
10 changes: 8 additions & 2 deletions src/services/GameService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import { updateGame, updatePlayers } from "../caches/GameSlice"
import { getAccessToken } from "../caches/MyProfileSlice"
import { addMyGame, removeMyGame, updateMyGames } from "../caches/MyGamesSlice"
import { updatePlayerProfiles } from "../caches/PlayerProfilesSlice"
import { clearSelectedCards, updateMyCards } from "../caches/MyCardsSlice"
import {
clearSelectedCards,
removeCard,
updateMyCards,
} from "../caches/MyCardsSlice"
import { clearAutoPlay } from "../caches/AutoPlaySlice"

const getGame =
Expand Down Expand Up @@ -173,7 +177,9 @@ const chooseFromDummy =

const playCard =
(gameId: string, card: string): AppThunk<Promise<void>> =>
async (_, getState) => {
async (dispatch, getState) => {
dispatch(removeCard(card))
dispatch(clearAutoPlay())
const accessToken = getAccessToken(getState())
await axios.put(
`${process.env.REACT_APP_API_URL}/api/v1/playCard?gameId=${gameId}&card=${card}`,
Expand Down

0 comments on commit 5d00047

Please sign in to comment.