hlockey 6 → 7

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