pokerstats 2.0.1

Sign up to get free protection for your applications and to get access to all the features.
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