active_genie 0.0.8 → 0.0.12

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.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +36 -72
  3. data/VERSION +1 -1
  4. data/lib/active_genie/battle/README.md +2 -2
  5. data/lib/active_genie/battle/basic.rb +52 -56
  6. data/lib/active_genie/battle.rb +1 -1
  7. data/lib/active_genie/clients/openai_client.rb +119 -0
  8. data/lib/active_genie/clients/unified_client.rb +19 -0
  9. data/lib/active_genie/configuration/log_config.rb +14 -0
  10. data/lib/active_genie/configuration/openai_config.rb +56 -0
  11. data/lib/active_genie/configuration/providers_config.rb +37 -0
  12. data/lib/active_genie/configuration.rb +18 -23
  13. data/lib/active_genie/data_extractor/README.md +4 -5
  14. data/lib/active_genie/data_extractor/basic.rb +13 -13
  15. data/lib/active_genie/data_extractor/from_informal.rb +7 -7
  16. data/lib/active_genie/data_extractor.rb +1 -1
  17. data/lib/active_genie/logger.rb +72 -0
  18. data/lib/active_genie/ranking/README.md +43 -0
  19. data/lib/active_genie/ranking/elo_round.rb +113 -0
  20. data/lib/active_genie/ranking/free_for_all.rb +76 -0
  21. data/lib/active_genie/ranking/player.rb +97 -0
  22. data/lib/active_genie/{leaderboard → ranking}/players_collection.rb +18 -11
  23. data/lib/active_genie/ranking/ranking.rb +98 -0
  24. data/lib/active_genie/ranking/ranking_scoring.rb +71 -0
  25. data/lib/active_genie/ranking.rb +12 -0
  26. data/lib/active_genie/scoring/README.md +4 -8
  27. data/lib/active_genie/scoring/basic.rb +58 -24
  28. data/lib/active_genie/scoring/{recommended_reviews.rb → recommended_reviewers.rb} +21 -12
  29. data/lib/active_genie/scoring.rb +4 -4
  30. data/lib/active_genie.rb +10 -18
  31. data/lib/tasks/install.rake +3 -3
  32. data/lib/tasks/templates/active_genie.rb +17 -0
  33. metadata +74 -90
  34. data/lib/active_genie/clients/openai.rb +0 -61
  35. data/lib/active_genie/clients/router.rb +0 -41
  36. data/lib/active_genie/leaderboard/elo_ranking.rb +0 -88
  37. data/lib/active_genie/leaderboard/leaderboard.rb +0 -72
  38. data/lib/active_genie/leaderboard/league.rb +0 -48
  39. data/lib/active_genie/leaderboard/player.rb +0 -52
  40. data/lib/active_genie/leaderboard.rb +0 -11
  41. data/lib/active_genie/utils/math.rb +0 -15
  42. data/lib/tasks/templates/active_genie.yml +0 -7
@@ -1,33 +1,28 @@
1
- require 'yaml'
1
+ require_relative 'configuration/providers_config'
2
+ require_relative 'configuration/openai_config'
3
+ require_relative 'configuration/log_config'
2
4
 
3
5
  module ActiveGenie
4
- class Configuration
5
- attr_accessor :path_to_config
6
+ module Configuration
7
+ module_function
6
8
 
7
- def initialize
8
- @path_to_config = File.join('config', 'active_genie.yml')
9
- end
10
-
11
- def values
12
- return @values if @values
13
-
14
- @values = load_values.transform_keys(&:to_sym)
15
- @values.each do |key, _value|
16
- @values[key][:model] = key
17
- @values[key] = @values[key].transform_keys(&:to_sym)
9
+ def providers
10
+ @providers ||= begin
11
+ p = ProvidersConfig.new
12
+ p.register(:openai, ActiveGenie::Configuration::OpenaiConfig)
13
+ p
18
14
  end
19
15
  end
20
16
 
21
- private
22
-
23
- def load_values
24
- return {} unless File.exist?(@path_to_config)
17
+ def log
18
+ @log ||= LogConfig.new
19
+ end
25
20
 
