hlockey 6 → 7

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,24 +1,28 @@
1
- require("hlockey/version")
1
+ require("hlockey/data")
2
+ require("hlockey/team")
2
3
 
3
4
  module Hlockey
4
5
  ##
5
6
  # Can be sent by a Game (in Game#stream) or a commonly used text
6
7
  class Message
7
- private_class_method(:new)
8
-
9
8
  @colorize = ->(_color, string) { string }
10
9
 
11
10
  # @return [Symbol] an action or event the message represents
12
11
  attr_reader(:event)
13
12
 
14
13
  # @param event [Symbol]
15
- # @param fields [Array<Symbol>] fields used in string interpolation in #to_s
16
- # @param data [Array<Object>] data corresponding to the fields
17
- def initialize(event, fields, data)
18
- self.class.attr_reader(*fields)
19
-
14
+ # @param data [Hash<Symbol => Object>] data for the message
15
+ def initialize(event, **data)
20
16
  @event = event
21
- fields.zip(data) { |f, d| instance_variable_set("@#{f}", d) }
17
+ @data = data
18
+ end
19
+
20
+ def to_s(do_color: true)
21
+ return format(Data.messages[@event], **@data) unless do_color
22
+
23
+ colorized_data = @data.transform_values { self.class.color(_1, do_color:) }
24
+
25
+ format(Data.messages[@event], **colorized_data)
22
26
  end
23
27
 
24
28
  class << self
@@ -33,150 +37,18 @@ module Hlockey
33
37
  # To control how this colors things, see #colorize=
34
38
  # @param obj [Team, Team::Player, Team::Stadium]
35
39
  # @return String
36
- def color(obj) =
37
- @colorize.call(obj.instance_of?(Team) ? obj.color : obj.team.color, obj.to_s)
38
-
39
- # These are messages logged to game streams / league alerts
40
- [
41
- # Game stream messages
42
- %i[StartOfGame],
43
- %i[EndOfGame winning_team],
44
- %i[StartOfPeriod period],
45
- %i[EndOfPeriod period home away home_score away_score],
46
- %i[FaceOff winning_player new_puck_team],
47
- %i[Hit puck_holder defender puck_taken new_puck_team shooting_chance],
48
- %i[Pass sender receiver shooting_chance interceptor new_puck_team],
49
- %i[ShootScore shooter home away home_score away_score audacity],
50
- %i[ShootBlock shooter blocker puck_taken new_puck_team shooting_chance audacity],
51
- %i[Partying player],
52
- %i[FightStarted home_player away_player],
53
- %i[FightAttack attacking_player defending_player blocked],
54
- %i[PlayerJoinedFight player],
55
- %i[FightEnded],
56
- %i[MoraleChange team amount],
57
- %i[ChickenedOut prev_player next_player],
58
- %i[InclineFavors team],
59
- %i[StarsAlign team],
60
- %i[WavesWashedAway prev_player next_player],
61
- %i[ImmaterialDodge player hitting_player],
62
- # League alert messages
63
- %i[SeasonChampion team]
64
- ].each do |event, *fields|
65
- define_method(event) { |*data| new(event, fields, data) }
66
- end
67
-
68
- # These are messages used elsewhere
69
-
70
- def SeasonDay(day) = "Season #{VERSION} day #{day}"
71
-
72
- def SeasonStarts(time) =
73
- time.strftime("Season #{VERSION} starts at %H:%M, %A, %B %d (%Z).")
74
-
75
- def NoGames =
76
- "no games right now. it is the offseason. join the Hlockey Discord for updates"
77
- end
78
-
79
- def to_s(do_color: true)
80
- c = -> { color(_1, do_color:) }
81
-
82
- case @event
83
- when :StartOfGame
84
- "Hocky!"
85
- when :EndOfGame
86
- "Game over.\n#{c.call(@winning_team)} win!"
87
- when :StartOfPeriod
88
- "Start#{of_period}"
89
- when :EndOfPeriod
90
- "End#{of_period}#{score(do_color:)}"
91
- when :FaceOff
92
- "#{c.call(@winning_player)} wins the faceoff!#{possession_change(do_color:)}"
93
- when :Hit
94
- str = "#{c.call(@defender)} hits #{c.call(@puck_holder)}"
95
- str += takes(do_color:) + score_chance if @puck_taken
96
- str
97
- when :Pass
98
- str = "#{c.call(@sender)} passes to #{c.call(@receiver)}."
99
- unless @interceptor.nil?
100
- str += "..\nIntercepted by #{c.call(@interceptor)}!" +
101
- possession_change(do_color:)
40
+ def color(obj, do_color: true)
41
+ return obj.to_s unless do_color
42
+
43
+ case obj
44
+ when Team
45
+ @colorize.call(obj.color, obj.to_s)
46
+ when Team::Player, Team::Stadium
47
+ @colorize.call(obj.team.color, obj.to_s)
48
+ else
49
+ obj.to_s
102
50
  end
