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.
@@ -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