diff --git a/Gemfile b/Gemfile index 9cf42a3..bd101aa 100644 --- a/Gemfile +++ b/Gemfile @@ -17,8 +17,9 @@ gem "rom", "~> 5.3" gem "rom-sql", "~> 3.6" gem "sqlite3" +gem "good_job", "~> 4.13" + group :development do - gem "hanami-webconsole", "~> 2.1" gem "guard-puma" end diff --git a/app/actions/admin/matches/create.rb b/app/actions/admin/matches/create.rb new file mode 100644 index 0000000..3a385f7 --- /dev/null +++ b/app/actions/admin/matches/create.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module EuchreCamp + module Actions + module Admin + module Matches + class Create < EuchreCamp::Action + include Deps[repo: 'repositories.matches'] + + def handle(request, response) + match_params = request.params.to_h + match_params[:played_at] = Time.parse(match_params[:played_at]) if match_params[:played_at] + + # Create the match + match = repo.create(match_params) + + # Enqueue background job to calculate Elo + EuchreCamp::Jobs::CalculateEloJob.perform_later(match[:id]) + + response.redirect_to("/admin/matches") + rescue StandardError => e + # Handle validation errors or other issues + puts "Error: #{e.message}" + puts e.backtrace.join("\n") + response.redirect_to("/admin/matches/new") + end + end + end + end + end +end diff --git a/app/actions/admin/matches/index.rb b/app/actions/admin/matches/index.rb new file mode 100644 index 0000000..ab7412b --- /dev/null +++ b/app/actions/admin/matches/index.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module EuchreCamp + module Actions + module Admin + module Matches + class Index < EuchreCamp::Action + include Deps[repo: 'repositories.matches'] + + def handle(_request, response) + matches = repo.all + response.render(view, matches: matches) + end + end + end + end + end +end diff --git a/app/actions/admin/matches/new.rb b/app/actions/admin/matches/new.rb new file mode 100644 index 0000000..8a0120e --- /dev/null +++ b/app/actions/admin/matches/new.rb @@ -0,0 +1,18 @@ +# frozen_string_literal: true + +module EuchreCamp + module Actions + module Admin + module Matches + class New < EuchreCamp::Action + include Deps[repo: 'repositories.players'] + + def handle(_request, response) + players = repo.all + response.render(view, players: players) + end + end + end + end + end +end diff --git a/app/actions/admin/matches/process_upload.rb b/app/actions/admin/matches/process_upload.rb new file mode 100644 index 0000000..2d1b2e8 --- /dev/null +++ b/app/actions/admin/matches/process_upload.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +require "csv" + +module EuchreCamp + module Actions + module Admin + module Matches + class ProcessUpload < EuchreCamp::Action + include Deps[repo: 'repositories.matches'] + + def handle(request, response) + # In Hanami, file uploads are in request.params + file = request.params[:csv_file] + + unless file && file[:tempfile] + response.redirect_to("/admin/matches/upload") + return + end + + matches_created = 0 + errors = [] + + # Parse CSV + CSV.foreach(file[:tempfile].path, headers: true, header_converters: :symbol) do |row| + begin + # Map CSV columns to our database structure + # Expected columns: played_at, team_1_p1_name, team_1_p2_name, team_1_score, team_2_p1_name, team_2_p2_name, team_2_score + + # Find or create players by name + player_1 = find_or_create_player(row[:team_1_p1_name]) + player_2 = find_or_create_player(row[:team_1_p2_name]) + player_3 = find_or_create_player(row[:team_2_p1_name]) + player_4 = find_or_create_player(row[:team_2_p2_name]) + + played_at = begin + Time.parse(row[:played_at]) + rescue + Time.now + end + + match_data = { + played_at: played_at, + team_1_p1_id: player_1[:id], + team_1_p2_id: player_2[:id], + team_1_score: row[:team_1_score].to_i, + team_2_p1_id: player_3[:id], + team_2_p2_id: player_4[:id], + team_2_score: row[:team_2_score].to_i, + status: 'completed' + } + + match = repo.create(match_data) + EuchreCamp::Jobs::CalculateEloJob.perform_later(match[:id]) + matches_created += 1 + + rescue => e + errors << "Row #{row}: #{e.message}" + end + end + + response.flash[:message] = "Created #{matches_created} matches.#{' Errors: ' + errors.join(', ') if errors.any?}" + response.redirect_to("/admin/matches") + end + + private + + def find_or_create_player(name) + rom = EuchreCamp::App["persistence.rom"] + return nil unless name + + name = name.strip + player = rom.relations[:players].where(name: name).one + + if player + player + else + rom.relations[:players].insert(name: name, rating: 0, current_elo: 1000) + rom.relations[:players].where(name: name).one + end + end + end + end + end + end +end diff --git a/app/actions/admin/matches/upload.rb b/app/actions/admin/matches/upload.rb new file mode 100644 index 0000000..21d5334 --- /dev/null +++ b/app/actions/admin/matches/upload.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +require "csv" + +module EuchreCamp + module Actions + module Admin + module Matches + class Upload < EuchreCamp::Action + def handle(request, response) + response.render(view) + end + end + end + end + end +end diff --git a/app/actions/players/rankings.rb b/app/actions/players/rankings.rb new file mode 100644 index 0000000..0d09ba9 --- /dev/null +++ b/app/actions/players/rankings.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +module EuchreCamp + module Actions + module Players + class Rankings < EuchreCamp::Action + include Deps[repo: 'repositories.players'] + + def handle(_request, response) + # Get all players and sort by current Elo + players = repo.all.sort_by { |p| -p.current_elo } + response.render(view, players: players) + end + end + end + end +end diff --git a/app/jobs/calculate_elo_job.rb b/app/jobs/calculate_elo_job.rb new file mode 100644 index 0000000..da5e125 --- /dev/null +++ b/app/jobs/calculate_elo_job.rb @@ -0,0 +1,86 @@ +# frozen_string_literal: true + +module EuchreCamp + module Jobs + class CalculateEloJob + def self.perform_later(match_id) + # Enqueue job using GoodJob's database directly + rom = EuchreCamp::App["persistence.rom"] + + job_params = { + queue_name: "default", + priority: 0, + serialized_params: { "match_id" => match_id }.to_json, + scheduled_at: Time.now, + created_at: Time.now, + updated_at: Time.now, + job_class: "EuchreCamp::Jobs::CalculateEloJob", + executions_count: 0 + } + + rom.relations[:good_jobs].insert(job_params) + end + + def self.perform(match_id) + # This is the actual job execution + match = find_match(match_id) + return unless match + + # Get current player ratings + player_ratings = fetch_player_ratings(match) + + # Calculate new ratings + calculation_results = EuchreCamp::Services::EloCalculator.calculate( + player_ratings: player_ratings, + team_1_players: [match[:team_1_p1_id], match[:team_1_p2_id]], + team_2_players: [match[:team_2_p1_id], match[:team_2_p2_id]], + team_1_score: match[:team_1_score], + team_2_score: match[:team_2_score] + ) + + # Update player ratings and create snapshots + update_player_ratings(calculation_results, match_id) + end + + private + + def self.find_match(match_id) + rom = EuchreCamp::App["persistence.rom"] + rom.relations[:matches].by_pk(match_id).one + end + + def self.fetch_player_ratings(match) + rom = EuchreCamp::App["persistence.rom"] + player_ids = [ + match[:team_1_p1_id], + match[:team_1_p2_id], + match[:team_2_p1_id], + match[:team_2_p2_id] + ] + + players = rom.relations[:players].where(id: player_ids).to_a + players.each_with_object({}) do |player, hash| + hash[player[:id]] = player[:current_elo] || 1000 + end + end + + def self.update_player_ratings(results, match_id) + rom = EuchreCamp::App["persistence.rom"] + + results.each do |player_id, data| + # Update player's current Elo + rom.relations[:players].where(id: player_id).update(current_elo: data[:rating_after]) + + # Create Elo snapshot record + rom.relations[:elo_snapshots].insert( + player_id: player_id, + match_id: match_id, + rating_before: data[:rating_before], + rating_after: data[:rating_after], + rating_change: data[:rating_change] + ) + end + end + end + end +end diff --git a/app/repositories/matches.rb b/app/repositories/matches.rb new file mode 100644 index 0000000..5e016f1 --- /dev/null +++ b/app/repositories/matches.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +require 'rom-repository' + +module EuchreCamp + module Repositories + class Matches < ::EuchreCamp::Repository[:matches] + commands :create, update: :by_pk, delete: :by_pk + + def all + matches.to_a + end + + def by_id(id) + matches.by_pk(id).one! + end + end + end +end diff --git a/app/services/elo_calculator.rb b/app/services/elo_calculator.rb new file mode 100644 index 0000000..07079c2 --- /dev/null +++ b/app/services/elo_calculator.rb @@ -0,0 +1,80 @@ +# frozen_string_literal: true + +module EuchreCamp + module Services + class EloCalculator + # Standard Elo K-factor + K_FACTOR = 32 + + # Calculate Elo ratings for a 2v2 Euchre match + # + # @param player_ratings [Hash] player_id => current_rating + # @param team_1_players [Array] player IDs for team 1 + # @param team_2_players [Array] player IDs for team 2 + # @param team_1_score [Integer] score for team 1 + # @param team_2_score [Integer] score for team 2 + # @return [Hash] player_id => { rating_before:, rating_after:, rating_change: } + def self.calculate(player_ratings:, team_1_players:, team_2_players:, team_1_score:, team_2_score:) + # Validate inputs + raise ArgumentError, "Team 1 must have exactly 2 players" unless team_1_players.length == 2 + raise ArgumentError, "Team 2 must have exactly 2 players" unless team_2_players.length == 2 + + # Determine winner (first to 10 points) + team_1_wins = team_1_score >= 10 + team_2_wins = team_2_score >= 10 + + unless team_1_wins || team_2_wins + raise ArgumentError, "One team must reach 10 points to win" + end + + if team_1_wins && team_2_wins + raise ArgumentError, "Only one team can win (both reached 10 points)" + end + + winner = team_1_wins ? :team_1 : :team_2 + + # Calculate average ratings for each team + team_1_avg = team_1_players.map { |id| player_ratings[id] }.sum / 2.0 + team_2_avg = team_2_players.map { |id| player_ratings[id] }.sum / 2.0 + + # Calculate expected scores using Elo formula + # E = 1 / (1 + 10^((R_opp - R_team) / 400)) + expected_team_1 = 1.0 / (1.0 + 10.0**((team_2_avg - team_1_avg) / 400.0)) + expected_team_2 = 1.0 / (1.0 + 10.0**((team_1_avg - team_2_avg) / 400.0)) + + # Actual scores (1 for win, 0 for loss) + actual_team_1 = team_1_wins ? 1.0 : 0.0 + actual_team_2 = team_2_wins ? 1.0 : 0.0 + + # Calculate rating changes for each player + results = {} + + # Team 1 players + team_1_players.each do |player_id| + old_rating = player_ratings[player_id] + rating_change = K_FACTOR * (actual_team_1 - expected_team_1) + new_rating = (old_rating + rating_change).round + results[player_id] = { + rating_before: old_rating, + rating_after: new_rating, + rating_change: rating_change.round + } + end + + # Team 2 players + team_2_players.each do |player_id| + old_rating = player_ratings[player_id] + rating_change = K_FACTOR * (actual_team_2 - expected_team_2) + new_rating = (old_rating + rating_change).round + results[player_id] = { + rating_before: old_rating, + rating_after: new_rating, + rating_change: rating_change.round + } + end + + results + end + end + end +end diff --git a/app/templates/admin/matches/index.html.erb b/app/templates/admin/matches/index.html.erb new file mode 100644 index 0000000..887460a --- /dev/null +++ b/app/templates/admin/matches/index.html.erb @@ -0,0 +1,36 @@ +

