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
@@ -0,0 +1,52 @@
1
+ AH,2
2
+ KH,3
3
+ QH,5
4
+ JH,7
5
+ TH,11
6
+ 9H,13
7
+ 8H,17
8
+ 7H,19
9
+ 6H,23
10
+ 5H,29
11
+ 4H,31
12
+ 3H,37
13
+ 2H,41
14
+ AD,43
15
+ KD,47
16
+ QD,53
17
+ JD,59
18
+ TD,61
19
+ 9D,67
20
+ 8D,71
21
+ 7D,73
22
+ 6D,79
23
+ 5D,83
24
+ 4D,89
25
+ 3D,97
26
+ 2D,101
27
+ AS,103
28
+ KS,107
29
+ QS,109
30
+ JS,113
31
+ TS,127
32
+ 9S,131
33
+ 8S,137
34
+ 7S,139
35
+ 6S,149
36
+ 5S,151
37
+ 4S,157
38
+ 3S,163
39
+ 2S,167
40
+ AC,173
41
+ KC,179
42
+ QC,181
43
+ JC,191
44
+ TC,193
45
+ 9C,197
46
+ 8C,199
47
+ 7C,211
48
+ 6C,223
49
+ 5C,227
50
+ 4C,229
51
+ 3C,233
52
+ 2C,239
@@ -0,0 +1,40 @@
1
+ 800,5,H
2
+ 1120,5,H
3
+ 480,5,H
4
+ 1568,5,H
5
+ 672,5,H
6
+ 288,5,H
7
+ 320,6,H
8
+ 448,6,H
9
+ 192,6,H
10
+ 128,7,H
11
+ 12500,5,S
12
+ 28125,5,S
13
+ 43750,5,S
14
+ 18750,5,S
15
+ 153125,5,S
16
+ 65625,5,S
17
+ 31250,6,S
18
+ 109375,6,S
19
+ 46875,6,S
20
+ 78125,7,S
21
+ 151263,5,C
22
+ 67228,5,C
23
+ 252105,5,C
24
+ 168070,5,C
25
+ 100842,5,C
26
+ 420175,5,C
27
+ 235298,6,C
28
+ 588245,6,C
29
+ 352947,6,C
30
+ 823543,7,C
31
+ 972,5,D
32
+ 2430,5,D
33
+ 3402,5,D
34
+ 11907,5,D
35
+ 6075,5,D
36
+ 8505,5,D
37
+ 1458,6,D
38
+ 3645,6,D
39
+ 5103,6,D
40
+ 2187,7,D
@@ -54,14 +54,14 @@ class Board
54
54
  def turn= card
55
55
  cd = card.kind_of?(Card) ? card : Card.new(card)
56
56
  raise RuntimeError, "The flop must be dealt before the turn card is dealt" if @flop.nil?
57
- raise ArgumentError, "The board already contains the #{cd.name}" if cards.include? cd
57
+ raise ArgumentError, "The board already contains the #{cd.value}" if cards.include? cd
58
58
  @turn = cd
59
59
  end
60
60
 
61
61
  def river= card
62
62
  cd = card.kind_of?(Card) ? card : Card.new(card)
63
63
  raise RuntimeError, "The turn card must be dealt before the river card is dealt" if @turn.nil?
64
- raise ArgumentError, "The board already contains the #{cd.name}" if cards.include? cd
64
+ raise ArgumentError, "The board already contains the #{cd.value}" if cards.include? cd
65
65
  @river = cd
66
66
  end
67
67
 
@@ -15,29 +15,31 @@
15
15
  # card = Card.new("kH")
16
16
  #
17
17
  class Card
18
- attr_reader :rank, :suit
18
+ include Comparable
19
19
 
20
+ attr_reader :rank, :suit, :key, :uid
21
+
20
22
  def initialize(*args)
21
23
  if(args.size == 2)
22
24
  @rank = args[0]
23
25
  @suit = args[1]
24
26
  end
25
27
  if(args.size == 1)
