rora 0.0.6 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. data/README.md +59 -15
  2. data/Rakefile +0 -1
  3. data/lib/rora.rb +4 -0
  4. data/lib/rora/{hands.csv → 5-card-hands.csv} +0 -0
  5. data/lib/rora/7-card-hands.csv +323765 -0
  6. data/lib/rora/cards.csv +52 -0
  7. data/lib/rora/flushes.csv +40 -0
  8. data/lib/rora/model/board.rb +2 -2
  9. data/lib/rora/model/card.rb +17 -14
  10. data/lib/rora/model/deck.rb +23 -0
  11. data/lib/rora/model/equity.rb +18 -0
  12. data/lib/rora/model/hand.rb +12 -27
  13. data/lib/rora/model/hand_type.rb +1 -1
  14. data/lib/rora/model/pot.rb +1 -1
  15. data/lib/rora/model/rank.rb +34 -16
  16. data/lib/rora/model/starting_hand.rb +22 -18
  17. data/lib/rora/model/suit.rb +26 -11
  18. data/lib/rora/model/table.rb +0 -3
  19. data/lib/rora/repository/card_repository.rb +17 -0
  20. data/lib/rora/repository/hand_repository.rb +60 -20
  21. data/lib/rora/repository/starting_hand_repository.rb +2 -1
  22. data/lib/rora/utils/equity_calculator.rb +47 -0
  23. data/lib/rora/utils/hand_ranking_generator.rb +135 -0
  24. data/test/rora/model/board_test.rb +19 -20
  25. data/test/rora/model/card_test.rb +45 -15
  26. data/test/rora/model/deck_test.rb +24 -0
  27. data/test/rora/model/hand_test.rb +5 -29
  28. data/test/rora/model/hand_type_test.rb +3 -3
  29. data/test/rora/model/pot_test.rb +2 -2
  30. data/test/rora/model/rank_test.rb +10 -2
  31. data/test/rora/model/seat_test.rb +1 -1
  32. data/test/rora/model/starting_hand_test.rb +8 -16
  33. data/test/rora/model/suit_test.rb +3 -3
  34. data/test/rora/model/table_test.rb +8 -8
  35. data/test/rora/repository/hand_repository_test.rb +14 -16
  36. data/test/rora/utils/equity_calculator_test.rb +53 -0
  37. data/test/rora_test.rb +1 -1
  38. metadata +13 -6
  39. data/test/rora/model/game_test.rb +0 -23
@@ -5,12 +5,15 @@
5
5
  # of cards has four suits: hearts, clubs, spades, and diamonds.
6
6
  #
7
7
  class Suit
8
- attr_reader :id, :key, :value
9
-
10
- def initialize(id, key, value)
8
+ include Comparable
9
+
10
+ attr_reader :id, :key, :value, :order
11
+
12
+ def initialize(id, key, value, order)
11
13
  @id = id
12
14
  @key = key
13
15
  @value = value
16
+ @order = order
14
17
  end
15
18
 
16
19
  def self.values
@@ -21,23 +24,35 @@ class Suit
21
24
  self.values.each do |suit|
22
25
  return suit if suit.key.casecmp(key) == 0
23
26
  end
24
- raise ArgumentError, "No suit exists for key " + key
27
+ raise ArgumentError, "No suit exists for key '#{key}'"
25
28
  end
26
29
 
27
- def to_s
28
- "Suit: #{@value}s"
30
+ def <=>(suit)
31
+ self.order <=> suit.order
32
+ end
33
+
34
+ def eql? suit
35
+ self == suit
29
36
  end
30
37
 
31
38
  def == suit
32
- self.id == suit.id
39
+ self.key == suit.key
40
+ end
41
+
42
+ def hash
43
+ return self.key.ord
44
+ end
45
+
46
+ def to_s
47
+ "#{@value}s"
33
48
  end
34
49
 
35
50
  class << self
36
51
  private :new
37
52
  end
38
53
 
