rora 0.0.6 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
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