hlockey 5 → 7

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