From 765a3365bd16a375f85e2fb2ecafc2902e0944bc Mon Sep 17 00:00:00 2001 From: Slavasil Date: Sun, 25 Oct 2026 17:10:40 +0300 Subject: [PATCH] add player rating to the db --- main-server/API.md | 7 +- .../app/channels/matchmaking_channel.rb | 6 +- main-server/app/models/user.rb | 1 + .../app/services/matchmaking_service.rb | 3 +- .../config/initializers/matchmaking.rb | 3 +- .../20261005000000_add_rating_to_users.rb | 7 + main-server/db/queue_schema.rb | 284 +++++++++--------- main-server/db/schema.rb | 3 +- main-server/spec/models/user_spec.rb | 13 + 9 files changed, 180 insertions(+), 147 deletions(-) create mode 100644 main-server/db/migrate/20261005000000_add_rating_to_users.rb diff --git a/main-server/API.md b/main-server/API.md index 756eb12..9baf09e 100644 --- a/main-server/API.md +++ b/main-server/API.md @@ -223,10 +223,10 @@ curl -X DELETE http://localhost:3000/api/logout \ 2. Отправить команду начала поиска: ```json -{ "action": "start", "rating": 1000 } +{ "action": "start" } ``` -Поле `rating` опционально (по умолчанию — `MATCHMAKING_DEFAULT_RATING`). Игроки подбираются по близости рейтинга; допуск расширяется с ростом времени ожидания. +Рейтинг для подбора берётся из базы данных (`users.rating`). У новых пользователей он инициализируется нейтральным значением `100` (значение задано в БД); в будущем рейтинг будет пересчитываться по результатам матчей. Игроки подбираются по близости рейтинга; допуск расширяется с ростом времени ожидания. 3. Дождаться одного из сообщений: @@ -324,7 +324,6 @@ curl -X DELETE http://localhost:3000/api/matchmaking \ |------------|--------------|----------| | `REDIS_URL` | `redis://localhost:6380/0` | Подключение к Redis (матчмейкинг и pub/sub Action Cable) | | `GAME_SERVER_URL` | `ws://localhost:8080` | Адрес игрового сервера для матчмейкинга | -| `MATCHMAKING_DEFAULT_RATING` | `1000` | Рейтинг по умолчанию для новых игроков | | `MATCHMAKING_QUEUE_TTL` | `60` | Время ожидания соперника до таймаута, сек | | `MATCHMAKING_PENDING_TTL` | `15` | Время ожидания финализации пары, сек | | `MATCHMAKING_MATCH_TTL` | `120` | Время жизни данных матча, сек | @@ -354,5 +353,5 @@ curl -s http://localhost:3000/api/me \ # 3. Поиск матча — WebSocket (например, через wscat): # wscat -c "ws://localhost:3000/cable?token=$TOKEN" # > {"command":"subscribe","identifier":"{\"channel\":\"MatchmakingChannel\"}"} -# > {"command":"message","identifier":"{\"channel\":\"MatchmakingChannel\"}","data":"{\"action\":\"start\",\"rating\":1000}"} +# > {"command":"message","identifier":"{\"channel\":\"MatchmakingChannel\"}","data":"{\"action\":\"start\"}"} ``` diff --git a/main-server/app/channels/matchmaking_channel.rb b/main-server/app/channels/matchmaking_channel.rb index d04ddb7..cc6d487 100644 --- a/main-server/app/channels/matchmaking_channel.rb +++ b/main-server/app/channels/matchmaking_channel.rb @@ -3,7 +3,7 @@ # Client flow: # 1. Connect: ws(s)://host/cable?token= # 2. Subscribe to "MatchmakingChannel" -# 3. Send { action: "start", rating: } to enter the queue +# 3. Send { action: "start" } to enter the queue # 4. Receive { type: "matched", ... } or { type: "timeout" } # 5. Disconnecting (or sending { action: "stop" }) cancels the search. class MatchmakingChannel < ApplicationCable::Channel @@ -17,8 +17,8 @@ class MatchmakingChannel < ApplicationCable::Channel end end - def start(data) - MatchmakingService.enqueue(current_user.id, rating: data["rating"]) + def start(_data) + MatchmakingService.enqueue(current_user.id, rating: current_user.rating) end def stop diff --git a/main-server/app/models/user.rb b/main-server/app/models/user.rb index 7516101..6af9418 100644 --- a/main-server/app/models/user.rb +++ b/main-server/app/models/user.rb @@ -7,6 +7,7 @@ class User < ApplicationRecord normalizes :nickname, with: ->(n) { n.strip } validates :nickname, presence: true, uniqueness: { case_sensitive: false }, length: { minimum: 3, maximum: 24 } + validates :rating, presence: true validates :password, presence: true, length: { minimum: 6 }, if: -> { new_record? || password_digest_changed? } validate :avatar_content_type, if: -> { avatar.attached? } diff --git a/main-server/app/services/matchmaking_service.rb b/main-server/app/services/matchmaking_service.rb index 521414a..00f912f 100644 --- a/main-server/app/services/matchmaking_service.rb +++ b/main-server/app/services/matchmaking_service.rb @@ -186,8 +186,7 @@ class MatchmakingService # Adds the user to the queue and immediately tries to pair them. # The result (match or timeout) is delivered asynchronously via # MatchmakingChannel broadcasts. - def enqueue(user_id, rating: nil) - rating ||= config.default_rating + def enqueue(user_id, rating:) results = call_pair_script("enqueue", extra: [ user_id, rating ]) process_results(results) end diff --git a/main-server/config/initializers/matchmaking.rb b/main-server/config/initializers/matchmaking.rb index 9c8dc37..8145c50 100644 --- a/main-server/config/initializers/matchmaking.rb +++ b/main-server/config/initializers/matchmaking.rb @@ -2,7 +2,8 @@ mm = Rails.application.config.x.matchmaking = ActiveSupport::OrderedOptions.new mm.game_server_url = ENV.fetch("GAME_SERVER_URL", "ws://localhost:8080") -mm.default_rating = ENV.fetch("MATCHMAKING_DEFAULT_RATING", 1000).to_i +# Players' initial rating lives in the DB (users.rating, default 100), +# so no default rating is configured here. # How long a found match (and its game token) stays valid, in seconds. mm.match_ttl = ENV.fetch("MATCHMAKING_MATCH_TTL", 120).to_i diff --git a/main-server/db/migrate/20261005000000_add_rating_to_users.rb b/main-server/db/migrate/20261005000000_add_rating_to_users.rb new file mode 100644 index 0000000..f6e5e27 --- /dev/null +++ b/main-server/db/migrate/20261005000000_add_rating_to_users.rb @@ -0,0 +1,7 @@ +class AddRatingToUsers < ActiveRecord::Migration[8.1] + def change + # Neutral initial rating. The value lives in the DB; in the future it + # will be recalculated based on player performance. + add_column :users, :rating, :integer, null: false, default: 100 + end +end diff --git a/main-server/db/queue_schema.rb b/main-server/db/queue_schema.rb index f9a71da..a4cda05 100644 --- a/main-server/db/queue_schema.rb +++ b/main-server/db/queue_schema.rb @@ -1,152 +1,164 @@ -ActiveRecord::Schema[7.1].define(version: 1) do - create_table "solid_queue_blocked_executions", force: :cascade do |t| +# This file is auto-generated from the current state of the database. Instead +# of editing this file, please use the migrations feature of Active Record to +# incrementally modify your database, and then regenerate this schema definition. +# +# This file is the source Rails uses to define your schema when running `bin/rails +# db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to +# be faster and is potentially less error prone than running all of your +# migrations from scratch. Old migrations may fail to apply correctly if those +# migrations use external dependencies or application code. +# +# It's strongly recommended that you check this file into your version control system. + +ActiveRecord::Schema[8.1].define(version: 1) do + create_table "solid_queue_batch_executions", force: :cascade do |t| + t.bigint "batch_id", null: false + t.datetime "created_at", null: false t.bigint "job_id", null: false - t.string "queue_name", null: false - t.integer "priority", default: 0, null: false - t.string "concurrency_key", null: false - t.datetime "expires_at", null: false - t.datetime "created_at", null: false - t.index [ "concurrency_key", "priority", "job_id" ], name: "index_solid_queue_blocked_executions_for_release" - t.index [ "expires_at", "concurrency_key" ], name: "index_solid_queue_blocked_executions_for_maintenance" - t.index [ "job_id" ], name: "index_solid_queue_blocked_executions_on_job_id", unique: true - end - - create_table "solid_queue_claimed_executions", force: :cascade do |t| - t.bigint "job_id", null: false - t.bigint "process_id" - t.datetime "created_at", null: false - t.index [ "job_id" ], name: "index_solid_queue_claimed_executions_on_job_id", unique: true - t.index [ "process_id", "job_id" ], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id" - end - - create_table "solid_queue_failed_executions", force: :cascade do |t| - t.bigint "job_id", null: false - t.text "error" - t.datetime "created_at", null: false - t.index [ "job_id" ], name: "index_solid_queue_failed_executions_on_job_id", unique: true - end - - create_table "solid_queue_jobs", force: :cascade do |t| - t.string "queue_name", null: false - t.string "class_name", null: false - t.text "arguments" - t.integer "priority", default: 0, null: false - t.string "active_job_id" - t.datetime "scheduled_at" - t.datetime "finished_at" - t.string "concurrency_key" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.bigint "batch_id" - t.index [ "active_job_id" ], name: "index_solid_queue_jobs_on_active_job_id" - t.index [ "batch_id" ], name: "index_solid_queue_jobs_on_batch_id" - t.index [ "class_name" ], name: "index_solid_queue_jobs_on_class_name" - t.index [ "finished_at" ], name: "index_solid_queue_jobs_on_finished_at" - t.index [ "queue_name", "finished_at" ], name: "index_solid_queue_jobs_for_filtering" - t.index [ "scheduled_at", "finished_at" ], name: "index_solid_queue_jobs_for_alerting" - end - - create_table "solid_queue_pauses", force: :cascade do |t| - t.string "queue_name", null: false - t.datetime "created_at", null: false - t.index [ "queue_name" ], name: "index_solid_queue_pauses_on_queue_name", unique: true - end - - create_table "solid_queue_processes", force: :cascade do |t| - t.string "kind", null: false - t.datetime "last_heartbeat_at", null: false - t.bigint "supervisor_id" - t.integer "pid", null: false - t.string "hostname" - t.text "metadata" - t.datetime "created_at", null: false - t.string "name", null: false - t.index [ "last_heartbeat_at" ], name: "index_solid_queue_processes_on_last_heartbeat_at" - t.index [ "name", "supervisor_id" ], name: "index_solid_queue_processes_on_name_and_supervisor_id", unique: true - t.index [ "supervisor_id" ], name: "index_solid_queue_processes_on_supervisor_id" - end - - create_table "solid_queue_ready_executions", force: :cascade do |t| - t.bigint "job_id", null: false - t.string "queue_name", null: false - t.integer "priority", default: 0, null: false - t.datetime "created_at", null: false - t.index [ "job_id" ], name: "index_solid_queue_ready_executions_on_job_id", unique: true - t.index [ "priority", "job_id" ], name: "index_solid_queue_poll_all" - t.index [ "queue_name", "priority", "job_id" ], name: "index_solid_queue_poll_by_queue" - end - - create_table "solid_queue_recurring_executions", force: :cascade do |t| - t.bigint "job_id", null: false - t.string "task_key", null: false - t.datetime "run_at", null: false - t.datetime "created_at", null: false - t.index [ "job_id" ], name: "index_solid_queue_recurring_executions_on_job_id", unique: true - t.index [ "task_key", "run_at" ], name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", unique: true - end - - create_table "solid_queue_recurring_tasks", force: :cascade do |t| - t.string "key", null: false - t.string "schedule", null: false - t.string "command", limit: 2048 - t.string "class_name" - t.text "arguments" - t.string "queue_name" - t.integer "priority", default: 0 - t.boolean "static", default: true, null: false - t.text "description" - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index [ "key" ], name: "index_solid_queue_recurring_tasks_on_key", unique: true - t.index [ "static" ], name: "index_solid_queue_recurring_tasks_on_static" - end - - create_table "solid_queue_scheduled_executions", force: :cascade do |t| - t.bigint "job_id", null: false - t.string "queue_name", null: false - t.integer "priority", default: 0, null: false - t.datetime "scheduled_at", null: false - t.datetime "created_at", null: false - t.index [ "job_id" ], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true - t.index [ "scheduled_at", "priority", "job_id" ], name: "index_solid_queue_dispatch_all" - end - - create_table "solid_queue_semaphores", force: :cascade do |t| - t.string "key", null: false - t.integer "value", default: 1, null: false - t.datetime "expires_at", null: false - t.datetime "created_at", null: false - t.datetime "updated_at", null: false - t.index [ "expires_at" ], name: "index_solid_queue_semaphores_on_expires_at" - t.index [ "key", "value" ], name: "index_solid_queue_semaphores_on_key_and_value" - t.index [ "key" ], name: "index_solid_queue_semaphores_on_key", unique: true + t.index ["batch_id"], name: "index_solid_queue_batch_executions_on_batch_id" + t.index ["job_id"], name: "index_solid_queue_batch_executions_on_job_id", unique: true end create_table "solid_queue_batches", force: :cascade do |t| t.string "active_job_batch_id" + t.integer "completed_jobs", default: 0, null: false + t.datetime "created_at", null: false t.string "description" + t.datetime "enqueued_at" + t.datetime "failed_at" + t.integer "failed_jobs", default: 0, null: false + t.datetime "finished_at" + t.text "metadata" + t.text "on_failure" t.text "on_finish" t.text "on_success" - t.text "on_failure" - t.text "metadata" t.integer "total_jobs", default: 0, null: false - t.integer "completed_jobs", default: 0, null: false - t.integer "failed_jobs", default: 0, null: false - t.datetime "enqueued_at" - t.datetime "finished_at" - t.datetime "failed_at" - t.datetime "created_at", null: false t.datetime "updated_at", null: false - t.index [ "active_job_batch_id" ], name: "index_solid_queue_batches_on_active_job_batch_id", unique: true - t.index [ "finished_at" ], name: "index_solid_queue_batches_on_finished_at" + t.index ["active_job_batch_id"], name: "index_solid_queue_batches_on_active_job_batch_id", unique: true + t.index ["finished_at"], name: "index_solid_queue_batches_on_finished_at" end - create_table "solid_queue_batch_executions", force: :cascade do |t| - t.bigint "job_id", null: false - t.bigint "batch_id", null: false + create_table "solid_queue_blocked_executions", force: :cascade do |t| + t.string "concurrency_key", null: false t.datetime "created_at", null: false - t.index [ "job_id" ], name: "index_solid_queue_batch_executions_on_job_id", unique: true - t.index [ "batch_id" ], name: "index_solid_queue_batch_executions_on_batch_id" + t.datetime "expires_at", null: false + t.bigint "job_id", null: false + t.integer "priority", default: 0, null: false + t.string "queue_name", null: false + t.index ["concurrency_key", "priority", "job_id"], name: "index_solid_queue_blocked_executions_for_release" + t.index ["expires_at", "concurrency_key"], name: "index_solid_queue_blocked_executions_for_maintenance" + t.index ["job_id"], name: "index_solid_queue_blocked_executions_on_job_id", unique: true + end + + create_table "solid_queue_claimed_executions", force: :cascade do |t| + t.datetime "created_at", null: false + t.bigint "job_id", null: false + t.bigint "process_id" + t.index ["job_id"], name: "index_solid_queue_claimed_executions_on_job_id", unique: true + t.index ["process_id", "job_id"], name: "index_solid_queue_claimed_executions_on_process_id_and_job_id" + end + + create_table "solid_queue_failed_executions", force: :cascade do |t| + t.datetime "created_at", null: false + t.text "error" + t.bigint "job_id", null: false + t.index ["job_id"], name: "index_solid_queue_failed_executions_on_job_id", unique: true + end + + create_table "solid_queue_jobs", force: :cascade do |t| + t.string "active_job_id" + t.text "arguments" + t.bigint "batch_id" + t.string "class_name", null: false + t.string "concurrency_key" + t.datetime "created_at", null: false + t.datetime "finished_at" + t.integer "priority", default: 0, null: false + t.string "queue_name", null: false + t.datetime "scheduled_at" + t.datetime "updated_at", null: false + t.index ["active_job_id"], name: "index_solid_queue_jobs_on_active_job_id" + t.index ["batch_id"], name: "index_solid_queue_jobs_on_batch_id" + t.index ["class_name"], name: "index_solid_queue_jobs_on_class_name" + t.index ["finished_at"], name: "index_solid_queue_jobs_on_finished_at" + t.index ["queue_name", "finished_at"], name: "index_solid_queue_jobs_for_filtering" + t.index ["scheduled_at", "finished_at"], name: "index_solid_queue_jobs_for_alerting" + end + + create_table "solid_queue_pauses", force: :cascade do |t| + t.datetime "created_at", null: false + t.string "queue_name", null: false + t.index ["queue_name"], name: "index_solid_queue_pauses_on_queue_name", unique: true + end + + create_table "solid_queue_processes", force: :cascade do |t| + t.datetime "created_at", null: false + t.string "hostname" + t.string "kind", null: false + t.datetime "last_heartbeat_at", null: false + t.text "metadata" + t.string "name", null: false + t.integer "pid", null: false + t.bigint "supervisor_id" + t.index ["last_heartbeat_at"], name: "index_solid_queue_processes_on_last_heartbeat_at" + t.index ["name", "supervisor_id"], name: "index_solid_queue_processes_on_name_and_supervisor_id", unique: true + t.index ["supervisor_id"], name: "index_solid_queue_processes_on_supervisor_id" + end + + create_table "solid_queue_ready_executions", force: :cascade do |t| + t.datetime "created_at", null: false + t.bigint "job_id", null: false + t.integer "priority", default: 0, null: false + t.string "queue_name", null: false + t.index ["job_id"], name: "index_solid_queue_ready_executions_on_job_id", unique: true + t.index ["priority", "job_id"], name: "index_solid_queue_poll_all" + t.index ["queue_name", "priority", "job_id"], name: "index_solid_queue_poll_by_queue" + end + + create_table "solid_queue_recurring_executions", force: :cascade do |t| + t.datetime "created_at", null: false + t.bigint "job_id", null: false + t.datetime "run_at", null: false + t.string "task_key", null: false + t.index ["job_id"], name: "index_solid_queue_recurring_executions_on_job_id", unique: true + t.index ["task_key", "run_at"], name: "index_solid_queue_recurring_executions_on_task_key_and_run_at", unique: true + end + + create_table "solid_queue_recurring_tasks", force: :cascade do |t| + t.text "arguments" + t.string "class_name" + t.string "command", limit: 2048 + t.datetime "created_at", null: false + t.text "description" + t.string "key", null: false + t.integer "priority", default: 0 + t.string "queue_name" + t.string "schedule", null: false + t.boolean "static", default: true, null: false + t.datetime "updated_at", null: false + t.index ["key"], name: "index_solid_queue_recurring_tasks_on_key", unique: true + t.index ["static"], name: "index_solid_queue_recurring_tasks_on_static" + end + + create_table "solid_queue_scheduled_executions", force: :cascade do |t| + t.datetime "created_at", null: false + t.bigint "job_id", null: false + t.integer "priority", default: 0, null: false + t.string "queue_name", null: false + t.datetime "scheduled_at", null: false + t.index ["job_id"], name: "index_solid_queue_scheduled_executions_on_job_id", unique: true + t.index ["scheduled_at", "priority", "job_id"], name: "index_solid_queue_dispatch_all" + end + + create_table "solid_queue_semaphores", force: :cascade do |t| + t.datetime "created_at", null: false + t.datetime "expires_at", null: false + t.string "key", null: false + t.datetime "updated_at", null: false + t.integer "value", default: 1, null: false + t.index ["expires_at"], name: "index_solid_queue_semaphores_on_expires_at" + t.index ["key", "value"], name: "index_solid_queue_semaphores_on_key_and_value" + t.index ["key"], name: "index_solid_queue_semaphores_on_key", unique: true end add_foreign_key "solid_queue_batch_executions", "solid_queue_batches", column: "batch_id", on_delete: :cascade diff --git a/main-server/db/schema.rb b/main-server/db/schema.rb index 971b1d0..25ec162 100644 --- a/main-server/db/schema.rb +++ b/main-server/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.1].define(version: 2026_09_20_183621) do +ActiveRecord::Schema[8.1].define(version: 2026_10_05_000000) do create_table "active_storage_attachments", force: :cascade do |t| t.bigint "blob_id", null: false t.datetime "created_at", null: false @@ -52,6 +52,7 @@ ActiveRecord::Schema[8.1].define(version: 2026_09_20_183621) do t.datetime "created_at", null: false t.string "nickname", null: false t.string "password_digest", null: false + t.integer "rating", default: 100, null: false t.datetime "updated_at", null: false t.index ["nickname"], name: "index_users_on_nickname", unique: true end diff --git a/main-server/spec/models/user_spec.rb b/main-server/spec/models/user_spec.rb index d78dbb0..bcbdd65 100644 --- a/main-server/spec/models/user_spec.rb +++ b/main-server/spec/models/user_spec.rb @@ -40,6 +40,19 @@ RSpec.describe User, type: :model do end end + describe "rating" do + it "defaults to the neutral initial value of 100" do + expect(User.create!(valid_attributes).rating).to eq(100) + end + + it "requires a rating" do + user = User.new(valid_attributes) + user.rating = nil + expect(user).not_to be_valid + expect(user.errors[:rating]).to be_present + end + end + describe "nickname normalization" do it "strips surrounding whitespace" do user = User.create!(valid_attributes.merge(nickname: " player_one "))