26
- raise ArgumentError, "#{args} is an invalid card sequence" if args[0].length != 2
27
- @rank = Rank.get(args[0][0])
28
- @suit = Suit.get(args[0][1])
28
+ raise ArgumentError, "#{args[0]} is an invalid card sequence" if args[0].length != 2
29
+ @rank = Rank.get(args[0][0].chr)
30
+ @suit = Suit.get(args[0][1].chr)
29
31
  end
32
+ @key = @rank.key + @suit.key
33
+ @card_repository = CardRepository.instance
34
+ @uid = @card_repository.get(@key)
30
35
  end
31
36
 
32
- def id
33
- @rank.id * @suit.id
37
+ def <=>(card)
38
+ return self.rank <=> card.rank if card.rank != self.rank
39
+ self.suit <=> card.suit
34
40
  end
35
41
 
36
- def key
37
- @rank.key + @suit.key
38
- end
39
-
40
- def name
42
+ def value
41
43
  "#{@rank.value} of #{@suit.value}s"
42
44
  end
43
45
 
@@ -46,11 +48,12 @@ class Card
46
48
  end
47
49
 
48
50
  def == card
49
- self.id == card.id
51
+ return false if !card.kind_of? Card
52
+ uid == card.uid
50
53
  end
51
54
 
52
55
  def hash
53
- return self.id
56
+ uid
54
57
  end
55
58
 
56
59
  def self.to_cards string
@@ -64,7 +67,7 @@ class Card
64
67
  end
65
68
 
66
69
  def to_s
67
- "Card: #{name}"
70
+ "#{value}"
68
71
  end
69
72
 
70
73
  end
@@ -14,6 +14,29 @@ class Deck
14
14
  @cards << Card.new(rank, suit)
15
15
  end
16
16
  end
17
+ @cards.sort!
18
+ end
19
+
20
+ # Retains all cards with the given suit or rank, removing all others.
21
+ def retain_all argument
22
+ if argument.kind_of? Suit
23
+ @cards = @cards.find_all{|card| card.suit.eql?(argument) }
24
+ end
25
+ if argument.kind_of? Rank
26
+ @cards = @cards.find_all{|card| card.rank.eql?(argument) }
27
+ end
28
+ self
29
+ end
30
+
31
+ # Removes all cards with the given suit or rank, retaining all others.
32
+ def remove_all argument
33
+ if argument.kind_of? Suit
34
+ @cards = @cards.find_all{|card| !card.suit.eql?(argument) }
35
+ end
36
+ if argument.kind_of? Rank
37
+ @cards = @cards.find_all{|card| !card.rank.eql?(argument) }
38
+ end
39
+ self
17
40
  end
18
41
 
19
42
  # Shuffles all cards in the deck.
@@ -0,0 +1,18 @@
1
+ class Equity
2
+
3
+ attr_accessor :starting_hand, :total_hands, :hands_won, :hands_tied, :value
4
+
5
+ def initialize(total_hands_played, total_hands_won, result)
6
+ @starting_hand = result[0]
7
+ @total_hands = total_hands_played
8
+ @hands_won = result[1]
9
+ @hands_tied = total_hands_played - total_hands_won
10
+ total_equity = @hands_won + @hands_tied
11
+ @value = (total_equity == 0 ? 0.00 : total_equity.quo(@total_hands) * 100.00)
12
+ end
13
+
14
+ def to_s
15
+ "starting hand: #{@starting_hand.key} => hands: #{@total_hands}, won: #{@hands_won}, tied: #{@hands_tied}, equity: #{sprintf("%05.3f", @value)}%"
16
+ end
17
+
18
+ end
@@ -11,43 +11,24 @@
11
11
  #
12
12
  class Hand
13
13
  attr_reader :cards
14
-
14
+
15
15
  def initialize cards
16
16
  @hand_repository = HandRepository.instance
17
- @cards = cards.kind_of?(Array) ? cards : Card.to_cards(cards)
17
+ @cards = (cards.kind_of?(Array) ? cards : Card.to_cards(cards)).sort
18
18
  raise ArgumentError, "Exactly 5 cards are required to create a hand, #{cards.size} provided" if @cards.size != 5
19
19
  raise ArgumentError, "The hand contains duplicate cards" if @cards.uniq.length != @cards.length
20
20
  end
21
21
 
