diff --git a/lib/app.rb b/lib/app.rb deleted file mode 100644 index d265456..0000000 --- a/lib/app.rb +++ /dev/null @@ -1,196 +0,0 @@ -# frozen_string_literal: true -require_relative "models" -require_relative "views" - -require "roda" - -module RankKing - module Models - require "sequel/model" - require_relative "db" - - class Pool < Sequel::Model - end - end - - class App < Roda - opts[:check_dynamic_arity] = false - opts[:check_arity] = :warn - - plugin :default_headers, - "Content-Type" => "text/html", - # "Strict-Transport-Security" => "max-age=16070400;", # Uncomment if only allowing https:// access - "X-Frame-Options" => "deny", - "X-Content-Type-Options" => "nosniff", - "X-XSS-Protection" => "1; mode=block" - - plugin :content_security_policy do |csp| - csp.default_src :none - # csp.style_src :self, "https://cdn.jsdelivr.net" - csp.form_action :self - csp.script_src :self - csp.connect_src :self - csp.base_uri :none - csp.frame_ancestors :none - end - - css_opts = {cache: false, style: :compressed} - # :nocov: - if ENV["RACK_ENV"] == 'development' - css_opts.merge!(source_map_embed: true, source_map_contents: true, source_map_file: ".") - end - plugin :render_coverage if defined?(SimpleCov) - # :nocov: - - plugin :route_csrf - plugin :flash - # plugin :assets, css: "app.scss", css_opts: css_opts, timestamp_paths: true - # plugin :render, escape: true, layout: "./layout", :template_opts=>{chain_appends: !defined?(SimpleCov), freeze: true, skip_compiled_encoding_detection: true} - plugin :public - plugin :Integer_matcher_max - plugin :typecast_params_sized_integers, sizes: [64], default_size: 64 - # plugin :hash_branch_view_subdir - plugin :custom_block_results - - logger = if ENV["RACK_ENV"] == "test" - Class.new{def write(_) end}.new - else - $stderr - end - plugin :common_logger, logger - - plugin :not_found do - Views::Layout.call(title: "File Not Found") - end - - if ENV["RACK_ENV"] == "development" - plugin :exception_page - class RodaRequest - def assets - exception_page_assets - super - end - end - else - def self.freeze - Sequel::Model.freeze_descendents - DB.freeze - super - end - end - - plugin :error_handler do |e| - case e - when Roda::RodaPlugins::RouteCsrf::InvalidToken - @page_title = "Invalid Security Token" - response.status = 400 - view(content: "
An invalid security token was submitted with this request, and this request could not be processed.
") - else - $stderr.print "#{e.class}: #{e.message}\n" - $stderr.puts e.backtrace - next exception_page(e, assets: true) if ENV["RACK_ENV"] == "development" - # @page_title = "Internal Server Error" - # view(content: "") - end - end - - plugin :sessions, - key: "_App.session", - #cookie_options: {secure: ENV['RACK_ENV'] != 'test'}, # Uncomment if only allowing https:// access - secret: ENV.send((ENV["RACK_ENV"] == "development" ? :[] : :delete), "APP_SESSION_SECRET") - - # if Unreloader.autoload? - # plugin :autoload_hash_branches - # autoload_hash_branch_dir("./routes") - # end - # Unreloader.autoload("routes", delete_hook: proc{|f| hash_branch(File.basename(f).delete_suffix(".rb"))}){} - - class Template < Phlex::HTML - def initialize(block) - @block = block - end - - def template - instance_eval(&@block) - end - end - - # def render(&block) - # template = Template.new(block) - # instance_variables - # .reject { _1.start_with?("@_") } - # .each { template.instance_variable_set(_1, instance_variable_get(_1)) } - # Views::Layout.call do - # plain "" - # end - # # Views::Layout.call(template) - # # Views::Layout.call do |layout| - # # layout.page_title "New Pool" - # # plain "" - # # end - # end - - def view(&block) - Class.new(Phlex::HTML, &block).call - end - - route do |r| - - # r.public - # r.assets - check_csrf! - - r.root do - if DB[:pools].empty? - r.redirect "/pools/new" - else - render do - h1 { "ohai" } - end - end - end - - r.on "pools" do - r.is "new" do - r.get do - @csrf_token = csrf_token(r.path) - view do - Views::Layout.new(title: "New Pool") do - Views::NewPool.new(csrf_token: @csrf_token) - end - end - # Views::Layout.call(title: "New Pool") do - # render Views::NewPool.new(csrf_token: csrf_token(r.path)) - # end - # layout = Views::Layout.new do |layout| - # layout.page_title "New Pool" - # render Views::NewPool.new(csrf_token: csrf_token(r.path)) - # end - # layout.call - # @csrf_token = csrf_token(r.path) - # render do - # form(method: "post") do - # input(type: "hidden", name: "_csrf", value: @csrf_token) - # label(for: "name") - # input(type: "text", name: "name", id: "name", required: true) - # input(type: "submit", value: "Create") - # end - # end - end - - r.post do - pool = Models::Pool.create(name: r.params.fetch("name")) - r.redirect "/pools/#{pool.id}" - end - end - - r.is Integer do |id| - pool = Models::Pool[id] - render do - h2 { pool.name } - end - end - end - end - end -end diff --git a/lib/rank_king.rb b/lib/rank_king.rb index 13c74d4..858bfa2 100644 --- a/lib/rank_king.rb +++ b/lib/rank_king.rb @@ -1,4 +1,3 @@ -require_relative "rank_king/web" require_relative "rank_king/models" require_relative "rank_king/open_skill" @@ -6,11 +5,12 @@ module RankKing OS = OpenSkill.new Match = Data.define(:a, :b) - def self.find_match(axis:) + def self.suggest_game(axis) items = axis.pool.items items.combination(2).sort_by {|combo| - ratings = combo.map {|item| os_rating(axis:, item:) } + # TODO get all the ratings at once + ratings = combo.map {|item| Rating.first(axis:, item:) || OS.rating } Math.sqrt( [ratings, ratings.reverse] .map {|game| [game, OS.rate(game.map {[_1]}).flatten] } @@ -19,10 +19,10 @@ module RankKing }.last end - def self.rank(axis:, winner:, loser:) + def self.rank(axis, winner:, loser:) fail unless [winner, loser].all? { _1.pool == axis.pool } - game = [winner, loser].map { os_rating(axis:, item: _1) } + game = [winner, loser].map { Rating.first(axis:, item: _1) || OS.rating } result = OS.rate(game.map { [_1] }).flatten DB.transaction do @@ -32,13 +32,4 @@ module RankKing end end - private - - def self.os_rating(**filter) - if result = Rating.where(**filter).first - OS.rating(mu: result.mu, sigma: result.sigma) - else - OS.rating - end - end end diff --git a/lib/rank_king/web.rb b/lib/web.rb similarity index 98% rename from lib/rank_king/web.rb rename to lib/web.rb index 723af51..499085b 100644 --- a/lib/rank_king/web.rb +++ b/lib/web.rb @@ -1,6 +1,6 @@ require "roda" -require_relative "db" +require_relative "rank_king" module RankKing class Web < Roda diff --git a/test/rank_king_test.rb b/test/rank_king_test.rb new file mode 100644 index 0000000..e8b3ab6 --- /dev/null +++ b/test/rank_king_test.rb @@ -0,0 +1,23 @@ +require "minitest" + +require "rank_king" + +class TestRankKing < Minitest::Test + def setup + @pool = Pool.create(name: "pool") + @axis = Axis.create(pool: @pool, name: "axis") + @items = (?a..?d).map { Item.create(pool: @pool, title: _1) } + end + + def test_rank_king + ratings = @pool.items.map {|item| Rating.first(axis: @axis, item:) || OS.rating } + original_sigma_sq = Math.sqrt(ratings.sum { _1.sigma ** 2 }) + + game = RankKing.suggest_game(@axis) + winner, loser = game.shuffle + RankKing.rank(@axis, winner:, loser:) + + sigma_sq = Math.sqrt(@axis.ratings.sum { _1.sigma ** 2 }) + assert original_sigma_sq > sigma_sq + end +end