26
- yaml_content = ERB.new(File.read(@path_to_config)).result
27
- YAML.safe_load(yaml_content, aliases: true) || {}
28
- rescue Psych::SyntaxError => e
29
- warn "ActiveGenie.warning: Config file '#{@path_to_config}' is not a valid YAML file (#{e.message}), using default configuration"
30
- {}
21
+ def to_h(configs = {})
22
+ {
23
+ providers: providers.to_h(configs.dig(:providers) || {}),
24
+ log: log.to_h(configs.dig(:log) || {})
25
+ }
31
26
  end
32
27
  end
33
28
  end
@@ -65,7 +65,6 @@ The `from_informal` method extends the basic extraction by analyzing rhetorical
65
65
  - Litotes ("not bad", "isn't terrible")
66
66
  - Affirmative expressions ("sure", "no problem")
67
67
  - Negative expressions ("nah", "not really")
68
- - Hedging ("maybe", "I guess")
69
68
 
70
69
  ### Example
71
70
 
@@ -95,7 +94,7 @@ result = ActiveGenie::DataExtractor.from_informal(text, schema)
95
94
 
96
95
  ## Interface
97
96
 
98
- ### `.call(text, data_to_extract, options = {})`
97
+ ### `.call(text, data_to_extract, config = {})`
99
98
  Extracts structured data from text based on a predefined schema.
100
99
 
101
100
  #### Parameters
@@ -103,9 +102,9 @@ Extracts structured data from text based on a predefined schema.
103
102
  | --- | --- | --- | --- | --- |
104
103
  | `text` | `String` | The text to analyze and extract data from | Yes | "John Doe is 25 years old" |
105
104
  | `data_to_extract` | `Hash` | Schema defining the data structure to extract | Yes | `{ name: { type: 'string' } }` |
106
- | `options` | `Hash` | Additional extraction configuration | No | `{ model: "gpt-4" }` |
105
+ | `config` | `Hash` | Additional extraction configuration | No | `{ model: "gpt-4" }` |
107
106
 
108
- #### Options
107
+ #### config
109
108
  | Name | Type | Description |
110
109
  | --- | --- | --- |
111
110
  | `model` | `String` | The model to use for the extraction |
@@ -117,7 +116,7 @@ Extracts structured data from text based on a predefined schema.
117
116
  - Explanation field for each extracted value
118
117
  - Additional analysis fields when using `from_informal`
119
118
 
120
- ### `.from_informal(text, data_to_extract, options = {})`
119
+ ### `.from_informal(text, data_to_extract, config = {})`
121
120
  Extends basic extraction with rhetorical analysis, particularly for litotes.
122
121
 
123
122
  #### Additional Return Fields
@@ -1,9 +1,10 @@
1
- require_relative '../clients/router.rb'
1
+
2
+ require_relative '../clients/unified_client'
2
3
 
3
4
  module ActiveGenie::DataExtractor
4
5
  class Basic
5
- def self.call(text, data_to_extract, options: {})
6
- new(text, data_to_extract, options:).call
6
+ def self.call(...)
7
+ new(...).call
7
8
  end
8
9
 
9
10
  # Extracts structured data from text based on a predefined schema.
@@ -11,9 +12,7 @@ module ActiveGenie::DataExtractor
11
12
  # @param text [String] The input text to analyze and extract data from
12
13
  # @param data_to_extract [Hash] Schema defining the data structure to extract.
13
14
  # Each key in the hash represents a field to extract, and its value defines the expected type and constraints.
14
- # @param options [Hash] Additional options for the extraction process
15
- # @option options [String] :model The model to use for the extraction
16
- # @option options [String] :api_key The API key to use for the extraction
15
+ # @param config [Hash] Additional config for the extraction process
17
16
  #
18
17
  # @return [Hash] The extracted data matching the schema structure. Each field will include
19
18
  # both the extracted value and an explanation of how it was derived.
@@ -27,10 +26,10 @@ module ActiveGenie::DataExtractor
27
26
  # DataExtractor.call(text, schema)
28
27
  # # => { name: "John Doe", name_explanation: "Found directly in text",
29
28
  # # age: 25, age_explanation: "Explicitly stated as 25 years old" }
30
- def initialize(text, data_to_extract, options: {})
29
+ def initialize(text, data_to_extract, config: {})
31
30
  @text = text
32
31
  @data_to_extract = data_to_extract
