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
data/lib/hlockey/league.rb
CHANGED
@@ -1,115 +1,150 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
require(
|
4
|
-
require(
|
5
|
-
require(
|
6
|
-
require(
|
7
|
-
|
8
|
-
module Hlockey
|
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
|
-
|
70
|
-
|
71
|
-
|
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
|
-
|
1
|
+
require("hlockey/constants")
|
2
|
+
require("hlockey/data")
|
3
|
+
require("hlockey/game")
|
4
|
+
require("hlockey/utils")
|
5
|
+
require("hlockey/version")
|
6
|
+
require("hlockey/weather")
|
7
|
+
|
8
|
+
module Hlockey
|
9
|
+
##
|
10
|
+
# The Hlockey League
|
11
|
+
class League
|
12
|
+
REGULAR_SEASON_DAY_AMT = 38
|
13
|
+
|
14
|
+
# @return [Time]
|
15
|
+
attr_reader(:start_time)
|
16
|
+
|
17
|
+
# @return [Hash<Symbol => Array<Team>>]
|
18
|
+
attr_reader(:divisions)
|
19
|
+
|
20
|
+
# @return [Array<Team>]
|
21
|
+
attr_reader(:teams, :playoff_teams)
|
22
|
+
|
23
|
+
# @return [Integer]
|
24
|
+
attr_reader(:day)
|
25
|
+
|
26
|
+
# @return [Array<Game>]
|
27
|
+
attr_reader(:games, :games_in_progress)
|
28
|
+
|
29
|
+
# @return [Team, nil]
|
30
|
+
attr_reader(:champion_team)
|
31
|
+
|
32
|
+
def initialize
|
33
|
+
@start_time, @divisions = Data.league
|
34
|
+
@start_time.localtime
|
35
|
+
@day = 0
|
36
|
+
@games_in_progress = []
|
37
|
+
@games = []
|
38
|
+
@champion_team = nil
|
39
|
+
@last_update_time = @start_time
|
40
|
+
@passed_updates = 0
|
41
|
+
@prng = Random.new(@start_time.to_i)
|
42
|
+
@teams = @divisions.values.reduce(:+)
|
43
|
+
@shuffled_teams = @teams.shuffle(random: @prng)
|
44
|
+
@playoff_teams = []
|
45
|
+
@game_in_matchup = 3
|
46
|
+
@matchup_game_amt = 3
|
47
|
+
end
|
48
|
+
|
49
|
+
##
|
50
|
+
# Updates the league to the current state
|
51
|
+
# This should be called whenever you need the current state of the league
|
52
|
+
def update_state
|
53
|
+
return if @champion_team
|
54
|
+
|
55
|
+
now = Time.at(Time.now.to_i)
|
56
|
+
intervals = (now - @last_update_time).div(UPDATE_FREQUENCY_SECONDS)
|
57
|
+
|
58
|
+
return unless intervals.positive?
|
59
|
+
|
60
|
+
intervals.times do |i|
|
61
|
+
if ((i + @passed_updates) % UPDATES_PER_HOUR).zero?
|
62
|
+
new_games
|
63
|
+
else
|
64
|
+
update_games
|
65
|
+
end
|
66
|
+
|
67
|
+
break if @champion_team
|
68
|
+
end
|
69
|
+
|
70
|
+
@last_update_time = now
|
71
|
+
@passed_updates += intervals
|
72
|
+
end
|
73
|
+
|
74
|
+
private
|
75
|
+
|
76
|
+
def new_games
|
77
|
+
if @game_in_matchup != @matchup_game_amt
|
78
|
+
# New game in matchups
|
79
|
+
@games.map! { |game| new_game(game.away, game.home) }
|
80
|
+
@game_in_matchup += 1
|
81
|
+
return
|
82
|
+
end
|
83
|
+
|
84
|
+
# New matchups
|
85
|
+
@games.clear
|
86
|
+
@game_in_matchup = 1
|
87
|
+
@day += 1
|
88
|
+
|
89
|
+
case @day <=> REGULAR_SEASON_DAY_AMT + 1
|
90
|
+
when -1 # In regular season
|
91
|
+
(@shuffled_teams.length / 2).times do |i|
|
92
|
+
pair = [@shuffled_teams[i], @shuffled_teams[-i - 1]]
|
93
|
+
pair.reverse! if @day > REGULAR_SEASON_DAY_AMT / 2
|
94
|
+
@games << new_game(*pair)
|
95
|
+
end
|
96
|
+
|
97
|
+
@shuffled_teams.insert(1, @shuffled_teams.pop)
|
98
|
+
when 0 # Playoffs about to start
|
99
|
+
@matchup_game_amt = 5
|
100
|
+
|
101
|
+
# get teams that qualified for playoffs, put in @playoff_teams
|
102
|
+
two_best_teams_each_division = @divisions.values.map do |teams|
|
103
|
+
sort_teams_by_wins(teams).first(2)
|
104
|
+
end.reduce(:+)
|
105
|
+
@playoff_teams = sort_teams_by_wins(two_best_teams_each_division).map(&:clone)
|
106
|
+
|
107
|
+
new_playoff_matchups
|
108
|
+
when 1 # Playoffs started
|
109
|
+
@playoff_teams.select! { |team| team.wins > team.losses }
|
110
|
+
|
111
|
+
if @playoff_teams.length == 1
|
112
|
+
@champion_team = @playoff_teams.first
|
113
|
+
return
|
114
|
+
end
|
115
|
+
|
116
|
+
new_playoff_matchups
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def update_games
|
121
|
+
@games_in_progress.each(&:update)
|
122
|
+
@games_in_progress = @games.select(&:in_progress)
|
123
|
+
@divisions.transform_values!(&method(:sort_teams_by_wins))
|
124
|
+
end
|
125
|
+
|
126
|
+
def new_playoff_matchups
|
127
|
+
@playoff_teams.each do |team|
|
128
|
+
team.wins = 0
|
129
|
+
team.losses = 0
|
130
|
+
end
|
131
|
+
|
132
|
+
(@playoff_teams.length / 2).times do |i|
|
133
|
+
@games << new_game(@playoff_teams[i], @playoff_teams[-i - 1])
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# @param home [Team]
|
138
|
+
# @param away [Team]
|
139
|
+
# @return [Game]
|
140
|
+
def new_game(home, away)
|
141
|
+
Game.new(home, away, @prng, Utils.weighted_random(Weather::WEIGHTS, @prng))
|
142
|
+
end
|
143
|
+
|
144
|
+
# @param teams [Array<Team>]
|
145
|
+
# @return [Array<Team>]
|
146
|
+
def sort_teams_by_wins(teams)
|
147
|
+
teams.sort { |a, b| b.wins <=> a.wins }
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
require("hlockey/version")
|
2
|
+
|
3
|
+
module Hlockey
|
4
|
+
##
|
5
|
+
# Can be sent by a Game (in Game#stream) or a commonly used text
|
6
|
+
class Message
|
7
|
+
private_class_method(:new)
|
8
|
+
|
9
|
+
# @return [Symbol] an action or event the message represents
|
10
|
+
attr_reader(:event)
|
11
|
+
|
12
|
+
# @param event [Symbol]
|
13
|
+
# @param fields [Array<Symbol>] fields used in string interpolation in #to_s
|
14
|
+
# @param data [Array<Object>] data corresponding to the fields
|
15
|
+
def initialize(event, fields, data)
|
16
|
+
@event = event
|
17
|
+
fields.zip(data) { |f, d| instance_variable_set("@#{f}", d) }
|
18
|
+
end
|
19
|
+
|
20
|
+
class << self
|
21
|
+
# These are messages logged to game streams
|
22
|
+
|
23
|
+
[
|
24
|
+
%i[StartOfGame],
|
25
|
+
%i[EndOfGame winning_team],
|
26
|
+
%i[StartOfPeriod period],
|
27
|
+
%i[EndOfPeriod period home away home_score away_score],
|
28
|
+
%i[FaceOff winning_player new_puck_team],
|
29
|
+
%i[Hit puck_holder defender puck_taken new_puck_team],
|
30
|
+
%i[Pass sender receiver interceptor new_puck_team],
|
31
|
+
%i[ShootScore shooter home away home_score away_score],
|
32
|
+
%i[ShootBlock shooter blocker puck_taken new_puck_team],
|
33
|
+
%i[FightStarted home_player away_player],
|
34
|
+
%i[FightAttack attacking_player defending_player blocked],
|
35
|
+
%i[PlayerJoinedFight team player],
|
36
|
+
%i[FightEnded],
|
37
|
+
%i[ChickenedOut prev_player next_player],
|
38
|
+
%i[InclineFavors team],
|
39
|
+
%i[StarsPity team],
|
40
|
+
%i[WavesWashedAway prev_player next_player]
|
41
|
+
].each do |event, *fields|
|
42
|
+
define_method(event) { |*data| new(event, fields, data) }
|
43
|
+
end
|
44
|
+
|
45
|
+
# These are messages used elsewhere
|
46
|
+
|
47
|
+
def SeasonDay(day)
|
48
|
+
"Season #{VERSION} day #{day}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def SeasonStarts(time)
|
52
|
+
time.strftime("Season #{VERSION} starts at %H:%M, %A, %B %d (%Z).")
|
53
|
+
end
|
54
|
+
|
55
|
+
def SeasonChampion(team)
|
56
|
+
"Your season #{VERSION} champions are the #{team}!"
|
57
|
+
end
|
58
|
+
|
59
|
+
def NoGames
|
60
|
+
"no games right now. it is the offseason. join the Hlockey Discord for updates"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
def to_s
|
65
|
+
case @event
|
66
|
+
when :StartOfGame
|
67
|
+
"Hocky!"
|
68
|
+
when :EndOfGame
|
69
|
+
"Game over.\n#{@winning_team} win!"
|
70
|
+
when :StartOfPeriod
|
71
|
+
"Start#{of_period}"
|
72
|
+
when :EndOfPeriod
|
73
|
+
"End#{of_period}#{score}"
|
74
|
+
when :FaceOff
|
75
|
+
"#{@winning_player} wins the faceoff!#{possession_change}"
|
76
|
+
when :Hit
|
77
|
+
"#{@defender} hits #{@puck_holder}#{takes}"
|
78
|
+
when :Pass
|
79
|
+
str = "#{@sender} passes to #{@receiver}."
|
80
|
+
str += "..\nIntercepted by #{@interceptor}!#{possession_change}" if @interceptor
|
81
|
+
str
|
82
|
+
when :ShootScore
|
83
|
+
"#{shot} and scores!#{score}"
|
84
|
+
when :ShootBlock
|
85
|
+
"#{shot}...\n#{@blocker} blocks the shot#{takes}"
|
86
|
+
when :FightStarted
|
87
|
+
"#{@home_player} and #{@away_player} start fighting!"
|
88
|
+
when :FightAttack
|
89
|
+
str = "#{@attacking_player} punches #{@defending_player}!"
|
90
|
+
str += "\n#{@defending_player} blocks the punch!" if @blocked
|
91
|
+
str
|
92
|
+
when :PlayerJoinedFight
|
93
|
+
"#{@player} from #{@team} joins the fight!"
|
94
|
+
when :FightEnded
|
95
|
+
"The fight has ended."
|
96
|
+
when :ChickenedOut
|
97
|
+
"#{@prev_player} chickened out!#{replaces_for("game")}"
|
98
|
+
when :InclineFavors
|
99
|
+
"The incline favors #{@team}."
|
100
|
+
when :StarsPity
|
101
|
+
"The stars take pity on #{@team} and give them half a goal."
|
102
|
+
when :WavesWashedAway
|
103
|
+
"#{@prev_player} is washed away by the waves...#{replaces_for("season")}"
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
private
|
108
|
+
|
109
|
+
def of_period
|
110
|
+
" of period #{@period}."
|
111
|
+
end
|
112
|
+
|
113
|
+
def score
|
114
|
+
"\n#{@home} #{@home_score.round(1)}, #{@away} #{@away_score.round(1)}"
|
115
|
+
end
|
116
|
+
|
117
|
+
def shot
|
118
|
+
"#{@shooter} takes a shot"
|
119
|
+
end
|
120
|
+
|
121
|
+
def takes
|
122
|
+
@puck_taken ? " and takes the puck!#{possession_change}" : "!"
|
123
|
+
end
|
124
|
+
|
125
|
+
def possession_change
|
126
|
+
"\n#{@new_puck_team} have possession."
|
127
|
+
end
|
128
|
+
|
129
|
+
def replaces_for(period)
|
130
|
+
"\n#{@next_player} replaces them for the rest of the #{period}."
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
require("hlockey/utils")
|
2
|
+
|
3
|
+
module Hlockey
|
4
|
+
class Player
|
5
|
+
attr_accessor(:stats)
|
6
|
+
attr_reader(:to_s)
|
7
|
+
|
8
|
+
@@cached_chain = nil
|
9
|
+
|
10
|
+
def initialize(prng = Random)
|
11
|
+
@to_s = make_name(prng)
|
12
|
+
# Generate stats
|
13
|
+
@stats = {}
|
14
|
+
%i[offense defense agility].each { |stat| @stats[stat] = prng.rand * 5 }
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def make_name(prng)
|
20
|
+
Array.new(2) do
|
21
|
+
combination = "__"
|
22
|
+
next_letter = ""
|
23
|
+
result = ""
|
24
|
+
|
25
|
+
10.times do
|
26
|
+
next_letters = name_chain[combination]
|
27
|
+
break if next_letters.nil?
|
28
|
+
|
29
|
+
cumulative_weights = []
|
30
|
+
next_letters.each_value do |v|
|
31
|
+
cumulative_weights << v + (cumulative_weights.last or 0)
|
32
|
+
end
|
33
|
+
|
34
|
+
next_letter = Utils.weighted_random(next_letters, prng)
|
35
|
+
break if next_letter == "_"
|
36
|
+
|
37
|
+
result += next_letter
|
38
|
+
combination = combination[1] + next_letter
|
39
|
+
end
|
40
|
+
|
41
|
+
result
|
42
|
+
end.join(" ")
|
43
|
+
end
|
44
|
+
|
45
|
+
def name_chain
|
46
|
+
if @@cached_chain.nil?
|
47
|
+
@@cached_chain = {}
|
48
|
+
File.open(File.expand_path("../../data/external/names.txt", __dir__)).each do |n|
|
49
|
+
name = "__#{n.chomp}__"
|
50
|
+
(name.length - 3).times do |i|
|
51
|
+
combination = name[i, 2]
|
52
|
+
next_letter = name[i + 2]
|
53
|
+
@@cached_chain[combination] ||= {}
|
54
|
+
@@cached_chain[combination][next_letter] ||= 0
|
55
|
+
@@cached_chain[combination][next_letter] += 1
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
@@cached_chain
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
data/lib/hlockey/team.rb
CHANGED
@@ -1,35 +1,59 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
module Hlockey
|
4
|
-
# Created by League when data is loaded
|
5
|
-
class Team
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
def
|
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
|
-
end
|
1
|
+
require("hlockey/player")
|
2
|
+
|
3
|
+
module Hlockey
|
4
|
+
# Created by League when data is loaded
|
5
|
+
class Team
|
6
|
+
attr_accessor(:wins, :losses, :roster)
|
7
|
+
attr_reader(:to_s, :emoji)
|
8
|
+
|
9
|
+
# rand is only used if roster is nil
|
10
|
+
def initialize(name, emoji, roster: nil, random: Random)
|
11
|
+
@to_s = name
|
12
|
+
@emoji = emoji
|
13
|
+
|
14
|
+
if roster.nil?
|
15
|
+
@roster = {}
|
16
|
+
%i[lwing center rwing ldef goalie rdef].each do |pos|
|
17
|
+
@roster[pos] = Player.new(random)
|
18
|
+
end
|
19
|
+
else
|
20
|
+
@roster = roster
|
21
|
+
end
|
22
|
+
|
23
|
+
@wins = 0
|
24
|
+
@losses = 0
|
25
|
+
end
|
26
|
+
|
27
|
+
def random_player(random, filter_predicate = nil)
|
28
|
+
players = @roster.values
|
29
|
+
players.select!(&filter_predicate) unless filter_predicate.nil?
|
30
|
+
players.sample(random: random)
|
31
|
+
end
|
32
|
+
|
33
|
+
def w_l
|
34
|
+
"#{@wins}-#{@losses}"
|
35
|
+
end
|
36
|
+
|
37
|
+
def worst_player_pos
|
38
|
+
@roster.transform_values do |player|
|
39
|
+
player.stats.values.reduce(:+)
|
40
|
+
end.min_by { |k, v| v }.first
|
41
|
+
end
|
42
|
+
|
43
|
+
# Takes symbol from Team roster & converts it to full position name
|
44
|
+
def self.pos_name(pos)
|
45
|
+
case pos
|
46
|
+
when :lwing
|
47
|
+
"Left wing"
|
48
|
+
when :rwing
|
49
|
+
"Right wing"
|
50
|
+
when :ldef
|
51
|
+
"Left defender"
|
52
|
+
when :rdef
|
53
|
+
"Right defender"
|
54
|
+
else
|
55
|
+
pos.capitalize.to_s
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Hlockey
|
2
|
+
module Utils
|
3
|
+
# Does a weighted random with a hash,
|
4
|
+
# where the keys are elements being randomly selected,
|
5
|
+
# and the values are the weights
|
6
|
+
def self.weighted_random(hash, prng = Random)
|
7
|
+
cumulative_weights = []
|
8
|
+
hash.each_value { |v| cumulative_weights << v + (cumulative_weights.last or 0) }
|
9
|
+
hash.keys[cumulative_weights.index { |n| prng.rand(cumulative_weights.last) < n }]
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
data/lib/hlockey/version.rb
CHANGED