103
- str += score_chance
104
- str
105
- when :ShootScore
106
- "#{shot(do_color:)} and scores!#{score(do_color:)}"
107
- when :ShootBlock
108
- "#{shot(do_color:)}...\n#{c.call(@blocker)} blocks the shot" +
109
- takes(do_color:) + score_chance
110
- when :Partying
111
- "#{c.call(@player)} is partying!"
112
- when :FightStarted
113
- "#{c.call(@home_player)} and #{c.call(@away_player)} " \
114
- "start fighting!"
115
- when :FightAttack
116
- def_player = c.call(@defending_player)
117
- str = "#{c.call(@attacking_player)} punches #{def_player}!"
118
- str += "\n#{def_player} blocks the punch!" if @blocked
119
- str
120
- when :PlayerJoinedFight
121
- "#{c.call(@player)} from #{c.call(@player.team)} joins the fight!"
122
- when :FightEnded
123
- "The fight has ended."
124
- when :MoraleChange
125
- "#{c.call(@team)} #{@amount.negative? ? "loses" : "gains"} #{@amount.abs} morale."
126
- when :ChickenedOut
127
- "#{c.call(@prev_player)} chickened out!#{replaces("game", do_color:)}"
128
- when :InclineFavors
129
- "The incline favors #{c.call(@team)}."
130
- when :StarsAlign
131
- "The stars align for #{c.call(@team)}. They get half a goal."
132
- when :WavesWashedAway
133
- "#{c.call(@prev_player)} is washed away by the waves...#{replaces(do_color:)}"
134
- when :ImmaterialDodge
135
- "#{c.call(@hitting_player)} goes right through #{c.call(@player)}!"
136
- when :SeasonChampion
137
- "Your season #{VERSION} champions are the #{c.call(@team)}!"
138
51
  end
139
52
  end
140
-
141
- private
142
-
143
- def color(obj, do_color: true) = do_color ? self.class.color(obj) : obj.to_s
144
-
145
- def score_chance
146
- return "" if @shooting_chance.nil?
147
-
148
- chance_str = case @shooting_chance
149
- when 0..2
150
- "none"
151
- when 3, 4
152
- "low"
153
- when 5, 6
154
- "medium"
155
- else
156
- "high"
157
- end
158
- "\nChance of scoring: #{chance_str} (#{@shooting_chance})"
159
- end
160
-
161
- def of_period = " of period #{@period}."
162
-
163
- def score(do_color: true) =
164
- "\n#{color(@home, do_color:)} #{@home_score.round(2)}, " \
165
- "#{color(@away, do_color:)} #{@away_score.round(2)}"
166
-
167
- def shot(do_color: true) =
168
- "#{color(@shooter, do_color:)} takes a#{"n audacious" if @audacity} shot"
169
-
170
- def takes(do_color: true) =
171
- @puck_taken ? " and takes the puck!#{possession_change(do_color:)}" : "!"
172
-
173
- def possession_change(do_color: true) =
174
- "\n#{color(@new_puck_team, do_color:)} have possession."
175
-
176
- def replaces(period = nil, do_color: true)
177
- str = "\n#{color(@next_player, do_color:)} replaces them"
178
- str += " for the rest of the #{period}." unless period.nil?
179
- str
180
- end
181
53
  end