33
- @options = options
32
+ @config = ActiveGenie::Configuration.to_h(config)
34
33
  end
35
34
 
36
35
  def call
@@ -47,7 +46,12 @@ module ActiveGenie::DataExtractor
47
46
  }
48
47
  }
49
48
 
50
- ::ActiveGenie::Clients::Router.function_calling(messages, function, options: @options)
49
+ ::ActiveGenie::Clients::UnifiedClient.function_calling(
50
+ messages,
51
+ function,
52
+ model_tier: 'lower_tier',
53
+ config: @config
54
+ )
51
55
  end
52
56
 
53
57
  private
@@ -60,10 +64,6 @@ module ActiveGenie::DataExtractor
60
64
  1. **Identify Data Types**: Determine the types of data to collect, such as names, dates, email addresses, phone numbers, etc.
61
65
  2. **Extract Information**: Use pattern recognition and language understanding to identify and extract the relevant pieces of data from the user message.
62
66
  3. **Categorize Data**: Assign the extracted data to the appropriate predefined fields.
63
- 4. **Structure Data**: Format the extracted and categorized data in a structured format, such as JSON.
64
-
65
- # Output Format
66
- The output should be a JSON object containing fields with their corresponding extracted values. If a value is not found, the field should still be included with a null value.
67
67
 
68
68
  # Notes
69
69
  - Handle missing or partial information gracefully.
@@ -1,7 +1,7 @@
1
1
  module ActiveGenie::DataExtractor
2
2
  class FromInformal
3
- def self.call(text, data_to_extract, options: {})
4
- new(text, data_to_extract, options:).call()
3
+ def self.call(...)
4
+ new(...).call()
5
5
  end
6
6
 
7
7
  # Extracts data from informal text while also detecting litotes and their meanings.
@@ -9,7 +9,7 @@ module ActiveGenie::DataExtractor
9
9
  #
10
10
  # @param text [String] The informal text to analyze
11
11
  # @param data_to_extract [Hash] Schema defining the data structure to extract
12
- # @param options [Hash] Additional options for the extraction process
12
+ # @param config [Hash] Additional config for the extraction process
13
13
  #
14
14
  # @return [Hash] The extracted data including litote analysis. In addition to the
15
15
  # schema-defined fields, includes:
@@ -23,17 +23,17 @@ module ActiveGenie::DataExtractor
23
23
  # # => { mood: "positive", mood_explanation: "Speaker views weather favorably",
24
24
  # # message_litote: true,
25
25
  # # litote_rephrased: "The weather is good today" }
26
- def initialize(text, data_to_extract, options: {})
26
+ def initialize(text, data_to_extract, config: {})
27
27
  @text = text
28
28
  @data_to_extract = data_to_extract
29
- @options = options
29
+ @config = ActiveGenie::Configuration.to_h(config)
30
30
  end
31
31
 
32
32
  def call
33
- response = Basic.call(@text, data_to_extract_with_litote, options: @options)
33
+ response = Basic.call(@text, data_to_extract_with_litote, config: @config)
34
34
 
35
35
  if response['message_litote']
36
- response = Basic.call(response['litote_rephrased'], @data_to_extract, options: @options)
36
+ response = Basic.call(response['litote_rephrased'], @data_to_extract, config: @config)
37
37
  end
38
38
 
39
39
  response
@@ -2,7 +2,7 @@ require_relative 'data_extractor/basic'
2
2
  require_relative 'data_extractor/from_informal'
3
3
 
4
4
  module ActiveGenie
5
- # Extract structured data from text using AI-powered analysis, handling informal language and complex expressions.
5
+ # See the [DataExtractor README](lib/active_genie/data_extractor/README.md) for more information.
6
6
  module DataExtractor
7
7
  module_function
8
8
 
