hlockey 2 → 4

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
@@ -1,159 +1,321 @@
1
- # frozen_string_literal: true
2
-
3
- require('hlockey/messages')
4
-
5
- module Hlockey
6
- class Game
7
- attr_reader(:home, :away, :stream, :score, :in_progress)
8
-
9
- def initialize(home, away, prng)
10
- @home = home
11
- @away = away
12
- @prng = prng
13
- @stream = [Messages.StartOfGame]
14
- @score = { home: 0, away: 0 }
15
- @in_progress = true
16
- @actions = 0
17
- @period = 1
18
- @face_off = true
19
- @team_with_puck = nil
20
- @puckless_team = nil
21
- @puck_holder = nil
22
- @shooting_chance = 0
23
- end
24
-
25
- def to_s
26
- "#{@home} vs #{@away}"
27
- end
28
-
29
- def update
30
- return unless @in_progress
31
-
32
- @stream << Messages.StartOfPeriod(@period) if @actions.zero?
33
- @actions += 1
34
-
35
- if @actions == 60
36
- @stream << Messages.EndOfPeriod(@period, @home, @away, *@score.values)
37
- @actions = 0
38
- @period += 1
39
- start_faceoff
40
-
41
- if @period > 3 && !(@score[:home] == @score[:away] && @period < 12)
42
- # Game is over
43
- @in_progress = false
44
-
45
- winner, loser = @score[:away] > @score[:home] ?
46
- [@away, @home] : [@home, @away]
47
-
48
- @stream << Messages.EndOfGame(winner)
49
- winner.wins += 1
50
- loser.losses += 1
51
- end
52
- return
53
- end
54
-
55
- if @face_off
56
- if @puck_holder.nil?
57
- # Pass opposite team to who wins the puck to switch_team_with_puck,
58
- # so @team_with_puck & @puckless_team are set to the correct values.
59
- switch_team_with_puck(
60
- action_succeeds?(@home.roster[:center].stats[:offense],
61
- @away.roster[:center].stats[:offense]) ? @away : @home
62
- )
63
-
64
- @puck_holder = @team_with_puck.roster[:center]
65
- @stream << Messages.FaceOff(@puck_holder, @team_with_puck)
66
- else
67
- pass
68
- @face_off = false
69
- end
70
- return
71
- end
72
-
73
- case @prng.rand 5 + @shooting_chance
74
- when 0..4
75
- pass
76
- when 5..6 # Check
77
- check
78
- else # Shoot
79
- unless @shooting_chance < 5 &&
80
- try_block_shot(@puckless_team.roster[@prng.rand(2).zero? ?
81
- :ldef : :rdef]) ||
82
- try_block_shot(@puckless_team.roster[:goalie])
83
- @score[@team_with_puck == @home ? :home : :away] += 1
84
-
85
- @stream << Messages.ShootScore(@puck_holder, @home, @away, *@score.values)
86
-
87
- start_faceoff
88
- @actions = 59 if @period > 3 # Sudden death overtime
89
- end
90
- end
91
- end
92
-
93
- private
94
-
95
- def action_succeeds?(helping_stat, hindering_stat)
96
- @prng.rand(10) + helping_stat - hindering_stat > 4
97
- end
98
-
99
- def start_faceoff
100
- @shooting_chance = 0
101
- @face_off = true
102
- @puck_holder = nil
103
- end
104
-
105
- def switch_team_with_puck(team_with_puck = @team_with_puck)
106
- @team_with_puck, @puckless_team = team_with_puck == @home ?
107
- [@away, @home] : [@home, @away]
108
- @shooting_chance = 0
109
- end
110
-
111
- def pass
112
- sender = @puck_holder
113
- receiver = puckless_non_goalie(@team_with_puck)
114
- interceptor = puckless_non_goalie
115
-
116
- if !@face_off && try_take_puck(interceptor, 4) # Pass intercepted
117
- @stream << Messages.Pass(sender, receiver, interceptor, @team_with_puck)
118
- return
119
- end
120
-
121
- @stream << Messages.Pass(sender, receiver)
122
- @puck_holder = receiver
123
- @shooting_chance += 1
124
- end
125
-
126
- def check
127
- defender = puckless_non_goalie
128
- @stream << Messages.Hit(@puck_holder, defender,
129
- try_take_puck(defender, 0, :defense), @team_with_puck)
130
- end
131
-
132
- def try_take_puck(player, dis = 0, stat = :agility)
133
- return false unless action_succeeds?(player.stats[stat] - dis,
134
- @puck_holder.stats[stat])
135
-
136
- switch_team_with_puck
137
- @puck_holder = player
138
-
139
- true
140
- end
141
-
142
- def try_block_shot(blocker)
143
- return false unless action_succeeds?(blocker.stats[:defense],
144
- @puck_holder.stats[:offense])
145
-
146
- @shooting_chance += 1
147
- @stream << Messages.ShootBlock(@puck_holder, blocker,
148
- try_take_puck(blocker), @team_with_puck)
149
-
150
- true
151
- end
152
-
153
- def puckless_non_goalie(team = @puckless_team)
154
- team.roster.values.select do |p|
155
- p != team.roster[:goalie] and p != @puck_holder
156
- end.sample(random: @prng)
157
- end
158
- end
159
- end
1
+ require("hlockey/game/actions")
2
+ require("hlockey/game/fight")
3
+ require("hlockey/constants")
4
+ require("hlockey/message")
5
+ require("hlockey/utils")
6
+
7
+ module Hlockey
8
+ ##
9
+ # A single game of Hlockey
10
+ class Game
11
+ include(Actions)
12
+
13
+ ACTIONS_PER_PERIOD = 60
14
+ NON_OT_PERIODS = 4
15
+ NON_OT_ACTIONS = ACTIONS_PER_PERIOD * NON_OT_PERIODS
16
+ TOTAL_ACTIONS = UPDATES_PER_HOUR - ACTIONS_PER_PERIOD
17
+ TOTAL_PERIODS = TOTAL_ACTIONS / ACTIONS_PER_PERIOD
18
+
19
+ # @return [Weather]
20
+ attr_reader(:weather)
21
+
22
+ # @return [Array<Message>]
23
+ attr_reader(:stream)
24
+
25
+ # @return [Integer]
26
+ attr_reader(:actions, :shooting_chance)
27
+
28
+ # @return [Fight, nil]
29
+ attr_reader(:fight)
30
+
31
+ # @param home [Team]
32
+ # @param away [Team]
33
+ # @param prng [Random] should be League#prng
34
+ # @param weather [Class<Weather>]
35
+ def initialize(home, away, prng, weather)
36
+ @home = home
37
+ @away = away
38
+ @prng = prng
39
+ @weather = weather.new(self)
40
+ @stream = [Message.StartOfGame]
41
+ @score = { home: 0, away: 0 }
42
+ @in_progress = true
43
+ @actions = 0
44
+ @period = 1
45
+ @faceoff = true
46
+ @faceoff_won = false
47
+ @team_with_puck = nil
48
+ @puckless_team = nil
49
+ @puck_holder_pos = nil
50
+ @shooting_chance = 0
51
+ @fight = nil
52
+ @fight_chance = 0
53
+ @pre_morale_change_stats = {}
54
+ @partying_teams = [@home, @away].select { |t| t.status == :partying }
55
+
56
+ @weather.on_game_start
57
+ end
58
+
59
+ # @return [String]
60
+ def to_s
61
+ "#{Message.color(@home)} vs #{Message.color(@away)}"
62
+ end
63
+
64
+ # Does an action in the game
65
+ def update
66
+ return unless @in_progress
67
+
68
+ @stream << Message.StartOfPeriod(@period) if @actions.zero?
69
+ @actions += 1
70
+
71
+ @weather.on_action
72
+
73
+ unless @fight.nil?
74
+ @stream << @fight.next_action
75
+ if @stream.last.event == :FightEnded
76
+ morale_change_amount = (@fight.score[:home] - @fight.score[:away]) / 10.0
77
+ change_morale(@home, morale_change_amount)
78
+ change_morale(@away, -morale_change_amount)
79
+
80
+ @fight = nil
81
+ end
82
+ return
83
+ end
84
+
85
+ if @actions >= ACTIONS_PER_PERIOD
86
+ end_period
87
+ return
88
+ end
89
+ if @faceoff
90
+ do_faceoff
91
+ return
92
+ end
93
+
94
+ case @prng.rand(5 + @shooting_chance)
95
+ when 0..4
96
+ pass
97
+ when 5, 6
98
+ check
99
+ else
100
+ shoot
101
+ end
102
+
103
+ return unless @partying_teams.any? && @prng.rand(NON_OT_ACTIONS / 5).zero?
104
+
105
+ @partying_teams.each do |team|
106
+ player = team.roster.values.sample(random: @prng)
107
+ stat = player.stats.keys.sample(random: @prng)
108
+ player.stats[stat] += 0.1
109
+ @stream << Message.Partying(player)
110
+ next if @pre_morale_change_stats[player].nil?
111
+
112
+ @pre_morale_change_stats[player][stat] += 0.1
113
+ end
114
+ end
115
+
116
+ # Makes a player shoot in the game, regardless of if it would normally occur
117
+ # @param audacity [Boolean] if the function was called by Audacity weather
118
+ def shoot(audacity: false)
119
+ return if @puck_holder_pos.nil?
120
+
121
+ if @shooting_chance < 5 &&
122
+ try_block_shot(@prng.rand(2).zero? ? :ldef : :rdef, audacity: audacity)
123
+ return
124
+ end
125
+ return if try_block_shot(:goalie, audacity: audacity)
126
+
127
+ # Goal scored
128
+
129
+ scoring_team = @team_with_puck == @home ? :home : :away
130
+ @score[scoring_team] += 1
131
+ weather.on_goal(scoring_team)
132
+
133
+ @stream << Message.ShootScore(puck_holder, @home, @away, *@score.values, audacity)
134
+
135
+ start_fight if @prng.rand(3 + @fight_chance) > 3
136
+
137
+ start_faceoff
138
+ @actions = ACTIONS_PER_PERIOD - 1 if @period >= NON_OT_PERIODS # Sudden death OT
139
+ end
140
+
141
+ private
142
+
143
+ # Makes a player pass in the game, regardless of if it would normally occur
144
+ def pass
145
+ sender = puck_holder
146
+ receiver = pass_reciever
147
+ interceptor = defensive_pos
148
+
149
+ if !@faceoff && try_take_puck(interceptor, 7 - @shooting_chance, :agility)
150
+ @stream << Message.Pass(sender,
151
+ @puckless_team.roster[receiver],
152
+ @shooting_chance,
153
+ @team_with_puck.roster[interceptor],
154
+ @team_with_puck)
155
+ return
156
+ end
157
+
158
+ @puck_holder_pos = receiver
159
+ @shooting_chance += 1
160
+ @stream << Message.Pass(sender, @team_with_puck.roster[receiver], @shooting_chance)
161
+ end
162
+
163
+ # Makes a player check in the game, regardless of if it would normally occur
164
+ def check
165
+ defender_pos = defensive_pos
166
+ defender = @puckless_team.roster[defender_pos]
167
+ @stream << Message.Hit(puck_holder, defender,
168
+ try_take_puck(defender_pos), @team_with_puck,
169
+ @shooting_chance)
170
+ @fight_chance += 0.1
171
+ end
172
+
173
+ def end_period
174
+ @weather.on_period_end
175
+ @stream << Message.EndOfPeriod(@period, @home, @away, *@score.values)
176
+ @actions = 0
177
+ @period += 1
178
+ start_faceoff
179
+
180
+ if @period >= NON_OT_PERIODS &&
181
+ !(@score[:home] == @score[:away] && @period <= TOTAL_PERIODS)
182
+ # Game is over
183
+ @pre_morale_change_stats.each { |player, stats| player.stats = stats }
184
+ @weather.on_game_end
185
+ @in_progress = false
186
+ winner, loser = @score[:away] > @score[:home] ? [@away, @home] : [@home, @away]
187
+ @stream << Message.EndOfGame(winner)
188
+ winner.wins += 1
189
+ loser.losses += 1
190
+ end
191
+ end
192
+
193
+ def start_faceoff
194
+ @shooting_chance = 0
195
+ @faceoff = true
196
+ @faceoff_won = false
197
+ end
198
+
199
+ def do_faceoff
200
+ if @faceoff_won
201
+ pass
202
+ @faceoff = false
203
+ return
204
+ end
205
+
206
+ # Pass opposite team to who wins the puck to switch_team_with_puck,
207
+ # so @team_with_puck & @puckless_team are set to the correct values.
208
+ switch_team_with_puck(
209
+ if action_succeeds?(@home.roster[:center].stats[:offense],
210
+ @away.roster[:center].stats[:offense])
211
+ @away
212
+ else
213
+ @home
214
+ end
215
+ )
216
+ @shooting_chance = 2
217
+ @puck_holder_pos = :center
218
+ @faceoff_won = true
219
+ @stream << Message.FaceOff(puck_holder, @team_with_puck)
220
+ end
221
+
222
+ def switch_team_with_puck(team_with_puck = @team_with_puck)
223
+ @team_with_puck, @puckless_team = if team_with_puck == @home
224
+ [@away, @home]
225
+ else
226
+ [@home, @away]
227
+ end
228
+ @shooting_chance = 3 - @shooting_chance
229
+ @shooting_chance = 0 if @shooting_chance.negative?
230
+ end
231
+
232
+ # @param pos [Symbol] position on puckless team that tries to take the puck
233
+ # @param dis [Integer] disadvantage against taking puck
234
+ # @param stat [Symbol] stat compared to change the odds of success/failure
235
+ # @return [Boolean] if taking the puck succeeded
236
+ def try_take_puck(pos, dis = 0, stat = :defense)
237
+ return false unless action_succeeds?(@puckless_team.roster[pos].stats[stat] - dis,
238
+ puck_holder.stats[stat])
239
+
240
+ switch_team_with_puck
241
+ @puck_holder_pos = pos
242
+
243
+ true
244
+ end
245
+
246
+ # @return [Boolean] if blocking the shot succeeded
247
+ def try_block_shot(pos, audacity: false)
248
+ blocker = @puckless_team.roster[pos]
249
+
250
+ return false unless action_succeeds?(blocker.stats[:defense],
251
+ puck_holder.stats[:offense])
252
+
253
+ @shooting_chance += 1
254
+ @stream << Message.ShootBlock(puck_holder, blocker,
255
+ try_take_puck(pos), @team_with_puck,
256
+ @shooting_chance, audacity)
257
+
258
+ true
259
+ end
260
+
261
+ def start_fight
262
+ @fight = Fight.new(self)
263
+ @stream << Message.FightStarted(@fight.players[:home].first,
264
+ @fight.players[:away].first)
265
+ end
266
+
267
+ # @param team [Team]
268
+ # @param amount [Numeric]
269
+ def change_morale(team, amount)
270
+ return if amount.zero?
271
+
272
+ team.roster.each_value do |player|
273
+ if @pre_morale_change_stats[player].nil?
274
+ @pre_morale_change_stats[player] = player.stats.clone
275
+ end
276
+
277
+ player.stats.transform_values! { |stat| stat + amount }
278
+ end
279
+
280
+ @stream << Message.MoraleChange(team, amount)
281
+ end
282
+
283
+ # @return [Player]
284
+ def puck_holder
285
+ @team_with_puck.roster[@puck_holder_pos]
286
+ end
287
+
288
+ # @return [Symbol]
289
+ def pass_reciever
290
+ w = weights(@shooting_chance > 3)
291
+ w.delete(@puck_holder_pos)
292
+ Utils.weighted_random(w, @prng)
293
+ end
294
+
295
+ # @return [Symbol]
296
+ def defensive_pos
297
+ Utils.weighted_random(weights(@shooting_chance < 3), @prng)
298
+ end
299
+
300
+ # @return [Hash<Symbol => Integer>]
301
+ def weights(offensive)
302
+ if offensive
303
+ {
304
+ lwing: 2,
305
+ center: 2,
306
+ rwing: 2,
307
+ ldef: 1,
308
+ rdef: 1
309
+ }
310
+ else
311
+ {
312
+ lwing: 1,
313
+ center: 1,
314
+ rwing: 1,
315
+ ldef: 3,
316
+ rdef: 3
317
+ }
318
+ end
319
+ end
320
+ end
321
+ end