hlockey 5 → 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.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/data/election.json +23 -0
  3. data/data/infinileague.json +189 -0
  4. data/data/information.json +6 -0
  5. data/data/league.json +2042 -0
  6. data/data/links.json +5 -0
  7. data/data/messages.json +46 -0
  8. data/data/previous_election_results.json +91 -0
  9. data/lib/hlockey/actions.rb +22 -0
  10. data/lib/hlockey/data.rb +6 -5
  11. data/lib/hlockey/game/fight.rb +100 -40
  12. data/lib/hlockey/game/weather/audacity.rb +1 -1
  13. data/lib/hlockey/game/weather/chicken.rb +3 -2
  14. data/lib/hlockey/game/weather/incline.rb +2 -1
  15. data/lib/hlockey/game/weather/stars.rb +1 -1
  16. data/lib/hlockey/game/weather/sunset.rb +2 -4
  17. data/lib/hlockey/game/weather/waves.rb +9 -5
  18. data/lib/hlockey/game/weather/weatherable.rb +9 -9
  19. data/lib/hlockey/game.rb +182 -116
  20. data/lib/hlockey/league.rb +90 -73
  21. data/lib/hlockey/message.rb +23 -153
  22. data/lib/hlockey/mod/fencebuilder.rb +15 -0
  23. data/lib/hlockey/mod/fencedestroyer.rb +16 -0
  24. data/lib/hlockey/mod/handholding.rb +57 -0
  25. data/lib/hlockey/mod/immaterial.rb +28 -0
  26. data/lib/hlockey/mod/locked.rb +28 -0
  27. data/lib/hlockey/mod/moddable.rb +70 -0
  28. data/lib/hlockey/mod/nonconfrontational.rb +17 -0
  29. data/lib/hlockey/mod/powernapper.rb +50 -0
  30. data/lib/hlockey/mod/punchy.rb +21 -0
  31. data/lib/hlockey/mod.rb +25 -0
  32. data/lib/hlockey/selfdescribable.rb +7 -0
  33. data/lib/hlockey/team/player.rb +40 -58
  34. data/lib/hlockey/team/stadium.rb +10 -7
  35. data/lib/hlockey/team.rb +56 -41
  36. data/lib/hlockey/utils.rb +36 -4
  37. data/lib/hlockey/version.rb +1 -1
  38. data/lib/hlockey.rb +15 -0
  39. metadata +30 -18
  40. data/data/election.yaml +0 -21
  41. data/data/external/names.txt +0 -19948
  42. data/data/information.yaml +0 -24
  43. data/data/league.yaml +0 -1694
  44. data/data/links.yaml +0 -3
  45. data/data/previous_election_results.yaml +0 -65
  46. data/lib/hlockey/game/actions.rb +0 -24
data/lib/hlockey/game.rb CHANGED
@@ -1,6 +1,6 @@
1
- require("hlockey/game/actions")
2
1
  require("hlockey/game/fight")
3
2
  require("hlockey/game/weather")
3
+ require("hlockey/actions")
4
4
  require("hlockey/constants")
5
5
  require("hlockey/message")
6
6
  require("hlockey/utils")
@@ -14,9 +14,12 @@ 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
+ # @return [Hash<Symbol => Integer>] the score of each team
21
+ attr_reader(:score)
22
+
20
23
  # @return [Team::Stadium] the stadium this game is located at
21
24
  attr_reader(:stadium)
22
25
 
@@ -26,11 +29,9 @@ module Hlockey
26
29
  # @return [Array<Message>] messages about game events
27
30
  attr_reader(:stream)
28
31
 
29
- # @return [Hash<Symbol => Integer>] the score of each team
30
- attr_reader(:score)
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)
@@ -38,20 +39,31 @@ module Hlockey
38
39
  # @return [Fight, nil] the current fight, if there is any
39
40
  attr_reader(:fight)
40
41
 