39
- HEART = new(43, "H", "Heart")
40
- SPADE = new(47, "S", "Spade")
41
- CLUB = new(53, "C", "Club")
42
- DIAMOND = new(59, "D", "Diamond")
54
+ HEART = new(2, "H", "Heart", 1)
55
+ DIAMOND = new(3, "D", "Diamond", 2)
56
+ SPADE = new(5, "S", "Spade", 3)
57
+ CLUB = new(7, "C", "Club", 4)
43
58
  end
@@ -36,17 +36,14 @@ class Table
36
36
  end
37
37
 
38
38
  def the_small_blind
39
- raise RuntimeError, "There are fewer than two players at the table" if players.size < 2
40
39
  players.size == 2 ? the_button : the_seat_after(the_button)
41
40
  end
42
41
 
43
42
  def the_big_blind
44
- raise RuntimeError, "There are fewer than two players at the table" if players.size < 2
45
43
  the_seat_after the_small_blind
46
44
  end
47
45
 
48
46
  def under_the_gun
49
- raise RuntimeError, "There are fewer than two players at the table" if players.size < 2
50
47
  the_seat_after(the_big_blind)
51
48
  end
52
49
 
@@ -0,0 +1,17 @@
1
+ require 'csv'
2
+ require 'singleton'
3
+
4
+ class CardRepository
5
+ include Singleton
6
+
7
+ def initialize
8
+ @card_table = Hash.new
9
+ CSV.foreach("lib/rora/cards.csv") do |row|
10
+ @card_table[row[0]] = row[1].to_i
11
+ end
12
+ end
13
+
14
+ def get(key)
15
+ @card_table.fetch(key)
16
+ end
17
+ end
@@ -4,16 +4,43 @@ require 'singleton'
4
4
  class HandRepository
5
5
  include Singleton
6
6
 
7
+ HEART_FLUSH=32
8
+ DIAMOND_FLUSH=243
9
+ SPADE_FLUSH=3125
10
+ CLUB_FLUSH=16807
11
+
7
12
  def initialize
8
13
  @hands = Array.new
9
- @table = Hash.new
10
- CSV.foreach("lib/rora/hands.csv") do |row|
11
- @table[row[1].to_i] = [row[0].to_i, row[3], row[4], row[5].to_f]
14
+
15
+ @five_card_table = Hash.new
16
+ CSV.foreach("lib/rora/5-card-hands.csv") do |row|
17
+ @five_card_table[row[1].to_i] = [row[0].to_i, row[3], row[4]]
18
+ end
19
+
20
+ @seven_card_table = Hash.new
21
+ CSV.foreach("lib/rora/7-card-hands.csv") do |row|
22
+ @seven_card_table[row[1].to_i] = [row[0].to_i, row[4], row[5]]
23
+ end
24
+
25
+ @flushes_table = Hash.new
26
+ CSV.foreach("lib/rora/flushes.csv") do |row|
27
+ @flushes_table[row[0].to_i] = [row[1].to_i, row[2]]
12
28
  end
13
29
  end
14
30
 
15
- def find id
16
- @table.fetch id
31
+ def evaluate_5_card_hand(cards)
32
+ flush = Hand.new(cards).flush?
33
+ @five_card_table.fetch(cards.inject(1) {|product, card| product * card.rank.id} * (flush ? 67 : 1))
34
+ end
35
+
36
+ def evaluate_7_card_hand(cards)
37
+ key = generate_suit_key(cards)
38
+ flush = has_flush?(key)
39
+ flush ? get_best_hand(cards, key) : @seven_card_table.fetch(cards[0].rank.id * cards[1].rank.id * cards[2].rank.id * cards[3].rank.id * cards[4].rank.id * cards[5].rank.id * cards[6].rank.id)
40
+ end
41
+
42
+ def has_flush?(key)
43
+ key % HEART_FLUSH == 0 || key % DIAMOND_FLUSH == 0 || key % SPADE_FLUSH == 0 || key % CLUB_FLUSH == 0
17
44
  end
18
45
 
19
46
  # Returns all possible poker hands.
@@ -58,29 +85,28 @@ class HandRepository
58
85
  # Starting Hand, Board and Deck
59
86
  # This method will return all poker hands that can be made with the given starting
