hlockey 6 → 7

Sign up to get free protection for your applications and to get access to all the features.
data/lib/hlockey/game.rb CHANGED
@@ -14,7 +14,7 @@ module Hlockey
14
14
  # @return [Team] the teams in the game
15
15
  attr_reader(:home, :away)
16
16
 
17
- # @return [Random] should be the same as the league's
17
+ # @return [Utils::Rng] the RNG the game is getting numbers from
18
18
  attr_reader(:prng)
19
19
 
20
20
  # @return [Hash<Symbol => Integer>] the score of each team
@@ -31,6 +31,7 @@ module Hlockey
31
31
 
32
32
  # @return [Boolean] if the game is still in progress
33
33
  attr_reader(:in_progress)
34
+ alias in_progress? in_progress
34
35
 
35
36
  # @return [Integer] what period the game is in
36
37
  attr_reader(:period)
@@ -41,26 +42,28 @@ module Hlockey
41
42
  # @return [Hash<Player => Hash<Symbol => Number>>] stats of players before changes
42
43
  attr_reader(:pre_tmp_change_stats)
43
44
 
44
- alias in_progress? in_progress
45
-
46
45
  # @param home [Team]
47
46
  # @param away [Team]
48
- # @param prng [Random] should be League#prng
47
+ # @param rng_seed [Integer] number to seed the game's RNG with
49
48
  # @param weather [Class<Weatherable>]
50
- def initialize(home, away, prng, weather = nil)
49
+ def initialize(home, away, rng_seed, weather = nil)
51
50
  @home = home
52
51
  @away = away
53
- @prng = prng
52
+ @prng = Utils::Rng.new(rng_seed)
54
53
 
55
54
  @stadium = @home.stadium
56
55
 
57
- weather ||= Weather::WEATHERS.sample(random: prng)
56
+ if weather.nil?
57
+ weather = Weather::WEATHERS.sample(random: prng)
58
+ @rng_seed = @prng.state
59
+ end
60
+
58
61
  @weather = weather.new(self)
59
62
  @score = { home: 0, away: 0 }
60
63
  @in_progress = true
61
64
  @actions = 0
62
65
  @period = 1
63
- @stream = [Message.StartOfGame]
66
+ @stream = [Message.new(:game_start)]
64
67
  @faceoff = true
65
68
  @faceoff_won = false
66
69
  @team_with_puck = nil
@@ -71,55 +74,138 @@ module Hlockey
71
74
  @fight_chance = 0
72
75
  @pre_tmp_change_stats = {}
73
76
  @partying_teams = [@home, @away].select { _1.status == :partying }
77
+ @do_final_cleanup = false
74
78
 
79
+ set_mods_game
75
80
  @weather.on_game_start
76
81
  end
77
82
 
78
83
  # @return [Hash]
79
- def to_h = {
80
- home: @home.name,
81
- away: @away.name,
82
- stadium: @stadium.to_s,
83
- weather: @weather.to_s,
84
- score: @score,
85
- in_progress?: @in_progress,
86
- period: @period,
87
- stream: @stream.map { _1.to_s(do_color: false) }
88
- }
84
+ def to_h(simple: false, stream_start_idx: 0, show_events: true, do_color: false)
85
+ res = {
86
+ home: @home.name,
87
+ away: @away.name,
88
+ stadium: @stadium.to_s,
89
+ weather: @weather.to_s,
90
+ score: @score,
91
+ in_progress: @in_progress,
92
+ period: @period
93
+ }
94
+ unless simple
95
+ if show_events
96
+ res[:events] = @stream[stream_start_idx..]&.map(&:event)
97
+ else
98
+ res[:stream] = @stream[stream_start_idx..]&.map { _1.to_s(do_color:) }
99
+ end
100
+
101
+ res[:team_with_puck] = @team_with_puck&.name
102
+ res[:shooting_chance] = @shooting_chance
103
+ end
104
+
105
+ res
106
+ end
89
107
 
90
108
  # @return [String]
91
- def to_s = "#{Message.color(@home)} vs #{Message.color(@away)}"
109
+ def to_s(do_color: true) =
110
+ "#{Message.color(@home, do_color:)} vs #{Message.color(@away, do_color:)}"
92
111
 
