hlockey 3 → 5
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 +10 -5
- data/data/information.yaml +21 -9
- data/data/league.yaml +1689 -841
- data/data/links.yaml +2 -1
- data/data/previous_election_results.yaml +65 -0
- data/lib/hlockey/constants.rb +6 -0
- data/lib/hlockey/data.rb +1 -1
- data/lib/hlockey/game/actions.rb +9 -15
- data/lib/hlockey/game/fight.rb +43 -23
- data/lib/hlockey/game/weather/audacity.rb +20 -0
- data/lib/hlockey/game/weather/chicken.rb +57 -0
- data/lib/hlockey/game/weather/incline.rb +43 -0
- data/lib/hlockey/game/weather/stars.rb +23 -0
- data/lib/hlockey/game/weather/sunset.rb +23 -0
- data/lib/hlockey/game/weather/waves.rb +27 -0
- data/lib/hlockey/game/weather/weatherable.rb +63 -0
- data/lib/hlockey/game/weather.rb +17 -0
- data/lib/hlockey/game.rb +186 -110
- data/lib/hlockey/league.rb +134 -35
- data/lib/hlockey/message.rb +77 -26
- data/lib/hlockey/team/player.rb +79 -0
- data/lib/hlockey/team/stadium.rb +49 -0
- data/lib/hlockey/team.rb +39 -20
- data/lib/hlockey/utils.rb +14 -3
- data/lib/hlockey/version.rb +1 -1
- metadata +29 -6
- data/lib/hlockey/player.rb +0 -63
- data/lib/hlockey/weather.rb +0 -114
data/lib/hlockey/game.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
require("hlockey/game/actions")
|
2
2
|
require("hlockey/game/fight")
|
3
|
+
require("hlockey/game/weather")
|
3
4
|
require("hlockey/constants")
|
4
5
|
require("hlockey/message")
|
6
|
+
require("hlockey/utils")
|
5
7
|
|
6
8
|
module Hlockey
|
7
9
|
##
|
@@ -9,93 +11,185 @@ module Hlockey
|
|
9
11
|
class Game
|
10
12
|
include(Actions)
|
11
13
|
|
12
|
-
|
13
|
-
|
14
|
-
NON_OT_ACTIONS = ACTIONS_PER_PERIOD * NON_OT_PERIODS
|
15
|
-
TOTAL_ACTIONS = UPDATES_PER_HOUR - ACTIONS_PER_PERIOD
|
16
|
-
TOTAL_PERIODS = TOTAL_ACTIONS / ACTIONS_PER_PERIOD
|
14
|
+
# @return [Team] the teams in the game
|
15
|
+
attr_reader(:home, :away)
|
17
16
|
|
18
|
-
# @return [
|
19
|
-
attr_reader(:
|
17
|
+
# @return [Random] should be the same as the league's
|
18
|
+
attr_reader(:prng)
|
19
|
+
|
20
|
+
# @return [Team::Stadium] the stadium this game is located at
|
21
|
+
attr_reader(:stadium)
|
20
22
|
|
21
|
-
# @return [Weather]
|
23
|
+
# @return [Weather::Weatherable] the weather effecting the game
|
22
24
|
attr_reader(:weather)
|
23
25
|
|
26
|
+
# @return [Array<Message>] messages about game events
|
27
|
+
attr_reader(:stream)
|
28
|
+
|
29
|
+
# @return [Hash<Symbol => Integer>] the score of each team
|
30
|
+
attr_reader(:score)
|
31
|
+
|
32
|
+
# @return [Boolean] if the game is still in progress
|
33
|
+
attr_reader(:in_progress)
|
34
|
+
|
35
|
+
# @return [Integer] what period the game is in
|
36
|
+
attr_reader(:period)
|
37
|
+
|
38
|
+
# @return [Fight, nil] the current fight, if there is any
|
39
|
+
attr_reader(:fight)
|
40
|
+
|
24
41
|
# @param home [Team]
|
25
42
|
# @param away [Team]
|
26
43
|
# @param prng [Random] should be League#prng
|
27
|
-
|
28
|
-
def initialize(home, away, prng, weather)
|
44
|
+
def initialize(home, away, prng)
|
29
45
|
@home = home
|
30
46
|
@away = away
|
31
47
|
@prng = prng
|
32
|
-
@
|
48
|
+
@stadium = @home.stadium
|
49
|
+
@weather = Weather::WEATHERS.sample(random: prng).new(self)
|
33
50
|
@stream = [Message.StartOfGame]
|
34
51
|
@score = { home: 0, away: 0 }
|
35
52
|
@in_progress = true
|
36
53
|
@actions = 0
|
37
54
|
@period = 1
|
38
55
|
@faceoff = true
|
56
|
+
@faceoff_won = false
|
39
57
|
@team_with_puck = nil
|
40
58
|
@puckless_team = nil
|
41
|
-
@
|
59
|
+
@puck_holder_pos = nil
|
42
60
|
@shooting_chance = 0
|
43
61
|
@fight = nil
|
44
62
|
@fight_chance = 0
|
45
|
-
@total_fight_actions = 0
|
46
63
|
@pre_morale_change_stats = {}
|
64
|
+
@partying_teams = [@home, @away].select { |t| t.status == :partying }
|
47
65
|
|
48
66
|
@weather.on_game_start
|
49
67
|
end
|
50
68
|
|
51
69
|
# @return [String]
|
52
70
|
def to_s
|
53
|
-
"#{@home} vs #{@away}"
|
71
|
+
"#{Message.color(@home)} vs #{Message.color(@away)}"
|
54
72
|
end
|
55
73
|
|
74
|
+
# Update the game state by one action
|
56
75
|
def update
|
57
76
|
return unless @in_progress
|
58
77
|
|
78
|
+
do_action
|
79
|
+
handle_parties
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
# Does an action in the game
|
85
|
+
def do_action
|
86
|
+
@stream << Message.StartOfPeriod(@period) if @actions.zero?
|
87
|
+
@actions += 1
|
88
|
+
|
89
|
+
@weather.on_action
|
90
|
+
|
59
91
|
unless @fight.nil?
|
60
|
-
|
61
|
-
|
62
|
-
|
92
|
+
fight_message = @fight.next_action
|
93
|
+
@stream << fight_message
|
94
|
+
return unless fight_message.event == :FightEnded
|
63
95
|
|
64
|
-
|
65
|
-
|
66
|
-
|
96
|
+
morale_change_amount = (@fight.score[:home] - @fight.score[:away]) / 10.0
|
97
|
+
change_morale(@home, morale_change_amount)
|
98
|
+
change_morale(@away, -morale_change_amount)
|
67
99
|
|
68
|
-
|
69
|
-
end
|
100
|
+
@fight = nil
|
70
101
|
return
|
71
102
|
end
|
72
103
|
|
73
|
-
@stream << Message.StartOfPeriod(@period) if @actions.zero?
|
74
|
-
@actions += 1
|
75
|
-
|
76
104
|
if @actions >= ACTIONS_PER_PERIOD
|
77
105
|
end_period
|
78
106
|
return
|
79
107
|
end
|
80
|
-
|
81
108
|
if @faceoff
|
82
109
|
do_faceoff
|
83
110
|
return
|
84
111
|
end
|
85
112
|
|
86
|
-
@weather.on_action
|
87
|
-
|
88
113
|
case @prng.rand(5 + @shooting_chance)
|
89
114
|
when 0..4
|
90
115
|
pass
|
91
|
-
when 5
|
116
|
+
when 5, 6
|
92
117
|
check
|
93
118
|
else
|
94
119
|
shoot
|
95
120
|
end
|
96
121
|
end
|
97
122
|
|
98
|
-
|
123
|
+
# Does the random chance of partying for teams in party time
|
124
|
+
def handle_parties
|
125
|
+
return unless @partying_teams.any? && random_event_occurs?(5)
|
126
|
+
|
127
|
+
@partying_teams.each do |team|
|
128
|
+
player = team.roster.values.sample(random: @prng)
|
129
|
+
stat = player.stats.keys.sample(random: @prng)
|
130
|
+
player.stats[stat] += 0.1
|
131
|
+
@stream << Message.Partying(player)
|
132
|
+
next if @pre_morale_change_stats[player].nil?
|
133
|
+
|
134
|
+
@pre_morale_change_stats[player][stat] += 0.1
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
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
|
99
193
|
|
100
194
|
def end_period
|
101
195
|
@weather.on_period_end
|
@@ -104,9 +198,8 @@ module Hlockey
|
|
104
198
|
@period += 1
|
105
199
|
start_faceoff
|
106
200
|
|
107
|
-
if @period
|
108
|
-
!(@score[:home] == @score[:away] &&
|
109
|
-
@period <= TOTAL_PERIODS - (@total_fight_actions / ACTIONS_PER_PERIOD))
|
201
|
+
if @period > NON_OT_PERIODS &&
|
202
|
+
!(@score[:home] == @score[:away] && @period <= TOTAL_PERIODS)
|
110
203
|
# Game is over
|
111
204
|
@pre_morale_change_stats.each { |player, stats| player.stats = stats }
|
112
205
|
@weather.on_game_end
|
@@ -121,28 +214,30 @@ module Hlockey
|
|
121
214
|
def start_faceoff
|
122
215
|
@shooting_chance = 0
|
123
216
|
@faceoff = true
|
124
|
-
@
|
217
|
+
@faceoff_won = false
|
125
218
|
end
|
126
219
|
|
127
220
|
def do_faceoff
|
128
|
-
if @
|
129
|
-
# Pass opposite team to who wins the puck to switch_team_with_puck,
|
130
|
-
# so @team_with_puck & @puckless_team are set to the correct values.
|
131
|
-
switch_team_with_puck(
|
132
|
-
if action_succeeds?(@home.roster[:center].stats[:offense],
|
133
|
-
@away.roster[:center].stats[:offense])
|
134
|
-
@away
|
135
|
-
else
|
136
|
-
@home
|
137
|
-
end
|
138
|
-
)
|
139
|
-
|
140
|
-
@puck_holder = @team_with_puck.roster[:center]
|
141
|
-
@stream << Message.FaceOff(@puck_holder, @team_with_puck)
|
142
|
-
else
|
221
|
+
if @faceoff_won
|
143
222
|
pass
|
144
223
|
@faceoff = false
|
224
|
+
return
|
145
225
|
end
|
226
|
+
|
227
|
+
# Pass opposite team to who wins the puck to switch_team_with_puck,
|
228
|
+
# so @team_with_puck & @puckless_team are set to the correct values.
|
229
|
+
switch_team_with_puck(
|
230
|
+
if action_succeeds?(@home.roster[:center].stats[:offense],
|
231
|
+
@away.roster[:center].stats[:offense])
|
232
|
+
@away
|
233
|
+
else
|
234
|
+
@home
|
235
|
+
end
|
236
|
+
)
|
237
|
+
@shooting_chance = 2
|
238
|
+
@puck_holder_pos = :center
|
239
|
+
@faceoff_won = true
|
240
|
+
@stream << Message.FaceOff(puck_holder, @team_with_puck)
|
146
241
|
end
|
147
242
|
|
148
243
|
def switch_team_with_puck(team_with_puck = @team_with_puck)
|
@@ -151,74 +246,35 @@ module Hlockey
|
|
151
246
|
else
|
152
247
|
[@home, @away]
|
153
248
|
end
|
154
|
-
@shooting_chance =
|
155
|
-
|
156
|
-
|
157
|
-
def pass
|
158
|
-
sender = @puck_holder
|
159
|
-
receiver = random_puckless_non_goalie(@team_with_puck)
|
160
|
-
interceptor = random_puckless_non_goalie(@puckless_team)
|
161
|
-
|
162
|
-
if !@faceoff && try_take_puck(interceptor, 4) # Pass intercepted
|
163
|
-
@stream << Message.Pass(sender, receiver, interceptor, @team_with_puck)
|
164
|
-
return
|
165
|
-
end
|
166
|
-
|
167
|
-
@stream << Message.Pass(sender, receiver)
|
168
|
-
@puck_holder = receiver
|
169
|
-
@shooting_chance += 1
|
170
|
-
end
|
171
|
-
|
172
|
-
def check
|
173
|
-
defender = random_puckless_non_goalie(@puckless_team)
|
174
|
-
@stream << Message.Hit(@puck_holder, defender,
|
175
|
-
try_take_puck(defender, 0, :defense), @team_with_puck)
|
176
|
-
@fight_chance += 0.1
|
177
|
-
end
|
178
|
-
|
179
|
-
def shoot
|
180
|
-
if @shooting_chance < 5 &&
|
181
|
-
try_block_shot(@puckless_team.roster[@prng.rand(2).zero? ? :ldef : :rdef])
|
182
|
-
return
|
183
|
-
end
|
184
|
-
return if try_block_shot(@puckless_team.roster[:goalie])
|
185
|
-
|
186
|
-
# Goal scored
|
187
|
-
|
188
|
-
scoring_team = @team_with_puck == @home ? :home : :away
|
189
|
-
@score[scoring_team] += 1
|
190
|
-
weather.on_goal(scoring_team)
|
191
|
-
|
192
|
-
@stream << Message.ShootScore(@puck_holder, @home, @away, *@score.values)
|
193
|
-
|
194
|
-
if @prng.rand(3 + @fight_chance) > 5 &&
|
195
|
-
@total_fight_actions < TOTAL_ACTIONS - NON_OT_ACTIONS
|
196
|
-
start_fight
|
197
|
-
end
|
198
|
-
|
199
|
-
start_faceoff
|
200
|
-
@actions = ACTIONS_PER_PERIOD - 1 if @period >= NON_OT_PERIODS # Sudden death OT
|
249
|
+
@shooting_chance = 3 - @shooting_chance
|
250
|
+
@shooting_chance = 0 if @shooting_chance.negative?
|
201
251
|
end
|
202
252
|
|
253
|
+
# @param pos [Symbol] position on puckless team that tries to take the puck
|
254
|
+
# @param dis [Integer] disadvantage against taking puck
|
255
|
+
# @param stat [Symbol] stat compared to change the odds of success/failure
|
203
256
|
# @return [Boolean] if taking the puck succeeded
|
204
|
-
def try_take_puck(
|
205
|
-
return false unless action_succeeds?(
|
206
|
-
|
257
|
+
def try_take_puck(pos, dis = 0, stat = :defense)
|
258
|
+
return false unless action_succeeds?(@puckless_team.roster[pos].stats[stat] - dis,
|
259
|
+
puck_holder.stats[stat])
|
207
260
|
|
208
261
|
switch_team_with_puck
|
209
|
-
@
|
262
|
+
@puck_holder_pos = pos
|
210
263
|
|
211
264
|
true
|
212
265
|
end
|
213
266
|
|
214
267
|
# @return [Boolean] if blocking the shot succeeded
|
215
|
-
def try_block_shot(
|
268
|
+
def try_block_shot(pos, audacity: false)
|
269
|
+
blocker = @puckless_team.roster[pos]
|
270
|
+
|
216
271
|
return false unless action_succeeds?(blocker.stats[:defense],
|
217
|
-
|
272
|
+
puck_holder.stats[:offense])
|
218
273
|
|
219
274
|
@shooting_chance += 1
|
220
|
-
@stream << Message.ShootBlock(
|
221
|
-
try_take_puck(
|
275
|
+
@stream << Message.ShootBlock(puck_holder, blocker,
|
276
|
+
try_take_puck(pos), @team_with_puck,
|
277
|
+
@shooting_chance, audacity)
|
222
278
|
|
223
279
|
true
|
224
280
|
end
|
@@ -231,7 +287,7 @@ module Hlockey
|
|
231
287
|
|
232
288
|
# @param team [Team]
|
233
289
|
# @param amount [Numeric]
|
234
|
-
def
|
290
|
+
def change_morale(team, amount)
|
235
291
|
return if amount.zero?
|
236
292
|
|
237
293
|
team.roster.each_value do |player|
|
@@ -241,14 +297,34 @@ module Hlockey
|
|
241
297
|
|
242
298
|
player.stats.transform_values! { |stat| stat + amount }
|
243
299
|
end
|
300
|
+
|
301
|
+
@stream << Message.MoraleChange(team, amount)
|
244
302
|
end
|
245
303
|
|
246
304
|
# @return [Player]
|
247
|
-
def
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
|
305
|
+
def puck_holder
|
306
|
+
@team_with_puck.roster[@puck_holder_pos]
|
307
|
+
end
|
308
|
+
|
309
|
+
# @return [Symbol]
|
310
|
+
def pass_reciever
|
311
|
+
w = weights(@shooting_chance > 3)
|
312
|
+
w.delete(@puck_holder_pos)
|
313
|
+
Utils.weighted_random(w, @prng)
|
314
|
+
end
|
315
|
+
|
316
|
+
# @return [Symbol]
|
317
|
+
def defensive_pos
|
318
|
+
Utils.weighted_random(weights(@shooting_chance < 3), @prng)
|
319
|
+
end
|
320
|
+
|
321
|
+
# @return [Hash<Symbol => Integer>]
|
322
|
+
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
|
252
328
|
end
|
253
329
|
end
|
254
330
|
end
|
data/lib/hlockey/league.rb
CHANGED
@@ -3,13 +3,12 @@ require("hlockey/data")
|
|
3
3
|
require("hlockey/game")
|
4
4
|
require("hlockey/utils")
|
5
5
|
require("hlockey/version")
|
6
|
-
require("hlockey/weather")
|
7
6
|
|
8
7
|
module Hlockey
|
9
8
|
##
|
10
9
|
# The Hlockey League
|
11
10
|
class League
|
12
|
-
|
11
|
+
GAMES_IN_REGULAR_SEASON = 111
|
13
12
|
|
14
13
|
# @return [Time]
|
15
14
|
attr_reader(:start_time)
|
@@ -17,31 +16,42 @@ module Hlockey
|
|
17
16
|
# @return [Hash<Symbol => Array<Team>>]
|
18
17
|
attr_reader(:divisions)
|
19
18
|
|
20
|
-
# @return [Array<Team>]
|
21
|
-
attr_reader(:teams, :playoff_teams)
|
22
|
-
|
23
19
|
# @return [Integer]
|
24
20
|
attr_reader(:day)
|
25
21
|
|
26
22
|
# @return [Array<Game>]
|
27
23
|
attr_reader(:games, :games_in_progress)
|
28
24
|
|
25
|
+
# @return [Array<String>]
|
26
|
+
attr_reader(:alerts)
|
27
|
+
|
29
28
|
# @return [Team, nil]
|
30
29
|
attr_reader(:champion_team)
|
31
30
|
|
31
|
+
# @return [Array<Team>]
|
32
|
+
attr_reader(:teams, :playoff_teams)
|
33
|
+
|
32
34
|
def initialize
|
33
35
|
@start_time, @divisions = Data.league
|
34
36
|
@start_time.localtime
|
35
37
|
@day = 0
|
36
|
-
@games_in_progress = []
|
37
38
|
@games = []
|
39
|
+
@games_in_progress = []
|
40
|
+
@alerts = []
|
38
41
|
@champion_team = nil
|
39
42
|
@last_update_time = @start_time
|
40
43
|
@passed_updates = 0
|
41
44
|
@prng = Random.new(@start_time.to_i)
|
42
|
-
@teams = @divisions.values.
|
43
|
-
@
|
45
|
+
@teams = @divisions.values.flatten
|
46
|
+
@sorted_teams_by_wins = @teams.shuffle(random: @prng)
|
44
47
|
@playoff_teams = []
|
48
|
+
@sleepers_changes = @teams.last.roster.values.zip([18, 4, 13, 19, 6, 0])
|
49
|
+
# warm vs warm / cool vs cool
|
50
|
+
rotated_divisions = game_divisions.values.rotate
|
51
|
+
@shuffled_division_pairs = Array.new(2) do |i|
|
52
|
+
(rotated_divisions[i] + rotated_divisions[-i - 1]).shuffle(random: @prng)
|
53
|
+
end
|
54
|
+
|
45
55
|
@game_in_matchup = 3
|
46
56
|
@matchup_game_amt = 3
|
47
57
|
end
|
@@ -52,13 +62,15 @@ module Hlockey
|
|
52
62
|
def update_state
|
53
63
|
return if @champion_team
|
54
64
|
|
55
|
-
|
65
|
+
now_i = Time.now.to_i
|
66
|
+
now = Time.at(now_i - (now_i % UPDATE_FREQUENCY_SECONDS))
|
56
67
|
intervals = (now - @last_update_time).div(UPDATE_FREQUENCY_SECONDS)
|
57
68
|
|
58
69
|
return unless intervals.positive?
|
59
70
|
|
60
71
|
intervals.times do |i|
|
61
72
|
if ((i + @passed_updates) % UPDATES_PER_HOUR).zero?
|
73
|
+
update_teams
|
62
74
|
new_games
|
63
75
|
else
|
64
76
|
update_games
|
@@ -73,6 +85,41 @@ module Hlockey
|
|
73
85
|
|
74
86
|
private
|
75
87
|
|
88
|
+
def update_teams
|
89
|
+
return if @day.zero?
|
90
|
+
|
91
|
+
games_remaining = GAMES_IN_REGULAR_SEASON - ((@day - 1) * 3 + @game_in_matchup)
|
92
|
+
return if games_remaining.negative?
|
93
|
+
|
94
|
+
if games_remaining.zero?
|
95
|
+
@playoff_teams = sort_teams_by_wins(playoff_qualifiers)
|
96
|
+
@playoff_teams.each { |team| team.status = :qualified }
|
97
|
+
(@teams[...-1] - @playoff_teams).each { |team| team.status = :partying }
|
98
|
+
return
|
99
|
+
end
|
100
|
+
|
101
|
+
set_to_qualify = playoff_qualifiers
|
102
|
+
@teams[...-1].each_with_index do |team, i|
|
103
|
+
next unless games_remaining < set_to_qualify[i / 5].wins - team.wins &&
|
104
|
+
games_remaining < set_to_qualify.last.wins - team.wins
|
105
|
+
|
106
|
+
team.status = :partying
|
107
|
+
end
|
108
|
+
|
109
|
+
threat_wins = sort_teams_by_wins(@teams - set_to_qualify).first.wins
|
110
|
+
set_to_qualify.each_with_index do |team, i|
|
111
|
+
division_threat_wins = if i < 4
|
112
|
+
sort_teams_by_wins(@divisions.values[i])[1].wins
|
113
|
+
else # Team is not a division leader, so use itself
|
114
|
+
team.wins
|
115
|
+
end
|
116
|
+
next unless games_remaining < team.wins - threat_wins ||
|
117
|
+
games_remaining < team.wins - division_threat_wins
|
118
|
+
|
119
|
+
team.status = :qualified
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
76
123
|
def new_games
|
77
124
|
if @game_in_matchup != @matchup_game_amt
|
78
125
|
# New game in matchups
|
@@ -86,34 +133,58 @@ module Hlockey
|
|
86
133
|
@game_in_matchup = 1
|
87
134
|
@day += 1
|
88
135
|
|
89
|
-
case @day
|
90
|
-
when
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
@games << new_game(*pair)
|
136
|
+
case @day
|
137
|
+
when 1..27
|
138
|
+
@shuffled_division_pairs.each { |p| new_matchups(p, reverse: @day.even?) }
|
139
|
+
when 28..37
|
140
|
+
@shuffled_division_pairs.transpose.each_with_index do |pair, i|
|
141
|
+
@games << new_game(*pair, reverse: i.even?)
|
95
142
|
end
|
96
|
-
|
97
|
-
|
98
|
-
when 0 # Playoffs about to start
|
143
|
+
@shuffled_division_pairs.first.rotate!
|
144
|
+
when 38
|
99
145
|
@matchup_game_amt = 5
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
end
|
105
|
-
@playoff_teams = sort_teams_by_wins(two_best_teams_each_division).map(&:clone)
|
106
|
-
|
146
|
+
@playoff_teams = sort_teams_by_wins(playoff_qualifiers).map do |team|
|
147
|
+
cloned_team = team.clone
|
148
|
+
cloned_team.status = :playoffs
|
149
|
+
cloned_team
|
150
|
+
end
|
107
151
|
new_playoff_matchups
|
108
|
-
when
|
152
|
+
when 39, 40
|
109
153
|
@playoff_teams.select! { |team| team.wins > team.losses }
|
154
|
+
new_playoff_matchups
|
155
|
+
when 41
|
156
|
+
champion = @playoff_teams.find { |team| team.wins > team.losses }
|
157
|
+
champion.wins = 0
|
158
|
+
champion.losses = 0
|
159
|
+
@playoff_teams = [@teams.last, champion]
|
160
|
+
@alerts = ["The #{@teams.last} have awoken..."]
|
161
|
+
new_playoff_matchups
|
162
|
+
when 42
|
163
|
+
return unless @champion_team.nil?
|
164
|
+
|
165
|
+
sleepers, @champion_team = @playoff_teams
|
166
|
+
@alerts = []
|
110
167
|
|
111
|
-
if @
|
112
|
-
@
|
168
|
+
if @champion_team.to_s == "Baden Hallucinations"
|
169
|
+
@alerts << "The Baden Hallucinations have reached 5 championships."
|
170
|
+
@alerts << "They will evolve soon."
|
171
|
+
end
|
172
|
+
|
173
|
+
if sleepers.wins > @champion_team.wins
|
174
|
+
@alerts << "The #{sleepers} have beat your champions."
|
175
|
+
@alerts << "The #{sleepers} are evolving!"
|
176
|
+
old_name = sleepers.to_s
|
177
|
+
sleepers.to_s = "#{old_name}z"
|
178
|
+
@alerts << "#{old_name} -> #{sleepers}"
|
113
179
|
return
|
114
180
|
end
|
115
181
|
|
116
|
-
|
182
|
+
@alerts << "The #{sleepers} have lost."
|
183
|
+
@sleepers_changes.each do |(player, team_idx)|
|
184
|
+
player.team = @teams[team_idx]
|
185
|
+
player.team.shadows << player
|
186
|
+
@alerts << "#{player} has adventured into the #{player.team} shadows."
|
187
|
+
end
|
117
188
|
end
|
118
189
|
end
|
119
190
|
|
@@ -129,22 +200,50 @@ module Hlockey
|
|
129
200
|
team.losses = 0
|
130
201
|
end
|
131
202
|
|
132
|
-
(@playoff_teams
|
133
|
-
|
203
|
+
new_matchups(@playoff_teams, rotate: false)
|
204
|
+
end
|
205
|
+
|
206
|
+
# @param matchup_teams[Array<Team>]
|
207
|
+
# @param rotate [Boolean]
|
208
|
+
# @param reverse [Boolean]
|
209
|
+
def new_matchups(matchup_teams, rotate: true, reverse: false)
|
210
|
+
(matchup_teams.length / 2).times do |i|
|
211
|
+
@games << new_game(matchup_teams[i], matchup_teams[-i - 1], reverse: reverse)
|
134
212
|
end
|
213
|
+
matchup_teams.insert(1, matchup_teams.pop) if rotate
|
135
214
|
end
|
136
215
|
|
137
216
|
# @param home [Team]
|
138
217
|
# @param away [Team]
|
139
|
-
# @
|
140
|
-
def new_game(home, away)
|
141
|
-
|
218
|
+
# @param reverse [Boolean]
|
219
|
+
def new_game(home, away, reverse: false)
|
220
|
+
teams = [home, away]
|
221
|
+
teams.reverse! if reverse
|
222
|
+
|
223
|
+
Game.new(*teams, @prng)
|
224
|
+
end
|
225
|
+
|
226
|
+
# @return [Array<Team>]
|
227
|
+
def playoff_qualifiers
|
228
|
+
top_divs = game_divisions.values.map { |teams| sort_teams_by_wins(teams).first }
|
229
|
+
# Prevents tied teams from being unfairly decided by which division they're in
|
230
|
+
@sorted_teams_by_wins = sort_teams_by_wins(@sorted_teams_by_wins)
|
231
|
+
rest = (@sorted_teams_by_wins - top_divs).first(4)
|
232
|
+
|
233
|
+
top_divs + rest
|
142
234
|
end
|
143
235
|
|
144
236
|
# @param teams [Array<Team>]
|
145
237
|
# @return [Array<Team>]
|
146
238
|
def sort_teams_by_wins(teams)
|
147
|
-
|
239
|
+
# A stable sort (Ruby's normal sort isn't always stable)
|
240
|
+
# t.wins is negative because it sorts the opposite way than I want otherwise
|
241
|
+
teams.sort_by.with_index { |t, i| [-t.wins, i] }
|
242
|
+
end
|
243
|
+
|
244
|
+
# @return [Hash<Symbol => Array<Team>]
|
245
|
+
def game_divisions
|
246
|
+
@divisions.except(:"Sleepy Tired")
|
148
247
|
end
|
149
248
|
end
|
150
249
|
end
|