60
87
  # hand and cards that remain in the deck.
61
- def list arguments=nil
62
- return hands if arguments.nil? || arguments[:starting_hand].nil?
88
+ def list(arguments=nil)
89
+ return all_hands if arguments.nil? || arguments[:starting_hand].nil?
63
90
 
64
91
  starting_hand = arguments[:starting_hand]
65
92
  deck = arguments[:deck].nil? ? Deck.new : arguments[:deck]
66
93
  board = arguments[:board]
67
- hands = Array.new
94
+ spec_hands = Array.new
68
95
 
69
96
  if !board.nil?
70
97
  raise RuntimeError if board.contains_any? starting_hand.cards
71
98
  if board.cards.size == 5
72
- (board.cards + starting_hand.cards).combination(5).to_a.each { |cards| hands << Hand.new(cards) }
73
- return hands
99
+ (board.cards + starting_hand.cards).combination(5).to_a.each { |cards| spec_hands << Hand.new(cards) }
74
100
  else
75
101
  deck.remove(starting_hand).remove(board).combination(5 - board.cards.size).to_a.each do |cards|
76
- (starting_hand.cards + board.cards + cards).combination(5).to_a.each { |cds| hands << Hand.new(cds) }
102
+ (starting_hand.cards + board.cards + cards).combination(5).to_a.each { |cds| spec_hands << Hand.new(cds) }
77
103
  end
78
- return hands
79
104
  end
105
+ return spec_hands
80
106
  end
81
107
 
82
- deck.remove(starting_hand).combination(3).each { |cards| hands << Hand.new(cards + starting_hand.cards) }
83
- hands
108
+ deck.remove(starting_hand).combination(3).each { |cards| spec_hands << Hand.new(cards + starting_hand.cards) }
109
+ spec_hands
84
110
  end
85
111
 
86
112
  # Returns unique poker hands.
@@ -110,20 +136,34 @@ class HandRepository
110
136
  #
111
137
  # This method carries the same semantics as the list method, but returns unique hands instead of
112
138
  # every possible hand.
113
- def list_unique arguments=nil
114
- results = list arguments
115
- return results if results.nil? || results.size == 0
139
+ def list_and_group_by_hand_score(arguments=nil)
140
+ spec_hands = list(arguments)
141
+ return spec_hands if spec_hands.nil? || spec_hands.size == 0
116
142
 
117
143
  hash = Hash.new
118
- results.each do |result|
119
- hash[result.hash_key] = result
144
+ spec_hands.each do |hand|
145
+ hash[(hand.cards.inject(1) {|product, card| product * card.rank.id } * (hand.flush? ? 67 : 1))] = hand
120
146
  end
121
147
  hash.values
122
148
  end
123
149
 
124
150
  private
125
151
 
126
- def hands
152
+ def get_best_hand(cards, key)
153
+ flush = @flushes_table.fetch(key)
154
+ if(flush[0] != 5)
155
+ return @seven_card_table.fetch(cards[0].uid * cards[1].uid * cards[2].uid * cards[3].uid * cards[4].uid * cards[5].uid * cards[6].uid)
156
+ end
157
+ @five_card_table.fetch(cards.inject(1) {|product, card| product * ((card.suit.key == flush[1]) ? card.rank.id : 1)} * 67)
158
+ end
159
+
160
+ def generate_suit_key(cards)
161
+ key = 1
162
+ cards.each {|card| key = key * card.suit.id}
163
+ key
164
+ end
165
+
166
+ def all_hands
127
167
  if @hands.empty?
128
168
  Deck.new.combination(5).each {|cards| @hands << Hand.new(cards)}
129
169
  end
@@ -8,7 +8,8 @@ class StartingHandRepository
8
8
  Deck.new.cards.combination(2) do |combination|
9
9
  starting_hand = StartingHand.new(combination)
10
10
  @all << starting_hand
11
- @distinct[starting_hand.key] = starting_hand
11
+ hash_key = starting_hand.cards.inject(1) {|product, card| product * card.rank.id } * (starting_hand.suited? ? 67 : 1)
12
+ @distinct[hash_key] = starting_hand
12
13
  end
