active_genie 0.0.3 → 0.0.10

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 (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