Match Administration

+ +

Enter New Match | Upload CSV

+ +<% if matches.any? %> + + + + + + + + + + + + + + <% matches.each do |match| %> + + + + + + + + + + <% end %> + +
IDDateTeam 1ScoreTeam 2ScoreStatus
<%= match.id %><%= match.played_at&.strftime('%Y-%m-%d %H:%M') %><%= match.team_1_p1_id %>/<%= match.team_1_p2_id %><%= match.team_1_score %><%= match.team_2_p1_id %>/<%= match.team_2_p2_id %><%= match.team_2_score %><%= match.status %>
+<% else %> +

No matches entered yet.

+<% end %> + +

Manage Players

diff --git a/app/templates/admin/matches/new.html.erb b/app/templates/admin/matches/new.html.erb new file mode 100644 index 0000000..45bd54d --- /dev/null +++ b/app/templates/admin/matches/new.html.erb @@ -0,0 +1,66 @@ +

Enter New Match

+ +
+ + +
+ + +
+ +
+ Team 1 +
+ + +
+
+ + +
+
+ + +
+
+ +
+ Team 2 +
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +

Back to Matches

diff --git a/app/templates/admin/matches/upload.html.erb b/app/templates/admin/matches/upload.html.erb new file mode 100644 index 0000000..6378429 --- /dev/null +++ b/app/templates/admin/matches/upload.html.erb @@ -0,0 +1,26 @@ +