93
112
  # Update the game state by one action
94
113
  def update
95
- return unless @in_progress
114
+ if @do_final_cleanup
115
+ @in_progress = false
116
+ set_mods_game(unset: true)
117
+ return
118
+ end
96
119
 
97
120
  do_action
98
121
  handle_parties
99
122
  end
100
123
 
124
+ # Makes a player pass in the game, regardless of if it would normally occur
125
+ def pass
126
+ sender = puck_holder
127
+ receiver_pos = pass_reciever_pos
128
+ interceptor_pos = defensive_pos
129
+
130
+ @stream << Message.new(:game_pass,
131
+ sender:,
132
+ receiver: @team_with_puck.roster[receiver_pos])
133
+
134
+ if !@faceoff && try_take_puck(interceptor_pos, dis: 10 - @shooting_chance)
135
+ @stream << Message.new(:game_pass_intercept,
136
+ sender:,
137
+ receiver: @puckless_team.roster[receiver_pos],
138
+ interceptor: @team_with_puck.roster[interceptor_pos])
139
+ return
140
+ end
141
+
142
+ @puck_holder_pos = receiver_pos
143
+ @shooting_chance += 1
144
+ end
145
+
146
+ # Makes a player check in the game, regardless of if it would normally occur
147
+ def check
148
+ defender_pos = defensive_pos
149
+ defender = @puckless_team.roster[defender_pos]
150
+
151
+ @stream << Message.new(:game_hit, defender:, puck_holder:)
152
+ if !puck_holder.mods_do(:on_got_hit, defender) &&
153
+ try_take_puck(defender_pos, stat: :defense)
154
+ @stream << Message.new(:game_possession_change,
155
+ player: defender, new_puck_team: defender.team)
156
+ end
157
+
158
+ @fight_chance += 0.1
159
+ end
160
+
161
+ # Makes a player shoot in the game, regardless of if it would normally occur
162
+ # @param audacity [Boolean] if the function was called by Audacity weather
163
+ def shoot(audacity: false)
164
+ return if @puck_holder_pos.nil?
165
+
166
+ shooter = puck_holder
167
+
168
+ @stream << Message.new(audacity ? :game_shot_audacious : :game_shot, shooter:)
169
+
170
+ blocking_pos = @prng.rand(2).zero? ? :ldef : :rdef
171
+ return if try_block_shot(blocking_pos) || try_block_shot(:goalie)
172
+
173
+ # Goal scored
174
+
175
+ scoring_team = @team_with_puck == @home ? :home : :away
176
+ @score[scoring_team] += 1
177
+ weather.on_goal(scoring_team)
178
+
179
+ @stream << Message.new(:game_shot_scored, shooter:)
180
+
181
+ start_fight if @prng.rand(3 + @fight_chance) > 3
182
+
183
+ start_faceoff
184
+ @actions = ACTIONS_PER_PERIOD - 1 if @period > NON_OT_PERIODS # Sudden death OT
185
+ end
186
+
101
187
  # Starts a fight in the game
102
188
  # @param starting_player [Player, nil] the player that started the fight (optional)
103
189
  def start_fight(starting_player = nil)
104
190
  @fight = Fight.new(self, starting_player)
105
- @stream << Message.FightStarted(@fight.players[:home].first,
106
- @fight.players[:away].first)
191
+ @stream << Message.new(:fight_start,
192
+ home_player: @fight.players[:home].first,
193
+ away_player: @fight.players[:away].first)
107
194
  end
108
195
 
109
196
  private
110
197
 
111
198
  # Does an action in the game
112
199
  def do_action
113
- @stream << Message.StartOfPeriod(@period) if @actions.zero?
200
+ @stream << Message.new(:game_period_start, period: @period) if @actions.zero?
114
201
  @actions += 1
115
202
 
116
203
  @weather.on_action
117
- (@home.roster.values + @away.roster.values).each { _1.mods_do(:on_action, self) }
204
+ (@home.roster.values + @away.roster.values).each { _1.mods_do(:on_action) }
118
205
 
119
206
  unless @fight.nil?
