Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
}
]
},
Expand Down
7 changes: 7 additions & 0 deletions exercises/practice/poker/.docs/instructions.md
Original file line number Diff line number Diff line change
@@ -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
29 changes: 29 additions & 0 deletions exercises/practice/poker/.meta/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
{
"authors": [
"thibault2705"
],
"contributors": [
],
"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.",
"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
},
"source": "Inspired by the training course from Udacity.",
"source_url": "https://www.udacity.com/course/design-of-computer-programs--cs212"
}
234 changes: 234 additions & 0 deletions exercises/practice/poker/.meta/proof.ci.ts
Original file line number Diff line number Diff line change
@@ -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<number, number> {
const counts: Record<number, number> = {}

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
}
Loading
Loading