182
54
  end
@@ -9,8 +9,7 @@ module Hlockey
9
9
 
10
10
  DESCRIPTION = "This player can only play offense.".freeze
11
11
 
12
- def on_swap(into_roster, pos, prev_pos, _prng) =
13
- fence_swap(into_roster, pos, prev_pos)
12
+ def on_swap(...) = fence_swap(...)
14
13
  end
15
14
  end
16
15
  end
@@ -9,7 +9,7 @@ module Hlockey
9
9
 
10
10
  DESCRIPTION = "This player can only play defense.".freeze
11
11
 
12
- def on_swap(into_roster, pos, prev_pos, _prng) =
12
+ def on_swap(into_roster, pos, prev_pos) =
13
13
  fence_swap(into_roster, pos, prev_pos, type: :destroyer)
14
14
  end
15
15
  end
@@ -18,38 +18,40 @@ module Hlockey
18
18
 
19
19
  def to_data = [to_s, @other_player.to_s]
20
20
 
21
- def on_swap(into_roster, pos, _prev_pos, prng)
21
+ def on_swap(into_roster, pos, _prev_pos)
22
22
  unless @other_player.is_a?(Team::Player)
23
23
  @other_player = @team.players.find { _1.name == @other_player }
24
24
  end
25
25
 
26
+ swap_player = nil
26
27
  if into_roster
27
28
  prev_pos = @team.shadows.index(@other_player)
28
29
  return if prev_pos.nil?
29
30
 
30
- other_pos = with_deleted(@team.roster.keys, pos).sample(random: prng)
31
+ swap_pos = sample_with_deleted(@team.roster.keys, pos)
32
+ swap_player = @team.roster[swap_pos]
31
33
 
32
- @team.shadows[prev_pos] = @team.roster[other_pos]
33
- @team.roster[other_pos] = @other_player
34
- return
35
- end
34
+ @team.shadows[prev_pos] = swap_player
35
+ @team.roster[swap_pos] = @other_player
36
+ else
37
+ prev_pos = @team.roster.key(@other_player)
38
+ return if prev_pos.nil?
36
39
 
37
- prev_pos = @team.roster.key(@other_player)
38
- return if prev_pos.nil?
40
+ swap_pos = sample_with_deleted(@team.shadows.each_index.to_a, pos)
41
+ swap_player = @team.shadows[swap_pos]
39
42
 
40
- other_pos = with_deleted(@team.shadows.each_index.to_a, pos).sample(random: prng)
43
+ @team.roster[prev_pos] = swap_player
44
+ @team.shadows[swap_pos] = @other_player
45
+ end
41
46
 
42
- @team.roster[prev_pos] = @team.shadows[other_pos]
43
- @team.shadows[other_pos] = @other_player
47
+ send_game_message(:mod_handholding_event,
48
+ player: @player, other_player: @other_player, swap_player:)
44
49
  end
45
50
 
46
51
  private
47
52
 
48
- # For `on_swap`, because array.delete doesn't return the array for no reason
49
- def with_deleted(array, object)
50
- array.delete(object)
51
- array
52
- end
53
+ def sample_with_deleted(array, object) =
54
+ array.tap { _1.delete(object) }.sample(random: prng)
53
55
  end
54
56
  end
55
57
  end
@@ -11,15 +11,17 @@ module Hlockey
11
11
 
12
12
  DESCRIPTION = "This player may be a hallucination.".freeze
13
13
 
14
- def on_got_hit(game, hitting_player)
15
- return if game.prng.rand(3).zero?
14
+ def on_got_hit(hitting_player)
15
+ return false if prng.rand(3).zero?
16
16
 
17
- if game.pre_tmp_change_stats[@player].nil?
18
- game.pre_tmp_change_stats[@player] = @player.stats.clone
17
+ if @game.pre_tmp_change_stats[@player].nil?
18
+ @game.pre_tmp_change_stats[@player] = @player.stats.clone
19
19
  end
20
20
  @player.stats[:defense] += 0.5
21
21
 