120
- fight_message = @fight.next_action
121
- @stream << fight_message
122
- return unless fight_message.event == :FightEnded
207
+ @fight.next_action
208
+ return unless @stream.last.event == :fight_end
123
209
 
124
210
  morale_change_amount = (@fight.score[:home] - @fight.score[:away]) / 10.0
125
211
  change_morale(@home, morale_change_amount)
@@ -156,92 +242,30 @@ module Hlockey
156
242
  player = team.roster.values.sample(random: @prng)
157
243
  stat = player.stats.keys.sample(random: @prng)
158
244
  player.stats[stat] += 0.1
159
- @stream << Message.Partying(player)
245
+ @stream << Message.new(:game_partying, player:)
160
246
  next if @pre_tmp_change_stats[player].nil?
161
247
 
162
248
  @pre_tmp_change_stats[player][stat] += 0.1
163
249
  end
164
250
  end
165
251
 
166
- # Makes a player pass in the game, regardless of if it would normally occur
167
- def pass
168
- sender = puck_holder
169
- receiver = pass_reciever
170
- interceptor = defensive_pos
171
-
172
- if !@faceoff && try_take_puck(interceptor, 7 - @shooting_chance, :agility)
173
- @stream << Message.Pass(sender,
174
- @puckless_team.roster[receiver],
175
- @shooting_chance,
176
- @team_with_puck.roster[interceptor],
177
- @team_with_puck)
178
- return
179
- end
180
-
181
- @puck_holder_pos = receiver
182
- @shooting_chance += 1
183
- @stream << Message.Pass(sender, @team_with_puck.roster[receiver], @shooting_chance)
184
- end
185
-
186
- # Makes a player check in the game, regardless of if it would normally occur
187
- def check
188
- defender_pos = defensive_pos
189
- defender = @puckless_team.roster[defender_pos]
190
-
191
- mods_messages = puck_holder.mods.map { _1.on_got_hit(self, defender) }.compact
192
-
193
- @stream << Message.Hit(puck_holder,
194
- defender,
195
- mods_messages.empty? && try_take_puck(defender_pos),
196
- @team_with_puck,
197
- @shooting_chance)
198
- @stream.concat(mods_messages)
199
- @fight_chance += 0.1
200
- end
201
-
202
- # Makes a player shoot in the game, regardless of if it would normally occur
203
- # This is called outside of the class by Audacity weather (using #send)
204
- # @param audacity [Boolean] if the function was called by Audacity weather
205
- def shoot(audacity: false)
206
- return if @puck_holder_pos.nil?
207
-
208
- if @shooting_chance < 5 &&
209
- try_block_shot(@prng.rand(2).zero? ? :ldef : :rdef, audacity:)
210
- return
211
- end
212
- return if try_block_shot(:goalie, audacity:)
213
-
214
- # Goal scored
215
-
216
- scoring_team = @team_with_puck == @home ? :home : :away
217
- @score[scoring_team] += 1
218
- weather.on_goal(scoring_team)
219
-
220
- @stream << Message.ShootScore(puck_holder, @home, @away, *@score.values, audacity)
221
-
222
- start_fight if @prng.rand(3 + @fight_chance) > 3
223
-
224
- start_faceoff
225
- @actions = ACTIONS_PER_PERIOD - 1 if @period > NON_OT_PERIODS # Sudden death OT
226
- end
227
-
228
252
  def end_period
229
253
  @weather.on_period_end
230
- @stream << Message.EndOfPeriod(@period, @home, @away, *@score.values)
254
+ @stream << Message.new(:game_period_end, period: @period)
231
255
  @actions = 0
232
256
  @period += 1
233
257
  start_faceoff
234
258
 
235
259
  if @period > NON_OT_PERIODS &&
236
- !(@score[:home] == @score[:away] && @period <= TOTAL_PERIODS)
260
+ (@score[:home] != @score[:away] || @period > TOTAL_PERIODS)
237
261
  # Game is over
238
262
  @pre_tmp_change_stats.each { |player, stats| player.stats = stats }
239
263
  @weather.on_game_end
240
- @in_progress = false
241
264
  winner, loser = @score[:away] > @score[:home] ? [@away, @home] : [@home, @away]
242
- @stream << Message.EndOfGame(winner)
265
+ @stream << Message.new(:game_end, winning_team: winner)
243
266
  winner.wins += 1
