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.
- checksums.yaml +4 -4
- data/data/election.yaml +21 -0
- data/data/external/names.txt +19948 -0
- data/data/information.yaml +24 -0
- data/data/league.yaml +1526 -0
- data/data/links.yaml +3 -0
- data/data/previous_election_results.yaml +59 -0
- data/lib/hlockey/constants.rb +4 -0
- data/lib/hlockey/data.rb +17 -13
- data/lib/hlockey/game/actions.rb +27 -0
- data/lib/hlockey/game/fight.rb +99 -0
- data/lib/hlockey/game.rb +321 -159
- data/lib/hlockey/league.rb +254 -115
- data/lib/hlockey/message.rb +182 -0
- data/lib/hlockey/player.rb +73 -0
- data/lib/hlockey/team.rb +52 -35
- data/lib/hlockey/utils.rb +15 -0
- data/lib/hlockey/version.rb +3 -5
- data/lib/hlockey/weather.rb +168 -0
- data/lib/hlockey.rb +1 -3
- metadata +36 -16
- data/bin/hlockey +0 -104
- data/lib/hlockey/data/election.yaml +0 -16
- data/lib/hlockey/data/information.yaml +0 -12
- data/lib/hlockey/data/league.yaml +0 -846
- data/lib/hlockey/data/links.yaml +0 -2
- data/lib/hlockey/messages.rb +0 -100
data/lib/hlockey/game.rb
CHANGED
@@ -1,159 +1,321 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require(
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
@
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
end
|
157
|
-
|
158
|
-
|
159
|
-
|
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
|