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.
- checksums.yaml +4 -4
- data/{lib/hlockey/data → data}/election.yaml +16 -16
- data/data/external/names.txt +19948 -0
- data/{lib/hlockey/data → data}/information.yaml +12 -12
- data/data/league.yaml +846 -0
- data/{lib/hlockey/data → data}/links.yaml +2 -2
- data/lib/hlockey/constants.rb +4 -0
- data/lib/hlockey/data.rb +17 -13
- data/lib/hlockey/game/actions.rb +30 -0
- data/lib/hlockey/game/fight.rb +83 -0
- data/lib/hlockey/game.rb +254 -159
- data/lib/hlockey/league.rb +150 -115
- data/lib/hlockey/message.rb +133 -0
- data/lib/hlockey/player.rb +63 -0
- data/lib/hlockey/team.rb +59 -35
- data/lib/hlockey/utils.rb +12 -0
- data/lib/hlockey/version.rb +3 -5
- data/lib/hlockey/weather.rb +114 -0
- data/lib/hlockey.rb +1 -3
- metadata +20 -15
- data/bin/hlockey +0 -104
- data/lib/hlockey/data/league.yaml +0 -846
- data/lib/hlockey/messages.rb +0 -100
@@ -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
|
data/lib/hlockey/data.rb
CHANGED
@@ -1,13 +1,17 @@
|
|
1
|
-
#
|
2
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
#
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
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
|
-
|
2
|
-
|
3
|
-
require(
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
@
|
33
|
-
@
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
@
|
69
|
-
end
|
70
|
-
return
|
71
|
-
end
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
@
|
102
|
-
@
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
@
|
123
|
-
@
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
159
|
-
|
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
|