244
267
  loser.losses += 1
268
+ @do_final_cleanup = true
245
269
  end
246
270
  end
247
271
 
@@ -271,7 +295,7 @@ module Hlockey
271
295
  @shooting_chance = 2
272
296
  @puck_holder_pos = :center
273
297
  @faceoff_won = true
274
- @stream << Message.FaceOff(puck_holder, @team_with_puck)
298
+ @stream << Message.new(:game_faceoff, winning_player: puck_holder)
275
299
  end
276
300
 
277
301
  def switch_team_with_puck(team_with_puck = @team_with_puck)
@@ -288,7 +312,7 @@ module Hlockey
288
312
  # @param dis [Integer] disadvantage against taking puck
289
313
  # @param stat [Symbol] stat compared to change the odds of success/failure
290
314
  # @return [Boolean] if taking the puck succeeded
291
- def try_take_puck(pos, dis = 0, stat = :defense)
315
+ def try_take_puck(pos, dis: 0, stat: :agility)
292
316
  return false unless action_succeeds?(@puckless_team.roster[pos].stats[stat] - dis,
293
317
  puck_holder.stats[stat])
294
318
 
@@ -299,20 +323,38 @@ module Hlockey
299
323
  end
300
324
 
301
325
  # @return [Boolean] if blocking the shot succeeded
302
- def try_block_shot(pos, audacity: false)
326
+ def try_block_shot(pos)
303
327
  blocker = @puckless_team.roster[pos]
304
328
 
305
- return false unless action_succeeds?(blocker.stats[:defense],
306
- puck_holder.stats[:offense])
329
+ defense = if pos == :goalie
330
+ blocker.stats[:defense] * 2
331
+ else
332
+ blocker.stats[:defense] - @shooting_chance
333
+ end
334
+ return false unless action_succeeds?(defense, puck_holder.stats[:offense])
307
335
 
308
336
  @shooting_chance += 1
309
- @stream << Message.ShootBlock(puck_holder, blocker,
310
- try_take_puck(pos), @team_with_puck,
311
- @shooting_chance, audacity)
337
+ @stream << Message.new(:game_shot_blocked, blocker:)
338
+
339
+ if action_succeeds?(defense, 0)
340
+ # blocker takes puck
341
+ switch_team_with_puck
342
+ @puck_holder_pos = pos
343
+ else
344
+ @puck_holder_pos = Utils.weighted_random(weights(true), @prng)
345
+ try_take_puck(Utils.weighted_random(weights(false).tap { _1.delete(pos) }, @prng))
346
+ end
347
+
348
+ @stream << Message.new(:game_possession_change,
349
+ player: puck_holder, new_puck_team: @team_with_puck)
312
350
 
313
351
  true
314
352
  end
315
353
 
354
+ # @param unset [Boolean] if the game should be set to nil (done when game is over)
355
+ def set_mods_game(unset: false) =
356
+ (@home.players + @away.players).each { _1.mods_do(:game=, unset ? nil : self) }
357
+
316
358
  # Add amount to each stat of each player of team until the end of the game
317
359
  # @param team [Team]
318
360
  # @param amount [Numeric]
@@ -320,21 +362,22 @@ module Hlockey
320
362
  return if amount.zero?
321
363
 
322
364
  team.roster.each_value do |player|
323
- if @pre_tmp_change_stats[player].nil?
324
- @pre_tmp_change_stats[player] = player.stats.clone
325
- end
326
-
327
- player.stats.transform_values! { |stat| stat + amount }
365
+ @pre_tmp_change_stats[player] ||= player.stats.clone
366
+ player.stats.transform_values! { _1 + amount }
328
367
  end
329
368
 
330
- @stream << Message.MoraleChange(team, amount)
369
+ @stream << if amount.positive?
370
+ Message.new(:fight_end_morale_gain, team:, num: amount)
371
+ else
372
+ Message.new(:fight_end_morale_loss, team:, num: -amount)
373
+ end
331
374
  end
332
375
 
333
- # @return [Player]
376
+ # @return [Team::Player]
334
377
  def puck_holder = @team_with_puck.roster[@puck_holder_pos]
335
378
 
