Skip to content

Commit e48514b

Browse files
committed
feat: improve inhouse features
1 parent b39a884 commit e48514b

10 files changed

+369
-39
lines changed

app/models/inhouse_participation.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ class InhouseParticipation < ApplicationRecord
1414
validates :player_id, uniqueness: { scope: :inhouse_id, message: 'is already in this inhouse' }
1515
validates :team, inclusion: { in: %w[none blue red] }
1616

17+
ROLES = %w[top jungle mid adc support fill].freeze
18+
1719
# Scopes
1820
scope :blue_team, -> { where(team: 'blue') }
1921
scope :red_team, -> { where(team: 'red') }
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# frozen_string_literal: true
2+
3+
# Stores per-player, per-role TrueSkill ratings for the inhouse ladder.
4+
#
5+
# One record per (player, role) pair. Created on first game and updated
6+
# after every record_game call via TrueSkillService.
7+
#
8+
# @example Find or initialise a rating
9+
# rating = PlayerInhouseRating.for(player, role)
10+
# rating.mmr # => 0 (fresh player)
11+
#
12+
class PlayerInhouseRating < ApplicationRecord
13+
MU_INITIAL = 25.0
14+
SIGMA_INITIAL = 25.0 / 3.0 # ≈ 8.333
15+
ROLES = %w[top jungle mid adc support fill].freeze
16+
17+
belongs_to :player
18+
belongs_to :organization
19+
20+
validates :role, inclusion: { in: ROLES }
21+
validates :player_id, uniqueness: { scope: :role }
22+
validates :mu, :sigma, :games_played, :wins, :losses, presence: true
23+
24+
# Conservative skill estimate used for ladder ranking.
25+
# Returns an integer in roughly 0–3000 range.
26+
def mmr
27+
[((mu - (3.0 * sigma)) * 100).round, 0].max
28+
end
29+
30+
def win_rate
31+
return 0.0 if games_played.zero?
32+
33+
(wins.to_f / games_played * 100).round(1)
34+
end
35+
36+
# Find an existing rating or build a fresh one (unsaved).
37+
def self.for(player, role, organization)
38+
find_or_initialize_by(player: player, role: role) do |r|
39+
r.organization = organization
40+
r.mu = MU_INITIAL
41+
r.sigma = SIGMA_INITIAL
42+
end
43+
end
44+
end

app/modules/inhouses/controllers/inhouse_queues_controller.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -200,12 +200,13 @@ def start_session
200200
formation_mode: formation_mode
201201
)
202202

203-
# Join all checked-in players
203+
# Join all checked-in players, preserving their queued role
204204
entries.each do |entry|
205205
inhouse.inhouse_participations.create!(
206206
player: entry.player,
207207
team: 'none',
208208
tier_snapshot: entry.tier_snapshot,
209+
role: entry.role,
209210
is_captain: false
210211
)
211212
end
@@ -409,7 +410,11 @@ def serialize_inhouse(inhouse, detailed: false)
409410
player_id: p.player_id,
410411
player_name: p.player&.summoner_name,
411412
team: p.team,
413+
role: p.role,
412414
tier_snapshot: p.tier_snapshot,
415+
mu_snapshot: p.mu_snapshot,
416+
sigma_snapshot: p.sigma_snapshot,
417+
mmr_delta: p.mmr_delta,
413418
is_captain: p.is_captain,
414419
wins: p.wins,
415420
losses: p.losses

app/modules/inhouses/controllers/inhouses_controller.rb

Lines changed: 67 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -22,51 +22,72 @@ class InhousesController < Api::V1::BaseController
2222
before_action :set_inhouse, only: %i[join balance_teams start_draft captain_pick start_game record_game close]
2323

2424
# GET /api/v1/inhouse/ladder
25-
# Returns per-player win/loss/win-rate aggregated across all done sessions.
25+
# Returns per-player TrueSkill ratings sorted by MMR descending.
26+
# Optional query param: ?role=mid (filter by role)
2627
def ladder
2728
authorize Inhouse
2829