22
- Message.ImmaterialDodge(@player, hitting_player)
22
+ send_game_message(:mod_immaterial_event, hitting_player:, player: @player)
23
+
24
+ true # Other player shouldn't take puck
23
25
  end
24
26
  end
25
27
  end
@@ -9,14 +9,20 @@ module Hlockey
9
9
 
10
10
  DESCRIPTION = "This player can not swap positions.".freeze
11
11
 
12
- def on_swap(into_roster, pos, prev_pos, _prng) =
12
+ def on_swap(into_roster, pos, prev_pos)
13
+ swap_player = nil
13
14
  if into_roster
14
- @team.roster[pos] = @team.shadows[prev_pos]
15
+ swap_player = @team.shadows[prev_pos]
16
+ @team.roster[pos] = swap_player
15
17
  @team.shadows[prev_pos] = @player
16
18
  else
17
- @team.shadows[pos] = @team.roster[prev_pos]
19
+ swap_player = @team.roster[prev_pos]
20
+ @team.shadows[pos] = swap_player
18
21
  @team.roster[prev_pos] = @player
19
22
  end
23
+
24
+ send_game_message(:mod_locked_event, player: @player, swap_player:)
25
+ end
20
26
  end
21
27
  end
22
28
  end
@@ -1,3 +1,4 @@
1
+ require("hlockey/message")
1
2
  require("hlockey/selfdescribable")
2
3
 
3
4
  module Hlockey
@@ -8,6 +9,8 @@ module Hlockey
8
9
  module Moddable
9
10
  include(SelfDescribable)
10
11
 
12
+ attr_writer(:game)
13
+
11
14
  DESCRIPTION = "A mod with no description.".freeze # Default description
12
15
 
13
16
  # If the mod has no extra parameters, same as #to_s
@@ -22,16 +25,28 @@ module Hlockey
22
25
  @team = player.team
23
26
  end
24
27
 
25
- def on_swap(into_roster, pos, prev_pos, prng) end
28
+ def on_swap(into_roster, pos, prev_pos) end
26
29
 
27
- def on_action(game) end
30
+ def on_action() end
28
31
 
29
- def on_got_hit(game, hitting_player) end
32
+ # May return truthy value to signal that hitting player should not try to take puck
33
+ def on_got_hit(hitting_player) end
30
34
 
35
+ # May return a different player to join the fight instead of this one
31
36
  def on_join_fight() end
32
37
 
33
38
  private
34
39
 
40
+ # Makes a new message from params, adds it to the game stream if there is a game
41
+ def send_game_message(...)
42
+ @game.stream << Message.new(...) unless @game.nil?
43
+ end
44
+
45
+ # Handles the on_swap event for Fencebuilder, Fencedestroyer mods
46
+ # @param into_roster [Boolean]
47
+ # @param pos [Symbol]
48
+ # @param prev_pos [Symbol]
49
+ # @param type [:builder | :destroyer]
35
50
  def fence_swap(into_roster, pos, prev_pos, type: :builder)
36
51
  return unless into_roster
37
52
 
@@ -40,10 +55,16 @@ module Hlockey
40
55
  new_pos = new_pos_hash[pos]
41
56
  return if new_pos.nil?
42
57
 
58
+ swap_player = @team.roster[new_pos]
43
59
  @team.roster[pos] = @team.shadows[prev_pos]
44
- @team.shadows[prev_pos] = @team.roster[new_pos]
60
+ @team.shadows[prev_pos] = swap_player
45
61
  @team.roster[new_pos] = @player
62
+
63
+ send_game_message(:mod_fence_event, type:, player: @player, swap_player:)
46
64
  end
65
+
66
+ # @return [Utils::Rng, Random] the object to use for RNG
67
+ def prng = @game&.prng || Random.new
47
68
  end
48
69
  end
49
70
  end
@@ -4,7 +4,8 @@ require("hlockey/actions")
4
4
  module Hlockey
5
5
  module Mod
6
6
  ##
7
- # Player has James stats added to their own at random
7
+ # Player has James stats added to their own at random for 5 actions
8
+ # These 5 actions carry over between games
8
9
  class Powernapper