336
379
  # @return [Symbol]
337
- def pass_reciever
380
+ def pass_reciever_pos
338
381
  w = weights(@shooting_chance > 3)
339
382
  w.delete(@puck_holder_pos)
340
383
  Utils.weighted_random(w, @prng)
@@ -345,11 +388,9 @@ module Hlockey
345
388
 
346
389
  # @return [Hash<Symbol => Integer>]
347
390
  def weights(offensive)
348
- if offensive
349
- { lwing: 2, center: 2, rwing: 2, ldef: 1, rdef: 1 }
350
- else
351
- { lwing: 1, center: 1, rwing: 1, ldef: 3, rdef: 3 }
352
- end
391
+ return { lwing: 2, center: 2, rwing: 2, ldef: 1, rdef: 1 } if offensive
392
+
393
+ { lwing: 1, center: 1, rwing: 1, ldef: 3, rdef: 3 }
353
394
  end
354
395
  end
355
396
  end
@@ -11,6 +11,10 @@ module Hlockey
11
11
  class League
12
12
  GAMES_IN_REGULAR_SEASON = 111
13
13
 
14
+ # @return [Boolean]
15
+ attr_reader(:infinite)
16
+ alias infinite? infinite
17
+
14
18
  # @return [Time]
15
19
  attr_reader(:start_time)
16
20
 
@@ -20,6 +24,9 @@ module Hlockey
20
24
  # @return [Integer]
21
25
  attr_reader(:day)
22
26
 
27
+ # @return [String]
28
+ attr_reader(:season)
29
+
23
30
  # @return [Array<Game>]
24
31
  attr_reader(:games, :games_in_progress)
25
32
 
@@ -32,21 +39,16 @@ module Hlockey
32
39
  # @return [Array<Team>]
33
40
  attr_reader(:teams, :playoff_teams)
34
41
 
35
- # @param start_time [Array<Integer>, nil] args to Time.utc
36
- # @param divisions [Hash<Symbol => Array<Hash>, nil]
37
- def initialize(start_time: nil, divisions: nil)
38
- if start_time.nil? || divisions.nil?
39
- league_hash = Data.league
40
- start_time ||= league_hash[:start_time]
41
- divisions ||= league_hash[:divisions]
42
- end
43
-
44
- @start_time = Time.utc(*start_time)
45
- @start_time.localtime
46
-
42
+ # @param start_time [Array<Integer>] parameters passed to Time.new
43
+ # @param divisions [Hash<Symbol => Array<Hash>>] division data
44
+ # @param infinite [Boolean] if the league is infinite, rather than a single season
45
+ def initialize(infinite:, start_time:, divisions:)
46
+ @infinite = infinite
47
+ @start_time = Time.utc(*start_time).localtime
47
48
  @divisions = divisions.transform_values { |teams| teams.map { Team.new(**_1) } }
48
49
 
49
50
  @day = 0
51
+ @season = infinite ? "?" : VERSION
50
52
 
51
53
  @games = []
52
54
  @games_in_progress = []
@@ -57,24 +59,27 @@ module Hlockey
57
59
  @last_update_time = @start_time
58
60
  @passed_updates = 0
59
61
 
60
- @prng = Random.new(@start_time.to_i)
62
+ @prng = Utils::Rng.new(@start_time.to_i)
61
63
 
62
64
  @teams = @divisions.values.flatten
63
65
  @sorted_teams_by_wins = @teams.shuffle(random: @prng)
64
66
  @playoff_teams = []
65
67
 
68
+ @game_in_matchup = @matchup_game_amt = 3
69
+
70
+ return if @infinite
71
+
66
72
  # warm vs warm / cool vs cool
67
73
  rotated_divisions = @divisions.values.rotate
68
74
  @shuffled_division_pairs = Array.new(2) do |i|
69
75
  (rotated_divisions[i] + rotated_divisions[-i - 1]).shuffle(random: @prng)
70
76
  end
71
-
72
- @game_in_matchup = @matchup_game_amt = 3
73
77
  end
74
78
 
75
79
  # @return [Hash]
76
80
  def to_h(simple: false)
