Skip to content

Commit

Permalink
Add Poker exercise (#559)
Browse files Browse the repository at this point in the history
  • Loading branch information
ryanplusplus authored Nov 18, 2023
1 parent 1393cca commit 623d31e
Show file tree
Hide file tree
Showing 8 changed files with 790 additions and 0 deletions.
8 changes: 8 additions & 0 deletions config.json
Original file line number Diff line number Diff line change
Expand Up @@ -941,6 +941,14 @@
"prerequisites": [],
"difficulty": 5
},
{
"slug": "poker",
"name": "Poker",
"uuid": "5eee3bb6-bcb8-4056-a854-805ecc4dd903",
"practices": [],
"prerequisites": [],
"difficulty": 5
},
{
"slug": "connect",
"name": "Connect",
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
17 changes: 17 additions & 0 deletions exercises/practice/poker/.meta/config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"authors": ["ryanplusplus"],
"files": {
"solution": [
"src/poker.cr"
],
"test": [
"spec/poker_spec.cr"
],
"example": [
".meta/src/example.cr"
]
},
"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"
}
198 changes: 198 additions & 0 deletions exercises/practice/poker/.meta/src/example.cr
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
module Poker
def self.best_hands(hands)
hands = hands.map { |hand| Hand.new(hand) }
max_score = hands.map(&.score).max
hands.select { |hand| hand.score == max_score }.map(&.to_s)
end
end

class Poker::Hand
@cards : Array(Card)
@s : String

def initialize(@s)
@cards = @s.split.map { |card| Card.new(card) }
end

def to_s
@s
end

def score
straight_flush ||
straight_flush_ace_low ||
four_of_a_kind ||
full_house ||
flush ||
straight ||
straight_ace_low ||
three_of_a_kind ||
two_pair ||
pair ||
high_card
end

private def partitioned_by_rank
@cards.group_by { |card| card.rank }.values
end

private def partitioned_by_suit
@cards.group_by { |card| card.suit }.values
end

private def sorted_by_rank(cards = @cards)
cards.sort_by { |card| card.rank }.reverse
end

private def sorted_by_rank_ace_low
@cards.sort_by { |card| card.rank_ace_low }.reverse
end

private def base_score(type)
{
:high_card => 1,
:pair => 2,
:two_pair => 3,
:three_of_a_kind => 4,
:straight => 5,
:flush => 6,
:full_house => 7,
:four_of_a_kind => 8,
:straight_flush => 9,
}[type]
end

private def fractional_score(cards, rank_type = :rank)
ranks = rank_type == :rank_ace_low ? cards.map(&.rank_ace_low) : cards.map(&.rank)
ranks += [0, 0, 0, 0, 0]
[
ranks[0] * 0.01,
ranks[1] * 0.00_01,
ranks[2] * 0.00_00_01,
ranks[3] * 0.00_00_00_01,
ranks[4] * 0.00_00_00_00_01,
].sum
end

private def high_card
base_score(:high_card) + fractional_score(sorted_by_rank)
end

private def pair
pairs = partitioned_by_rank.select { |p| p.size == 2 }

if pairs.size == 1
everything_else = partitioned_by_rank.select { |p| p.size == 1 }.flatten
base_score(:pair) + fractional_score([pairs.first.first, *sorted_by_rank(everything_else)])
end
end

private def two_pair
pairs = partitioned_by_rank.select { |p| p.size == 2 }

if pairs.size == 2
everything_else = partitioned_by_rank.select { |p| p.size == 1 }.flatten
pair_ranks = pairs.flatten.map(&.rank)
base_score(:two_pair) + fractional_score([
pairs.flatten.max_by { |c| c.rank },
pairs.flatten.min_by { |c| c.rank },
*sorted_by_rank(everything_else),
])
end
end

private def three_of_a_kind
triples = partitioned_by_rank.select { |p| p.size == 3 }

if triples.size == 1
everything_else = partitioned_by_rank.select { |p| p.size == 1 }.flatten
base_score(:three_of_a_kind) + fractional_score([triples.first.first, *sorted_by_rank(everything_else)])
end
end

private def straight
cards = sorted_by_rank

if cards.each_cons(2).all? { |pair| pair[1].rank + 1 == pair[0].rank }
return base_score(:straight) + fractional_score([cards.last])
end
end

private def straight_ace_low
cards = sorted_by_rank_ace_low

if cards.each_cons(2).all? { |pair| pair[1].rank_ace_low + 1 == pair[0].rank_ace_low }
base_score(:straight) + fractional_score([cards.last], :rank_ace_low)
end
end

private def flush
cards = partitioned_by_suit

if cards.size == 1
base_score(:flush) + fractional_score([*sorted_by_rank(cards.first)])
end
end

private def full_house
triples = partitioned_by_rank.select { |p| p.size == 3 }
pairs = partitioned_by_rank.select { |p| p.size == 2 }

if pairs.size == 1 && triples.size == 1
base_score(:full_house) + fractional_score([triples.first.first, pairs.first.first])
end
end

private def four_of_a_kind
quads = partitioned_by_rank.select { |p| p.size == 4 }

if quads.size == 1
everything_else = partitioned_by_rank.select { |p| p.size == 1 }
base_score(:four_of_a_kind) + fractional_score([quads.first.first, *everything_else.flatten])
end
end

private def straight_flush
if straight && flush
base_score(:straight_flush) + fractional_score([sorted_by_rank.last])
end
end

private def straight_flush_ace_low
if straight_ace_low && flush
base_score(:straight_flush) + fractional_score([sorted_by_rank_ace_low.last], :rank_ace_low)
end
end
end

class Poker::Hand::Card
getter suit : Char, rank : Int32, rank_ace_low : Int32

def initialize(s)
@rank = parse_rank(s)
@rank_ace_low = parse_rank_ace_low(s)
@suit = parse_suit(s)
end

private def parse_rank(s)
{
'J' => 11,
'Q' => 12,
'K' => 13,
'A' => 14,
}[s[0]]? || s[0...-1].to_i
end

private def parse_rank_ace_low(s)
{
'J' => 11,
'Q' => 12,
'K' => 13,
'A' => 1,
}[s[0]]? || s[0...-1].to_i
end

private def parse_suit(s)
s[-1]
end
end
12 changes: 12 additions & 0 deletions exercises/practice/poker/.meta/test_template.ecr
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
require "spec"
require "../src/*"

describe <%= to_capitalized(@json["exercise"].to_s) %> do
<% @json["cases"].as_a.each do |cases| %>
<%= status()%> "<%-= cases["description"] %>" do
hands = <%= cases["input"]["hands"].inspect.gsub("[", "[\n").gsub("\", \"", "\",\n\"") %>
expected = <%= cases["expected"].inspect.gsub("[", "[\n").gsub("\", \"", "\",\n\"") %>
<%= to_capitalized(@json["exercise"].to_s) %>.<%= cases["property"].to_s.underscore %>(hands).should eq(expected)
end
<% end %>
end
131 changes: 131 additions & 0 deletions exercises/practice/poker/.meta/tests.toml
Original file line number Diff line number Diff line change
@@ -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"
Loading

0 comments on commit 623d31e

Please sign in to comment.