active_genie 0.0.3 → 0.0.10

Sign up to get free protection for your applications and to get access to all the features.
Files changed (35) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +146 -59
  3. data/VERSION +1 -1
  4. data/lib/active_genie/battle/README.md +39 -0
  5. data/lib/active_genie/battle/basic.rb +130 -0
  6. data/lib/active_genie/battle.rb +13 -0
  7. data/lib/active_genie/clients/openai_client.rb +77 -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 -22
  13. data/lib/active_genie/data_extractor/README.md +4 -4
  14. data/lib/active_genie/data_extractor/basic.rb +19 -9
  15. data/lib/active_genie/data_extractor/from_informal.rb +18 -7
  16. data/lib/active_genie/data_extractor.rb +5 -5
  17. data/lib/active_genie/league/README.md +43 -0
  18. data/lib/active_genie/league/elo_ranking.rb +121 -0
  19. data/lib/active_genie/league/free_for_all.rb +62 -0
  20. data/lib/active_genie/league/league.rb +120 -0
  21. data/lib/active_genie/league/player.rb +59 -0
  22. data/lib/active_genie/league/players_collection.rb +68 -0
  23. data/lib/active_genie/league.rb +12 -0
  24. data/lib/active_genie/logger.rb +45 -0
  25. data/lib/active_genie/scoring/README.md +4 -8
  26. data/lib/active_genie/scoring/basic.rb +24 -14
  27. data/lib/active_genie/scoring/recommended_reviews.rb +7 -9
  28. data/lib/active_genie/scoring.rb +5 -5
  29. data/lib/active_genie.rb +14 -11
  30. data/lib/tasks/install.rake +3 -3
  31. data/lib/tasks/templates/active_genie.rb +17 -0
  32. metadata +119 -14
  33. data/lib/active_genie/clients/openai.rb +0 -59
  34. data/lib/active_genie/clients/router.rb +0 -41
  35. data/lib/tasks/templates/active_ai.yml +0 -7
@@ -1,32 +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(__dir__, 'config', 'gen_ai.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.load_file(@path_to_config) || {}
27
- rescue Psych::SyntaxError => e
28
- warn "ActiveGenie.warning: Config file '#{@path_to_config}' is not a valid YAML file (#{e.message}), using default configuration"
29
- {}
21
+ def to_h(configs = {})
22
+ {
23
+ providers: providers.to_h(configs.dig(:providers) || {}),
24
+ log: log.to_h(configs.dig(:log) || {})
25
+ }
30
26
  end
31
27
  end
32
28
  end
@@ -95,7 +95,7 @@ result = ActiveGenie::DataExtractor.from_informal(text, schema)
95
95
 
96
96
  ## Interface
97
97
 
98
- ### `.call(text, data_to_extract, options = {})`
98
+ ### `.call(text, data_to_extract, config = {})`
99
99
  Extracts structured data from text based on a predefined schema.
100
100
 
101
101
  #### Parameters
@@ -103,9 +103,9 @@ Extracts structured data from text based on a predefined schema.
103
103
  | --- | --- | --- | --- | --- |
104
104
  | `text` | `String` | The text to analyze and extract data from | Yes | "John Doe is 25 years old" |
105
105
  | `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" }` |
106
+ | `config` | `Hash` | Additional extraction configuration | No | `{ model: "gpt-4" }` |
107
107
 
108
- #### Options
108
+ #### config
109
109
  | Name | Type | Description |
110
110
  | --- | --- | --- |
111
111
  | `model` | `String` | The model to use for the extraction |
@@ -117,7 +117,7 @@ Extracts structured data from text based on a predefined schema.
117
117
  - Explanation field for each extracted value
118
118
  - Additional analysis fields when using `from_informal`
119
119
 
120
- ### `.from_informal(text, data_to_extract, options = {})`
120
+ ### `.from_informal(text, data_to_extract, config = {})`
121
121
  Extends basic extraction with rhetorical analysis, particularly for litotes.
122
122
 
123
123
  #### 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(text, data_to_extract, config: {})
7
+ new(text, data_to_extract, config:).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,7 @@ 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(messages, function, config:)
51
50
  end
52
51
 
53
52
  private
@@ -84,5 +83,16 @@ module ActiveGenie::DataExtractor
84
83
 
85
84
  with_explaination
86
85
  end
86
+
87
+ def config
88
+ {
89
+ all_providers: { model_tier: 'lower_tier' },
90
+ log: {
91
+ **(@config.dig(:log) || {}),
92
+ trace: self.class.name,
93
+ },
94
+ **@config
95
+ }
96
+ end
87
97
  end
88
98
  end
@@ -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(text, data_to_extract, config: {})
4
+ new(text, data_to_extract, config:).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:)
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:)
37
37
  end
