ranking: order by OpenSkill ordinal
Assisted-by: Claude Sonnet 5.5 via Claude Code
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