ranking: order by OpenSkill ordinal
Assisted-by: Claude Sonnet 5.5 via Claude Code
change ukrursrvnlnywomkwmyqxllwlnuvswmy
commit 153158ed941ab47c08b94934f65d74ef23609264
author Alpha Chen <alpha@kejadlen.dev>
date
parent 1a2d4ce3
diff --git a/lib/rank_king/models.rb b/lib/rank_king/models.rb
index 0e2dca8..1a1f498 100644
--- a/lib/rank_king/models.rb
+++ b/lib/rank_king/models.rb
@@ -1,5 +1,6 @@
 # frozen_string_literal: true
 
+require_relative "../open_skill"
 require_relative "db"
 require_relative "sources"
 
@@ -41,17 +42,28 @@ module RankKing
     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. Ordered by win count until OpenSkill is wired in.
+    # results on it yet. Ranked by OpenSkill ordinal, best first.
     def ranking
-      wins = Hash.new(0)
-      compared = Set.new
-      results.each do |result|
-        wins[result.winner_id] += 1
-        compared << result.winner_id << result.loser_id
-      end
+      ratings = self.ratings
+      ranked, unranked = list.items.partition { |item| ratings.key?(item.id) }
+      model = OpenSkill::Model.new
+      [ranked.sort_by { |item| [-model.ordinal(ratings.fetch(item.id)), item.id] }, unranked]
+    end
 
-      ranked, unranked = list.items.partition { |item| compared.include?(item.id) }
-      [ranked.sort_by { |item| [-wins[item.id], item.id] }, unranked]
+    # Each compared item's rating, from replaying the axis's results in
+    # the order they were recorded.
+    #: () -> Hash[Integer, OpenSkill::Rating]
+    def ratings
+      model = OpenSkill::Model.new
+      ratings = {}
+      results_dataset.order(:id).each do |result|
+        winner = ratings.fetch(result.winner_id) { model.rating }
+        loser = ratings.fetch(result.loser_id) { model.rating }
+        (new_winner,), (new_loser,) = model.rate(OpenSkill::Game.win_lose([winner], [loser]))
+        ratings[result.winner_id] = new_winner
+        ratings[result.loser_id] = new_loser
+      end
+      ratings
     end
 
     def before_destroy
diff --git a/sig/rank_king/models.rbs b/sig/rank_king/models.rbs
index 76e948a..62373e2 100644
--- a/sig/rank_king/models.rbs
+++ b/sig/rank_king/models.rbs
@@ -35,6 +35,7 @@ module RankKing
     def results: () -> Array[Result]
     def results_dataset: () -> Sequel::Dataset[Result]
     def ranking: () -> [Array[Item], Array[Item]]
+    def ratings: () -> Hash[Integer, OpenSkill::Rating]
   end
 
   class Result < Sequel::Model
diff --git a/test/test_web.rb b/test/test_web.rb
index cbbf488..4b3ed9d 100644
--- a/test/test_web.rb
+++ b/test/test_web.rb
@@ -240,6 +240,18 @@ class TestWeb < Minitest::Test
     assert_includes body, "Compare on Taste"
   end
 
+  def test_ranking_orders_by_rating_not_win_count
+    list, axis = create_list_with_axis(items: "A\nB\nC")
+    a, b, c = list.items.sort_by(&:id)
+    record_result(list, axis, winner: a, loser: b)
+    record_result(list, axis, winner: c, loser: a)
+
+    ranked, unranked = axis.ranking
+    assert_equal [c, a, b].map(&:id), ranked.map(&:id)
+    assert_empty unranked
+    assert_operator axis.ratings.fetch(c.id).mu, :>, axis.ratings.fetch(a.id).mu
+  end
+
   def test_show_list_selects_the_requested_axis
     create_list(name: "Foods", items: "Pizza\nSushi", axes: %w[Taste Value])
     list = RankKing::List.first