42
+ # @return [Hash<Player => Hash<Symbol => Number>>] stats of players before changes
43
+ attr_reader(:pre_tmp_change_stats)
44
+
41
45
  # @param home [Team]
42
46
  # @param away [Team]
43
- # @param prng [Random] should be League#prng
44
- def initialize(home, away, prng)
47
+ # @param rng_seed [Integer] number to seed the game's RNG with
48
+ # @param weather [Class<Weatherable>]
49
+ def initialize(home, away, rng_seed, weather = nil)
45
50
  @home = home
46
51
  @away = away
47
- @prng = prng
52
+ @prng = Utils::Rng.new(rng_seed)
53
+
48
54
  @stadium = @home.stadium
49
- @weather = Weather::WEATHERS.sample(random: prng).new(self)
50
- @stream = [Message.StartOfGame]
55
+
56
+ if weather.nil?
57
+ weather = Weather::WEATHERS.sample(random: prng)
58
+ @rng_seed = @prng.state
59
+ end
60
+
61
+ @weather = weather.new(self)
51
62
  @score = { home: 0, away: 0 }
52
63
  @in_progress = true
53
64
  @actions = 0
54
65
  @period = 1
66
+ @stream = [Message.new(:game_start)]
55
67
  @faceoff = true
56
68
  @faceoff_won = false
57
69
  @team_with_puck = nil
@@ -60,38 +72,140 @@ module Hlockey
60
72
  @shooting_chance = 0
61
73
  @fight = nil
62
74
  @fight_chance = 0
63
- @pre_morale_change_stats = {}
64
- @partying_teams = [@home, @away].select { |t| t.status == :partying }
75
+ @pre_tmp_change_stats = {}
76
+ @partying_teams = [@home, @away].select { _1.status == :partying }
77
+ @do_final_cleanup = false
65
78
 
79
+ set_mods_game
66
80
  @weather.on_game_start
67
81
  end
68
82
 
69
- # @return [String]
70
- def to_s
71
- "#{Message.color(@home)} vs #{Message.color(@away)}"
83
+ # @return [Hash]
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
72
106
  end
73
107
 
108
+ # @return [String]
109
+ def to_s(do_color: true) =
110
+ "#{Message.color(@home, do_color:)} vs #{Message.color(@away, do_color:)}"
111
+
74
112
  # Update the game state by one action
75
113
  def update
76
- return unless @in_progress
114
+ if @do_final_cleanup
115
+ @in_progress = false
116
+ set_mods_game(unset: true)
117
+ return
118
+ end
77
119
 
78
120
  do_action
79
121
  handle_parties
80
122
  end
81
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
+
187
+ # Starts a fight in the game
188
+ # @param starting_player [Player, nil] the player that started the fight (optional)
189
+ def start_fight(starting_player = nil)
190
+ @fight = Fight.new(self, starting_player)
191
+ @stream << Message.new(:fight_start,
192
+ home_player: @fight.players[:home].first,
193
+ away_player: @fight.players[:away].first)
194
+ end
195
+
82
196
  private
83
197
 
84
198
  # Does an action in the game
85
199
  def do_action
86
- @stream << Message.StartOfPeriod(@period) if @actions.zero?
200
+ @stream << Message.new(:game_period_start, period: @period) if @actions.zero?
87
201
  @actions += 1
88
202
 
89
203
  @weather.on_action
204
+ (@home.roster.values + @away.roster.values).each { _1.mods_do(:on_action) }
90
205
 
91
206
  unless @fight.nil?
92
- fight_message = @fight.next_action
93
- @stream << fight_message
94
- return unless fight_message.event == :FightEnded
207
+ @fight.next_action
208
+ return unless @stream.last.event == :fight_end
95
209
 
96
210
  morale_change_amount = (@fight.score[:home] - @fight.score[:away]) / 10.0
97
211
  change_morale(@home, morale_change_amount)
@@ -128,86 +242,30 @@ module Hlockey
128
242
  player = team.roster.values.sample(random: @prng)
