pokerstats 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. data/.document +5 -0
  2. data/.gitignore +5 -0
  3. data/LICENSE +20 -0
  4. data/README.rdoc +18 -0
  5. data/Rakefile +50 -0
  6. data/VERSION +1 -0
  7. data/bin/checkps +190 -0
  8. data/generators/pokerstats/USAGE +1 -0
  9. data/generators/pokerstats/pokerstats_generator.rb +7 -0
  10. data/generators/pokerstats/templates/create_pokerstats.rhtml +28 -0
  11. data/lib/pokerstats.rb +2 -0
  12. data/lib/pokerstats/.gitignore +0 -0
  13. data/lib/pokerstats/hand_constants.rb +27 -0
  14. data/lib/pokerstats/hand_history.rb +38 -0
  15. data/lib/pokerstats/hand_statistics.rb +225 -0
  16. data/lib/pokerstats/hand_statistics_api.rb +64 -0
  17. data/lib/pokerstats/player_statistics.rb +58 -0
  18. data/lib/pokerstats/plugins/aggression_statistics.rb +57 -0
  19. data/lib/pokerstats/plugins/blind_attack_statistics.rb +78 -0
  20. data/lib/pokerstats/plugins/cash_statistics.rb +95 -0
  21. data/lib/pokerstats/plugins/continuation_bet_statistics.rb +68 -0
  22. data/lib/pokerstats/plugins/preflop_raise_statistics.rb +50 -0
  23. data/lib/pokerstats/poker-edge.rb +57 -0
  24. data/lib/pokerstats/pokerstars_file.rb +100 -0
  25. data/lib/pokerstats/pokerstars_hand_history_parser.rb +141 -0
  26. data/pokerstats.gemspec +91 -0
  27. data/spec/file_empty.txt +0 -0
  28. data/spec/file_many_hands.txt +564 -0
  29. data/spec/file_one_hand.txt +52 -0
  30. data/spec/hand_statistics_spec.rb +846 -0
  31. data/spec/hand_statistics_spec_helper.rb +89 -0
  32. data/spec/player_statistics_spec.rb +230 -0
  33. data/spec/pokerstars_file_spec.rb +160 -0
  34. data/spec/pokerstars_hand_history_parser_spec.rb +197 -0
  35. data/spec/spec_helper.rb +9 -0
  36. data/spec/zpokerstars_hand_history_parser_integration.rb.txt +67 -0
  37. metadata +125 -0
