wizardwerdna-pokerstats 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
data/.document ADDED
@@ -0,0 +1,5 @@
1
+ README.rdoc
2
+ lib/**/*.rb
3
+ bin/*
4
+ features/**/*.feature
5
+ LICENSE
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
data/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2009 Andrew C. Greenberg
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,18 @@
1
+ = pokerstats
2
+
3
+ A library for extracting, computing and reporting statistics of poker hands parsed from hand history files.
4
+
5
+ == Note on Patches/Pull Requests
6
+
7
+ * Fork the project.
8
+ * Make your feature addition or bug fix.
9
+ * Add tests for it. This is important so I don't break it in a
10
+ future version unintentionally.
11
+ * Commit, do not mess with rakefile, version, or history.
12
+ (if you want to have your own version, that is fine but
13
+ bump version in a commit by itself I can ignore when I pull)
14
+ * Send me a pull request. Bonus points for topic branches.
15
+
16
+ == Copyright
17
+
18
+ Copyright (c) 2009 Andrew C. Greenberg. See LICENSE for details.
data/Rakefile ADDED
@@ -0,0 +1,50 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+
4
+ begin
5
+ require 'jeweler'
6
+ Jeweler::Tasks.new do |gem|
7
+ gem.name = "pokerstats"
8
+ gem.summary = %Q{poker hand history statistics library}
9
+ gem.description = %Q{a library for extracting, computing and reporting statistics of poker hands parsed from hand history files}
10
+ gem.email = "wizardwerdna@gmail.com"
11
+ gem.homepage = "http://github.com/wizardwerdna/pokerstats"
12
+ gem.authors = ["Andrew C. Greenberg"]
13
+ gem.add_development_dependency "rspec"
14
+ gem.add_development_dependency "wizardwerdna-pluggable"
15
+ gem.add_dependency "wizardwerdna-pluggable"
16
+ # gem is a Gem::Specification... see http://www.rubygems.org/read/chapter/20 for additional settings
17
+ end
18
+ rescue LoadError
19
+ puts "Jeweler (or a dependency) not available. Install it with: sudo gem install jeweler"
20
+ end
21
+
22
+ require 'spec/rake/spectask'
23
+ Spec::Rake::SpecTask.new(:spec) do |spec|
24
+ spec.libs << 'lib' << 'spec'
25
+ spec.spec_files = FileList['spec/**/*_spec.rb']
26
+ end
27
+
28
+ Spec::Rake::SpecTask.new(:rcov) do |spec|
29
+ spec.libs << 'lib' << 'spec'
30
+ spec.pattern = 'spec/**/*_spec.rb'
31
+ spec.rcov = true
32
+ end
33
+
34
+ task :spec => :check_dependencies
35
+
36
+ task :default => :spec
37
+
38
+ require 'rake/rdoctask'
39
+ Rake::RDocTask.new do |rdoc|
40
+ if File.exist?('VERSION')
41
+ version = File.read('VERSION')
42
+ else
43
+ version = ""
44
+ end
45
+
46
+ rdoc.rdoc_dir = 'rdoc'
47
+ rdoc.title = "pokerstats #{version}"
48
+ rdoc.rdoc_files.include('README*')
49
+ rdoc.rdoc_files.include('lib/**/*.rb')
50
+ end
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.4.0
data/lib/checkps ADDED
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/ruby
2
+ require "getoptlong"
3
+ # ENV['RAILS_ENV'] = ENV['RAILS_ENV'] || 'development'
4
+ # require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
5
+ require File.expand_path(File.dirname(__FILE__) + "/pokerstats/pokerstars_file")
6
+ require File.expand_path(File.dirname(__FILE__) + '/pokerstats/player_statistics')
7
+
8
+ def search_poker_edge(playername, players_shown = {})
9
+ return if players_shown[playername]
10
+ escaped_playername = URI.escape(playername).gsub(/["'\[\]]/,'\\\\\&').gsub(/[\[\]]/,'\\\\\\\\\&')
11
+ result = `curl -s http://www.poker-edge.com/whoami.php?site=Stars\\&name=#{escaped_playername}`
12
+ if result =~ /(Pre-Flop Tend.*\n)/
13
+ verbose = $1.gsub(/<\/?[^>]*>/, "")
14
+ if verbose =~ /Pre-Flop Tendency: ([^-]*) -/
15
+ preflop = $1
16
+ else
17
+ preflop = "N/A"
18
+ end
19
+ else
20
+ preflop = "N/A (data error)"
21
+ end
22
+ if result =~ /(Player Type.*\n)/
23
+ verbose = $1.gsub(/<\/?[^>]*>/, "")
24
+ if verbose =~ /[Yy]ou are a ([^(]* \(.*\))/
25
+ player_type = $1
26
+ else
27
+ player_type = ""
28
+ end
29
+ else
30
+ player_type = ""
31
+ end
32
+ players_shown[playername] = preflop
33
+ players_shown[playername] += " " + player_type unless player_type.empty?
34
+ end
35
+
36
+ def display_ratio numerator, denominator
37
+ if numerator.nil? or denominator.nil?
38
+ return "***"
39
+ elsif denominator < 9
40
+ return "#{numerator}/#{denominator}"
41
+ else
42
+ return "#{(100.0 * numerator / denominator).to_i}%"
43
+ end
44
+ end
45
+
46
+ def dopsfile(file, players_shown)
47
+ return if File.directory?(file)
48
+ players = {}
49
+ last = nil
50
+ statistics = PlayerStatistics.new
51
+ PokerstarsFile.open(file).each do |handrecord|
52
+ begin
53
+ handrecord.parse
54
+ statistics.record(handrecord)
55
+ last = handrecord
56
+ rescue Exception => e
57
+ puts e.message
58
+ end
59
+ end
60
+ return if last.nil?
61
+ players = last.stats.players
62
+ puts
63
+ puts "=" * file.size
64
+ puts file
65
+ puts "=" * file.size
66
+ STDOUT.sync = true
67
+ printf("Searching Poker-Edge: ")
68
+ players.each {|each| printf("%s ", each); search_poker_edge(each, players_shown)}
69
+ printf("\n")
70
+ STDOUT.sync = false
71
+ puts "=" * file.size
72
+ reports = statistics.reports
73
+ printf "%-20s %3s %4s %4s %5s %5s %5s %5s %s\n", "Screen Name", "Num", "VP$%", "PFR%", "Pre/Pos", "BAtt%", "BDef%", "CBet%", "Poker-Edge Description"
74
+ printf "%-20s %-39s %s\n", "-"*20, "-"*39, "-"*47
75
+ players.each do |each|
76
+ report = reports[each]
77
+ # puts report.to_yaml
78
+ t_hands = report[:t_hands]
79
+ vpi_p = display_ratio report[:t_vpip], report[:t_hands]
80
+ pfr_p = display_ratio report[:t_pfr_opportunity_taken], report[:t_pfr_opportunity]
81
+ prefa = report[:t_preflop_passive]. zero? ? 0.0 : 1.0 * report[:t_preflop_aggressive] / report[:t_preflop_passive]
82
+ posfa = report[:t_postflop_passive]. zero? ? 0.0 : 1.0 * report[:t_postflop_aggressive] / report[:t_postflop_passive]
83
+ batt_p = display_ratio report[:t_blind_attack_opportunity_taken], report[:t_blind_attack_opportunity]
84
+ bdef_p = display_ratio report[:t_blind_defense_opportunity_taken], report[:t_blind_defense_opportunity]
85
+ cbet_p = display_ratio report[:t_cbet_opportunity_taken], report[:t_cbet_opportunity]
86
+ description = players_shown[each][/\(.*\)/]
87
+ description ||= ""
88
+ description.gsub!("Passive", "P")
89
+ description.gsub!("Aggressive", "A")
90
+ description.gsub!("Tight", "T")
91
+ description.gsub!("Loose", "L")
92
+ players_shown[each].gsub!(/\(.*\)/, description)
93
+ printf "%-20s %3d %4s %4s %2.1f/%2.1f %5s %5s %5s %s\n", each, t_hands, vpi_p, pfr_p, prefa, posfa, batt_p, bdef_p, cbet_p, players_shown[each]
94
+ end
95
+ puts "=" * file.size
96
+ GC.start
97
+ # puts last.reports.keys.inspect
98
+ # puts
99
+ # # puts
100
+ # # puts "=" * 90
101
+ # # puts last.path
102
+ # # players.each {|each| display(each, players_shown)}
103
+ # # puts
104
+ # # puts "=" * 90
105
+ # # puts "PLAYERS NOW AT THIS TABLE"
106
+ # # puts "=" * 90
107
+ # # printf "%-20s %3s %4s %4s %5s %s\n", "Screen Name", "Num", "VP$%", "PFR%", "Pre/Pos", "Poker-Edge Description"
108
+ # # printf "%-20s %-14s %s\n", "-"*20, "-"*21, "-"*47
109
+ # # players.each do |each|
110
+ # # description = players_shown[each][/\(.*\)/]
111
+ # # description ||= ""
112
+ # # description.gsub!("Passive", "P")
113
+ # # description.gsub!("Aggressive", "A")
114
+ # # description.gsub!("Tight", "T")
115
+ # # description.gsub!("Loose", "L")
116
+ # # players_shown[each].gsub!(/\(.*\)/, description)
117
+ # # printf "%-20s %3d %3d%% %3d%% %2.1f/%2.1f %s\n", each,
118
+ # # hands[each], (100.0 * vpip[each])/hands[each], (100.0 * pfr[each])/hands[each],
119
+ # # preflop_passive[each].zero? ? 0.0 : (1.0 * preflop_aggressive[each]) / preflop_passive[each],
120
+ # # postflop_passive[each].zero? ? 0.0 : (1.0 * postflop_aggressive[each]) / postflop_passive[each],
121
+ # # players_shown[each]
122
+ # # end
123
+ # # puts "=" * 90
124
+ # # puts "information on #{hands.size} players collected"
125
+ # # hands = vpip = pfr = sawflop = preflop_aggressive = preflop_passive = nil
126
+
127
+ # # puts
128
+ end
129
+
130
+ def newpsfiles(user, time)
131
+ Dir["/Users/#{user}/Library/Application Support/PokerStars/HandHistory/**/*"].select{|each| File.mtime(each) > time}
132
+ end
133
+
134
+ def getpsdata(user, time, players_shown)
135
+ puts "Loading PokerStars HandHistories that have changed since #{time}"
136
+ while (files = newpsfiles(user, time)).empty?
137
+ sleep 1
138
+ end
139
+ puts files.inspect
140
+ files.each {|each| dopsfile(each, players_shown)}
141
+ end
142
+
143
+ def display_recent_pokerstars_results user
144
+ players_shown = {}
145
+ getpsdata(user, Time.now - 3000, players_shown)
146
+ loop {getpsdata(user, Time.now, players_shown)}
147
+ end
148
+
149
+ def display_poker_edge_results
150
+ players_shown = {}
151
+ $*.each do |playername|
152
+ puts "Poker Edge Search for #{playername}"
153
+ search_poker_edge(playername, players_shown)
154
+ puts "="*80
155
+ printf "%-20s %s\n", playername, players_shown[playername]
156
+ puts "="*80
157
+ end
158
+ end
159
+
160
+
161
+ opts = GetoptLong.new(
162
+ [ "--help", "-h", GetoptLong::NO_ARGUMENT],
163
+ [ "--version", "-v", GetoptLong::NO_ARGUMENT],
164
+ [ "--user", "-u", GetoptLong::OPTIONAL_ARGUMENT]
165
+ )
166
+
167
+ user = `whoami`.chop
168
+ opts.each do |opt, arg|
169
+ case opt
170
+ when "--help", "--usage"
171
+ print "#{$0} playername {--user username} {--player playername} {--help } {--version}\n"
172
+ when "--version"
173
+ print "Judi's Awesome Poker Program -- for Absolute Poker, version 0.6\n"
174
+ when "--user"
175
+ user = arg unless arg.empty?
176
+ end
177
+ end
178
+
179
+ if $*.empty?
180
+ display_recent_pokerstars_results user
181
+ else
182
+ display_poker_edge_results
183
+ end
data/lib/pokerstats.rb ADDED
File without changes
File without changes
@@ -0,0 +1,12 @@
1
+ module HandConstants
2
+ HAND_INFORMATION_KEYS = [:session_filename, :starting_at, :name, :description, :sb, :bb, :board, :total_pot, :rake, :played_at, :tournament]
3
+
4
+ HAND_RECORD_INCOMPLETE_MESSAGE = "hand record is incomplete"
5
+ PLAYER_RECORDS_NO_PLAYER_REGISTERED = "no players have been registered"
6
+ PLAYER_RECORDS_DUPLICATE_PLAYER_NAME = "player screen_name has been registered twice"
7
+ PLAYER_RECORDS_NO_BUTTON_REGISTERED = "no button has been registered"
8
+ PLAYER_RECORDS_UNREGISTERED_PLAYER = "player has not been registered"
9
+ PLAYER_RECORDS_OUT_OF_BALANCE = "hand record is out of balance"
10
+
11
+ MAX_SEATS = 12
12
+ end
@@ -0,0 +1,35 @@
1
+ require File.expand_path(File.dirname(__FILE__) + "/hand_constants")
2
+ require File.expand_path(File.dirname(__FILE__) + "/pokerstars_hand_history_parser")
3
+ class HandHistory
4
+ attr_accessor :lines, :source, :position, :stats
5
+ def initialize lines, source, position, parser_class = PokerstarsHandHistoryParser
6
+ @lines = lines
7
+ @source = source
8
+ @position = position
9
+ @parsed = false
10
+ @parser_class = parser_class
11
+ @stats = HandStatistics.new
12
+ end
13
+
14
+ def parsed?
15
+ @parsed
16
+ end
17
+
18
+ def parse
19
+ @parser = @parser_class.new(@stats)
20
+ @lines.each do |each_line|
21
+ begin
22
+ @parser.parse(each_line)
23
+ rescue => e
24
+ raise "#{@source}:#{position}: #{e.message}"
25
+ end
26
+ end
27
+ @stats.update_hand :session_filename => source, :starting_at => position
28
+ @parsed = true
29
+ end
30
+
31
+ def reports
32
+ parse unless parsed?
33
+ @stats.reports
34
+ end
35
+ end
@@ -0,0 +1,223 @@
1
+ require 'rubygems'
2
+ require 'pluggable'
3
+ require File.expand_path(File.dirname(__FILE__) + '/hand_constants')
4
+ require File.expand_path(File.dirname(__FILE__) + '/hand_statistics_api')
5
+
6
+ class HandStatistics
7
+ include Pluggable
8
+ plugin_include_module HandStatisticsAPI
9
+ def initialize
10
+ install_plugins self
11
+ @hand_information = {}
12
+ @player_hashes = []
13
+ @button_player_index = nil
14
+ @cached_player_position = nil
15
+ @street_state = nil
16
+ street_transition(:prelude)
17
+ end
18
+
19
+ ##
20
+ # Hand Information
21
+ ##
22
+
23
+ def hand_record
24
+ raise "#{HAND_RECORD_INCOMPLETE_MESSAGE}: #{(HAND_INFORMATION_KEYS - @hand_information.keys).inspect}" unless (HAND_INFORMATION_KEYS - @hand_information.keys).empty?
25
+ @hand_information
26
+ end
27
+
28
+ def update_hand update
29
+ street_transition(update[:street]) unless update[:street] == @street_state
30
+ @hand_information.update(update)
31
+ self
32
+ end
33
+
34
+ ##
35
+ # Player Information
36
+ ##
37
+
38
+ def player_records_without_validation
39
+ @player_hashes
40
+ end
41
+
42
+ def player_records
43
+ raise PLAYER_RECORDS_NO_PLAYER_REGISTERED if players.empty?
44
+ raise PLAYER_RECORDS_NO_BUTTON_REGISTERED if button.nil?
45
+ raise PLAYER_RECORDS_OUT_OF_BALANCE if out_of_balance
46
+ self.player_records_without_validation
47
+ end
48
+
49
+ def players
50
+ @player_hashes.sort{|a, b| a[:seat] <=> b[:seat]}.collect{|each| each[:screen_name]}
51
+ end
52
+
53
+ def number_players
54
+ @player_hashes.size
55
+ end
56
+
57
+ def register_player player
58
+ screen_name = player[:screen_name]
59
+ raise "#{PLAYER_RECORDS_DUPLICATE_PLAYER_NAME}: #{screen_name.inspect}" if players.member?(screen_name)
60
+ @cached_player_position = nil
61
+ @player_hashes << player
62
+ plugins.each{|each| each.register_player(screen_name, @street_state)} #why the second parameter?
63
+ street_transition_for_player(@street_state, screen_name)
64
+ end
65
+
66
+ ###
67
+ # Street state information
68
+ ##
69
+
70
+ def street
71
+ @street_state
72
+ end
73
+
74
+ def street_transition street
75
+ @street_state = street
76
+ plugins.each{|each| each.street_transition(street)}
77
+ players.each {|player| street_transition_for_player(street, player)}
78
+ end
79
+
80
+ def street_transition_for_player street, screen_name
81
+ plugins.each{|each| each.street_transition_for_player(street, screen_name)}
82
+ end
83
+
84
+ ##
85
+ # Button and Position Information
86
+ ##
87
+
88
+ def register_button button_index
89
+ @cached_player_position = nil
90
+ @button_player_index = button_index
91
+ end
92
+
93
+ def button
94
+ @button_player_index
95
+ end
96
+
97
+ def button_relative_seat(player_hash)
98
+ (player_hash[:seat] + MAX_SEATS - @button_player_index) % MAX_SEATS
99
+ end
100
+
101
+ # long computation is cached, which cache is cleared every time a new player is registered
102
+ def calculate_player_position screen_name
103
+ @cached_player_position = {}
104
+ @player_hashes.sort!{|a,b| button_relative_seat(a) <=> button_relative_seat(b)}
105
+ @player_hashes = [@player_hashes.pop] + @player_hashes unless @player_hashes.first[:seat] == @button_player_index
106
+ @player_hashes.each_with_index{|player, index| player[:position] = index, @cached_player_position[player[:screen_name]] = index}
107
+ @cached_player_position[screen_name]
108
+ end
109
+
110
+ def position screen_name
111
+ (@cached_player_position && @cached_player_position[screen_name]) || calculate_player_position(screen_name)
112
+ end
113
+
114
+ def button?(screen_name)
115
+ position(screen_name) && position(screen_name).zero?
116
+ end
117
+
118
+ # The cutoff position is defined as the player to the left of the button if there are three players, otherwise nil
119
+ def cutoff_position
120
+ # formerly: (number_players > 3) && (-1 % number_players)
121
+ -1 % number_players if number_players > 3
122
+ end
123
+
124
+ def cutoff?(screen_name)
125
+ position(screen_name) == cutoff_position
126
+ end
127
+
128
+ def blind?(screen_name)
129
+ (sbpos?(screen_name) || bbpos?(screen_name)) and !posted(screen_name).zero?
130
+ end
131
+
132
+ def sbpos?(screen_name)
133
+ (number_players > 2) && position(screen_name) == 1
134
+ end
135
+
136
+ def bbpos?(screen_name)
137
+ (number_players > 2) && position(screen_name) == 2
138
+ end
139
+
140
+ def attacker?(screen_name)
141
+ (number_players > 2) && (button?(screen_name) || cutoff?(screen_name))
142
+ end
143
+
144
+
145
+ ##
146
+ # Action Information
147
+ ##
148
+ def aggression(description)
149
+ case description
150
+ when /call/
151
+ :passive
152
+ when /raise/
153
+ :aggressive
154
+ when /bet/
155
+ :aggressive
156
+ when /fold/
157
+ :fold
158
+ when /check/
159
+ :check
160
+ else
161
+ :neutral
162
+ end
163
+ end
164
+
165
+ def register_action(screen_name, description, options={})
166
+ raise "#{PLAYER_RECORDS_UNREGISTERED_PLAYER}: #{screen_name.inspect}" unless players.member?(screen_name)
167
+ plugins.each do |each|
168
+ each.apply_action(
169
+ {:screen_name => screen_name, :description => description, :aggression => aggression(description)}.update(options),
170
+ @street_state)
171
+ end
172
+ end
173
+
174
+ ##
175
+ # Reporting Information
176
+ ##
177
+
178
+ def report_player(player)
179
+ result = {}
180
+ plugins.each {|each| result.merge!(each.report(player))}
181
+ result
182
+ end
183
+
184
+ def reports
185
+ result = {}
186
+ players.each{|each| result[each] = report_player(each)}
187
+ result
188
+ end
189
+
190
+ def report_hand_information
191
+ @hand_information
192
+ end
193
+
194
+ def self.rails_migration_for_player_data
195
+ prefix = <<-PREFIX
196
+ class AddHandStatisticsForPlayer < ActiveRecord::Migration
197
+ def self.up
198
+ create_table :player_hand_statistics do |t|
199
+ t.integer :hand_id
200
+ PREFIX
201
+ middle = plugin_factory.inject(""){|string, each| string + each.rails_migration_segment_for_player_data}
202
+ suffix = <<-SUFFIX
203
+ end
204
+ end
205
+ def self.down
206
+ drop_table :player_hand_statistics
207
+ end
208
+ end
209
+ SUFFIX
210
+ return prefix + middle + suffix
211
+ end
212
+
213
+ private
214
+ def method_missing symbol, *args
215
+ plugins.send symbol, *args
216
+ end
217
+ end
218
+
219
+ # Load Plugins and Delegate non-api public methods to plugins
220
+ Dir[File.dirname(__FILE__) + "/plugins/*_statistics.rb"].each {|filename| require File.expand_path(filename)}
221
+ HandStatistics.delegate_plugin_public_methods_except HandStatisticsAPI.public_methods
222
+
223
+ puts HandStatistics.rails_migration_for_player_data