@@ -0,0 +1,72 @@
1
+ require 'json'
2
+ require 'fileutils'
3
+
4
+ module ActiveGenie
5
+ module Logger
6
+ module_function
7
+
8
+ def with_context(context)
9
+ @context ||= {}
10
+ begin
11
+ @context = @context.merge(context)
12
+ yield if block_given?
13
+ ensure
14
+ @context.delete_if { |key, _| context.key?(key) }
15
+ end
16
+ end
17
+
18
+ def info(log)
19
+ save(log, level: :info)
20
+ end
21
+
22
+ def error(log)
23
+ save(log, level: :error)
24
+ end
25
+
26
+ def warn(log)
27
+ save(log, level: :warn)
28
+ end
29
+
30
+ def debug(log)
31
+ save(log, level: :debug)
32
+ end
33
+
34
+ def trace(log)
35
+ save(log, level: :trace)
36
+ end
37
+
38
+ def save(data, level: :info)
39
+ log = @context.merge(data || {})
40
+ log[:timestamp] = Time.now
41
+ log[:level] = level.to_s.upcase
42
+ log[:process_id] = Process.pid
43
+ config_log_level = LOG_LEVELS[log.dig(:config, :log_level)] || LOG_LEVELS[:info]
44
+
45
+ FileUtils.mkdir_p('logs')
46
+ File.write('logs/active_genie.log', "#{JSON.generate(log)}\n", mode: 'a')
47
+ if config_log_level >= LOG_LEVELS[level]
48
+ $stdout.puts log
49
+ else
50
+ $stdout.print '.'
51
+ end
52
+
53
+ log
54
+ end
55
+
56
+ private
57
+
58
+ # Log Levels
59
+ #
60
+ # LOG_LEVELS defines different levels of logging within the application.
61
+ # Each level serves a specific purpose, balancing verbosity and relevance.
62
+ #
63
+ # - :info -> General log messages providing an overview of application behavior, ensuring readability without excessive detail.
64
+ # - :warn -> Indicates unexpected behaviors that do not halt execution but require attention, such as retries, timeouts, or necessary conversions.
65
+ # - :error -> Represents critical errors that prevent the application from functioning correctly.
66
+ # - :debug -> Provides detailed logs for debugging, offering the necessary context for audits but with slightly less detail than trace logs.
67
+ # - :trace -> Logs every external call with the highest level of detail, primarily for auditing or state-saving purposes. These logs do not provide context regarding triggers or reasons.
68
+ LOG_LEVELS = { info: 0, error: 0, warn: 1, debug: 2, trace: 3 }.freeze
69
+
70
+ attr_accessor :context
71
+ end
72
+ end
@@ -0,0 +1,43 @@
1
+ # Ranking
2
+
3
+ The `ActiveGenie::Ranking` module organizes players based on scores derived from textual evaluations and then ranks them using a multi-stage process. It leverages the scoring system from the `ActiveGenie::Scoring` module to assign initial scores, and then applies advanced ranking methods to produce a competitive ranking.
4
+
5
+ ## Overview
6
+
7
+ The ranking system performs the following steps:
8
+
9
+ 1. **Initial Scoring**: Each player’s textual content is evaluated using `ActiveGenie::Scoring`. This produces a `score` based on multiple expert reviews.
10
+
11
+ 2. **Elimination of Poor Performers**: Players whose scores show a high coefficient of variation (indicating inconsistency) are progressively eliminated. This ensures that only competitive candidates continue in the ranking process.
12
+
13
+ 3. **ELO Ranking**: If there are more than 10 eligible players, an ELO-based ranking is applied. Battles between players are simulated via `ActiveGenie::Battle`, and scores are updated using an ELO algorithm tailored to the ranking.
14
+
15
+ 4. **Free for all Matches**: Finally, the remaining players engage in head-to-head matches where each unique pair competes. Match outcomes (wins, losses, draws) are recorded to finalize the rankings.
16
+
17
+ ## Components
18
+
19
+ - **ranking**: Orchestrates the entire ranking process. Initializes player scores, eliminates outliers, and coordinates ranking rounds.
20
+
21
+ - **EloRanking**: Applies an ELO-based system to rank players through simulated battles. It updates players’ ELO scores based on match outcomes and predefined rules (including penalties for losses).
22
+
23
+ - **Free for all**: Conducts complete pairwise matches among eligible players to record win/loss/draw statistics and refine the final standings.
24
+
25
+ ## Usage
26
+
27
+ Call the ranking using:
28
+
29
+ ```ruby
30
+ result = ActiveGenie::Ranking.call(players, criteria, config: {})
31
+ ```
32
+
33
+ - `players`: A collection of player instances, each containing textual content to be scored.
34
+ - `criteria`: A string defining the evaluation criteria used by the scoring system.
35
+ - `config`: A hash of additional parameters for customization (e.g., model, api_key).
36
+
37
+ The method processes the players through scoring, elimination, and ranking phases, then returns a hash containing the player statistics and rankings.
38
+
39
+ ## Possible improvements
40
+ - Adjust initial criteria to ensure consistency
41
+ - Adjust each player's content to ensure consistency
42
+ - Support players with images or audio
43
+ - Parallelize processing battles and scoring
@@ -0,0 +1,113 @@
1
+ require_relative '../battle/basic'
2
+
3
+ module ActiveGenie::Ranking
4
+ class EloRound
5
+ def self.call(...)
6
+ new(...).call
7
+ end
8
+
9
+ def initialize(players, criteria, config: {})
10
+ @players = players
11
+ @relegation_tier = players.calc_relegation_tier
12
+ @defender_tier = players.calc_defender_tier
13
+ @criteria = criteria
14
+ @config = config
15
+ @tmp_defenders = []
16
+ end
17
+
18
+ def call
19
+ ActiveGenie::Logger.with_context(log_context) do
20
+ matches.each do |player_a, player_b|
21
+ # TODO: battle can take a while, can be parallelized
22
+ winner, loser = battle(player_a, player_b)
23
+
24
+ next if winner.nil? || loser.nil?
25
+
26
+ new_winner_elo, new_loser_elo = calculate_new_elo(winner.elo, loser.elo)
27
+
28
+ winner.elo = new_winner_elo
29
+ loser.elo = new_loser_elo
30
+ end
31
+
32
+ # TODO: add a round report. Duration, Elo changes, etc.
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ BATTLE_PER_PLAYER = 3
39
+ LOSE_PENALTY = 15
40
+ K = 32
41
+
42
+ def matches
43
+ @relegation_tier.reduce([]) do |matches, attack_player|
44
+ BATTLE_PER_PLAYER.times do
45
+ matches << [attack_player, next_defense_player].shuffle
46
+ end
47
+ matches
48
+ end
49
+ end
50
+
51
+ def next_defense_player
52
+ @tmp_defenders = @defender_tier if @tmp_defenders.size.zero?
53
+
54
+ @tmp_defenders.shuffle.pop!
55
+ end
56
+
57
+ def battle(player_a, player_b)
58
+ result = ActiveGenie::Battle.basic(
59
+ player_a,
60
+ player_b,
61
+ @criteria,
62
+ config: @config
63
+ )
64
+
65
+ winner, loser = case result['winner']
66
+ when 'player_a' then [player_a, player_b]
67
+ when 'player_b' then [player_b, player_a]
68
+ when 'draw' then [nil, nil]
69
+ end
70
+
71
+ ActiveGenie::Logger.debug({
72
+ step: :elo_round_battle,
73
+ player_ids: [player_a.id, player_b.id],
74
+ winner_id: winner&.id,
75
+ loser_id: loser&.id,
76
+ reasoning: result['reasoning']
77
+ })
78
+
79
+ [winner, loser]
80
+ end
81
+
82
+ # INFO: Read more about the Elo rating system on https://en.wikipedia.org/wiki/Elo_rating_system
83
+ def calculate_new_elo(winner_elo, loser_elo)
84
+ expected_score_a = 1 / (1 + 10**((loser_elo - winner_elo) / 400))
85
+ expected_score_b = 1 - expected_score_a
86
+
87
+ new_winner_elo = [winner_elo + K * (1 - expected_score_a), max_defense_elo].min
88
+ new_loser_elo = [loser_elo + K * (1 - expected_score_b) - LOSE_PENALTY, min_relegation_elo].max
89
+
90
+ [new_winner_elo, new_loser_elo]
91
+ end
92
+
93
+ def max_defense_elo
94
+ @defender_tier.max_by(&:elo).elo
95
+ end
96
+
97
+ def min_relegation_elo
98
+ @relegation_tier.min_by(&:elo).elo
99
+ end
100
+
101
+ def log_context
102
+ { elo_round_id: }
103
+ end
104
+
105
+ def elo_round_id
106
+ relegation_tier_ids = @relegation_tier.map(&:id).join(',')
107
+ defender_tier_ids = @defender_tier.map(&:id).join(',')
108
+
109
+ ranking_unique_key = [relegation_tier_ids, defender_tier_ids, @criteria, @config.to_json].join('-')
110
+ Digest::MD5.hexdigest(ranking_unique_key)
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,76 @@
1
+ require_relative '../battle/basic'
2
+
3
+ module ActiveGenie::Ranking
4
+ class FreeForAll
5
+ def self.call(...)
6
+ new(...).call
7
+ end
8
+
9
+ def initialize(players, criteria, config: {})
10
+ @players = players
11
+ @criteria = criteria
12
+ @config = config
13
+ end
14
+
15
+ def call
16
+ ActiveGenie::Logger.with_context(log_context) do
17
+ matches.each do |player_a, player_b|
18
+ winner, loser = battle(player_a, player_b)
19
+
20
+ if winner.nil? || loser.nil?
21
+ player_a.draw!
22
+ player_b.draw!
23
+ else
24
+ winner.win!
25
+ loser.lose!
26
+ end
27
+ end
28
+
29
+ # TODO: add a freeForAll report. Duration, Elo changes, etc.
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ # TODO: reduce the number of matches based on transitivity.
36
+ # For example, if A is better than B, and B is better than C, then battle between A and C should be auto win A
37
+ def matches
38
+ @players.eligible.combination(2).to_a
39
+ end
40
+
41
+ def battle(player_a, player_b)
42
+ result = ActiveGenie::Battle.basic(
43
+ player_a,
44
+ player_b,
45
+ @criteria,
46
+ config: @config
47
+ )
48
+
49
+ winner, loser = case result['winner']
50
+ when 'player_a' then [player_a, player_b, result['reasoning']]
51
+ when 'player_b' then [player_b, player_a, result['reasoning']]
52
+ when 'draw' then [nil, nil, result['reasoning']]
53
+ end
54
+
55
+ ActiveGenie::Logger.debug({
56
+ step: :free_for_all_battle,
57
+ player_ids: [player_a.id, player_b.id],
58
+ winner_id: winner&.id,
59
+ loser_id: loser&.id,
60
+ reasoning: result['reasoning']
61
+ })
62
+
63
+ [winner, loser]
64
+ end
65
+
66
+ def log_context
67
+ { free_for_all_id: }
68
+ end
69
+
70
+ def free_for_all_id
71
+ eligible_ids = @players.eligible.map(&:id).join(',')
72
+ ranking_unique_key = [eligible_ids, @criteria, @config.to_json].join('-')
73
+ Digest::MD5.hexdigest(ranking_unique_key)
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,97 @@
1
+ require 'digest'
2
+
3
+ module ActiveGenie::Ranking
4
+ class Player
5
+ def initialize(params)
6
+ params = { content: params } if params.is_a?(String)
7
+
8
+ @content = params.dig(:content) || params
9
+ @id = params.dig(:id) || Digest::MD5.hexdigest(@content)
10
+ @score = params.dig(:score) || nil
11
+ @elo = params.dig(:elo) || nil
12
+ @ffa_win_count = params.dig(:ffa_win_count) || 0
13
+ @ffa_lose_count = params.dig(:ffa_lose_count) || 0
14
+ @ffa_draw_count = params.dig(:ffa_draw_count) || 0
15
+ @eliminated = params.dig(:eliminated) || nil
16
+ end
17
+
18
+ attr_reader :id, :content, :score, :elo,
19
+ :ffa_win_count, :ffa_lose_count, :ffa_draw_count,
20
+ :eliminated
21
+ attr_accessor :rank
22
+
23
+ def score=(value)
24
+ @score = value
25
+ ActiveGenie::Logger.debug({ step: :new_score, player_id: id, score: value })
26
+ end
27
+
28
+ def elo
29
+ generate_elo_by_score if @elo.nil?
30
+
31
+ @elo
32
+ end
33
+
34
+ def elo=(value)
35
+ @elo = value
36
+ ActiveGenie::Logger.debug({ step: :new_elo, player_id: id, elo: value })
37
+ end
38
+
39
+ def eliminated=(value)
40
+ @eliminated = value
41
+ ActiveGenie::Logger.debug({ step: :new_eliminated, player_id: id, eliminated: value })
42
+ end
43
+
44
+ def draw!
45
+ @ffa_draw_count += 1
46
+ ActiveGenie::Logger.debug({ step: :new_ffa_score, player_id: id, result: 'draw', ffa_score: })
47
+ end
48
+
49
+ def win!
50
+ @ffa_win_count += 1
51
+ ActiveGenie::Logger.debug({ step: :new_ffa_score, player_id: id, result: 'win', ffa_score: })
52
+ end
53
+
54
+ def lose!
55
+ @ffa_lose_count += 1
56
+ ActiveGenie::Logger.debug({ step: :new_ffa_score, player_id: id, result: 'lose', ffa_score: })
57
+ end
58
+
59
+ def ffa_score
60
+ @ffa_win_count * 3 + @ffa_draw_count
61
+ end
62
+
63
+ def to_h
64
+ {
65
+ id:, content:, score:, elo:,
66
+ ffa_win_count:, ffa_lose_count:, ffa_draw_count:,
67
+ eliminated:, ffa_score:
68
+ }
69
+ end
70
+
71
+ def method_missing(method_name, *args, &block)
72
+ if method_name == :[] && args.size == 1
73
+ attr_name = args.first.to_sym
74
+
75
+ if respond_to?(attr_name)
76
+ return send(attr_name)
77
+ else
78
+ return nil
79
+ end
80
+ end
81
+
82
+ super
83
+ end
84
+
85
+ def respond_to_missing?(method_name, include_private = false)
86
+ method_name == :[] || super
87
+ end
88
+
89
+ private
90
+
91
+ BASE_ELO = 1000
92
+
93
+ def generate_elo_by_score
94
+ @elo = BASE_ELO + ((@score || 0) - 50)
95
+ end
96
+ end
97
+ end
@@ -1,7 +1,6 @@
1
- require_relative '../utils/math'
2
1
  require_relative './player'
