hlockey 2 → 3

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,150 @@
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
+ REGULAR_SEASON_DAY_AMT = 38
13
+
14
+ # @return [Time]
15
+ attr_reader(:start_time)
16
+
17
+ # @return [Hash<Symbol => Array<Team>>]
18
+ attr_reader(:divisions)
19
+
20
+ # @return [Array<Team>]
21
+ attr_reader(:teams, :playoff_teams)
22
+
23
+ # @return [Integer]
24
+ attr_reader(:day)
25
+
26
+ # @return [Array<Game>]
27
+ attr_reader(:games, :games_in_progress)
28
+
29
+ # @return [Team, nil]
30
+ attr_reader(:champion_team)
31
+
32
+ def initialize
33
+ @start_time, @divisions = Data.league
34
+ @start_time.localtime
35
+ @day = 0
36
+ @games_in_progress = []
37
+ @games = []
38
+ @champion_team = nil
39
+ @last_update_time = @start_time
40
+ @passed_updates = 0
41
+ @prng = Random.new(@start_time.to_i)
42
+ @teams = @divisions.values.reduce(:+)
43
+ @shuffled_teams = @teams.shuffle(random: @prng)
44
+ @playoff_teams = []
45
+ @game_in_matchup = 3
46
+ @matchup_game_amt = 3
47
+ end
48
+
49
+ ##
50
+ # Updates the league to the current state
51
+ # This should be called whenever you need the current state of the league
52
+ def update_state
53
+ return if @champion_team
54
+
55
+ now = Time.at(Time.now.to_i)
56
+ intervals = (now - @last_update_time).div(UPDATE_FREQUENCY_SECONDS)
57
+
58
+ return unless intervals.positive?
59
+
60
+ intervals.times do |i|
61
+ if ((i + @passed_updates) % UPDATES_PER_HOUR).zero?
62
+ new_games
63
+ else
64
+ update_games
65
+ end
66
+
67
+ break if @champion_team
68
+ end
69
+
70
+ @last_update_time = now
71
+ @passed_updates += intervals
72
+ end
73
+
74
+ private
75
+
76
+ def new_games
77
+ if @game_in_matchup != @matchup_game_amt
78
+ # New game in matchups
79
+ @games.map! { |game| new_game(game.away, game.home) }
80
+ @game_in_matchup += 1
81
+ return
82
+ end
83
+
84
+ # New matchups
85
+ @games.clear
86
+ @game_in_matchup = 1
87
+ @day += 1
88
+
89
+ case @day <=> REGULAR_SEASON_DAY_AMT + 1
90
+ when -1 # In regular season
91
+ (@shuffled_teams.length / 2).times do |i|
92
+ pair = [@shuffled_teams[i], @shuffled_teams[-i - 1]]
93
+ pair.reverse! if @day > REGULAR_SEASON_DAY_AMT / 2
94
+ @games << new_game(*pair)
95
+ end
96
+
97
+ @shuffled_teams.insert(1, @shuffled_teams.pop)
98
+ when 0 # Playoffs about to start
99
+ @matchup_game_amt = 5
100
+
101
+ # get teams that qualified for playoffs, put in @playoff_teams
102
+ two_best_teams_each_division = @divisions.values.map do |teams|
103
+ sort_teams_by_wins(teams).first(2)
104
+ end.reduce(:+)
105
+ @playoff_teams = sort_teams_by_wins(two_best_teams_each_division).map(&:clone)
106
+
107
+ new_playoff_matchups
108
+ when 1 # Playoffs started
109
+ @playoff_teams.select! { |team| team.wins > team.losses }
110
+
111
+ if @playoff_teams.length == 1
112
+ @champion_team = @playoff_teams.first
113
+ return
114
+ end
115
+
116
+ new_playoff_matchups
117
+ end
118
+ end
119
+
120
+ def update_games
121
+ @games_in_progress.each(&:update)
122
+ @games_in_progress = @games.select(&:in_progress)
123
+ @divisions.transform_values!(&method(:sort_teams_by_wins))
124
+ end
125
+
126
+ def new_playoff_matchups
127
+ @playoff_teams.each do |team|
128
+ team.wins = 0
129
+ team.losses = 0
130
+ end
131
+
132
+ (@playoff_teams.length / 2).times do |i|
133
+ @games << new_game(@playoff_teams[i], @playoff_teams[-i - 1])
134
+ end
135
+ end
136
+
137
+ # @param home [Team]
138
+ # @param away [Team]
139
+ # @return [Game]
140
+ def new_game(home, away)
141
+ Game.new(home, away, @prng, Utils.weighted_random(Weather::WEIGHTS, @prng))
142
+ end
143
+
144
+ # @param teams [Array<Team>]
145
+ # @return [Array<Team>]
146
+ def sort_teams_by_wins(teams)
147
+ teams.sort { |a, b| b.wins <=> a.wins }
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,133 @@
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
+ # @return [Symbol] an action or event the message represents
10
+ attr_reader(:event)
11
+
12
+ # @param event [Symbol]
13
+ # @param fields [Array<Symbol>] fields used in string interpolation in #to_s
14
+ # @param data [Array<Object>] data corresponding to the fields
15
+ def initialize(event, fields, data)
16
+ @event = event
17
+ fields.zip(data) { |f, d| instance_variable_set("@#{f}", d) }
18
+ end
19
+
20
+ class << self
21
+ # These are messages logged to game streams
22
+
23
+ [
24
+ %i[StartOfGame],
25
+ %i[EndOfGame winning_team],
26
+ %i[StartOfPeriod period],
27
+ %i[EndOfPeriod period home away home_score away_score],
28
+ %i[FaceOff winning_player new_puck_team],
29
+ %i[Hit puck_holder defender puck_taken new_puck_team],
30
+ %i[Pass sender receiver interceptor new_puck_team],
31
+ %i[ShootScore shooter home away home_score away_score],
32
+ %i[ShootBlock shooter blocker puck_taken new_puck_team],
33
+ %i[FightStarted home_player away_player],
34
+ %i[FightAttack attacking_player defending_player blocked],
35
+ %i[PlayerJoinedFight team player],
36
+ %i[FightEnded],
37
+ %i[ChickenedOut prev_player next_player],
38
+ %i[InclineFavors team],
39
+ %i[StarsPity team],
40
+ %i[WavesWashedAway prev_player next_player]
41
+ ].each do |event, *fields|
42
+ define_method(event) { |*data| new(event, fields, data) }
43
+ end
44
+
45
+ # These are messages used elsewhere
46
+
47
+ def SeasonDay(day)
48
+ "Season #{VERSION} day #{day}"
49
+ end
50
+
51
+ def SeasonStarts(time)
52
+ time.strftime("Season #{VERSION} starts at %H:%M, %A, %B %d (%Z).")
53
+ end
54
+
55
+ def SeasonChampion(team)
56
+ "Your season #{VERSION} champions are the #{team}!"
57
+ end
58
+
59
+ def NoGames
60
+ "no games right now. it is the offseason. join the Hlockey Discord for updates"
61
+ end
62
+ end
63
+
64
+ def to_s
65
+ case @event
66
+ when :StartOfGame
67
+ "Hocky!"
68
+ when :EndOfGame
69
+ "Game over.\n#{@winning_team} win!"
70
+ when :StartOfPeriod
71
+ "Start#{of_period}"
72
+ when :EndOfPeriod
73
+ "End#{of_period}#{score}"
74
+ when :FaceOff
75
+ "#{@winning_player} wins the faceoff!#{possession_change}"
76
+ when :Hit
77
+ "#{@defender} hits #{@puck_holder}#{takes}"
78
+ when :Pass
79
+ str = "#{@sender} passes to #{@receiver}."
80
+ str += "..\nIntercepted by #{@interceptor}!#{possession_change}" if @interceptor
81
+ str
82
+ when :ShootScore
83
+ "#{shot} and scores!#{score}"
84
+ when :ShootBlock
85
+ "#{shot}...\n#{@blocker} blocks the shot#{takes}"
86
+ when :FightStarted
87
+ "#{@home_player} and #{@away_player} start fighting!"
88
+ when :FightAttack
89
+ str = "#{@attacking_player} punches #{@defending_player}!"
90
+ str += "\n#{@defending_player} blocks the punch!" if @blocked
91
+ str
92
+ when :PlayerJoinedFight
93
+ "#{@player} from #{@team} joins the fight!"
94
+ when :FightEnded
95
+ "The fight has ended."
96
+ when :ChickenedOut
97
+ "#{@prev_player} chickened out!#{replaces_for("game")}"
98
+ when :InclineFavors
99
+ "The incline favors #{@team}."
100
+ when :StarsPity
101
+ "The stars take pity on #{@team} and give them half a goal."
102
+ when :WavesWashedAway
103
+ "#{@prev_player} is washed away by the waves...#{replaces_for("season")}"
104
+ end
105
+ end
106
+
107
+ private
108
+
109
+ def of_period
110
+ " of period #{@period}."
111
+ end
112
+
113
+ def score
114
+ "\n#{@home} #{@home_score.round(1)}, #{@away} #{@away_score.round(1)}"
115
+ end
116
+
117
+ def shot
118
+ "#{@shooter} takes a shot"
119
+ end
120
+
121
+ def takes
122
+ @puck_taken ? " and takes the puck!#{possession_change}" : "!"
123
+ end
124
+
125
+ def possession_change
126
+ "\n#{@new_puck_team} have possession."
127
+ end
128
+
129
+ def replaces_for(period)
130
+ "\n#{@next_player} replaces them for the rest of the #{period}."
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,63 @@
1
+ require("hlockey/utils")
2
+
3
+ module Hlockey
4
+ class Player
5
+ attr_accessor(:stats)
6
+ attr_reader(:to_s)
7
+
8
+ @@cached_chain = nil
9
+
10
+ def initialize(prng = Random)
11
+ @to_s = make_name(prng)
12
+ # Generate stats
13
+ @stats = {}
14
+ %i[offense defense agility].each { |stat| @stats[stat] = prng.rand * 5 }
15
+ end
16
+
17
+ private
18
+
19
+ def make_name(prng)
20
+ Array.new(2) do
21
+ combination = "__"
22
+ next_letter = ""
23
+ result = ""
24
+
25
+ 10.times do
26
+ next_letters = name_chain[combination]
27
+ break if next_letters.nil?
28
+
29
+ cumulative_weights = []
30
+ next_letters.each_value do |v|
31
+ cumulative_weights << v + (cumulative_weights.last or 0)
32
+ end
33
+
34
+ next_letter = Utils.weighted_random(next_letters, prng)
35
+ break if next_letter == "_"
36
+
37
+ result += next_letter
38
+ combination = combination[1] + next_letter
39
+ end
40
+
41
+ result
42
+ end.join(" ")
43
+ end
44
+
45
+ def name_chain
46
+ if @@cached_chain.nil?
47
+ @@cached_chain = {}
48
+ File.open(File.expand_path("../../data/external/names.txt", __dir__)).each do |n|
49
+ name = "__#{n.chomp}__"
50
+ (name.length - 3).times do |i|
51
+ combination = name[i, 2]
52
+ next_letter = name[i + 2]
53
+ @@cached_chain[combination] ||= {}
54
+ @@cached_chain[combination][next_letter] ||= 0
55
+ @@cached_chain[combination][next_letter] += 1
56
+ end
57
+ end
58
+ end
59
+
60
+ @@cached_chain
61
+ end
62
+ end
63
+ end
data/lib/hlockey/team.rb CHANGED
@@ -1,35 +1,59 @@
1
- # frozen_string_literal: true
2
-
3
- module Hlockey
4
- # Created by League when data is loaded
5
- class Team
6
- private_class_method(:new)
7
- attr_accessor(:wins, :losses, :roster)
8
- attr_reader(:to_s, :emoji)
9
-
10
- def w_l
11
- "#{@wins}-#{@losses}"
12
- end
13
-
14
- # Created by League when data is loaded
15
- class Player
16
- attr_accessor(:stats, :to_s)
17
- end
18
- end
19
-
20
- # Takes symbol from Team roster & converts it to full position name
21
- def Hlockey.pos_name(pos)
22
- case pos
23
- when :lwing
24
- 'Left wing'
25
- when :rwing
26
- 'Right wing'
27
- when :ldef
28
- 'Left defender'
29
- when :rdef
30
- 'Right defender'
31
- else
32
- pos.capitalize.to_s
33
- end
34
- end
35
- end
1
+ require("hlockey/player")
2
+
3
+ module Hlockey
4
+ # Created by League when data is loaded
5
+ class Team
6
+ attr_accessor(:wins, :losses, :roster)
7
+ attr_reader(:to_s, :emoji)
8
+
9
+ # rand is only used if roster is nil
10
+ def initialize(name, emoji, roster: nil, random: Random)
11
+ @to_s = name
12
+ @emoji = emoji
13
+
14
+ if roster.nil?
15
+ @roster = {}
16
+ %i[lwing center rwing ldef goalie rdef].each do |pos|
17
+ @roster[pos] = Player.new(random)
18
+ end
19
+ else
20
+ @roster = roster
21
+ end
22
+
23
+ @wins = 0
24
+ @losses = 0
25
+ end
26
+
27
+ def random_player(random, filter_predicate = nil)
28
+ players = @roster.values
29
+ players.select!(&filter_predicate) unless filter_predicate.nil?
30
+ players.sample(random: random)
31
+ end
32
+
33
+ def w_l
34
+ "#{@wins}-#{@losses}"
35
+ end
36
+
37
+ def worst_player_pos
38
+ @roster.transform_values do |player|
39
+ player.stats.values.reduce(:+)
40
+ end.min_by { |k, v| v }.first
41
+ end
42
+
43
+ # Takes symbol from Team roster & converts it to full position name
44
+ def self.pos_name(pos)
45
+ case pos
46
+ when :lwing
47
+ "Left wing"
48
+ when :rwing
49
+ "Right wing"
50
+ when :ldef
51
+ "Left defender"
52
+ when :rdef
53
+ "Right defender"
54
+ else
55
+ pos.capitalize.to_s
56
+ end
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,12 @@
1
+ module Hlockey
2
+ module Utils
3
+ # Does a weighted random with a hash,
4
+ # where the keys are elements being randomly selected,
5
+ # and the values are the weights
6
+ def self.weighted_random(hash, prng = Random)
7
+ cumulative_weights = []
8
+ hash.each_value { |v| cumulative_weights << v + (cumulative_weights.last or 0) }
9
+ hash.keys[cumulative_weights.index { |n| prng.rand(cumulative_weights.last) < n }]
10
+ end
11
+ end
12
+ end
@@ -1,5 +1,3 @@
1
- # frozen_string_literal: true
2
-
3
- module Hlockey
4
- VERSION = '2'
5
- end
1
+ module Hlockey
2
+ VERSION = "3"
3
+ end