acpc_poker_types 5.0.3 → 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (63) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +1 -1
  3. data/acpc_poker_types.gemspec +3 -2
  4. data/lib/acpc_poker_types.rb +1 -1
  5. data/lib/acpc_poker_types/acpc_dealer_data/hand_results.rb +5 -5
  6. data/lib/acpc_poker_types/acpc_dealer_data/poker_match_data.rb +290 -323
  7. data/lib/acpc_poker_types/game_definition.rb +54 -37
  8. data/lib/acpc_poker_types/hand_player.rb +119 -0
  9. data/lib/acpc_poker_types/match_state.rb +323 -204
  10. data/lib/acpc_poker_types/player_group.rb +62 -0
  11. data/lib/acpc_poker_types/poker_action.rb +13 -6
  12. data/lib/acpc_poker_types/version.rb +1 -1
  13. data/spec/game_definition_spec.rb +23 -4
  14. data/spec/hand_player_spec.rb +254 -0
  15. data/spec/match_state_spec.rb +655 -112
  16. data/spec/player_group_spec.rb +57 -0
  17. data/spec/poker_match_data_spec.rb +24 -21
  18. data/spec/support/spec_helper.rb +2 -16
  19. metadata +68 -148
  20. data/lib/acpc_poker_types/player.rb +0 -182
  21. data/spec/coverage/assets/0.7.1/application.css +0 -1110
  22. data/spec/coverage/assets/0.7.1/application.js +0 -626
  23. data/spec/coverage/assets/0.7.1/fancybox/blank.gif +0 -0
  24. data/spec/coverage/assets/0.7.1/fancybox/fancy_close.png +0 -0
  25. data/spec/coverage/assets/0.7.1/fancybox/fancy_loading.png +0 -0
  26. data/spec/coverage/assets/0.7.1/fancybox/fancy_nav_left.png +0 -0
  27. data/spec/coverage/assets/0.7.1/fancybox/fancy_nav_right.png +0 -0
  28. data/spec/coverage/assets/0.7.1/fancybox/fancy_shadow_e.png +0 -0
  29. data/spec/coverage/assets/0.7.1/fancybox/fancy_shadow_n.png +0 -0
  30. data/spec/coverage/assets/0.7.1/fancybox/fancy_shadow_ne.png +0 -0
  31. data/spec/coverage/assets/0.7.1/fancybox/fancy_shadow_nw.png +0 -0
  32. data/spec/coverage/assets/0.7.1/fancybox/fancy_shadow_s.png +0 -0
  33. data/spec/coverage/assets/0.7.1/fancybox/fancy_shadow_se.png +0 -0
  34. data/spec/coverage/assets/0.7.1/fancybox/fancy_shadow_sw.png +0 -0
  35. data/spec/coverage/assets/0.7.1/fancybox/fancy_shadow_w.png +0 -0
  36. data/spec/coverage/assets/0.7.1/fancybox/fancy_title_left.png +0 -0
  37. data/spec/coverage/assets/0.7.1/fancybox/fancy_title_main.png +0 -0
  38. data/spec/coverage/assets/0.7.1/fancybox/fancy_title_over.png +0 -0
  39. data/spec/coverage/assets/0.7.1/fancybox/fancy_title_right.png +0 -0
  40. data/spec/coverage/assets/0.7.1/fancybox/fancybox-x.png +0 -0
  41. data/spec/coverage/assets/0.7.1/fancybox/fancybox-y.png +0 -0
  42. data/spec/coverage/assets/0.7.1/fancybox/fancybox.png +0 -0
  43. data/spec/coverage/assets/0.7.1/favicon_green.png +0 -0
  44. data/spec/coverage/assets/0.7.1/favicon_red.png +0 -0
  45. data/spec/coverage/assets/0.7.1/favicon_yellow.png +0 -0
  46. data/spec/coverage/assets/0.7.1/loading.gif +0 -0
  47. data/spec/coverage/assets/0.7.1/magnify.png +0 -0
  48. data/spec/coverage/assets/0.7.1/smoothness/images/ui-bg_flat_0_aaaaaa_40x100.png +0 -0
  49. data/spec/coverage/assets/0.7.1/smoothness/images/ui-bg_flat_75_ffffff_40x100.png +0 -0
  50. data/spec/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_55_fbf9ee_1x400.png +0 -0
  51. data/spec/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_65_ffffff_1x400.png +0 -0
  52. data/spec/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_75_dadada_1x400.png +0 -0
  53. data/spec/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_75_e6e6e6_1x400.png +0 -0
  54. data/spec/coverage/assets/0.7.1/smoothness/images/ui-bg_glass_95_fef1ec_1x400.png +0 -0
  55. data/spec/coverage/assets/0.7.1/smoothness/images/ui-bg_highlight-soft_75_cccccc_1x100.png +0 -0
  56. data/spec/coverage/assets/0.7.1/smoothness/images/ui-icons_222222_256x240.png +0 -0
  57. data/spec/coverage/assets/0.7.1/smoothness/images/ui-icons_2e83ff_256x240.png +0 -0
  58. data/spec/coverage/assets/0.7.1/smoothness/images/ui-icons_454545_256x240.png +0 -0
  59. data/spec/coverage/assets/0.7.1/smoothness/images/ui-icons_888888_256x240.png +0 -0
  60. data/spec/coverage/assets/0.7.1/smoothness/images/ui-icons_cd0a0a_256x240.png +0 -0
  61. data/spec/coverage/index.html +0 -72
  62. data/spec/match_state_perf.rb +0 -14
  63. data/spec/player_spec.rb +0 -372