13
14
  end
14
15
 
@@ -0,0 +1,47 @@
1
+ require 'date'
2
+
3
+ class EquityCalculator
4
+
5
+ def initialize
6
+ @hand_repository = HandRepository.instance
7
+ @best_scores = Hash.new
8
+ end
9
+
10
+ def calculate_equity(starting_hands, board = Board.new)
11
+ raise ArgumentError, "Must have at least two starting hands for equity comparison" if (starting_hands.nil? || starting_hands.size < 2)
12
+ raise ArgumentError, "There are duplicate cards" if duplicates?(starting_hands, board)
13
+
14
+ equity_map = Hash[starting_hands.map{|starting_hand| [starting_hand, 0]}]
15
+ game = Deck.new.remove(merge(starting_hands, board)).combination(5 - board.cards.size)
16
+
17
+ game.each do |cards|
18
+ showdown(equity_map, starting_hands, board.cards + cards)
19
+ end
20
+
21
+ total_winning_hands = equity_map.inject(0) {|total, result| total += result[1]}
22
+ equity_map.inject(Hash.new) { |hash, result| hash[result[0]] = Equity.new(game.size, total_winning_hands, result); hash}
23
+ end
24
+
25
+ private
26
+
27
+ def showdown(results, starting_hands, board_cards)
28
+ @best_scores.clear
29
+
30
+ starting_hands.each do |starting_hand|
31
+ @best_scores[starting_hand] = @hand_repository.evaluate_7_card_hand((board_cards + starting_hand.cards))[0]
32
+ end
33
+
34
+ winner = @best_scores.min_by{|key,value| value}
35
+ results[winner[0]] += 1 if(@best_scores.select {|k,v| v == winner[1]}.size == 1)
36
+ end
37
+
38
+ def duplicates?(starting_hands, board)
39
+ cards = merge(starting_hands, board)
40
+ cards.uniq.length != cards.length
41
+ end
42
+
43
+ def merge(starting_hands, board)
44
+ starting_hands.inject(board.cards) {|cards, starting_hand| cards.concat(starting_hand.cards)}
45
+ end
46
+
47
+ end
@@ -0,0 +1,135 @@
1
+ require 'date'
2
+
3
+ class HandRankingGenerator
4
+
5
+ HEART_FLUSH=32
6
+ DIAMOND_FLUSH=243
7
+ SPADE_FLUSH=3125
8
+ CLUB_FLUSH=16807
9
+
10
+ def initialize
11
+ @total = 133784560
12
+ @count = 0
13
+ @scores = {}
14
+ end
15
+
16
+ def generate_flush_map
17
+ file = File.new("flushes.csv", "w")
18
+
19
+ Suit.values().each do |suit_1|
20
+ Suit.values().each do |suit_2|
21
+ Suit.values().each do |suit_3|
22
+ Suit.values().each do |suit_4|
23
+ Suit.values().each do |suit_5|
24
+ Suit.values().each do |suit_6|
25
+ Suit.values().each do |suit_7|
26
+ key = suit_1.id * suit_2.id * suit_3.id * suit_4.id * suit_5.id * suit_6.id * suit_7.id
27
+ if(!@scores.has_key?(key))
28
+ @scores[key] = key
29
+ suit = flush_suit(key)
30
+ if(!suit.nil?)
31
+ count = flush_count([suit_1, suit_2, suit_3, suit_4, suit_5, suit_6, suit_7], suit)
32
+ file.write("#{key},#{count},#{suit}\n")
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ file.close
44
+ end
45
+
46
+ def generate_non_flush_hand_rankings
47
+ file = File.new("7_card_non_flush_hands.csv", "w")
48
+ cds = Deck.new.cards
49
+
50
+ (0..45).each do |index_1|
51
+ ((index_1 + 1)..46).each do |index_2|
52
+ ((index_2 + 1)..47).each do |index_3|
53
+ ((index_3 + 1)..48).each do |index_4|
54
+ ((index_4 + 1)..49).each do |index_5|
55
+ ((index_5 + 1)..50).each do |index_6|
56
+ ((index_6 + 1)..51).each do |index_7|
57
+ cards = [cds[index_1], cds[index_2], cds[index_3], cds[index_4], cds[index_5], cds[index_6], cds[index_7]].sort
58
+ hand = get_best_hand(cards)
59
+ key = get_key(cards, hand)
60
+ if(!@scores.has_key?(key) && !hand.flush?)
61
+ @scores[key] = hand.score
62
+ file.write("#{hand.score},#{key}\n")
63
+ end
64
+ @count = @count + 1
65
+ if(@count % 1000 == 0)
66
+ puts "#{DateTime.now.strftime('%Y-%m-%d %H:%M:%S')} :: #{@count} records processed #{sprintf("%05.3f", (Float(@count)/Float(@total)) * 100.0)}% complete, #{@scores.size} records captured"
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+
76
+ file.close
77
+ end
78
+
79
+ def generate_flush_hand_rankings
80
+ file = File.new("7_card_flush_hands.csv", "w")
81
+ Suit.values().each do |suit|
82
+ generate_hand_rankings(suit, 6, 1, file)
83
+ generate_hand_rankings(suit, 7, 0, file)
84
+ end
85
+ file.close
86
+ end
87
+
88
+ private
89
+
90
+ def generate_hand_rankings(suit, i, j, file)
91
+ Deck.new.retain_all(suit).combination(i).each do |suited|
92
+ Deck.new.remove_all(suit).combination(j).each do |remainder|
93
+ cards = suited + remainder
94
+ hand = get_best_hand(cards)
95
+ key = get_key(cards, hand)
96
+ if(!@scores.has_key?(key))
97
+ @scores[key] = hand.score
98
+ file.write("#{hand.score},#{key}")
99
+ end
100
+ @count = @count + 1
101
+ if(@count % 1000 == 0)
102
+ puts "#{DateTime.now.strftime('%Y-%m-%d %H:%M:%S')} :: #{@count} records processed, #{@scores.size} records captured"
103
+ end
104
+ end
105
+ end
106
+ end
107
+
108
+ def flush_count(suits, suit)
109
+ count = 0
110
+ suits.each do |s|
111
+ count = count + 1 if s.key == suit
112
+ end
113
+ count
114
+ end
115
+
116
+ def flush_suit(key)
117
+ return "H" if(key % HEART_FLUSH == 0)
118
+ return "D" if(key % DIAMOND_FLUSH == 0)
119
+ return "S" if(key % SPADE_FLUSH == 0)
120
+ return "C" if(key % CLUB_FLUSH == 0)
121
+ end
122
+
123
+ def get_key(cards, hand)
124
+ cards.inject(1) {|product, card| product * (hand.flush? ? card.uid : card.rank.id) }
125
+ end
126
+
127
+ def ranks(cards)
128
+ cards.inject('') { |string, card| string << card.rank.key }
129
+ end
130
+
131
+ def get_best_hand(cards)
132
+ cards.combination(5).to_a.each.collect { |selection| Hand.new(selection) }.sort {|x,y| x.score <=> y.score }[0]
133
+ end
134
+
135
+ end
@@ -7,19 +7,19 @@ class BoardTest < ActiveSupport::TestCase
7
7
  end