@@ -0,0 +1,89 @@
1
+ require 'bigdecimal'
2
+ require 'bigdecimal/util'
3
+ require File.expand_path(File.dirname(__FILE__) + '/../lib/pokerstats/hand_constants')
4
+ include HandConstants
5
+
6
+ module HandStatisticsFactories
7
+ def sample_hand
8
+ @@sample_hand_result ||= {
9
+ :session_filename => "a/b/c",
10
+ :starting_at => Time.now,
11
+ :name => "PS0001",
12
+ :description => "This is a description",
13
+ :sb => "1.00".to_d,
14
+ :bb => "2.00".to_d,
15
+ :board => "AS KS QS JS TS",
16
+ :total_pot => "100.00".to_d,
17
+ :rake => "4.00".to_d,
18
+ :played_at => Time.now,
19
+ :tournament => "T123"
20
+ }
21
+ end
22
+
23
+ def sample_player
24
+ @@sample_player_result ||= {
25
+ :screen_name => 'sample_player_screen_name',
26
+ :seat => 1
27
+ }
28
+ end
29
+
30
+ def next_sample_player hash={}
31
+ @@sample_player_count ||= 0
32
+ player = sample_player.clone
33
+ player[:screen_name] = player[:screen_name] + "_" + @@sample_player_count.to_s
34
+ @@sample_player_count += 1
35
+ player.merge(hash)
36
+ end
37
+
38
+ def sample_action
39
+ @@sample_action_result ||= {
40
+ :state => :prelude,
41
+ :screen_name => sample_player[:screen_name],
42
+ :action => :pay,
43
+ :result => :pay,
44
+ :amount => 10,
45
+ :other_thing => :baz
46
+ }
47
+ end
48
+
49
+ def register_street(street, hash={})
50
+ @stats.update_hand(hash.merge(:street => street))
51
+ end
52
+
53
+ def register_bet(player, amount, hash={})
54
+ @stats.register_action player[:screen_name], 'bets', hash.merge(:result => :pay, :amount => amount)
55
+ end
56
+
57
+ def register_post(player, amount, hash={})
58
+ @stats.register_action player[:screen_name], 'posts', hash.merge(:result => :post, :amount => amount)
59
+ end
60
+
61
+ def register_ante(player, amount, hash={})
62
+ @stats.register_action player[:screen_name], 'antes', hash.merge(:result => :post, :amount => amount)
63
+ end
64
+
65
+ def register_raise_to(player, amount, hash={})
66
+ @stats.register_action player[:screen_name], 'raises', hash.merge(:result => :pay_to, :amount => amount)
67
+ end
68
+
69
+ def register_call(player, amount, hash={})
70
+ @stats.register_action player[:screen_name], 'calls', hash.merge(:result => :pay, :amount => amount)
71
+ end
72
+
73
+ def register_check(player, hash={})
74
+ @stats.register_action player[:screen_name], 'checks', hash.merge(:result => :neutral)
75
+ end
76
+
77
+ def register_fold(player, hash={})
78
+ @stats.register_action player[:screen_name], 'folds', hash.merge(:result => :neutral)
79
+ end
80
+
81
+ def register_win(player, amount, hash={})
82
+ @stats.register_action player[:screen_name], 'wins', hash.merge(:result => :win, :amount => amount)
83
+ end
84
+
85
+ def register_cards(player, data, hash={})
86
+ @stats.register_action player[:screen_name], 'shows', hash.merge(:result => :cards, :data => data)
87
+ end
88
+ end
89
+ include HandStatisticsFactories
@@ -0,0 +1,230 @@
1
+ require 'rubygems'
2
+ require 'activesupport'
3
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
4
+ require File.expand_path(File.dirname(__FILE__) + '/../lib/pokerstats/hand_statistics')
5
+ require File.expand_path(File.dirname(__FILE__) + '/../lib/pokerstats/player_statistics')
6
+ include Pokerstats
7
+
8
+ TEST = [
9
+ {"andy" => {
10
+ :is_blind_attack_opportunity => false,
11
+ :is_blind_attack_opportunity_taken => nil,
12
+ :is_blind_defense_opportunity => false,
13
+ :is_blind_defense_opportunity_taken => nil,
14
+ :is_cbet_opportunity => true,
15
+ :is_cbet_opportunity_taken => true,
16
+ :posted => 1,
17
+ :paid => 2,
18
+ :won => 3,
19
+ :cards => nil,
20
+ :preflop_passive => 1,
21
+ :preflop_aggressive => 2,
22
+ :postflop_passive => 3,
23
+ :postflop_aggressive => 4,
24
+ :is_pfr_opportunity => true,
25
+ :is_pfr_opportunity_taken => true
26
+ },
27
+ "judi" => {
28
+ :is_blind_attack_opportunity => true,
29
+ :is_blind_attack_opportunity_taken => false,
30
+ :is_blind_defense_opportunity => true,
31
+ :is_blind_defense_opportunity_taken => true,
32
+ :is_cbet_opportunity => true,
33
+ :is_cbet_opportunity_taken => false,
34
+ :posted => 10,
35
+ :paid => 20,
36
+ :won => 30,
37
+ :cards => nil,
38
+ :preflop_passive => 10,
39
+ :preflop_aggressive => 20,
40
+ :postflop_passive => 30,
41
+ :postflop_aggressive => 40,
42
+ :is_pfr_opportunity => true,
43
+ :is_pfr_opportunity_taken => false
44
+ }
45
+ },
46
+ {"andy" => {
47
+ :is_blind_attack_opportunity => false,
48
+ :is_blind_attack_opportunity_taken => nil,
49
+ :is_blind_defense_opportunity => false,
50
+ :is_blind_defense_opportunity_taken => nil,
51
+ :is_cbet_opportunity => true,
52
+ :is_cbet_opportunity_taken => false,
53
+ :posted => 2,
54
+ :paid => 4,
55
+ :won => 6,
56
+ :cards => nil,
57
+ :preflop_passive => 2,
58
+ :preflop_aggressive => 4,
59
+ :postflop_passive => 6,
60
+ :postflop_aggressive => 8,
61
+ :is_pfr_opportunity => true,
62
+ :is_pfr_opportunity_taken => true
63
+ },
64
+ "judi" => {
65
+ :is_blind_attack_opportunity => true,
66
+ :is_blind_attack_opportunity_taken => false,
67
+ :is_blind_defense_opportunity => true,
68
+ :is_blind_defense_opportunity_taken => true,
69
+ :is_cbet_opportunity => false,
70
+ :is_cbet_opportunity_taken => nil,
71
+ :posted => 20,
72
+ :paid => 30,
73
+ :won => 40,
74
+ :cards => nil,
75
+ :preflop_passive => 20,
76
+ :preflop_aggressive => 40,
77
+ :postflop_passive => 60,
78
+ :postflop_aggressive => 80,
79
+ :is_pfr_opportunity => true,
80
+ :is_pfr_opportunity_taken => false
81
+ }
82
+ },
83
+ {"andy" => {
84
+ :is_blind_attack_opportunity => false,
85
+ :is_blind_attack_opportunity_taken => nil,
86
+ :is_blind_defense_opportunity => true,
87
+ :is_blind_defense_opportunity_taken => false,
88
+ :is_cbet_opportunity => false,
89
+ :is_cbet_opportunity_taken => nil,
90
+ :posted => 3,
91
+ :paid => 6,
92
+ :won => 9,
93
+ :cards => nil,
94
+ :preflop_passive => 3,
95
+ :preflop_aggressive => 6,
96
+ :postflop_passive => 9,
97
+ :postflop_aggressive => 12,
98
+ :is_pfr_opportunity => true,
99
+ :is_pfr_opportunity_taken => true
100
+ },
101
+ "judi" => {
102
+ :is_blind_attack_opportunity => true,
103
+ :is_blind_attack_opportunity_taken => true,
104
+ :is_blind_defense_opportunity => true,
105
+ :is_blind_defense_opportunity_taken => true,
106
+ :is_cbet_opportunity => false,
107
+ :is_cbet_opportunity_taken => nil,
108
+ :posted => 30,
109
+ :paid => 0,
110
+ :won => 0,
111
+ :cards => nil,
112
+ :preflop_passive => 30,
113
+ :preflop_aggressive => 60,
114
+ :postflop_passive => 90,
115
+ :postflop_aggressive => 120,
116
+ :is_pfr_opportunity => true,
117
+ :is_pfr_opportunity_taken => false
118
+ }
119
+ }
120
+ ]
121
+
122
+ PS_RESULT = {
123
+ "andy" => {
124
+ :t_hands => 3,
125
+ :t_vpip => 3,
126
+ :t_posted => 6,
127
+ :t_paid => 12,
128
+ :t_won => 18,
129
+ :t_blind_attack_opportunity => 0,
130
+ :t_blind_attack_opportunity_taken => 0,
131
+ :t_blind_defense_opportunity => 1,
132
+ :t_blind_defense_opportunity_taken => 0,
133
+ :t_cbet_opportunity => 2,
134
+ :t_cbet_opportunity_taken => 1,
135
+ :t_preflop_passive => 6,
136
+ :t_preflop_aggressive => 12,
137
+ :t_postflop_passive => 18,
138
+ :t_postflop_aggressive => 24,
139
+ :t_pfr_opportunity => 3,
140
+ :t_pfr_opportunity_taken => 3
141
+ },
142
+ "judi" => {
143
+ :t_hands => 3,
144
+ :t_vpip => 2,
145
+ :t_posted => 60,
146
+ :t_paid => 50,
147
+ :t_won => 70,
148
+ :t_blind_attack_opportunity => 3,
149
+ :t_blind_attack_opportunity_taken => 1,
150
+ :t_blind_defense_opportunity => 3,
151
+ :t_blind_defense_opportunity_taken => 3,
152
+ :t_cbet_opportunity => 1,
153
+ :t_cbet_opportunity_taken => 0,
154
+ :t_preflop_passive => 60,
155
+ :t_preflop_aggressive => 120,
156
+ :t_postflop_passive => 180,
157
+ :t_postflop_aggressive => 240,
158
+ :t_pfr_opportunity => 3,
159
+ :t_pfr_opportunity_taken => 0
160
+ }
161
+ }
162
+
163
+ class AggregateTo
164
+ def initialize(player, stat, expected)
165
+ @player = player
166
+ @stat = stat
167
+ @expected = expected
168
+ end
169
+
170
+ def matches?(hash)
171
+ @actual = hash[@player][@stat]
172
+ # Satisfy expectation here. Return false or raise an error if it's not met.
173
+ return @actual == @expected
174
+ end
175
+
176
+ def failure_message
177
+ "expected report[#{@player.inspect}][#{@stat.inspect}] to aggregate to #{@expected.inspect}, got #{@actual.inspect}"
178
+ end
179
+
180
+ def negative_failure_message
181
+ "expected report[#{@player.inspect}][#{@stat.inspect}] not to aggregate to #{@expected.inspect}, but it did"
182
+ end
183
+ end
184
+
185
+ def should_correctly_aggregate(reports, stats = [])
186
+ reports.keys.each do |each_player|
187
+ stats.each do |each_stat|
188
+ reports.should AggregateTo.new(each_player, each_stat, PS_RESULT[each_player][each_stat])
189
+ end
190
+ end
191
+ end
192
+
193
+ describe PlayerStatistics, "when aggregating statistics" do
194
+ before do
195
+ @player_statistics = PlayerStatistics.new
196
+ for each_test in TEST
197
+ @hand_statistics = HandStatistics.new
198
+ @hand_statistics.should_receive(:reports).and_return(each_test)
199
+ @player_statistics.record(@hand_statistics)
200
+ end
201
+ @reports = @player_statistics.reports
202
+ end
203
+
204
+ it "should produce a report with data for each player in the reports" do
205
+ players = {}
206
+ for each_hand in TEST
207
+ for each_player in each_hand.keys
208
+ players[each_player] = true
209
+ end
210
+ end
211
+ @reports.should have(players.size).keys
212
+ end
213
+
214
+ it "should correctly aggregate aggression for each player" do
215
+ should_correctly_aggregate @reports, [:t_preflop_passive, :t_preflop_aggressive, :t_postflop_passive, :t_postflop_aggressive]
216
+ end
217
+
218
+ it 'should correctly aggregate blind attack statistics' do
219
+ should_correctly_aggregate @reports, [:t_blind_attack_opportunity, :t_blind_attack_opportunity_taken, :t_blind_defense_opportunity, :t_blind_defense_opportunity_taken]
220
+ end
221
+ it 'should correctly aggregate cash statistics' do
222
+ should_correctly_aggregate @reports, [:t_hands, :t_vpip, :t_posted, :t_paid, :t_won]
223
+ end
224
+ it 'should correctly aggregate continuation bet statistics' do
225
+ should_correctly_aggregate @reports, [:t_cbet_opportunity, :t_cbet_opportunity_taken]
226
+ end
227
+ it 'should correctly aggregate preflop raise statistics' do
228
+ should_correctly_aggregate @reports, [:t_pfr_opportunity, :t_pfr_opportunity_taken]
229
+ end
230
+ end
@@ -0,0 +1,160 @@
1
+ require 'rubygems'
2
+ require 'activesupport'
3
+ require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
4
+ require File.expand_path(File.dirname(__FILE__) + '/../lib/pokerstats/hand_history')
5
+ require File.expand_path(File.dirname(__FILE__) + '/../lib/pokerstats/pokerstars_file')
6
+ include Pokerstats
7
+
8
+ describe PokerstarsFile, "when opened on an empty file" do
9
+ it "should complain" do
10
+ lambda{@psfile = PokerstarsFile.open(File.dirname(__FILE__) + '/file_empty.txt')}.should raise_error
11
+ end
12
+ end
13
+
14
+ describe PokerstarsFile, "when opened on a single-hand file" do
15
+ ONE_HAND_NUMBER_OF_ENTRIES = 1
16
+
17
+ ONE_HAND_ENTRY_NUMBER_OF_LINES = 50
18
+ ONE_HAND_ENTRY_FIRST_LINE = "PokerStars Game #24759011305: Tournament #139359808, $10+$1 Hold'em No Limit - Level I (10/20) - 2009/02/09 14:02:39 ET"
19
+ ONE_HAND_ENTRY_LAST_LINE = "Seat 9: MartinBOF84 folded before Flop (didn't bet)"
20
+
21
+ before do
22
+ @psfile = PokerstarsFile.open(File.dirname(__FILE__) + '/file_one_hand.txt')
23
+ end
24
+
25
+ it "should be open" do
26
+ @psfile.should_not be_closed
27
+ end
28
+
29
+ it "should not be eof" do
30
+ @psfile.should_not be_eof
31
+ end
32
+
33
+ it "should be at position zero" do
34
+ @psfile.pos.should be_zero
35
+ end
36
+
37
+ it "should read all the hands in the test file" do
38
+ @psfile.should have(ONE_HAND_NUMBER_OF_ENTRIES).entries
39
+ end
40
+
41
+ it "should have an entry, answering to first, having the correct lines" do
42
+ @entry = @psfile.first
43
+ @entry.should have(ONE_HAND_ENTRY_NUMBER_OF_LINES).lines
44
+ @entry.lines[0].should == ONE_HAND_ENTRY_FIRST_LINE
45
+ @entry.lines[ONE_HAND_ENTRY_NUMBER_OF_LINES-1].should == ONE_HAND_ENTRY_LAST_LINE
46
+ @psfile.should be_closed
47
+ end
48
+
49
+ it "should have an entry, answering to entries, having the correct lines" do
50
+ @entry = @psfile.entries.first
51
+ @entry.should have(ONE_HAND_ENTRY_NUMBER_OF_LINES).lines
52
+ @entry.lines.first.should == ONE_HAND_ENTRY_FIRST_LINE
53
+ @entry.lines.last.should == ONE_HAND_ENTRY_LAST_LINE
54
+ end
55
+
56
+ it "should be at the end of file after reading the entry" do
57
+ @psfile.first
58
+ @psfile.should be_eof
59
+ end
60
+ end
61
+
62
+ describe PokerstarsFile, "when opened on a file encoded in Latin-1, should transliterate properly to ASCII" do
63
+ LATIN1_LINE_INDEX = 8
64
+ LATIN1_LINE_TRANSLITERATED = "Seat 8: Gw\"unni (3000 in chips) "
65
+
66
+ before do
67
+ @psfile = PokerstarsFile.open(File.dirname(__FILE__) + '/file_one_hand.txt')
68
+ @entry = @psfile.entries.first
69
+ end
70
+
71
+ it "should properly transliterate the selected line" do
72
+ @entry.lines[LATIN1_LINE_INDEX].should == LATIN1_LINE_TRANSLITERATED
73
+ end
74
+ end
75
+
76
+ describe PokerstarsFile, "when opened on a multi-hand file" do
77
+ NUMBER_OF_ENTRIES = 12
78
+
79
+ FIRST_ENTRY_NUMBER_OF_LINES = 47
80
+ FIRST_ENTRY_FIRST_LINE = "PokerStars Game #21650146783: Hold'em No Limit ($0.25/$0.50) - 2008/10/31 17:14:44 ET"
81
+ FIRST_ENTRY_LAST_LINE = "Seat 9: ATTACCA folded before Flop (didn't bet)"
82
+
83
+ LAST_ENTRY_NUMBER_OF_LINES = 48
84
+ LAST_ENTRY_FIRST_LINE = "PokerStars Game #21650401569: Hold'em No Limit ($0.25/$0.50) - 2008/10/31 17:24:58 ET"
85
+ LAST_ENTRY_LAST_LINE = "Seat 9: ATTACCA folded before Flop (didn't bet)"
86
+
87
+ TABLE_OF_STARTING_INDICES = [0, 1526, 2997, 4594, 5939, 7650, 9174, 10740, 12137, 13446, 14589, 15823]
88
+
89
+ before do
90
+ @psfile = PokerstarsFile.open(File.dirname(__FILE__) + '/file_many_hands.txt')
91
+ @expanded_path = File.expand_path(File.dirname(__FILE__) + '/file_many_hands.txt')
92
+ end
93
+
94
+ it "should be open" do
95
+ @psfile.should_not be_closed
96
+ end
97
+
98
+ it "should not be eof" do
99
+ @psfile.should_not be_eof
100
+ end
101
+
102
+ it "should read all the hands in the test file" do
103
+ @psfile.should have(NUMBER_OF_ENTRIES).entries
104
+ end
105
+
106
+ it "should collect entries with all the proper information" do
107
+ list = TABLE_OF_STARTING_INDICES.clone
108
+ @psfile.entries.each do |handrecord|
109
+ handrecord.source.should == @expanded_path
110
+ handrecord.position.should == list.shift
111
+ end
112
+ end
113
+
114
+ it "should be able to access records through valid positions" do
115
+ TABLE_OF_STARTING_INDICES.each do |index|
116
+ @entry = @psfile.first(index)
117
+ end
118
+ end
119
+
120
+ it "should complain when attempting to reach records through invalid positions" do
121
+ TABLE_OF_STARTING_INDICES.each do |index|
122
+ lambda {@psfile.pos=each+1}.should raise_error
123
+ end
124
+ end
125
+
126
+ it "should have a first entry having the correct lines, addressable through #first" do
127
+ @psfile.entries #run through the file to see if it resets properly
128
+ @entry = @psfile.first(TABLE_OF_STARTING_INDICES.first)
129
+ @entry.should have(FIRST_ENTRY_NUMBER_OF_LINES).lines
130
+ @entry.lines.first.should == FIRST_ENTRY_FIRST_LINE
131
+ @entry.lines.last.should == FIRST_ENTRY_LAST_LINE
132
+ @psfile.should be_closed
133
+ end
134
+
135
+ it "should have a first entry having the correct lines, addresable through #entries" do
136
+ @entries = @psfile.entries.first
137
+ @entries.should have(FIRST_ENTRY_NUMBER_OF_LINES).lines
138
+ @entries.lines.first.should == FIRST_ENTRY_FIRST_LINE
139
+ @entries.lines.last.should == FIRST_ENTRY_LAST_LINE
140
+ end
141
+
142
+ it "should have a last entry having the correct lines, addressable through #first" do
143
+ @entry = @psfile.first(TABLE_OF_STARTING_INDICES.last)
144
+ @entry.should have(LAST_ENTRY_NUMBER_OF_LINES).lines
145
+ @entry.lines.first.should == LAST_ENTRY_FIRST_LINE
146
+ @entry.lines.last.should == LAST_ENTRY_LAST_LINE
147
+ end
148
+
149
+ it "should have a last entry having the correct lines, addressable through #entries" do
150
+ @entries = @psfile.entries.last
151
+ @entries.should have(LAST_ENTRY_NUMBER_OF_LINES).lines
152
+ @entries.lines.first.should == LAST_ENTRY_FIRST_LINE
153
+ @entries.lines.last.should == LAST_ENTRY_LAST_LINE
154
+ end
155
+
156
+ it "should be at the end of file after reading all the entries" do
157
+ @psfile.entries
158
+ @psfile.should be_eof
159
+ end
160
+ end