@@ -1,5 +1,3 @@
1
- require 'set'
2
-
3
1
  require 'acpc_poker_types/chip_stack'
4
2
  require 'acpc_poker_types/suit'
5
3
  require 'acpc_poker_types/rank'
@@ -28,9 +26,6 @@ module AcpcPokerTypes
28
26
  # number_of_board_cards == [0, 3, 1, 1]
29
27
  attr_reader :number_of_board_cards
30
28
 
31
- # @return [Array] The minimum wager in each round.
32
- attr_reader :min_wagers
33
-
34
29
  # @return [Array] The position relative to the dealer that is first to act
35
30
  # in each round, indexed from 0.
36
31
  # @example The usual Texas hold'em sequence would look like this:
@@ -99,14 +94,6 @@ module AcpcPokerTypes
99
94
  :@number_of_board_cards => 'numBoardCards'
100
95
  }
101
96
 
102
- ALL_PLAYER_ALL_ROUND_DEFS = [
103
- DEFINITIONS[:@number_of_players],
104
- DEFINITIONS[:@number_of_rounds],
105
- DEFINITIONS[:@number_of_suits],
106
- DEFINITIONS[:@number_of_ranks],
107
- DEFINITIONS[:@number_of_hole_cards]
108
- ]
109
-
110
97
  def self.default_first_player_positions(number_of_rounds)
111
98
  number_of_rounds.to_i.times.inject([]) do |list, i|
112
99
  list << 0
@@ -128,6 +115,12 @@ module AcpcPokerTypes
128
115
  end
129
116
  end
130
117
 
118
+ # @param [Array] array The array to flatten into a single element.
119
+ # @return +array+ if +array+ has more than one element, the single element in +array+ otherwise.
120
+ def self.flatten_if_single_element_array(array)
121
+ if 1 == array.length then array[0] else array end
122
+ end
123
+
131
124
  # Checks if the given line is a comment beginning with '#' or ';', or empty.
132
125
  #
133
126
  # @param [String] line