8
8
 
9
9
  test "should raise an error when a board is created with less than 3 cards" do
10
- assert_raise ArgumentError do
10
+ assert_raise_message "3 to 5 cards are required to create a board, 2 cards provided", ArgumentError do
11
11
  Board.new "AS,KS"
12
12
  end
13
13
  end
14
14
 
15
15
  test "should raise an error when a board is created with more than 5 cards" do
16
- assert_raise ArgumentError do
16
+ assert_raise_message "3 to 5 cards are required to create a board, 6 cards provided", ArgumentError do
17
17
  Board.new "AS,KS,QS,JS,TS,9S"
18
18
  end
19
19
  end
20
20
 
21
21
  test "should raise an error when a board is created with duplicate cards" do
22
- assert_raise ArgumentError do
22
+ assert_raise_message "The board contains duplicate cards", ArgumentError do
23
23
  Board.new "AS KS JS AS KS"
24
24
  end
25
25
  end
@@ -30,8 +30,6 @@ class BoardTest < ActiveSupport::TestCase
30
30
 
31
31
  test "the board should have three cards if the board was initialized with the flop" do
32
32
  assert_equal 3, Board.new("AS,KS,QS").cards.size
33
- b = Board.new("AS,KS,QS")
34
- puts b.flop
35
33
  end
