Skip to content

Commit bdf1ea8

Browse files
committed
Autoplaying last hand on the server
1 parent 23502e3 commit bdf1ea8

File tree

7 files changed

+137
-83
lines changed

7 files changed

+137
-83
lines changed

src/caches/GameSlice.ts

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { createSelector, createSlice, PayloadAction } from "@reduxjs/toolkit"
22

3-
import { GameState, GameStatus } from "../model/Game"
3+
import { GameState, GameStatus, PlayedCard } from "../model/Game"
44
import { Player } from "../model/Player"
55
import { RoundStatus } from "../model/Round"
66
import { RootState } from "./caches"
@@ -13,7 +13,6 @@ const initialState: GameState = {
1313
cards: [],
1414
status: GameStatus.NONE,
1515
players: [],
16-
playedCards: [],
1716
}
1817

1918
export const gameSlice = createSlice({
@@ -24,11 +23,24 @@ export const gameSlice = createSlice({
2423
updatePlayers: (state, action: PayloadAction<Player[]>) => {
2524
state.players = action.payload
2625
},
26+
updatePlayedCards: (state, action: PayloadAction<PlayedCard[]>) => {
27+
if (state.round)
28+
state.round.currentHand.playedCards = action.payload
29+
},
30+
disableActions: state => {
31+
state.isMyGo = false
32+
},
2733
resetGame: () => initialState,
2834
},
2935
})
3036

31-
export const { updateGame, updatePlayers, resetGame } = gameSlice.actions
37+
export const {
38+
updateGame,
39+
disableActions,
40+
updatePlayedCards,
41+
updatePlayers,
42+
resetGame,
43+
} = gameSlice.actions
3244

3345
export const getGame = (state: RootState) => state.game
3446
export const getGamePlayers = createSelector(getGame, game => game.players)

src/caches/MyCardsSlice.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@ export const myCardsSlice = createSlice({
2828
cards: action.payload,
2929
}
3030
},
31+
removeCard: (state, action: PayloadAction<string>) => {
32+
const idx = state.cards.findIndex(c => c.name === action.payload)
33+
if (idx > 0) state.cards[idx] = { ...BLANK_CARD, selected: false }
34+
},
3135
toggleSelect: (state, action: PayloadAction<SelectableCard>) =>
3236
state.cards.forEach(c => {
3337
if (c.name === action.payload.name) c.selected = !c.selected
@@ -49,6 +53,7 @@ export const myCardsSlice = createSlice({
4953
export const {
5054
updateMyCards,
5155
replaceMyCards,
56+
removeCard,
5257
clearSelectedCards,
5358
toggleSelect,
5459
toggleUniqueSelect,

src/components/Game/AutoActionManager.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,7 @@ const AutoActionManager = () => {
5151
}, [gameId, isInBunker])
5252

5353
// 1. Play card when you've pre-selected a card
54-
// 2. Play card when you only have one left
55-
// 3. Play worst card if best card lead out
54+
// 2. Play worst card if best card lead out
5655
useEffect(() => {
5756
if (
5857
gameId &&
@@ -61,7 +60,6 @@ const AutoActionManager = () => {
6160
round.status === RoundStatus.PLAYING
6261
) {
6362
if (autoPlayCard) playCard(gameId, autoPlayCard, true)
64-
else if (cards.length === 1) playCard(gameId, cards[0], true)
6563
else if (bestCardLead(round)) {
6664
const cardToPlay = getWorstCard(cards, round.suit)
6765
if (cardToPlay) playCard(gameId, cardToPlay.name, true)

src/components/Game/WebsocketManager.tsx

Lines changed: 99 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,13 @@ import React, { useCallback, useEffect, useState } from "react"
22

33
import { StompSessionProvider, useSubscription } from "react-stomp-hooks"
44
import { useAppDispatch, useAppSelector } from "../../caches/hooks"
5-
import { getGameId, getIsMyGo, updateGame } from "../../caches/GameSlice"
5+
import {
6+
disableActions,
7+
getGameId,
8+
getIsMyGo,
9+
updateGame,
10+
updatePlayedCards,
11+
} from "../../caches/GameSlice"
612
import { getAccessToken } from "../../caches/MyProfileSlice"
713
import { getPlayerProfiles } from "../../caches/PlayerProfilesSlice"
814
import { GameState } from "../../model/Game"
@@ -16,6 +22,7 @@ import playCardAudioFile from "../../assets/sounds/play_card.ogg"
1622
import callAudioFile from "../../assets/sounds/call.ogg"
1723
import passAudioFile from "../../assets/sounds/pass.ogg"
1824
import AutoActionManager from "./AutoActionManager"
25+
import { Round } from "../../model/Round"
1926

2027
const shuffleAudio = new Audio(shuffleAudioFile)
2128
const playCardAudio = new Audio(playCardAudioFile)
@@ -44,7 +51,8 @@ const passSound = () => {
4451

4552
interface ActionEvent {
4653
type: Actions
47-
content: unknown
54+
gameState: GameState
55+
transitionData: unknown
4856
}
4957

5058
const WebsocketHandler = () => {
@@ -55,100 +63,129 @@ const WebsocketHandler = () => {
5563
const playerProfiles = useAppSelector(getPlayerProfiles)
5664
const { enqueueSnackbar } = useSnackbar()
5765

58-
const [previousAction, updatePreviousAction] = useState<Actions>()
59-
6066
// Enable the auto action manager after a delay if it isn't already active
6167
useEffect(() => {
6268
setTimeout(() => {
6369
if (!autoActionEnabled) setAutoActionEnabled(true)
6470
}, 2000)
6571
}, [autoActionEnabled])
6672

67-
const handleWebsocketMessage = useCallback(
68-
(message: string) => {
69-
if (previousAction === "LAST_CARD_PLAYED") {
70-
console.info(
71-
"Waiting on last card to allow time to view cards...",
72-
)
73-
setTimeout(() => processWebsocketMessage(message), 4000)
74-
} else {
75-
processWebsocketMessage(message)
76-
}
77-
},
78-
[previousAction],
79-
)
80-
81-
const processWebsocketMessage = (message: string) => {
73+
const handleWebsocketMessage = (message: string) => {
8274
const payload = JSON.parse(message)
8375
const actionEvent = JSON.parse(payload.payload) as ActionEvent
8476

85-
updatePreviousAction(actionEvent.type)
86-
processActons(actionEvent.type, actionEvent.content)
87-
88-
if (actionEvent.type !== Actions.BuyCardsNotification) {
89-
const gameState = actionEvent.content as GameState
90-
dispatch(updateGame(gameState))
91-
}
77+
processAction(actionEvent)
9278

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

97-
const reloadCards = (payload: unknown, clearSelected = false) => {
98-
const gameState = payload as GameState
83+
const sendCardsBoughtNotification = useCallback(
84+
(buyCardsEvt: BuyCardsEvent) => {
85+
const player = playerProfiles.find(
86+
p => p.id === buyCardsEvt.playerId,
87+
)
88+
if (player)
89+
enqueueSnackbar(`${player.name} bought ${buyCardsEvt.bought}`)
90+
},
91+
[playerProfiles],
92+
)
9993

94+
const reloadCards = (cards: string[], clearSelected = false) => {
10095
if (clearSelected) {
10196
dispatch(clearSelectedCards())
10297
dispatch(clearAutoPlay())
10398
}
104-
dispatch(updateMyCards(gameState.cards))
99+
dispatch(updateMyCards(cards))
105100
}
106101

107-
const processActons = useCallback(
108-
(type: Actions, payload: unknown) => {
109-
switch (type) {
110-
case "DEAL":
111-
shuffleSound()
112-
reloadCards(payload, true)
113-
break
114-
case "CHOOSE_FROM_DUMMY":
115-
case "BUY_CARDS":
116-
case "LAST_CARD_PLAYED":
117-
case "CARD_PLAYED":
118-
playCardSound()
119-
reloadCards(payload, isMyGo)
120-
break
121-
case "REPLAY":
122-
break
123-
case "GAME_OVER":
124-
break
125-
case "BUY_CARDS_NOTIFICATION":
126-
const buyCardsEvt = payload as BuyCardsEvent
127-
const player = playerProfiles.find(
128-
p => p.id === buyCardsEvt.playerId,
129-
)
130-
if (!player) {
131-
break
132-
}
102+
// On hand completion we need to display the last card to the user
103+
const processHandCompleted = async (
104+
game: GameState,
105+
previousRound: Round,
106+
) => {
107+
// Disable actions by setting isMyGo to false
108+
dispatch(disableActions())
109+
110+
// Show the last card of the last roung being played
111+
playCardSound()
112+
dispatch(updatePlayedCards(previousRound.currentHand.playedCards))
113+
await new Promise(r => setTimeout(r, 4000))
114+
115+
// Finally update the game with the latest state
116+
dispatch(updateGame(game))
117+
dispatch(updateMyCards(game.cards))
118+
}
133119

134-
enqueueSnackbar(
135-
`${player.name} bought ${buyCardsEvt.bought}`,
136-
)
120+
// On round completion we need to display the last round to the user
121+
const processRoundCompleted = async (
122+
game: GameState,
123+
previousRound: Round,
124+
) => {
125+
// Disable actions by setting isMyGo to false
126+
dispatch(disableActions())
127+
128+
// Show the last card of the penultimate round being played
129+
playCardSound()
130+
const penultimateHand = previousRound.completedHands.pop()
131+
if (!penultimateHand) throw Error("Failed to get the penultimate round")
132+
dispatch(updatePlayedCards(penultimateHand.playedCards))
133+
await new Promise(r => setTimeout(r, 4000))
134+
135+
// Next show the final round being played
136+
playCardSound()
137+
dispatch(updatePlayedCards(previousRound.currentHand.playedCards))
138+
dispatch(updateMyCards([]))
139+
await new Promise(r => setTimeout(r, 6000))
140+
141+
// Finally update the game with the latest state
142+
shuffleSound()
143+
dispatch(updateGame(game))
144+
dispatch(updateMyCards(game.cards))
145+
}
137146

138-
break
147+
const processAction = useCallback(
148+
async (action: ActionEvent) => {
149+
console.log(action.type)
150+
switch (action.type) {
139151
case "HAND_COMPLETED":
152+
await processHandCompleted(
153+
action.gameState,
154+
action.transitionData as Round,
155+
)
140156
break
141157
case "ROUND_COMPLETED":
142-
reloadCards(payload)
158+
await processRoundCompleted(
159+
action.gameState,
160+
action.transitionData as Round,
161+
)
162+
break
163+
case "BUY_CARDS":
164+
const buyCardsEvt = action.transitionData as BuyCardsEvent
165+
sendCardsBoughtNotification(buyCardsEvt)
166+
reloadCards(action.gameState.cards, isMyGo)
167+
dispatch(updateGame(action.gameState))
168+
break
169+
case "CHOOSE_FROM_DUMMY":
170+
case "CARD_PLAYED":
171+
playCardSound()
172+
reloadCards(action.gameState.cards, isMyGo)
173+
dispatch(updateGame(action.gameState))
143174
break
144175
case "CALL":
145176
callSound()
146-
reloadCards(payload, true)
177+
reloadCards(action.gameState.cards, true)
178+
dispatch(updateGame(action.gameState))
147179
break
148180
case "PASS":
149181
passSound()
150-
reloadCards(payload, true)
182+
reloadCards(action.gameState.cards, true)
183+
dispatch(updateGame(action.gameState))
151184
break
185+
186+
case "REPLAY":
187+
case "GAME_OVER":
188+
dispatch(updateGame(action.gameState))
152189
}
153190
},
154191
[playerProfiles, isMyGo],

src/model/Events.ts

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,13 @@
11
export enum Actions {
2-
Deal = "DEAL",
3-
ChooseFromDummy = "CHOOSE_FROM_DUMMY",
4-
BuyCards = "BUY_CARDS",
5-
LastCardPlayed = "LAST_CARD_PLAYED",
6-
CardPlayed = "CARD_PLAYED",
7-
Replay = "REPLAY",
8-
GameOver = "GAME_OVER",
9-
BuyCardsNotification = "BUY_CARDS_NOTIFICATION",
10-
HandCompleted = "HAND_COMPLETED",
11-
RoundCompleted = "ROUND_COMPLETED",
12-
Call = "CALL",
13-
Pass = "PASS",
2+
REPLAY = "REPLAY",
3+
CALL = "CALL",
4+
PASS = "PASS",
5+
CHOOSE_FROM_DUMMY = "CHOOSE_FROM_DUMMY",
6+
BUY_CARDS = "BUY_CARDS",
7+
CARD_PLAYED = "CARD_PLAYED",
8+
GAME_OVER = "GAME_OVER",
9+
ROUND_COMPLETED = "ROUND_COMPLETED",
10+
HAND_COMPLETED = "HAND_COMPLETED",
1411
}
1512

1613
export interface BuyCardsEvent {

src/model/Game.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,6 @@ export interface GameState {
3838
maxCall?: number
3939
me?: Player
4040
players: Player[]
41-
playedCards: PlayedCard[]
4241
}
4342

4443
export interface CreateGame {

src/services/GameService.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,11 @@ import { updateGame, updatePlayers } from "../caches/GameSlice"
99
import { getAccessToken } from "../caches/MyProfileSlice"
1010
import { addMyGame, removeMyGame, updateMyGames } from "../caches/MyGamesSlice"
1111
import { updatePlayerProfiles } from "../caches/PlayerProfilesSlice"
12-
import { clearSelectedCards, updateMyCards } from "../caches/MyCardsSlice"
12+
import {
13+
clearSelectedCards,
14+
removeCard,
15+
updateMyCards,
16+
} from "../caches/MyCardsSlice"
1317
import { clearAutoPlay } from "../caches/AutoPlaySlice"
1418

1519
const getGame =
@@ -173,7 +177,9 @@ const chooseFromDummy =
173177

174178
const playCard =
175179
(gameId: string, card: string): AppThunk<Promise<void>> =>
176-
async (_, getState) => {
180+
async (dispatch, getState) => {
181+
dispatch(removeCard(card))
182+
dispatch(clearAutoPlay())
177183
const accessToken = getAccessToken(getState())
178184
await axios.put(
179185
`${process.env.REACT_APP_API_URL}/api/v1/playCard?gameId=${gameId}&card=${card}`,

0 commit comments

Comments
 (0)