9
10
  include(Moddable)
10
11
  include(Actions)
@@ -16,30 +17,33 @@ module Hlockey
16
17
  @currently_boosted_for = -1
17
18
  end
18
19
 
19
- def on_action(game)
20
+ def on_action
20
21
  if @currently_boosted_for.negative?
21
- return unless random_event_occurs?(prng: game.prng)
22
+ return unless random_event_occurs?(prng:)
22
23
 
23
- change_stats(game)
24
+ change_stats
24
25
  end
25
26
  @currently_boosted_for += 1
26
27
  return if @currently_boosted_for < 5
27
28
 
28
- change_stats(game, reset: true)
29
+ change_stats(reset: true)
29
30
  @currently_boosted_for = -1
30
31
  end
31
32
 
32
33
  private
33
34
 
34
- def change_stats(game, reset: false)
35
+ def change_stats(reset: false)
35
36
  stat_change = { offense: 0.5, defense: 1.0 } # James stats (excluding 0 agility)
36
37
  stat_change.transform_values!(&:-@) if reset
37
38
 
38
- pre_tmp_change_stats = game.pre_tmp_change_stats[@player]
39
+ pre_tmp_change_stats = @game.pre_tmp_change_stats[@player]
39
40
  stat_change.each do |stat, change|
40
41
  @player.stats[stat] += change
41
42
  pre_tmp_change_stats[stat] += change unless pre_tmp_change_stats.nil?
42
43
  end
44
+
45
+ send_game_message(reset ? :mod_powernap_end : :mod_powernap_start,
46
+ player: @player)
43
47
  end
44
48
  end
45
49
  end
@@ -10,8 +10,12 @@ module Hlockey
10
10
 
11
11
  DESCRIPTION = "This player may randomly start fights.".freeze
12
12
 
13
- def on_action(game) =
14
- random_event_occurs?(prng: game.prng) && game.start_fight(@player)
13
+ def on_action
14
+ return unless random_event_occurs?(prng:)
15
+
16
+ send_game_message(:mod_punchy_event, player: @player)
17
+ @game.start_fight(@player)
18
+ end
15
19
  end
16
20
  end
17
21
  end
@@ -2,8 +2,6 @@ module Hlockey
2
2
  ##
3
3
  # For things that name themselves by class name
4
4
  module SelfDescribable
5
- def to_s
6
- self.class.to_s.split("::").last
7
- end
5
+ def to_s = self.class.to_s.split("::").last
8
6
  end
9
7
  end
@@ -47,7 +47,10 @@ module Hlockey
47
47
  @stats.transform_values { _1.round(2).to_s.ljust(4, "0") }
48
48
  )
49
49
 
50
+ # Calls a method on every mod this player has
50
51
  # @param action [Symbol] method to call on each mod
52
+ # @param *args [Object] any arguments to pass to the mods
53
+ # @return [Object] the first non-nil return value given from a mod, if any
51
54
  def mods_do(action, *args)
52
55
  res = nil
53
56
  @mods.each { res ||= _1.send(action, *args) }
data/lib/hlockey/team.rb CHANGED
@@ -28,8 +28,8 @@ module Hlockey
28
28
 
29
29
  @stadium = Stadium.new(team: self, **stadium)
30
30
 
31
- @roster = roster.transform_values { Player.new(team: self, **_1) }
32
- @shadows = shadows.map { Player.new(team: self, **_1) }
31
+ @roster = roster.transform_values { Player.new(**_1, team: self) }
32
+ @shadows = shadows.map { Player.new(**_1, team: self) }
33
33
 
34
34
  @status = :in_contention
35
35
  @wins = @losses = 0
@@ -70,7 +70,7 @@ module Hlockey
70
70
 
71
71
  # @return [Symbol] the position of the worst player on the roster
72
72
  def worst_player_pos =
73
- @roster.transform_values { _1.stats.values.sum }.min_by { _2 }.first
73
+ @roster.transform_values { |v| v.stats.values.sum }.min_by { |_, v| v }.first
74
74
 
75
75
  # @return [Hash<String => Player>] #roster, but with keys better for displaying