129
243
  stat = player.stats.keys.sample(random: @prng)
130
244
  player.stats[stat] += 0.1
131
- @stream << Message.Partying(player)
132
- next if @pre_morale_change_stats[player].nil?
245
+ @stream << Message.new(:game_partying, player:)
246
+ next if @pre_tmp_change_stats[player].nil?
133
247
 
134
- @pre_morale_change_stats[player][stat] += 0.1
248
+ @pre_tmp_change_stats[player][stat] += 0.1
135
249
  end
136
250
  end
137
251
 
138
- # Makes a player pass in the game, regardless of if it would normally occur
139
- def pass
140
- sender = puck_holder
141
- receiver = pass_reciever
142
- interceptor = defensive_pos
143
-
144
- if !@faceoff && try_take_puck(interceptor, 7 - @shooting_chance, :agility)
145
- @stream << Message.Pass(sender,
146
- @puckless_team.roster[receiver],
147
- @shooting_chance,
148
- @team_with_puck.roster[interceptor],
149
- @team_with_puck)
150
- return
151
- end
152
-
153
- @puck_holder_pos = receiver
154
- @shooting_chance += 1
155
- @stream << Message.Pass(sender, @team_with_puck.roster[receiver], @shooting_chance)
156
- end
157
-
158
- # Makes a player check in the game, regardless of if it would normally occur
159
- def check
160
- defender_pos = defensive_pos
161
- defender = @puckless_team.roster[defender_pos]
162
- @stream << Message.Hit(puck_holder, defender,
163
- try_take_puck(defender_pos), @team_with_puck,
164
- @shooting_chance)
165
- @fight_chance += 0.1
166
- end
167
-
168
- # Makes a player shoot in the game, regardless of if it would normally occur
169
- # This is called outside of the class by Audacity weather (using #send)
170
- # @param audacity [Boolean] if the function was called by Audacity weather
171
- def shoot(audacity: false)
172
- return if @puck_holder_pos.nil?
173
-
174
- if @shooting_chance < 5 &&
175
- try_block_shot(@prng.rand(2).zero? ? :ldef : :rdef, audacity: audacity)
176
- return
177
- end
178
- return if try_block_shot(:goalie, audacity: audacity)
179
-
180
- # Goal scored
181
-
182
- scoring_team = @team_with_puck == @home ? :home : :away
183
- @score[scoring_team] += 1
184
- weather.on_goal(scoring_team)
185
-
186
- @stream << Message.ShootScore(puck_holder, @home, @away, *@score.values, audacity)
187
-
188
- start_fight if @prng.rand(3 + @fight_chance) > 3
189
-
190
- start_faceoff
191
- @actions = ACTIONS_PER_PERIOD - 1 if @period > NON_OT_PERIODS # Sudden death OT
192
- end
193
-
194
252
  def end_period
195
253
  @weather.on_period_end
196
- @stream << Message.EndOfPeriod(@period, @home, @away, *@score.values)
254
+ @stream << Message.new(:game_period_end, period: @period)
197
255
  @actions = 0
198
256
  @period += 1
199
257
  start_faceoff
200
258
 
201
259
  if @period > NON_OT_PERIODS &&
202
- !(@score[:home] == @score[:away] && @period <= TOTAL_PERIODS)
260
+ (@score[:home] != @score[:away] || @period > TOTAL_PERIODS)
203
261
  # Game is over
204
- @pre_morale_change_stats.each { |player, stats| player.stats = stats }
262
+ @pre_tmp_change_stats.each { |player, stats| player.stats = stats }
205
263
  @weather.on_game_end
206
- @in_progress = false
207
264
  winner, loser = @score[:away] > @score[:home] ? [@away, @home] : [@home, @away]
208
- @stream << Message.EndOfGame(winner)
265
+ @stream << Message.new(:game_end, winning_team: winner)
209
266
  winner.wins += 1
210
267
  loser.losses += 1