22
- # Returns the hand id.
23
- def id
24
- i = 1
25
- @cards.each { |card| i = i * card.id }
26
- i
27
- end
28
-
29
- # Returns the hand hash key.
30
- def hash_key
31
- j = 1
32
- @cards.each { |card| j = j * card.rank.id }
33
- flush? ? (j * 67) : j
34
- end
35
-
36
- # Returns the hand value.
37
- def value
38
- @cards.map { |card| "#{card.key}" }.join(",")
22
+ # Returns the hand key.
23
+ def key
24
+ @cards.map { |card| "#{card.key}" }.join
39
25
  end
40
26
 
41
- # Returns the hand strength, from 1 (strongest) to 7462 (weakest).
27
+ # Returns the hand score, from 1 (strongest) to 7462 (weakest).
42
28
  def score
43
29
  resolve_hand_attribute(0)
44
30
  end
45
31
 
46
- # Returns the probability of this hand being dealt.
47
- def probability
48
- resolve_hand_attribute(3)
49
- end
50
-
51
32
  # Returns the hand name.
52
33
  def name
53
34
  resolve_hand_attribute(2)
@@ -65,7 +46,7 @@ class Hand
65
46
 
66
47
  # Determines if this hand is a flush.
67
48
  def flush?
68
- for i in 0..@cards.size - 2 do
49
+ for i in 0..(@cards.size - 2) do
69
50
  return false if @cards[i].suit != @cards[i+1].suit
70
51
  end
71
52
  true
@@ -116,10 +97,14 @@ class Hand
116
97
  @cards.contains cards
117
98
  end
118
99
 
100
+ def to_s
101
+ "#{@cards.inject('') { |string, card| string << card.key }} (#{name})"
102
+ end
103
+
119
104
  private
120
105
 
121
106
  def resolve_hand_attribute value
122
- @hand_repository.find(hash_key)[value]
107
+ @hand_repository.evaluate_5_card_hand(cards)[value]
123
108
  end
124
109
 
125
110
  end
@@ -30,7 +30,7 @@ class HandType
30
30
  self.values.each do |type|
31
31
  return type if type.key.casecmp(key) == 0
32
32
  end
33
- raise ArgumentError, "No hand type exists for key " + key
33
+ raise ArgumentError, "No hand type exists for key '#{key}'"
34
34
  end
35
35
 
36
36
  def to_s
@@ -10,7 +10,7 @@ class Pot
10
10
 
11
11
  # Adds the specified bet to the pot
12
12
  def add bet
13
- raise ArgumentError if bet < 0
13
+ raise ArgumentError, "Can only add positive values" if bet < 0
14
14
  @value += bet
15
15
  self
16
16
  end
@@ -8,8 +8,10 @@
8
8
  # and the strongest value (12) assigned to the ace.
9
9
  #
10
10
  class Rank
11
- attr_reader :id, :key, :value, :order
11
+ include Comparable
12
12
 
13
+ attr_reader :id, :key, :value, :order
14
+
13
15
  def initialize(id, key, value, order)
14
16
  @id = id
15
17
  @key = key
@@ -23,30 +25,46 @@ class Rank
23
25
 
24
26
  def self.get(key)
25
27
  self.values.each do |rank|
26
- return rank if rank.key.casecmp(key) == 0
28
+ return rank if(rank.key.casecmp(key) == 0)
27
29
  end
28
30
  raise ArgumentError, "No rank exists for key '#{key}'"
29
31
  end
30
32
 
33
+ def <=>(rank)
34
+ self.order <=> rank.order
35
+ end
36
+
37
+ def eql? rank
38
+ self == rank
39
+ end
40
+
41
+ def == rank
42
+ self.key == rank.key
43
+ end
44
+
45
+ def hash
46
+ return self.rank.ord
47
+ end
48
+
31
49
  def to_s
32
- "Rank: #{@value}"
50
+ "#{@value}"
33
51
  end
34
52
 
35
53
  class << self
36
54
  private :new
37
55
  end
38
56
 
