hlockey 2 → 4

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