hlockey 2 → 3

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