add player rating to the db

This commit is contained in:
2026-10-25 17:10:40 +03:00
parent acdaebd491
commit 765a3365bd
9 changed files with 180 additions and 147 deletions
+3 -4
View File
@@ -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\"}"}
```
@@ -3,7 +3,7 @@
# Client flow:
# 1. Connect: ws(s)://host/cable?token=<JWT>
# 2. Subscribe to "MatchmakingChannel"
# 3. Send { action: "start", rating: <optional int> } 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
+1
View File
@@ -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? }
@@ -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
@@ -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
@@ -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
+148 -136
View File
@@ -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
+2 -1
View File
@@ -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
+13
View File
@@ -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 "))