hlockey 2 → 3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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