76
76
  def roster_display = @roster.transform_keys(&self.class.method(:pos_name))
data/lib/hlockey/utils.rb CHANGED
@@ -1,13 +1,45 @@
1
1
  module Hlockey
2
2
  ##
3
- # Utility methods to help with various Hlockey related tasks
3
+ # Utilities to help with various Hlockey related tasks
4
4
  module Utils
5
+ ##
6
+ # Lehmer random number generator,
7
+ # to avoid incompatabilities in RNG between Ruby implementations.
8
+ # From https://en.wikipedia.org/wiki/Lehmer_random_number_generator#Sample_C99_code.
9
+ class Rng
10
+ MOD_VAL = 0x7fffffff
11
+
12
+ # @return [Integer]
13
+ attr_reader(:state)
14
+
15
+ # @param seed [Integer]
16
+ def initialize(seed)
17
+ seed = seed.zero? ? 1 : seed.abs
18
+ seed %= MOD_VAL if seed >= MOD_VAL
19
+
20
+ @state = seed
21
+ end
22
+
23
+ # @param upper_bound [Numeric, Range, nil] max value from RNG
24
+ # @return [Numeric]
25
+ def rand(bound = nil)
26
+ if bound.is_a?(Range)
27
+ start = bound.begin || 0
28
+ return rand(bound.end.nil? ? nil : bound.end - start) + start
29
+ end
30
+
31
+ @state = @state * 48_271 % MOD_VAL
32
+ return @state if bound.nil? || bound.zero?
33
+
34
+ @state % bound
35
+ end
36
+ end
37
+
5
38
  # Transforms hash keys to be better for displaying the information in the hash
6
39
  # @param hash [Hash<#to_s => Object>]
7
40
  # @return [Hash<String => Object>] the new hash
8
- def self.hash_display_keys(hash)
9
- hash.transform_keys { |key| key.to_s.capitalize.gsub("_", " ") }
10
- end
41
+ def self.hash_display_keys(hash) =
42
+ hash.transform_keys { |key| key.to_s.capitalize.gsub("_", " ") }.compact
11
43
 
12
44
  # Does a weighted random with a hash,
13
45
  # where the keys are elements being randomly selected and the values are the weights
@@ -1,3 +1,3 @@
1
1
  module Hlockey
2
- VERSION = "6".freeze
2
+ VERSION = "7".freeze
3
3
  end
data/lib/hlockey.rb CHANGED
@@ -1 +1,16 @@
1
1
  require("hlockey/league")
2
+
3
+ ##
4
+ # All Hlockey library code lives here.
5
+ module Hlockey
6
+ # @return [League]
7
+ def self.new_current_league
8
+ league_hash = Data.league
9
+ league_start_time = Time.utc(*league_hash[:start_time])
10
+ one_week = 604_800 # In seconds, to add/subtract with Time objects
11
+
12
+ return League.new(**Data.infinileague) if Time.now < league_start_time - one_week
13
+
14
+ League.new(**league_hash)
15
+ end
16
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hlockey
3
3
  version: !ruby/object:Gem::Version
4
- version: '6'
4
+ version: '7'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lavender Perry
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-12-30 00:00:00.000000000 Z
11
+ date: 2024-05-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json
@@ -31,9 +31,11 @@ extensions: []
31
31
  extra_rdoc_files: []
32
32
  files:
33
33
  - data/election.json
34
+ - data/infinileague.json
34
35
  - data/information.json
35
36
  - data/league.json
36
37
  - data/links.json
38
+ - data/messages.json
37
39
  - data/previous_election_results.json
38
40
  - lib/hlockey.rb
39
41
  - lib/hlockey/actions.rb
@@ -87,8 +89,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
87
89
  - !ruby/object:Gem::Version
88
90
  version: '0'
89
91
  requirements: []
90
- rubygems_version: 3.4.10
92
+ rubygems_version: 3.5.9
91
93
  signing_key:
92
94
  specification_version: 4
93
- summary: Hlockey season 6.
95
+ summary: Hlockey season 7.
94
96
  test_files: []