active_genie 0.25.1 → 0.26.0
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/README.md +5 -5
- data/VERSION +1 -1
- data/lib/active_genie/battle/README.md +7 -7
- data/lib/active_genie/battle/generalist.json +36 -0
- data/lib/active_genie/battle/generalist.md +16 -0
- data/lib/active_genie/battle/generalist.rb +16 -69
- data/lib/active_genie/clients/providers/anthropic_client.rb +61 -40
- data/lib/active_genie/clients/providers/base_client.rb +44 -57
- data/lib/active_genie/clients/providers/deepseek_client.rb +57 -52
- data/lib/active_genie/clients/providers/google_client.rb +58 -60
- data/lib/active_genie/clients/providers/openai_client.rb +52 -55
- data/lib/active_genie/clients/unified_client.rb +4 -4
- data/lib/active_genie/config/battle_config.rb +2 -0
- data/lib/active_genie/config/llm_config.rb +3 -1
- data/lib/active_genie/config/log_config.rb +38 -14
- data/lib/active_genie/config/providers/anthropic_config.rb +2 -2
- data/lib/active_genie/config/providers/deepseek_config.rb +2 -2
- data/lib/active_genie/config/providers/google_config.rb +2 -2
- data/lib/active_genie/config/providers/openai_config.rb +2 -2
- data/lib/active_genie/config/providers_config.rb +4 -4
- data/lib/active_genie/config/scoring_config.rb +2 -0
- data/lib/active_genie/configuration.rb +14 -8
- data/lib/active_genie/data_extractor/from_informal.json +11 -0
- data/lib/active_genie/data_extractor/from_informal.rb +5 -13
- data/lib/active_genie/data_extractor/generalist.json +9 -0
- data/lib/active_genie/data_extractor/generalist.rb +12 -11
- data/lib/active_genie/errors/invalid_log_output_error.rb +19 -0
- data/lib/active_genie/logger.rb +13 -5
- data/lib/active_genie/{concerns → ranking/concerns}/loggable.rb +2 -5
- data/lib/active_genie/ranking/elo_round.rb +30 -28
- data/lib/active_genie/ranking/free_for_all.rb +30 -22
- data/lib/active_genie/ranking/player.rb +53 -19
- data/lib/active_genie/ranking/players_collection.rb +17 -13
- data/lib/active_genie/ranking/ranking.rb +21 -20
- data/lib/active_genie/ranking/ranking_scoring.rb +2 -20
- data/lib/active_genie/scoring/generalist.json +9 -0
- data/lib/active_genie/scoring/generalist.md +46 -0
- data/lib/active_genie/scoring/generalist.rb +13 -65
- data/lib/active_genie/scoring/recommended_reviewers.rb +2 -2
- metadata +11 -4
@@ -38,23 +38,19 @@ module ActiveGenie
|
|
38
38
|
@llm ||= Config::LlmConfig.new
|
39
39
|
end
|
40
40
|
|
41
|
+
SUB_CONFIGS = %w[log providers llm ranking scoring data_extractor battle].freeze
|
42
|
+
|
41
43
|
def merge(config_params = {})
|
42
44
|
return config_params if config_params.is_a?(Configuration)
|
43
45
|
|
44
46
|
new_configuration = dup
|
45
47
|
|
46
|
-
|
48
|
+
SUB_CONFIGS.each do |key|
|
47
49
|
config = new_configuration.send(key)
|
48
50
|
|
49
51
|
next unless config.respond_to?(:merge)
|
50
52
|
|
51
|
-
new_config =
|
52
|
-
config.merge(config_params[key.to_s])
|
53
|
-
elsif config_params.key?(key.to_sym)
|
54
|
-
config.merge(config_params[key.to_sym])
|
55
|
-
else
|
56
|
-
config.merge(config_params)
|
57
|
-
end
|
53
|
+
new_config = sub_config_merge(config, key, config_params)
|
58
54
|
|
59
55
|
new_configuration.send("#{key}=", new_config)
|
60
56
|
end
|
@@ -62,6 +58,16 @@ module ActiveGenie
|
|
62
58
|
new_configuration
|
63
59
|
end
|
64
60
|
|
61
|
+
def sub_config_merge(config, key, config_params)
|
62
|
+
if config_params.key?(key.to_s)
|
63
|
+
config.merge(config_params[key.to_s])
|
64
|
+
elsif config_params.key?(key.to_sym)
|
65
|
+
config.merge(config_params[key.to_sym])
|
66
|
+
else
|
67
|
+
config.merge(config_params)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
65
71
|
attr_writer :log, :providers, :ranking, :scoring, :data_extractor, :battle, :llm
|
66
72
|
end
|
67
73
|
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
|
2
|
+
{
|
3
|
+
"message_litote": {
|
4
|
+
"type": "boolean",
|
5
|
+
"description": "Return true if the message is a litote. A litote is a figure of speech that uses understatement to emphasize a point by stating a negative to further affirm a positive, often incorporating double negatives for effect."
|
6
|
+
},
|
7
|
+
"litote_rephrased": {
|
8
|
+
"type": "string",
|
9
|
+
"description": "The true meaning of the litote. Rephrase the message to a positive and active statement."
|
10
|
+
}
|
11
|
+
}
|
@@ -37,8 +37,8 @@ module ActiveGenie
|
|
37
37
|
def call
|
38
38
|
response = Generalist.call(@text, data_to_extract_with_litote, config: @config)
|
39
39
|
|
40
|
-
if response[
|
41
|
-
response = Generalist.call(response[
|
40
|
+
if response[:message_litote]
|
41
|
+
response = Generalist.call(response[:litote_rephrased], @data_to_extract, config: @config)
|
42
42
|
end
|
43
43
|
|
44
44
|
response
|
@@ -47,17 +47,9 @@ module ActiveGenie
|
|
47
47
|
private
|
48
48
|
|
49
49
|
def data_to_extract_with_litote
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
type: 'boolean',
|
54
|
-
description: 'Return true if the message is a litote. A litote is a figure of speech that uses understatement to emphasize a point by stating a negative to further affirm a positive, often incorporating double negatives for effect.'
|
55
|
-
},
|
56
|
-
litote_rephrased: {
|
57
|
-
type: 'string',
|
58
|
-
description: 'The true meaning of the litote. Rephrase the message to a positive and active statement.'
|
59
|
-
}
|
60
|
-
}
|
50
|
+
parameters = JSON.parse(File.read(File.join(__dir__, 'from_informal.json')), symbolize_names: true)
|
51
|
+
|
52
|
+
@data_to_extract.merge(parameters)
|
61
53
|
end
|
62
54
|
end
|
63
55
|
end
|
@@ -42,15 +42,9 @@ module ActiveGenie
|
|
42
42
|
|
43
43
|
properties = data_to_extract_with_explanation
|
44
44
|
|
45
|
-
function =
|
46
|
-
|
47
|
-
|
48
|
-
parameters: {
|
49
|
-
type: 'object',
|
50
|
-
properties:,
|
51
|
-
required: properties.keys
|
52
|
-
}
|
53
|
-
}
|
45
|
+
function = JSON.parse(File.read(File.join(__dir__, 'generalist.json')), symbolize_names: true)
|
46
|
+
function[:parameters][:properties] = properties
|
47
|
+
function[:parameters][:required] = properties.keys
|
54
48
|
|
55
49
|
response = function_calling(messages, function)
|
56
50
|
|
@@ -68,11 +62,18 @@ module ActiveGenie
|
|
68
62
|
with_explanation[key] = value
|
69
63
|
with_explanation["#{key}_explanation"] = {
|
70
64
|
type: 'string',
|
71
|
-
description: "
|
65
|
+
description: "
|
66
|
+
The chain of thought that led to the conclusion about: #{key}.
|
67
|
+
Can be blank if the user didn't provide any context
|
68
|
+
"
|
72
69
|
}
|
73
70
|
with_explanation["#{key}_accuracy"] = {
|
74
71
|
type: 'integer',
|
75
|
-
description: '
|
72
|
+
description: '
|
73
|
+
The accuracy of the extracted data, what is the percentage of confidence?
|
74
|
+
When 100 it means the data is explicitly stated in the text.
|
75
|
+
When 0 it means is no way to discover the data from the text
|
76
|
+
'
|
76
77
|
}
|
77
78
|
end
|
78
79
|
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveGenie
|
4
|
+
class InvalidLogOutputError < StandardError
|
5
|
+
TEXT = <<~TEXT
|
6
|
+
Invalid log output option. Must be a callable object. Given: %<output>s
|
7
|
+
Example:
|
8
|
+
```ruby
|
9
|
+
ActiveGenie.configure do |config|
|
10
|
+
config.log.output = ->(log) { puts log }
|
11
|
+
end
|
12
|
+
```
|
13
|
+
TEXT
|
14
|
+
|
15
|
+
def initialize(output)
|
16
|
+
super(format(TEXT, output:))
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/active_genie/logger.rb
CHANGED
@@ -16,27 +16,35 @@ module ActiveGenie
|
|
16
16
|
}
|
17
17
|
|
18
18
|
persist!(log)
|
19
|
-
|
20
|
-
ActiveGenie.configuration.log.call_observers(log)
|
19
|
+
config.output_call(log)
|
21
20
|
|
22
21
|
log
|
23
22
|
end
|
24
23
|
|
25
|
-
def with_context(context)
|
24
|
+
def with_context(context, observer: nil)
|
26
25
|
@context ||= {}
|
27
26
|
begin
|
28
27
|
@context = @context.merge(context)
|
28
|
+
config.add_observer(observers: [observer])
|
29
29
|
yield if block_given?
|
30
30
|
ensure
|
31
31
|
@context.delete_if { |key, _| context.key?(key) }
|
32
|
+
config.remove_observer([observer])
|
32
33
|
end
|
33
34
|
end
|
34
35
|
|
35
36
|
attr_accessor :context
|
36
37
|
|
37
38
|
def persist!(log)
|
38
|
-
|
39
|
-
File.
|
39
|
+
file_path = log.key?(:fine_tune) && log[:fine_tune] ? config.fine_tune_file_path : config.file_path
|
40
|
+
folder_path = File.dirname(file_path)
|
41
|
+
|
42
|
+
FileUtils.mkdir_p(folder_path)
|
43
|
+
File.write(file_path, "#{JSON.generate(log)}\n", mode: 'a')
|
44
|
+
end
|
45
|
+
|
46
|
+
def config
|
47
|
+
ActiveGenie.configuration.log
|
40
48
|
end
|
41
49
|
end
|
42
50
|
end
|
@@ -16,38 +16,29 @@ module ActiveGenie
|
|
16
16
|
@criteria = criteria
|
17
17
|
@config = config
|
18
18
|
@tmp_defenders = []
|
19
|
-
@start_time = Time.now
|
20
19
|
@total_tokens = 0
|
21
20
|
@previous_elo = {}
|
22
21
|
@previous_highest_elo = @defender_tier.max_by(&:elo).elo
|
23
22
|
end
|
24
23
|
|
25
24
|
def call
|
25
|
+
@previous_elo = @players.to_h { |player| [player.id, player.elo] }
|
26
|
+
|
26
27
|
ActiveGenie::Logger.with_context(log_context) do
|
27
|
-
|
28
|
-
matches.each do |player_1, player_2|
|
28
|
+
matches.each do |player_a, player_b|
|
29
29
|
# TODO: battle can take a while, can be parallelized
|
30
|
-
winner, loser = battle(
|
31
|
-
|
32
|
-
|
33
|
-
winner.elo = calculate_new_elo(winner.elo, loser.elo, 1)
|
34
|
-
loser.elo = calculate_new_elo(loser.elo, winner.elo, 0)
|
30
|
+
winner, loser = battle(player_a, player_b)
|
31
|
+
update_players_elo(winner, loser)
|
35
32
|
end
|
36
33
|
end
|
37
34
|
|
38
|
-
|
39
|
-
|
40
|
-
report
|
35
|
+
build_report
|
41
36
|
end
|
42
37
|
|
43
|
-
private
|
44
|
-
|
45
38
|
BATTLE_PER_PLAYER = 3
|
46
39
|
K = 32
|
47
40
|
|
48
|
-
|
49
|
-
@previous_elo = @players.map { |player| [player.id, player.elo] }.to_h
|
50
|
-
end
|
41
|
+
private
|
51
42
|
|
52
43
|
def matches
|
53
44
|
@relegation_tier.each_with_object([]) do |attack_player, matches|
|
@@ -63,18 +54,18 @@ module ActiveGenie
|
|
63
54
|
@tmp_defenders.pop
|
64
55
|
end
|
65
56
|
|
66
|
-
def battle(
|
67
|
-
ActiveGenie::Logger.with_context({
|
57
|
+
def battle(player_a, player_b)
|
58
|
+
ActiveGenie::Logger.with_context({ player_a_id: player_a.id, player_b_id: player_b.id }) do
|
68
59
|
result = ActiveGenie::Battle.call(
|
69
|
-
|
70
|
-
|
60
|
+
player_a.content,
|
61
|
+
player_b.content,
|
71
62
|
@criteria,
|
72
63
|
config: @config
|
73
64
|
)
|
74
65
|
|
75
66
|
winner, loser = case result['winner']
|
76
|
-
when '
|
77
|
-
when '
|
67
|
+
when 'player_a' then [player_a, player_b]
|
68
|
+
when 'player_b' then [player_b, player_a]
|
78
69
|
when 'draw' then [nil, nil]
|
79
70
|
end
|
80
71
|
|
@@ -82,9 +73,16 @@ module ActiveGenie
|
|
82
73
|
end
|
83
74
|
end
|
84
75
|
|
76
|
+
def update_players_elo(winner, loser)
|
77
|
+
return if winner.nil? || loser.nil?
|
78
|
+
|
79
|
+
winner.elo = calculate_new_elo(winner.elo, loser.elo, 1)
|
80
|
+
loser.elo = calculate_new_elo(loser.elo, winner.elo, 0)
|
81
|
+
end
|
82
|
+
|
85
83
|
# INFO: Read more about the Elo rating system on https://en.wikipedia.org/wiki/Elo_rating_system
|
86
84
|
def calculate_new_elo(player_rating, opponent_rating, score)
|
87
|
-
expected_score = 1.0 / (1.0 + 10.0**((opponent_rating - player_rating) / 400.0))
|
85
|
+
expected_score = 1.0 / (1.0 + (10.0**((opponent_rating - player_rating) / 400.0)))
|
88
86
|
|
89
87
|
player_rating + (K * (score - expected_score)).round
|
90
88
|
end
|
@@ -101,18 +99,21 @@ module ActiveGenie
|
|
101
99
|
Digest::MD5.hexdigest(ranking_unique_key)
|
102
100
|
end
|
103
101
|
|
104
|
-
def
|
105
|
-
{
|
102
|
+
def build_report
|
103
|
+
report = {
|
106
104
|
elo_round_id:,
|
107
105
|
players_in_round: players_in_round.map(&:id),
|
108
106
|
battles_count: matches.size,
|
109
|
-
duration_seconds: Time.now - @start_time,
|
110
107
|
total_tokens: @total_tokens,
|
111
108
|
previous_highest_elo: @previous_highest_elo,
|
112
109
|
highest_elo:,
|
113
110
|
highest_elo_diff: highest_elo - @previous_highest_elo,
|
114
111
|
players_elo_diff:
|
115
112
|
}
|
113
|
+
|
114
|
+
ActiveGenie::Logger.call({ code: :elo_round_report, **report })
|
115
|
+
|
116
|
+
report
|
116
117
|
end
|
117
118
|
|
118
119
|
def players_in_round
|
@@ -124,9 +125,10 @@ module ActiveGenie
|
|
124
125
|
end
|
125
126
|
|
126
127
|
def players_elo_diff
|
127
|
-
players_in_round.map do |player|
|
128
|
+
elo_diffs = players_in_round.map do |player|
|
128
129
|
[player.id, player.elo - @previous_elo[player.id]]
|
129
|
-
end
|
130
|
+
end
|
131
|
+
elo_diffs.sort_by { |_, diff| -(diff || 0) }.to_h
|
130
132
|
end
|
131
133
|
|
132
134
|
def log_observer(log)
|
@@ -18,23 +18,15 @@ module ActiveGenie
|
|
18
18
|
end
|
19
19
|
|
20
20
|
def call
|
21
|
-
ActiveGenie::Logger.with_context(log_context, observer:
|
22
|
-
matches.each do |
|
23
|
-
winner, loser = battle(
|
24
|
-
|
25
|
-
|
26
|
-
player_1.draw!
|
27
|
-
player_2.draw!
|
28
|
-
else
|
29
|
-
winner.win!
|
30
|
-
loser.lose!
|
31
|
-
end
|
21
|
+
ActiveGenie::Logger.with_context(log_context, observer: ->(log) { log_observer(log) }) do
|
22
|
+
matches.each do |player_a, player_b|
|
23
|
+
winner, loser = battle(player_a, player_b)
|
24
|
+
|
25
|
+
update_players_score(winner, loser)
|
32
26
|
end
|
33
27
|
end
|
34
28
|
|
35
|
-
|
36
|
-
|
37
|
-
report
|
29
|
+
build_report
|
38
30
|
end
|
39
31
|
|
40
32
|
private
|
@@ -45,23 +37,23 @@ module ActiveGenie
|
|
45
37
|
@players.eligible.combination(2).to_a
|
46
38
|
end
|
47
39
|
|
48
|
-
def battle(
|
40
|
+
def battle(player_a, player_b)
|
49
41
|
result = ActiveGenie::Battle.call(
|
50
|
-
|
51
|
-
|
42
|
+
player_a.content,
|
43
|
+
player_b.content,
|
52
44
|
@criteria,
|
53
45
|
config: @config
|
54
46
|
)
|
55
47
|
|
56
48
|
winner, loser = case result['winner']
|
57
|
-
when '
|
58
|
-
when '
|
49
|
+
when 'player_a' then [player_a, player_b, result['reasoning']]
|
50
|
+
when 'player_b' then [player_b, player_a, result['reasoning']]
|
59
51
|
when 'draw' then [nil, nil, result['reasoning']]
|
60
52
|
end
|
61
53
|
|
62
54
|
ActiveGenie::Logger.call({
|
63
55
|
code: :free_for_all_battle,
|
64
|
-
player_ids: [
|
56
|
+
player_ids: [player_a.id, player_b.id],
|
65
57
|
winner_id: winner&.id,
|
66
58
|
loser_id: loser&.id,
|
67
59
|
reasoning: result['reasoning']
|
@@ -70,6 +62,18 @@ module ActiveGenie
|
|
70
62
|
[winner, loser]
|
71
63
|
end
|
72
64
|
|
65
|
+
def update_players_score(winner, loser)
|
66
|
+
return if winner.nil? || loser.nil?
|
67
|
+
|
68
|
+
if winner.nil? || loser.nil?
|
69
|
+
player_a.draw!
|
70
|
+
player_b.draw!
|
71
|
+
else
|
72
|
+
winner.win!
|
73
|
+
loser.lose!
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
73
77
|
def log_context
|
74
78
|
{ free_for_all_id: }
|
75
79
|
end
|
@@ -80,13 +84,17 @@ module ActiveGenie
|
|
80
84
|
Digest::MD5.hexdigest(ranking_unique_key)
|
81
85
|
end
|
82
86
|
|
83
|
-
def
|
84
|
-
{
|
87
|
+
def build_report
|
88
|
+
report = {
|
85
89
|
free_for_all_id:,
|
86
90
|
battles_count: matches.size,
|
87
91
|
duration_seconds: Time.now - @start_time,
|
88
92
|
total_tokens: @total_tokens
|
89
93
|
}
|
94
|
+
|
95
|
+
ActiveGenie::Logger.call({ code: :free_for_all_report, **report })
|
96
|
+
|
97
|
+
report
|
90
98
|
end
|
91
99
|
|
92
100
|
def log_observer(log)
|
@@ -6,19 +6,51 @@ module ActiveGenie
|
|
6
6
|
module Ranking
|
7
7
|
class Player
|
8
8
|
def initialize(params)
|
9
|
-
params = { content: params }
|
9
|
+
@params = params.is_a?(String) ? { content: params } : params.dup
|
10
|
+
@params[:content] ||= @params
|
11
|
+
end
|
12
|
+
|
13
|
+
def content
|
14
|
+
@content ||= @params[:content]
|
15
|
+
end
|
16
|
+
|
17
|
+
def name
|
18
|
+
@name ||= @params[:name] || content[0..10]
|
19
|
+
end
|
20
|
+
|
21
|
+
def id
|
22
|
+
@id ||= @params[:id] || Digest::MD5.hexdigest(content.to_s)
|
23
|
+
end
|
24
|
+
|
25
|
+
def score
|
26
|
+
@score ||= @params[:score]
|
27
|
+
end
|
28
|
+
|
29
|
+
def elo
|
30
|
+
@elo = if @params[:elo]
|
31
|
+
@params[:elo]
|
32
|
+
elsif @score
|
33
|
+
generate_elo_by_score
|
34
|
+
else
|
35
|
+
BASE_ELO
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def ffa_win_count
|
40
|
+
@ffa_win_count ||= @params[:ffa_win_count] || 0
|
41
|
+
end
|
42
|
+
|
43
|
+
def ffa_lose_count
|
44
|
+
@ffa_lose_count ||= @params[:ffa_lose_count] || 0
|
45
|
+
end
|
10
46
|
|
11
|
-
|
12
|
-
@
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
@
|
17
|
-
@ffa_lose_count = params[:ffa_lose_count] || 0
|
18
|
-
@ffa_draw_count = params[:ffa_draw_count] || 0
|
19
|
-
@eliminated = params[:eliminated] || nil
|
47
|
+
def ffa_draw_count
|
48
|
+
@ffa_draw_count ||= @params[:ffa_draw_count] || 0
|
49
|
+
end
|
50
|
+
|
51
|
+
def eliminated
|
52
|
+
@eliminated ||= @params[:eliminated]
|
20
53
|
end
|
21
|
-
attr_accessor :rank
|
22
54
|
|
23
55
|
def score=(value)
|
24
56
|
ActiveGenie::Logger.call({ code: :new_score, player_id: id, score: value }) if value != @score
|
@@ -28,7 +60,7 @@ module ActiveGenie
|
|
28
60
|
|
29
61
|
def elo=(value)
|
30
62
|
ActiveGenie::Logger.call({ code: :new_elo, player_id: id, elo: value }) if value != @elo
|
31
|
-
@elo = value
|
63
|
+
@elo = value || BASE_ELO
|
32
64
|
end
|
33
65
|
|
34
66
|
def eliminated=(value)
|
@@ -51,10 +83,8 @@ module ActiveGenie
|
|
51
83
|
ActiveGenie::Logger.call({ code: :new_ffa_score, player_id: id, result: 'lose', ffa_score: })
|
52
84
|
end
|
53
85
|
|
54
|
-
attr_reader :id, :content, :score, :elo, :ffa_win_count, :ffa_lose_count, :ffa_draw_count, :eliminated, :name
|
55
|
-
|
56
86
|
def ffa_score
|
57
|
-
@ffa_win_count * 3 + @ffa_draw_count
|
87
|
+
(@ffa_win_count * 3) + @ffa_draw_count
|
58
88
|
end
|
59
89
|
|
60
90
|
def sort_value
|
@@ -67,13 +97,15 @@ module ActiveGenie
|
|
67
97
|
|
68
98
|
def to_h
|
69
99
|
{
|
70
|
-
id:, name:, content:,
|
100
|
+
id:, name:, content:,
|
101
|
+
|
102
|
+
score:, elo:,
|
71
103
|
ffa_win_count:, ffa_lose_count:, ffa_draw_count:,
|
72
104
|
eliminated:, ffa_score:, sort_value:
|
73
105
|
}
|
74
106
|
end
|
75
107
|
|
76
|
-
def method_missing(method_name, *args, &
|
108
|
+
def method_missing(method_name, *args, &)
|
77
109
|
if method_name == :[] && args.size == 1
|
78
110
|
attr_name = args.first.to_sym
|
79
111
|
|
@@ -90,11 +122,13 @@ module ActiveGenie
|
|
90
122
|
method_name == :[] || super
|
91
123
|
end
|
92
124
|
|
125
|
+
BASE_ELO = 1000
|
126
|
+
|
127
|
+
private
|
128
|
+
|
93
129
|
def generate_elo_by_score
|
94
130
|
BASE_ELO + ((@score || 0) - 50)
|
95
131
|
end
|
96
|
-
|
97
|
-
BASE_ELO = 1000
|
98
132
|
end
|
99
133
|
end
|
100
134
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative '
|
3
|
+
require_relative 'player'
|
4
4
|
|
5
5
|
module ActiveGenie
|
6
6
|
module Ranking
|
@@ -11,18 +11,26 @@ module ActiveGenie
|
|
11
11
|
attr_reader :players
|
12
12
|
|
13
13
|
def coefficient_of_variation
|
14
|
-
|
15
|
-
return nil if score_list.empty?
|
14
|
+
mean = score_mean
|
16
15
|
|
17
|
-
mean = score_list.sum.to_f / score_list.size
|
18
16
|
return nil if mean.zero?
|
19
17
|
|
20
|
-
variance =
|
18
|
+
variance = all_scores.map { |num| (num - mean)**2 }.sum / all_scores.size
|
21
19
|
standard_deviation = Math.sqrt(variance)
|
22
20
|
|
23
21
|
(standard_deviation / mean) * 100
|
24
22
|
end
|
25
23
|
|
24
|
+
def all_scores
|
25
|
+
eligible.map(&:score).compact
|
26
|
+
end
|
27
|
+
|
28
|
+
def score_mean
|
29
|
+
return 0 if all_scores.empty?
|
30
|
+
|
31
|
+
all_scores.sum.to_f / all_scores.size
|
32
|
+
end
|
33
|
+
|
26
34
|
def calc_relegation_tier
|
27
35
|
eligible[(tier_size * -1)..]
|
28
36
|
end
|
@@ -44,19 +52,13 @@ module ActiveGenie
|
|
44
52
|
end
|
45
53
|
|
46
54
|
def sorted
|
47
|
-
|
48
|
-
sorted_players.each_with_index { |p, i| p.rank = i + 1 }
|
49
|
-
sorted_players
|
55
|
+
@players.sort_by { |p| -p.sort_value }
|
50
56
|
end
|
51
57
|
|
52
58
|
def to_json(*_args)
|
53
59
|
to_h.to_json
|
54
60
|
end
|
55
61
|
|
56
|
-
def to_h
|
57
|
-
sorted.map(&:to_h)
|
58
|
-
end
|
59
|
-
|
60
62
|
def method_missing(...)
|
61
63
|
@players.send(...)
|
62
64
|
end
|
@@ -80,7 +82,9 @@ module ActiveGenie
|
|
80
82
|
# - 14 eligible, tier_size: 4
|
81
83
|
# 4 rounds to reach top 10 with 50 players
|
82
84
|
def tier_size
|
83
|
-
|
85
|
+
size = (eligible_size / 3).ceil
|
86
|
+
|
87
|
+
size.clamp(10, eligible_size - 10)
|
84
88
|
end
|
85
89
|
end
|
86
90
|
end
|