From 7cdec1f6a9992932c2f471a05ba3243f2141416c Mon Sep 17 00:00:00 2001 From: Daithi Hearn Date: Sun, 1 Oct 2023 19:31:15 +0200 Subject: [PATCH 1/2] test: adding unit tests --- package.json | 3 +- public/manifest.json | 2 +- src/caches/MyCardsSlice.ts | 14 +- src/components/Game/Actions/Buying.tsx | 4 +- src/components/Game/Actions/SelectSuit.tsx | 4 +- .../Game/Actions/ThrowCardsWarningModal.tsx | 10 +- src/components/Game/MyCards.tsx | 9 +- src/model/Cards.ts | 59 ++- src/utils/GameUtils.spec.ts | 455 +++++++++++++++++- src/utils/GameUtils.ts | 116 ++--- 10 files changed, 568 insertions(+), 108 deletions(-) diff --git a/package.json b/package.json index 0f5e7bb..ebfa5c3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "frontend", - "version": "7.4.0", + "version": "7.4.1", "description": "React frontend for the Cards 110", "author": "Daithi Hearn", "license": "MIT", @@ -53,6 +53,7 @@ "uuid-random": "1.3.2" }, "devDependencies": { + "@babel/plugin-proposal-private-property-in-object": "^7.21.11", "@types/crypto-js": "4.1.2", "@types/enzyme": "3.10.14", "@types/enzyme-adapter-react-16": "1.0.7", diff --git a/public/manifest.json b/public/manifest.json index 586f8a2..5350cb9 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -1,7 +1,7 @@ { "short_name": "Cards 110", "name": "Cards 110", - "version": "7.4.0", + "version": "7.4.1", "icons": [ { "src": "./assets/favicon.png", diff --git a/src/caches/MyCardsSlice.ts b/src/caches/MyCardsSlice.ts index a11994c..a55bf9b 100644 --- a/src/caches/MyCardsSlice.ts +++ b/src/caches/MyCardsSlice.ts @@ -1,10 +1,10 @@ import { createSelector, createSlice, PayloadAction } from "@reduxjs/toolkit" -import { CardName, EMPTY, SelectableCard } from "model/Cards" +import { CardName, EMPTY, Card } from "model/Cards" import { processOrderedCardsAfterGameUpdate } from "utils/GameUtils" import { RootState } from "./caches" export interface MyCardsState { - cards: SelectableCard[] + cards: Card[] } const initialState: MyCardsState = { @@ -23,7 +23,7 @@ export const myCardsSlice = createSlice({ ), } }, - replaceMyCards: (_, action: PayloadAction) => { + replaceMyCards: (_, action: PayloadAction) => { return { cards: action.payload, } @@ -32,22 +32,22 @@ export const myCardsSlice = createSlice({ const idx = state.cards.findIndex(c => c.name === action.payload) if (idx > 0) state.cards[idx] = { ...EMPTY, selected: false } }, - selectCard: (state, action: PayloadAction) => { + selectCard: (state, action: PayloadAction) => { state.cards.forEach(c => { if (c.name === action.payload.name) c.selected = true }) }, - selectCards: (state, action: PayloadAction) => { + selectCards: (state, action: PayloadAction) => { state.cards.forEach(c => { if (action.payload.some(a => a.name === c.name)) c.selected = true }) }, - toggleSelect: (state, action: PayloadAction) => + toggleSelect: (state, action: PayloadAction) => state.cards.forEach(c => { if (c.name === action.payload.name) c.selected = !c.selected }), - toggleUniqueSelect: (state, action: PayloadAction) => + toggleUniqueSelect: (state, action: PayloadAction) => state.cards.forEach(c => { if (c.name === action.payload.name) c.selected = !c.selected else c.selected = false diff --git a/src/components/Game/Actions/Buying.tsx b/src/components/Game/Actions/Buying.tsx index dd93e25..734d03b 100644 --- a/src/components/Game/Actions/Buying.tsx +++ b/src/components/Game/Actions/Buying.tsx @@ -17,7 +17,7 @@ import { } from "caches/GameSlice" import { pickBestCards, riskOfMistakeBuyingCards } from "utils/GameUtils" import ThrowCardsWarningModal from "./ThrowCardsWarningModal" -import { SelectableCard } from "model/Cards" +import { Card } from "model/Cards" import { Button } from "@mui/material" import { getSettings } from "caches/SettingsSlice" @@ -48,7 +48,7 @@ const Buying = () => { }, [readyToBuy]) const buyCards = useCallback( - (sel: SelectableCard[]) => { + (sel: Card[]) => { if (!gameId) return dispatch(GameService.buyCards(gameId, sel)).catch(console.error) }, diff --git a/src/components/Game/Actions/SelectSuit.tsx b/src/components/Game/Actions/SelectSuit.tsx index 7a60706..4770c16 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 { CardName, SelectableCard } from "model/Cards" +import { CardName, Card } from "model/Cards" import parseError from "utils/ErrorUtils" import { Button } from "@mui/material" @@ -59,7 +59,7 @@ const SelectSuit = () => { ) const selectFromDummyCallback = useCallback( - (sel: SelectableCard[], suit?: Suit) => { + (sel: Card[], 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( diff --git a/src/components/Game/Actions/ThrowCardsWarningModal.tsx b/src/components/Game/Actions/ThrowCardsWarningModal.tsx index ccf5385..80e6b1e 100644 --- a/src/components/Game/Actions/ThrowCardsWarningModal.tsx +++ b/src/components/Game/Actions/ThrowCardsWarningModal.tsx @@ -5,20 +5,20 @@ import { DialogContent, Button, ButtonGroup, - Card, + Card as MuiCard, CardMedia, CardContent, } from "@mui/material" import { useAppSelector } from "caches/hooks" import { getMyCardsWithoutBlanks, getSelectedCards } from "caches/MyCardsSlice" -import { SelectableCard } from "model/Cards" +import { Card } from "model/Cards" import { Suit } from "model/Suit" import { getTrumpCards, removeAllFromHand } from "utils/GameUtils" interface ModalOpts { modalVisible: boolean cancelCallback: () => void - continueCallback: (sel: SelectableCard[], suit?: Suit) => void + continueCallback: (sel: Card[], suit?: Suit) => void suit: Suit } @@ -52,7 +52,7 @@ const ThrowCardsWarningModal: React.FC = ({ Are you sure you want to throw these cards away? - + {cardsToBeThrown.map(card => ( = ({ - + ) diff --git a/src/components/Game/MyCards.tsx b/src/components/Game/MyCards.tsx index a2aebd4..aa0202f 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 { EMPTY, SelectableCard } from "model/Cards" +import { EMPTY, Card } from "model/Cards" import { RoundStatus } from "model/Round" import { getGameId, @@ -90,10 +90,7 @@ const MyCards: React.FC = () => { ) const handleSelectCard = useCallback( - ( - card: SelectableCard, - event: React.MouseEvent, - ) => { + (card: Card, event: React.MouseEvent) => { if (!cardsSelectable || card.name === EMPTY.name) { return } @@ -150,7 +147,7 @@ const MyCards: React.FC = () => { ) const getStyleForCard = useCallback( - (card: SelectableCard) => { + (card: Card) => { let classes = "thumbnail-size" if (cardsSelectable && card.name !== EMPTY.name) { diff --git a/src/model/Cards.ts b/src/model/Cards.ts index 3b264a8..c7d9a32 100644 --- a/src/model/Cards.ts +++ b/src/model/Cards.ts @@ -63,9 +63,6 @@ export interface Card { coldValue: number suit: Suit renegable: boolean -} - -export interface SelectableCard extends Card { selected: boolean } @@ -76,6 +73,7 @@ export const CARDS: Record = { coldValue: 0, suit: Suit.EMPTY, renegable: false, + selected: false, }, [CardName.TWO_HEARTS]: { name: CardName.TWO_HEARTS, @@ -83,6 +81,7 @@ export const CARDS: Record = { coldValue: 2, suit: Suit.HEARTS, renegable: false, + selected: false, }, [CardName.THREE_HEARTS]: { name: CardName.THREE_HEARTS, @@ -90,6 +89,7 @@ export const CARDS: Record = { coldValue: 3, suit: Suit.HEARTS, renegable: false, + selected: false, }, [CardName.FOUR_HEARTS]: { name: CardName.FOUR_HEARTS, @@ -97,6 +97,7 @@ export const CARDS: Record = { coldValue: 4, suit: Suit.HEARTS, renegable: false, + selected: false, }, [CardName.SIX_HEARTS]: { name: CardName.SIX_HEARTS, @@ -104,6 +105,7 @@ export const CARDS: Record = { coldValue: 6, suit: Suit.HEARTS, renegable: false, + selected: false, }, [CardName.SEVEN_HEARTS]: { name: CardName.SEVEN_HEARTS, @@ -111,6 +113,7 @@ export const CARDS: Record = { coldValue: 7, suit: Suit.HEARTS, renegable: false, + selected: false, }, [CardName.EIGHT_HEARTS]: { name: CardName.EIGHT_HEARTS, @@ -118,6 +121,7 @@ export const CARDS: Record = { coldValue: 8, suit: Suit.HEARTS, renegable: false, + selected: false, }, [CardName.NINE_HEARTS]: { name: CardName.NINE_HEARTS, @@ -125,6 +129,7 @@ export const CARDS: Record = { coldValue: 9, suit: Suit.HEARTS, renegable: false, + selected: false, }, [CardName.TEN_HEARTS]: { name: CardName.TEN_HEARTS, @@ -132,6 +137,7 @@ export const CARDS: Record = { coldValue: 10, suit: Suit.HEARTS, renegable: false, + selected: false, }, [CardName.QUEEN_HEARTS]: { name: CardName.QUEEN_HEARTS, @@ -139,6 +145,7 @@ export const CARDS: Record = { coldValue: 12, suit: Suit.HEARTS, renegable: false, + selected: false, }, [CardName.KING_HEARTS]: { name: CardName.KING_HEARTS, @@ -146,6 +153,7 @@ export const CARDS: Record = { coldValue: 13, suit: Suit.HEARTS, renegable: false, + selected: false, }, [CardName.ACE_HEARTS]: { name: CardName.ACE_HEARTS, @@ -153,6 +161,7 @@ export const CARDS: Record = { coldValue: 0, suit: Suit.WILD, renegable: true, + selected: false, }, [CardName.JACK_HEARTS]: { name: CardName.JACK_HEARTS, @@ -160,6 +169,7 @@ export const CARDS: Record = { coldValue: 11, suit: Suit.HEARTS, renegable: true, + selected: false, }, [CardName.FIVE_HEARTS]: { name: CardName.FIVE_HEARTS, @@ -167,6 +177,7 @@ export const CARDS: Record = { coldValue: 5, suit: Suit.HEARTS, renegable: true, + selected: false, }, [CardName.TWO_DIAMONDS]: { name: CardName.TWO_DIAMONDS, @@ -174,6 +185,7 @@ export const CARDS: Record = { coldValue: 2, suit: Suit.DIAMONDS, renegable: false, + selected: false, }, [CardName.THREE_DIAMONDS]: { name: CardName.THREE_DIAMONDS, @@ -181,6 +193,7 @@ export const CARDS: Record = { coldValue: 3, suit: Suit.DIAMONDS, renegable: false, + selected: false, }, [CardName.FOUR_DIAMONDS]: { name: CardName.FOUR_DIAMONDS, @@ -188,6 +201,7 @@ export const CARDS: Record = { coldValue: 4, suit: Suit.DIAMONDS, renegable: false, + selected: false, }, [CardName.SIX_DIAMONDS]: { name: CardName.SIX_DIAMONDS, @@ -195,6 +209,7 @@ export const CARDS: Record = { coldValue: 6, suit: Suit.DIAMONDS, renegable: false, + selected: false, }, [CardName.SEVEN_DIAMONDS]: { name: CardName.SEVEN_DIAMONDS, @@ -202,6 +217,7 @@ export const CARDS: Record = { coldValue: 7, suit: Suit.DIAMONDS, renegable: false, + selected: false, }, [CardName.EIGHT_DIAMONDS]: { name: CardName.EIGHT_DIAMONDS, @@ -209,6 +225,7 @@ export const CARDS: Record = { coldValue: 8, suit: Suit.DIAMONDS, renegable: false, + selected: false, }, [CardName.NINE_DIAMONDS]: { name: CardName.NINE_DIAMONDS, @@ -216,6 +233,7 @@ export const CARDS: Record = { coldValue: 9, suit: Suit.DIAMONDS, renegable: false, + selected: false, }, [CardName.TEN_DIAMONDS]: { name: CardName.TEN_DIAMONDS, @@ -223,6 +241,7 @@ export const CARDS: Record = { coldValue: 10, suit: Suit.DIAMONDS, renegable: false, + selected: false, }, [CardName.QUEEN_DIAMONDS]: { name: CardName.QUEEN_DIAMONDS, @@ -230,6 +249,7 @@ export const CARDS: Record = { coldValue: 12, suit: Suit.DIAMONDS, renegable: false, + selected: false, }, [CardName.KING_DIAMONDS]: { name: CardName.KING_DIAMONDS, @@ -237,6 +257,7 @@ export const CARDS: Record = { coldValue: 13, suit: Suit.DIAMONDS, renegable: false, + selected: false, }, [CardName.ACE_DIAMONDS]: { name: CardName.ACE_DIAMONDS, @@ -244,6 +265,7 @@ export const CARDS: Record = { coldValue: 1, suit: Suit.DIAMONDS, renegable: false, + selected: false, }, [CardName.JACK_DIAMONDS]: { name: CardName.JACK_DIAMONDS, @@ -251,6 +273,7 @@ export const CARDS: Record = { coldValue: 11, suit: Suit.DIAMONDS, renegable: true, + selected: false, }, [CardName.FIVE_DIAMONDS]: { name: CardName.FIVE_DIAMONDS, @@ -258,6 +281,7 @@ export const CARDS: Record = { coldValue: 5, suit: Suit.DIAMONDS, renegable: true, + selected: false, }, [CardName.TEN_CLUBS]: { name: CardName.TEN_CLUBS, @@ -265,6 +289,7 @@ export const CARDS: Record = { coldValue: 1, suit: Suit.CLUBS, renegable: false, + selected: false, }, [CardName.NINE_CLUBS]: { name: CardName.NINE_CLUBS, @@ -272,6 +297,7 @@ export const CARDS: Record = { coldValue: 2, suit: Suit.CLUBS, renegable: false, + selected: false, }, [CardName.EIGHT_CLUBS]: { name: CardName.EIGHT_CLUBS, @@ -279,6 +305,7 @@ export const CARDS: Record = { coldValue: 3, suit: Suit.CLUBS, renegable: false, + selected: false, }, [CardName.SEVEN_CLUBS]: { name: CardName.SEVEN_CLUBS, @@ -286,6 +313,7 @@ export const CARDS: Record = { coldValue: 4, suit: Suit.CLUBS, renegable: false, + selected: false, }, [CardName.SIX_CLUBS]: { name: CardName.SIX_CLUBS, @@ -293,6 +321,7 @@ export const CARDS: Record = { coldValue: 5, suit: Suit.CLUBS, renegable: false, + selected: false, }, [CardName.FOUR_CLUBS]: { name: CardName.FOUR_CLUBS, @@ -300,6 +329,7 @@ export const CARDS: Record = { coldValue: 7, suit: Suit.CLUBS, renegable: false, + selected: false, }, [CardName.THREE_CLUBS]: { name: CardName.THREE_CLUBS, @@ -307,6 +337,7 @@ export const CARDS: Record = { coldValue: 8, suit: Suit.CLUBS, renegable: false, + selected: false, }, [CardName.TWO_CLUBS]: { name: CardName.TWO_CLUBS, @@ -314,6 +345,7 @@ export const CARDS: Record = { coldValue: 9, suit: Suit.CLUBS, renegable: false, + selected: false, }, [CardName.QUEEN_CLUBS]: { name: CardName.QUEEN_CLUBS, @@ -321,6 +353,7 @@ export const CARDS: Record = { coldValue: 12, suit: Suit.CLUBS, renegable: false, + selected: false, }, [CardName.KING_CLUBS]: { name: CardName.KING_CLUBS, @@ -328,6 +361,7 @@ export const CARDS: Record = { coldValue: 13, suit: Suit.CLUBS, renegable: false, + selected: false, }, [CardName.ACE_CLUBS]: { name: CardName.ACE_CLUBS, @@ -335,6 +369,7 @@ export const CARDS: Record = { coldValue: 10, suit: Suit.CLUBS, renegable: false, + selected: false, }, [CardName.JACK_CLUBS]: { name: CardName.JACK_CLUBS, @@ -342,6 +377,7 @@ export const CARDS: Record = { coldValue: 11, suit: Suit.CLUBS, renegable: true, + selected: false, }, [CardName.FIVE_CLUBS]: { name: CardName.FIVE_CLUBS, @@ -349,6 +385,7 @@ export const CARDS: Record = { coldValue: 6, suit: Suit.CLUBS, renegable: true, + selected: false, }, [CardName.TEN_SPADES]: { name: CardName.TEN_SPADES, @@ -356,6 +393,7 @@ export const CARDS: Record = { coldValue: 1, suit: Suit.SPADES, renegable: false, + selected: false, }, [CardName.NINE_SPADES]: { name: CardName.NINE_SPADES, @@ -363,6 +401,7 @@ export const CARDS: Record = { coldValue: 2, suit: Suit.SPADES, renegable: false, + selected: false, }, [CardName.EIGHT_SPADES]: { name: CardName.EIGHT_SPADES, @@ -370,6 +409,7 @@ export const CARDS: Record = { coldValue: 3, suit: Suit.SPADES, renegable: false, + selected: false, }, [CardName.SEVEN_SPADES]: { name: CardName.SEVEN_SPADES, @@ -377,6 +417,7 @@ export const CARDS: Record = { coldValue: 4, suit: Suit.SPADES, renegable: false, + selected: false, }, [CardName.SIX_SPADES]: { name: CardName.SIX_SPADES, @@ -384,6 +425,7 @@ export const CARDS: Record = { coldValue: 5, suit: Suit.SPADES, renegable: false, + selected: false, }, [CardName.FOUR_SPADES]: { name: CardName.FOUR_SPADES, @@ -391,6 +433,7 @@ export const CARDS: Record = { coldValue: 7, suit: Suit.SPADES, renegable: false, + selected: false, }, [CardName.THREE_SPADES]: { name: CardName.THREE_SPADES, @@ -398,6 +441,7 @@ export const CARDS: Record = { coldValue: 8, suit: Suit.SPADES, renegable: false, + selected: false, }, [CardName.TWO_SPADES]: { name: CardName.TWO_SPADES, @@ -405,6 +449,7 @@ export const CARDS: Record = { coldValue: 9, suit: Suit.SPADES, renegable: false, + selected: false, }, [CardName.QUEEN_SPADES]: { name: CardName.QUEEN_SPADES, @@ -412,6 +457,7 @@ export const CARDS: Record = { coldValue: 12, suit: Suit.SPADES, renegable: false, + selected: false, }, [CardName.KING_SPADES]: { name: CardName.KING_SPADES, @@ -419,6 +465,7 @@ export const CARDS: Record = { coldValue: 13, suit: Suit.SPADES, renegable: false, + selected: false, }, [CardName.ACE_SPADES]: { name: CardName.ACE_SPADES, @@ -426,6 +473,7 @@ export const CARDS: Record = { coldValue: 10, suit: Suit.SPADES, renegable: false, + selected: false, }, [CardName.JACK_SPADES]: { name: CardName.JACK_SPADES, @@ -433,6 +481,7 @@ export const CARDS: Record = { coldValue: 11, suit: Suit.SPADES, renegable: true, + selected: false, }, [CardName.FIVE_SPADES]: { name: CardName.FIVE_SPADES, @@ -440,6 +489,7 @@ export const CARDS: Record = { coldValue: 6, suit: Suit.SPADES, renegable: true, + selected: false, }, [CardName.JOKER]: { name: CardName.JOKER, @@ -447,7 +497,8 @@ export const CARDS: Record = { coldValue: 0, suit: Suit.WILD, renegable: true, + selected: false, }, } -export const EMPTY: Card = CARDS[CardName.EMPTY] +export const EMPTY: Card = CARDS.EMPTY diff --git a/src/utils/GameUtils.spec.ts b/src/utils/GameUtils.spec.ts index 46481fb..67b3b7a 100644 --- a/src/utils/GameUtils.spec.ts +++ b/src/utils/GameUtils.spec.ts @@ -1,6 +1,21 @@ import { Suit } from "model/Suit" -import { compareCards, getBestCard, getWorstCard } from "./GameUtils" -import { Card, CardName, CARDS } from "model/Cards" +import { + areAllTrumpCards, + bestCardLead, + compareCards, + containsATrumpCard, + getBestCard, + getTrumpCards, + getWorstCard, + padMyHand, + pickBestCards, + processOrderedCardsAfterGameUpdate, + removeAllFromHand, + removeCard, + removeEmptyCards, + riskOfMistakeBuyingCards, +} from "./GameUtils" +import { Card, CardName, CARDS, EMPTY } from "model/Cards" import { Round, RoundStatus } from "model/Round" const ROUND: Round = { @@ -19,44 +34,412 @@ const ROUND: Round = { completedHands: [], } +const ROUND_BEST_LEAD: Round = { + suit: Suit.HEARTS, + currentHand: { + leadOut: CardName.FIVE_HEARTS, + timestamp: "", + currentPlayerId: "", + playedCards: [], + }, + timestamp: "", + number: 0, + dealerId: "", + status: RoundStatus.PLAYING, + dealerSeeingCall: false, + completedHands: [], +} + const HAND1: Card[] = [ - CARDS[CardName.TWO_HEARTS], - CARDS[CardName.THREE_HEARTS], - CARDS[CardName.FOUR_HEARTS], - CARDS[CardName.FIVE_HEARTS], - CARDS[CardName.SIX_HEARTS], + { ...CARDS.TWO_HEARTS, selected: true }, + CARDS.THREE_HEARTS, + CARDS.FOUR_HEARTS, + CARDS.FIVE_HEARTS, + CARDS.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], + CARDS.TEN_CLUBS, + CARDS.JACK_CLUBS, + CARDS.QUEEN_CLUBS, + { ...CARDS.TWO_DIAMONDS, selected: true }, + CARDS.THREE_DIAMONDS, ] -const HAND3: Card[] = [ - CARDS[CardName.KING_SPADES], - CARDS[CardName.THREE_DIAMONDS], - CARDS[CardName.TWO_CLUBS], +const HAND3: Card[] = [CARDS.KING_SPADES, CARDS.THREE_DIAMONDS, CARDS.TWO_CLUBS] + +const HAND4: Card[] = [ + CARDS.TWO_HEARTS, + CARDS.JOKER, + CARDS.THREE_CLUBS, + CARDS.ACE_HEARTS, + CARDS.TWO_DIAMONDS, ] describe("GameUtils", () => { + describe("removeEmptyCards", () => { + it("empty hand", () => { + expect(removeEmptyCards([])).toStrictEqual([]) + }) + + it("full hand", () => { + expect(removeEmptyCards([...HAND1])).toStrictEqual([...HAND1]) + }) + + it("hand with empty card at end", () => { + expect(removeEmptyCards([...HAND1, EMPTY])).toStrictEqual(HAND1) + }) + + it("hand with empty card at beginning", () => { + expect(removeEmptyCards([EMPTY, ...HAND1])).toStrictEqual(HAND1) + }) + + it("hand with empty card in middle", () => { + expect( + removeEmptyCards([CARDS.TWO_HEARTS, EMPTY, ...HAND3]), + ).toStrictEqual([CARDS.TWO_HEARTS, ...HAND3]) + }) + + it("hand with multiple empty cards", () => { + expect( + removeEmptyCards([CARDS.TWO_HEARTS, EMPTY, ...HAND3, EMPTY]), + ).toStrictEqual([CARDS.TWO_HEARTS, ...HAND3]) + }) + + it("hand with multiple empty cards at beginning", () => { + expect( + removeEmptyCards([EMPTY, EMPTY, CARDS.TWO_HEARTS, ...HAND3]), + ).toStrictEqual([CARDS.TWO_HEARTS, ...HAND3]) + }) + + it("hand with multiple empty cards at end", () => { + expect( + removeEmptyCards([CARDS.TWO_HEARTS, ...HAND3, EMPTY, EMPTY]), + ).toStrictEqual([CARDS.TWO_HEARTS, ...HAND3]) + }) + + it("hand with multiple empty cards in middle", () => { + expect( + removeEmptyCards([ + CARDS.TWO_HEARTS, + EMPTY, + ...HAND3, + EMPTY, + EMPTY, + ]), + ).toStrictEqual([CARDS.TWO_HEARTS, ...HAND3]) + }) + + it("hand with only empty cards", () => { + expect(removeEmptyCards([EMPTY, EMPTY, EMPTY])).toStrictEqual([]) + }) + }) describe("compareCards", () => { it("2 empty hands should return true", () => { expect(compareCards([], [])).toBe(true) }) it("equal hands", () => { - expect(compareCards(HAND1, HAND1)).toBe(true) + expect(compareCards([...HAND1], [...HAND1])).toBe(true) }) it("different hands", () => { - expect(compareCards(HAND1, HAND2)).toBe(false) + expect(compareCards([...HAND1], [...HAND2])).toBe(false) }) it("equal hands with different order", () => { - expect(compareCards(HAND1, HAND1.reverse())).toBe(true) + expect(compareCards([...HAND1], [...HAND1].reverse())).toBe(true) + }) + }) + + describe("padMyHand", () => { + it("empty hand", () => { + expect(padMyHand([])).toStrictEqual([ + { ...EMPTY }, + { ...EMPTY }, + { ...EMPTY }, + { ...EMPTY }, + { ...EMPTY }, + ]) + }) + + it("full hand", () => { + expect(padMyHand([...HAND1])).toStrictEqual([...HAND1]) + }) + + it("partial hand", () => { + expect(padMyHand(HAND3)).toStrictEqual([ + CARDS.KING_SPADES, + CARDS.THREE_DIAMONDS, + CARDS.TWO_CLUBS, + EMPTY, + EMPTY, + ]) + }) + }) + + describe("processOrderedCardsAfterGameUpdate", () => { + it("same cards", () => { + expect( + processOrderedCardsAfterGameUpdate( + [...HAND1], + HAND1.map(c => c.name), + ), + ).toStrictEqual(HAND1) + }) + + it("same cards with different order", () => { + expect( + processOrderedCardsAfterGameUpdate( + [...HAND1], + HAND1.map(c => c.name).reverse(), + ), + ).toStrictEqual(HAND1) + }) + + it("First card removed", () => { + const handWithFirstCardRemoved = [...HAND1].slice(1) + const responseFromServer = handWithFirstCardRemoved.map(c => c.name) + const result = processOrderedCardsAfterGameUpdate( + [...HAND1], + responseFromServer, + ) + expect(result).toStrictEqual([EMPTY, ...handWithFirstCardRemoved]) + }) + + it("Last card removed", () => { + const handWithLastCardRemoved = [...HAND1].slice(0, 4) + const responseFromServer = handWithLastCardRemoved.map(c => c.name) + const result = processOrderedCardsAfterGameUpdate( + [...HAND1], + responseFromServer, + ) + expect(result).toStrictEqual([...handWithLastCardRemoved, EMPTY]) + }) + + it("Middle card removed", () => { + const handWithMiddleCardRemoved = [ + ...[...HAND1].slice(0, 2), + ...[...HAND1].slice(3), + ] + const responseFromServer = handWithMiddleCardRemoved.map( + c => c.name, + ) + const result = processOrderedCardsAfterGameUpdate( + [...HAND1], + responseFromServer, + ) + const expected = [ + ...[...HAND1].slice(0, 2), + EMPTY, + ...[...HAND1].slice(3), + ] + expect(result).toStrictEqual(expected) + }) + + it("any other configuration 1", () => { + expect( + processOrderedCardsAfterGameUpdate(HAND1, [ + CardName.FIVE_CLUBS, + ]), + ).toStrictEqual(padMyHand([CARDS.FIVE_CLUBS])) + }) + + it("any other configuration 2", () => { + expect( + processOrderedCardsAfterGameUpdate( + HAND1, + HAND3.map(c => c.name), + ), + ).toStrictEqual(padMyHand(HAND3)) + }) + }) + + describe("areAllTrumpCards", () => { + it("empty hand", () => { + expect(areAllTrumpCards([], Suit.HEARTS)).toBe(true) + }) + + it("all trump cards", () => { + expect(areAllTrumpCards([...HAND1], Suit.HEARTS)).toBe(true) + }) + + it("not all trump cards", () => { + expect(areAllTrumpCards([...HAND3], Suit.HEARTS)).toBe(false) + }) + }) + + describe("containsATrumpCard", () => { + it("empty hand", () => { + expect(containsATrumpCard([], Suit.HEARTS)).toBe(false) + }) + + it("has a trump card", () => { + expect(containsATrumpCard([...HAND1], Suit.HEARTS)).toBe(true) + }) + + it("doesn't have a trump card", () => { + expect(containsATrumpCard([...HAND3], Suit.HEARTS)).toBe(false) + }) + }) + + describe("removeCard", () => { + it("empty hand", () => { + expect(removeCard(CARDS.TWO_HEARTS, [])).toStrictEqual([]) + }) + + it("remove first card", () => { + expect(removeCard(CARDS.TWO_HEARTS, HAND1)).toStrictEqual( + [...HAND1].slice(1), + ) + }) + + it("remove last card", () => { + expect(removeCard(CARDS.SIX_HEARTS, [...HAND1])).toStrictEqual( + [...HAND1].slice(0, 4), + ) + }) + + it("remove middle card", () => { + expect(removeCard(CARDS.FOUR_HEARTS, [...HAND1])).toStrictEqual( + [...HAND1].slice(0, 2).concat([...HAND1].slice(3)), + ) + }) + }) + + describe("removeAllFromHand", () => { + it("empty hand", () => { + expect(removeAllFromHand(HAND1, [])).toStrictEqual([]) + }) + + it("remove first card", () => { + expect(removeAllFromHand([CARDS.TWO_HEARTS], HAND1)).toStrictEqual( + [...HAND1].slice(1), + ) + }) + + it("remove last card", () => { + expect( + removeAllFromHand([CARDS.SIX_HEARTS], [...HAND1]), + ).toStrictEqual([...HAND1].slice(0, 4)) + }) + + it("remove middle card", () => { + expect( + removeAllFromHand([CARDS.FOUR_HEARTS], [...HAND1]), + ).toStrictEqual([...HAND1].slice(0, 2).concat([...HAND1].slice(3))) + }) + + it("remove multiple cards", () => { + expect( + removeAllFromHand( + [CARDS.FOUR_HEARTS, CARDS.TWO_HEARTS], + [...HAND1], + ), + ).toStrictEqual([ + CARDS.THREE_HEARTS, + CARDS.FIVE_HEARTS, + CARDS.SIX_HEARTS, + ]) + }) + + it("remove multiple cards in different order", () => { + expect( + removeAllFromHand( + [CARDS.TWO_HEARTS, CARDS.FOUR_HEARTS], + [...HAND1], + ), + ).toStrictEqual([ + CARDS.THREE_HEARTS, + CARDS.FIVE_HEARTS, + CARDS.SIX_HEARTS, + ]) + }) + + it("remove all cards", () => { + expect(removeAllFromHand([...HAND1], [...HAND1])).toStrictEqual([]) + }) + }) + + describe("riskOfMistakeBuyingCards", () => { + it("none selected", () => { + expect(riskOfMistakeBuyingCards(Suit.HEARTS, [], HAND1)).toBe(true) + }) + + it("no trump cards", () => { + expect( + riskOfMistakeBuyingCards(Suit.DIAMONDS, [], [...HAND1]), + ).toBe(false) + }) + + it("select all cards", () => { + expect( + riskOfMistakeBuyingCards(Suit.HEARTS, [...HAND1], [...HAND1]), + ).toBe(false) + }) + + it("select all trumps", () => { + expect( + riskOfMistakeBuyingCards( + Suit.SPADES, + [CARDS.KING_SPADES], + [...HAND3], + ), + ).toBe(false) + }) + + it("don't select all trumps", () => { + expect( + riskOfMistakeBuyingCards( + Suit.HEARTS, + [CARDS.TWO_HEARTS], + [...HAND1], + ), + ).toBe(true) + }) + }) + + describe("getTrumpCards", () => { + it("empty hand", () => { + expect(getTrumpCards([], Suit.HEARTS)).toStrictEqual([]) + }) + + it("all trump cards", () => { + expect(getTrumpCards([...HAND1], Suit.HEARTS)).toStrictEqual(HAND1) + }) + + it("no trump cards", () => { + expect(getTrumpCards([...HAND3], Suit.HEARTS)).toStrictEqual([]) + }) + + it("all trump cards with joker and ace of hearts", () => { + expect(getTrumpCards([...HAND4], Suit.HEARTS)).toStrictEqual([ + CARDS.TWO_HEARTS, + CARDS.JOKER, + CARDS.ACE_HEARTS, + ]) + }) + + it("some trump cards", () => { + expect(getTrumpCards([...HAND3], Suit.SPADES)).toStrictEqual([ + CARDS.KING_SPADES, + ]) + }) + }) + + describe("bestCardLead", () => { + it("no suit", () => { + expect(bestCardLead({ ...ROUND, suit: undefined })).toBe(false) + }) + + it("best card lead", () => { + expect( + bestCardLead({ ...ROUND_BEST_LEAD, suit: Suit.HEARTS }), + ).toBe(true) + }) + + it("best card not lead", () => { + expect(bestCardLead({ ...ROUND, suit: Suit.HEARTS })).toBe(false) }) }) @@ -65,10 +448,14 @@ describe("GameUtils", () => { expect(getBestCard([], ROUND)).toBe(undefined) }) it("trump card", () => { - expect(getBestCard(HAND1, ROUND)).toBe(CARDS[CardName.FIVE_HEARTS]) + expect(getBestCard([...HAND1], ROUND)).toStrictEqual( + CARDS.FIVE_HEARTS, + ) }) it("follow cold card", () => { - expect(getBestCard(HAND3, ROUND)).toBe(CARDS[CardName.TWO_CLUBS]) + expect(getBestCard([...HAND3], ROUND)).toStrictEqual( + CARDS.TWO_CLUBS, + ) }) }) @@ -77,10 +464,32 @@ describe("GameUtils", () => { expect(getBestCard([], ROUND)).toBe(undefined) }) it("trump card", () => { - expect(getWorstCard(HAND1, ROUND)).toBe(CARDS[CardName.TWO_HEARTS]) + expect(getWorstCard([...HAND1], ROUND)).toStrictEqual({ + ...CARDS.TWO_HEARTS, + selected: true, + }) }) it("follow cold card", () => { - expect(getWorstCard(HAND3, ROUND)).toBe(CARDS[CardName.TWO_CLUBS]) + expect(getWorstCard([...HAND3], ROUND)).toStrictEqual( + CARDS.TWO_CLUBS, + ) + }) + }) + + describe("pickBestCards", () => { + it("empty hand", () => { + expect(pickBestCards([], Suit.CLUBS, 4)).toStrictEqual([]) + }) + it("all trumps", () => { + expect(pickBestCards([...HAND1], Suit.HEARTS, 3)).toStrictEqual( + HAND1, + ) }) + // it("Must keep 2", () => { + // expect(pickBestCards([...HAND1], Suit.DIAMONDS, 5)).toStrictEqual([ + // CARDS.FIVE_HEARTS, + // CARDS.SIX_HEARTS, + // ]) + // }) }) }) diff --git a/src/utils/GameUtils.ts b/src/utils/GameUtils.ts index ffa6910..356de5e 100644 --- a/src/utils/GameUtils.ts +++ b/src/utils/GameUtils.ts @@ -1,7 +1,10 @@ -import { CARDS, EMPTY, Card, SelectableCard, CardName } from "model/Cards" +import { CARDS, EMPTY, Card, CardName } from "model/Cards" import { Round } from "model/Round" import { Suit } from "model/Suit" +export const removeEmptyCards = (cards: Card[]): Card[] => + [...cards].filter(c => c.name !== CardName.EMPTY) + export const compareCards = ( hand1: Card[] | undefined, hand2: Card[] | undefined, @@ -10,15 +13,18 @@ export const compareCards = ( return false } - const arr1 = [...hand1].filter(ca => ca.name !== CardName.EMPTY).sort() - const arr2 = [...hand2].filter(ca => ca.name !== CardName.EMPTY).sort() + let h1 = removeEmptyCards(hand1) + let h2 = removeEmptyCards(hand2) - if (arr1.length !== arr2.length) { + if (h1.length !== h2.length) { return false } - for (let i = 0; i < arr1.length; i++) { - if (arr1[i].name !== arr2[i].name) { + h1 = h1.sort((a, b) => a.name.localeCompare(b.name)) + h2 = h2.sort((a, b) => a.name.localeCompare(b.name)) + + for (let i = 0; i < h1.length; i++) { + if (h1[i].name !== h2[i].name) { return false } } @@ -26,7 +32,7 @@ export const compareCards = ( return true } -export const padMyHand = (cards: SelectableCard[]): SelectableCard[] => { +export const padMyHand = (cards: Card[]): Card[] => { const paddedCards = [...cards] for (let i = 0; i < 5 - cards.length; i++) { @@ -36,10 +42,10 @@ export const padMyHand = (cards: SelectableCard[]): SelectableCard[] => { return paddedCards } -export const processOrderedCardsAfterGameUpdate = ( - currentCards: SelectableCard[], +export const processOrderedCardsAfterGameUpdate = ( + currentCards: Card[], updatedCardNames: CardName[], -): SelectableCard[] => { +): Card[] => { // Remove blanks const currentCardsNoBlanks = currentCards.filter( c => c.name !== CardName.EMPTY, @@ -63,13 +69,13 @@ export const processOrderedCardsAfterGameUpdate = ( ) { const updatedCurrentCards = [...currentCards] const idx = updatedCurrentCards.findIndex(c => c.name === delta[0].name) - updatedCurrentCards[idx] = { ...EMPTY, selected: false } + updatedCurrentCards[idx] = { ...EMPTY } return padMyHand(updatedCurrentCards) - - // 3. Else send back a fresh hand constructed from the API data - } else { - const updatedCards = updatedCardNames.map(name => { + } + // 3. Else send back a fresh hand constructed from the API data + else { + const updatedCards = updatedCardNames.map(name => { return { ...CARDS[name], selected: false } }) @@ -77,22 +83,7 @@ export const processOrderedCardsAfterGameUpdate = ( } } -export const riskOfMistakeBuyingCards = ( - suit: Suit, - selectedCards: T[], - myCards: T[], -) => { - // If you have selected 5 trumps then return false - if (areAllTrumpCards(selectedCards, suit)) { - return false - } - - const deletingCards = removeAllFromHand(selectedCards, myCards) - - return containsATrumpCard(deletingCards, suit) -} - -export const areAllTrumpCards = (cards: T[], suit: Suit) => { +export const areAllTrumpCards = (cards: Card[], suit: Suit) => { for (const element of cards) { if ( element.name !== CardName.JOKER && @@ -106,7 +97,7 @@ export const areAllTrumpCards = (cards: T[], suit: Suit) => { return true } -export const containsATrumpCard = (cards: T[], suit: Suit) => { +export const containsATrumpCard = (cards: Card[], suit: Suit) => { for (const element of cards) { if ( element.name === CardName.JOKER || @@ -120,9 +111,19 @@ export const containsATrumpCard = (cards: T[], suit: Suit) => { return false } -export const removeAllFromHand = ( - cardsToRemove: T[], - originalHand: T[], +export const removeCard = (cardToRemove: Card, orginalHand: Card[]) => { + const newHand = [...orginalHand] // make a separate copy of the array + const index = newHand.findIndex(c => c.name === cardToRemove.name) + if (index !== -1) { + newHand.splice(index, 1) + } + + return newHand +} + +export const removeAllFromHand = ( + cardsToRemove: Card[], + originalHand: Card[], ) => { let newHand = [...originalHand] const ctr = [...cardsToRemove] @@ -132,24 +133,33 @@ export const removeAllFromHand = ( return newHand } -export const removeCard = ( - cardToRemove: T, - orginalHand: T[], +export const riskOfMistakeBuyingCards = ( + suit: Suit, + selectedCards: Card[], + myCards: Card[], ) => { - const newHand = [...orginalHand] // make a separate copy of the array - const index = newHand.indexOf(cardToRemove) - if (index !== -1) { - newHand.splice(index, 1) + // If you have selected 5 trumps then return false + if (selectedCards.length === 5 && areAllTrumpCards(selectedCards, suit)) { + return false } - return newHand + const deletingCards = removeAllFromHand(selectedCards, myCards) + + return containsATrumpCard(deletingCards, suit) } -export const bestCardLead = (round: Round) => { - let trumpCards = Object.values(CARDS).filter( - c => c.suit === round.suit || c.suit === Suit.WILD, +export const getTrumpCards = (cards: Card[], suit: Suit): Card[] => + cards.filter( + card => + card.suit === suit || + card.name === CardName.JOKER || + card.name === CardName.ACE_HEARTS, ) +export const bestCardLead = (round: Round) => { + if (!round.suit) return false + let trumpCards = getTrumpCards(Object.values(CARDS), round.suit) + // Remove played trump cards round.completedHands.forEach(hand => { hand.playedCards.forEach(p => { @@ -228,11 +238,11 @@ export const getBestCard = (cards: Card[], round: Round) => { return cards[0] } -export const pickBestCards = ( - cards: T[], +export const pickBestCards = ( + cards: Card[], suit: Suit, numPlayers: number, -): T[] => { +): Card[] => { // If number of players not in range 2-5 then throw an error if (numPlayers < 2 || numPlayers > 5) { throw new Error("Number of players must be between 2 and 5") @@ -253,11 +263,3 @@ export const pickBestCards = ( return bestCards } - -export const getTrumpCards = (cards: T[], suit: Suit): T[] => - cards.filter( - card => - card.suit === suit || - card.name === CardName.JOKER || - card.name === CardName.ACE_HEARTS, - ) From 1896bc6afcea1274b127dfbf7796d7f91ae002d7 Mon Sep 17 00:00:00 2001 From: Daithi Hearn Date: Sun, 1 Oct 2023 19:41:44 +0200 Subject: [PATCH 2/2] chore: upgrading dependencies --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index ebfa5c3..1a0ba28 100644 --- a/package.json +++ b/package.json @@ -15,12 +15,12 @@ "@emotion/styled": "11.11.0", "@mui/icons-material": "5.14.11", "@mui/material": "5.14.11", - "@mui/x-data-grid": "6.15.0", + "@mui/x-data-grid": "6.16.0", "@popperjs/core": "2.11.8", "@reduxjs/toolkit": "1.9.6", "@types/jest": "29.5.5", - "@types/node": "20.7.1", - "@types/react": "18.2.23", + "@types/node": "20.8.0", + "@types/react": "18.2.24", "@types/react-avatar-editor": "13", "@types/react-dom": "18.2.8", "@types/react-router-dom": "5.3.3", @@ -40,7 +40,7 @@ "react-chartjs-2": "5", "react-confetti": "6.1.0", "react-dom": "18.2.0", - "react-redux": "8.1.2", + "react-redux": "8.1.3", "react-router": "6.16.0", "react-router-config": "5.1.1", "react-router-dom": "6.16.0",