29-
rows = InhouseParticipation
30-
.joins(:inhouse, :player)
31-
.where(inhouses: { organization_id: current_organization.id, status: 'done' })
32-
.where.not(team: 'none')
33-
.group(:player_id)
34-
.select(
35-
'player_id',
36-
'SUM(wins) AS total_wins',
37-
'SUM(losses) AS total_losses'
38-
)
39-
.to_a
40-
41-
player_ids = rows.map(&:player_id)
42-
players_by_id = current_organization.players
43-
.where(id: player_ids)
44-
.index_by(&:id)
45-
46-
entries = rows.map do |row|
47-
player = players_by_id[row.player_id]
48-
next unless player
49-
50-
total = row.total_wins.to_i + row.total_losses.to_i
51-
win_rate = total.zero? ? 0.0 : (row.total_wins.to_f / total * 100).round(1)
30+
scope = PlayerInhouseRating
31+
.joins(:player)
32+
.where(organization_id: current_organization.id)
5233

34+
scope = scope.where(role: params[:role].downcase) if params[:role].present?
35+
36+
ratings = scope.includes(:player).to_a
37+
38+
entries = ratings.map do |r|
5339
{
54-
player_id: row.player_id,
55-
player_name: player.summoner_name,
56-
role: player.role,
57-
wins: row.total_wins.to_i,
58-
losses: row.total_losses.to_i,
59-
total_games: total,
60-
win_rate: win_rate
40+
player_id: r.player_id,
41+
player_name: r.player&.summoner_name,
42+
role: r.role,
43+
mu: r.mu.round(3),
44+
sigma: r.sigma.round(3),
45+
mmr: r.mmr,
46+
games_played: r.games_played,
47+
wins: r.wins,
48+
losses: r.losses,
49+
win_rate: r.win_rate
6150
}
62-
end.compact
51+
end
6352

64-
entries.sort_by! { |e| [-e[:wins], e[:losses]] }
53+
entries.sort_by! { |e| -e[:mmr] }
6554
entries.each_with_index { |e, i| e[:rank] = i + 1 }
6655

6756
render_success({ entries: entries, total: entries.size })
6857
end
6958

59+
# GET /api/v1/inhouse/ladder/:player_id
60+
# Returns all role ratings for a single player.
61+
def player_ratings
62+
authorize Inhouse
63+
64+
player = current_organization.players.find_by(id: params[:player_id])
65+
return render_not_found unless player
66+
67+
ratings = PlayerInhouseRating
68+
.where(player: player, organization: current_organization)
69+
.order(:role)
70+
71+
entries = ratings.map do |r|
72+
{
73+
role: r.role,
74+
mu: r.mu.round(3),
75+
sigma: r.sigma.round(3),
76+
mmr: r.mmr,
77+
games_played: r.games_played,
78+
wins: r.wins,
79+
losses: r.losses,
80+
win_rate: r.win_rate
81+
}
82+
end
83+
84+
render_success({
85+
player_id: player.id,
86+
player_name: player.summoner_name,
87+
ratings: entries
88+
})
89+
end
90+
7091
# GET /api/v1/inhouse/sessions
7192
# Returns paginated history of completed inhouse sessions with summary.
7293
def sessions
@@ -208,10 +229,15 @@ def join
208229
)
209230
end
210231

232+
role = params[:role].to_s.downcase.presence
233+
role = nil unless InhouseParticipation::ROLES.include?(role)
234+
role ||= player.role.presence
235+
211236
participation = @inhouse.inhouse_participations.new(
212237
player: player,
213238
team: 'none',
214-
tier_snapshot: player.solo_queue_tier.presence
239+
tier_snapshot: player.solo_queue_tier.presence,
240+
role: role
215241
)
216242

217243
if participation.save
@@ -455,7 +481,7 @@ def record_game
455481
@inhouse.increment!(:red_wins)
456482
end
457483

458-
# Update per-player wins/losses
484+
# Update per-player wins/losses and TrueSkill ratings
459485
@inhouse.inhouse_participations.each do |p|
460486
next if p.team == 'none'
461487

@@ -466,6 +492,8 @@ def record_game
466492
end
467493
end
468494

495+
TrueSkillService.update_ratings(@inhouse, winner)
496+
469497
render_success(
470498
{ inhouse: serialize_inhouse(@inhouse.reload) },
471499
message: "Game recorded — #{winner.capitalize} team wins"
@@ -574,7 +602,11 @@ def serialize_inhouse(inhouse, detailed: false)
574602
player_id: p.player_id,
575603
player_name: p.player&.summoner_name,
576604
team: p.team,
605+
role: p.role,
577606
tier_snapshot: p.tier_snapshot,
607+
mu_snapshot: p.mu_snapshot,
608+
sigma_snapshot: p.sigma_snapshot,
609+
mmr_delta: p.mmr_delta,
578610
is_captain: p.is_captain,
579611
wins: p.wins,
580612
losses: p.losses

0 commit comments

Comments
 (0)