From 63547b951345950f5080cb0b412b7f8ab414ba01 Mon Sep 17 00:00:00 2001 From: Daithi Hearn Date: Fri, 29 Sep 2023 14:24:11 +0200 Subject: [PATCH] refactor: using enums for cards --- jest.config.js | 12 + package.json | 2 +- .../originals/{blank_card.png => EMPTY.png} | Bin .../thumbnails/{blank_card.png => EMPTY.png} | Bin public/manifest.json | 2 +- src/caches/MyCardsSlice.ts | 10 +- src/components/Game/Actions/Calling.tsx | 2 +- src/components/Game/Actions/PlayCard.tsx | 10 +- src/components/Game/Actions/SelectSuit.tsx | 6 +- src/components/Game/MyCards.tsx | 28 +- src/components/Game/PlayerCard.tsx | 7 +- src/components/Game/PlayersAndCards.tsx | 2 +- src/components/Game/WebsocketManager.tsx | 3 +- src/components/GameStats/PlayerSwitcher.tsx | 2 +- .../Leaderboard/DoublesLeaderboard.tsx | 12 +- .../Leaderboard/SinglesLeaderboard.tsx | 10 +- src/model/Cards.ts | 289 ++++++++++-------- src/model/Game.ts | 5 +- src/model/Hand.ts | 3 +- src/pages/Home/Home.tsx | 1 - src/utils/ErrorUtils.ts | 7 +- src/utils/GameUtils.spec.ts | 38 +++ src/utils/GameUtils.ts | 57 ++-- tsconfig.json | 7 +- 24 files changed, 291 insertions(+), 224 deletions(-) create mode 100644 jest.config.js rename public/cards/originals/{blank_card.png => EMPTY.png} (100%) rename public/cards/thumbnails/{blank_card.png => EMPTY.png} (100%) create mode 100644 src/utils/GameUtils.spec.ts diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..0dc796f --- /dev/null +++ b/jest.config.js @@ -0,0 +1,12 @@ +/** @type {import("jest").Config} */ +const { defaults: tsjPreset } = require("ts-jest/presets") + +module.exports = { + ...tsjPreset, + testTimeout: 10000, + moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], + testMatch: ["**/*.(spec|test).(ts|tsx|js|jsx)"], + maxWorkers: 2, + workerIdleMemoryLimit: "1GB", + reporters: ["default", "jest-junit"], +} diff --git a/package.json b/package.json index 75d4540..0f5e7bb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "7.3.3", + "version": "7.4.0", "description": "React frontend for the Cards 110", "author": "Daithi Hearn", "license": "MIT", diff --git a/public/cards/originals/blank_card.png b/public/cards/originals/EMPTY.png similarity index 100% rename from public/cards/originals/blank_card.png rename to public/cards/originals/EMPTY.png diff --git a/public/cards/thumbnails/blank_card.png b/public/cards/thumbnails/EMPTY.png similarity index 100% rename from public/cards/thumbnails/blank_card.png rename to public/cards/thumbnails/EMPTY.png diff --git a/public/manifest.json b/public/manifest.json index 2d66663..586f8a2 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,7 +1,7 @@ { "short_name": "Cards 110", "name": "Cards 110", - "version": "7.3.3", + "version": "7.4.0", "icons": [ { "src": "./assets/favicon.png", diff --git a/src/caches/MyCardsSlice.ts b/src/caches/MyCardsSlice.ts index 502fcc6..a11994c 100644 --- a/src/caches/MyCardsSlice.ts +++ b/src/caches/MyCardsSlice.ts @@ -1,5 +1,5 @@ import { createSelector, createSlice, PayloadAction } from "@reduxjs/toolkit" -import { BLANK_CARD, SelectableCard } from "model/Cards" +import { CardName, EMPTY, SelectableCard } from "model/Cards" import { processOrderedCardsAfterGameUpdate } from "utils/GameUtils" import { RootState } from "./caches" @@ -15,7 +15,7 @@ export const myCardsSlice = createSlice({ name: "myCards", initialState: initialState, reducers: { - updateMyCards: (state, action: PayloadAction) => { + updateMyCards: (state, action: PayloadAction) => { return { cards: processOrderedCardsAfterGameUpdate( state.cards, @@ -30,7 +30,7 @@ export const myCardsSlice = createSlice({ }, removeCard: (state, action: PayloadAction) => { const idx = state.cards.findIndex(c => c.name === action.payload) - if (idx > 0) state.cards[idx] = { ...BLANK_CARD, selected: false } + if (idx > 0) state.cards[idx] = { ...EMPTY, selected: false } }, selectCard: (state, action: PayloadAction) => { state.cards.forEach(c => { @@ -54,7 +54,7 @@ export const myCardsSlice = createSlice({ }), selectAll: state => { state.cards.forEach(c => { - if (c.name !== BLANK_CARD.name) c.selected = true + if (c.name !== EMPTY.name) c.selected = true }) }, clearSelectedCards: state => { @@ -82,7 +82,7 @@ export const { export const getMyCards = (state: RootState) => state.myCards.cards export const getMyCardsWithoutBlanks = createSelector(getMyCards, cards => - cards.filter(c => c.name !== BLANK_CARD.name), + cards.filter(c => c.name !== EMPTY.name), ) export const getSelectedCards = createSelector(getMyCards, cards => diff --git a/src/components/Game/Actions/Calling.tsx b/src/components/Game/Actions/Calling.tsx index cfd0b04..594804b 100644 --- a/src/components/Game/Actions/Calling.tsx +++ b/src/components/Game/Actions/Calling.tsx @@ -37,7 +37,7 @@ const Calling = () => { ) const buttonsEnabled = useMemo( - () => round && round.currentHand && cards.length > 0 && isMyGo, + () => round?.currentHand && cards.length > 0 && isMyGo, [round, cards, isMyGo], ) diff --git a/src/components/Game/Actions/PlayCard.tsx b/src/components/Game/Actions/PlayCard.tsx index cf25709..11695aa 100644 --- a/src/components/Game/Actions/PlayCard.tsx +++ b/src/components/Game/Actions/PlayCard.tsx @@ -4,7 +4,6 @@ import GameService from "services/GameService" import { useAppDispatch, useAppSelector } from "caches/hooks" 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 { @@ -19,6 +18,7 @@ import { } from "@mui/material" import { getCardToPlay, updateCardToPlay } from "caches/PlayCardSlice" import { bestCardLead, getBestCard, getWorstCard } from "utils/GameUtils" +import { CardName } from "model/Cards" type AutoPlayState = "off" | "best" | "worst" @@ -74,7 +74,7 @@ const PlayCard = () => { round && round.status === RoundStatus.PLAYING && round.completedHands.length + - myCards.filter(c => c.name !== BLANK_CARD.name).length === + myCards.filter(c => c.name !== CardName.EMPTY).length === 5, [isMyGo, round, myCards], @@ -112,12 +112,10 @@ const PlayCard = () => { if (cardToPlay) playCard(cardToPlay) else if (autoPlay === "worst" || bestCardLead(round)) { const worstCard = getWorstCard(myCards, round.suit) - if (worstCard) - playCard(worstCard.name) + if (worstCard) playCard(worstCard.name) } else if (autoPlay === "best") { const bestCard = getBestCard(myCards, round.suit) - if (bestCard) - playCard(bestCard.name) + if (bestCard) playCard(bestCard.name) } } }, [playCard, autoPlay, round, isMyGo, myCards, cardToPlay]) diff --git a/src/components/Game/Actions/SelectSuit.tsx b/src/components/Game/Actions/SelectSuit.tsx index 59f7774..7a60706 100644 --- a/src/components/Game/Actions/SelectSuit.tsx +++ b/src/components/Game/Actions/SelectSuit.tsx @@ -8,7 +8,7 @@ import { useSnackbar } from "notistack" import { getMyCardsWithoutBlanks, getSelectedCards } from "caches/MyCardsSlice" import { removeAllFromHand } from "utils/GameUtils" import ThrowCardsWarningModal from "./ThrowCardsWarningModal" -import { SelectableCard } from "model/Cards" +import { CardName, SelectableCard } from "model/Cards" import parseError from "utils/ErrorUtils" import { Button } from "@mui/material" @@ -81,8 +81,8 @@ const SelectSuit = () => { for (const element of deletingCards) { if ( - element.name === "JOKER" || - element.name === "ACE_HEARTS" || + element.name === CardName.JOKER || + element.name === CardName.ACE_HEARTS || element.suit === suit ) { return true diff --git a/src/components/Game/MyCards.tsx b/src/components/Game/MyCards.tsx index 92946b5..a2aebd4 100644 --- a/src/components/Game/MyCards.tsx +++ b/src/components/Game/MyCards.tsx @@ -5,7 +5,7 @@ import { Droppable, DropResult, } from "react-beautiful-dnd" -import { BLANK_CARD, SelectableCard } from "model/Cards" +import { EMPTY, SelectableCard } from "model/Cards" import { RoundStatus } from "model/Round" import { getGameId, @@ -32,11 +32,11 @@ import { CardContent, CardMedia, useTheme } from "@mui/material" import { pickBestCards } from "utils/GameUtils" const EMPTY_HAND = [ - { ...BLANK_CARD, selected: false }, - { ...BLANK_CARD, selected: false }, - { ...BLANK_CARD, selected: false }, - { ...BLANK_CARD, selected: false }, - { ...BLANK_CARD, selected: false }, + { ...EMPTY, selected: false }, + { ...EMPTY, selected: false }, + { ...EMPTY, selected: false }, + { ...EMPTY, selected: false }, + { ...EMPTY, selected: false }, ] const usePrevious = (value: any) => { @@ -94,7 +94,7 @@ const MyCards: React.FC = () => { card: SelectableCard, event: React.MouseEvent, ) => { - if (!cardsSelectable || card.name === BLANK_CARD.name) { + if (!cardsSelectable || card.name === EMPTY.name) { return } @@ -153,7 +153,7 @@ const MyCards: React.FC = () => { (card: SelectableCard) => { let classes = "thumbnail-size" - if (cardsSelectable && card.name !== BLANK_CARD.name) { + if (cardsSelectable && card.name !== EMPTY.name) { if (card.name === autoPlayCard) { classes += " auto-played-" + theme.palette.mode } else if (!card.selected) { @@ -179,9 +179,7 @@ const MyCards: React.FC = () => { ref={provided.innerRef}> {myCards.slice(0, 5).map((card, index) => { const draggableId = `${card.name}${ - card.name === BLANK_CARD.name - ? index - : "" + card.name === EMPTY.name ? index : "" }` return ( { draggableId={draggableId} index={index} isDragDisabled={ - card.name === BLANK_CARD.name + card.name === EMPTY.name }> {provided => (
{ : EMPTY_HAND ).map((card, index) => { const draggableId = `${card.name}${ - card.name === BLANK_CARD.name - ? index - : "" + card.name === EMPTY.name ? index : "" }` return ( { draggableId={draggableId} index={index} isDragDisabled={ - card.name === BLANK_CARD.name + card.name === EMPTY.name }> {provided => (
= ({ isGoer }) => { const round = useAppSelector(getRound) const suitChip = useMemo(() => { - if (!round || !round.suit) return undefined + if (!round?.suit) return undefined switch (round.suit) { case Suit.CLUBS: return ChipType.Clubs @@ -181,7 +180,7 @@ const PlayerCard: React.FC = ({ player, className }) => { c => c.playerId === player.id, ) } - return { card: BLANK_CARD.name, playerId: player.id } + return { card: CardName.EMPTY, playerId: player.id } }, [round, player]) const isCurrentPlayer: boolean = useMemo( diff --git a/src/components/Game/PlayersAndCards.tsx b/src/components/Game/PlayersAndCards.tsx index d69f94a..916a5d5 100644 --- a/src/components/Game/PlayersAndCards.tsx +++ b/src/components/Game/PlayersAndCards.tsx @@ -1,5 +1,5 @@ import { useMemo } from "react" -import { Grid, CardContent, Container } from "@mui/material" +import { Grid, CardContent } from "@mui/material" import { getGamePlayers } from "caches/GameSlice" import { useAppSelector } from "caches/hooks" import { Player } from "model/Player" diff --git a/src/components/Game/WebsocketManager.tsx b/src/components/Game/WebsocketManager.tsx index 234499b..9c942ea 100644 --- a/src/components/Game/WebsocketManager.tsx +++ b/src/components/Game/WebsocketManager.tsx @@ -24,6 +24,7 @@ import callAudioFile from "assets/sounds/call.ogg" import passAudioFile from "assets/sounds/pass.ogg" import AutoActionManager from "./AutoActionManager" import { Round } from "model/Round" +import { CardName } from "model/Cards" const shuffleAudio = new Audio(shuffleAudioFile) const playCardAudio = new Audio(playCardAudioFile) @@ -95,7 +96,7 @@ const WebsocketHandler = () => { [playerProfiles], ) - const reloadCards = (cards: string[], clearSelected = false) => { + const reloadCards = (cards: CardName[], clearSelected = false) => { if (clearSelected) { dispatch(clearSelectedCards()) dispatch(clearAutoPlay()) diff --git a/src/components/GameStats/PlayerSwitcher.tsx b/src/components/GameStats/PlayerSwitcher.tsx index b6b78c7..e83d338 100644 --- a/src/components/GameStats/PlayerSwitcher.tsx +++ b/src/components/GameStats/PlayerSwitcher.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useState } from "react" +import { useEffect, useMemo, useState } from "react" import { IconButton, ListItemIcon, diff --git a/src/components/Leaderboard/DoublesLeaderboard.tsx b/src/components/Leaderboard/DoublesLeaderboard.tsx index dd43e3c..32c8598 100644 --- a/src/components/Leaderboard/DoublesLeaderboard.tsx +++ b/src/components/Leaderboard/DoublesLeaderboard.tsx @@ -5,17 +5,7 @@ import { getGamePlayers, getIsGameActive, getRound } from "caches/GameSlice" import { getPlayerProfiles } from "caches/PlayerProfilesSlice" import { compareScore, compareTeamIds } from "utils/PlayerUtils" import { Player } from "model/Player" -import { - List, - ListItem, - ListItemIcon, - Avatar, - ListItemText, - ListItemSecondaryAction, - Box, - Grid, - Typography, -} from "@mui/material" +import { Box, Grid, Typography } from "@mui/material" interface LeaderBoardPlayer { cardsBought?: number diff --git a/src/components/Leaderboard/SinglesLeaderboard.tsx b/src/components/Leaderboard/SinglesLeaderboard.tsx index 68514d9..936782b 100644 --- a/src/components/Leaderboard/SinglesLeaderboard.tsx +++ b/src/components/Leaderboard/SinglesLeaderboard.tsx @@ -5,9 +5,10 @@ import { useAppSelector } from "caches/hooks" import { getPlayerProfiles } from "caches/PlayerProfilesSlice" import { Player } from "model/Player" import { Box, Grid, Typography } from "@mui/material" +import { CardName } from "model/Cards" interface LeaderboardItem { - previousCard?: string + previousCard?: CardName score: number rings: number winner: boolean @@ -59,12 +60,7 @@ const SinglesLeaderboard = () => { return leaderboardData.sort((a, b) => b.score - a.score) }, [playerProfiles, game, getProfile, previousHand]) - if ( - !game || - !game.status || - !playerProfiles || - playerProfiles.length === 0 - ) { + if (!game?.status || !playerProfiles || playerProfiles.length === 0) { return null } diff --git a/src/model/Cards.ts b/src/model/Cards.ts index 14c539c..3b264a8 100644 --- a/src/model/Cards.ts +++ b/src/model/Cards.ts @@ -1,7 +1,64 @@ import { Suit } from "./Suit" +export enum CardName { + EMPTY = "EMPTY", + TWO_HEARTS = "TWO_HEARTS", + THREE_HEARTS = "THREE_HEARTS", + FOUR_HEARTS = "FOUR_HEARTS", + SIX_HEARTS = "SIX_HEARTS", + SEVEN_HEARTS = "SEVEN_HEARTS", + EIGHT_HEARTS = "EIGHT_HEARTS", + NINE_HEARTS = "NINE_HEARTS", + TEN_HEARTS = "TEN_HEARTS", + QUEEN_HEARTS = "QUEEN_HEARTS", + KING_HEARTS = "KING_HEARTS", + ACE_HEARTS = "ACE_HEARTS", + JACK_HEARTS = "JACK_HEARTS", + FIVE_HEARTS = "FIVE_HEARTS", + TWO_DIAMONDS = "TWO_DIAMONDS", + THREE_DIAMONDS = "THREE_DIAMONDS", + FOUR_DIAMONDS = "FOUR_DIAMONDS", + SIX_DIAMONDS = "SIX_DIAMONDS", + SEVEN_DIAMONDS = "SEVEN_DIAMONDS", + EIGHT_DIAMONDS = "EIGHT_DIAMONDS", + NINE_DIAMONDS = "NINE_DIAMONDS", + TEN_DIAMONDS = "TEN_DIAMONDS", + QUEEN_DIAMONDS = "QUEEN_DIAMONDS", + KING_DIAMONDS = "KING_DIAMONDS", + ACE_DIAMONDS = "ACE_DIAMONDS", + JACK_DIAMONDS = "JACK_DIAMONDS", + FIVE_DIAMONDS = "FIVE_DIAMONDS", + TEN_CLUBS = "TEN_CLUBS", + NINE_CLUBS = "NINE_CLUBS", + EIGHT_CLUBS = "EIGHT_CLUBS", + SEVEN_CLUBS = "SEVEN_CLUBS", + SIX_CLUBS = "SIX_CLUBS", + FOUR_CLUBS = "FOUR_CLUBS", + THREE_CLUBS = "THREE_CLUBS", + TWO_CLUBS = "TWO_CLUBS", + QUEEN_CLUBS = "QUEEN_CLUBS", + KING_CLUBS = "KING_CLUBS", + ACE_CLUBS = "ACE_CLUBS", + JACK_CLUBS = "JACK_CLUBS", + FIVE_CLUBS = "FIVE_CLUBS", + TEN_SPADES = "TEN_SPADES", + NINE_SPADES = "NINE_SPADES", + EIGHT_SPADES = "EIGHT_SPADES", + SEVEN_SPADES = "SEVEN_SPADES", + SIX_SPADES = "SIX_SPADES", + FOUR_SPADES = "FOUR_SPADES", + THREE_SPADES = "THREE_SPADES", + TWO_SPADES = "TWO_SPADES", + QUEEN_SPADES = "QUEEN_SPADES", + KING_SPADES = "KING_SPADES", + ACE_SPADES = "ACE_SPADES", + JACK_SPADES = "JACK_SPADES", + FIVE_SPADES = "FIVE_SPADES", + JOKER = "JOKER", +} + export interface Card { - name: string + name: CardName value: number coldValue: number suit: Suit @@ -12,391 +69,385 @@ export interface SelectableCard extends Card { selected: boolean } -export const BLANK_CARD: Card = { - name: "blank_card", - value: 0, - coldValue: 0, - suit: Suit.EMPTY, - renegable: false, -} - -export const CARDS: Card[] = [ - { - name: "EMPTY", +export const CARDS: Record = { + [CardName.EMPTY]: { + name: CardName.EMPTY, value: 0, coldValue: 0, suit: Suit.EMPTY, renegable: false, }, - { - name: "TWO_HEARTS", + [CardName.TWO_HEARTS]: { + name: CardName.TWO_HEARTS, value: 101, coldValue: 2, suit: Suit.HEARTS, renegable: false, }, - { - name: "THREE_HEARTS", + [CardName.THREE_HEARTS]: { + name: CardName.THREE_HEARTS, value: 102, coldValue: 3, suit: Suit.HEARTS, renegable: false, }, - { - name: "FOUR_HEARTS", + [CardName.FOUR_HEARTS]: { + name: CardName.FOUR_HEARTS, value: 103, coldValue: 4, suit: Suit.HEARTS, renegable: false, }, - { - name: "SIX_HEARTS", + [CardName.SIX_HEARTS]: { + name: CardName.SIX_HEARTS, value: 104, coldValue: 6, suit: Suit.HEARTS, renegable: false, }, - { - name: "SEVEN_HEARTS", + [CardName.SEVEN_HEARTS]: { + name: CardName.SEVEN_HEARTS, value: 105, coldValue: 7, suit: Suit.HEARTS, renegable: false, }, - { - name: "EIGHT_HEARTS", + [CardName.EIGHT_HEARTS]: { + name: CardName.EIGHT_HEARTS, value: 106, coldValue: 8, suit: Suit.HEARTS, renegable: false, }, - { - name: "NINE_HEARTS", + [CardName.NINE_HEARTS]: { + name: CardName.NINE_HEARTS, value: 107, coldValue: 9, suit: Suit.HEARTS, renegable: false, }, - { - name: "TEN_HEARTS", + [CardName.TEN_HEARTS]: { + name: CardName.TEN_HEARTS, value: 108, coldValue: 10, suit: Suit.HEARTS, renegable: false, }, - { - name: "QUEEN_HEARTS", + [CardName.QUEEN_HEARTS]: { + name: CardName.QUEEN_HEARTS, value: 109, coldValue: 12, suit: Suit.HEARTS, renegable: false, }, - { - name: "KING_HEARTS", + [CardName.KING_HEARTS]: { + name: CardName.KING_HEARTS, value: 110, coldValue: 13, suit: Suit.HEARTS, renegable: false, }, - { - name: "ACE_HEARTS", + [CardName.ACE_HEARTS]: { + name: CardName.ACE_HEARTS, value: 112, coldValue: 0, suit: Suit.WILD, renegable: true, }, - { - name: "JACK_HEARTS", + [CardName.JACK_HEARTS]: { + name: CardName.JACK_HEARTS, value: 114, coldValue: 11, suit: Suit.HEARTS, renegable: true, }, - { - name: "FIVE_HEARTS", + [CardName.FIVE_HEARTS]: { + name: CardName.FIVE_HEARTS, value: 115, coldValue: 5, suit: Suit.HEARTS, renegable: true, }, - { - name: "TWO_DIAMONDS", + [CardName.TWO_DIAMONDS]: { + name: CardName.TWO_DIAMONDS, value: 101, coldValue: 2, suit: Suit.DIAMONDS, renegable: false, }, - { - name: "THREE_DIAMONDS", + [CardName.THREE_DIAMONDS]: { + name: CardName.THREE_DIAMONDS, value: 102, coldValue: 3, suit: Suit.DIAMONDS, renegable: false, }, - { - name: "FOUR_DIAMONDS", + [CardName.FOUR_DIAMONDS]: { + name: CardName.FOUR_DIAMONDS, value: 103, coldValue: 4, suit: Suit.DIAMONDS, renegable: false, }, - { - name: "SIX_DIAMONDS", + [CardName.SIX_DIAMONDS]: { + name: CardName.SIX_DIAMONDS, value: 104, coldValue: 6, suit: Suit.DIAMONDS, renegable: false, }, - { - name: "SEVEN_DIAMONDS", + [CardName.SEVEN_DIAMONDS]: { + name: CardName.SEVEN_DIAMONDS, value: 105, coldValue: 7, suit: Suit.DIAMONDS, renegable: false, }, - { - name: "EIGHT_DIAMONDS", + [CardName.EIGHT_DIAMONDS]: { + name: CardName.EIGHT_DIAMONDS, value: 106, coldValue: 8, suit: Suit.DIAMONDS, renegable: false, }, - { - name: "NINE_DIAMONDS", + [CardName.NINE_DIAMONDS]: { + name: CardName.NINE_DIAMONDS, value: 107, coldValue: 9, suit: Suit.DIAMONDS, renegable: false, }, - { - name: "TEN_DIAMONDS", + [CardName.TEN_DIAMONDS]: { + name: CardName.TEN_DIAMONDS, value: 108, coldValue: 10, suit: Suit.DIAMONDS, renegable: false, }, - { - name: "QUEEN_DIAMONDS", + [CardName.QUEEN_DIAMONDS]: { + name: CardName.QUEEN_DIAMONDS, value: 109, coldValue: 12, suit: Suit.DIAMONDS, renegable: false, }, - { - name: "KING_DIAMONDS", + [CardName.KING_DIAMONDS]: { + name: CardName.KING_DIAMONDS, value: 110, coldValue: 13, suit: Suit.DIAMONDS, renegable: false, }, - { - name: "ACE_DIAMONDS", + [CardName.ACE_DIAMONDS]: { + name: CardName.ACE_DIAMONDS, value: 111, coldValue: 1, suit: Suit.DIAMONDS, renegable: false, }, - { - name: "JACK_DIAMONDS", + [CardName.JACK_DIAMONDS]: { + name: CardName.JACK_DIAMONDS, value: 114, coldValue: 11, suit: Suit.DIAMONDS, renegable: true, }, - { - name: "FIVE_DIAMONDS", + [CardName.FIVE_DIAMONDS]: { + name: CardName.FIVE_DIAMONDS, value: 115, coldValue: 5, suit: Suit.DIAMONDS, renegable: true, }, - { - name: "TEN_CLUBS", + [CardName.TEN_CLUBS]: { + name: CardName.TEN_CLUBS, value: 101, coldValue: 1, suit: Suit.CLUBS, renegable: false, }, - { - name: "NINE_CLUBS", + [CardName.NINE_CLUBS]: { + name: CardName.NINE_CLUBS, value: 102, coldValue: 2, suit: Suit.CLUBS, renegable: false, }, - { - name: "EIGHT_CLUBS", + [CardName.EIGHT_CLUBS]: { + name: CardName.EIGHT_CLUBS, value: 103, coldValue: 3, suit: Suit.CLUBS, renegable: false, }, - { - name: "SEVEN_CLUBS", + [CardName.SEVEN_CLUBS]: { + name: CardName.SEVEN_CLUBS, value: 104, coldValue: 4, suit: Suit.CLUBS, renegable: false, }, - { - name: "SIX_CLUBS", + [CardName.SIX_CLUBS]: { + name: CardName.SIX_CLUBS, value: 105, coldValue: 5, suit: Suit.CLUBS, renegable: false, }, - { - name: "FOUR_CLUBS", + [CardName.FOUR_CLUBS]: { + name: CardName.FOUR_CLUBS, value: 106, coldValue: 7, suit: Suit.CLUBS, renegable: false, }, - { - name: "THREE_CLUBS", + [CardName.THREE_CLUBS]: { + name: CardName.THREE_CLUBS, value: 107, coldValue: 8, suit: Suit.CLUBS, renegable: false, }, - { - name: "TWO_CLUBS", + [CardName.TWO_CLUBS]: { + name: CardName.TWO_CLUBS, value: 108, coldValue: 9, suit: Suit.CLUBS, renegable: false, }, - { - name: "QUEEN_CLUBS", + [CardName.QUEEN_CLUBS]: { + name: CardName.QUEEN_CLUBS, value: 109, coldValue: 12, suit: Suit.CLUBS, renegable: false, }, - { - name: "KING_CLUBS", + [CardName.KING_CLUBS]: { + name: CardName.KING_CLUBS, value: 110, coldValue: 13, suit: Suit.CLUBS, renegable: false, }, - { - name: "ACE_CLUBS", + [CardName.ACE_CLUBS]: { + name: CardName.ACE_CLUBS, value: 111, coldValue: 10, suit: Suit.CLUBS, renegable: false, }, - { - name: "JACK_CLUBS", + [CardName.JACK_CLUBS]: { + name: CardName.JACK_CLUBS, value: 114, coldValue: 11, suit: Suit.CLUBS, renegable: true, }, - { - name: "FIVE_CLUBS", + [CardName.FIVE_CLUBS]: { + name: CardName.FIVE_CLUBS, value: 115, coldValue: 6, suit: Suit.CLUBS, renegable: true, }, - { - name: "TEN_SPADES", + [CardName.TEN_SPADES]: { + name: CardName.TEN_SPADES, value: 101, coldValue: 1, suit: Suit.SPADES, renegable: false, }, - { - name: "NINE_SPADES", + [CardName.NINE_SPADES]: { + name: CardName.NINE_SPADES, value: 102, coldValue: 2, suit: Suit.SPADES, renegable: false, }, - { - name: "EIGHT_SPADES", + [CardName.EIGHT_SPADES]: { + name: CardName.EIGHT_SPADES, value: 103, coldValue: 3, suit: Suit.SPADES, renegable: false, }, - { - name: "SEVEN_SPADES", + [CardName.SEVEN_SPADES]: { + name: CardName.SEVEN_SPADES, value: 104, coldValue: 4, suit: Suit.SPADES, renegable: false, }, - { - name: "SIX_SPADES", + [CardName.SIX_SPADES]: { + name: CardName.SIX_SPADES, value: 105, coldValue: 5, suit: Suit.SPADES, renegable: false, }, - { - name: "FOUR_SPADES", + [CardName.FOUR_SPADES]: { + name: CardName.FOUR_SPADES, value: 106, coldValue: 7, suit: Suit.SPADES, renegable: false, }, - { - name: "THREE_SPADES", + [CardName.THREE_SPADES]: { + name: CardName.THREE_SPADES, value: 107, coldValue: 8, suit: Suit.SPADES, renegable: false, }, - { - name: "TWO_SPADES", + [CardName.TWO_SPADES]: { + name: CardName.TWO_SPADES, value: 108, coldValue: 9, suit: Suit.SPADES, renegable: false, }, - { - name: "QUEEN_SPADES", + [CardName.QUEEN_SPADES]: { + name: CardName.QUEEN_SPADES, value: 109, coldValue: 12, suit: Suit.SPADES, renegable: false, }, - { - name: "KING_SPADES", + [CardName.KING_SPADES]: { + name: CardName.KING_SPADES, value: 110, coldValue: 13, suit: Suit.SPADES, renegable: false, }, - { - name: "ACE_SPADES", + [CardName.ACE_SPADES]: { + name: CardName.ACE_SPADES, value: 111, coldValue: 10, suit: Suit.SPADES, renegable: false, }, - { - name: "JACK_SPADES", + [CardName.JACK_SPADES]: { + name: CardName.JACK_SPADES, value: 114, coldValue: 11, suit: Suit.SPADES, renegable: true, }, - { - name: "FIVE_SPADES", + [CardName.FIVE_SPADES]: { + name: CardName.FIVE_SPADES, value: 115, coldValue: 6, suit: Suit.SPADES, renegable: true, }, - { - name: "JOKER", + [CardName.JOKER]: { + name: CardName.JOKER, value: 113, coldValue: 0, suit: Suit.WILD, renegable: true, }, -] +} + +export const EMPTY: Card = CARDS[CardName.EMPTY] diff --git a/src/model/Game.ts b/src/model/Game.ts index 6ff143f..3c6faed 100644 --- a/src/model/Game.ts +++ b/src/model/Game.ts @@ -1,3 +1,4 @@ +import { CardName } from "./Cards" import { Player } from "./Player" import { Round } from "./Round" @@ -23,7 +24,7 @@ export interface Game { export interface PlayedCard { playerId: string - card: string + card: CardName } export interface GameState { @@ -33,7 +34,7 @@ export interface GameState { iamGoer: boolean iamDealer: boolean iamAdmin: boolean - cards: string[] + cards: CardName[] status: GameStatus round?: Round maxCall?: number diff --git a/src/model/Hand.ts b/src/model/Hand.ts index 3c24f23..9568d17 100644 --- a/src/model/Hand.ts +++ b/src/model/Hand.ts @@ -1,8 +1,9 @@ +import { CardName } from "./Cards" import { PlayedCard } from "./Game" export interface Hand { timestamp: string - leadOut?: string + leadOut?: CardName currentPlayerId: string playedCards: PlayedCard[] } diff --git a/src/pages/Home/Home.tsx b/src/pages/Home/Home.tsx index 79e5548..8388759 100644 --- a/src/pages/Home/Home.tsx +++ b/src/pages/Home/Home.tsx @@ -13,7 +13,6 @@ import { useSnackbar } from "notistack" import StatsService from "services/StatsService" import parseError from "utils/ErrorUtils" import SettingsService from "services/SettingsService" -import { Spa } from "@mui/icons-material" const Home = () => { const dispatch = useAppDispatch() diff --git a/src/utils/ErrorUtils.ts b/src/utils/ErrorUtils.ts index 0d388d3..98b8e69 100644 --- a/src/utils/ErrorUtils.ts +++ b/src/utils/ErrorUtils.ts @@ -2,15 +2,12 @@ const parseError = (error: any) => { console.log(error) let errorMessage = "Undefined error" if ( - error.response !== undefined && - error.response.data !== undefined && - error.response.data.message !== undefined && + error.response?.data?.message !== undefined && error.response.data.message !== "" ) { errorMessage = error.response.data.message } else if ( - error.response !== undefined && - error.response.statusText !== undefined && + error.response?.statusText !== undefined && error.response.statusText !== "" ) { errorMessage = error.response.statusText diff --git a/src/utils/GameUtils.spec.ts b/src/utils/GameUtils.spec.ts new file mode 100644 index 0000000..8f2bec0 --- /dev/null +++ b/src/utils/GameUtils.spec.ts @@ -0,0 +1,38 @@ +import { compareCards } from "./GameUtils" +import { Card, CardName, CARDS } from "model/Cards" + +const HAND1: Card[] = [ + CARDS[CardName.TWO_HEARTS], + CARDS[CardName.THREE_HEARTS], + CARDS[CardName.FOUR_HEARTS], + CARDS[CardName.FIVE_HEARTS], + CARDS[CardName.SIX_HEARTS], +] + +const HAND2: Card[] = [ + CARDS[CardName.TEN_CLUBS], + CARDS[CardName.JACK_CLUBS], + CARDS[CardName.QUEEN_CLUBS], + CARDS[CardName.TWO_DIAMONDS], + CARDS[CardName.THREE_DIAMONDS], +] + +describe("GameUtils", () => { + describe("compareCards", () => { + it("2 empty hands should return true", () => { + expect(compareCards([], [])).toBe(true) + }) + + it("equal hands", () => { + expect(compareCards(HAND1, HAND1)).toBe(true) + }) + + it("different hands", () => { + expect(compareCards(HAND1, HAND2)).toBe(false) + }) + + it("equal hands with different order", () => { + expect(compareCards(HAND1, HAND1.reverse())).toBe(true) + }) + }) +}) diff --git a/src/utils/GameUtils.ts b/src/utils/GameUtils.ts index ff2220f..c4f6463 100644 --- a/src/utils/GameUtils.ts +++ b/src/utils/GameUtils.ts @@ -1,4 +1,4 @@ -import { CARDS, BLANK_CARD, Card, SelectableCard } from "model/Cards" +import { CARDS, EMPTY, Card, SelectableCard, CardName } from "model/Cards" import { Round } from "model/Round" import { Suit } from "model/Suit" @@ -10,8 +10,8 @@ export const compareCards = ( return false } - const arr1 = [...hand1].filter(ca => ca.name !== BLANK_CARD.name).sort() - const arr2 = [...hand2].filter(ca => ca.name !== BLANK_CARD.name).sort() + const arr1 = [...hand1].filter(ca => ca.name !== CardName.EMPTY).sort() + const arr2 = [...hand2].filter(ca => ca.name !== CardName.EMPTY).sort() if (arr1.length !== arr2.length) { return false @@ -30,7 +30,7 @@ export const padMyHand = (cards: SelectableCard[]): SelectableCard[] => { const paddedCards = [...cards] for (let i = 0; i < 5 - cards.length; i++) { - paddedCards.push({ ...BLANK_CARD, selected: false }) + paddedCards.push({ ...EMPTY, selected: false }) } return paddedCards @@ -38,11 +38,11 @@ export const padMyHand = (cards: SelectableCard[]): SelectableCard[] => { export const processOrderedCardsAfterGameUpdate = ( currentCards: SelectableCard[], - updatedCardNames: string[], + updatedCardNames: CardName[], ): SelectableCard[] => { // Remove blanks const currentCardsNoBlanks = currentCards.filter( - c => c.name !== BLANK_CARD.name, + c => c.name !== CardName.EMPTY, ) // Find the delta between the existing cards and the updated cards we got from the api @@ -63,15 +63,14 @@ export const processOrderedCardsAfterGameUpdate = ( ) { const updatedCurrentCards = [...currentCards] const idx = updatedCurrentCards.findIndex(c => c.name === delta[0].name) - updatedCurrentCards[idx] = { ...BLANK_CARD, selected: false } + updatedCurrentCards[idx] = { ...EMPTY, selected: false } return padMyHand(updatedCurrentCards) // 3. Else send back a fresh hand constructed from the API data } else { const updatedCards = updatedCardNames.map(name => { - const card = CARDS.find(c => c.name === name)! - return { ...card, selected: false } + return { ...CARDS[name], selected: false } }) return padMyHand(updatedCards) @@ -96,8 +95,8 @@ export const riskOfMistakeBuyingCards = ( export const areAllTrumpCards = (cards: T[], suit: Suit) => { for (const element of cards) { if ( - element.name !== "JOKER" && - element.name !== "ACE_HEARTS" && + element.name !== CardName.JOKER && + element.name !== CardName.ACE_HEARTS && element.suit !== suit ) { return false @@ -110,8 +109,8 @@ export const areAllTrumpCards = (cards: T[], suit: Suit) => { export const containsATrumpCard = (cards: T[], suit: Suit) => { for (const element of cards) { if ( - element.name === "JOKER" || - element.name === "ACE_HEARTS" || + element.name === CardName.JOKER || + element.name === CardName.ACE_HEARTS || element.suit === suit ) { return true @@ -147,18 +146,18 @@ export const removeCard = ( } export const bestCardLead = (round: Round) => { - let trumpCards = CARDS.filter( + let trumpCards = Object.values(CARDS).filter( c => c.suit === round.suit || c.suit === Suit.WILD, ) // Remove played trump cards round.completedHands.forEach(hand => { hand.playedCards.forEach(p => { - const card = CARDS.find(c => c.name === p.card) + const card = CARDS[p.card] if ( (card && card.suit === round.suit) || - p.card === "JOKER" || - p.card === "ACE_HEARTS" + p.card === CardName.JOKER || + p.card === CardName.ACE_HEARTS ) trumpCards = trumpCards.filter(c => p.card !== c.name) }) @@ -171,10 +170,7 @@ export const bestCardLead = (round: Round) => { } export const getWorstCard = (cards: SelectableCard[], suit: Suit) => { - const myCardsRich = CARDS.filter(card => - cards.some(c => c.name === card.name), - ) - const myTrumpCards = myCardsRich.filter( + const myTrumpCards = cards.filter( card => card.suit === suit || card.suit === Suit.WILD, ) @@ -184,17 +180,14 @@ export const getWorstCard = (cards: SelectableCard[], suit: Suit) => { return myTrumpCards[0] } else { // Sort ascending by cold value - myCardsRich.sort((a, b) => a.coldValue - b.coldValue) + cards.sort((a, b) => a.coldValue - b.coldValue) - return myCardsRich[0] + return cards[0] } } export const getBestCard = (cards: SelectableCard[], suit: Suit) => { - const myCardsRich = CARDS.filter(card => - cards.some(c => c.name === card.name), - ) - const myTrumpCards = myCardsRich.filter( + const myTrumpCards = cards.filter( card => card.suit === suit || card.suit === Suit.WILD, ) @@ -204,9 +197,9 @@ export const getBestCard = (cards: SelectableCard[], suit: Suit) => { return myTrumpCards[0] } else { // Sort descending by cold value - myCardsRich.sort((a, b) => b.coldValue - a.coldValue) + cards.sort((a, b) => b.coldValue - a.coldValue) - return myCardsRich[0] + return cards[0] } } @@ -229,7 +222,7 @@ export const pickBestCards = ( ) if (remainingCards.length === 0) break bestCards.push( - remainingCards.sort((a, b) => b.coldValue - a.coldValue)[0], + remainingCards.toSorted((a, b) => b.coldValue - a.coldValue)[0], ) } @@ -240,6 +233,6 @@ export const getTrumpCards = (cards: T[], suit: Suit): T[] => cards.filter( card => card.suit === suit || - card.name === "JOKER" || - card.name === "ACE_HEARTS", + card.name === CardName.JOKER || + card.name === CardName.ACE_HEARTS, ) diff --git a/tsconfig.json b/tsconfig.json index 58ff4dd..4d7443e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -18,10 +18,5 @@ "baseUrl": "src" }, "include": ["src"], - "exclude": [ - "build", - "node_modules", - "src/**/*.spec.ts", - "src/**/*.spec.tsx" - ] + "exclude": ["build", "node_modules"] }