From adbcc8249f5198920a3af0c7f3f603af4039fa4b Mon Sep 17 00:00:00 2001 From: Daithi Hearn Date: Tue, 3 Oct 2023 00:39:56 +0200 Subject: [PATCH 1/4] fix: sonar code smell --- src/caches/GameSlice.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/caches/GameSlice.ts b/src/caches/GameSlice.ts index 7c473c6..c164980 100644 --- a/src/caches/GameSlice.ts +++ b/src/caches/GameSlice.ts @@ -95,7 +95,7 @@ export const getIsDoublesGame = createSelector( players => players.length === 6, ) -export const getMaxCall = createSelector(getGame, game => game.maxCall || 0) +export const getMaxCall = createSelector(getGame, game => game.maxCall ?? 0) export const getIsMyGo = createSelector(getGame, game => game.isMyGo) export const getIamGoer = createSelector(getGame, game => game.iamGoer) export const getIamDealer = createSelector(getGame, game => game.iamDealer) From 43dbb0c0db92b5830f0ff18d28ac68e56812cc1c Mon Sep 17 00:00:00 2001 From: Daithi Hearn Date: Tue, 3 Oct 2023 06:33:17 +0200 Subject: [PATCH 2/4] fix: min cards to keep logic --- src/utils/GameUtils.spec.ts | 16 +++++++++++++--- src/utils/GameUtils.ts | 6 ++---- 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/utils/GameUtils.spec.ts b/src/utils/GameUtils.spec.ts index 19c100a..703985f 100644 --- a/src/utils/GameUtils.spec.ts +++ b/src/utils/GameUtils.spec.ts @@ -495,10 +495,10 @@ describe("GameUtils", () => { expect(calculateMinCardsToKeep(3)).toBe(0) }) it("4 players", () => { - expect(calculateMinCardsToKeep(4)).toBe(1) + expect(calculateMinCardsToKeep(4)).toBe(0) }) it("5 players", () => { - expect(calculateMinCardsToKeep(5)).toBe(2) + expect(calculateMinCardsToKeep(5)).toBe(1) }) it("6 players", () => { expect(calculateMinCardsToKeep(6)).toBe(2) @@ -521,10 +521,20 @@ describe("GameUtils", () => { ]) }) it("Must keep 1", () => { - expect(pickBestCards([...HAND1], Suit.DIAMONDS, 4)).toStrictEqual([ + expect(pickBestCards([...HAND1], Suit.DIAMONDS, 5)).toStrictEqual([ CARDS.SIX_HEARTS, ]) }) + it("Must keep 0", () => { + expect(pickBestCards([...HAND1], Suit.DIAMONDS, 4)).toStrictEqual( + [], + ) + }) + it("Must keep 0", () => { + expect(pickBestCards([...HAND1], Suit.DIAMONDS, 3)).toStrictEqual( + [], + ) + }) it("Must keep 0", () => { expect(pickBestCards([...HAND1], Suit.DIAMONDS, 2)).toStrictEqual( [], diff --git a/src/utils/GameUtils.ts b/src/utils/GameUtils.ts index 6969f8a..7f83cfb 100644 --- a/src/utils/GameUtils.ts +++ b/src/utils/GameUtils.ts @@ -246,13 +246,11 @@ export const getBestCard = (cards: Card[], round: Round) => { export const calculateMinCardsToKeep = (numPlayers: number): number => { switch (numPlayers) { case 2: - return 0 case 3: - return 0 case 4: - return 1 + return 0 case 5: - return 2 + return 1 case 6: return 2 default: From 87d42132b5ba6786e70805e7825c5afe685e623a Mon Sep 17 00:00:00 2001 From: Daithi Hearn Date: Tue, 3 Oct 2023 06:43:24 +0200 Subject: [PATCH 3/4] test: cleaning up ci pipeline --- .github/workflows/unit-tests.yml | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/unit-tests.yml b/.github/workflows/unit-tests.yml index 59cd4b4..56c5f16 100644 --- a/.github/workflows/unit-tests.yml +++ b/.github/workflows/unit-tests.yml @@ -6,7 +6,7 @@ on: branches: ["main"] jobs: - test: + unit-tests-and-coverage: runs-on: ubuntu-latest steps: @@ -17,14 +17,12 @@ jobs: uses: actions/setup-node@v2 with: node-version: "16.19.x" - - name: Install dependencies run: yarn install --frozen-lockfile - - name: Test and coverage run: yarn jest --coverage - name: SonarCloud Scan uses: SonarSource/sonarcloud-github-action@master env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} From 442a5ae3b414c56abdcbf81923c5f3da2dff7c99 Mon Sep 17 00:00:00 2001 From: Daithi Hearn Date: Tue, 3 Oct 2023 08:09:20 +0200 Subject: [PATCH 4/4] fix: renege logic --- src/utils/GameUtils.spec.ts | 191 ++++++++++++++++++++++++++++++++---- src/utils/GameUtils.ts | 75 ++++++++++---- 2 files changed, 228 insertions(+), 38 deletions(-) diff --git a/src/utils/GameUtils.spec.ts b/src/utils/GameUtils.spec.ts index 703985f..47d2eb6 100644 --- a/src/utils/GameUtils.spec.ts +++ b/src/utils/GameUtils.spec.ts @@ -3,9 +3,11 @@ import { areAllTrumpCards, bestCardLead, calculateMinCardsToKeep, + canRenege, compareCards, containsATrumpCard, getBestCard, + getColdCards, getTrumpCards, getWorstCard, padMyHand, @@ -48,14 +50,44 @@ const ROUND_BEST_LEAD: Round = { dealerId: "", status: RoundStatus.PLAYING, dealerSeeingCall: false, - completedHands: [], + completedHands: [ + { + leadOut: CardName.FIVE_DIAMONDS, + timestamp: "", + currentPlayerId: "", + playedCards: [{ card: CardName.ACE_HEARTS, playerId: "blah" }], + }, + ], +} + +const ROUND_WILD_LEAD: Round = { + suit: Suit.HEARTS, + currentHand: { + leadOut: CardName.JOKER, + timestamp: "", + currentPlayerId: "", + playedCards: [], + }, + timestamp: "", + number: 0, + dealerId: "", + status: RoundStatus.PLAYING, + dealerSeeingCall: false, + completedHands: [ + { + leadOut: CardName.FIVE_DIAMONDS, + timestamp: "", + currentPlayerId: "", + playedCards: [{ card: CardName.ACE_HEARTS, playerId: "blah" }], + }, + ], } const HAND1: Card[] = [ { ...CARDS.TWO_HEARTS, selected: true }, CARDS.THREE_HEARTS, CARDS.FOUR_HEARTS, - CARDS.FIVE_HEARTS, + CARDS.JACK_HEARTS, CARDS.SIX_HEARTS, ] @@ -77,6 +109,13 @@ const HAND4: Card[] = [ CARDS.TWO_DIAMONDS, ] +const HAND_RENEG: Card[] = [ + CARDS.THREE_DIAMONDS, + CARDS.TWO_CLUBS, + CARDS.THREE_CLUBS, + CARDS.FIVE_HEARTS, +] + describe("GameUtils", () => { describe("removeEmptyCards", () => { it("empty hand", () => { @@ -140,6 +179,12 @@ describe("GameUtils", () => { expect(compareCards([], [])).toBe(true) }) + it("different length hands", () => { + expect(compareCards([...HAND1], [...HAND1, CARDS.ACE_CLUBS])).toBe( + false, + ) + }) + it("equal hands", () => { expect(compareCards([...HAND1], [...HAND1])).toBe(true) }) @@ -339,7 +384,7 @@ describe("GameUtils", () => { ), ).toStrictEqual([ CARDS.THREE_HEARTS, - CARDS.FIVE_HEARTS, + CARDS.JACK_HEARTS, CARDS.SIX_HEARTS, ]) }) @@ -352,7 +397,7 @@ describe("GameUtils", () => { ), ).toStrictEqual([ CARDS.THREE_HEARTS, - CARDS.FIVE_HEARTS, + CARDS.JACK_HEARTS, CARDS.SIX_HEARTS, ]) }) @@ -428,6 +473,34 @@ describe("GameUtils", () => { }) }) + describe("getColdCards", () => { + it("empty hand", () => { + expect(getColdCards([], Suit.HEARTS)).toStrictEqual([]) + }) + + it("all cold cards", () => { + expect(getColdCards([...HAND1], Suit.HEARTS)).toStrictEqual([]) + }) + + it("no cold cards", () => { + expect(getColdCards([...HAND3], Suit.HEARTS)).toStrictEqual(HAND3) + }) + + it("all cold cards with joker and ace of hearts", () => { + expect(getColdCards([...HAND4], Suit.HEARTS)).toStrictEqual([ + CARDS.THREE_CLUBS, + CARDS.TWO_DIAMONDS, + ]) + }) + + it("some cold cards", () => { + expect(getColdCards([...HAND3], Suit.SPADES)).toStrictEqual([ + CARDS.THREE_DIAMONDS, + CARDS.TWO_CLUBS, + ]) + }) + }) + describe("bestCardLead", () => { it("no suit", () => { expect(bestCardLead({ ...ROUND, suit: undefined })).toBe(false) @@ -446,11 +519,11 @@ describe("GameUtils", () => { describe("getBestCard", () => { it("empty hand", () => { - expect(getBestCard([], ROUND)).toBe(undefined) + expect(() => getBestCard([], ROUND)).toThrow() }) it("trump card", () => { - expect(getBestCard([...HAND1], ROUND)).toStrictEqual( - CARDS.FIVE_HEARTS, + expect(getBestCard([...HAND1], ROUND_BEST_LEAD)).toStrictEqual( + CARDS.JACK_HEARTS, ) }) it("follow cold card", () => { @@ -460,21 +533,99 @@ describe("GameUtils", () => { }) }) - describe("getWorstCard", () => { - it("empty hand", () => { - expect(getBestCard([], ROUND)).toBe(undefined) + describe("canRenege", () => { + it("not a trump card", () => { + expect(() => + canRenege(CARDS.TWO_CLUBS, CARDS.TWO_HEARTS, Suit.HEARTS), + ).toThrow() }) - it("trump card", () => { - expect(getWorstCard([...HAND1], ROUND)).toStrictEqual({ - ...CARDS.TWO_HEARTS, - selected: true, - }) + it("not a trump card", () => { + expect(() => + canRenege(CARDS.TWO_HEARTS, CARDS.TWO_CLUBS, Suit.HEARTS), + ).toThrow() }) - it("follow cold card", () => { - expect(getWorstCard([...HAND3], ROUND)).toStrictEqual( - CARDS.TWO_CLUBS, + it("not a renegable card", () => { + expect( + canRenege(CARDS.THREE_HEARTS, CARDS.TWO_HEARTS, Suit.HEARTS), + ).toBe(false) + }) + it("renegable card", () => { + expect( + canRenege(CARDS.ACE_HEARTS, CARDS.TWO_HEARTS, Suit.HEARTS), + ).toBe(true) + }) + it("renegable card", () => { + expect(canRenege(CARDS.JOKER, CARDS.TWO_HEARTS, Suit.HEARTS)).toBe( + true, + ) + }) + it("renegable card", () => { + expect( + canRenege(CARDS.JACK_HEARTS, CARDS.TWO_HEARTS, Suit.HEARTS), + ).toBe(true) + }) + it("renegable card", () => { + expect( + canRenege(CARDS.FIVE_HEARTS, CARDS.TWO_HEARTS, Suit.HEARTS), + ).toBe(true) + }) + it("renegable card lower than card lead out", () => { + expect(canRenege(CARDS.ACE_HEARTS, CARDS.JOKER, Suit.HEARTS)).toBe( + false, ) }) + it("renegable card lower than card lead out", () => { + expect( + canRenege(CARDS.ACE_HEARTS, CARDS.JACK_HEARTS, Suit.HEARTS), + ).toBe(false) + }) + it("renegable card lower than card lead out", () => { + expect( + canRenege(CARDS.ACE_HEARTS, CARDS.FIVE_HEARTS, Suit.HEARTS), + ).toBe(false) + }) + it("renegable card lower than card lead out", () => { + expect(canRenege(CARDS.JOKER, CARDS.JACK_HEARTS, Suit.HEARTS)).toBe( + false, + ) + }) + it("renegable card lower than card lead out", () => { + expect(canRenege(CARDS.JOKER, CARDS.FIVE_HEARTS, Suit.HEARTS)).toBe( + false, + ) + }) + it("renegable card lower than card lead out", () => { + expect( + canRenege(CARDS.JACK_HEARTS, CARDS.FIVE_HEARTS, Suit.HEARTS), + ).toBe(false) + }) + }) + + describe("getWorstCard", () => { + // it("empty hand", () => { + // expect(() => getBestCard([], ROUND)).toThrow() + // }) + // it("trump card", () => { + // expect(getWorstCard([...HAND1], ROUND)).toStrictEqual({ + // ...CARDS.TWO_HEARTS, + // selected: true, + // }) + // }) + // it("follow cold card", () => { + // expect(getWorstCard([...HAND3], ROUND)).toStrictEqual( + // CARDS.TWO_CLUBS, + // ) + // }) + // it("wild cards lead", () => { + // expect(getWorstCard([...HAND4], ROUND_WILD_LEAD)).toStrictEqual( + // CARDS.TWO_HEARTS, + // ) + // }) + it("Can reneg five", () => { + expect( + getWorstCard([...HAND_RENEG], ROUND_WILD_LEAD), + ).toStrictEqual(CARDS.THREE_DIAMONDS) + }) }) describe("calculateMinCardsToKeep", () => { @@ -516,13 +667,13 @@ describe("GameUtils", () => { }) it("Must keep 2", () => { expect(pickBestCards([...HAND1], Suit.DIAMONDS, 6)).toStrictEqual([ + CARDS.JACK_HEARTS, CARDS.SIX_HEARTS, - CARDS.FIVE_HEARTS, ]) }) it("Must keep 1", () => { expect(pickBestCards([...HAND1], Suit.DIAMONDS, 5)).toStrictEqual([ - CARDS.SIX_HEARTS, + CARDS.JACK_HEARTS, ]) }) it("Must keep 0", () => { diff --git a/src/utils/GameUtils.ts b/src/utils/GameUtils.ts index 7f83cfb..79ea9fb 100644 --- a/src/utils/GameUtils.ts +++ b/src/utils/GameUtils.ts @@ -5,14 +5,7 @@ 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, -) => { - if (!Array.isArray(hand1) || !Array.isArray(hand2)) { - return false - } - +export const compareCards = (hand1: Card[], hand2: Card[]) => { let h1 = removeEmptyCards(hand1) let h2 = removeEmptyCards(hand2) @@ -149,12 +142,10 @@ export const riskOfMistakeBuyingCards = ( } export const getTrumpCards = (cards: Card[], suit: Suit): Card[] => - cards.filter( - card => - card.suit === suit || - card.name === CardName.JOKER || - card.name === CardName.ACE_HEARTS, - ) + cards.filter(card => card.suit === suit || card.suit === Suit.WILD) + +export const getColdCards = (cards: Card[], suit: Suit): Card[] => + cards.filter(card => card.suit !== suit && card.suit !== Suit.WILD) export const bestCardLead = (round: Round) => { if (!round.suit) return false @@ -179,15 +170,63 @@ export const bestCardLead = (round: Round) => { return round.currentHand.leadOut === trumpCards[0].name } +export const canRenege = (myCard: Card, cardLead: Card, suit: Suit) => { + // If the card lead isn't a trump card then throw an error + if (cardLead.suit !== suit && cardLead.suit !== Suit.WILD) { + throw new Error("Card lead must be a trump card") + } + + // If my card isn't a trump card then throw an error + if (myCard.suit !== suit && myCard.suit !== Suit.WILD) { + throw new Error("My card must be a trump card") + } + + // If my card has a value greater than 112 and greater than the card lead then you can renege + return myCard.value >= 112 && myCard.value > cardLead.value +} + export const getWorstCard = (cards: Card[], round: Round) => { - if (cards.length === 0) return undefined + if (cards.length === 0) throw new Error("No cards to choose from") // Check if must follow suit + const roundSuit = round.suit + if (!roundSuit) throw new Error("Round suit cannot be undefined") const leadOut = round.currentHand?.leadOut let suitLead = leadOut ? CARDS[leadOut]?.suit : undefined + if (suitLead === Suit.WILD) suitLead = roundSuit + + const myTrumpCards = getTrumpCards(cards, roundSuit).sort( + (a, b) => a.value - b.value, + ) + const myColdCards = getColdCards(cards, roundSuit).sort( + (a, b) => a.coldValue - b.coldValue, + ) + + // If no card lead play the worst card + if (!leadOut) { + // If we have cold cards then play the worst one + if (myColdCards.length > 0) { + return myColdCards[0] + } + // Otherwise play the worst trump card + else if (myTrumpCards.length > 0) { + return myTrumpCards[0] + } else throw new Error("No cards to choose from") + } - if (suitLead === Suit.WILD) { - suitLead = round.suit + // Handle when suit lead is the trump suit + if (suitLead === roundSuit) { + // Get trump cards that aren't renegable + const notRenegableTrumpCards = myTrumpCards.filter( + c => !canRenege(c, CARDS[leadOut], roundSuit), + ) + if (notRenegableTrumpCards.length > 0) { + return notRenegableTrumpCards[0] + } else if (myColdCards.length > 0) { + return myColdCards[0] + } else if (myTrumpCards.length > 0) { + return myTrumpCards[0] + } else throw new Error("No cards to choose from") } if (suitLead) { @@ -206,7 +245,7 @@ export const getWorstCard = (cards: Card[], round: Round) => { } export const getBestCard = (cards: Card[], round: Round) => { - if (cards.length === 0) return undefined + if (cards.length === 0) throw new Error("No cards to choose from") // Check for trump cards const myTrumpCards = cards.filter(