38
38
 
39
39
  response
@@ -54,5 +54,16 @@ module ActiveGenie::DataExtractor
54
54
  }
55
55
  }
56
56
  end
57
+
58
+ def config
59
+ {
60
+ all_providers: { model_tier: 'lower_tier' },
61
+ **@config,
62
+ log: {
63
+ **@config.dig(:log),
64
+ trace: self.class.name,
65
+ },
66
+ }
67
+ end
57
68
  end
58
69
  end
@@ -2,16 +2,16 @@ 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
 
9
- def basic(text, data_to_extract, options: {})
10
- Basic.call(text, data_to_extract, options:)
9
+ def basic(...)
10
+ Basic.call(...)
11
11
  end
12
12
 
13
- def from_informal(text, data_to_extract, options: {})
14
- FromInformal.call(text, data_to_extract, options:)
13
+ def from_informal(...)
14
+ FromInformal.call(...)
15
15
  end
16
16
  end
17
17
  end
@@ -0,0 +1,43 @@
1
+ # league
2
+
3
+ The `ActiveGenie::League` 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 league.
4
+
5
+ ## Overview
6
+
7
+ The league 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 league.
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
+ - **league**: 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 league using:
28
+
29
+ ```ruby
30
+ result = ActiveGenie::League::league.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,121 @@
1
+ require_relative '../battle/basic'
2
+
3
+ module ActiveGenie::Leaderboard
4
+ class EloRanking
5
+ def self.call(players, criteria, config: {})
6
+ new(players, criteria, config:).call
7
+ end
8
+
9
+ def initialize(players, criteria, config: {})
10
+ @players = players
11
+ @criteria = criteria
12
+ @config = config
13
+ @start_time = Time.now
14
+ end
15
+
16
+ def call
17
+ @players.each(&:generate_elo_by_score)
18
+
19
+ round_count = 0
20
+ while @players.eligible_size > MINIMAL_PLAYERS_TO_BATTLE
21
+ round = create_round(@players.tier_relegation, @players.tier_defense)
22
+
23
+ round.each do |player_a, player_b|
24
+ winner, loser = battle(player_a, player_b) # This can take a while, can be parallelized
25
+ update_elo(winner, loser)
26
+ ActiveGenie::Logger.trace({ **log, step: :elo_battle, winner_id: winner.id, loser_id: loser.id, winner_elo: winner.elo, loser_elo: loser.elo })
27
+ end
28
+
29
+ eliminate_all_relegation_players
30
+ round_count += 1
31
+ end
32
+
33
+ ActiveGenie::Logger.info({ **log, step: :elo_end, round_count:, eligible_size: @players.eligible_size })
34
+ @players
35
+ end
36
+
37
+ private
38
+
39
+ MATCHS_PER_PLAYER = 3
40
+ LOSE_PENALTY = 15
41
+ MINIMAL_PLAYERS_TO_BATTLE = 10
42
+ K = 32
43
+
44
+ # Create a round of matches
45
+ # each round is exactly 1 regation player vs 3 defense players for all regation players
46
+ # each match is unique (player vs player)
47
+ # each defense player is battle exactly 3 times
48
+ def create_round(relegation_players, defense_players)
49
+ matches = []
50
+
51
+ relegation_players.each do |player_a|
52
+ player_enemies = []
53
+ MATCHS_PER_PLAYER.times do
54
+ defender = nil
55
+ while defender.nil? || player_enemies.include?(defender.id)
56
+ defender = defense_players.sample
57
+ end
58
+
59
+ matches << [player_a, defender].shuffle
60
+ player_enemies << defender.id
61
+ end
62
+ end
63
+
64
+ matches
65
+ end
66
+
67
+ def battle(player_a, player_b)
68
+ ActiveGenie::Battle.basic(
69
+ player_a,
70
+ player_b,
71
+ @criteria,
72
+ config:
73
+ ).values_at('winner', 'loser')
74
+ end
75
+
76
+ def update_elo(winner, loser)
77
+ return if winner.nil? || loser.nil?
78
+
79
+ new_winner_elo, new_loser_elo = calculate_new_elo(winner.elo, loser.elo)
80
+
81
+ winner.elo = [new_winner_elo, max_defense_elo].min
82
+ loser.elo = [new_loser_elo - LOSE_PENALTY, min_relegation_elo].max
83
+ end
84
+
85
+ def max_defense_elo
86
+ @players.tier_defense.max_by(&:elo).elo
87
+ end
88
+
89
+ def min_relegation_elo
90
+ @players.tier_relegation.min_by(&:elo).elo
91
+ end
92
+
93
+ # Read more about the formula on https://en.wikipedia.org/wiki/Elo_rating_system
94
+ def calculate_new_elo(winner_elo, loser_elo)
95
+ expected_score_a = 1 / (1 + 10**((loser_elo - winner_elo) / 400))
96
+ expected_score_b = 1 - expected_score_a
97
+
98
+ new_elo_winner = winner_elo + K * (1 - expected_score_a)
99
+ new_elo_loser = loser_elo + K * (1 - expected_score_b)
100
+
101
+ [new_elo_winner, new_elo_loser]
102
+ end
103
+
104
+ def eliminate_all_relegation_players
105
+ eliminations = @players.tier_relegation.size
106
+ @players.tier_relegation.each { |player| player.eliminated = 'tier_relegation' }
107
+ ActiveGenie::Logger.trace({ **log, step: :elo_round, eligible_size: @players.eligible_size, eliminations: })
108
+ end
109
+
110
+ def config
111
+ { **@config }
112
+ end
113
+
114
+ def log
115
+ {
116
+ **(@config.dig(:log) || {}),
117
+ duration: Time.now - @start_time
118
+ }
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,62 @@
1
+ require_relative '../battle/basic'
2
+
3
+ module ActiveGenie::Leaderboard
4
+ class FreeForAll
5
+ def self.call(players, criteria, config: {})
6
+ new(players, criteria, config:).call
7
+ end
8
+
9
+ def initialize(players, criteria, config: {})
10
+ @players = players
11
+ @criteria = criteria
12
+ @config = config
13
+ @start_time = Time.now
14
+ end
15
+
16
+ def call
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.free_for_all[:draw] += 1
22
+ player_b.free_for_all[:draw] += 1
23
+ else
24
+ winner.free_for_all[:win] += 1
25
+ loser.free_for_all[:lose] += 1
26
+ end
27
+
28
+ ActiveGenie::Logger.trace({**log, step: :free_for_all_battle, winner_id: winner&.id, player_a_id: player_a.id, player_a_free_for_all_score: player_a.free_for_all_score, player_b_id: player_b.id, player_b_free_for_all_score: player_b.free_for_all_score })
29
+ end
30
+
31
+ @players
32
+ end
33
+
34
+ private
35
+
36
+ # TODO: reduce the number of matches based on transitivity.
37
+ # For example, if A is better than B, and B is better than C, then A should clearly be better than C
38
+ def matches
39
+ @players.eligible.combination(2).to_a
40
+ end
41
+
42
+ def battle(player_a, player_b)
43
+ result = ActiveGenie::Battle.basic(
44
+ player_a,
45
+ player_b,
46
+ @criteria,
47
+ config:
48
+ )
49
+
50
+
51
+ result.values_at('winner', 'loser')
52
+ end
53
+
54
+ def config
55
+ { **@config }
56
+ end
57
+
58
+ def log
59
+ { **(@config.dig(:log) || {}), duration: Time.now - @start_time }
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,120 @@
1
+ require 'securerandom'
2
+
3
+ require_relative './players_collection'
4
+ require_relative './free_for_all'
5
+ require_relative './elo_ranking'
6
+ require_relative '../scoring/recommended_reviews'
7
+
8
+ # This class orchestrates player ranking through multiple evaluation stages
9
+ # using Elo ranking and free-for-all match simulations.
10
+ # 1. Sets initial scores
11
+ # 2. Eliminates low performers
12
+ # 3. Runs Elo ranking (for large groups)
13
+ # 4. Conducts free-for-all matches
14
+ #
15
+ # @example Basic usage
16
+ # League.call(players, criteria)
17
+ #
18
+ # @param param_players [Array] Collection of player objects to evaluate
19
+ # Example: ["Circle", "Triangle", "Square"]
20
+ # or
21
+ # [
22
+ # { content: "Circle", score: 10 },
23
+ # { content: "Triangle", score: 7 },
24
+ # { content: "Square", score: 5 }
25
+ # ]
26
+ # @param criteria [String] Evaluation criteria configuration
27
+ # Example: "What is more similar to the letter 'O'?"
28
+ # @param config [Hash] Additional configuration config
29
+ # Example: { model: "gpt-4o", api_key: ENV['OPENAI_API_KEY'] }
30
+ # @return [Hash] Final ranked player results
31
+ module ActiveGenie::League
32
+ class League
33
+ def self.call(param_players, criteria, config: {})
34
+ new(param_players, criteria, config:).call
35
+ end
36
+
37
+ def initialize(param_players, criteria, config: {})
38
+ @param_players = param_players
39
+ @criteria = criteria
40
+ @config = config
41
+ @league_id = SecureRandom.uuid
42
+ @start_time = Time.now
43
+ end
44
+
45
+ def call
46
+ set_initial_score_players
47
+ eliminate_obvious_bad_players
48
+ run_elo_ranking if players.eligible_size > 10
49
+ run_free_for_all
50
+
51
+ ActiveGenie::Logger.info({ **log, step: :league_end, top5: players.first(5).map(&:id) })
52
+ players.to_h
53
+ end
54
+
55
+ private
56
+
57
+ SCORE_VARIATION_THRESHOLD = 10
58
+
59
+ def set_initial_score_players
60
+ players_without_score = players.reject { |player| player.score }
61
+ players_without_score.each do |player|
62
+ player.score = generate_score(player.content) # This can take a while, can be parallelized
63
+ ActiveGenie::Logger.trace({ **log, step: :player_score, player_id: player.id, score: player.score })
64
+ end
65
+
66
+ ActiveGenie::Logger.info({ **log, step: :initial_score, evaluated_players: players_without_score.size })
67
+ end
68
+
69
+ def generate_score(content)
70
+ ActiveGenie::Scoring::Basic.call(content, @criteria, reviewers, config:)['final_score']
71
+ end
72
+
73
+ def eliminate_obvious_bad_players
74
+ eliminated_count = 0
75
+ while players.coefficient_of_variation >= SCORE_VARIATION_THRESHOLD
76
+ players.eligible.last.eliminated = 'variation_too_high'
77
+ eliminated_count += 1
78
+ end
79
+
80
+ ActiveGenie::Logger.info({ **log, step: :eliminate_obvious_bad_players, eliminated_count: })
81
+ end
82
+
83
+ def run_elo_ranking
84
+ EloRanking.call(players, @criteria, config:)
85
+ end
86
+
87
+ def run_free_for_all
88
+ FreeForAll.call(players, @criteria, config:)
89
+ end
90
+
91
+ def reviewers
92
+ [recommended_reviews['reviewer1'], recommended_reviews['reviewer2'], recommended_reviews['reviewer3']]
93
+ end
94
+
95
+ def recommended_reviews
96
+ @recommended_reviews ||= ActiveGenie::Scoring::RecommendedReviews.call(
97
+ [players.sample.content, players.sample.content].join("\n\n"),
98
+ @criteria,
99
+ config:
100
+ )
101
+ end
102
+
103
+ def players
104
+ @players ||= PlayersCollection.new(@param_players)
105
+ end
106
+
107
+ def config
108
+ { log:, **@config }
109
+ end
110
+
111
+ def log
112
+ {
113
+ **(@config.dig(:log) || {}),
114
+ league_id: @league_id,
115
+ league_start_time: @start_time,
116
+ duration: Time.now - @start_time
117
+ }
118
+ end
119
+ end
120
+ end
@@ -0,0 +1,59 @@
1
+ require 'securerandom'
2
+
3
+ module ActiveGenie::Leaderboard
4
+ class Player
5
+ def initialize(params)
6
+ params = { content: params } if params.is_a?(String)
7
+
8
+ @id = params.dig(:id) || SecureRandom.uuid
9
+ @content = params.dig(:content) || params
10
+ @score = params.dig(:score) || nil
11
+ @elo = params.dig(:elo) || nil
12
+ @free_for_all = {
13
+ win: params.dig(:free_for_all, :win) || 0,
14
+ lose: params.dig(:free_for_all, :lose) || 0,
15
+ draw: params.dig(:free_for_all, :draw) || 0
16
+ }
17
+ @eliminated = params.dig(:eliminated) || nil
18
+ end
19
+
20
+ attr_reader :id, :content, :score, :elo, :free_for_all, :eliminated
21
+
22
+ def generate_elo_by_score
23
+ return if !@elo.nil?
24
+
25
+ if @score.nil?
26
+ @elo = BASE_ELO
27
+ else
28
+ @elo = BASE_ELO + (@score - 50)
29
+ end
30
+ end
31
+
32
+ def score=(value)
33
+ @score = value
34
+ end
35
+
36
+ def elo=(value)
37
+ @elo = value
38
+ end
39
+
40
+ def eliminated=(value)
41
+ @eliminated = value
42
+ end
43
+
44
+ def free_for_all_score
45
+ @free_for_all[:win] * 3 + @free_for_all[:draw]
46
+ end
47
+
48
+ def to_h
49
+ {
50
+ id:, content:, score:, elo:,
51
+ eliminated:, free_for_all:, free_for_all_score:
52
+ }
53
+ end
54
+
55
+ private
56
+
57
+ BASE_ELO = 1000
58
+ end
59
+ end