1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# frozen_string_literal: true

require_relative "../open_skill"
require_relative "db"
require_relative "sources"

Sequel::Model.plugin :timestamps, update_on_create: true

module RankKing
  # Signed in sig/rank_king/models.rbs: the superclass is not a constant.
  # @rbs skip
  class User < Sequel::Model(DB[:users])
    one_to_many :lists, class: "RankKing::List"
  end

  # @rbs skip
  class List < Sequel::Model(DB[:lists])
    many_to_one :user, class: "RankKing::User"
    # Archived items are hidden everywhere for now.
    one_to_many :items, class: "RankKing::Item", conditions: { archived_at: nil }
    one_to_many :axes, class: "RankKing::Axis"

    # Archived items too, which the items association leaves out.
    def before_destroy
      Result.where(axis_id: axes_dataset.select(:id)).delete
      axes_dataset.delete
      Item.where(list_id: id).delete
      super
    end
  end

  # @rbs skip
  class Item < Sequel::Model(DB[:items])
    many_to_one :list, class: "RankKing::List"
  end

  # @rbs skip
  class Axis < Sequel::Model(DB[:axes])
    many_to_one :list, class: "RankKing::List"
    one_to_many :results, class: "RankKing::Result"

    # The list's items in order on this axis, then the ones with no
    # results on it yet. Ranked by OpenSkill ordinal, best first. Pass
    # the axis's `ratings` when the caller already has them.
    def ranking(ratings = self.ratings)
      ranked, unranked = list.items.partition { |item| ratings.key?(item.id) }
      model = CONFIG.open_skill
      [ranked.sort_by { |item| [-model.ordinal(ratings.fetch(item.id)), item.id] }, unranked]
    end

    # Each compared item's rating, from replaying the axis's results in
    # the order they were recorded. A tie is a draw: both placed first.
    def ratings
      model = CONFIG.open_skill
      ratings = {} #: Hash[Integer, OpenSkill::Rating]
      results_dataset.order(:id).each do |result|
        winner = ratings.fetch(result.winner_id) { model.rating }
        loser = ratings.fetch(result.loser_id) { model.rating }
        game = result.tie ? OpenSkill::Game.new([[winner], [loser]]) : OpenSkill::Game.win_lose([winner], [loser])
        (new_winner,), (new_loser,) = model.rate(game)
        ratings[result.winner_id] = new_winner
        ratings[result.loser_id] = new_loser
      end
      ratings
    end

    def before_destroy
      results_dataset.delete
      super
    end
  end

  # @rbs skip
  class Result < Sequel::Model(DB[:results])
    many_to_one :axis, class: "RankKing::Axis"
    many_to_one :winner, class: "RankKing::Item"
    many_to_one :loser, class: "RankKing::Item"
  end
end