add random match-making logic
This commit is contained in:
+1
-1
@@ -33,5 +33,5 @@
|
|||||||
# Ignore key files for decrypting credentials and more.
|
# Ignore key files for decrypting credentials and more.
|
||||||
/main-server/config/*.key
|
/main-server/config/*.key
|
||||||
|
|
||||||
/frontend/node-modules
|
/frontend/node_modules
|
||||||
/frontend/dist
|
/frontend/dist
|
||||||
|
|||||||
@@ -38,3 +38,5 @@ group :development, :test do
|
|||||||
gem "brakeman", require: false
|
gem "brakeman", require: false
|
||||||
gem "rubocop-rails-omakase", require: false
|
gem "rubocop-rails-omakase", require: false
|
||||||
end
|
end
|
||||||
|
|
||||||
|
gem "redis", "~> 6.0"
|
||||||
|
|||||||
@@ -250,6 +250,10 @@ GEM
|
|||||||
prism (>= 1.6.0)
|
prism (>= 1.6.0)
|
||||||
rbs (>= 4.0.0)
|
rbs (>= 4.0.0)
|
||||||
tsort
|
tsort
|
||||||
|
redis (6.0.0)
|
||||||
|
redis-client (= 0.30.1)
|
||||||
|
redis-client (0.30.1)
|
||||||
|
connection_pool
|
||||||
regexp_parser (2.12.0)
|
regexp_parser (2.12.0)
|
||||||
reline (0.7.0)
|
reline (0.7.0)
|
||||||
io-console (~> 0.5)
|
io-console (~> 0.5)
|
||||||
@@ -357,6 +361,7 @@ DEPENDENCIES
|
|||||||
puma (>= 5.0)
|
puma (>= 5.0)
|
||||||
rack-cors
|
rack-cors
|
||||||
rails (~> 8.1.2)
|
rails (~> 8.1.2)
|
||||||
|
redis (~> 6.0)
|
||||||
rubocop-rails-omakase
|
rubocop-rails-omakase
|
||||||
solid_cable
|
solid_cable
|
||||||
solid_cache
|
solid_cache
|
||||||
@@ -459,6 +464,8 @@ CHECKSUMS
|
|||||||
rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
|
rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
|
||||||
rbs (4.2.0) sha256=51f7b886dcc05bc09e10b901daa6a81829f6adc03101d6ca9ea4aac6103e0674
|
rbs (4.2.0) sha256=51f7b886dcc05bc09e10b901daa6a81829f6adc03101d6ca9ea4aac6103e0674
|
||||||
rdoc (8.0.0) sha256=03bf8c08a9639658855a0cfd77c0abca8325c227693f7f33f82957811348c469
|
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
|
regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb
|
||||||
reline (0.7.0) sha256=5b012d8e55dbf9d450f12bde2cf7d15ff546ae80b3f8f3b30e570d431815583d
|
reline (0.7.0) sha256=5b012d8e55dbf9d450f12bde2cf7d15ff546ae80b3f8f3b30e570d431815583d
|
||||||
rubocop (1.91.0) sha256=9c82b7bf391c5d7e3798c5b9996e22a1fe3bd7468e351dfdeb96140c058296d0
|
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"
|
post "login", to: "sessions#create"
|
||||||
delete "logout", to: "sessions#destroy"
|
delete "logout", to: "sessions#destroy"
|
||||||
get "me", to: "sessions#me"
|
get "me", to: "sessions#me"
|
||||||
|
|
||||||
|
post "matchmaking", to: "matchmaking#create"
|
||||||
|
delete "matchmaking", to: "matchmaking#destroy"
|
||||||
end
|
end
|
||||||
|
|
||||||
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
|
# Reveal health status on /up that returns 200 if the app boots with no exceptions, otherwise 500.
|
||||||
|
|||||||
Reference in New Issue
Block a user