├── .gitignore ├── Gemfile ├── range.rb ├── app.rb ├── Gemfile.lock ├── hand_group.rb ├── spec ├── range_parser.rb ├── range_formatter.rb ├── hand_group.rb ├── hand_groups.rb ├── flush_evaluator.rb ├── pair_evaluator.rb ├── straight_evaluator.rb ├── range_evaluator.rb ├── range_manager.rb ├── hand_evaluator.rb └── range_integration.rb ├── range_formatter.rb ├── flush_evaluator.rb ├── hand_groups.rb ├── straight_evaluator.rb ├── hand_evaluator.rb ├── range_parser.rb ├── range_manager.rb ├── pair_evaluator.rb ├── range_evaluator.rb └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | *.swo 3 | *.swn 4 | *.swm 5 | todo.txt 6 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | # A sample Gemfile 2 | source "https://rubygems.org" 3 | 4 | # gem "rails" 5 | gem 'rspec' 6 | gem 'sinatra' 7 | -------------------------------------------------------------------------------- /range.rb: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | require 'pp' 3 | require 'json' 4 | require './range_manager.rb' 5 | require './range_evaluator.rb' 6 | 7 | board = ARGV[0] 8 | range = ARGV[1] 9 | 10 | rangeManager = RangeTools::RangeManager.new 11 | rangeManager.populateRange(range) 12 | 13 | rangeEvaluator = RangeTools::RangeEvaluator.new(board) 14 | rangeEvaluator.evaluateRange(rangeManager.range) 15 | 16 | x = rangeEvaluator.rangeReport(rangeManager) 17 | puts x.to_json 18 | -------------------------------------------------------------------------------- /app.rb: -------------------------------------------------------------------------------- 1 | require 'sinatra' 2 | require 'pp' 3 | require 'json' 4 | require './range_manager.rb' 5 | require './range_evaluator.rb' 6 | 7 | set :bind, "0.0.0.0" 8 | 9 | get '/:board/:range' do 10 | content_type :json 11 | headers 'Access-Control-Allow-Origin' => 'http://localhost:8080' 12 | puts params 13 | board = params['board'] 14 | range = params['range'] 15 | rangeManager = RangeTools::RangeManager.new 16 | rangeManager.populateRange(range) 17 | 18 | rangeEvaluator = RangeTools::RangeEvaluator.new(board) 19 | rangeEvaluator.evaluateRange(rangeManager.range) 20 | 21 | x = rangeEvaluator.rangeReport(rangeManager) 22 | x.to_json.to_json 23 | end 24 | -------------------------------------------------------------------------------- /Gemfile.lock: -------------------------------------------------------------------------------- 1 | GEM 2 | remote: https://rubygems.org/ 3 | specs: 4 | diff-lcs (1.2.5) 5 | mustermann (1.0.3) 6 | rack (2.0.8) 7 | rack-protection (2.0.7) 8 | rack 9 | rspec (2.14.1) 10 | rspec-core (~> 2.14.0) 11 | rspec-expectations (~> 2.14.0) 12 | rspec-mocks (~> 2.14.0) 13 | rspec-core (2.14.8) 14 | rspec-expectations (2.14.5) 15 | diff-lcs (>= 1.1.3, < 2.0) 16 | rspec-mocks (2.14.6) 17 | sinatra (2.0.7) 18 | mustermann (~> 1.0) 19 | rack (~> 2.0) 20 | rack-protection (= 2.0.7) 21 | tilt (~> 2.0) 22 | tilt (2.0.10) 23 | 24 | PLATFORMS 25 | ruby 26 | 27 | DEPENDENCIES 28 | rspec 29 | sinatra 30 | 31 | BUNDLED WITH 32 | 1.17.2 33 | -------------------------------------------------------------------------------- /hand_group.rb: -------------------------------------------------------------------------------- 1 | module RangeTools 2 | class HandGroup 3 | attr_accessor :hands 4 | 5 | def initialize(hands,type=:all) 6 | @hands = hands 7 | @typeString = getTypeString(type) 8 | @singles = hands[0].length == 4 9 | end 10 | 11 | def setType(type) 12 | @typeString = getTypeString(type) 13 | self 14 | end 15 | 16 | def getTypeString(type) 17 | { 18 | suits: 's', 19 | offsuits: 'o', 20 | all: '', 21 | }[type] 22 | end 23 | 24 | def to_s 25 | if @singles 26 | @hands.map {|hand| hand.to_s}.join(',') 27 | elsif isSpanner? 28 | @hands.first.to_s + '-' + @hands.last.to_s[1] + @typeString 29 | else 30 | @hands.first.to_s + @typeString 31 | end 32 | end 33 | 34 | def isSpanner? 35 | @hands.length > 1 36 | end 37 | 38 | end 39 | end 40 | -------------------------------------------------------------------------------- /spec/range_parser.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | require 'rspec' 4 | require './range_parser.rb' 5 | require './range_manager.rb' 6 | 7 | module RangeTools 8 | class RangeManager 9 | include RangeParser 10 | end 11 | end 12 | 13 | rangeManager = RangeTools::RangeManager.new 14 | 15 | describe 'RangeParser module' do 16 | it 'parses range returns tag buckets' do 17 | tagBuckets = rangeManager.parseRange('KJ, QTs,98cc') 18 | tagBuckets[:suited].should == [:QT] 19 | tagBuckets[:offsuited].should == [] 20 | tagBuckets[:both].should == [:KJ] 21 | tagBuckets[:single].should == {:'98' => [:cc]} 22 | 23 | tagBuckets = rangeManager.parseRange('AA-JJ, 87-5') 24 | tagBuckets[:suited].should == [] 25 | tagBuckets[:offsuited].should == [] 26 | tagBuckets[:both].should == [:AA, :KK, :QQ, :JJ, :'87', :'86', :'85'] 27 | tagBuckets[:single].should == {} 28 | end 29 | end 30 | -------------------------------------------------------------------------------- /spec/range_formatter.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | require 'rspec' 4 | require './range_formatter.rb' 5 | require './range_manager.rb' 6 | 7 | rangeManager = RangeTools::RangeManager.new 8 | 9 | describe 'RangeFormatter module' do 10 | before(:each) do 11 | @rangeManager = RangeTools::RangeManager.new 12 | end 13 | it 'range manager parses range string, populates range, and creates new representation of range string' do 14 | #idea is there are many valid range strings that represent the same range 15 | #but each range will be formatted by the rangemanager to the same string 16 | @rangeManager.populateRange('33,Q9-4,Q2sc,AK,AQ,88,99,Q2cc,Q2sh,QJss,QJcc,QJdd,QJhh') 17 | @rangeManager.formatRange.should == '99-8,33,AK-Q,Q9-4,QJs,Q2cc,Q2sc,Q2sh' 18 | 19 | @rangeManager.resetAll 20 | @rangeManager.populateRange('KJo,98-3o') 21 | @rangeManager.formatRange.should == 'KJo,98-3o' 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /spec/hand_group.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | require 'rspec' 4 | require './hand_group.rb' 5 | 6 | 7 | describe 'Hand Group' do 8 | before(:each) do 9 | group = ["AK", "AQ", "AJ", "AT"] 10 | @hg = RangeTools::HandGroup.new(group, :offsuits) 11 | end 12 | 13 | it 'has working to_s method' do 14 | @hg.to_s.should == 'AK-To' 15 | 16 | group = ["AK"] 17 | @hg = RangeTools::HandGroup.new(group, :offsuits) 18 | @hg.to_s.should == 'AKo' 19 | 20 | group = ["AK"] 21 | @hg = RangeTools::HandGroup.new(group, :suits) 22 | @hg.to_s.should == 'AKs' 23 | 24 | group = ["AK"] 25 | @hg = RangeTools::HandGroup.new(group) 26 | @hg.to_s.should == 'AK' 27 | 28 | group = ["AK", "AQ"] 29 | @hg = RangeTools::HandGroup.new(group) 30 | @hg.to_s.should == 'AK-Q' 31 | 32 | group = ["AKcs", "AQss"] 33 | @hg = RangeTools::HandGroup.new(group) 34 | @hg.to_s.should == 'AKcs,AQss' 35 | end 36 | 37 | end 38 | -------------------------------------------------------------------------------- /range_formatter.rb: -------------------------------------------------------------------------------- 1 | require ('./hand_groups.rb') 2 | 3 | module RangeTools 4 | module RangeFormatter 5 | 6 | def cards 7 | '23456789TJQKA'.split('').reverse 8 | end 9 | 10 | def nonPaired 11 | _nonPaired = []#lower left triangle of sklansky table[[AK..A2],[KQ..K2]..] 12 | cards.each do |lCard| 13 | rCards = cards[(cards.index(lCard) + 1)..-1] 14 | col = [] 15 | rCards.each do |rCard| 16 | col << lCard + rCard 17 | end 18 | _nonPaired << col if !col.empty? 19 | end 20 | _nonPaired 21 | end 22 | 23 | def paired 24 | _paired = [] 25 | cards.each do |card| 26 | _paired << card + card 27 | end 28 | [_paired] 29 | end 30 | 31 | 32 | def formatRange 33 | rangeStrings = [] 34 | 35 | paired.each do |col| 36 | rangeStrings << HandGroups.new(col,self).to_s 37 | end 38 | 39 | nonPaired.each do |col| 40 | rangeStrings << HandGroups.new(col,self).to_s 41 | end 42 | 43 | rangeStrings.reject {|s| s.empty?}.join(',') 44 | end 45 | 46 | end 47 | end 48 | -------------------------------------------------------------------------------- /flush_evaluator.rb: -------------------------------------------------------------------------------- 1 | module RangeTools 2 | module FlushEvaluator 3 | extend self 4 | 5 | def evalFlush(twoCardHand, board) 6 | fullHand, board = prepareForFlush(twoCardHand, board) 7 | { 8 | fullHand: flushStrength(fullHand), 9 | board: flushStrength(board) 10 | } 11 | end 12 | 13 | private 14 | 15 | def flushStrength(hand) 16 | suitBuckets = buildSuitBuckets(hand) 17 | found = nil 18 | suitBuckets.each_pair do |suit, count| 19 | if count > 4 20 | found = :flush 21 | elsif count > 3 22 | found = :flush_draw 23 | end 24 | end 25 | found 26 | end 27 | 28 | def buildSuitBuckets(fullHand) 29 | fullHand.inject({}) do |suitBuckets, suit| 30 | suitBuckets[suit] ||= 0 31 | suitBuckets[suit] += 1 32 | suitBuckets 33 | end 34 | end 35 | 36 | def prepareForFlush(twoCardHand, board) 37 | twoCardHand = twoCardHand.collect {|card| card[:suit]} 38 | board = board.collect {|card| card[:suit]} 39 | fullHand = (twoCardHand + board) 40 | [fullHand, board] 41 | end 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /spec/hand_groups.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | require 'rspec' 4 | require './hand_groups.rb' 5 | require './range_manager.rb' 6 | 7 | HandGroup = Struct.new('HandGroup', :hands) do 8 | def setType(_) 9 | self 10 | end 11 | end 12 | 13 | describe 'Hand Groups' do 14 | before(:each) do 15 | @rangeManager = RangeTools::RangeManager.new 16 | @rangeManager.populateRange('Q9-4,Q2sc,Q2cc,Q2sh,QJss,QJcc,QJdd,QJhh') 17 | @col = "QJ QT Q9 Q8 Q7 Q6 Q5 Q4 Q3 Q2".split(' ') 18 | @hg = RangeTools::HandGroups.new(@col, @rangeManager) 19 | end 20 | 21 | it 'has group hands method' do 22 | expected = [["QJ"], ["Q9", "Q8", "Q7", "Q6", "Q5", "Q4"], ["Q2cc"]] 23 | @hg.groupHands(@col, :suits).map {|g| g.hands}.should == expected 24 | end 25 | 26 | it 'groups offsuited hands' do 27 | expected = [["Q9", "Q8", "Q7", "Q6", "Q5", "Q4"], ["Q2sc", "Q2sh"]] 28 | @hg.groupHands(@col, :offsuits).map {|g| g.hands}.should == expected 29 | end 30 | it 'has inBothGruops method' do 31 | x = [['KJ'], ['QT', 'Q9'], ['AK']] 32 | y = [['AA', 'KK', 'QQ'], ['AJ'], ['QT', 'Q9'], ['AK'], ['88']] 33 | 34 | 35 | x = x.map {|a| HandGroup.new(a) } 36 | y = y.map {|a| HandGroup.new(a) } 37 | 38 | @hg.inBothGroups(x,y).map {|g| g.hands}.should == [["QT", "Q9"], ["AK"]] 39 | 40 | end 41 | 42 | it 'has remove duplicates method' do 43 | x = [['KJ'], ['QT', 'Q9'], ['AK']] 44 | y = [['AA', 'KK', 'QQ'], ['AJ'], ['QT', 'Q9'], ['AK'], ['88']] 45 | 46 | 47 | x = x.map {|a| HandGroup.new(a) } 48 | y = y.map {|a| HandGroup.new(a) } 49 | 50 | @hg.removeDuplicates(x,y).map {|g| g.hands}.should == [['KJ']] 51 | 52 | end 53 | 54 | it 'has to_s method' do 55 | @hg.to_s.should == 'Q9-4,QJs,Q2cc,Q2sc,Q2sh' 56 | end 57 | 58 | it 'groups pairs' do 59 | @rangeManager = RangeTools::RangeManager.new 60 | @rangeManager.populateRange('Q9-4,Q2sc,Q2cc,88,99,33') 61 | @col = "AA KK QQ JJ TT 99 88 77 66 55 44 33 22".split(' ') 62 | @hg = RangeTools::HandGroups.new(@col, @rangeManager) 63 | 64 | puts @hg.groups.map{|g| g.hands }.should == [["99", "88"], ["33"]] 65 | 66 | end 67 | 68 | end 69 | -------------------------------------------------------------------------------- /hand_groups.rb: -------------------------------------------------------------------------------- 1 | require './hand_group.rb' 2 | 3 | module RangeTools 4 | class HandGroups 5 | attr_accessor :rangeManager 6 | attr_accessor :groups 7 | 8 | def initialize(hands, rangeManager) 9 | @rangeManager = rangeManager 10 | @groups = buildGroups(hands) 11 | end 12 | 13 | def buildGroups(hands) 14 | offsuits = groupHands(hands, :offsuits) 15 | suits = groupHands(hands, :suits) 16 | @both = inBothGroups(suits, offsuits) 17 | @offsuits = removeDuplicates(offsuits, @both) 18 | @suits = removeDuplicates(suits, @both) 19 | 20 | @both + @suits + @offsuits 21 | end 22 | 23 | def to_s 24 | @groups.join(',') 25 | end 26 | 27 | 28 | def inBothGroups(suits, offsuits) 29 | suits.reduce([]) do |both, group| 30 | found = false 31 | offsuits.each do |ogroup| 32 | found ||= ogroup.hands == group.hands 33 | end 34 | both << group.setType(:all) if found 35 | both 36 | end 37 | end 38 | #todo dry 39 | def removeDuplicates(groups, toremove) 40 | groups.reduce([]) do |newgroups, group| 41 | found = false 42 | toremove.each do |othergroup| 43 | found ||= othergroup.hands == group.hands 44 | end 45 | newgroups << group unless found 46 | newgroups 47 | end 48 | end 49 | 50 | def groupHands(hands, type) 51 | prevStaged = false 52 | groupIndex = -1 53 | groups = [] 54 | 55 | hands.each do |hand| 56 | all = @rangeManager.allSet?(hand.to_sym, type) 57 | any = @rangeManager.anySet?(hand.to_sym, type) 58 | if all && prevStaged 59 | groups[groupIndex].hands << hand 60 | elsif all && !prevStaged 61 | groups << HandGroup.new([hand], type) 62 | groupIndex += 1 63 | prevStaged = true 64 | elsif any 65 | singleCombos = @rangeManager.getSetCombos(hand.to_sym, type) 66 | singles = singleCombos.map do |combo| 67 | hand.to_s + combo.to_s 68 | end 69 | groups << HandGroup.new(singles) 70 | prevStaged = false 71 | groupIndex += 1 72 | else 73 | prevStaged = false 74 | end 75 | end 76 | groups 77 | end 78 | 79 | end 80 | end 81 | -------------------------------------------------------------------------------- /spec/flush_evaluator.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | require 'rspec' 4 | require './flush_evaluator.rb' 5 | 6 | 7 | describe 'Flush Evaluator' do 8 | before(:each) do 9 | @hand = [ 10 | {suit: :h, tag: :'J', rank: 11}, 11 | {suit: :c, tag: :'A', rank: 14} 12 | ] 13 | @board = [ 14 | {suit: :c, tag: :A, rank: 14}, 15 | {suit: :h, tag: :J, rank: 11}, 16 | {suit: :s, tag: :"3", rank: 3} 17 | ] 18 | @flushEvaluator = RangeTools::FlushEvaluator 19 | end 20 | 21 | it 'has class method evalFlush' do 22 | @flushEvaluator.evalFlush(@hand, @board)[:fullHand].should == nil 23 | @hand = [ 24 | {suit: :h, tag: :'J', rank: 11}, 25 | {suit: :h, tag: :'A', rank: 14} 26 | ] 27 | @board = [ 28 | {suit: :c, tag: :A, rank: 14}, 29 | {suit: :h, tag: :J, rank: 11}, 30 | {suit: :h, tag: :"3", rank: 3} 31 | ] 32 | @flushEvaluator.evalFlush(@hand, @board)[:fullHand].should == :flush_draw 33 | @hand = [ 34 | {suit: :h, tag: :'J', rank: 11}, 35 | {suit: :h, tag: :'A', rank: 14} 36 | ] 37 | @board = [ 38 | {suit: :h, tag: :A, rank: 14}, 39 | {suit: :h, tag: :J, rank: 11}, 40 | {suit: :h, tag: :"3", rank: 3} 41 | ] 42 | @flushEvaluator.evalFlush(@hand, @board)[:fullHand].should == :flush 43 | end 44 | 45 | it 'returns flush on board if on board' do 46 | @hand = [ 47 | {suit: :h, tag: :'J', rank: 11}, 48 | {suit: :h, tag: :'A', rank: 14} 49 | ] 50 | @board = [ 51 | {suit: :c, tag: :A, rank: 14}, 52 | {suit: :c, tag: :A, rank: 12}, 53 | {suit: :c, tag: :A, rank: 13}, 54 | {suit: :c, tag: :J, rank: 11}, 55 | {suit: :c, tag: :"3", rank: 3} 56 | ] 57 | @flushEvaluator.evalFlush(@hand, @board)[:board].should == :flush 58 | @hand = [ 59 | {suit: :h, tag: :'J', rank: 11}, 60 | {suit: :h, tag: :'A', rank: 14} 61 | ] 62 | @board = [ 63 | {suit: :c, tag: :A, rank: 14}, 64 | {suit: :c, tag: :A, rank: 12}, 65 | {suit: :c, tag: :A, rank: 13}, 66 | {suit: :c, tag: :J, rank: 11}, 67 | {suit: :s, tag: :"3", rank: 3} 68 | ] 69 | @flushEvaluator.evalFlush(@hand, @board)[:board].should == :flush_draw 70 | end 71 | end 72 | -------------------------------------------------------------------------------- /spec/pair_evaluator.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | require 'rspec' 4 | require './pair_evaluator.rb' 5 | 6 | 7 | describe 'pairevaluator' do 8 | before(:each) do 9 | end 10 | it 'correctly identifies no pair' do 11 | board = [ 12 | {suit: :c, tag: :T, rank: 10}, 13 | {suit: :h, tag: :K, rank: 13}, 14 | {suit: :s, tag: :"3", rank: 3} 15 | ] 16 | hand = [ 17 | {suit: :h, tag: :J, rank: 11}, 18 | {suit: :h, tag: :A, rank: 14} 19 | ] 20 | x = RangeTools::PairEvaluator.evalPairHands(hand, board) 21 | x[:ace_high].should == true 22 | x[:one_over_card].should == true 23 | x.keys.should_not include :pair 24 | end 25 | it 'correctly identifies pair' do 26 | board = [ 27 | {suit: :c, tag: :T, rank: 10}, 28 | {suit: :h, tag: :K, rank: 13}, 29 | {suit: :s, tag: :"3", rank: 3} 30 | ] 31 | hand = [ 32 | {suit: :h, tag: :J, rank: 11}, 33 | {suit: :h, tag: :T, rank: 10} 34 | ] 35 | x = RangeTools::PairEvaluator.evalPairHands(hand, board) 36 | x[:pair].should == true 37 | end 38 | it 'correctly identifies trips' do 39 | board = [ 40 | {suit: :c, tag: :T, rank: 10}, 41 | {suit: :h, tag: :K, rank: 13}, 42 | {suit: :s, tag: :"3", rank: 3} 43 | ] 44 | hand = [ 45 | {suit: :h, tag: :T, rank: 10}, 46 | {suit: :h, tag: :T, rank: 10} 47 | ] 48 | x = RangeTools::PairEvaluator.evalPairHands(hand, board) 49 | x[:trips].should == true 50 | end 51 | it 'correctly identifies two pair' do 52 | board = [ 53 | {suit: :c, tag: :T, rank: 10}, 54 | {suit: :h, tag: :K, rank: 13}, 55 | {suit: :s, tag: :"3", rank: 3} 56 | ] 57 | hand = [ 58 | {suit: :h, tag: :T, rank: 10}, 59 | {suit: :h, tag: :"3", rank: 3} 60 | ] 61 | x = RangeTools::PairEvaluator.evalPairHands(hand, board) 62 | x[:two_pair].should == true 63 | end 64 | it 'correctly identifies fullhouse' do 65 | board = [ 66 | {suit: :c, tag: :T, rank: 10}, 67 | {suit: :d, tag: :"3", rank: 3}, 68 | {suit: :s, tag: :"3", rank: 3} 69 | ] 70 | hand = [ 71 | {suit: :h, tag: :T, rank: 10}, 72 | {suit: :s, tag: :T, rank: 10} 73 | ] 74 | x = RangeTools::PairEvaluator.evalPairHands(hand, board) 75 | x[:full_house].should == true 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /straight_evaluator.rb: -------------------------------------------------------------------------------- 1 | module RangeTools 2 | module StraightEvaluator 3 | extend self 4 | 5 | def evalStraight(twoCardHand, board) 6 | fullHand, board = prepareForStraight(twoCardHand, board) 7 | { 8 | fullHand: straightStrength(fullHand), 9 | board: straightStrength(board) 10 | } 11 | end 12 | 13 | private 14 | 15 | def prepareForStraight(twoCardHand, board) 16 | twoCardHand = twoCardHand.collect {|card| card[:rank]} 17 | board = board.collect {|card| card[:rank]} 18 | fullHand = (twoCardHand + board).uniq.sort 19 | fullHand = wheelFix(fullHand) 20 | board = wheelFix(board).uniq.sort 21 | [fullHand, board] 22 | 23 | end 24 | 25 | def straightStrength(fullhand) 26 | diffs = straightDiffs(fullhand) 27 | match = straightMatch(diffs) 28 | return match unless [:a234_false_positive, :akqj_false_positive].include?(match) 29 | fixedDiffs = removeEndAce(diffs, match) 30 | fixedMatch = straightMatch(fixedDiffs) 31 | if fixedMatch.nil? then fixedMatch = :gutshot end 32 | fixedMatch 33 | end 34 | 35 | def removeEndAce(diffs, match) 36 | if match == :a234_false_positive 37 | diffs.shift(2) 38 | diffs 39 | else 40 | diffs.pop(2) 41 | diffs 42 | end 43 | end 44 | 45 | def straightMatch(diffs) 46 | matches = [ 47 | [:straight, [1,1,1,1]], 48 | [:a234_false_positive, [100,1,1,1]], 49 | [:akqj_false_positive, [1,1,1,100]], 50 | [:oesd, [1,1,1]], 51 | [:doublegut, [2,1,1,2]], 52 | [:gutshot, [2,1,1]], 53 | [:gutshot, [1,2,1]], 54 | [:gutshot, [1,1,2]], 55 | ] 56 | found = nil 57 | matches.each do |m| 58 | if findSubArray(diffs, m[1]) 59 | found = m[0] 60 | break 61 | end 62 | end 63 | found 64 | end 65 | 66 | def findSubArray(hay, needle) 67 | len = needle.length 68 | subs = hay.length - len 69 | return false if subs < 0 70 | found = false 71 | (0..subs).each do |i| 72 | found = true if hay[i,len] == needle 73 | end 74 | found 75 | end 76 | 77 | def straightDiffs(board) 78 | len = board.length - 2 79 | diffs = [] 80 | (0..len).each do |i| 81 | diffs << board[i+1] - board[i] 82 | end 83 | diffs 84 | end 85 | 86 | def wheelFix(hand) 87 | return hand unless hand.include?(14) 88 | lowSentinel = -99 89 | highSentinel = 114 90 | (hand << 1 << lowSentinel << highSentinel).sort 91 | end 92 | 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /spec/straight_evaluator.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | require 'rspec' 4 | require './straight_evaluator.rb' 5 | 6 | 7 | describe 'Straight Evaluator' do 8 | 9 | it 'has evalStraights method' do 10 | board = [ 11 | {suit: :c, tag: :A, rank: 14}, 12 | {suit: :h, tag: :'2', rank: 2}, 13 | {suit: :s, tag: :"3", rank: 3} 14 | ] 15 | hand = [ 16 | {suit: :c, tag: :'5', rank: 5}, 17 | {suit: :h, tag: :'4', rank: 4}, 18 | ] 19 | RangeTools::StraightEvaluator.evalStraight(hand, board)[:fullHand].should == :straight 20 | RangeTools::StraightEvaluator.evalStraight(hand, board)[:board].should == nil 21 | 22 | board = [ 23 | {suit: :c, tag: :A, rank: 14}, 24 | {suit: :h, tag: :'2', rank: 2}, 25 | {suit: :s, tag: :"3", rank: 3} 26 | ] 27 | hand = [ 28 | {suit: :c, tag: :'5', rank: 5}, 29 | {suit: :h, tag: :'9', rank: 9}, 30 | ] 31 | RangeTools::StraightEvaluator.evalStraight(hand, board)[:fullHand].should == :gutshot 32 | 33 | board = [ 34 | {suit: :c, tag: :J, rank: 11}, 35 | {suit: :h, tag: :T, rank: 10}, 36 | {suit: :s, tag: :"3", rank: 3} 37 | ] 38 | hand = [ 39 | {suit: :c, tag: :Q, rank: 12}, 40 | {suit: :h, tag: :K, rank: 13}, 41 | ] 42 | RangeTools::StraightEvaluator.evalStraight(hand, board)[:fullHand].should == :oesd 43 | 44 | board = [ 45 | {suit: :c, tag: :J, rank: 11}, 46 | {suit: :h, tag: :T, rank: 10}, 47 | {suit: :s, tag: :Q, rank: 12} 48 | ] 49 | hand = [ 50 | {suit: :c, tag: :A, rank: 14}, 51 | {suit: :h, tag: :'8', rank: 8}, 52 | ] 53 | RangeTools::StraightEvaluator.evalStraight(hand, board)[:fullHand].should == :doublegut 54 | RangeTools::StraightEvaluator.evalStraight(hand, board)[:board].should == nil 55 | 56 | board = [ 57 | {suit: :c, tag: :J, rank: 11}, 58 | {suit: :h, tag: :T, rank: 10}, 59 | {suit: :s, tag: :Q, rank: 12} 60 | ] 61 | hand = [ 62 | {suit: :c, tag: :'2', rank: 2}, 63 | {suit: :h, tag: :'7', rank: 7}, 64 | ] 65 | RangeTools::StraightEvaluator.evalStraight(hand, board)[:fullHand].should == nil 66 | RangeTools::StraightEvaluator.evalStraight(hand, board)[:board].should == nil 67 | 68 | board = [ 69 | {suit: :c, tag: :J, rank: 11}, 70 | {suit: :h, tag: :"9", rank: 9}, 71 | {suit: :s, tag: :T, rank: 10}, 72 | {suit: :h, tag: :T, rank: 10}, 73 | {suit: :s, tag: :Q, rank: 12} 74 | ] 75 | hand = [ 76 | {suit: :c, tag: :'2', rank: 2}, 77 | {suit: :h, tag: :'7', rank: 7}, 78 | ] 79 | RangeTools::StraightEvaluator.evalStraight(hand, board)[:fullHand].should == :oesd 80 | RangeTools::StraightEvaluator.evalStraight(hand, board)[:board].should == :oesd 81 | end 82 | 83 | end 84 | -------------------------------------------------------------------------------- /spec/range_evaluator.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | require 'rspec' 4 | require './hand_evaluator.rb' 5 | require './range_evaluator.rb' 6 | require './range_manager.rb' 7 | require 'ostruct' 8 | 9 | 10 | describe 'Range Evaluator' do 11 | before(:each) do 12 | board = [ 13 | {suit: :h, tag: :A, rank: 14}, 14 | {suit: :d, tag: :A, rank: 14}, 15 | {suit: :h, tag: :"3", rank: 3} 16 | ] 17 | @rangeEvaluator = RangeTools::RangeEvaluator.new(board) 18 | 19 | range = "AKsc,QT-9,33"; 20 | @rangeManager = rangeManager = RangeTools::RangeManager.new 21 | rangeManager.populateRange(range) 22 | @range = rangeManager.range 23 | end 24 | 25 | it 'builds card hashes from string of board cards' do 26 | board = @rangeEvaluator.buildBoard('Ks,9h,2c,Ts') 27 | board.should == [ 28 | {:tag=>:K, :rank=>13, :suit=>:s}, 29 | {:tag=>:"9", :rank=>9, :suit=>:h}, 30 | {:tag=>:"2", :rank=>2, :suit=>:c}, 31 | {:tag=>:T, :rank=>10, :suit=>:s} 32 | ] 33 | 34 | end 35 | 36 | it 'builds board when instantiated with string' do 37 | board = 'Ks,9h,2c,Ts' 38 | rangeEvaluator = RangeTools::RangeEvaluator.new(board) 39 | 40 | rangeEvaluator.board.should == [ 41 | {:tag=>:K, :rank=>13, :suit=>:s}, 42 | {:tag=>:"9", :rank=>9, :suit=>:h}, 43 | {:tag=>:"2", :rank=>2, :suit=>:c}, 44 | {:tag=>:T, :rank=>10, :suit=>:s} 45 | ] 46 | end 47 | 48 | it 'has allTwoCardHashes method which gets every possible hand from range for the evalHand method' do 49 | range = "AKsc,QT-9"; 50 | rangeManager = RangeTools::RangeManager.new 51 | rangeManager.populateRange(range) 52 | hashes = @rangeEvaluator.allTwoCardHashes(rangeManager.range) 53 | hashes.length.should == 33 54 | #this order could possibly change but should be 33 combos with at least one matching this 55 | hashes[2].should == [ 56 | {:suit=>:c, :rank=>12, :tag=>:Q}, 57 | {:suit=>:h, :rank=>9, :tag=>:"9"} 58 | ] 59 | end 60 | 61 | it 'has evaluateRange method' do 62 | @rangeEvaluator.evaluateRange(@range) 63 | @rangeEvaluator.madeHands[:full_house].should == 64 | ["33cd", "33cs", "33dc", "33ds", "33sc", "33sd"] 65 | end 66 | 67 | 68 | it 'statistics method loops through and builds hash with percents of hand type' do 69 | @rangeEvaluator.evaluateRange(@range) 70 | stats = @rangeEvaluator.statistics 71 | stats[:full_house].should == 0.15384615384615385 72 | end 73 | 74 | it 'range report is hash with percent of range and specific hands for type' do 75 | @rangeEvaluator.evaluateRange(@range) 76 | report = @rangeEvaluator.rangeReport(@rangeManager) 77 | report[:pair_plus_draw].should == 78 | {:percent=>0.05128205128205128, 79 | :percent_of_group=>0, 80 | :hands=>["Q9hh", "QThh"], 81 | :handRange=>"QThh,Q9hh" 82 | } 83 | 84 | report[:full_house].should == 85 | {:percent=>0.15384615384615385, 86 | :percent_of_group=>1.0, 87 | :hands=>["33cd", "33cs", "33dc", "33ds", "33sc", "33sd"], 88 | :handRange=>"33cd,33cs,33dc,33ds,33sc,33sd" 89 | } 90 | end 91 | 92 | end 93 | -------------------------------------------------------------------------------- /spec/range_manager.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | require 'rspec' 4 | require './range_manager.rb' 5 | 6 | 7 | describe 'RangeTools::RangeManager' do 8 | before(:each) do 9 | @rangeManager = RangeTools::RangeManager.new 10 | end 11 | it 'has build range method called during init ' do 12 | @rangeManager.range.length.should == 91 13 | @rangeManager.range[:"AK"].length.should == 16 14 | @rangeManager.range[:"AA"].length.should == 12 15 | end 16 | 17 | it 'has set all method' do 18 | @rangeManager.setAll(:K3) 19 | @rangeManager.range[:K3].each_value do |val| 20 | val.should == true 21 | end 22 | @rangeManager.range[:K2][:cc].should == false 23 | end 24 | 25 | it 'has set suited method' do 26 | @rangeManager.setSuited(:"QJ") 27 | @rangeManager.range[:QJ][:cc].should == true 28 | @rangeManager.range[:QJ][:cs].should == false 29 | @rangeManager.range[:QJ][:ss].should == true 30 | @rangeManager.range[:QJ][:ds].should == false 31 | end 32 | 33 | it 'has offset suit ed method' do 34 | @rangeManager.setOffSuited(:"Q8") 35 | @rangeManager.range[:Q8][:ss].should == false 36 | @rangeManager.range[:Q8][:sc].should == true 37 | @rangeManager.range[:Q8][:hh].should == false 38 | @rangeManager.range[:Q8][:dc].should == true 39 | end 40 | 41 | it 'has setsinglehand method' do 42 | single = [:AA, [:cs, :dc, :hs]] 43 | @rangeManager.setSingleHand(single[0], single[1]) 44 | @rangeManager.range[:AA][:cd].should == false 45 | @rangeManager.range[:AA][:cs].should == true 46 | @rangeManager.range[:AA][:ds].should == false 47 | @rangeManager.range[:AA][:dc].should == true 48 | @rangeManager.range[:AA][:hs].should == true 49 | end 50 | 51 | it 'has resetAll method set all combos to false' do 52 | single = [:AA, [:cs, :dc, :hs]] 53 | @rangeManager.setSingleHand(single[0], single[1]) 54 | @rangeManager.setOffSuited(:"Q8") 55 | @rangeManager.setAll(:K3) 56 | @rangeManager.setSuited(:"QJ") 57 | @rangeManager.range[:AA][:cs].should == true 58 | @rangeManager.range[:K3][:cs].should == true 59 | 60 | @rangeManager.resetAll 61 | @rangeManager.range[:AA][:cs].should == false 62 | @rangeManager.range[:K3][:cs].should == false 63 | @rangeManager.range[:Q8][:cs].should == false 64 | 65 | end 66 | 67 | it 'has process tagbuckets method' do 68 | tagBuckets = { 69 | single: {AA: [:cs, :dc, :hs], KJ: [:ss, :hh]}, 70 | both: [:KT, :"77", :QJ], 71 | suited: [:"88", :"A4"], 72 | offsuited: [:"43", :JT], 73 | } 74 | @rangeManager.processTagBuckets(tagBuckets) 75 | 76 | @rangeManager.range[:AA][:cs].should == true 77 | @rangeManager.range[:KT][:ds].should == true 78 | @rangeManager.range[:"88"][:cs].should == false 79 | @rangeManager.range[:"88"][:cc].should == true 80 | @rangeManager.range[:"43"][:cc].should == false 81 | @rangeManager.range[:"43"][:cd].should == true 82 | 83 | end 84 | 85 | it 'has populate range method' do 86 | @rangeManager.range[:AK][:cc].should == false 87 | @rangeManager.populateRange('AK, 87') 88 | @rangeManager.range[:AK][:cc].should == true 89 | end 90 | 91 | it 'populate range will take empty string' do 92 | @rangeManager.range[:AK][:cc].should == false 93 | @rangeManager.populateRange('') 94 | @rangeManager.range[:AK][:cc].should == false 95 | end 96 | end 97 | -------------------------------------------------------------------------------- /hand_evaluator.rb: -------------------------------------------------------------------------------- 1 | require('./pair_evaluator.rb') 2 | require('./flush_evaluator.rb') 3 | require('./straight_evaluator.rb') 4 | 5 | module RangeTools 6 | module HandEvaluator 7 | 8 | def evalHand(board, twoCardHand, madeHands) 9 | madePairHands, flushStrength, straightStrength = madeHandInfo(twoCardHand, board) 10 | madeHand = bestMadeHand(flushStrength, straightStrength, madePairHands) 11 | if madeHand.nil? 12 | madeHands = markDrawHands(madeHands, madePairHands, straightStrength, flushStrength, twoCardHand) 13 | elsif madeHand == :quads_or_full_house 14 | madeHands = markHands(madePairHands, madeHands, twoCardHand) 15 | else 16 | madeHands = markMadeHand(madeHands, madeHand, twoCardHand) 17 | end 18 | madeHands 19 | end 20 | 21 | def madeHandInfo(twoCardHand, board) 22 | [ 23 | PairEvaluator.evalPairHands(twoCardHand, board), 24 | FlushEvaluator.evalFlush(twoCardHand, board), 25 | StraightEvaluator.evalStraight(twoCardHand, board) 26 | ] 27 | end 28 | 29 | def markDrawHands(madeHands, madePairHands, straightStrength, flushStrength, twoCardHand) 30 | hasPair = true if madePairHands[:pair] 31 | drawHands = madeDrawHands(straightStrength, flushStrength, hasPair) 32 | madeHands = markHands(drawHands, madeHands, twoCardHand) 33 | markHands(madePairHands, madeHands, twoCardHand)#ace high, overcards 34 | end 35 | 36 | def bestMadeHand(flushStrength, straightStrength, madePairHands) 37 | if (flushStrength[:fullHand] == :flush && straightStrength[:fullHand] == :straight) 38 | :straight_flush 39 | elsif (madePairHands[:quads] || madePairHands[:full_house]) 40 | :quads_or_full_house 41 | elsif flushStrength[:fullHand] == :flush 42 | :flush 43 | elsif straightStrength[:fullHand] == :straight 44 | :straight 45 | end 46 | end 47 | 48 | def madeDrawHands(straightStrength, flushStrength, hasPair) 49 | straight, straightBoard = straightStrength[:fullHand], straightStrength[:board] 50 | flush, flushBoard = flushStrength[:fullHand], flushStrength[:board] 51 | hands = { 52 | combo_draw: straight && flush, 53 | pair_plus_flush_draw: hasPair && (flush == :flush_draw), 54 | flush_draw_on_board: flushBoard && flushBoard == :flush_draw 55 | } 56 | pairPlusX = ('pair_plus_' + straight.to_s).to_sym if straightBoard 57 | hands[pairPlusX] = hasPair && straight if pairPlusX 58 | hands[flush] = true if flush 59 | hands[straight] = true if straight 60 | hands[(straightBoard.to_s + '_on_board').to_sym] = true if straightBoard 61 | hands 62 | end 63 | 64 | def markHands(hands, madeHands, twoCardHand) 65 | hands.each_pair do |hand, isSet| 66 | if isSet 67 | madeHands = markMadeHand(madeHands, hand, twoCardHand) 68 | end 69 | end 70 | madeHands 71 | end 72 | 73 | def markMadeHand(madeHands, handType, twoCardHand) 74 | handTag = buildHandTag(twoCardHand) 75 | madeHands[handType] << handTag 76 | madeHands 77 | end 78 | 79 | def buildHandTag(twoCardHand) 80 | card1, card2 = twoCardHand[0], twoCardHand[1] 81 | if twoCardHand[0][:rank] < twoCardHand[1][:rank] 82 | card1, card2 = card2, card1 83 | end 84 | r1, r2 = card1[:tag].to_s, card2[:tag].to_s 85 | s1, s2 = card1[:suit].to_s, card2[:suit].to_s 86 | r1+r2+s1+s2 87 | end 88 | 89 | end 90 | end 91 | -------------------------------------------------------------------------------- /range_parser.rb: -------------------------------------------------------------------------------- 1 | module RangeTools 2 | module RangeParser 3 | 4 | def parseRange(rangeString) 5 | tagBuckets = { 6 | suited: [], 7 | offsuited: [], 8 | both: [], 9 | single: {} 10 | } 11 | expandRangeTags(rangeString, tagBuckets) 12 | end 13 | 14 | def expandRangeTags(rangeString, tagBuckets) 15 | rangeTags = rangeString.split(',') 16 | rangeTags.each do |rangeTag| 17 | rangeTag.strip! 18 | tagType = getTagType(rangeTag) 19 | if tagType.to_s.index('spanner').nil? 20 | tags = [rangeTag] 21 | else 22 | tags = expandRangeTag(rangeTag, tagType) 23 | end 24 | tagBuckets = addToTagBucket(tags, tagType, tagBuckets) 25 | end 26 | tagBuckets 27 | end 28 | 29 | def addToTagBucket(tags, tagType, tagBuckets) 30 | bucket = bucketType(tagType) 31 | if bucket == :single 32 | tag = tags[0] 33 | tagBuckets = addSingle(tag, tagBuckets) 34 | else 35 | tags.each do |tag| 36 | tagBuckets[bucket] << tag.slice(0,2).to_sym 37 | end 38 | end 39 | tagBuckets 40 | end 41 | 42 | def addSingle(tag, tagBuckets) 43 | hand = tag.slice(0,2).to_sym 44 | suit = tag.slice(2,4).to_sym 45 | tagBuckets[:single][hand] ||= [] 46 | tagBuckets[:single][hand] << suit 47 | tagBuckets 48 | end 49 | 50 | def bucketType(tagType) 51 | types = { 52 | suited: [:suitspanner, :suit], 53 | offsuited: [:offsuitspanner, :offsuit], 54 | both: [:spanner, :pairspanner, :both, :pair], 55 | single: [:single] 56 | } 57 | 58 | bucketType = nil 59 | types.each_pair do |bucket,tagTypes| 60 | bucketType = bucket if tagTypes.include?(tagType) 61 | end 62 | bucketType 63 | end 64 | 65 | def expandRangeTag(rangeTag, tagType) 66 | span = expandSpanner(rangeTag) 67 | expandedTags = nil 68 | if tagType == :pairspanner 69 | expandedTags = span.collect {|card| card + card} 70 | else 71 | suitTag = suitedType(tagType) 72 | expandedTags = span.collect do |rightCard| 73 | rangeTag[0] + rightCard + suitTag 74 | end 75 | end 76 | expandedTags 77 | end 78 | 79 | def suitedType(tagType) 80 | case tagType 81 | when :suitspanner then 's' 82 | when :offsuitspanner then 'o' 83 | when :spanner then '' 84 | end 85 | end 86 | 87 | def expandSpanner(rangeTag) 88 | ranks = 'AKQJT98765432' 89 | startCard = ranks.index(rangeTag[1]) 90 | endCard = ranks.index(rangeTag[3]) 91 | span = ranks.slice(startCard, endCard - startCard + 1) 92 | span.split('') 93 | end 94 | 95 | def getTagType(rangeTag) 96 | #order matters here! todo fix it 97 | tagTypes = { 98 | spanner: /^[A-Z\d]+-[A-Z\d]+$/, 99 | suitspanner: /^[A-Z\d]+-[A-Z\d]s+$/, 100 | offsuitspanner: /^[A-Z\d]+-[A-Z\d]o+$/, 101 | pairspanner: /^([A-Z\d])\1-([A-Z\d])\2$/, 102 | offsuit: /^[A-Z\d][A-Z\d]o$/, 103 | suit: /^[A-Z\d][A-Z\d]s$/, 104 | both: /^[A-Z\d][A-Z\d]$/, 105 | single: /^[A-Z\d][A-Z\d][cdhs][cdhs]$/, 106 | pair: /^([A-Z\d])\1$/ 107 | } 108 | found = nil 109 | tagTypes.each_pair do |type, pattern| 110 | found = type if rangeTag =~ pattern 111 | end 112 | raise 'bad tag in rangestring' if found.nil? 113 | found 114 | end 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /range_manager.rb: -------------------------------------------------------------------------------- 1 | require './range_parser.rb' 2 | require './range_formatter.rb' 3 | 4 | module RangeTools 5 | class RangeManager 6 | include RangeFormatter 7 | include RangeParser 8 | attr_accessor :range 9 | 10 | def initialize 11 | @range = buildRange 12 | end 13 | 14 | def buildRange 15 | ranks = %w(2 3 4 5 6 7 8 9 T J Q K A) 16 | len = ranks.length 17 | range = {} 18 | ranks.each do |right| 19 | cur = ranks.index(right) 20 | lefts = ranks.slice(cur..len) 21 | lefts.each do |left| 22 | pair = true if left == right 23 | range[(left + right).to_sym] = _combos(pair) 24 | end 25 | end 26 | range 27 | end 28 | 29 | def combos 30 | @combos ||= _combos 31 | end 32 | 33 | def _combos(pair=false) 34 | combos = {} 35 | suits = %w(c d h s) 36 | suits.each do |lCombo| 37 | suits.each do |rCombo| 38 | next if lCombo == rCombo && pair 39 | combos[(lCombo + rCombo).to_sym] = false 40 | end 41 | end 42 | combos 43 | end 44 | 45 | def setAll(tag) 46 | pair = true if tag[0] == tag[1] 47 | _combos(pair).each_key do |combo| 48 | range[tag][combo] = true 49 | end 50 | end 51 | 52 | def setSuited(tag) 53 | suits = [:cc, :dd, :hh, :ss] 54 | suits.each do |suit| 55 | range[tag][suit] = true 56 | end 57 | end 58 | 59 | def setOffSuited(tag) 60 | offSuits = combos.keys - [:cc, :dd, :hh, :ss] 61 | offSuits.each do |offSuit| 62 | range[tag][offSuit] = true 63 | end 64 | 65 | end 66 | 67 | def setSingleHand(tag, suits) 68 | suits.each do |combo| 69 | range[tag][combo] = true 70 | end 71 | end 72 | 73 | def resetAll 74 | @range = buildRange 75 | end 76 | 77 | def populateRange(rangeString) 78 | tagBuckets = parseRange(rangeString) 79 | resetAll 80 | processTagBuckets(tagBuckets) 81 | end 82 | 83 | def processTagBuckets(tagBuckets) 84 | rangeSetters = { 85 | setSingleHand: tagBuckets[:single], 86 | setOffSuited: tagBuckets[:offsuited], 87 | setSuited: tagBuckets[:suited], 88 | setAll: tagBuckets[:both] 89 | } 90 | rangeSetters.each_pair do |setter, bucket| 91 | setAllInBucket(setter, bucket) 92 | end 93 | end 94 | 95 | def setAllInBucket(setter, bucket) 96 | if bucket.is_a?(Hash) then 97 | bucket.each_pair {|tag, suits| send(setter,tag, suits)} 98 | else 99 | bucket.each { |tag| send(setter, tag) } 100 | end 101 | end 102 | 103 | 104 | def allSet?(hand, type) 105 | hands = handCombosByType(hand,type) 106 | hands.all? do |combo, set| 107 | set 108 | end 109 | end 110 | 111 | def anySet?(hand, type) 112 | hands = handCombosByType(hand,type) 113 | hands.any? do |combo, set| 114 | set 115 | end 116 | end 117 | 118 | def getSetCombos(hand, type) 119 | hands = handCombosByType(hand,type) 120 | hands.select do |combo, set| 121 | set 122 | end.keys 123 | end 124 | 125 | def handCombosByType(hand,type) 126 | type = :pair if isPair(hand) 127 | hands = @range[hand] 128 | if type == :offsuits 129 | hands = hands.select do |combo,v| 130 | (combos.keys - [:cc, :dd, :hh, :ss]).include?(combo) 131 | end 132 | elsif type == :suits 133 | hands = hands.select do |combo,v| 134 | [:cc, :dd, :hh, :ss].include?(combo) 135 | end 136 | end 137 | hands 138 | end 139 | 140 | def isPair(hand) 141 | hand = hand.to_s 142 | hand[0] == hand[1] 143 | end 144 | 145 | end 146 | end 147 | -------------------------------------------------------------------------------- /pair_evaluator.rb: -------------------------------------------------------------------------------- 1 | module RangeTools 2 | module PairEvaluator 3 | extend self 4 | 5 | def evalPairHands(twoCardHand, board) 6 | pairBuckets = buildPairBuckets(twoCardHand, board) 7 | pairBuckets = preparePairBuckets(pairBuckets) 8 | evalPairBuckets(pairBuckets, twoCardHand, board) 9 | end 10 | 11 | private 12 | 13 | def evalPairBuckets(pairBuckets, twoCardHand, board) 14 | pairHands = {} 15 | pairValue = findPairValue(pairBuckets) 16 | pairHands[pairValue] = true if pairValue 17 | if pairValue == :pair 18 | pairHands = evalPairType(pairHands, pairBuckets, twoCardHand, board) 19 | elsif pairValue.nil? 20 | pairHands = evalOverCards(pairHands, twoCardHand, board) 21 | end 22 | pairHands 23 | end 24 | 25 | def findPairValue(pairBuckets) 26 | pairValue = nil 27 | if pairValue = hasQuads(pairBuckets) 28 | elsif pairValue = hasFullHouse(pairBuckets) 29 | elsif pairValue = hasTrips(pairBuckets) 30 | elsif pairValue = hasTwoPair(pairBuckets) 31 | else pairValue = hasPair(pairBuckets) 32 | end 33 | pairValue 34 | end 35 | 36 | def hasQuads(pairBuckets) 37 | :quads if pairBuckets[:quads].any? 38 | end 39 | 40 | def hasFullHouse(pairBuckets) 41 | found = false 42 | if pairBuckets[:trips].length > 1 43 | found = true 44 | elsif pairBuckets[:trips].any? and pairBuckets[:pairs].any? 45 | found = true 46 | end 47 | :full_house if found 48 | end 49 | 50 | def hasTrips(pairBuckets) 51 | :trips if pairBuckets[:trips].any? 52 | end 53 | 54 | def hasTwoPair(pairBuckets) 55 | :two_pair if pairBuckets[:pairs].length > 1 56 | end 57 | 58 | def hasPair(pairBuckets) 59 | :pair if pairBuckets[:pairs].any? 60 | end 61 | 62 | def evalPairType(pairHands, pairBuckets, twoCardHand, board) 63 | lcard, rcard = twoCardRanks(twoCardHand) 64 | ranks = boardRanks(board) 65 | if ranks.include?(lcard) || ranks.include?(rcard) || (lcard == rcard) 66 | pairHands = evalPairStrength(pairHands, pairBuckets) 67 | pairHands = evalPocketPair(pairHands, lcard, rcard, board) 68 | pairHands = evalTopPair(pairHands, lcard, rcard, board) 69 | else 70 | pairHands[:pair_on_board] = true 71 | end 72 | pairHands 73 | end 74 | 75 | 76 | def evalTopPair(pairHands, lcard, rcard, board) 77 | ranks = boardRanks(board) 78 | if (ranks.include?(lcard) && ranks.max == lcard) \ 79 | || (ranks.include?(rcard) && ranks.max == rcard) 80 | pairHands[:top_pair] = true 81 | end 82 | pairHands 83 | end 84 | 85 | def evalPocketPair(pairHands, lcard, rcard, board) 86 | if lcard == rcard 87 | pairHands[:pocket_pair] = true 88 | pairHands[:premium_pocket] = true if lcard > 11#QQ+ 89 | pairHands[:over_pair] = true if boardRanks(board).max < lcard 90 | end 91 | pairHands 92 | end 93 | 94 | def evalPairStrength(pairHands, pairBuckets) 95 | pair = pairBuckets[:pairs].first 96 | if pair < 7 97 | pairHands[:low_pair] = true 98 | elsif pair < 10 99 | pairHands[:mid_pair] = true 100 | else 101 | pairHands[:high_pair] = true 102 | end 103 | pairHands 104 | end 105 | 106 | def evalOverCards(pairHands, twoCardHand, board) 107 | board = boardRanks(board) 108 | lcard, rcard = twoCardRanks(twoCardHand) 109 | if lcard > board.max && rcard > board.max 110 | pairHands[:over_cards] = true 111 | elsif lcard > board.max || rcard > board.max 112 | pairHands[:one_over_card] = true 113 | end 114 | pairHands[:premium_overs] = true if [lcard, rcard].min > 11 115 | pairHands[:ace_high] = true if [lcard, rcard].include?(14) 116 | pairHands 117 | end 118 | 119 | def preparePairBuckets(pairBuckets) 120 | replaceTagsWithNumbers(pairBuckets) 121 | end 122 | 123 | def replaceTagsWithNumbers(pairBuckets) 124 | numberBuckets = {} 125 | pairBuckets.each_pair do |bucket, tags| 126 | numberBuckets[bucket] = tags.map {|tag| rankNumber(tag)} 127 | end 128 | numberBuckets 129 | end 130 | 131 | def rankNumber(rank) 132 | nums = (2..9).collect { |x| x.to_s.to_sym } 133 | orders = nums + [:T, :J, :Q, :K, :A] 134 | orders.index(rank) + 2 135 | end 136 | 137 | #hand is array of card hashes 138 | #{suit: :c, rank: 13, tag: :A} => Ac 139 | def buildPairBuckets(hand, board) 140 | tags = (hand + board).collect { |card| card[:tag] } 141 | uniques = tags.uniq 142 | tagCounts = uniques.collect { |tag| Hash[tag, tags.count(tag)] } 143 | tagCounts.inject(_pairBuckets) { |bucket, tagCount| 144 | tag, count = tagCount.shift 145 | bucket[whichPairBucket(count)] << tag 146 | bucket 147 | } 148 | end 149 | 150 | def whichPairBucket(count) 151 | case count 152 | when 1 then :highs 153 | when 2 then :pairs 154 | when 3 then :trips 155 | when 4 then :quads 156 | end 157 | end 158 | 159 | def _pairBuckets 160 | pairBuckets = { 161 | highs: [], 162 | pairs: [], 163 | trips: [], 164 | quads: [] 165 | } 166 | end 167 | 168 | def twoCardRanks(twoCardHand) 169 | twoCardHand.collect {|card| card[:rank] } 170 | end 171 | 172 | def boardRanks(board) 173 | board.collect {|card| card[:rank] } 174 | end 175 | 176 | end 177 | end 178 | -------------------------------------------------------------------------------- /range_evaluator.rb: -------------------------------------------------------------------------------- 1 | require './hand_evaluator.rb' 2 | 3 | module RangeTools 4 | class RangeEvaluator 5 | include HandEvaluator 6 | attr_accessor :board 7 | attr_accessor :madeHands 8 | 9 | def initialize(board) 10 | @board = buildBoard(board) 11 | @draws = { 12 | oesd: [], 13 | doublegut: [], 14 | gutshot: [], 15 | pair_plus_gutshot: [], 16 | pair_plus_oesd: [], 17 | pair_plus_doublegut: [], 18 | pair_plus_flush_draw: [], 19 | flush_draw: [], 20 | flush_draw_on_board: [], 21 | pair_plus_oesd: [], 22 | pair_plus_gut: [], 23 | pair_plus_over: [], 24 | oesd_on_board: [], 25 | gutshot_on_board: [], 26 | doublegut_on_board: [], 27 | combo_draw: [], 28 | over_cards: [], 29 | one_over_card: [], 30 | premium_overs: [], 31 | } 32 | @madeHands = { 33 | total: 0, 34 | straight_flush: [], 35 | quads: [], 36 | pocket_pair: [], 37 | premium_pocket: [], 38 | pair: [], 39 | straight: [], 40 | flush: [], 41 | two_pair: [], 42 | trips: [], 43 | full_house: [], 44 | ace_high: [], 45 | mid_pair: [], 46 | high_pair: [], 47 | low_pair: [], 48 | top_pair: [], 49 | over_pair: [], 50 | pair_on_board: [] 51 | }.merge(@draws) 52 | end 53 | 54 | def buildBoard(board) 55 | #should there be some validation??? 56 | #just wrap in a try? 57 | if board.is_a? String 58 | board.split(',').map do |tag| 59 | card = tag.split('') 60 | { 61 | tag: card[0].to_sym, 62 | rank: rankNumber(card[0].to_sym), 63 | suit: card[1].to_sym 64 | } 65 | end 66 | elsif board.is_a? Array 67 | board 68 | end 69 | end 70 | 71 | def evaluateRange(range) 72 | twoCardHashes = allTwoCardHashes(range) 73 | twoCardHashes.each do |twoCardHand| 74 | @madeHands[:total] += 1 75 | @madeHands = evalHand(@board, twoCardHand, @madeHands) 76 | end 77 | end 78 | 79 | def rangeReport(rangeManager) 80 | addExtraHandTypes 81 | statistics.each_with_object({}) do |hands,report| 82 | hand_type = hands[0] 83 | report[hand_type] = { 84 | percent: hands[1], 85 | percent_of_group: percent_of_group(hand_type), 86 | hands: @madeHands[hand_type], 87 | handRange: rangeString(rangeManager,@madeHands[hand_type]) 88 | } 89 | end 90 | end 91 | 92 | def percent_of_group(hand_type) 93 | found_group = findGroup(hand_type) 94 | return 0 if found_group.nil? 95 | total = @madeHands[found_group].length 96 | return 0 if total.zero?#todo maybe throw error or remove line, don't think we can reach this point if no hands in group? 97 | hand_count = @madeHands[hand_type].length 98 | hand_count.to_f / total.to_f 99 | end 100 | 101 | def findGroup(hand_type) 102 | found_group = nil 103 | extraHandTypes.each_pair do |group,group_types| 104 | found_group = group if group_types.include?(hand_type) 105 | end 106 | found_group = :pair if [:pocket_pair, :premium_pocket, :mid_pair, :high_pair, :low_pair, :top_pair, :over_pair, :pair_on_board].include?(hand_type) 107 | found_group 108 | end 109 | 110 | def extraHandTypes 111 | { 112 | full_house_plus: [:full_house, :quads, :straight_flush], 113 | draws: [:combo_draw, :flush_draw, :oesd, :doublegut, :gutshot], 114 | pair_plus_draw: [:pair_plus_gutshot, :pair_plus_oesd, :pair_plus_flush_draw, :pair_plus_over], 115 | overcards: [:ace_high, :premium_overs, :over_cards, :one_over_card] 116 | } 117 | end 118 | 119 | def addExtraHandTypes 120 | extraHandTypes.each_pair do |newLabel,handTypes| 121 | @madeHands[newLabel] = mergeHandArrays(handTypes) 122 | end 123 | end 124 | 125 | def mergeHandArrays(handTypes) 126 | handTypes.reduce([]) do |m, handType| 127 | m += @madeHands[handType] unless @board.length == 5 && @draws.has_key?(handType) 128 | m 129 | end.uniq 130 | end 131 | 132 | def rangeString(rangeManager, singles) 133 | rangeManager.resetAll 134 | rangeManager.populateRange(singles.join(',')) 135 | rangeManager.formatRange 136 | end 137 | 138 | def statistics 139 | total = @madeHands[:total].to_f 140 | @madeHands.each_with_object({}) do |made,stats| 141 | next if made[0] == :total 142 | next if @board.length == 5 && @draws.has_key?(made[0]) 143 | addStat(stats,made,total) 144 | end 145 | end 146 | 147 | def addStat(stats,made,total) 148 | hand_label, hands = made[0],made[1] 149 | count = hands.kind_of?(Array) ? hands.length : 0 150 | stats[hand_label] = total == 0 ? 0 : count.to_f / total 151 | end 152 | 153 | def allTwoCardHashes(range) 154 | twoCardHands = [] 155 | range.each_pair do |tag, combos| 156 | twoCardHand = unpackHands(tag, combos) 157 | twoCardHands += removeDeadCards(twoCardHand) 158 | end 159 | twoCardHands 160 | end 161 | 162 | def removeDeadCards(twoCardHand) 163 | twoCardHand.reject { |hand| 164 | hand.select { |card| 165 | @board.include?(card) 166 | }.any? 167 | } 168 | end 169 | 170 | def unpackHands(tag, combos) 171 | lRank, rRank = unpackRanks(tag) 172 | combos.keep_if {|combo, on| on} 173 | buildCardHashes(lRank, rRank, combos) 174 | end 175 | 176 | def unpackRanks(tag) 177 | l, r = tag[0].to_sym, tag[1].to_sym 178 | end 179 | 180 | def buildCardHashes(lRank, rRank, combos) 181 | cardHashes = [] 182 | combos.each_key do |combo| 183 | hand = [] 184 | hand << buildCardHash(lRank, combo, 0) 185 | hand << buildCardHash(rRank, combo, 1) 186 | cardHashes << hand 187 | end 188 | cardHashes 189 | end 190 | 191 | def buildCardHash(rank, combo, lOrR) 192 | { 193 | suit: combo[lOrR].to_sym, 194 | rank: rankNumber(rank), 195 | tag: rank 196 | } 197 | end 198 | 199 | def rankNumber(rank) 200 | nums = (2..9).collect { |x| x.to_s.to_sym } 201 | orders = nums + [:T, :J, :Q, :K, :A] 202 | orders.index(rank) + 2 203 | end 204 | 205 | end 206 | end 207 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | RangeTools 2 | =============== 3 | 4 | A Texas Holdem poker library and command line tool for evaluating the distribution of hand strength for a given set of hands ("range" in poker terminology) and known board cards. 5 | 6 | Quick Start 7 | ------------- 8 | ``` 9 | $bundle install 10 | $bundle exec rspec spec/*.rb 11 | $bundle exec range.rb 4s,5s,7c AA-JJ,87-4s,AK-Ts,KQ-J 12 | 13 | { 14 | "straight_flush": { 15 | "percent": 0, 16 | "percent_of_group": 0, 17 | "hands": [], 18 | "handRange": "" 19 | }, 20 | ..... 21 | "pocket_pair": { 22 | "percent": 0.44036697247706424, 23 | "percent_of_group": 0.8421052631578947, 24 | "hands": [ 25 | "JJcd", 26 | ... 27 | ], 28 | "handRange": "AA-J", 29 | ...full listing of hand strengths and the percent of total hands with this strength. Also grouped into more arbitrary but useful groupings like draws, pair+draws, premium hands etc 30 | ``` 31 | The above evaluates a flop of 4 of spades, 5 of spades, 7 of clubs against range of pocket aces to jacks, AK,AQ,AJ,AT of suited combinations, 87,86,85,84 of suited combinations and KQ,KJ of all combinations. From the full output you will see only 3.7% of the range is a straight, 14.7% is only Ace high, 6.4% is a flush draw etc 32 | 33 | ------------ 34 | ## Terminology 35 | Terminology used within the program 36 | 37 | **tag**: Any symbol representation of a card or cards. :AK is a tag for all combos of AK, :J is a tag for any card that is jack. This term is mostly used to distinguish itself from "card" hashes which have the full information about a specific card and from the range hash representation of AK, containing the suit combos of AK which are present in a range. 38 | 39 | **singles**: Tokens which only represent a single hand like Ks5h. Within the tagBuckets hash they are a hash where the key is the tag and the value is an array of the combos. So Ks5h,Kd5h,Kc5c would have single: {K5: [:sh,:dh,:cc]}. 40 | 41 | **hand group**: A hand group will be represented by a single token in the range string. So AK-T is the token and the hand group is AK,AQ,AJ,AT. Hand groups can either represent suited combos only, offsuits only, all combos, or a single hand. So the hand group for 76cs contains just that single hand. 42 | 43 | **twoCardHand**: used throughout the program, is just an array of card hashes representing a single holdem hand, [{tag: :J, suit: :c, rank: 11}, {tag: :"5", suit: :d, rank: 5}] 44 | 45 | **rank**: the numerical value for each hand so ten is 10, jack is 11, queen is 12, king 13 and ace 14. 46 | 47 | **combo**: For any two card tag, the possible suit combinations for the tag are the combos for the tag. :cs is the combo where the leftmost card is a club and the rightmost card is a spade. 48 | 49 | **rangeFormatter**: Unsure why I called this formatter, a better term may have been serializer. It just creates a string representation of the range object. 50 | 51 | **range string**: A string representation of all the two card hands a player may be holding. 52 | 53 | ------------ 54 | 55 | 56 | ## Range String Syntax 57 | "87" is all suit combinations of 87, the higher card is always listed first so TJ is not a valid range string. 58 | AK-8 is AK,AQ,AJ,...A8 of all suit combinations 59 | AK-3s is AK,AQ,AJ,...A3 of only suited combinations (cc,dd,hh,ss) 60 | QJ-7o is QJ,QT,Q9,...Q7 of only offsuit combinations (cd,ch,cs,dc,dh,ds,hc,hd,hs,sc,sd,sh) 61 | 88-44 is 88,77,66,55,44 of all combinations 62 | JTcs represents the single hand of Jack of clubs, Ten of spades 63 | 64 | The shorthand notation is optional, populateRange will accept any valid representation of a range (ie AA,KK,QQ will result in the same range as AA-Q and AKo,AKs,AKcs,AKdd,AKcc will result in the same as AK). 65 | 66 | 67 | ------------ 68 | 69 | ## Use as a library 70 | ```ruby 71 | range = "AK-2,KQ-T,QJ-9s,JT,98-7s,87-6s,AA-22" 72 | rangeManager = RangeTools::RangeManager.new 73 | rangeManager.populateRange(range) 74 | 75 | board = "Qh,7h,6s" 76 | rangeEvaluator = RangeTools::RangeEvaluator.new(board) 77 | rangeEvaluator.evaluateRange(rangeManager.range) 78 | ``` 79 | After calling .evaluateRange, the @madeHands hash is populated with all the data about the strength of the individual hand values. You can either use this directly or use the .rangeReport method which returns a hash that loops through the @madeHands value and determines the percent of an overall range for each hand type. The rangeReport also adds some helpful new hand types which just merge some of the preexisting types. This is done so you can see what % of a range is draws, pair plus draws etc (see the rangeEvaluator.extraHandTypes method ) 80 | 81 | When hand types are part of one of these new merged hand types, the :percent_of_group will show what % of that specific group it makes up. So for example you can see what % of your draws are open ended straight draws (oesd) vs gutshots, in addition to seeing what % of your overall range is oesd. 82 | 83 | 84 | ------------ 85 | 86 | ## Internal Details 87 | The rangeManager has an @range attribute which holds all the possible 2card holdings in Texas holdem. It represents hands using a ruby hash where the key is an identifier for the ranks of the cards (internally called a tag). For example, the key of :KJ represents all the possible combinations of hands which are made up of king and a jack. The value for this key is another hash where each key represents the suits of the king and jack cards. Their value is set to true when that specific combination is present in a given range. 88 | 89 | ```ruby 90 | @rangeManager.range[:KJ][:cs] === true #King of clubs, Jacks of spades is in the range 91 | @rangeManager.range[:AJ][:dc] === true #Ace of diamonds, Jacks of clubs is in the range 92 | ``` 93 | 94 | The rangeManager has a few methods for setting and retrieving facts about which specific hands are present in a range. See spec/range_manager.rb for examples. Generally, these methods will not be used directly 95 | because they are used internally by the populateRange method. The populateRange method takes a "range string" and sets all the individual combinations in the @range. The rangeManager also has a formatRange method which will output the range string representation of the current range. 96 | 97 | The rangeEvaluator is responsible for determining the hand strength of the individual hands that make up a range. The @madeHands hash has hand strength types as keys (:straight,:flush,:flush_draw,etc) and the values are the individual hand tags from the range that have that value. So on KQT if our range contains AJs the @madeHands[:straight] == [AJss,AJcc,AJdd,AJhh]. 98 | 99 | The rangeEvaluator is initalized with a board string (or array of hashes representing cards, see tests) and then can have the evaluateRange method called, passing in a range hash (from the rangeManager's range attribute). 100 | 101 | Some arbitrary logic is used in hand_evaluator.rb for determining when to include hands of a certain strength in the @madeHands hash. For example if a specific 102 | hand has a pair but is also a made flush, I made the choice to not count the hand as having a pair. Draws are also not included if a hand has a straight or better. This may seem incorrect from an objective view of a range but from the viewpoint of wanting the best possible information for in game decisions I believe it is preferable. In a sense we're getting an estimate of their best made hand for each holding in their range instead of every hand value, avoiding double counting low value hands. I think it's important to not over represent how many draws are in a range since that is often relied on for estimating bluffing frequencies. It's actually still probably over estimating draws because two-pair and trips will also have draws included. The first few methods in hand_evaluator.rb determine what is included in the @madeHands hash so this is where to start looking if you want to adjust this functionality. 103 | 104 | 105 | ------------ 106 | 107 | ### known bugs 108 | Straight flush detection does not work. The problem is the straight detection and flush detection functions don't return any data about the rank of the hand, only the presence of any flush. These modules would need to return the highest rank from the flush/straight and if they were equal there would be a straight flush. 109 | -------------------------------------------------------------------------------- /spec/hand_evaluator.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | require 'rspec' 4 | require './hand_evaluator.rb' 5 | 6 | describe 'Hand Evaluator' do 7 | before(:each) do 8 | @handEvaluator = Object.new 9 | @handEvaluator.extend(RangeTools::HandEvaluator) 10 | @madeHands = { 11 | total: 0, 12 | straight_flush: [], 13 | quads: [], 14 | pocket_pair: [], 15 | premium_pocket: [], 16 | pair: ['KTcc'], 17 | straight: [], 18 | straight_on_board: [], 19 | oesd: [], 20 | doublegut: [], 21 | gutshot: [], 22 | gutshot_on_board: [], 23 | oesd_on_board: [], 24 | pair_plus_gutshot: [], 25 | pair_plus_oesd: [], 26 | pair_plus_doublegut: [], 27 | pair_plus_flush_draw: [], 28 | flush: [], 29 | flush_draw: [], 30 | flush_on_board: [], 31 | flush_draw_on_board: [], 32 | two_pair: [], 33 | trips: ['QJds'], 34 | set: [], 35 | full_house: [], 36 | pair_plus_oesd: [], 37 | pair_plus_gut: [], 38 | pair_plus_over: [], 39 | pair_plus_flush: [], 40 | combo_draw: [], 41 | ace_high: [], 42 | over_cards: [], 43 | one_over_card: [], 44 | premium_overs: [], 45 | mid_pair: [], 46 | high_pair: [], 47 | low_pair: [], 48 | over_pair: [], 49 | top_pair: [], 50 | pair_on_board: [] 51 | } 52 | end 53 | 54 | it 'has evalhand method' do 55 | hand = [ 56 | {suit: :h, tag: :'J', rank: 11}, 57 | {suit: :c, tag: :'A', rank: 14} 58 | ] 59 | board = [ 60 | {suit: :c, tag: :A, rank: 14}, 61 | {suit: :h, tag: :K, rank: 13}, 62 | {suit: :s, tag: :"3", rank: 3} 63 | ] 64 | result = @handEvaluator.evalHand(board, hand, @madeHands) 65 | result[:pair].should == ['KTcc', 'AJch'] 66 | result[:high_pair].should == ['AJch'] 67 | 68 | hand = [ 69 | {suit: :h, tag: :J, rank: 11}, 70 | {suit: :c, tag: :A, rank: 14} 71 | ] 72 | board = [ 73 | {suit: :d, tag: :J, rank: 11}, 74 | {suit: :s, tag: :J, rank: 11}, 75 | {suit: :s, tag: :"3", rank: 3} 76 | ] 77 | result = @handEvaluator.evalHand(board, hand, @madeHands) 78 | result[:trips].should == ["QJds", "AJch"] 79 | 80 | hand = [ 81 | {suit: :h, tag: :J, rank: 11}, 82 | {suit: :c, tag: :A, rank: 14} 83 | ] 84 | board = [ 85 | {suit: :d, tag: :'4', rank: 4}, 86 | {suit: :s, tag: :'9', rank: 9}, 87 | {suit: :s, tag: :"3", rank: 3} 88 | ] 89 | result = @handEvaluator.evalHand(board, hand, @madeHands) 90 | result[:ace_high].should == ['AJch'] 91 | result[:over_cards].should == ['AJch'] 92 | end 93 | 94 | it 'evals flush' do 95 | hand = [ 96 | {suit: :h, tag: :'J', rank: 11}, 97 | {suit: :h, tag: :'A', rank: 14} 98 | ] 99 | board = [ 100 | {suit: :h, tag: :T, rank: 10}, 101 | {suit: :h, tag: :K, rank: 13}, 102 | {suit: :h, tag: :"3", rank: 3} 103 | ] 104 | result = @handEvaluator.evalHand(board, hand, @madeHands) 105 | result[:flush].should == ["AJhh"] 106 | end 107 | it 'evals flush draw on board' do 108 | hand = [ 109 | {suit: :c, tag: :'J', rank: 11}, 110 | {suit: :c, tag: :'A', rank: 14} 111 | ] 112 | board = [ 113 | {suit: :h, tag: :T, rank: 10}, 114 | {suit: :h, tag: :K, rank: 13}, 115 | {suit: :d, tag: :"3", rank: 3}, 116 | {suit: :h, tag: :'J', rank: 11}, 117 | {suit: :h, tag: :'A', rank: 14} 118 | ] 119 | result = @handEvaluator.evalHand(board, hand, @madeHands) 120 | result[:flush_draw_on_board].should == ["AJcc"] 121 | end 122 | it 'evals flush' do 123 | hand = [ 124 | {suit: :h, tag: :'J', rank: 11}, 125 | {suit: :h, tag: :'A', rank: 14} 126 | ] 127 | board = [ 128 | {suit: :h, tag: :T, rank: 10}, 129 | {suit: :h, tag: :K, rank: 13}, 130 | {suit: :d, tag: :"3", rank: 3}, 131 | {suit: :h, tag: :'J', rank: 11}, 132 | {suit: :h, tag: :'A', rank: 14} 133 | ] 134 | result = @handEvaluator.evalHand(board, hand, @madeHands) 135 | result[:flush].should == ["AJhh"] 136 | end 137 | xit 'evals as two pair when pair on board' do 138 | hand = [ 139 | {suit: :h, tag: :'J', rank: 11}, 140 | {suit: :c, tag: :'A', rank: 14} 141 | ] 142 | board = [ 143 | {suit: :h, tag: :T, rank: 10}, 144 | {suit: :s, tag: :K, rank: 13}, 145 | {suit: :d, tag: :"3", rank: 3}, 146 | {suit: :h, tag: :'3', rank: 3}, 147 | {suit: :h, tag: :'A', rank: 14} 148 | ] 149 | result = @handEvaluator.evalHand(board, hand, @madeHands) 150 | #result[:pair_plus_flush_draw].should_not == ["AJch"] 151 | #result[:two_pair].should == ["AJch"] 152 | #result[:combo_draw].should == ["AJch"] 153 | #not sure above behavior is correct? 154 | #it evals as 2 pair even though the lower pair is on the board (do we want this? this is a unique 155 | #situation compared to having a real 2 pair 156 | #also it ranks as a combo draw because of the gutshot but not pair plus flush draw 157 | end 158 | it 'evals pair plus flush draw' do 159 | hand = [ 160 | {suit: :h, tag: :'J', rank: 11}, 161 | {suit: :c, tag: :'A', rank: 14} 162 | ] 163 | board = [ 164 | {suit: :h, tag: :T, rank: 10}, 165 | {suit: :d, tag: :"7", rank: 7}, 166 | {suit: :h, tag: :'3', rank: 3}, 167 | {suit: :h, tag: :'A', rank: 14} 168 | ] 169 | result = @handEvaluator.evalHand(board, hand, @madeHands) 170 | result[:pair_plus_flush_draw].should == ["AJch"] 171 | end 172 | 173 | it 'evals oesd' do 174 | hand = [ 175 | {suit: :h, tag: :'8', rank: 8}, 176 | {suit: :c, tag: :'9', rank: 9} 177 | ] 178 | board = [ 179 | {suit: :h, tag: :T, rank: 10}, 180 | {suit: :d, tag: :"7", rank: 7}, 181 | {suit: :h, tag: :'3', rank: 3}, 182 | {suit: :s, tag: :'A', rank: 14} 183 | ] 184 | result = @handEvaluator.evalHand(board, hand, @madeHands) 185 | result[:oesd].should == ["98ch"] 186 | end 187 | it 'evals gutshot' do 188 | hand = [ 189 | {suit: :h, tag: :'8', rank: 8}, 190 | {suit: :c, tag: :'9', rank: 9} 191 | ] 192 | board = [ 193 | {suit: :h, tag: :T, rank: 10}, 194 | {suit: :d, tag: :"6", rank: 6}, 195 | {suit: :h, tag: :'3', rank: 3}, 196 | {suit: :s, tag: :'A', rank: 14} 197 | ] 198 | result = @handEvaluator.evalHand(board, hand, @madeHands) 199 | result[:gutshot].should == ["98ch"] 200 | end 201 | it 'evals doublegutshot' do 202 | hand = [ 203 | {suit: :h, tag: :'6', rank: 6}, 204 | {suit: :c, tag: :Q, rank: 12} 205 | ] 206 | board = [ 207 | {suit: :h, tag: :T, rank: 10}, 208 | {suit: :d, tag: :"8", rank: 8}, 209 | {suit: :h, tag: :'9', rank: 9}, 210 | {suit: :s, tag: :'A', rank: 14} 211 | ] 212 | result = @handEvaluator.evalHand(board, hand, @madeHands) 213 | result[:doublegut].should == ["Q6ch"] 214 | end 215 | it 'evals top pair' do 216 | hand = [ 217 | {suit: :h, tag: :'6', rank: 6}, 218 | {suit: :c, tag: :Q, rank: 12} 219 | ] 220 | board = [ 221 | {suit: :h, tag: :T, rank: 10}, 222 | {suit: :d, tag: :"8", rank: 8}, 223 | {suit: :h, tag: :'9', rank: 8}, 224 | {suit: :s, tag: :'Q', rank: 12} 225 | ] 226 | result = @handEvaluator.evalHand(board, hand, @madeHands) 227 | result[:top_pair].should == ["Q6ch"] 228 | end 229 | it 'evals pair on board' do 230 | hand = [ 231 | {suit: :h, tag: :'6', rank: 6}, 232 | {suit: :c, tag: :Q, rank: 12} 233 | ] 234 | board = [ 235 | {suit: :h, tag: :T, rank: 10}, 236 | {suit: :d, tag: :"8", rank: 8}, 237 | {suit: :h, tag: :'9', rank: 8}, 238 | {suit: :s, tag: :'9', rank: 9} 239 | ] 240 | result = @handEvaluator.evalHand(board, hand, @madeHands) 241 | result[:pair_on_board].should == ["Q6ch"] 242 | end 243 | it 'evals two pair' do 244 | hand = [ 245 | {suit: :c, tag: :'9', rank: 9}, 246 | {suit: :c, tag: :Q, rank: 12} 247 | ] 248 | board = [ 249 | {suit: :h, tag: :Q, rank: 12}, 250 | {suit: :d, tag: :"8", rank: 8}, 251 | {suit: :h, tag: :'9', rank: 9}, 252 | {suit: :s, tag: :'2', rank: 2} 253 | ] 254 | result = @handEvaluator.evalHand(board, hand, @madeHands) 255 | result[:two_pair].should == ["Q9cc"] 256 | end 257 | end 258 | -------------------------------------------------------------------------------- /spec/range_integration.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | require 'rspec' 4 | require './range_manager.rb' 5 | require './range_evaluator.rb' 6 | require './hand_evaluator.rb' 7 | require './pair_evaluator.rb' 8 | require 'json' 9 | 10 | 11 | #all the moving pieces working together 12 | describe 'Range Tools' do 13 | before(:each) do 14 | @rangeManager = RangeTools::RangeManager.new 15 | 16 | board = [ 17 | {suit: :c, tag: :A, rank: 14}, 18 | {suit: :s, tag: :J, rank: 11}, 19 | {suit: :s, tag: :"3", rank: 3} 20 | ] 21 | @rangeEvaluator = RangeTools::RangeEvaluator.new(board) 22 | end 23 | it 'rangeparser parses range string and populates range object' do 24 | @rangeManager.range[:AK][:cc].should == false 25 | @rangeManager.range[:KJ][:cc].should == false 26 | @rangeManager.populateRange('AK, KJs, 99, QJ-6, T4-2s, 53o, 87-3o, 66-22') 27 | @rangeManager.range[:AK][:cc].should == true 28 | @rangeManager.range[:KJ][:cc].should == true 29 | @rangeManager.range[:KJ][:cs].should == false 30 | @rangeManager.range[:'99'][:cs].should == true 31 | @rangeManager.range[:QJ][:cs].should == true 32 | @rangeManager.range[:Q8][:ch].should == true 33 | @rangeManager.range[:Q6][:ds].should == true 34 | @rangeManager.range[:T4][:dd].should == true 35 | @rangeManager.range[:T3][:hh].should == true 36 | @rangeManager.range[:T3][:hs].should == false 37 | @rangeManager.range[:T2][:ss].should == true 38 | @rangeManager.range[:'53'][:ss].should == false 39 | @rangeManager.range[:'53'][:sc].should == true 40 | @rangeManager.range[:'87'][:sc].should == true 41 | @rangeManager.range[:'87'][:ss].should == false 42 | @rangeManager.range[:'84'][:hh].should == false 43 | @rangeManager.range[:'84'][:hc].should == true 44 | @rangeManager.range[:'83'][:hc].should == true 45 | @rangeManager.range[:'66'][:ch].should == true 46 | @rangeManager.range[:'44'][:cs].should == true 47 | @rangeManager.range[:'22'][:ds].should == true 48 | end 49 | it 'evaluateRange method will populate the madeHands hash with hand tags' do 50 | 51 | @rangeManager.populateRange('AK, KJs, 99, QJ-6, T4-2s, 53o, 87-3o, 66-22') 52 | @rangeEvaluator.evaluateRange(@rangeManager.range) 53 | 54 | @rangeEvaluator.madeHands[:pocket_pair].should include('99cd') 55 | @rangeEvaluator.madeHands[:pocket_pair].should_not include('99cc') 56 | end 57 | 58 | it 'A234 should be a gutshot' do 59 | rangeManager = RangeTools::RangeManager.new 60 | board = [ 61 | {suit: :c, tag: :K, rank: 13}, 62 | {suit: :s, tag: :'3', rank: 3}, 63 | {suit: :s, tag: :'2', rank: 2} 64 | ] 65 | rangeEvaluator = RangeTools::RangeEvaluator.new(board) 66 | rangeManager.populateRange('A4dd') 67 | rangeEvaluator.evaluateRange(rangeManager.range) 68 | rangeEvaluator.madeHands[:gutshot].should include('A4dd') 69 | end 70 | 71 | it 'AKQJ should be a gutshot' do 72 | rangeManager = RangeTools::RangeManager.new 73 | board = [ 74 | {suit: :c, tag: :A, rank: 14}, 75 | {suit: :s, tag: :K, rank: 13}, 76 | {suit: :s, tag: :Q, rank: 12}, 77 | {suit: :s, tag: :J, rank: 11} 78 | ] 79 | rangeEvaluator = RangeTools::RangeEvaluator.new(board) 80 | rangeManager.populateRange('43dd') 81 | rangeEvaluator.evaluateRange(rangeManager.range) 82 | rangeEvaluator.madeHands[:gutshot].should include('43dd') 83 | end 84 | 85 | it 'Removes board cards from range' do 86 | rangeManager = RangeTools::RangeManager.new 87 | board = [ 88 | {suit: :c, tag: :A, rank: 14}, 89 | {suit: :s, tag: :'2', rank: 2}, 90 | {suit: :s, tag: :'3', rank: 3}, 91 | {suit: :c, tag: :'3', rank: 3} 92 | ] 93 | rangeEvaluator = RangeTools::RangeEvaluator.new(board) 94 | rangeManager.populateRange('T3') 95 | rangeEvaluator.evaluateRange(rangeManager.range) 96 | rangeEvaluator.madeHands[:trips].length.should == 8 97 | end 98 | it 'evaluateRange properly fills madeHands for trips' do 99 | rangeManager = RangeTools::RangeManager.new 100 | board = [ 101 | {suit: :c, tag: :A, rank: 14}, 102 | {suit: :s, tag: :'7', rank: 7}, 103 | {suit: :c, tag: :'3', rank: 3} 104 | ] 105 | rangeEvaluator = RangeTools::RangeEvaluator.new(board) 106 | rangeManager.populateRange('AK-T, AA-22, KQ-T, QJ-8, JT-8, T9-7, 98-6, 87-5, 76-5,65ss,65dd') 107 | rangeEvaluator.evaluateRange(rangeManager.range) 108 | rangeEvaluator.madeHands[:trips].length.should == 18 109 | end 110 | it 'evaluateRange properly fills madeHands for fullhouses' do 111 | rangeManager = RangeTools::RangeManager.new 112 | board = [ 113 | {suit: :c, tag: :A, rank: 14}, 114 | {suit: :s, tag: :'7', rank: 7}, 115 | {suit: :c, tag: :'7', rank: 7} 116 | ] 117 | rangeEvaluator = RangeTools::RangeEvaluator.new(board) 118 | rangeManager.populateRange('AA') 119 | rangeEvaluator.evaluateRange(rangeManager.range) 120 | rangeEvaluator.madeHands[:full_house].length.should == 6 121 | end 122 | xit 'straight on board' do 123 | #doing away with this functionailty for now, possibly bring back? 124 | #problem is it gives all your junk hands this value 125 | #but for this example, QJ is the nuts and Jx is a higher straight so need to account for this 126 | rangeManager = RangeTools::RangeManager.new 127 | board = [ 128 | {suit: :c, tag: :T, rank: 10}, 129 | {suit: :s, tag: :'8', rank: 8}, 130 | {suit: :c, tag: :'7', rank: 7}, 131 | {suit: :c, tag: :'9', rank: 6}, 132 | {suit: :d, tag: :'6', rank: 9} 133 | ] 134 | rangeEvaluator = RangeTools::RangeEvaluator.new(board) 135 | rangeManager.populateRange('AKcs') 136 | rangeEvaluator.evaluateRange(rangeManager.range) 137 | # rangeEvaluator.madeHands[:straight_on_board].should include('AKcs') 138 | end 139 | it 'flush draw on board' do 140 | rangeManager = RangeTools::RangeManager.new 141 | board = [ 142 | {suit: :d, tag: :T, rank: 10}, 143 | {suit: :d, tag: :'8', rank: 8}, 144 | {suit: :c, tag: :'2', rank: 2}, 145 | {suit: :d, tag: :K, rank: 13}, 146 | {suit: :d, tag: :'6', rank: 9} 147 | ] 148 | rangeEvaluator = RangeTools::RangeEvaluator.new(board) 149 | rangeManager.populateRange('A3cs') 150 | rangeEvaluator.evaluateRange(rangeManager.range) 151 | rangeEvaluator.madeHands[:flush_draw_on_board].should include('A3cs') 152 | end 153 | it 'pair plus flushdraw' do 154 | rangeManager = RangeTools::RangeManager.new 155 | board = [ 156 | {suit: :c, tag: :T, rank: 10}, 157 | {suit: :c, tag: :'8', rank: 8}, 158 | {suit: :c, tag: :'7', rank: 7}, 159 | {suit: :h, tag: :A, rank: 14}, 160 | {suit: :d, tag: :'2', rank: 2} 161 | ] 162 | rangeEvaluator = RangeTools::RangeEvaluator.new(board) 163 | rangeManager.populateRange('A4cs') 164 | rangeEvaluator.evaluateRange(rangeManager.range) 165 | rangeEvaluator.madeHands[:pair_plus_flush_draw].should include('A4cs') 166 | 167 | end 168 | it 'straight draw on board' do 169 | rangeManager = RangeTools::RangeManager.new 170 | board = 'Tc,8s,7c,9c' 171 | rangeEvaluator = RangeTools::RangeEvaluator.new(board) 172 | rangeManager.populateRange('AKcs') 173 | rangeEvaluator.evaluateRange(rangeManager.range) 174 | rangeEvaluator.madeHands[:oesd_on_board].should include('AKcs') 175 | end 176 | it 'not include draws in stats if 5 board cards present' do 177 | rangeManager = RangeTools::RangeManager.new 178 | board = [ 179 | {suit: :c, tag: :T, rank: 10}, 180 | {suit: :s, tag: :'8', rank: 8}, 181 | {suit: :c, tag: :'7', rank: 7}, 182 | {suit: :c, tag: :'9', rank: 9}, 183 | {suit: :d, tag: :'2', rank: 2} 184 | ] 185 | rangeEvaluator = RangeTools::RangeEvaluator.new(board) 186 | rangeManager.populateRange('AK-Ts, AA-JJ, KQs, AK-Jo') 187 | rangeEvaluator.evaluateRange(rangeManager.range) 188 | x = rangeEvaluator.rangeReport(rangeManager) 189 | x[:straight_flush][:handRange].should == 'AJcc' 190 | x.keys.index(:oesd).should be_nil 191 | end 192 | it 'adds extra entries to madehands for combination hand types ie all draws' do 193 | rangeManager = RangeTools::RangeManager.new 194 | board = [ 195 | {suit: :c, tag: :T, rank: 10}, 196 | {suit: :s, tag: :'8', rank: 8}, 197 | {suit: :c, tag: :'7', rank: 7}, 198 | {suit: :c, tag: :'9', rank: 9}, 199 | {suit: :d, tag: :'2', rank: 2} 200 | ] 201 | rangeEvaluator = RangeTools::RangeEvaluator.new(board) 202 | rangeManager.populateRange('AK-Ts, AA-JJ, KQs, AK-Jo') 203 | rangeEvaluator.evaluateRange(rangeManager.range) 204 | x = rangeEvaluator.rangeReport(rangeManager) 205 | x[:overcards][:handRange].should == 'AKdd,AKhh,AKss,AQdd,AQhh,AQss,AK-Qo' 206 | end 207 | it 'range report returns handrange and percent' do 208 | rangeManager = RangeTools::RangeManager.new 209 | board = 'Kc,Qc,7s' 210 | rangeEvaluator = RangeTools::RangeEvaluator.new(board) 211 | rangeManager.populateRange('AK-2s, AA-TT, KQ, AK-To, QJ-Ts, JT-9s') 212 | rangeEvaluator.evaluateRange(rangeManager.range) 213 | x = rangeEvaluator.rangeReport(rangeManager) 214 | x[:mid_pair][:hands].should == ["A7cc", "A7dd", "A7hh"] 215 | x[:flush_draw][:percent].should == 0.0759493670886076 216 | end 217 | it 'range report includes percent of group' do 218 | rangeManager = RangeTools::RangeManager.new 219 | board = 'Kc,Qc,7s' 220 | rangeEvaluator = RangeTools::RangeEvaluator.new(board) 221 | rangeManager.populateRange('AK-2s, AA-TT, KQ, AK-To, QJ-Ts, JT-9s') 222 | rangeEvaluator.evaluateRange(rangeManager.range) 223 | x = rangeEvaluator.rangeReport(rangeManager) 224 | x[:oesd][:percent_of_group].should == 0.08333333333333333 225 | 226 | end 227 | end 228 | --------------------------------------------------------------------------------