77
81
  res = {
82
+ infinite: @infinite,
78
83
  start_time: @start_time.getutc.to_a.first(6).reverse,
79
84
  divisions: @divisions.transform_values { |d| d.map { |t| t.to_h(simple:) } }
80
85
  }
@@ -107,13 +112,16 @@ module Hlockey
107
112
  return unless intervals.positive?
108
113
 
109
114
  intervals.times do |i|
110
- if ((i + @passed_updates) % UPDATES_PER_HOUR).zero?
115
+ if !@infinite && ((i + @passed_updates) % UPDATES_PER_HOUR).zero?
111
116
  update_teams
112
117
  new_games
113
- else
114
- update_games
118
+ next
115
119
  end
116
120
 
121
+ update_games
122
+
123
+ new_games if @infinite && @games_in_progress.empty?
124
+
117
125
  break if @champion_team
118
126
  end
119
127
 
@@ -161,7 +169,9 @@ module Hlockey
161
169
  def new_games
162
170
  if @game_in_matchup != @matchup_game_amt
163
171
  # New game in matchups
164
- @games.map! { new_game(_1.away, _1.home) }
172
+ prev_games = @games
173
+ @games = []
174
+ prev_games.each { new_game(_1.away, _1.home) }
165
175
  @game_in_matchup += 1
166
176
  return
167
177
  end
@@ -171,12 +181,17 @@ module Hlockey
171
181
  @game_in_matchup = 1
172
182
  @day += 1
173
183
 
184
+ if @infinite
185
+ new_matchups(@teams)
186
+ return
187
+ end
188
+
174
189
  case @day
175
190
  when 1..27
176
191
  @shuffled_division_pairs.each { new_matchups(_1, reverse: @day.even?) }
177
192
  when 28..37
178
193
  @shuffled_division_pairs.transpose.each_with_index do |pair, i|
179
- @games << new_game(*pair, reverse: i.even?)
194
+ new_game(*pair, reverse: i.even?)
180
195
  end
181
196
  @shuffled_division_pairs.first.rotate!
182
197
  when 38
@@ -192,13 +207,17 @@ module Hlockey
192
207
  new_playoff_matchups
193
208
  when 41
194
209
  @champion_team = @playoff_teams.find(&:positive_record?)
195
- @alerts << Message.SeasonChampion(@champion_team)
210
+ @alerts << Message.new(:season_champion, season: @season, team: @champion_team)
196
211
  end
197
212
  end
198
213
 
199
214
  def update_games
200
215
  @games_in_progress.each(&:update)
201
- @games_in_progress = @games.select(&:in_progress?)
216
+
217
+ prev_in_progress_amt = @games_in_progress.length
218
+ @games_in_progress.select!(&:in_progress?)
219
+ return if @games_in_progress.length == prev_in_progress_amt
220
+
202
221
  @divisions.transform_values!(&method(:sort_teams_by_wins))
203
222
  end
204
223
 
@@ -208,12 +227,12 @@ module Hlockey
208
227
  new_matchups(@playoff_teams, rotate: false)
209
228
  end
210
229
 
211
- # @param matchup_teams[Array<Team>]
230
+ # @param matchup_teams [Array<Team>]
212
231
  # @param rotate [Boolean]
213
232
  # @param reverse [Boolean]
214
233
  def new_matchups(matchup_teams, rotate: true, reverse: false)
215
234
  (matchup_teams.length / 2).times do |i|
216
- @games << new_game(matchup_teams[i], matchup_teams[-i - 1], reverse:)
235
+ new_game(matchup_teams[i], matchup_teams[-i - 1], reverse:)
217
236
  end
218
237
  matchup_teams.insert(1, matchup_teams.pop) if rotate
219
238
  end
@@ -222,10 +241,11 @@ module Hlockey
222
241
  # @param away [Team]
223
242
  # @param reverse [Boolean]
224
243
  def new_game(home, away, reverse: false)
225
- teams = [home, away]
226
- teams.reverse! if reverse
244
+ teams = reverse ? [away, home] : [home, away]
227
245
 
228
- Game.new(*teams, @prng)
246
+ game = Game.new(*teams, @prng.rand)
247
+ @games << game
248
+ @games_in_progress << game
229
249
  end
230
250
 
231
251
  # @return [Array<Team>]