268
+ @do_final_cleanup = true
211
269
  end
212
270
  end
213
271
 
@@ -237,7 +295,7 @@ module Hlockey
237
295
  @shooting_chance = 2
238
296
  @puck_holder_pos = :center
239
297
  @faceoff_won = true
240
- @stream << Message.FaceOff(puck_holder, @team_with_puck)
298
+ @stream << Message.new(:game_faceoff, winning_player: puck_holder)
241
299
  end
242
300
 
243
301
  def switch_team_with_puck(team_with_puck = @team_with_puck)
@@ -254,7 +312,7 @@ module Hlockey
254
312
  # @param dis [Integer] disadvantage against taking puck
255
313
  # @param stat [Symbol] stat compared to change the odds of success/failure
256
314
  # @return [Boolean] if taking the puck succeeded
257
- def try_take_puck(pos, dis = 0, stat = :defense)
315
+ def try_take_puck(pos, dis: 0, stat: :agility)
258
316
  return false unless action_succeeds?(@puckless_team.roster[pos].stats[stat] - dis,
259
317
  puck_holder.stats[stat])
260
318
 
@@ -265,66 +323,74 @@ module Hlockey
265
323
  end
266
324
 
267
325
  # @return [Boolean] if blocking the shot succeeded
268
- def try_block_shot(pos, audacity: false)
326
+ def try_block_shot(pos)
269
327
  blocker = @puckless_team.roster[pos]
270
328
 
271
- return false unless action_succeeds?(blocker.stats[:defense],
272
- 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])
273
335
 
274
336
  @shooting_chance += 1
275
- @stream << Message.ShootBlock(puck_holder, blocker,
276
- try_take_puck(pos), @team_with_puck,
277
- @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)
278
350
 
279
351
  true
280
352
  end
281
353
 
282
- def start_fight
283
- @fight = Fight.new(self)
284
- @stream << Message.FightStarted(@fight.players[:home].first,
285
- @fight.players[:away].first)
286
- end
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) }
287
357
 
358
+ # Add amount to each stat of each player of team until the end of the game
288
359
  # @param team [Team]
289
360
  # @param amount [Numeric]
290
361
  def change_morale(team, amount)
291
362
  return if amount.zero?
292
363
 
293
364
  team.roster.each_value do |player|
294
- if @pre_morale_change_stats[player].nil?
295
- @pre_morale_change_stats[player] = player.stats.clone
296
- end
297
-
298
- player.stats.transform_values! { |stat| stat + amount }
365
+ @pre_tmp_change_stats[player] ||= player.stats.clone
366
+ player.stats.transform_values! { _1 + amount }
299
367
  end
300
368
 
301
- @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
302
374
  end
303
375
 
304
- # @return [Player]
305
- def puck_holder
306
- @team_with_puck.roster[@puck_holder_pos]
307
- end
376
+ # @return [Team::Player]
377
+ def puck_holder = @team_with_puck.roster[@puck_holder_pos]
308
378
 
309
379
  # @return [Symbol]
310
- def pass_reciever
380
+ def pass_reciever_pos
311
381
  w = weights(@shooting_chance > 3)
312
382
  w.delete(@puck_holder_pos)
313
383
  Utils.weighted_random(w, @prng)
314
384
  end
315
385
 
316
386
  # @return [Symbol]
317
- def defensive_pos
318
- Utils.weighted_random(weights(@shooting_chance < 3), @prng)
319
- end
387
+ def defensive_pos = Utils.weighted_random(weights(@shooting_chance < 3), @prng)
320
388
 
321
389
  # @return [Hash<Symbol => Integer>]
322
390
  def weights(offensive)
323
- if offensive
324
- { lwing: 2, center: 2, rwing: 2, ldef: 1, rdef: 1 }
325
- else
326
- { lwing: 1, center: 1, rwing: 1, ldef: 3, rdef: 3 }
327
- 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 }
328
394
  end
329
395
  end
330
396
  end