36
34
 
37
35
  test "the board should have four cards if the board was initialized with the flop and turn" do
@@ -43,20 +41,20 @@ class BoardTest < ActiveSupport::TestCase
43
41
  end
44
42
 
45
43
  test "should raise an error when a flop is not created with 3 cards" do
46
- assert_raise ArgumentError do
47
- Board.new.flop "AS,KS"
44
+ assert_raise_message "3 cards are required on the flop, 2 cards provided", ArgumentError do
45
+ Board.new.flop= "AS,KS"
48
46
  end
49
47
  end
50
48
 
51
49
  test "should raise an error when a flop is set with duplicate cards" do
52
- assert_raise ArgumentError do
53
- Board.new.flop "AS AS JS"
50
+ assert_raise_message "The flop contains duplicate cards", ArgumentError do
51
+ Board.new.flop= "AS AS JS"
54
52
  end
55
53
  end
56
54
 
57
55
  test "should raise an error when a flop is set with no cards" do
58
- assert_raise ArgumentError do
59
- Board.new.flop []
56
+ assert_raise_message "Cannot deal a flop with empty array of cards", ArgumentError do
57
+ Board.new.flop= []
60
58
  end
61
59
  end
62
60
 
@@ -68,14 +66,14 @@ class BoardTest < ActiveSupport::TestCase
68
66
  end
69
67
 
70
68
  test "should raise an error when a turn card is dealt before the flop has been dealt" do
71
- assert_raise RuntimeError do
69
+ assert_raise_message "The flop must be dealt before the turn card is dealt", RuntimeError do
72
70
  board = Board.new
73
71
  board.turn = "AS"
74
72
  end
75
73
  end
76
74
 
77
- test "should raise an error when the turn card has already been dealt" do
78
- assert_raise ArgumentError do
75
+ test "should raise an error when the turn card has already been dealt on the flop" do
76
+ assert_raise_message "The board already contains the Ace of Hearts", ArgumentError do
79
77
  board = Board.new
80
78
  board.flop= "AS,AH,AD"
81
79
  board.turn= "AH"
@@ -91,15 +89,15 @@ class BoardTest < ActiveSupport::TestCase
91
89
  end
92
90
 
93
91
  test "should raise an error when the river card is dealt before the turn card has been dealt" do
94
- assert_raise RuntimeError do
92
+ assert_raise_message "The turn card must be dealt before the river card is dealt", RuntimeError do
95
93
  board = Board.new
96
94
  board.flop = "AS KS QS"
97
95
  board.river = "JS"
98
96
  end
99
97
  end
100
98
 
101
- test "should raise an error when the river card has already been dealt" do
102
- assert_raise ArgumentError do
99
+ test "should raise an error when the river card was dealt on a previous street" do
100
+ assert_raise_message "The board already contains the Ace of Spades", ArgumentError do
103
101
  board = Board.new
104
102
  board.flop = "AS,KS,QS"
105
103
  board.turn = "JS"
@@ -121,9 +119,10 @@ class BoardTest < ActiveSupport::TestCase
121
119
  assert_equal false, @board.contains?(Card.new("3D"))
122
120
  end
123
121
 
124
- test "should return true if the boad does contain the given card" do
125
- assert_equal true, @board.contains?("TS")
126
- assert_equal true, @board.contains?(Card.new("AS"))
122
+ test "should return true if the board contains the given card" do
123
+ b = Board.new "AS,KS,QS,JS,TS"
124
+ assert_equal true, b.contains?("TS")
125
+ assert_equal true, b.contains?(Card.new("AS"))
127
126
  end
128
127
 
129
128
  test "should return false if the board does not contain any of the given cards" do