wizardwerdna-pokerstats 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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