hlockey 2 → 4

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,115 +1,254 @@
1
- # frozen_string_literal: true
2
-
3
- require('hlockey/data')
4
- require('hlockey/game')
5
- require('hlockey/team')
6
- require('hlockey/version')
7
-
8
- module Hlockey
9
- class League
10
- attr_reader(:start_time, :divisions, :teams, :day,
11
- :games, :games_in_progress, :playoff_teams, :champion_team)
12
-
13
- def initialize
14
- @start_time, @divisions = Hlockey.load_data('league')
15
- @start_time.localtime
16
- @day = 0
17
- @games_in_progress = []
18
- @games = []
19
- @champion_team = nil
20
- @last_update_time = @start_time
21
- @passed_updates = 0
22
- @prng = Random.new(69_420 * VERSION.to_i)
23
- @teams = @divisions.values.reduce(:+)
24
- @shuffled_teams = teams.shuffle(random: @prng)
25
- @playoff_teams = nil
26
- @game_in_matchup = 3
27
- end
28
-
29
- def update_state
30
- return if @champion_team
31
-
32
- now = Time.at(Time.now.to_i)
33
- five_sec_intervals = (now - @last_update_time).div(5)
34
-
35
- return unless five_sec_intervals.positive?
36
-
37
- five_sec_intervals.times do |i|
38
- if ((i + @passed_updates) % 720).zero?
39
- new_games
40
- else
41
- update_games
42
- end
43
-
44
- break if @champion_team
45
- end
46
-
47
- @last_update_time = now
48
- @passed_updates += five_sec_intervals
49
- end
50
-
51
- private
52
-
53
- def new_games
54
- if @game_in_matchup != (@day > 38 ? 5 : 3)
55
- # New game in matchups
56
- @games.map! { |game| Game.new(game.away, game.home, @prng) }
57
- @game_in_matchup += 1
58
- return
59
- end
60
-
61
- # New matchups
62
- @games.clear
63
- @game_in_matchup = 1
64
- @day += 1
65
-
66
- case @day <=> 39
67
- when -1
68
- (@shuffled_teams.length / 2).times do |i|
69
- pair = [@shuffled_teams[i], @shuffled_teams[-i - 1]]
70
- @games << Game.new(*(@day > 19 ? pair : pair.reverse), @prng)
71
- end
72
-
73
- @shuffled_teams.insert 1, @shuffled_teams.pop
74
- when 0
75
- @playoff_teams = sort_teams_by_wins(
76
- @divisions.values.map do |teams|
77
- sort_teams_by_wins(teams).first(2)
78
- end.reduce(:+)
79
- ).map(&:clone)
80
-
81
- new_playoff_matchups
82
- when 1
83
- @playoff_teams.select! { |team| team.wins > team.losses }
84
-
85
- if @playoff_teams.length == 1
86
- @champion_team = @playoff_teams[0]
87
- return
88
- end
89
-
90
- new_playoff_matchups
91
- end
92
- end
93
-
94
- def update_games
95
- @games_in_progress.each(&:update)
96
- @games_in_progress = @games.select(&:in_progress)
97
- @divisions.transform_values!(&method(:sort_teams_by_wins))
98
- end
99
-
100
- def new_playoff_matchups
101
- @playoff_teams.each do |team|
102
- team.wins = 0
103
- team.losses = 0
104
- end
105
-
106
- (@playoff_teams.length / 2).times do |i|
107
- @games << Game.new(@playoff_teams[i], @playoff_teams[-i - 1], @prng)
108
- end
109
- end
110
-
111
- def sort_teams_by_wins(teams)
112
- teams.sort { |a, b| b.wins <=> a.wins }
113
- end
114
- end
115
- end
1
+ require("hlockey/constants")
2
+ require("hlockey/data")
3
+ require("hlockey/game")
4
+ require("hlockey/utils")
5
+ require("hlockey/version")
6
+ require("hlockey/weather")
7
+
8
+ module Hlockey
9
+ ##
10
+ # The Hlockey League
11
+ class League
12
+ GAMES_IN_REGULAR_SEASON = 114
13
+
14
+ # @return [Time]
15
+ attr_reader(:start_time)
16
+
17
+ # @return [Hash<Symbol => Array<Team>>]
18
+ attr_reader(:divisions)
19
+
20
+ # @return [Integer]
21
+ attr_reader(:day)
22
+
23
+ # @return [Array<Game>]
24
+ attr_reader(:games, :games_in_progress)
25
+
26
+ # @return [Array<String>]
27
+ attr_reader(:alerts)
28
+
29
+ # @return [Team, nil]
30
+ attr_reader(:champion_team)
31
+
32
+ # @return [Array<Team>]
33
+ attr_reader(:teams, :playoff_teams)
34
+
35
+ def initialize
36
+ @start_time, @divisions = Data.league
37
+ @start_time.localtime
38
+ @day = 0
39
+ @games = []
40
+ @games_in_progress = []
41
+ @alerts = []
42
+ @champion_team = nil
43
+ @last_update_time = @start_time
44
+ @passed_updates = 0
45
+ @prng = Random.new(@start_time.to_i)
46
+ @teams = @divisions.values.flatten
47
+ @sorted_teams_by_wins = @teams.shuffle(random: @prng)
48
+ @playoff_teams = []
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
+
55
+ @game_in_matchup = 3
56
+ @matchup_game_amt = 3
57
+ end
58
+
59
+ ##
60
+ # Updates the league to the current state
61
+ # This should be called whenever you need the current state of the league
62
+ def update_state
63
+ return if @champion_team
64
+
65
+ now = Time.at(Time.now.to_i)
66
+ intervals = (now - @last_update_time).div(UPDATE_FREQUENCY_SECONDS)
67
+
68
+ return unless intervals.positive?
69
+
70
+ intervals.times do |i|
71
+ if ((i + @passed_updates) % UPDATES_PER_HOUR).zero?
72
+ update_teams
73
+ new_games
74
+ else
75
+ update_games
76
+ end
77
+
78
+ break if @champion_team
79
+ end
80
+
81
+ @last_update_time = now
82
+ @passed_updates += intervals
83
+ end
84
+
85
+ private
86
+
87
+ def update_teams
88
+ return if @day.zero?
89
+
90
+ # Boost Sleepers roster
91
+ unless @day > 41 # Unless Sleepers are playing
92
+ @teams.last.roster.each_value do |player|
93
+ stat_change_amount = (10 - player.stats.values.reduce(:+)) / 129
94
+ player.stats.transform_values! { |stat| stat + stat_change_amount }
95
+ end
96
+ end
97
+
98
+ # Update team statuses
99
+ games_remaining = GAMES_IN_REGULAR_SEASON - ((@day - 1) * 3 + @game_in_matchup)
100
+ return if games_remaining.negative?
101
+
102
+ if games_remaining.zero?
103
+ @playoff_teams = sort_teams_by_wins(playoff_qualifiers)
104
+ @playoff_teams.each { |team| team.status = :qualified }
105
+ (@teams[...-1] - @playoff_teams).each { |team| team.status = :partying }
106
+ return
107
+ end
108
+
109
+ set_to_qualify = playoff_qualifiers
110
+ @teams[...-1].each_with_index do |team, i|
111
+ next unless games_remaining < set_to_qualify[i / 5].wins - team.wins &&
112
+ games_remaining < set_to_qualify.last.wins - team.wins
113
+
114
+ team.status = :partying
115
+ end
116
+
117
+ threat_wins = sort_teams_by_wins(@teams - set_to_qualify).first.wins
118
+ set_to_qualify.each_with_index do |team, i|
119
+ division_threat_wins = if i < 4
120
+ sort_teams_by_wins(@divisions.values[i])[1].wins
121
+ else # Team is not a division leader, so use itself
122
+ team.wins
123
+ end
124
+ next unless games_remaining < team.wins - threat_wins ||
125
+ games_remaining < team.wins - division_threat_wins
126
+
127
+ team.status = :qualified
128
+ end
129
+ end
130
+
131
+ def new_games
132
+ if @game_in_matchup != @matchup_game_amt
133
+ # New game in matchups
134
+ @games.map! { |game| new_game(game.away, game.home) }
135
+ @game_in_matchup += 1
136
+ return
137
+ end
138
+
139
+ # New matchups
140
+ @games.clear
141
+ @game_in_matchup = 1
142
+ @day += 1
143
+
144
+ case @day
145
+ when 1..27
146
+ @shuffled_division_pairs.each { |p| new_matchups(p, reverse: @day.even?) }
147
+ when 28..38
148
+ @shuffled_division_pairs.transpose.each_with_index do |pair, i|
149
+ @games << new_game(*pair, reverse: i.even?)
150
+ end
151
+ @shuffled_division_pairs.first.rotate!
152
+ when 39
153
+ @matchup_game_amt = 5
154
+ @playoff_teams = sort_teams_by_wins(playoff_qualifiers).map do |team|
155
+ cloned_team = team.clone
156
+ cloned_team.status = :playoffs
157
+ cloned_team
158
+ end
159
+ new_playoff_matchups
160
+ when 40, 41
161
+ @playoff_teams.select! { |team| team.wins > team.losses }
162
+ new_playoff_matchups
163
+ when 42
164
+ @matchup_game_amt = 1
165
+ champion = @playoff_teams.find { |team| team.wins > team.losses }
166
+ champion.wins = 0
167
+ champion.losses = 0
168
+ @playoff_teams = [@teams.last, champion]
169
+ @alerts = ["The Sleepers have awoken..."]
170
+ @games = [Game.new(*@playoff_teams, @prng, Stars)]
171
+ when 43, 44
172
+ @games = [Game.new(*@playoff_teams, @prng, Stars)]
173
+ when 45
174
+ return unless @champion_team.nil?
175
+
176
+ sleepers, @champion_team = @playoff_teams
177
+ @alerts = []
178
+
179
+ if sleepers.wins > @champion_team.wins
180
+ @alerts << "The Sleepers have beat your champions."
181
+ @alerts << "The Sleepers are evolving!"
182
+ sleepers.to_s = "Sleepersz"
183
+ @alerts << "Sleepers -> Sleepersz"
184
+ return
185
+ end
186
+
187
+ @alerts << "The Sleepers have lost."
188
+ sleepers.roster.values.zip([18, 4, 13, 19, 6, 0]) do |player, team_idx|
189
+ player.team = @teams[team_idx]
190
+ player.team.shadows << player
191
+ @alerts << "#{player} has adventured into the #{player.team} shadows."
192
+ end
193
+ end
194
+ end
195
+
196
+ def update_games
197
+ @games_in_progress.each(&:update)
198
+ @games_in_progress = @games.select(&:in_progress)
199
+ @divisions.transform_values!(&method(:sort_teams_by_wins))
200
+ end
201
+
202
+ def new_playoff_matchups
203
+ @playoff_teams.each do |team|
204
+ team.wins = 0
205
+ team.losses = 0
206
+ end
207
+
208
+ new_matchups(@playoff_teams, rotate: false)
209
+ end
210
+
211
+ # @param matchup_teams[Array<Team>]
212
+ # @param rotate [Boolean]
213
+ # @param reverse [Boolean]
214
+ def new_matchups(matchup_teams, rotate: true, reverse: false)
215
+ (matchup_teams.length / 2).times do |i|
216
+ @games << new_game(matchup_teams[i], matchup_teams[-i - 1], reverse: reverse)
217
+ end
218
+ matchup_teams.insert(1, matchup_teams.pop) if rotate
219
+ end
220
+
221
+ # @param home [Team]
222
+ # @param away [Team]
223
+ # @param reverse [Boolean]
224
+ def new_game(home, away, reverse: false)
225
+ teams = [home, away]
226
+ teams.reverse! if reverse
227
+
228
+ Game.new(*teams, @prng, Weather::WEATHERS.sample(random: @prng))
229
+ end
230
+
231
+ # @return [Array<Team>]
232
+ def playoff_qualifiers
233
+ top_divs = game_divisions.values.map { |teams| sort_teams_by_wins(teams).first }
234
+ # Prevents tied teams from being unfairly decided by which division they're in
235
+ @sorted_teams_by_wins = sort_teams_by_wins(@sorted_teams_by_wins)
236
+ rest = (@sorted_teams_by_wins - top_divs).first(4)
237
+
238
+ top_divs + rest
239
+ end
240
+
241
+ # @param teams [Array<Team>]
242
+ # @return [Array<Team>]
243
+ def sort_teams_by_wins(teams)
244
+ # A stable sort (Ruby's normal sort isn't always stable)
245
+ # t.wins is negative because it sorts the opposite way than I want otherwise
246
+ teams.sort_by.with_index { |t, i| [-t.wins, i] }
247
+ end
248
+
249
+ # @return [Hash<Symbol => Array<Team>]
250
+ def game_divisions
251
+ @divisions.except(:"Sleepy Tired")
252
+ end
253
+ end
254
+ end
@@ -0,0 +1,182 @@
1
+ require("hlockey/version")
2
+
3
+ module Hlockey
4
+ ##
5
+ # Can be sent by a Game (in Game#stream) or a commonly used text
6
+ class Message
7
+ private_class_method(:new)
8
+
9
+ @colorize = ->(_color, string) { string }
10
+
11
+ # @return [Symbol] an action or event the message represents
12
+ attr_reader(:event)
13
+
14
+ # @param event [Symbol]
15
+ # @param fields [Array<Symbol>] fields used in string interpolation in #to_s
16
+ # @param data [Array<Object>] data corresponding to the fields
17
+ def initialize(event, fields, data)
18
+ self.class.attr_reader(*fields)
19
+
20
+ @event = event
21
+ fields.zip(data) { |f, d| instance_variable_set("@#{f}", d) }
22
+ end
23
+
24
+ class << self
25
+ # Variable/method controlling how coloring players/teams is done
26
+ # The default is to not color things at all
27
+
28
+ attr_writer(:colorize)
29
+
30
+ # @param obj [Team, Player]
31
+ # @return String
32
+ def color(obj)
33
+ col = obj.instance_of?(Team) ? obj.color : obj.team.color
34
+
35
+ @colorize.call(col, obj.to_s)
36
+ end
37
+
38
+ # These are messages logged to game streams
39
+ [
40
+ %i[StartOfGame],
41
+ %i[EndOfGame winning_team],
42
+ %i[StartOfPeriod period],
43
+ %i[EndOfPeriod period home away home_score away_score],
44
+ %i[FaceOff winning_player new_puck_team],
45
+ %i[Hit puck_holder defender puck_taken new_puck_team shooting_chance],
46
+ %i[Pass sender receiver shooting_chance interceptor new_puck_team],
47
+ %i[ShootScore shooter home away home_score away_score audacity],
48
+ %i[ShootBlock shooter blocker puck_taken new_puck_team shooting_chance audacity],
49
+ %i[Partying player],
50
+ %i[FightStarted home_player away_player],
51
+ %i[FightAttack attacking_player defending_player blocked],
52
+ %i[PlayerJoinedFight team player],
53
+ %i[FightEnded],
54
+ %i[MoraleChange team amount],
55
+ %i[ChickenedOut prev_player next_player],
56
+ %i[InclineFavors team],
57
+ %i[StarsAlign team],
58
+ %i[WavesWashedAway prev_player next_player]
59
+ ].each do |event, *fields|
60
+ define_method(event) { |*data| new(event, fields, data) }
61
+ end
62
+
63
+ # These are messages used elsewhere
64
+
65
+ def SeasonDay(day)
66
+ "Season #{VERSION} day #{day}"
67
+ end
68
+
69
+ def SeasonStarts(time)
70
+ time.strftime("Season #{VERSION} starts at %H:%M, %A, %B %d (%Z).")
71
+ end
72
+
73
+ def SeasonChampion(team)
74
+ "Your season #{VERSION} champions are the #{team}!"
75
+ end
76
+
77
+ def NoGames
78
+ "no games right now. it is the offseason. join the Hlockey Discord for updates"
79
+ end
80
+ end
81
+
82
+ def to_s
83
+ case @event
84
+ when :StartOfGame
85
+ "Hocky!"
86
+ when :EndOfGame
87
+ "Game over.\n#{color(@winning_team)} win!"
88
+ when :StartOfPeriod
89
+ "Start#{of_period}"
90
+ when :EndOfPeriod
91
+ "End#{of_period}#{score}"
92
+ when :FaceOff
93
+ "#{color(@winning_player)} wins the faceoff!#{possession_change}"
94
+ when :Hit
95
+ str = "#{color(@defender)} hits #{color(@puck_holder)}"
96
+ str += takes + score_chance if @puck_taken
97
+ str
98
+ when :Pass
99
+ str = "#{color(@sender)} passes to #{color(@receiver)}."
100
+ unless @interceptor.nil?
101
+ str += "..\nIntercepted by #{color(@interceptor)}!#{possession_change}"
102
+ end
103
+ str += score_chance
104
+ str
105
+ when :ShootScore
106
+ "#{shot} and scores!#{score}"
107
+ when :ShootBlock
108
+ "#{shot}...\n#{color(@blocker)} blocks the shot#{takes}#{score_chance}"
109
+ when :Partying
110
+ "#{color(@player)} is partying!"
111
+ when :FightStarted
112
+ "#{color(@home_player)} and #{color(@away_player)} start fighting!"
113
+ when :FightAttack
114
+ str = "#{color(@attacking_player)} punches #{color(@defending_player)}!"
115
+ str += "\n#{color(@defending_player)} blocks the punch!" if @blocked
116
+ str
117
+ when :PlayerJoinedFight
118
+ "#{color(@player)} from #{color(@team)} joins the fight!"
119
+ when :FightEnded
120
+ "The fight has ended."
121
+ when :MoraleChange
122
+ "#{color(@team)} #{@amount.negative? ? "loses" : "gains"} #{@amount.abs} morale."
123
+ when :ChickenedOut
124
+ "#{color(@prev_player)} chickened out!#{replaces("game")}"
125
+ when :InclineFavors
126
+ "The incline favors #{color(@team)}."
127
+ when :StarsAlign
128
+ "The stars align for #{color(@team)}. They get half a goal."
129
+ when :WavesWashedAway
130
+ "#{color(@prev_player)} is washed away by the waves...#{replaces}"
131
+ end
132
+ end
133
+
134
+ private
135
+
136
+ def color(obj)
137
+ self.class.color(obj)
138
+ end
139
+
140
+ def score_chance
141
+ return "" if @shooting_chance.nil?
142
+
143
+ chance_str = case @shooting_chance
144
+ when 0..2
145
+ "none"
146
+ when 3, 4
147
+ "low"
148
+ when 5, 6
149
+ "medium"
150
+ else
151
+ "high"
152
+ end
153
+ "\nChance of scoring: #{chance_str} (#{@shooting_chance})"
154
+ end
155
+
156
+ def of_period
157
+ " of period #{@period}."
158
+ end
159
+
160
+ def score
161
+ "\n#{color(@home)} #{@home_score.round(2)}, #{color(@away)} #{@away_score.round(2)}"
162
+ end
163
+
164
+ def shot
165
+ "#{color(@shooter)} takes a#{"n audacious" if @audacity} shot"
166
+ end
167
+
168
+ def takes
169
+ @puck_taken ? " and takes the puck!#{possession_change}" : "!"
170
+ end
171
+
172
+ def possession_change
173
+ "\n#{color(@new_puck_team)} have possession."
174
+ end
175
+
176
+ def replaces(period = nil)
177
+ str = "\n#{color(@next_player)} replaces them"
178
+ str += " for the rest of the #{period}." unless period.nil?
179
+ str
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,73 @@
1
+ require("hlockey/utils")
2
+
3
+ module Hlockey
4
+ class Player
5
+ attr_accessor(:team, :stats)
6
+ attr_reader(:to_s)
7
+
8
+ @cached_chain = nil
9
+
10
+ # @param team [Team]
11
+ # @param prng [#rand]
12
+ def initialize(team, prng = Random)
13
+ @to_s = make_name(prng)
14
+ @team = team
15
+ # Generate stats
16
+ @stats = {}
17
+ %i[offense defense agility].each { |stat| @stats[stat] = prng.rand * 5 }
18
+ end
19
+
20
+ # @return [Hash<Symbol => String>]
21
+ def stat_strings
22
+ @stats.transform_values do |stat|
23
+ stat.instance_of?(String) ? stat : stat.round(2).to_s.ljust(4, "0")
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def make_name(prng)
30
+ Array.new(2) do
31
+ combination = "__"
32
+ next_letter = ""
33
+ result = ""
34
+
35
+ 10.times do
36
+ next_letters = name_chain[combination]
37
+ break if next_letters.nil?
38
+
39
+ cumulative_weights = []
40
+ next_letters.each_value do |v|
41
+ cumulative_weights << v + (cumulative_weights.last or 0)
42
+ end
43
+
44
+ next_letter = Utils.weighted_random(next_letters, prng)
45
+ break if next_letter == "_"
46
+
47
+ result += next_letter
48
+ combination = combination[1] + next_letter
49
+ end
50
+
51
+ result
52
+ end.join(" ")
53
+ end
54
+
55
+ def name_chain
56
+ if @cached_chain.nil?
57
+ @cached_chain = {}
58
+ File.open(File.expand_path("../../data/external/names.txt", __dir__)).each do |n|
59
+ name = "__#{n.chomp}__"
60
+ (name.length - 3).times do |i|
61
+ combination = name[i, 2]
62
+ next_letter = name[i + 2]
63
+ @cached_chain[combination] ||= {}
64
+ @cached_chain[combination][next_letter] ||= 0
65
+ @cached_chain[combination][next_letter] += 1
66
+ end
67
+ end
68
+ end
69
+
70
+ @cached_chain
71
+ end
72
+ end
73
+ end