add random match-making logic
This commit is contained in:
+1
-1
@@ -33,5 +33,5 @@
|
||||
# Ignore key files for decrypting credentials and more.
|
||||
/main-server/config/*.key
|
||||
|
||||
/frontend/node-modules
|
||||
/frontend/node_modules
|
||||
/frontend/dist
|
||||
|
||||
@@ -38,3 +38,5 @@ group :development, :test do
|
||||
gem "brakeman", require: false
|
||||
gem "rubocop-rails-omakase", require: false
|
||||
end
|
||||
|
||||
gem "redis", "~> 6.0"
|
||||
|
||||
@@ -250,6 +250,10 @@ GEM
|
||||
prism (>= 1.6.0)
|
||||
rbs (>= 4.0.0)
|
||||
tsort
|
||||
redis (6.0.0)
|
||||
redis-client (= 0.30.1)
|
||||
redis-client (0.30.1)
|
||||
connection_pool
|
||||
regexp_parser (2.12.0)
|
||||
reline (0.7.0)
|
||||
io-console (~> 0.5)
|
||||
@@ -357,6 +361,7 @@ DEPENDENCIES
|
||||
puma (>= 5.0)
|
||||
rack-cors
|
||||
rails (~> 8.1.2)
|
||||
redis (~> 6.0)
|
||||
rubocop-rails-omakase
|
||||
solid_cable
|
||||
solid_cache
|
||||
@@ -459,6 +464,8 @@ CHECKSUMS
|
||||
rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
|
||||
rbs (4.2.0) sha256=51f7b886dcc05bc09e10b901daa6a81829f6adc03101d6ca9ea4aac6103e0674
|
||||
rdoc (8.0.0) sha256=03bf8c08a9639658855a0cfd77c0abca8325c227693f7f33f82957811348c469
|
||||
redis (6.0.0) sha256=de71c10edd106986b759ec7ecdd08b63b9c0ee7414a0d0c1da73d31ba2bccda6
|
||||
redis-client (0.30.1) sha256=5151bc5c7bbfe48623732cdae3b900d8a22dc691cc7cdfacfb351ac55116522d
|
||||
regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb
|
||||
reline (0.7.0) sha256=5b012d8e55dbf9d450f12bde2cf7d15ff546ae80b3f8f3b30e570d431815583d
|
||||
rubocop (1.91.0) sha256=9c82b7bf391c5d7e3798c5b9996e22a1fe3bd7468e351dfdeb96140c058296d0
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
class Api::MatchmakingController < ApplicationController
|
||||
def create
|
||||
result = MatchmakingService.find_match(current_user.id)
|
||||
|
||||
if result
|
||||
render json: {
|
||||
match_id: result[:match_id],
|
||||
game_token: result[:game_token],
|
||||
game_server: result[:game_server],
|
||||
players: result[:players]
|
||||
}
|
||||
else
|
||||
render json: { error: "No opponent found. Try again." }, status: :request_timeout
|
||||
end
|
||||
end
|
||||
|
||||
def destroy
|
||||
MatchmakingService.cancel(current_user.id)
|
||||
render json: { message: "Matchmaking cancelled" }
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,122 @@
|
||||
class MatchmakingService
|
||||
QUEUE_KEY = "matchmaking:queue"
|
||||
LOCK_KEY = "matchmaking:lock"
|
||||
MATCH_PREFIX = "matchmaking:match:"
|
||||
MATCH_TTL = 60 # seconds
|
||||
POLL_INTERVAL = 0.5 # seconds
|
||||
MAX_WAIT = 30 # seconds
|
||||
|
||||
# Game server to redirect players to (hardcoded for now)
|
||||
GAME_SERVER = ENV.fetch("GAME_SERVER_URL", "ws://game-server.example.com:8080")
|
||||
|
||||
class << self
|
||||
# Main entry point: waits for an opponent and returns match data
|
||||
def find_match(user_id)
|
||||
user_id = user_id.to_i
|
||||
|
||||
# Try to pair with an existing waiting player
|
||||
opponent_id = pop_opponent(user_id)
|
||||
|
||||
if opponent_id
|
||||
create_match(user_id, opponent_id)
|
||||
else
|
||||
# Add self to queue and wait
|
||||
add_to_queue(user_id)
|
||||
wait_for_match(user_id)
|
||||
end
|
||||
end
|
||||
|
||||
# Cancel matchmaking (remove from queue)
|
||||
def cancel(user_id)
|
||||
RedisConnection.with do |redis|
|
||||
redis.lrem(QUEUE_KEY, 0, user_id)
|
||||
end
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
# Atomically pop an opponent from the queue (excluding self)
|
||||
def pop_opponent(user_id)
|
||||
RedisConnection.with do |redis|
|
||||
redis.watch(QUEUE_KEY) do
|
||||
queue = redis.lrange(QUEUE_KEY, 0, -1)
|
||||
opponent = queue.find { |id| id.to_i != user_id }
|
||||
|
||||
if opponent
|
||||
result = redis.multi do |tx|
|
||||
tx.lrem(QUEUE_KEY, 0, opponent)
|
||||
end
|
||||
return opponent.to_i if result&.first&.positive?
|
||||
else
|
||||
redis.unwatch
|
||||
end
|
||||
end
|
||||
end
|
||||
nil
|
||||
end
|
||||
|
||||
def add_to_queue(user_id)
|
||||
RedisConnection.with do |redis|
|
||||
# Remove any stale entries first
|
||||
redis.lrem(QUEUE_KEY, 0, user_id)
|
||||
redis.rpush(QUEUE_KEY, user_id)
|
||||
end
|
||||
end
|
||||
|
||||
def wait_for_match(user_id)
|
||||
start_time = Time.current
|
||||
match_id = nil
|
||||
|
||||
while Time.current - start_time < MAX_WAIT
|
||||
RedisConnection.with do |redis|
|
||||
match_id = redis.get("#{MATCH_PREFIX}#{user_id}")
|
||||
end
|
||||
break if match_id
|
||||
|
||||
sleep POLL_INTERVAL
|
||||
end
|
||||
|
||||
if match_id
|
||||
# Retrieve and delete the match data
|
||||
data = nil
|
||||
RedisConnection.with do |redis|
|
||||
data = redis.get("#{MATCH_PREFIX}match:#{match_id}")
|
||||
redis.del("#{MATCH_PREFIX}#{user_id}")
|
||||
redis.del("#{MATCH_PREFIX}match:#{match_id}")
|
||||
end
|
||||
JSON.parse(data, symbolize_names: true) if data
|
||||
else
|
||||
# Timeout: remove from queue
|
||||
cancel(user_id)
|
||||
nil
|
||||
end
|
||||
end
|
||||
|
||||
def create_match(player1_id, player2_id)
|
||||
match_id = SecureRandom.uuid
|
||||
game_token = JwtService.encode(
|
||||
match_id: match_id,
|
||||
player1_id: player1_id,
|
||||
player2_id: player2_id,
|
||||
game_server: GAME_SERVER,
|
||||
exp: 1.hour.from_now.to_i
|
||||
)
|
||||
|
||||
match_data = {
|
||||
match_id: match_id,
|
||||
game_token: game_token,
|
||||
game_server: GAME_SERVER,
|
||||
players: [player1_id, player2_id]
|
||||
}
|
||||
|
||||
# Store match data for both players
|
||||
RedisConnection.with do |redis|
|
||||
redis.set("#{MATCH_PREFIX}match:#{match_id}", match_data.to_json, ex: MATCH_TTL)
|
||||
redis.set("#{MATCH_PREFIX}#{player1_id}", match_id, ex: MATCH_TTL)
|
||||
redis.set("#{MATCH_PREFIX}#{player2_id}", match_id, ex: MATCH_TTL)
|
||||
end
|
||||
|
||||
match_data
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,7 @@
|
||||
# Redis configuration
|
||||
# Development/test: localhost:6379 (default)
|
||||
# Production: set REDIS_URL env variable, e.g. redis://redis.example.com:6379/0
|
||||
|
||||
RedisConnection = ConnectionPool.new(size: 5, timeout: 5) do
|
||||
Redis.new(url: ENV.fetch("REDIS_URL", "redis://localhost:6380/0"))
|
||||
end
|
||||
@@ -4,6 +4,9 @@ Rails.application.routes.draw do
|
||||
post "login", to: "sessions#create"
|
||||
delete "logout", to: "sessions#destroy"
|
||||
get "me", to: "sessions#me"
|
||||
|
||||
post "matchmaking", to: "matchmaking#create"
|
||||
delete "matchmaking", to: "matchmaking#destroy"
|
||||
end
|
||||
|
||||
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
|
||||
|
||||
Reference in New Issue
Block a user