diff --git a/config.json b/config.json index 930b0edb7..d1b611a36 100644 --- a/config.json +++ b/config.json @@ -1573,6 +1573,15 @@ "prerequisites": [], "difficulty": 4, "topics": ["arrays", "conditionals", "filtering", "games"] + }, + { + "slug": "poker", + "name": "Poker", + "uuid": "56f78d43-4715-4e0b-80e9-b257b966db12", + "practices": [], + "prerequisites": [], + "difficulty": 4, + "topics": ["arrays", "conditionals", "filtering", "games"] } ] }, diff --git a/exercises/practice/poker/.docs/instructions.md b/exercises/practice/poker/.docs/instructions.md new file mode 100644 index 000000000..107cd49d6 --- /dev/null +++ b/exercises/practice/poker/.docs/instructions.md @@ -0,0 +1,7 @@ +# Instructions + +Pick the best hand(s) from a list of poker hands. + +See [Wikipedia][poker-hands] for an overview of poker hands. + +[poker-hands]: https://en.wikipedia.org/wiki/List_of_poker_hands diff --git a/exercises/practice/poker/.meta/config.json b/exercises/practice/poker/.meta/config.json new file mode 100644 index 000000000..7127c19b9 --- /dev/null +++ b/exercises/practice/poker/.meta/config.json @@ -0,0 +1,27 @@ +{ + "authors": [ + "thibault2705" + ], + "files": { + "solution": [ + "poker.ts" + ], + "test": [ + "poker.test.ts" + ], + "example": [ + ".meta/proof.ci.ts" + ] + }, + "blurb": "Pick the best hand(s) from a list of poker hands.", + "source": "Inspired by the training course from Udacity.", + "source_url": "https://www.udacity.com/course/design-of-computer-programs--cs212", + "custom": { + "version.tests.compatibility": "jest-29", + "flag.tests.task-per-describe": false, + "flag.tests.may-run-long": false, + "flag.tests.includes-optional": false, + "flag.tests.jest": true, + "flag.tests.tstyche": false + } +} diff --git a/exercises/practice/poker/.meta/proof.ci.ts b/exercises/practice/poker/.meta/proof.ci.ts new file mode 100644 index 000000000..ee4102b52 --- /dev/null +++ b/exercises/practice/poker/.meta/proof.ci.ts @@ -0,0 +1,234 @@ +export function bestHands(hands: string[]): string[] { + const parsed = hands.map((hand) => ({ + original: hand, + score: evaluateHand(hand), + })) + + let best = parsed[0] + + for (const current of parsed.slice(1)) { + if (compareScores(current.score, best.score) > 0) { + best = current + } + } + + return parsed + .filter((hand) => compareScores(hand.score, best.score) === 0) + .map((hand) => hand.original) +} + +type Card = { + rank: number + suit: string +} + +type HandScore = { + category: number + tiebreakers: number[] +} + +function evaluateHand(hand: string): HandScore { + const cards = parseHand(hand) + const ranks = cards.map((c) => c.rank) + const sortedRanksDesc = [...ranks].sort((a, b) => b - a) + + const flush = isFlush(cards) + const straightHigh = getStraightHigh(ranks) + + const counts = getRankCounts(ranks) + const groups = Object.entries(counts) + .map(([rank, count]) => ({ + rank: Number(rank), + count, + })) + .sort((a, b) => { + if (b.count !== a.count) return b.count - a.count + return b.rank - a.rank + }) + + // Straight flush + if (flush && straightHigh !== null) { + return { + category: 8, + tiebreakers: [straightHigh], + } + } + + // Four of a kind + if (groups[0].count === 4) { + const quadRank = groups[0].rank + const kicker = groups[1].rank + return { + category: 7, + tiebreakers: [quadRank, kicker], + } + } + + // Full house + if (groups[0].count === 3 && groups[1].count === 2) { + return { + category: 6, + tiebreakers: [groups[0].rank, groups[1].rank], + } + } + + // Flush + if (flush) { + return { + category: 5, + tiebreakers: sortedRanksDesc, + } + } + + // Straight + if (straightHigh !== null) { + return { + category: 4, + tiebreakers: [straightHigh], + } + } + + // Three of a kind + if (groups[0].count === 3) { + const tripRank = groups[0].rank + const kickers = groups + .filter((g) => g.count === 1) + .map((g) => g.rank) + .sort((a, b) => b - a) + + return { + category: 3, + tiebreakers: [tripRank, ...kickers], + } + } + + // Two pair + if (groups[0].count === 2 && groups[1].count === 2) { + const pairRanks = groups + .filter((g) => g.count === 2) + .map((g) => g.rank) + .sort((a, b) => b - a) + + const kicker = groups.find((g) => g.count === 1)!.rank + + return { + category: 2, + tiebreakers: [...pairRanks, kicker], + } + } + + // One pair + if (groups[0].count === 2) { + const pairRank = groups[0].rank + const kickers = groups + .filter((g) => g.count === 1) + .map((g) => g.rank) + .sort((a, b) => b - a) + + return { + category: 1, + tiebreakers: [pairRank, ...kickers], + } + } + + // High card + return { + category: 0, + tiebreakers: sortedRanksDesc, + } +} + +function parseHand(hand: string): Card[] { + return hand.split(' ').map(parseCard) +} + +function parseCard(card: string): Card { + const suit = card.slice(-1) + const rankText = card.slice(0, -1) + + return { + rank: parseRank(rankText), + suit, + } +} + +function parseRank(rank: string): number { + switch (rank) { + case 'J': + return 11 + case 'Q': + return 12 + case 'K': + return 13 + case 'A': + return 14 + default: + return Number(rank) + } +} + +function isFlush(cards: Card[]): boolean { + return cards.every((card) => card.suit === cards[0].suit) +} + +function getRankCounts(ranks: number[]): Record { + const counts: Record = {} + + for (const rank of ranks) { + counts[rank] = (counts[rank] ?? 0) + 1 + } + + return counts +} + +function getStraightHigh(ranks: number[]): number | null { + const unique = [...new Set(ranks)].sort((a, b) => a - b) + + if (unique.length !== 5) { + return null + } + + // Normal straight + let isSequential = true + for (let i = 1; i < unique.length; i++) { + if (unique[i] !== unique[i - 1] + 1) { + isSequential = false + break + } + } + + if (isSequential) { + return unique[4] + } + + // Ace-low straight: A,2,3,4,5 + const aceLow = [2, 3, 4, 5, 14] + const isAceLow = + unique.length === aceLow.length && + unique.every((value, index) => value === aceLow[index]) + + if (isAceLow) { + return 5 + } + + return null +} + +function compareScores(a: HandScore, b: HandScore): number { + if (a.category !== b.category) { + return a.category - b.category + } + + const maxLen = Math.max(a.tiebreakers.length, b.tiebreakers.length) + + for (let i = 0; i < maxLen; i++) { + const av = a.tiebreakers[i] ?? 0 + const bv = b.tiebreakers[i] ?? 0 + + if (av !== bv) { + return av - bv + } + } + + return 0 +} diff --git a/exercises/practice/poker/.meta/tests.toml b/exercises/practice/poker/.meta/tests.toml new file mode 100644 index 000000000..2e654ef63 --- /dev/null +++ b/exercises/practice/poker/.meta/tests.toml @@ -0,0 +1,131 @@ +# This is an auto-generated file. +# +# Regenerating this file via `configlet sync` will: +# - Recreate every `description` key/value pair +# - Recreate every `reimplements` key/value pair, where they exist in problem-specifications +# - Remove any `include = true` key/value pair (an omitted `include` key implies inclusion) +# - Preserve any other key/value pair +# +# As user-added comments (using the # character) will be removed when this file +# is regenerated, comments can be added via a `comment` key. + +[161f485e-39c2-4012-84cf-bec0c755b66c] +description = "single hand always wins" + +[370ac23a-a00f-48a9-9965-6f3fb595cf45] +description = "highest card out of all hands wins" + +[d94ad5a7-17df-484b-9932-c64fc26cff52] +description = "a tie has multiple winners" + +[61ed83a9-cfaa-40a5-942a-51f52f0a8725] +description = "multiple hands with the same high cards, tie compares next highest ranked, down to last card" + +[da01becd-f5b0-4342-b7f3-1318191d0580] +description = "winning high card hand also has the lowest card" + +[f7175a89-34ff-44de-b3d7-f6fd97d1fca4] +description = "one pair beats high card" + +[e114fd41-a301-4111-a9e7-5a7f72a76561] +description = "highest pair wins" + +[b3acd3a7-f9fa-4647-85ab-e0a9e07d1365] +description = "both hands have the same pair, high card wins" + +[935bb4dc-a622-4400-97fa-86e7d06b1f76] +description = "two pairs beats one pair" + +[c8aeafe1-6e3d-4711-a6de-5161deca91fd] +description = "both hands have two pairs, highest ranked pair wins" + +[88abe1ba-7ad7-40f3-847e-0a26f8e46a60] +description = "both hands have two pairs, with the same highest ranked pair, tie goes to low pair" + +[15a7a315-0577-47a3-9981-d6cf8e6f387b] +description = "both hands have two identically ranked pairs, tie goes to remaining card (kicker)" + +[f761e21b-2560-4774-a02a-b3e9366a51ce] +description = "both hands have two pairs that add to the same value, win goes to highest pair" + +[fc6277ac-94ac-4078-8d39-9d441bc7a79e] +description = "two pairs first ranked by largest pair" + +[21e9f1e6-2d72-49a1-a930-228e5e0195dc] +description = "three of a kind beats two pair" + +[c2fffd1f-c287-480f-bf2d-9628e63bbcc3] +description = "both hands have three of a kind, tie goes to highest ranked triplet" + +[eb856cc2-481c-4b0d-9835-4d75d07a5d9d] +description = "with multiple decks, two players can have same three of a kind, ties go to highest remaining cards" +include = false + +[26a4a7d4-34a2-4f18-90b4-4a8dd35d2bb1] +description = "with multiple decks, two players can have same three of a kind, ties go to highest remaining cards" +reimplements = "eb856cc2-481c-4b0d-9835-4d75d07a5d9d" + +[a858c5d9-2f28-48e7-9980-b7fa04060a60] +description = "a straight beats three of a kind" + +[73c9c756-e63e-4b01-a88d-0d4491a7a0e3] +description = "aces can end a straight (10 J Q K A)" + +[76856b0d-35cd-49ce-a492-fe5db53abc02] +description = "aces can start a straight (A 2 3 4 5)" + +[e214b7df-dcba-45d3-a2e5-342d8c46c286] +description = "aces cannot be in the middle of a straight (Q K A 2 3)" + +[6980c612-bbff-4914-b17a-b044e4e69ea1] +description = "both hands with a straight, tie goes to highest ranked card" + +[5135675c-c2fc-4e21-9ba3-af77a32e9ba4] +description = "even though an ace is usually high, a 5-high straight is the lowest-scoring straight" + +[c601b5e6-e1df-4ade-b444-b60ce13b2571] +description = "flush beats a straight" + +[4d90261d-251c-49bd-a468-896bf10133de] +description = "both hands have a flush, tie goes to high card, down to the last one if necessary" +include = false + +[e04137c5-c19a-4dfc-97a1-9dfe9baaa2ff] +description = "both hands have a flush, tie goes to high card, down to the last one if necessary" +reimplements = "4d90261d-251c-49bd-a468-896bf10133de" + +[3a19361d-8974-455c-82e5-f7152f5dba7c] +description = "full house beats a flush" + +[eb73d0e6-b66c-4f0f-b8ba-bf96bc0a67f0] +description = "both hands have a full house, tie goes to highest-ranked triplet" + +[34b51168-1e43-4c0d-9b32-e356159b4d5d] +description = "with multiple decks, both hands have a full house with the same triplet, tie goes to the pair" + +[d61e9e99-883b-4f99-b021-18f0ae50c5f4] +description = "four of a kind beats a full house" + +[2e1c8c63-e0cb-4214-a01b-91954490d2fe] +description = "both hands have four of a kind, tie goes to high quad" + +[892ca75d-5474-495d-9f64-a6ce2dcdb7e1] +description = "with multiple decks, both hands with identical four of a kind, tie determined by kicker" + +[923bd910-dc7b-4f7d-a330-8b42ec10a3ac] +description = "straight flush beats four of a kind" + +[d9629e22-c943-460b-a951-2134d1b43346] +description = "aces can end a straight flush (10 J Q K A)" + +[05d5ede9-64a5-4678-b8ae-cf4c595dc824] +description = "aces can start a straight flush (A 2 3 4 5)" + +[ad655466-6d04-49e8-a50c-0043c3ac18ff] +description = "aces cannot be in the middle of a straight flush (Q K A 2 3)" + +[d0927f70-5aec-43db-aed8-1cbd1b6ee9ad] +description = "both hands have a straight flush, tie goes to highest-ranked card" + +[be620e09-0397-497b-ac37-d1d7a4464cfc] +description = "even though an ace is usually high, a 5-high straight flush is the lowest-scoring straight flush" diff --git a/exercises/practice/poker/.vscode/extensions.json b/exercises/practice/poker/.vscode/extensions.json new file mode 100644 index 000000000..daaa5ee2e --- /dev/null +++ b/exercises/practice/poker/.vscode/extensions.json @@ -0,0 +1,7 @@ +{ + "recommendations": [ + "arcanis.vscode-zipfs", + "dbaeumer.vscode-eslint", + "esbenp.prettier-vscode" + ] +} diff --git a/exercises/practice/poker/.vscode/settings.json b/exercises/practice/poker/.vscode/settings.json new file mode 100644 index 000000000..761fb422a --- /dev/null +++ b/exercises/practice/poker/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "cSpell.words": ["exercism"], + "search.exclude": { + "**/.yarn": true, + "**/.pnp.*": true + } +} diff --git a/exercises/practice/poker/.yarnrc.yml b/exercises/practice/poker/.yarnrc.yml new file mode 100644 index 000000000..23e4a6d3d --- /dev/null +++ b/exercises/practice/poker/.yarnrc.yml @@ -0,0 +1,3 @@ +compressionLevel: mixed + +enableGlobalCache: true diff --git a/exercises/practice/poker/babel.config.cjs b/exercises/practice/poker/babel.config.cjs new file mode 100644 index 000000000..164552797 --- /dev/null +++ b/exercises/practice/poker/babel.config.cjs @@ -0,0 +1,5 @@ +module.exports = { + // eslint-disable-next-line @typescript-eslint/no-require-imports + presets: [[require('@exercism/babel-preset-typescript'), { corejs: '3.38' }]], + plugins: [], +} diff --git a/exercises/practice/poker/eslint.config.mjs b/exercises/practice/poker/eslint.config.mjs new file mode 100644 index 000000000..1be39c53f --- /dev/null +++ b/exercises/practice/poker/eslint.config.mjs @@ -0,0 +1,26 @@ +// @ts-check + +import tsEslint from 'typescript-eslint' +import config from '@exercism/eslint-config-typescript' +import maintainersConfig from '@exercism/eslint-config-typescript/maintainers.mjs' + +export default [ + ...tsEslint.config(...config, { + files: ['.meta/proof.ci.ts', '.meta/exemplar.ts', '*.test.ts'], + extends: maintainersConfig, + }), + { + ignores: [ + // # Protected or generated + '.git/**/*', + '.vscode/**/*', + + //# When using npm + 'node_modules/**/*', + + // # Configuration files + 'babel.config.cjs', + 'jest.config.cjs', + ], + }, +] diff --git a/exercises/practice/poker/jest.config.cjs b/exercises/practice/poker/jest.config.cjs new file mode 100644 index 000000000..0aba1a59e --- /dev/null +++ b/exercises/practice/poker/jest.config.cjs @@ -0,0 +1,22 @@ +module.exports = { + verbose: true, + projects: [''], + testMatch: [ + '**/__tests__/**/*.[jt]s?(x)', + '**/test/**/*.[jt]s?(x)', + '**/?(*.)+(spec|test).[jt]s?(x)', + ], + testPathIgnorePatterns: [ + '/(?:production_)?node_modules/', + '.d.ts$', + '/test/fixtures', + '/test/helpers', + '__mocks__', + ], + transform: { + '^.+\\.[jt]sx?$': 'babel-jest', + }, + moduleNameMapper: { + '^(\\.\\/.+)\\.js$': '$1', + }, +} diff --git a/exercises/practice/poker/package.json b/exercises/practice/poker/package.json new file mode 100644 index 000000000..636a51edd --- /dev/null +++ b/exercises/practice/poker/package.json @@ -0,0 +1,38 @@ +{ + "name": "@exercism/typescript-poker", + "version": "1.0.0", + "description": "Exercism exercises in Typescript.", + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/exercism/typescript" + }, + "type": "module", + "engines": { + "node": "^18.16.0 || >=20.0.0" + }, + "devDependencies": { + "@exercism/babel-preset-typescript": "^0.6.0", + "@exercism/eslint-config-typescript": "^0.8.0", + "@jest/globals": "^29.7.0", + "@types/node": "~22.7.6", + "babel-jest": "^29.7.0", + "core-js": "~3.38.1", + "eslint": "^9.12.0", + "expect": "^29.7.0", + "jest": "^29.7.0", + "prettier": "^3.3.3", + "tstyche": "^2.1.1", + "typescript": "~5.6.3", + "typescript-eslint": "^8.10.0" + }, + "scripts": { + "test": "corepack yarn node test-runner.mjs", + "test:types": "corepack yarn tstyche", + "test:implementation": "corepack yarn jest --no-cache --passWithNoTests", + "lint": "corepack yarn lint:types && corepack yarn lint:ci", + "lint:types": "corepack yarn tsc --noEmit -p .", + "lint:ci": "corepack yarn eslint . --ext .tsx,.ts" + }, + "packageManager": "yarn@4.5.1" +} diff --git a/exercises/practice/poker/poker.test.ts b/exercises/practice/poker/poker.test.ts new file mode 100644 index 000000000..05a2d7257 --- /dev/null +++ b/exercises/practice/poker/poker.test.ts @@ -0,0 +1,235 @@ +import { bestHands } from './poker' +import { describe, expect, it, xit } from '@jest/globals' + +describe('Poker', () => { + it('single hand always wins', () => { + expect(bestHands(['4S 5S 7H 8D JC'])).toEqual(['4S 5S 7H 8D JC']) + }) + + xit('highest card out of all hands wins', () => { + expect( + bestHands(['4D 5S 6S 8D 3C', '2S 4C 7S 9H 10H', '3S 4S 5D 6H JH']) + ).toEqual(['3S 4S 5D 6H JH']) + }) + + xit('a tie has multiple winners', () => { + expect( + bestHands([ + '4D 5S 6S 8D 3C', + '2S 4C 7S 9H 10H', + '3S 4S 5D 6H JH', + '3H 4H 5C 6C JD', + ]) + ).toEqual(['3S 4S 5D 6H JH', '3H 4H 5C 6C JD']) + }) + + xit('multiple hands with the same high cards, tie compares next highest ranked, down to last card', () => { + expect(bestHands(['3S 5H 6S 8D 7H', '2S 5D 6D 8C 7S'])).toEqual([ + '3S 5H 6S 8D 7H', + ]) + }) + + xit('winning high card hand also has the lowest card', () => { + expect(bestHands(['2S 5H 6S 8D 7H', '3S 4D 6D 8C 7S'])).toEqual([ + '2S 5H 6S 8D 7H', + ]) + }) + + xit('one pair beats high card', () => { + expect(bestHands(['4S 5H 6C 8D KH', '2S 4H 6S 4D JH'])).toEqual([ + '2S 4H 6S 4D JH', + ]) + }) + + xit('highest pair wins', () => { + expect(bestHands(['4S 2H 6S 2D JH', '2S 4H 6C 4D JD'])).toEqual([ + '2S 4H 6C 4D JD', + ]) + }) + + xit('both hands have the same pair, high card wins', () => { + expect(bestHands(['4H 4S AH JC 3D', '4C 4D AS 5D 6C'])).toEqual([ + '4H 4S AH JC 3D', + ]) + }) + + xit('two pairs beats one pair', () => { + expect(bestHands(['2S 8H 6S 8D JH', '4S 5H 4C 8C 5C'])).toEqual([ + '4S 5H 4C 8C 5C', + ]) + }) + + xit('both hands have two pairs, highest ranked pair wins', () => { + expect(bestHands(['2S 8H 2D 8D 3H', '4S 5H 4C 8S 5D'])).toEqual([ + '2S 8H 2D 8D 3H', + ]) + }) + + xit('both hands have two pairs, with the same highest ranked pair, tie goes to low pair', () => { + expect(bestHands(['2S QS 2C QD JH', 'JD QH JS 8D QC'])).toEqual([ + 'JD QH JS 8D QC', + ]) + }) + + xit('both hands have two identically ranked pairs, tie goes to remaining card (kicker)', () => { + expect(bestHands(['JD QH JS 8D QC', 'JS QS JC 2D QD'])).toEqual([ + 'JD QH JS 8D QC', + ]) + }) + + xit('both hands have two pairs that add to the same value, win goes to highest pair', () => { + expect(bestHands(['6S 6H 3S 3H AS', '7H 7S 2H 2S AC'])).toEqual([ + '7H 7S 2H 2S AC', + ]) + }) + + xit('two pairs first ranked by largest pair', () => { + expect(bestHands(['5C 2S 5S 4H 4C', '6S 2S 6H 7C 2C'])).toEqual([ + '6S 2S 6H 7C 2C', + ]) + }) + + xit('three of a kind beats two pair', () => { + expect(bestHands(['2S 8H 2H 8D JH', '4S 5H 4C 8S 4H'])).toEqual([ + '4S 5H 4C 8S 4H', + ]) + }) + + xit('both hands have three of a kind, tie goes to highest ranked triplet', () => { + expect(bestHands(['2S 2H 2C 8D JH', '4S AH AS 8C AD'])).toEqual([ + '4S AH AS 8C AD', + ]) + }) + + xit('with multiple decks, two players can have same three of a kind, ties go to highest remaining cards', () => { + expect(bestHands(['5S AH AS 7C AD', '4S AH AS 8C AD'])).toEqual([ + '4S AH AS 8C AD', + ]) + }) + + xit('a straight beats three of a kind', () => { + expect(bestHands(['4S 5H 4C 8D 4H', '3S 4D 2S 6D 5C'])).toEqual([ + '3S 4D 2S 6D 5C', + ]) + }) + + xit('aces can end a straight (10 J Q K A)', () => { + expect(bestHands(['4S 5H 4C 8D 4H', '10D JH QS KD AC'])).toEqual([ + '10D JH QS KD AC', + ]) + }) + + xit('aces can start a straight (A 2 3 4 5)', () => { + expect(bestHands(['4S 5H 4C 8D 4H', '4D AH 3S 2D 5C'])).toEqual([ + '4D AH 3S 2D 5C', + ]) + }) + + xit('aces cannot be in the middle of a straight (Q K A 2 3)', () => { + expect(bestHands(['2C 3D 7H 5H 2S', 'QS KH AC 2D 3S'])).toEqual([ + '2C 3D 7H 5H 2S', + ]) + }) + + xit('both hands with a straight, tie goes to highest ranked card', () => { + expect(bestHands(['4S 6C 7S 8D 5H', '5S 7H 8S 9D 6H'])).toEqual([ + '5S 7H 8S 9D 6H', + ]) + }) + + xit('even though an ace is usually high, a 5-high straight is the lowest-scoring straight', () => { + expect(bestHands(['2H 3C 4D 5D 6H', '4S AH 3S 2D 5H'])).toEqual([ + '2H 3C 4D 5D 6H', + ]) + }) + + xit('flush beats a straight', () => { + expect(bestHands(['4C 6H 7D 8D 5H', '2S 4S 5S 6S 7S'])).toEqual([ + '2S 4S 5S 6S 7S', + ]) + }) + + xit('both hands have a flush, tie goes to high card, down to the last one if necessary', () => { + expect(bestHands(['4H 7H 8H 9H 6H', '2S 4S 5S 6S 7S'])).toEqual([ + '4H 7H 8H 9H 6H', + ]) + }) + + xit('both hands have a flush, tie goes to high card, down to the last one if necessary', () => { + expect(bestHands(['2H 7H 8H 9H 6H', '3S 5S 6S 7S 8S'])).toEqual([ + '2H 7H 8H 9H 6H', + ]) + }) + + xit('full house beats a flush', () => { + expect(bestHands(['3H 6H 7H 8H 5H', '4S 5H 4C 5D 4H'])).toEqual([ + '4S 5H 4C 5D 4H', + ]) + }) + + xit('both hands have a full house, tie goes to highest-ranked triplet', () => { + expect(bestHands(['4H 4S 4D 9S 9D', '5H 5S 5D 8S 8D'])).toEqual([ + '5H 5S 5D 8S 8D', + ]) + }) + + xit('with multiple decks, both hands have a full house with the same triplet, tie goes to the pair', () => { + expect(bestHands(['5H 5S 5D 9S 9D', '5H 5S 5D 8S 8D'])).toEqual([ + '5H 5S 5D 9S 9D', + ]) + }) + + xit('four of a kind beats a full house', () => { + expect(bestHands(['4S 5H 4D 5D 4H', '3S 3H 2S 3D 3C'])).toEqual([ + '3S 3H 2S 3D 3C', + ]) + }) + + xit('both hands have four of a kind, tie goes to high quad', () => { + expect(bestHands(['2S 2H 2C 8D 2D', '4S 5H 5S 5D 5C'])).toEqual([ + '4S 5H 5S 5D 5C', + ]) + }) + + xit('with multiple decks, both hands with identical four of a kind, tie determined by kicker', () => { + expect(bestHands(['3S 3H 2S 3D 3C', '3S 3H 4S 3D 3C'])).toEqual([ + '3S 3H 4S 3D 3C', + ]) + }) + + xit('straight flush beats four of a kind', () => { + expect(bestHands(['4S 5H 5S 5D 5C', '7S 8S 9S 6S 10S'])).toEqual([ + '7S 8S 9S 6S 10S', + ]) + }) + + xit('aces can end a straight flush (10 J Q K A)', () => { + expect(bestHands(['KC AH AS AD AC', '10C JC QC KC AC'])).toEqual([ + '10C JC QC KC AC', + ]) + }) + + xit('aces can start a straight flush (A 2 3 4 5)', () => { + expect(bestHands(['KS AH AS AD AC', '4H AH 3H 2H 5H'])).toEqual([ + '4H AH 3H 2H 5H', + ]) + }) + + xit('aces cannot be in the middle of a straight flush (Q K A 2 3)', () => { + expect(bestHands(['2C AC QC 10C KC', 'QH KH AH 2H 3H'])).toEqual([ + '2C AC QC 10C KC', + ]) + }) + + xit('both hands have a straight flush, tie goes to highest-ranked card', () => { + expect(bestHands(['4H 6H 7H 8H 5H', '5S 7S 8S 9S 6S'])).toEqual([ + '5S 7S 8S 9S 6S', + ]) + }) + + xit('even though an ace is usually high, a 5-high straight flush is the lowest-scoring straight flush', () => { + expect(bestHands(['2H 3H 4H 5H 6H', '4D AD 3D 2D 5D'])).toEqual([ + '2H 3H 4H 5H 6H', + ]) + }) +}) diff --git a/exercises/practice/poker/poker.ts b/exercises/practice/poker/poker.ts new file mode 100644 index 000000000..3be6f8b12 --- /dev/null +++ b/exercises/practice/poker/poker.ts @@ -0,0 +1,3 @@ +export function bestHands(hands: string[]): string[] { + throw new Error('Remove this line and implement the function') +} diff --git a/exercises/practice/poker/test-runner.mjs b/exercises/practice/poker/test-runner.mjs new file mode 100644 index 000000000..44b205fc2 --- /dev/null +++ b/exercises/practice/poker/test-runner.mjs @@ -0,0 +1,111 @@ +#!/usr/bin/env node + +/** + * 👋🏽 Hello there reader, + * + * It looks like you are working on this solution using the Exercism CLI and + * not the online editor. That's great! The file you are looking at executes + * the various steps the online test-runner also takes. + * + * @see https://github.com/exercism/typescript-test-runner + * + * TypeScript track exercises generally consist of at least two out of three + * types of tests to run. + * + * 1. tsc, the TypeScript compiler. This tests if the TypeScript code is valid + * 2. tstyche, static analysis tests to see if the types used are expected + * 3. jest, runtime implementation tests to see if the solution is correct + * + * If one of these three fails, this script terminates with -1, -2, or -3 + * respectively. If it succeeds, it terminates with exit code 0. + * + * @note you need corepack (bundled with node LTS) enabled in order for this + * test runner to work as expected. Follow the installation and test + * instructions if you see errors about corepack or pnp. + */ + +import { execSync } from 'node:child_process' +import { existsSync, readFileSync } from 'node:fs' +import { exit } from 'node:process' +import { URL } from 'node:url' + +/** + * Before executing any tests, the test runner attempts to find the + * exercise config.json file which has metadata about which types of tests + * to run for this solution. + */ +const metaDirectory = new URL('./.meta/', import.meta.url) +const exercismDirectory = new URL('./.exercism/', import.meta.url) +const configDirectory = existsSync(metaDirectory) + ? metaDirectory + : existsSync(exercismDirectory) + ? exercismDirectory + : null + +if (configDirectory === null) { + throw new Error( + 'Expected .meta or .exercism directory to exist, but I cannot find it.' + ) +} + +const configFile = new URL('./config.json', configDirectory) +if (!existsSync(configFile)) { + throw new Error('Expected config.json to exist at ' + configFile.toString()) +} + +// Experimental: import config from './config.json' with { type: 'json' } +/** @type {import('./config.json') } */ +const config = JSON.parse(readFileSync(configFile)) + +const jest = !config.custom || config.custom['flag.tests.jest'] +const tstyche = config.custom?.['flag.tests.tstyche'] +console.log( + `[tests] tsc: ✅, tstyche: ${tstyche ? '✅' : '❌'}, jest: ${jest ? '✅' : '❌'}, ` +) + +/** + * 1. tsc: the typescript compiler + */ +try { + console.log('[tests] tsc (compile)') + execSync('corepack yarn lint:types', { + stdio: 'inherit', + cwd: process.cwd(), + }) +} catch { + exit(-1) +} + +/** + * 2. tstyche: type tests + */ +if (tstyche) { + try { + console.log('[tests] tstyche (type tests)') + execSync('corepack yarn test:types', { + stdio: 'inherit', + cwd: process.cwd(), + }) + } catch { + exit(-2) + } +} + +/** + * 3. jest: implementation tests + */ +if (jest) { + try { + console.log('[tests] tstyche (implementation tests)') + execSync('corepack yarn test:implementation', { + stdio: 'inherit', + cwd: process.cwd(), + }) + } catch { + exit(-3) + } +} + +/** + * Done! 🥳 + */ diff --git a/exercises/practice/poker/tsconfig.json b/exercises/practice/poker/tsconfig.json new file mode 100644 index 000000000..574616245 --- /dev/null +++ b/exercises/practice/poker/tsconfig.json @@ -0,0 +1,38 @@ +{ + "display": "Configuration for Exercism TypeScript Exercises", + "compilerOptions": { + // Allows you to use the newest syntax, and have access to console.log + // https://www.typescriptlang.org/tsconfig#lib + "lib": ["ES2020", "dom"], + // Make sure typescript is configured to output ESM + // https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c#how-can-i-make-my-typescript-project-output-esm + "module": "Node16", + // Since this project is using babel, TypeScript may target something very + // high, and babel will make sure it runs on your local Node version. + // https://babeljs.io/docs/en/ + "target": "ES2020", // ESLint doesn't support this yet: "es2022", + + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + + // Because jest-resolve isn't like node resolve, the absolute path must be .ts + "allowImportingTsExtensions": true, + "noEmit": true, + + // Because we'll be using babel: ensure that Babel can safely transpile + // files in the TypeScript project. + // + // https://babeljs.io/docs/en/babel-plugin-transform-typescript/#caveats + "isolatedModules": true + }, + "include": [ + "*.ts", + "*.tsx", + ".meta/*.ts", + ".meta/*.tsx", + "__typetests__/*.tst.ts" + ], + "exclude": ["node_modules"] +} diff --git a/exercises/practice/poker/yarn.lock b/exercises/practice/poker/yarn.lock new file mode 100644 index 000000000..e69de29bb