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,2 +1,2 @@
1
- :GitHub: https://github.com/Hlockey
2
- :Discord: https://discord.gg/hQMRZyexUB
1
+ :GitHub: https://github.com/Hlockey
2
+ :Discord: https://discord.gg/hQMRZyexUB
@@ -0,0 +1,4 @@
1
+ module Hlockey
2
+ UPDATE_FREQUENCY_SECONDS = 5
3
+ UPDATES_PER_HOUR = 3600 / UPDATE_FREQUENCY_SECONDS
4
+ end
data/lib/hlockey/data.rb CHANGED
@@ -1,13 +1,17 @@
1
- # frozen_string_literal: true
2
-
3
- require('yaml')
4
-
5
- module Hlockey
6
- # Loads data for the current Hlockey season.
7
- # +category+ can be 'league', 'election', 'information', or 'links'.
8
- # If it is anything else, there will be an error.
9
- def Hlockey.load_data(category)
10
- # If this is only used on data included with the gem, it should be safe
11
- YAML.unsafe_load_file(File.expand_path("data/#{category}.yaml", __dir__))
12
- end
13
- end
1
+ require("hlockey/team") # Class stored in league data
2
+ require("yaml")
3
+
4
+ module Hlockey
5
+ ##
6
+ # Module containing methods to load Hlockey data
7
+ module Data
8
+ class << self
9
+ data_dir = File.expand_path("../../data", __dir__)
10
+
11
+ Dir.glob("*.yaml", base: data_dir).each do |data_file|
12
+ category = File.basename(data_file, ".yaml")
13
+ define_method(category) { YAML.unsafe_load_file("#{data_dir}/#{data_file}") }
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,30 @@
1
+ module Hlockey
2
+ class Game
3
+ ##
4
+ # For classes that deal with actions within a Hlockey game.
5
+ # Any class including this must have instance variables indicated by the readers
6
+ module Actions
7
+ # @return [Team] the teams in the game
8
+ attr_reader(:home, :away)
9
+
10
+ # @return [Random] should be the same as the league's
11
+ attr_reader(:prng)
12
+
13
+ # @return [Integer] how many actions have passed
14
+ attr_reader(:actions)
15
+
16
+ # @return [Hash<Symbol => Integer>] the score of each team
17
+ attr_reader(:score)
18
+
19
+ # @return [Boolean] if the sequence of actions is still in progress
20
+ attr_reader(:in_progress)
21
+
22
+ # @param succeed_boost [Numeric] increases chance of action succeeding
23
+ # @param fail_boost [Numeric] decreases chance of action succeeding
24
+ # @return [Boolean] if the action succeeded
25
+ def action_succeeds?(succeed_boost, fail_boost)
26
+ @prng.rand(10) + succeed_boost - fail_boost > 4
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,83 @@
1
+ require("hlockey/game/actions")
2
+ require("hlockey/message")
3
+
4
+ module Hlockey
5
+ class Game
6
+ ##
7
+ # A fight within a Hlockey game
8
+ class Fight
9
+ include(Actions)
10
+
11
+ # @return [Hash<Symbol => Array<Player>>] the players in the fight
12
+ attr_reader(:players)
13
+
14
+ # @param game [Game] the game the fight is a part of
15
+ def initialize(game)
16
+ @home = game.home
17
+ @away = game.away
18
+ @prng = game.prng
19
+ @actions = 0
20
+ @players = {
21
+ home: [@home.random_player(@prng)],
22
+ away: [@away.random_player(@prng)]
23
+ }
24
+ @score = { home: 0, away: 0 }
25
+ @in_progress = true
26
+ end
27
+
28
+ # @return [Message] the message to add to the game stream
29
+ def next_action
30
+ @actions += 1
31
+
32
+ case @prng.rand(@actions)
33
+ when 0..3 # attack
34
+ if rand_is_home?
35
+ attack(:home, :away)
36
+ else
37
+ attack(:away, :home)
38
+ end
39
+ when 4..7 # player joins
40
+ team_joining_sym = rand_is_home? ? :away : :home
41
+ team_joining = send(team_joining_sym)
42
+
43
+ player_joining = team_joining.random_player(
44
+ @prng,
45
+ proc { |player| !@players[team_joining_sym].include?(player) }
46
+ )
47
+ @players[team_joining_sym] << player_joining
48
+
49
+ Message.PlayerJoinedFight(team_joining, player_joining)
50
+ else # fight ends
51
+ @in_progress = false
52
+
53
+ Message.FightEnded
54
+ end
55
+ end
56
+
57
+ private
58
+
59
+ # @param attacking_team [Team]
60
+ # @param defending_team [Team]
61
+ # @return [Message] returned by #next_action for same purpose
62
+ def attack(attacking_team, defending_team)
63
+ attacking_player = @players[attacking_team].sample(random: @prng)
64
+ defending_player = @players[defending_team].sample(random: @prng)
65
+
66
+ blocked = !action_succeeds?(attacking_player.stats[:offense],
67
+ defending_player.stats[:defense])
68
+
69
+ @score[attacking_team] += 1 unless blocked
70
+
71
+ Message.FightAttack(attacking_player, defending_player, blocked)
72
+ end
73
+
74
+ # @return [Boolean] if randomly selected team is home team
75
+ def rand_is_home?
76
+ home_player_amount = @home.roster.values.length
77
+ away_player_amount = @away.roster.values.length
78
+
79
+ @prng.rand(home_player_amount + away_player_amount) < home_player_amount
80
+ end
81
+ end
82
+ end
83
+ end
data/lib/hlockey/game.rb CHANGED
@@ -1,159 +1,254 @@
1
- # frozen_string_literal: true
2
-
3
- require('hlockey/messages')
4
-
5
- module Hlockey
6
- class Game
7
- attr_reader(:home, :away, :stream, :score, :in_progress)
8
-
9
- def initialize(home, away, prng)
10
- @home = home
11
- @away = away
12
- @prng = prng
13
- @stream = [Messages.StartOfGame]
14
- @score = { home: 0, away: 0 }
15
- @in_progress = true
16
- @actions = 0
17
- @period = 1
18
- @face_off = true
19
- @team_with_puck = nil
20
- @puckless_team = nil
21
- @puck_holder = nil
22
- @shooting_chance = 0
23
- end
24
-
25
- def to_s
26
- "#{@home} vs #{@away}"
27
- end
28
-
29
- def update
30
- return unless @in_progress
31
-
32
- @stream << Messages.StartOfPeriod(@period) if @actions.zero?
33
- @actions += 1
34
-
35
- if @actions == 60
36
- @stream << Messages.EndOfPeriod(@period, @home, @away, *@score.values)
37
- @actions = 0
38
- @period += 1
39
- start_faceoff
40
-
41
- if @period > 3 && !(@score[:home] == @score[:away] && @period < 12)
42
- # Game is over
43
- @in_progress = false
44
-
45
- winner, loser = @score[:away] > @score[:home] ?
46
- [@away, @home] : [@home, @away]
47
-
48
- @stream << Messages.EndOfGame(winner)
49
- winner.wins += 1
50
- loser.losses += 1
51
- end
52
- return
53
- end
54
-
55
- if @face_off
56
- if @puck_holder.nil?
57
- # Pass opposite team to who wins the puck to switch_team_with_puck,
58
- # so @team_with_puck & @puckless_team are set to the correct values.
59
- switch_team_with_puck(
60
- action_succeeds?(@home.roster[:center].stats[:offense],
61
- @away.roster[:center].stats[:offense]) ? @away : @home
62
- )
63
-
64
- @puck_holder = @team_with_puck.roster[:center]
65
- @stream << Messages.FaceOff(@puck_holder, @team_with_puck)
66
- else
67
- pass
68
- @face_off = false
69
- end
70
- return
71
- end
72
-
73
- case @prng.rand 5 + @shooting_chance
74
- when 0..4
75
- pass
76
- when 5..6 # Check
77
- check
78
- else # Shoot
79
- unless @shooting_chance < 5 &&
80
- try_block_shot(@puckless_team.roster[@prng.rand(2).zero? ?
81
- :ldef : :rdef]) ||
82
- try_block_shot(@puckless_team.roster[:goalie])
83
- @score[@team_with_puck == @home ? :home : :away] += 1
84
-
85
- @stream << Messages.ShootScore(@puck_holder, @home, @away, *@score.values)
86
-
87
- start_faceoff
88
- @actions = 59 if @period > 3 # Sudden death overtime
89
- end
90
- end
91
- end
92
-
93
- private
94
-
95
- def action_succeeds?(helping_stat, hindering_stat)
96
- @prng.rand(10) + helping_stat - hindering_stat > 4
97
- end
98
-
99
- def start_faceoff
100
- @shooting_chance = 0
101
- @face_off = true
102
- @puck_holder = nil
103
- end
104
-
105
- def switch_team_with_puck(team_with_puck = @team_with_puck)
106
- @team_with_puck, @puckless_team = team_with_puck == @home ?
107
- [@away, @home] : [@home, @away]
108
- @shooting_chance = 0
109
- end
110
-
111
- def pass
112
- sender = @puck_holder
113
- receiver = puckless_non_goalie(@team_with_puck)
114
- interceptor = puckless_non_goalie
115
-
116
- if !@face_off && try_take_puck(interceptor, 4) # Pass intercepted
117
- @stream << Messages.Pass(sender, receiver, interceptor, @team_with_puck)
118
- return
119
- end
120
-
121
- @stream << Messages.Pass(sender, receiver)
122
- @puck_holder = receiver
123
- @shooting_chance += 1
124
- end
125
-
126
- def check
127
- defender = puckless_non_goalie
128
- @stream << Messages.Hit(@puck_holder, defender,
129
- try_take_puck(defender, 0, :defense), @team_with_puck)
130
- end
131
-
132
- def try_take_puck(player, dis = 0, stat = :agility)
133
- return false unless action_succeeds?(player.stats[stat] - dis,
134
- @puck_holder.stats[stat])
135
-
136
- switch_team_with_puck
137
- @puck_holder = player
138
-
139
- true
140
- end
141
-
142
- def try_block_shot(blocker)
143
- return false unless action_succeeds?(blocker.stats[:defense],
144
- @puck_holder.stats[:offense])
145
-
146
- @shooting_chance += 1
147
- @stream << Messages.ShootBlock(@puck_holder, blocker,
148
- try_take_puck(blocker), @team_with_puck)
149
-
150
- true
151
- end
152
-
153
- def puckless_non_goalie(team = @puckless_team)
154
- team.roster.values.select do |p|
155
- p != team.roster[:goalie] and p != @puck_holder
156
- end.sample(random: @prng)
157
- end
158
- end
159
- end
1
+ require("hlockey/game/actions")
2
+ require("hlockey/game/fight")
3
+ require("hlockey/constants")
4
+ require("hlockey/message")
5
+
6
+ module Hlockey
7
+ ##
8
+ # A single game of Hlockey
9
+ class Game
10
+ include(Actions)
11
+
12
+ ACTIONS_PER_PERIOD = 60
13
+ NON_OT_PERIODS = 4
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
17
+
18
+ # @return [Array<Message>]
19
+ attr_reader(:stream)
20
+
21
+ # @return [Weather]
22
+ attr_reader(:weather)
23
+
24
+ # @param home [Team]
25
+ # @param away [Team]
26
+ # @param prng [Random] should be League#prng
27
+ # @param weather [Class<Weather>]
28
+ def initialize(home, away, prng, weather)
29
+ @home = home
30
+ @away = away
31
+ @prng = prng
32
+ @weather = weather.new(self)
33
+ @stream = [Message.StartOfGame]
34
+ @score = { home: 0, away: 0 }
35
+ @in_progress = true
36
+ @actions = 0
37
+ @period = 1
38
+ @faceoff = true
39
+ @team_with_puck = nil
40
+ @puckless_team = nil
41
+ @puck_holder = nil
42
+ @shooting_chance = 0
43
+ @fight = nil
44
+ @fight_chance = 0
45
+ @total_fight_actions = 0
46
+ @pre_morale_change_stats = {}
47
+
48
+ @weather.on_game_start
49
+ end
50
+
51
+ # @return [String]
52
+ def to_s
53
+ "#{@home} vs #{@away}"
54
+ end
55
+
56
+ def update
57
+ return unless @in_progress
58
+
59
+ unless @fight.nil?
60
+ @stream << @fight.next_action
61
+ if @stream.last.event == :FightEnded
62
+ @total_fight_actions += @fight.actions
63
+
64
+ morale_change_amount = (@fight.score[:home] - @fight.score[:away]) * 0.05
65
+ boost_morale(@home, morale_change_amount)
66
+ boost_morale(@away, -morale_change_amount)
67
+
68
+ @fight = nil
69
+ end
70
+ return
71
+ end
72
+
73
+ @stream << Message.StartOfPeriod(@period) if @actions.zero?
74
+ @actions += 1
75
+
76
+ if @actions >= ACTIONS_PER_PERIOD
77
+ end_period
78
+ return
79
+ end
80
+
81
+ if @faceoff
82
+ do_faceoff
83
+ return
84
+ end
85
+
86
+ @weather.on_action
87
+
88
+ case @prng.rand(5 + @shooting_chance)
89
+ when 0..4
90
+ pass
91
+ when 5..6
92
+ check
93
+ else
94
+ shoot
95
+ end
96
+ end
97
+
98
+ private
99
+
100
+ def end_period
101
+ @weather.on_period_end
102
+ @stream << Message.EndOfPeriod(@period, @home, @away, *@score.values)
103
+ @actions = 0
104
+ @period += 1
105
+ start_faceoff
106
+
107
+ if @period >= NON_OT_PERIODS &&
108
+ !(@score[:home] == @score[:away] &&
109
+ @period <= TOTAL_PERIODS - (@total_fight_actions / ACTIONS_PER_PERIOD))
110
+ # Game is over
111
+ @pre_morale_change_stats.each { |player, stats| player.stats = stats }
112
+ @weather.on_game_end
113
+ @in_progress = false
114
+ winner, loser = @score[:away] > @score[:home] ? [@away, @home] : [@home, @away]
115
+ @stream << Message.EndOfGame(winner)
116
+ winner.wins += 1
117
+ loser.losses += 1
118
+ end
119
+ end
120
+
121
+ def start_faceoff
122
+ @shooting_chance = 0
123
+ @faceoff = true
124
+ @puck_holder = nil
125
+ end
126
+
127
+ def do_faceoff
128
+ if @puck_holder.nil?
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
143
+ pass
144
+ @faceoff = false
145
+ end
146
+ end
147
+
148
+ def switch_team_with_puck(team_with_puck = @team_with_puck)
149
+ @team_with_puck, @puckless_team = if team_with_puck == @home
150
+ [@away, @home]
151
+ else
152
+ [@home, @away]
153
+ end
154
+ @shooting_chance = 0
155
+ end
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
201
+ end
202
+
203
+ # @return [Boolean] if taking the puck succeeded
204
+ def try_take_puck(player, disadvantage = 0, stat = :agility)
205
+ return false unless action_succeeds?(player.stats[stat] - disadvantage,
206
+ @puck_holder.stats[stat])
207
+
208
+ switch_team_with_puck
209
+ @puck_holder = player
210
+
211
+ true
212
+ end
213
+
214
+ # @return [Boolean] if blocking the shot succeeded
215
+ def try_block_shot(blocker)
216
+ return false unless action_succeeds?(blocker.stats[:defense],
217
+ @puck_holder.stats[:offense])
218
+
219
+ @shooting_chance += 1
220
+ @stream << Message.ShootBlock(@puck_holder, blocker,
221
+ try_take_puck(blocker), @team_with_puck)
222
+
223
+ true
224
+ end
225
+
226
+ def start_fight
227
+ @fight = Fight.new(self)
228
+ @stream << Message.FightStarted(@fight.players[:home].first,
229
+ @fight.players[:away].first)
230
+ end
231
+
232
+ # @param team [Team]
233
+ # @param amount [Numeric]
234
+ def boost_morale(team, amount)
235
+ return if amount.zero?
236
+
237
+ team.roster.each_value do |player|
238
+ if @pre_morale_change_stats[player].nil?
239
+ @pre_morale_change_stats[player] = player.stats.clone
240
+ end
241
+
242
+ player.stats.transform_values! { |stat| stat + amount }
243
+ end
244
+ end
245
+
246
+ # @return [Player]
247
+ def random_puckless_non_goalie(team)
248
+ team.random_player(
249
+ @prng,
250
+ proc { |player| player != team.roster[:goalie] and player != @puck_holder }
251
+ )
252
+ end
253
+ end
254
+ end