39
- TWO = new(2, "2", "Two", 0)
40
- THREE = new(3, "3", "Three", 1)
41
- FOUR = new(5, "4", "Four", 2)
42
- FIVE = new(7, "5", "Five", 3)
43
- SIX = new(11, "6", "Six", 4)
44
- SEVEN = new(13, "7", "Seven", 5)
45
- EIGHT = new(17, "8", "Eight", 6)
46
- NINE = new(19, "9", "Nine", 7)
47
- TEN = new(23, "T", "Ten", 8)
48
- JACK = new(29, "J", "Jack", 9)
49
- QUEEN = new(31, "Q", "Queen", 10)
50
- KING = new(37, "K", "King", 11)
51
- ACE = new(41, "A", "Ace", 12)
57
+ TWO = new(2, "2", "Two", 13)
58
+ THREE = new(3, "3", "Three", 12)
59
+ FOUR = new(5, "4", "Four", 11)
60
+ FIVE = new(7, "5", "Five", 10)
61
+ SIX = new(11, "6", "Six", 9)
62
+ SEVEN = new(13, "7", "Seven", 8)
63
+ EIGHT = new(17, "8", "Eight", 7)
64
+ NINE = new(19, "9", "Nine", 6)
65
+ TEN = new(23, "T", "Ten", 5)
66
+ JACK = new(29, "J", "Jack", 4)
67
+ QUEEN = new(31, "Q", "Queen", 3)
68
+ KING = new(37, "K", "King", 2)
69
+ ACE = new(41, "A", "Ace", 1)
52
70
  end
@@ -3,18 +3,14 @@
3
3
  # one player and remain hidden from the other players.
4
4
  #
5
5
  class StartingHand
6
- attr_reader :id, :key, :cards
7
-
6
+ attr_reader :cards, :key, :id
7
+
8
8
  def initialize cards
9
- @cards = Array.new, @id = 1, @key = 1
10
-
11
- @cards = cards.kind_of?(Array) ? cards : Card.to_cards(cards)
9
+ @cards = (cards.kind_of?(Array) ? cards : Card.to_cards(cards)).sort
12
10
  raise ArgumentError, "Exactly 2 cards are required to create a starting hand, #{@cards.size} provided" if @cards.size != 2
13
- raise ArgumentError, "The hand contains duplicate cards" if @cards.uniq.length != @cards.length
14
-
15
- @cards.each {|card| @id *= card.id}
16
- @cards.each {|card| @key *= card.rank.id}
17
- @key = suited? ? @key * 67 : @key
11
+ raise ArgumentError, "The starting hand contains duplicate cards" if @cards.uniq.length != @cards.length
12
+ @key = @cards.inject('') { |string, card| string << card.key }
13
+ @id = @cards.inject(1) { |product, card| product * card.uid }
18
14
  end
19
15
 
20
16
  # Returns all cards contained in the starting hand.
@@ -22,11 +18,10 @@ class StartingHand
22
18
  @cards.dup
23
19
  end
24
20
 
25
- # Returns the starting hand value.
26
21
  def value
27
- @cards.map { |card| "#{card.key}" }.join(",")
22
+ @cards.map { |card| "#{card.value}" }.join(", ")
28
23
  end
29
-
24
+
30
25
  # Returns the shorthand notation for the starting hand.
31
26
  #
32
27
  # It is often desirable to have a short hand notation for starting hands,
@@ -52,10 +47,7 @@ class StartingHand
52
47
 
53
48
  # Determines if the starting hand is suited.
54
49
  def suited?
55
- for i in 0..@cards.size - 2 do
56
- return false if @cards[i].suit != @cards[i+1].suit
57
- end
58
- true
50
+ @cards[0].suit == @cards[1].suit
59
51
  end
60
52
 
61
53
  # Returns all possible starting hands.
@@ -99,8 +91,20 @@ class StartingHand
99
91
  StartingHandRepository.instance.distinct_starting_hands
100
92
  end
101
93
 
94
+ def eql? starting_hand
95
+ self == starting_hand
96
+ end
97
+
98
+ def == starting_hand
99
+ self.id == starting_hand.id
100
+ end
101
+
102
+ def hash
103
+ return self.id
104
+ end
105
+
102
106
  def to_s
103
- "StartingHand => " + @cards[0].to_s + ", " + @cards[1].to_s
107
+ value
104
108
  end
105
109
 
106
110
  end