hlockey 6 → 7

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