3
2
 
4
- module ActiveGenie::Leaderboard
3
+ module ActiveGenie::Ranking
5
4
  class PlayersCollection
6
5
  def initialize(param_players)
7
6
  @players = build(param_players)
@@ -9,9 +8,11 @@ module ActiveGenie::Leaderboard
9
8
  attr_reader :players
10
9
 
11
10
  def coefficient_of_variation
12
- score_list = eligible.map(&:score)
11
+ score_list = eligible.map(&:score).compact
12
+ return nil if score_list.empty?
13
+
13
14
  mean = score_list.sum.to_f / score_list.size
14
- return nil if mean == 0 # To avoid division by zero
15
+ return nil if mean == 0
15
16
 
16
17
  variance = score_list.map { |num| (num - mean) ** 2 }.sum / score_list.size
17
18
  standard_deviation = Math.sqrt(variance)
@@ -19,11 +20,11 @@ module ActiveGenie::Leaderboard
19
20
  (standard_deviation / mean) * 100
20
21
  end
21
22
 
22
- def tier_relegation
23
+ def calc_relegation_tier
23
24
  eligible[(tier_size*-1)..-1]
24
25
  end
25
26
 
26
- def tier_defense
27
+ def calc_defender_tier
27
28
  eligible[(tier_size*-2)...(tier_size*-1)]
28
29
  end
29
30
 
@@ -35,18 +36,24 @@ module ActiveGenie::Leaderboard
35
36
  @players.reject(&:eliminated).size
36
37
  end
37
38
 
39
+ def elo_eligible?
40
+ eligible.size > 15
41
+ end
42
+
43
+ def sorted
44
+ @players.sort_by { |p| [-p.ffa_score, -(p.elo || 0), -(p.score || 0)] }
45
+ @players.each_with_index { |p, i| p.rank = i + 1 }
46
+ @players
47
+ end
48
+
38
49
  def to_h
39
- sorted.map(&:to_h)
50
+ sorted.map { |p| p.to_h }
40
51
  end
41
52
 
42
53
  def method_missing(...)
43
54
  @players.send(...)
44
55
  end
45
56
 
46
- def sorted
47
- @players.sort_by { |p| [-p.league_score, -(p.elo || 0), -p.score] }
48
- end
49
-
50
57
  private
51
58
 
52
59
  def build(param_players)