Upload Matches (CSV)

+ +

Upload a CSV file with the following columns:

+
+played_at, team_1_p1_name, team_1_p2_name, team_1_score, team_2_p1_name, team_2_p2_name, team_2_score
+
+ +
+ + +
+ + +
+ + +
+ +

Back to Matches

+ +

Example CSV:

+
+played_at,team_1_p1_name,team_1_p2_name,team_1_score,team_2_p1_name,team_2_p2_name,team_2_score
+2026-03-26 10:00,Alice,Bob,10,Charlie,David,7
+2026-03-26 10:30,Alice,Bob,10,Charlie,David,7
+
diff --git a/app/templates/players/rankings.html.erb b/app/templates/players/rankings.html.erb new file mode 100644 index 0000000..0ed675f --- /dev/null +++ b/app/templates/players/rankings.html.erb @@ -0,0 +1,28 @@ +

Player Rankings

+ +<% if players.any? %> + + + + + + + + + + + <% players.each_with_index do |player, index| %> + + + + + + + <% end %> + +
RankPlayerElo RatingOriginal Rating
<%= index + 1 %><%= player.name %><%= player.current_elo %><%= player.rating %>
+<% else %> +

No players found.

+<% end %> + +

Admin

diff --git a/app/views/admin/matches/index.rb b/app/views/admin/matches/index.rb new file mode 100644 index 0000000..a9d843b --- /dev/null +++ b/app/views/admin/matches/index.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module EuchreCamp + module Views + module Admin + module Matches + class Index < EuchreCamp::View + expose :matches + end + end + end + end +end diff --git a/app/views/admin/matches/new.rb b/app/views/admin/matches/new.rb new file mode 100644 index 0000000..93f8ddf --- /dev/null +++ b/app/views/admin/matches/new.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module EuchreCamp + module Views + module Admin + module Matches + class New < EuchreCamp::View + expose :players + end + end + end + end +end diff --git a/app/views/admin/matches/upload.rb b/app/views/admin/matches/upload.rb new file mode 100644 index 0000000..1f15fb7 --- /dev/null +++ b/app/views/admin/matches/upload.rb @@ -0,0 +1,12 @@ +# frozen_string_literal: true + +module EuchreCamp + module Views + module Admin + module Matches + class Upload < EuchreCamp::View + end + end + end + end +end diff --git a/app/views/players/rankings.rb b/app/views/players/rankings.rb new file mode 100644 index 0000000..b6a4e41 --- /dev/null +++ b/app/views/players/rankings.rb @@ -0,0 +1,11 @@ +# frozen_string_literal: true + +module EuchreCamp + module Views + module Players + class Rankings < EuchreCamp::View + expose :players + end + end + end +end diff --git a/bin/worker.rb b/bin/worker.rb new file mode 100644 index 0000000..f2539bf --- /dev/null +++ b/bin/worker.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +require_relative "../config/app" + +# Boot the Hanami app to ensure persistence is available +EuchreCamp::App.boot + +# Simple worker that polls for jobs and executes them +def poll_and_execute_jobs + rom = EuchreCamp::App["persistence.rom"] + + puts "Worker started, waiting for jobs..." + + loop do + # Find a pending job + job = rom.relations[:good_jobs].where(finished_at: nil).order(:scheduled_at).limit(1).one + + if job + begin + # Mark as performed + rom.relations[:good_jobs].where(id: job[:id]).update( + performed_at: Time.now, + executions_count: (job[:executions_count] || 0) + 1 + ) + + # Execute the job + params = JSON.parse(job[:serialized_params]) + job_class = Object.const_get(job[:job_class]) + + if params["match_id"] + job_class.perform(params["match_id"]) + else + # Legacy format for backwards compatibility + job_class.perform(*params["arguments"]) + end + + # Mark as finished + rom.relations[:good_jobs].where(id: job[:id]).update(finished_at: Time.now) + + puts "Executed job #{job[:id]}: #{job[:job_class]}" + rescue => e + # Mark as failed + rom.relations[:good_jobs].where(id: job[:id]).update( + finished_at: Time.now, + error: e.message + ) + puts "Failed job #{job[:id]}: #{e.message}" + puts e.backtrace.first(5).join("\n") + end + else + # No jobs, wait a bit + sleep 1 + end + end +end + +poll_and_execute_jobs diff --git a/config/app.rb b/config/app.rb index a326611..6e0a186 100644 --- a/config/app.rb +++ b/config/app.rb @@ -1,8 +1,13 @@ # frozen_string_literal: true require "hanami" +require "good_job" module EuchreCamp class App < Hanami::App + config.root = File.expand_path("..", __dir__) + + # Enable sessions + config.actions.sessions = :cookie, {secret: ENV.fetch("SESSION_SECRET", "change_me_in_production")} end end diff --git a/config/initializers/active_job.rb b/config/initializers/active_job.rb new file mode 100644 index 0000000..17b72ca --- /dev/null +++ b/config/initializers/active_job.rb @@ -0,0 +1,6 @@ +# frozen_string_literal: true + +require "active_job" + +# Configure Active Job to use GoodJob +ActiveJob::Base.queue_adapter = :good_job diff --git a/config/initializers/good_job.rb b/config/initializers/good_job.rb new file mode 100644 index 0000000..081ece7 --- /dev/null +++ b/config/initializers/good_job.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true + +require "good_job" + +# GoodJob configuration for Hanami +# Since GoodJob is primarily designed for Rails, we configure it directly + +GoodJob.preserve_job_records = true +GoodJob.retry_on_unhandled_error = false +GoodJob.on_thread_error = ->(exception) { puts "GoodJob error: #{exception.message}" } + +# Set execution mode based on environment +if ENV["GOOD_JOB_EXECUTION_MODE"] + GoodJob.execution_mode = ENV["GOOD_JOB_EXECUTION_MODE"].to_sym +elsif ENV["RACK_ENV"] == "test" + GoodJob.execution_mode = :inline +elsif ENV["RACK_ENV"] == "development" + GoodJob.execution_mode = :async +else + GoodJob.execution_mode = :external +end diff --git a/config/routes.rb b/config/routes.rb index 8805065..dc2d739 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -4,6 +4,7 @@ module EuchreCamp class Routes < Hanami::Routes # Add your routes here. See https://guides.hanamirb.org/routing/overview/ for details. get "/players/:id", to: "players.show" + get "/rankings", to: "players.rankings" get "/admin/players/new", to: "admin.players.new" post "/admin/players", to: "admin.players.create" @@ -11,5 +12,14 @@ module EuchreCamp get "/admin/players/:id/edit", to: "admin.players.edit" patch "/admin/players/:id", to: "admin.players.update" delete "/admin/players/:id", to: "admin.players.destroy" + + # Match routes + get "/admin/matches/new", to: "admin.matches.new" + post "/admin/matches", to: "admin.matches.create" + get "/admin/matches", to: "admin.matches.index" + + # CSV Upload routes + get "/admin/matches/upload", to: "admin.matches.upload" + post "/admin/matches/process_upload", to: "admin.matches.process_upload" end end diff --git a/cookies.txt b/cookies.txt new file mode 100644 index 0000000..357703e --- /dev/null +++ b/cookies.txt @@ -0,0 +1,5 @@ +# Netscape HTTP Cookie File +# https://curl.se/docs/http-cookies.html +# This file was generated by libcurl! Edit at your own risk. + +#HttpOnly_localhost FALSE / FALSE 0 rack.session ASyilKVTxn1iBCRZVUIw0JytfvCxi0S7cf1Cyrqsal0zKn_qN-XMmRdfPBJSFliFhkK0_LVlDU99FIVtgnT4FnLijl8lYpcQzF1dVtPOGvEQpYLM5-44U9nsm1angx6MrLhNuwwizUIUHTtJFpvM7iA5hU1S5PwHF5hXvTYy1zQAVMJOsiF1rUORTb-pbsImZButiYE1qw29AF6RxVn8LnaM3mGBfA7b8jBaSe6gnaEjeJ5gbQMrI8o0efXcmJW2ViovTvto-gIvElOL0H3cDV-AcvmBp_QUpL3buZUD-A65c2lT91s92R19pkS1AM_hHTN9v1gGlcY2eyCVoqC4RYI-RG_HYdp7fkWEnf0rb8uXDNh9GJdqg0TPMUu0P_klRs7Jdx0bFJcAjWMzpQhYRmisGJ8Mf7RN6XmtYDLyqopcMBS3U5mpDpk6p1Rf_yr6vQ%3D%3D diff --git a/db/migrate/20240915000000_create_good_job_tables.rb b/db/migrate/20240915000000_create_good_job_tables.rb new file mode 100644 index 0000000..733b41b --- /dev/null +++ b/db/migrate/20240915000000_create_good_job_tables.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +ROM::SQL.migration do + change do + create_table :good_jobs do + primary_key :id + column :queue_name, String + column :priority, Integer, default: 0 + column :serialized_params, String + column :scheduled_at, DateTime + column :performed_at, DateTime + column :finished_at, DateTime + column :error, String + column :created_at, DateTime, null: false + column :updated_at, DateTime, null: false + column :active_job_id, String + column :concurrency_key, String + column :sequence, Integer + column :executions_count, Integer + column :job_class, String + column :error_event, Integer + column :labels, String + column :locked_by_id, String + column :locked_at, DateTime + end + + add_index :good_jobs, [:scheduled_at, :priority, :id], name: "index_good_jobs_on_scheduled_at" + + create_table :good_job_processes do + primary_key :id + column :state, String + column :started_at, DateTime + column :singleton_key, String + column :created_at, DateTime, null: false + column :updated_at, DateTime, null: false + end + + create_table :good_job_execution_errors do + primary_key :id + column :active_job_id, String + column :error, String + column :created_at, DateTime, null: false + end + + create_table :good_job_settings do + primary_key :id + column :key, String + column :value, String + column :created_at, DateTime, null: false + column :updated_at, DateTime, null: false + end + end +end diff --git a/db/migrate/20240915000001_create_matches_and_elo_snapshots.rb b/db/migrate/20240915000001_create_matches_and_elo_snapshots.rb new file mode 100644 index 0000000..e25494e --- /dev/null +++ b/db/migrate/20240915000001_create_matches_and_elo_snapshots.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +ROM::SQL.migration do + change do + # Drop and recreate matches table with new structure + drop_table?(:matches) + + create_table :matches do + primary_key :id + foreign_key :event_id, :events + column :played_at, DateTime + column :team_1_p1_id, Integer + column :team_1_p2_id, Integer + column :team_2_p1_id, Integer + column :team_2_p2_id, Integer + column :team_1_score, Integer + column :team_2_score, Integer + column :status, String, default: 'completed' + end + + # Add current_elo to players table + alter_table :players do + add_column :current_elo, Integer, default: 1000 + end + + # Create elo_snapshots table + create_table :elo_snapshots do + primary_key :id + foreign_key :player_id, :players, null: false + foreign_key :match_id, :matches, null: false + column :rating_before, Integer, null: false + column :rating_after, Integer, null: false + column :rating_change, Integer, null: false + column :created_at, DateTime, null: false, default: Sequel::CURRENT_TIMESTAMP + end + + # Add indexes for performance + add_index :elo_snapshots, [:player_id, :match_id], unique: true + add_index :matches, :played_at + add_index :matches, :status + end +end diff --git a/euchre_camp.db b/euchre_camp.db index 4ed84d0..a2ba187 100644 Binary files a/euchre_camp.db and b/euchre_camp.db differ diff --git a/lib/euchre_camp/persistence/relations/elo_snapshots.rb b/lib/euchre_camp/persistence/relations/elo_snapshots.rb new file mode 100644 index 0000000..c1052b0 --- /dev/null +++ b/lib/euchre_camp/persistence/relations/elo_snapshots.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +module EuchreCamp + module Persistence + module Relations + class EloSnapshots < ROM::Relation[:sql] + schema(:elo_snapshots, infer: true) do + attribute :id, ROM::Types::Integer, primary_key: true + attribute :player_id, ROM::Types::Integer + attribute :match_id, ROM::Types::Integer + attribute :rating_before, ROM::Types::Integer + attribute :rating_after, ROM::Types::Integer + attribute :rating_change, ROM::Types::Integer + attribute :created_at, ROM::Types::DateTime + end + end + end + end +end diff --git a/lib/euchre_camp/persistence/relations/good_jobs.rb b/lib/euchre_camp/persistence/relations/good_jobs.rb new file mode 100644 index 0000000..765b3b9 --- /dev/null +++ b/lib/euchre_camp/persistence/relations/good_jobs.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +module EuchreCamp + module Persistence + module Relations + class GoodJobs < ROM::Relation[:sql] + schema(:good_jobs, infer: true) do + attribute :id, ROM::Types::Integer + attribute :queue_name, ROM::Types::String + attribute :priority, ROM::Types::Integer + attribute :serialized_params, ROM::Types::String + attribute :scheduled_at, ROM::Types::DateTime.optional + attribute :performed_at, ROM::Types::DateTime.optional + attribute :finished_at, ROM::Types::DateTime.optional + attribute :error, ROM::Types::String.optional + attribute :created_at, ROM::Types::DateTime + attribute :updated_at, ROM::Types::DateTime + attribute :active_job_id, ROM::Types::String.optional + attribute :concurrency_key, ROM::Types::String.optional + attribute :sequence, ROM::Types::Integer.optional + attribute :executions_count, ROM::Types::Integer.optional + attribute :job_class, ROM::Types::String.optional + attribute :error_event, ROM::Types::Integer.optional + attribute :labels, ROM::Types::String.optional + attribute :locked_by_id, ROM::Types::String.optional + attribute :locked_at, ROM::Types::DateTime.optional + end + end + end + end +end diff --git a/lib/euchre_camp/persistence/relations/matches.rb b/lib/euchre_camp/persistence/relations/matches.rb new file mode 100644 index 0000000..5f9cd29 --- /dev/null +++ b/lib/euchre_camp/persistence/relations/matches.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module EuchreCamp + module Persistence + module Relations + class Matches < ROM::Relation[:sql] + schema(:matches, infer: true) do + attribute :id, ROM::Types::Integer, primary_key: true + attribute :event_id, ROM::Types::Integer.optional + attribute :played_at, ROM::Types::DateTime.optional + attribute :team_1_p1_id, ROM::Types::Integer + attribute :team_1_p2_id, ROM::Types::Integer + attribute :team_2_p1_id, ROM::Types::Integer + attribute :team_2_p2_id, ROM::Types::Integer + attribute :team_1_score, ROM::Types::Integer + attribute :team_2_score, ROM::Types::Integer + attribute :status, ROM::Types::String.default('completed') + + associations do + belongs_to :team_1_player, relation: :players, foreign_key: :team_1_p1_id + belongs_to :team_2_player, relation: :players, foreign_key: :team_1_p2_id + belongs_to :team_3_player, relation: :players, foreign_key: :team_2_p1_id + belongs_to :team_4_player, relation: :players, foreign_key: :team_2_p2_id + end + end + end + end + end +end diff --git a/lib/euchre_camp/persistence/relations/players.rb b/lib/euchre_camp/persistence/relations/players.rb index f1fd95c..cebece8 100644 --- a/lib/euchre_camp/persistence/relations/players.rb +++ b/lib/euchre_camp/persistence/relations/players.rb @@ -1,8 +1,15 @@ +# frozen_string_literal: true + module EuchreCamp module Persistence module Relations class Players < ROM::Relation[:sql] - schema(:players, infer: true) + schema(:players, infer: true) do + attribute :id, ROM::Types::Integer, primary_key: true + attribute :name, ROM::Types::String + attribute :rating, ROM::Types::Integer + attribute :current_elo, ROM::Types::Integer.default(1000) + end end end end