@@ -136,7 +129,7 @@ module AcpcPokerTypes
136
129
  line.nil? || line.match(/^\s*[#;]/) || line.empty?
137
130
  end
138
131
 
139
- # Checks a given line frcom a game definition file for a game
132
+ # Checks a given line from a game definition file for a game
140
133
  # definition name and returns the given default value unless there is a match.
141
134
  #
142
135
  # @param [String, #match] line A line from a game definition file.
@@ -146,12 +139,11 @@ module AcpcPokerTypes
146
139
  # referred to by +definition_name+, +nil+ otherwise.
147
140
  def self.check_game_def_line_for_definition(line, definition_name)
148
141
  if line.match(/^\s*#{definition_name}\s*=\s*([\d\s]+)/i)
149
- value = $1.chomp.split(/\s+/).map{ |elem| elem.to_i }
150
- if ALL_PLAYER_ALL_ROUND_DEFS.include? definition_name
151
- value.shift
152
- else
153
- value
142
+ values = $1.chomp.split(/\s+/)
143
+ (0..values.length-1).each do |i|
144
+ values[i] = values[i].to_i
154
145
  end
146
+ flatten_if_single_element_array values
155
147
  end
156
148
  end
157
149
 
@@ -174,7 +166,12 @@ module AcpcPokerTypes
174
166
 
175
167
  def initialize(definitions)
176
168
  initialize_members!
177
- parse_definitions! definitions
169
+
170
+ if definitions.is_a?(Hash)
171
+ assign_definitions! definitions
172
+ else
173
+ parse_definitions! definitions
174
+ end
178
175
 
179
176
  @chip_stacks = AcpcPokerTypes::GameDefinition.default_chip_stacks(@number_of_players) if @chip_stacks.empty?
180
177
 
@@ -191,29 +188,38 @@ module AcpcPokerTypes
191
188
 
192
189
  alias_method :to_str, :to_s
193
190
 
191
+ def to_h
192
+ @hash ||= DEFINITIONS.keys.inject({betting_type: @betting_type}) do |h, instance_variable_symbol|
193
+ h[instance_variable_symbol[1..-1].to_sym] = instance_variable_get(instance_variable_symbol)
194
+ h
195
+ end
196
+ end
197
+
194
198
  def to_a
195
- list_of_lines = []
196
- list_of_lines << BETTING_TYPES[@betting_type] if @betting_type
197
- list_of_lines << "stack = #{@chip_stacks.join(' ')}" unless @chip_stacks.empty?
198
- list_of_lines << "numPlayers = #{@number_of_players}" if @number_of_players
199
- list_of_lines << "blind = #{@blinds.join(' ')}" unless @blinds.empty?
200
- list_of_lines << "raiseSize = #{min_wagers.join(' ')}" unless min_wagers.empty?
201
- list_of_lines << "numRounds = #{@number_of_rounds}" if @number_of_rounds
202
- list_of_lines << "firstPlayer = #{(@first_player_positions.map{|p| p + 1}).join(' ')}" unless @first_player_positions.empty?
203
- list_of_lines << "maxRaises = #{@max_number_of_wagers.join(' ')}" unless @max_number_of_wagers.empty?
204
- list_of_lines << "numSuits = #{@number_of_suits}" if @number_of_suits
205
- list_of_lines << "numRanks = #{@number_of_ranks}" if @number_of_ranks
206
- list_of_lines << "numHoleCards = #{@number_of_hole_cards}" if @number_of_hole_cards
207
- list_of_lines << "numBoardCards = #{@number_of_board_cards.join(' ')}" unless @number_of_board_cards.empty?
208
- list_of_lines
199
+ @array ||= -> do
200
+ list_of_lines = []
201
+ list_of_lines << BETTING_TYPES[@betting_type] if @betting_type
202
+ list_of_lines << "stack = #{@chip_stacks.join(' ')}" unless @chip_stacks.empty?
203
+ list_of_lines << "numPlayers = #{@number_of_players}" if @number_of_players
204
+ list_of_lines << "blind = #{@blinds.join(' ')}" unless @blinds.empty?
205
+ list_of_lines << "raiseSize = #{min_wagers.join(' ')}" unless min_wagers.empty?
206
+ list_of_lines << "numRounds = #{@number_of_rounds}" if @number_of_rounds
207
+ list_of_lines << "firstPlayer = #{(@first_player_positions.map{|p| p + 1}).join(' ')}" unless @first_player_positions.empty?
208
+ list_of_lines << "maxRaises = #{@max_number_of_wagers.join(' ')}" unless @max_number_of_wagers.empty?
209
+ list_of_lines << "numSuits = #{@number_of_suits}" if @number_of_suits
210
+ list_of_lines << "numRanks = #{@number_of_ranks}" if @number_of_ranks
211
+ list_of_lines << "numHoleCards = #{@number_of_hole_cards}" if @number_of_hole_cards
212
+ list_of_lines << "numBoardCards = #{@number_of_board_cards.join(' ')}" unless @number_of_board_cards.empty?
213
+ list_of_lines
214
+ end.call
209
215
  end
210
216
 
211
217
  def ==(other_game_definition)
212
- Set.new(to_a) == Set.new(other_game_definition.to_a)
218
+ to_h == other_game_definition.to_h
213
219
  end
214
220
 
215
221
  def min_wagers
216
- if @raise_sizes
222
+ @min_wagers ||= if @raise_sizes
217
223
  @raise_sizes
218
224
  else
219
225
  @number_of_rounds.times.map { |i| @blinds.max }
@@ -251,6 +257,12 @@ module AcpcPokerTypes
251
257
  end
252
258
  end
253
259
 
260
+ def assign_definitions!(definitions)
261
+ definitions.each do |key, value|
262
+ instance_variable_set("@#{key}", value)
263
+ end
264
+ end
265
+
254
266
  def parse_definitions!(definitions)
255
267
  definitions.each do |line|
256
268
  break if line.match(/\bend\s*gamedef\b/i)
@@ -308,7 +320,7 @@ module AcpcPokerTypes
308
320
  unless Seat.in_bounds?(@first_player_positions[i], @number_of_players)
309
321
  raise(
310
322
  ParseError,
311
- "Invalid first player #{@first_player_positions[i]} on round #{i+1}"
323
+ "Invalid first player #{@first_player_positions[i]} on round #{i+1} for #{@number_of_players} players"
312
324
  )
313
325
  end
314
326
  end
@@ -333,6 +345,11 @@ module AcpcPokerTypes
333
345
  end
334
346
 
335
347
  def adjust_definitions_if_necessary!
348
+ if @number_of_players < @chip_stacks.length
349
+ @number_of_players = @chip_stacks.length
350
+ elsif @number_of_players < @blinds.length
351
+ @number_of_players = @blinds.length
352
+ end
336
353
  @number_of_players.times do |i|
337
354
  @blinds << 0 unless @blinds.length > i
338
355
  @chip_stacks << DEFAULT_CHIP_STACK unless @chip_stacks.length > i
@@ -0,0 +1,119 @@
1
+ require 'acpc_poker_types/chip_stack'
2
+ require 'acpc_poker_types/hand'
3
+
4
+ require 'contextual_exceptions'
5
+ using ContextualExceptions::ClassRefinement
6
+
7
+ module AcpcPokerTypes
8
+
9
+ # Class to model a player during a hand from information in a +MatchState+
10
+ class HandPlayer
11
+ exceptions :unable_to_pay_ante, :inactive
12
+
13
+ # @return [AcpcPokerTypes::ChipStack] This player's chip stack at the beginning of the
14
+ # hand before paying their ante.
15
+ attr_reader :initial_stack
16
+
17
+ # @return [AcpcPokerTypes::ChipStack] The ante this player paid at the beginning of
18
+ # the hand.
19
+ attr_reader :ante
20
+
21
+ # @return [Hand] This player's hole cards or nil if this player is not
22
+ # holding cards.
23
+ attr_reader :hand
24
+
25
+ # @return [Array<PokerAction>] The actions this player has taken
26
+ attr_reader :actions
27
+
28
+ attr_accessor :winnings
29
+
30
+ # @param hand [Hand]
31
+ # @param initial_chip_stack [#to_i]
32
+ # @param ante [#to_i]
33
+ def initialize(hand, initial_stack, ante)
34
+ raise UnableToPayAnte if ante > initial_stack
35
+
36
+ @hand = hand
37
+ @initial_stack = ChipStack.new initial_stack
38
+ @ante = ChipStack.new ante
39
+ @actions = [[]]
40
+ @winnings = ChipStack.new 0
41
+ end
42
+
43
+ def stack
44
+ @initial_stack + balance
45
+ end
46
+
47
+ def balance
48
+ @winnings - total_contribution
49
+ end
50
+
51
+ def contributions
52
+ @actions.map do |actions_per_round|
53
+ actions_per_round.inject(0) { |sum, action| sum += action.cost }
54
+ end.unshift @ante
55
+ end
56
+
57
+ def total_contribution
58
+ @ante + @actions.flatten.inject(0) { |sum, action| sum += action.cost }
59
+ end
60
+
61
+ # @param amount_to_call [#to_r] The amount to call for this player
62
+ # @param wager_illegal [Boolean]
63
+ # @return [Array<PokerAction>] The list of legal actions for this player. If a wager is legal,
64
+ # the largest possible wager will be returned in the list.
65
+ def legal_actions(
66
+ round,
67
+ amount_to_call: ChipStack.new(0),
68
+ wager_illegal: false
69
+ )
70
+ l_actions = []
71
+ return l_actions if inactive?
72
+
73
+ if amount_to_call.to_r > 0
74
+ l_actions << PokerAction.new(PokerAction::CALL) << PokerAction.new(PokerAction::FOLD)
75
+ else
76
+ l_actions << PokerAction.new(PokerAction::CHECK)
77
+ end
78
+ if !wager_illegal && stack > amount_to_call.to_r
79
+ l_actions << if contributions[round] > 0 || amount_to_call.to_r > 0
80
+ PokerAction.new(PokerAction::RAISE, cost: stack - amount_to_call.to_r)
81
+ else
82
+ PokerAction.new(PokerAction::BET, cost: stack - amount_to_call.to_r)
83
+ end
84
+ end
85
+
86
+ l_actions
87
+ end
88
+
89
+ # @param round [Integer] The round in which the largest wager by size is desired.
90
+ # defaults to +nil+.
91
+ # @return [ChipStack] The largest wager by size this player has made.
92
+ # Checks only in the specified +round+ or over the entire hand if round is +nil+.
93
+ def largest_wager_by(round=nil)
94
+ # @todo
95
+ end
96
+
97
+ def append_action!(action, round = @actions.length - 1)
98
+ raise Inactive if inactive?
99
+
100
+ @actions[round] ||= []
101
+ @actions[round] << action
102
+
103
+ self
104
+ end
105
+
106
+ def inactive?
107
+ folded? || all_in?
108
+ end
109
+
110
+ def all_in?
111
+ stack <= 0
112
+ end
113
+
114
+ def folded?
115
+ @actions.flatten.last == PokerAction::FOLD
116
+ end
117
+ end
118
+
119
+ end
@@ -3,263 +3,382 @@ require 'acpc_poker_types/hand'
3
3
  require 'acpc_poker_types/rank'
4
4
  require 'acpc_poker_types/suit'
5
5
  require 'acpc_poker_types/poker_action'
6
+ require 'acpc_poker_types/hand_player'
7
+ require 'acpc_poker_types/player_group'
6
8
 
7
- require 'contextual_exceptions'
8
- using ContextualExceptions::ClassRefinement
9
+ module AcpcPokerTypes
10
+ module Indices
11
+ refine Array do
12
+ def inject_with_index(init)
13
+ i = 0
14
+ inject(init) do |accum, elem|
15
+ yield accum, elem, i if block_given?
16
+ i += 1
17
+ accum
18
+ end
19
+ end
20
+ def indices(elem_to_find=nil)
21
+ if elem_to_find
22
+ indices { |elem| elem == elem_to_find }
23
+ else
24
+ inject_with_index([]) do |array, elem, i|
25
+ array << i if yield elem
26
+ array
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ using AcpcPokerTypes::Indices
9
34
 
10
- # Model to parse and manage information from a given match state string.
11
35
  module AcpcPokerTypes
12
- class MatchState
13
- exceptions :incomplete_match_state
14
36
 
15
- # @return [Integer] The position relative to the dealer of the player that
16
- # received the match state string, indexed from 0, modulo the
17
- # number of players.
18
- # @example The player immediately to the left of the dealer has
19
- # +position_relative_to_dealer+ == 0
20
- # @example The dealer has
21
- # +position_relative_to_dealer+ == <number of players> - 1
22
- attr_reader :position_relative_to_dealer
37
+ # Model to parse and manage information from a given match state string.
38
+ class MatchState
39
+ # @return [Integer] The position relative to the dealer of the player that
40
+ # received the match state string, indexed from 0, modulo the
41
+ # number of players.
42
+ # @example The player immediately to the left of the dealer has
43
+ # +position_relative_to_dealer+ == 0
44
+ # @example The dealer has
45
+ # +position_relative_to_dealer+ == <number of players> - 1
46
+ attr_reader :position_relative_to_dealer
23
47
 
24
- # @return [Integer] The hand number.
25
- attr_reader :hand_number
48
+ # @return [Integer] The hand number.
49
+ attr_reader :hand_number
26
50
 
27
- # @return [Array<Array<AcpcPokerTypes::PokerAction>>] The sequence of betting actions.
28
- attr_reader :betting_sequence
51
+ # @return [String] The ACPC string created by the given betting sequence.
52
+ attr_reader :betting_sequence_string
29
53
 
30
- # @return [Array<AcpcPokerTypes::Hand>] The list of visible hole card sets for each player.
31
- attr_reader :list_of_hole_card_hands
54
+ # @todo Move this @return [Array<Hand>] The list of visible hole card sets for each player.
32
55
 
33
- # @return [AcpcPokerTypes::BoardCards] All visible community cards on the board.
34
- attr_reader :board_cards
56
+ attr_reader :hands_string
35
57
 
36
- # @return [Array<Integer>] The list of first seats for each round.
37
- attr_reader :first_seat_in_each_round
58
+ # @todo Move this @return [BoardCards] All visible community cards on the board.
38
59
 
39
- # @return [String] Label for match state strings.
40
- LABEL = 'MATCHSTATE'
60
+ attr_reader :community_cards_string
41
61
 
42
- class << self; alias_method(:parse, :new) end
62
+ # @return [String] Label for match state strings.
63
+ LABEL = 'MATCHSTATE'
43
64
 
44
- # Receives a match state string from the given +connection+.
45
- # @param [#gets] connection The connection from which a match state string should be received.
46
- # @return [MatchState] The match state string that was received from the +connection+ or +nil+ if none could be received.
47
- def self.receive(connection)
48
- AcpcPokerTypes::MatchState.parse connection.gets
49
- end
65
+ COMMUNITY_CARD_SEPARATOR = '/'
66
+ BETTING_SEQUENCE_SEPARATOR = COMMUNITY_CARD_SEPARATOR
67
+ HAND_SEPARATOR = '|'
68
+
69
+ class << self; alias_method(:parse, :new) end
70
+
71
+ # Receives a match state string from the given +connection+.
72
+ # @param [#gets] connection The connection from which a match state string should be received.
73
+ # @return [MatchState] The match state string that was received from the +connection+ or +nil+ if none could be received.
74
+ def self.receive(connection)
75
+ MatchState.parse connection.gets
76
+ end
50
77
 
51
- # Builds a match state string from its given component parts.
52
- #
53
- # @param [#to_s] position_relative_to_dealer The position relative to the dealer.
54
- # @param [#to_s] hand_number The hand number.
55
- # @param [#to_s] betting_sequence The betting sequence.
56
- # @param [#to_s] all_hole_cards All the hole cards visible.
57
- # @param [#to_acpc, #empty?] board_cards All the community cards on the board.
58
- # @return [String] The constructed match state string.
59
- def self.build_match_state_string(
60
- position_relative_to_dealer,
61
- hand_number,
62
- betting_sequence,
63
- all_hole_cards,
64
- board_cards
78
+ # Builds a match state string from its given component parts.
79
+ #
80
+ # @param [#to_s] position_relative_to_dealer The position relative to the dealer.
81
+ # @param [#to_s] hand_number The hand number.
82
+ # @param [#to_s] betting_sequence The betting sequence.
83
+ # @param [#to_s] all_hole_cards All the hole cards visible.
84
+ # @param [#to_s, #empty?] board_cards All the community cards on the board.
85
+ # @return [String] The constructed match state string.
86
+ def self.build_match_state_string(
87
+ position_relative_to_dealer,
88
+ hand_number,
89
+ betting_sequence,
90
+ all_hole_cards,
91
+ board_cards
92
+ )
93
+ string = "#{LABEL}:#{position_relative_to_dealer}:#{hand_number}:#{betting_sequence}:#{all_hole_cards}"
94
+ string << "/#{board_cards.to_s}" if board_cards && !board_cards.empty?
95
+ string
96
+ end
97
+
98
+ # @param [String] raw_match_state A raw match state string to be parsed.
99
+ # @raise IncompleteMatchState
100
+ def initialize(raw_match_state)
101
+ if raw_match_state.match(
102
+ /#{LABEL}:(\d+):(\d+):([^:]*):([^#{COMMUNITY_CARD_SEPARATOR}]+)#{COMMUNITY_CARD_SEPARATOR}*([^\s:]*)/
65
103
  )
66
- string = "#{LABEL}:#{position_relative_to_dealer}:#{hand_number}:#{betting_sequence}:#{all_hole_cards}"
67
- string << board_cards.to_acpc if board_cards && !board_cards.empty?
68
- string
104
+ @position_relative_to_dealer = $1.to_i
105
+ @hand_number = $2.to_i
106
+ @betting_sequence_string = $3
107
+ @hands_string = $4
108
+ @community_cards_string = $5
69
109
  end
110
+ end
70
111
 
71
- # Checks if the given line is a comment beginning with '#' or ';', or empty.
72
- #
73
- # @param [String] line
74
- # @return [Boolean] True if +line+ is a comment or empty, false otherwise.
75
- def self.line_is_comment_or_empty?(line)
76
- line.nil? || line.match(/^\s*[#;]/) || line.empty?
77
- end
112
+ # @return [String] The MatchState in raw text form.
113
+ def to_str
114
+ @str ||= MatchState.build_match_state_string(
115
+ @position_relative_to_dealer,
116
+ @hand_number,
117
+ @betting_sequence_string,
118
+ @hands_string,
119
+ @community_cards_string
120
+ )
121
+ end
78
122
 
79
- # @param [String] raw_match_state A raw match state string to be parsed.
80
- # @raise IncompleteMatchState
81
- def initialize(raw_match_state)
82
- raise IncompleteMatchState, raw_match_state if AcpcPokerTypes::MatchState.line_is_comment_or_empty? raw_match_state
83
-
84
- if raw_match_state.match(
85
- /#{LABEL}:(\d+):(\d+):([\d#{AcpcPokerTypes::PokerAction::CONCATONATED_ACTIONS}\/]*):([|#{AcpcPokerTypes::Card::CONCATONATED_CARDS}]+)\/*([\/#{AcpcPokerTypes::Card::CONCATONATED_CARDS}]*)/)
86
- @position_relative_to_dealer = $1.to_i
87
- @hand_number = $2.to_i
88
- @betting_sequence = parse_betting_sequence $3
89
- @list_of_hole_card_hands = parse_list_of_hole_card_hands $4
90
- @board_cards = parse_board_cards $5
91
- end
123
+ # @see to_str
124
+ alias_method :to_s, :to_str
92
125
 
93
- raise IncompleteMatchState, raw_match_state if incomplete_match_state?
94
- end
126
+ # @param [MatchState] another_match_state A match state string to compare against this one.
127
+ # @return [Boolean] +true+ if this match state string is equivalent to +another_match_state+, +false+ otherwise.
128
+ def ==(another_match_state)
129
+ another_match_state.to_s == to_s
130
+ end
95
131
 
96
- # @return [String] The AcpcPokerTypes::MatchState in raw text form.
97
- def to_str
98
- AcpcPokerTypes::MatchState.build_match_state_string(
99
- @position_relative_to_dealer,
100
- @hand_number, betting_sequence_string,
101
- hole_card_strings,
102
- @board_cards
103
- )
104
- end
132
+ def all_hands
133
+ @all_hands ||= -> do
134
+ lcl_hole_card_hands = all_string_hands(@hands_string).map do |string_hand|
135
+ Hand.from_acpc string_hand
136
+ end
137
+ while lcl_hole_card_hands.length < number_of_players
138
+ lcl_hole_card_hands.push Hand.new
139
+ end
140
+ lcl_hole_card_hands
141
+ end.call
142
+ end
105
143
 
106
- # @see to_str
107
- alias_method :to_s, :to_str
144
+ # @return [Array<Array<PokerAction>>] The sequence of betting actions.
145
+ def betting_sequence
146
+ @betting_sequence ||= if @betting_sequence_string.empty?
147
+ [[]]
148
+ else
149
+ lcl_betting_sequence = @betting_sequence_string.split(
150
+ BETTING_SEQUENCE_SEPARATOR
151
+ ).map do |betting_string_per_round|
152
+ actions_from_acpc_characters(betting_string_per_round).map do |action|
153
+ PokerAction.new(action)
154
+ end
155
+ end
108
156
 
109
- # @param [AcpcPokerTypes::MatchState] another_match_state A match state string to compare against this one.
110
- # @return [Boolean] +true+ if this match state string is equivalent to +another_match_state+, +false+ otherwise.
111
- def ==(another_match_state)
112
- another_match_state.to_s == to_s
157
+ # Adjust the number of rounds if the last action was the last action in the round
158
+ lcl_betting_sequence << [] if first_state_of_round?
159
+ lcl_betting_sequence
113
160
  end
161
+ end
114
162
 
115
- # @return [Integer] The number of players in this match.
116
- def number_of_players() @list_of_hole_card_hands.length end
117
-
118
- # @param [Array<Array<AcpcPokerTypes::PokerAction>>] betting_sequence The betting sequence from which the last action should be retrieved.
119
- # @return [AcpcPokerTypes::PokerAction] The last action taken.
120
- # @raise NoActionsHaveBeenTaken if no actions have been taken.
121
- def last_action(betting_sequence=@betting_sequence)
122
- if betting_sequence.nil? || betting_sequence.empty?
123
- nil
124
- elsif betting_sequence.last.last
125
- betting_sequence.last.last
126
- else
127
- last_action(betting_sequence.reject{ |elem| elem.equal?(betting_sequence.last) })
163
+ def community_cards
164
+ @community_cards ||= -> do
165
+ lcl_community_cards = BoardCards.new(
166
+ all_sets_of_community_cards(@community_cards_string).map do |cards_in_round|
167
+ Card.cards(cards_in_round)
168
+ end
169
+ )
170
+ if lcl_community_cards.round < @community_cards_string.count(COMMUNITY_CARD_SEPARATOR)
171
+ lcl_community_cards.next_round!
128
172
  end
129
- end
173
+ lcl_community_cards
174
+ end.call
175
+ end
130
176
 
131
- # @return [AcpcPokerTypes::Hand] The user's hole cards.
132
- # @example An ace of diamonds and a 4 of clubs is represented as
133
- # 'Ad4c'
134
- def users_hole_cards
135
- @list_of_hole_card_hands[@position_relative_to_dealer]
136
- end
177
+ # @return [Integer] The zero indexed current round number.
178
+ def round
179
+ @round ||= @betting_sequence_string.count BETTING_SEQUENCE_SEPARATOR
180
+ end
137
181
 
138
- # @return [Array] The list of opponent hole cards that are visible.
139
- # @example If there are two opponents, one with AhKs and the other with QdJc, then
140
- # list_of_opponents_hole_cards == [AhKs:AcpcPokerTypes::Hand, QdJc:AcpcPokerTypes::Hand]
141
- def list_of_opponents_hole_cards
142
- local_list_of_hole_card_hands = @list_of_hole_card_hands.dup
143
- local_list_of_hole_card_hands.delete_at @position_relative_to_dealer
144
- local_list_of_hole_card_hands
145
- end
182
+ # @return [Hand] The user's hand.
183
+ # @example An ace of diamonds and a 4 of clubs is represented as
184
+ # 'Ad4c'
185
+ def hand
186
+ @hand ||= all_hands[@position_relative_to_dealer]
187
+ end
146
188
 
147
- # @return [Integer] The zero indexed current round number.
148
- def round
149
- @betting_sequence.length - 1
150
- end
189
+ # @return [Array] The list of opponent hole card hands.
190
+ # @example If there are two opponents, one with AhKs and the other with QdJc, then
191
+ # list_of_opponents_hole_cards == [AhKs:Hand, QdJc:Hand]
192
+ def opponent_hands
193
+ @opponent_hands ||= -> do
194
+ hands = all_hands.dup
195
+ hands.delete_at @position_relative_to_dealer
196
+ hands
197
+ end.call
198
+ end
151
199
 
152
- # @return [Integer] The number of actions in the current round.
153
- def number_of_actions_this_round() @betting_sequence[round].length end
200
+ # @return [Boolean] Reports whether or not it is the first state of the first round.
201
+ def first_state_of_first_round?
202
+ @first_state_of_first_round ||= @betting_sequence_string.empty?
203
+ end
154
204
 
155
- # @return [Integer] The number of actions in the current hand.
156
- def number_of_actions_this_hand
157
- @betting_sequence.inject(0) do |sum, sequence_per_round|
158
- sum += sequence_per_round.length
159
- end
160
- end
205
+ def first_state_of_round?
206
+ @first_state_of_round ||= @betting_sequence_string[-1] == BETTING_SEQUENCE_SEPARATOR
207
+ end
161
208
 
162
- # @param [Array<Action>] betting_sequence=@betting_sequence The sequence of
163
- # actions to link into an ACPC string.
164
- # @return [String] The ACPC string created by the given betting sequence,
165
- # +betting_sequence+.
166
- def betting_sequence_string(betting_sequence=@betting_sequence)
167
- (round + 1).times.inject('') do |string, i|
168
- string << (betting_sequence[i].map { |action| action.to_s }).join('')
169
- string << '/' unless i == round
170
- string
171
- end
172
- end
209
+ # @return [Integer] The number of players in this match.
210
+ def number_of_players
211
+ @number_of_players ||= @hands_string.count(HAND_SEPARATOR) + 1
212
+ end
173
213
 
174
- # @return [Boolean] Reports whether or not it is the first state of the first round.
175
- def first_state_of_first_round?
176
- (0 == number_of_actions_this_hand)
214
+ # @return [PokerAction] The last action taken.
215
+ def last_action
216
+ @last_action ||= if @betting_sequence_string.match(
217
+ /([^#{BETTING_SEQUENCE_SEPARATOR}])#{BETTING_SEQUENCE_SEPARATOR}*$/
218
+ )
219
+ PokerAction.new($1)
220
+ else
221
+ nil
177
222
  end
223
+ end
224
+
225
+ # @return [Integer] The number of actions in the current round.
226
+ def number_of_actions_this_round
227
+ @number_of_actions_this_round ||= betting_sequence[round].length
228
+ end
178
229
 
179
- def player_position_relative_to_self
180
- number_of_players - 1
230
+ # @return [Integer] The number of actions in the current hand.
231
+ def number_of_actions_this_hand
232
+ @number_of_actions_this_hand ||= betting_sequence.inject(0) do |sum, sequence_per_round|
233
+ sum += sequence_per_round.length
181
234
  end
235
+ end
182
236
 
183
- def round_in_which_last_action_taken
184
- unless number_of_actions_this_hand > 0
185
- nil
237
+ def round_in_which_last_action_taken
238
+ @round_in_which_last_action_taken ||= if first_state_of_first_round?
239
+ nil
240
+ else
241
+ if @betting_sequence_string[-1] == BETTING_SEQUENCE_SEPARATOR
242
+ round - 1
186
243
  else
187
- if number_of_actions_this_round < 1
188
- round - 1
189
- else
190
- round
191
- end
244
+ round
192
245
  end
193
246
  end
247
+ end
248
+
249
+ # @param stacks [Array<#to_f>]
250
+ # @param blinds [Array<#to_f>]
251
+ # @return [PlayerGroup] The state of the players in this hand at the
252
+ # when the hand began.
253
+ def players_at_hand_start(stacks, blinds)
254
+ PlayerGroup.new all_hands, stacks, blinds
255
+ end
194
256
 
195
- private
196
-
197
- def validate_first_seats(list_of_first_seats)
198
- begin
199
- raise UnknownFirstSeat, round unless round < list_of_first_seats.length
200
- all_seats_are_occupied = ((problem_seat = list_of_first_seats.max) < number_of_players) && ((problem_seat = list_of_first_seats.min.abs)-1 < number_of_players)
201
- raise FirstSeatIsUnoccupied, problem_seat unless all_seats_are_occupied
202
- rescue UnknownFirstSeat => e
203
- raise e
204
- rescue FirstSeatIsUnoccupied => e
205
- raise e
206
- rescue => e
207
- raise UnknownFirstSeat, e.message
257
+ # @param game_def [GameDefinition]
258
+ # @return [PlayerGroup] The current state of the players.
259
+ def every_action(game_def)
260
+ @players = players_at_hand_start game_def.chip_stacks, game_def.blinds
261
+
262
+ last_round = -1
263
+ acting_player_position = nil
264
+ @player_acting_sequence = []
265
+ every_action_token do |action, round|
266
+ if round != last_round
267
+ acting_player_position = game_def.first_player_positions[round]
268
+ @player_acting_sequence << []
269
+ last_round = round
208
270
  end
209
- list_of_first_seats
210
- end
211
271
 
212
- def list_of_actions_from_acpc_characters(betting_sequence)
213
- betting_sequence.scan(/[#{AcpcPokerTypes::PokerAction::CONCATONATED_ACTIONS}]\d*/)
214
- end
272
+ acting_player_position = @players.position_of_first_active_player(
273
+ acting_player_position
274
+ )
215
275
 
216
- def incomplete_match_state?
217
- !(@position_relative_to_dealer and @hand_number and
218
- @list_of_hole_card_hands) || @list_of_hole_card_hands.empty?
219
- end
276
+ @player_acting_sequence.last << acting_player_position
220
277
 
221
- def parse_list_of_hole_card_hands(string_of_hole_cards)
222
- list_of_hole_card_hands = every_set_of_cards(string_of_hole_cards, '\|').map do |string_hand|
223
- AcpcPokerTypes::Hand.from_acpc string_hand
224
- end
225
- while list_of_hole_card_hands.length < (string_of_hole_cards.count('|') + 1)
226
- list_of_hole_card_hands.push AcpcPokerTypes::Hand.new
227
- end
228
- list_of_hole_card_hands
278
+ cost = @players.action_cost(
279
+ acting_player_position,
280
+ action,
281
+ game_def.min_wagers[round]
282
+ )
283
+
284
+ @players[acting_player_position].append_action!(
285
+ if cost > 0
286
+ action = PokerAction.new(action.to_s, cost: cost)
287
+ else
288
+ action
289
+ end,
290
+ round
291
+ )
292
+
293
+ yield action, round, acting_player_position, @players if block_given?
294
+
295
+ acting_player_position = @players.next_player_position(
296
+ acting_player_position
297
+ )
229
298
  end
230
299
 
231
- def parse_betting_sequence(string_betting_sequence)
232
- return [[]] if string_betting_sequence.empty?
300
+ distribute_chips!(game_def) if hand_ended?(game_def)
301
+
302
+ @players
303
+ end
304
+
305
+ def players(game_def)
306
+ @players ||= every_action(game_def)
307
+ end
308
+
309
+ def player_acting_sequence(game_def)
310
+ every_action(game_def) unless @player_acting_sequence
311
+
312
+ @player_acting_sequence
313
+ end
233
314
 
234
- betting_sequence = string_betting_sequence.split(/\//).map do |betting_string_per_round|
235
- list_of_actions_in_a_particular_round = list_of_actions_from_acpc_characters(
236
- betting_string_per_round
237
- ).map do |action|
238
- AcpcPokerTypes::PokerAction.new(action)
315
+ # @return [Boolean] +true+ if the hand has ended, +false+ otherwise.
316
+ def hand_ended?(game_def)
317
+ @hand_ended ||= reached_showdown? || players(game_def).count { |player| player.inactive? } >= number_of_players - 1
318
+ end
319
+
320
+ def reached_showdown?
321
+ opponents_cards_visible?
322
+ end
323
+
324
+ def opponents_cards_visible?
325
+ @opponents_cards_visible ||= all_hands.count { |h| !h.empty? } > 1 # At least one opponent hand visible
326
+ end
327
+
328
+ def pot(game_def)
329
+ @pot ||= players(game_def).map { |player| player.contributions }.flatten.inject(:+)
330
+ end
331
+
332
+ private
333
+
334
+ # Distribute chips to all winning players
335
+ def distribute_chips!(game_def)
336
+ return self if pot(game_def) <= 0
337
+
338
+ # @todo This only works for Doyle's game where there are no side-pots.
339
+ if 1 == players(game_def).count { |player| !player.folded? }
340
+ players(game_def).select { |player| !player.folded? }.first.winnings = pot(game_def)
341
+ else
342
+ hand_strengths = players(game_def).map do |player|
343
+ if player.folded?
344
+ -1
345
+ else
346
+ PileOfCards.new(community_cards.flatten + player.hand).to_poker_hand_strength
239
347
  end
240
348
  end
349
+ winning_players = hand_strengths.indices(hand_strengths.max)
350
+ amount_each_player_wins = pot(game_def)/winning_players.length.to_r
241
351
 
242
- # Adjust the number of rounds if the last action was the last action in the round
243
- betting_sequence << [] if string_betting_sequence.match(/\/$/)
244
- betting_sequence
352
+ winning_players.each do |player_index|
353
+ @players[player_index].winnings = amount_each_player_wins
354
+ end
245
355
  end
246
356
 
247
- def parse_board_cards(string_board_cards)
248
- board_cards = AcpcPokerTypes::BoardCards.new(
249
- every_set_of_cards(string_board_cards, '\/').map do |cards_in_round|
250
- AcpcPokerTypes::Card.cards(cards_in_round)
251
- end
252
- )
253
- board_cards.next_round! if board_cards.round < string_board_cards.count('/')
254
- board_cards
255
- end
357
+ self
358
+ end
256
359
 
257
- def every_set_of_cards(string_of_card_sets, divider)
258
- string_of_card_sets.split(/#{divider}/)
360
+ def every_action_token
361
+ betting_sequence.each_with_index do |actions_per_round, round|
362
+ actions_per_round.each do |action|
363
+ yield action, round if block_given?
364
+ end
259
365
  end
366
+ end
260
367
 
261
- def hole_card_strings
262
- (@list_of_hole_card_hands.map { |hand| hand.to_acpc }).join '|'
263
- end
368
+ def all_string_hands(string_of_card_sets)
369
+ all_sets_of_cards(string_of_card_sets, HAND_SEPARATOR)
370
+ end
371
+
372
+ def all_sets_of_community_cards(string_of_card_sets)
373
+ all_sets_of_cards(string_of_card_sets, COMMUNITY_CARD_SEPARATOR)
374
+ end
375
+
376
+ def all_sets_of_cards(string_of_card_sets, divider)
377
+ string_of_card_sets.split(divider)
378
+ end
379
+
380
+ def actions_from_acpc_characters(action_sequence)
381
+ action_sequence.scan(/[^#{BETTING_SEQUENCE_SEPARATOR}\d]\d*/)
264
382
  end
383
+ end
265
384
  end