Skip to content

Commit 9477bd5

Browse files
committed
Fixing Queues and matchmaking
1 parent b17d672 commit 9477bd5

File tree

7 files changed

+437
-239
lines changed

7 files changed

+437
-239
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/**
2+
* @type {import('node-pg-migrate').ColumnDefinitions | undefined}
3+
*/
4+
export const shorthands = undefined;
5+
6+
/**
7+
* @param pgm {import('node-pg-migrate').MigrationBuilder}
8+
* @param run {() => void | undefined}
9+
* @returns {Promise<void> | void}
10+
*/
11+
export const up = (pgm) => {
12+
pgm.createTable('votes', {
13+
id: { type: 'serial', primaryKey: true },
14+
match_id: { type: 'integer', notNull: true, references: '"matches"', onDelete: 'CASCADE' },
15+
user_id: { type: 'varchar(255)', notNull: true, references: '"users"(user_id)', onDelete: 'CASCADE', onUpdate: 'CASCADE' },
16+
vote_type: { type: 'varchar(50)', notNull: true }, // 'win', 'cancel', 'rematch', 'bo3', 'bo5'
17+
vote_value: { type: 'integer' }, // For win votes: team number (1, 2, etc), for others: null (presence = yes vote)
18+
created_at: { type: 'timestamp with time zone', notNull: true, default: pgm.func('NOW()') },
19+
});
20+
21+
// Index for faster lookups
22+
pgm.addIndex('votes', ['match_id', 'user_id']);
23+
pgm.addIndex('votes', ['match_id', 'vote_type']);
24+
25+
// Constraint: A user can only have one active vote per match (they can change their vote)
26+
pgm.addConstraint('votes', 'unique_user_vote_per_match', {
27+
unique: ['match_id', 'user_id']
28+
});
29+
};
30+
31+
/**
32+
* @param pgm {import('node-pg-migrate').MigrationBuilder}
33+
* @param run {() => void | undefined}
34+
* @returns {Promise<void> | void}
35+
*/
36+
export const down = (pgm) => {
37+
pgm.dropTable('votes', { ifExists: true, cascade: true });
38+
};

src/events/interactionCreate.ts

Lines changed: 45 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -210,55 +210,58 @@ export default {
210210
await setWinningTeam(matchId, winner)
211211
await endMatch(matchId)
212212
return
213-
}
213+
} else {
214+
const embed = interaction.message.embeds[0]
215+
const fields = embed.data.fields || []
214216

215-
const embed = interaction.message.embeds[0]
216-
const fields = embed.data.fields || []
217+
// Update Best of scores in the embed (for display only)
218+
const winnerIndex = winner === 1 ? 0 : 1
219+
for (let i = 0; i < Math.min(2, fields.length); i++) {
220+
const val = fields[i].value || ''
221+
const lines = val.split('\n')
217222

218-
// Update Best of scores and check for match winner
219-
const winnerIndex = winner === 1 ? 0 : 1
220-
for (let i = 0; i < Math.min(2, fields.length); i++) {
221-
const val = fields[i].value || ''
222-
const lines = val.split('\n')
223+
const cleaned = lines.filter(
224+
(l) => !l.includes('Win Votes') && !l.includes('<@'),
225+
)
223226

224-
const cleaned = lines.filter(
225-
(l) => !l.includes('Win Votes') && !l.includes('<@'),
226-
)
227+
const mmrIdx = cleaned.findIndex((l) => l.includes('MMR'))
228+
if (mmrIdx !== -1) {
229+
const mmrLine = cleaned[mmrIdx]
230+
const m = mmrLine.match(/Score:\s*(\d+)/i)
231+
let score = m ? parseInt(m[1], 10) || 0 : 0
227232

228-
const mmrIdx = cleaned.findIndex((l) => l.includes('MMR'))
229-
if (mmrIdx !== -1) {
230-
const mmrLine = cleaned[mmrIdx]
231-
const m = mmrLine.match(/Score:\s*(\d+)/i)
232-
let score = m ? parseInt(m[1], 10) || 0 : 0
233+
if (i === winnerIndex) score += 1
233234

234-
if (i === winnerIndex) score += 1
235+
cleaned[mmrIdx] =
236+
mmrLine.replace(/\s*-?\s*Score:\s*\d+/i, '').trimEnd() +
237+
` - Score: ${score}`
238+
}
235239

236-
cleaned[mmrIdx] =
237-
mmrLine.replace(/\s*-?\s*Score:\s*\d+/i, '').trimEnd() +
238-
` - Score: ${score}`
240+
fields[i].value = cleaned.join('\n')
239241
}
240242

241-
fields[i].value = cleaned.join('\n')
242-
}
243-
244-
const scores = getBestOfMatchScores(fields)
245-
const requiredWins = isBo5 ? 3 : isBo3 ? 2 : 1
246-
const [team1Wins, team2Wins] = scores
243+
// Get updated scores from the embed (after incrementing)
244+
let scores = getBestOfMatchScores(fields)
245+
let requiredWins = isBo5 ? 3 : isBo3 ? 2 : 1
246+
const [team1Wins, team2Wins] = scores
247+
248+
// Check if a team has won the Best of series
249+
let winningTeam = 0
250+
if (team1Wins >= requiredWins) {
251+
winningTeam = 1
252+
} else if (team2Wins >= requiredWins) {
253+
winningTeam = 2
254+
}
247255

248-
let winningTeam = 0
249-
if (team1Wins >= requiredWins) {
250-
winningTeam = 1
251-
} else if (team2Wins >= requiredWins) {
252-
winningTeam = 2
253-
}
256+
if (winningTeam) {
257+
await setWinningTeam(matchId, winningTeam)
258+
await endMatch(matchId)
259+
return
260+
}
254261

255-
if (winningTeam) {
256-
await endMatch(matchId)
257-
return
262+
interaction.message.embeds[0] = embed
263+
await interaction.update({ embeds: interaction.message.embeds })
258264
}
259-
260-
interaction.message.embeds[0] = embed
261-
await interaction.update({ embeds: interaction.message.embeds })
262265
},
263266
})
264267
}
@@ -542,6 +545,7 @@ export default {
542545
voteType: 'Cancel Match Votes',
543546
embedFieldIndex: 2,
544547
participants: matchUsersArray,
548+
matchId: matchId,
545549
onComplete: async (interaction) => {
546550
await cancel(interaction, matchId)
547551
},
@@ -724,6 +728,8 @@ export default {
724728
voteType: 'Rematch Votes',
725729
embedFieldIndex: 2,
726730
participants: matchUsersArray,
731+
matchId: matchId,
732+
resendMessage: false,
727733
onComplete: async (interaction, { embed }) => {
728734
await interaction.update({
729735
content: 'A Rematch for this matchup has begun!',
@@ -796,6 +802,7 @@ export default {
796802
voteType: voteFieldName,
797803
embedFieldIndex: 3,
798804
participants: matchUsersArray,
805+
matchId: matchId,
799806
onComplete: async (interaction, { embed }) => {
800807
const rows = interaction.message.components.map((row) =>
801808
ActionRowBuilder.from(row as any),

src/utils/algorithms/calculateMMR.ts

Lines changed: 86 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ import type { teamResults } from 'psqlDB'
88
import { setUserQueueRole } from 'utils/queueHelpers'
99
import { clamp } from 'lodash-es'
1010

11+
// MMR formula constants
12+
const C_MMR_CHANGE = 25 // Base MMR change value
13+
const V_VARIANCE = 1200 // MMR variance
14+
1115
// Function is from Owen, named Elowen, blame him if anything goes wrong
1216
// - Jeff
1317
function calculateRatingChange(
@@ -27,23 +31,26 @@ function calculateRatingChange(
2731
return gMultiplier * (numerator / denominator)
2832
}
2933

30-
// Calculate predicted MMR changes for each team without updating the database
31-
export async function calculatePredictedMMR(
32-
queueId: number,
34+
// Helper function to calculate team statistics and MMR changes
35+
async function calculateTeamStatsAndChanges(
3336
teamResults: teamResults,
3437
winningTeamId: number,
35-
): Promise<Map<string, number>> {
36-
const settings = await getQueueSettings(queueId)
37-
38-
// MMR formula constants
39-
const C_MMR_CHANGE = 25
40-
const V_VARIANCE = 1200
41-
38+
defaultElo: number,
39+
): Promise<{
40+
teamStats: Array<{
41+
team: teamResults['teams'][0]
42+
avgMMR: number
43+
avgVolatility: number
44+
isWinner: boolean
45+
}>
46+
ratingChange: number
47+
loserCount: number
48+
}> {
4249
// Calculate average MMR and volatility for each team
4350
const teamStats = teamResults.teams.map((team) => {
4451
const players = team.players
4552
const avgMMR =
46-
players.reduce((sum, p) => sum + (p.elo ?? settings.default_elo), 0) /
53+
players.reduce((sum, p) => sum + (p.elo ?? defaultElo), 0) /
4754
players.length
4855
const avgVolatility =
4956
players.reduce((sum, p) => sum + (p.volatility ?? 0), 0) / players.length
@@ -60,7 +67,7 @@ export async function calculatePredictedMMR(
6067
const loserStats = teamStats.filter((ts) => !ts.isWinner)
6168

6269
if (!winnerStats || loserStats.length === 0) {
63-
return new Map()
70+
throw new Error('Invalid team stats: no winner or losers found')
6471
}
6572

6673
const avgLoserMMR =
@@ -80,21 +87,46 @@ export async function calculatePredictedMMR(
8087
globalAvgVolatility,
8188
)
8289

83-
// Build map of user_id -> predicted MMR change
84-
const predictions = new Map<string, number>()
90+
return {
91+
teamStats,
92+
ratingChange,
93+
loserCount: loserStats.length,
94+
}
95+
}
96+
97+
// Calculate predicted MMR changes for each team without updating the database
98+
export async function calculatePredictedMMR(
99+
queueId: number,
100+
teamResults: teamResults,
101+
winningTeamId: number,
102+
): Promise<Map<string, number>> {
103+
const settings = await getQueueSettings(queueId)
104+
105+
try {
106+
const { teamStats, ratingChange, loserCount } =
107+
await calculateTeamStatsAndChanges(
108+
teamResults,
109+
winningTeamId,
110+
settings.default_elo,
111+
)
85112

86-
for (const ts of teamStats) {
87-
const isWinner = ts.isWinner
88-
const mmrChange = isWinner
89-
? ratingChange
90-
: -ratingChange / loserStats.length
113+
// Build map of user_id -> predicted MMR change
114+
const predictions = new Map<string, number>()
91115

92-
for (const player of ts.team.players) {
93-
predictions.set(player.user_id, parseFloat(mmrChange.toFixed(1)))
116+
for (const ts of teamStats) {
117+
const isWinner = ts.isWinner
118+
const mmrChange = isWinner ? ratingChange : -ratingChange / loserCount
119+
120+
for (const player of ts.team.players) {
121+
predictions.set(player.user_id, parseFloat(mmrChange.toFixed(1)))
122+
}
94123
}
95-
}
96124

97-
return predictions
125+
return predictions
126+
} catch (err) {
127+
console.error('Error calculating predicted MMR:', err)
128+
return new Map()
129+
}
98130
}
99131

100132
export async function calculateNewMMR(
@@ -106,84 +138,45 @@ export async function calculateNewMMR(
106138
const settings = await getQueueSettings(matchData.queue_id)
107139
const winningTeamId = await getWinningTeamFromMatch(matchId)
108140

109-
// MMR formula constants
110-
const C_MMR_CHANGE = 25 // Base MMR change value
111-
const V_VARIANCE = 1200 // MMR variance
141+
try {
142+
const { teamStats, ratingChange, loserCount } =
143+
await calculateTeamStatsAndChanges(
144+
teamResults,
145+
winningTeamId ?? 0,
146+
settings.default_elo,
147+
)
112148

113-
// Calculate average MMR and volatility for each team
114-
const teamStats = teamResults.teams.map((team) => {
115-
const players = team.players
116-
const avgMMR =
117-
players.reduce((sum, p) => sum + (p.elo ?? settings.default_elo), 0) /
118-
players.length
119-
const avgVolatility =
120-
players.reduce((sum, p) => sum + (p.volatility ?? 0), 0) / players.length
149+
// Apply changes to all teams and players
150+
for (const ts of teamStats) {
151+
const isWinner = ts.isWinner
152+
const mmrChange = isWinner ? ratingChange : -ratingChange / loserCount
121153

122-
return {
123-
team,
124-
avgMMR,
125-
avgVolatility,
126-
isWinner: team.id === winningTeamId,
127-
}
128-
})
154+
for (const player of ts.team.players) {
155+
const oldMMR = player.elo ?? settings.default_elo
156+
const oldVolatility = player.volatility ?? 0
129157

130-
// Find winner and calculate average MMR of all losing teams
131-
const winnerStats = teamStats.find((ts) => ts.isWinner)
132-
const loserStats = teamStats.filter((ts) => !ts.isWinner)
158+
const newMMR = parseFloat((oldMMR + mmrChange).toFixed(1))
159+
const newVolatility = Math.min(oldVolatility + 1, 10)
133160

134-
if (!winnerStats || loserStats.length === 0) {
135-
throw new Error('Unable to determine winner and loser teams')
136-
}
161+
// Update database
162+
await updatePlayerMmrAll(queueId, player.user_id, newMMR, newVolatility)
137163

138-
// Average MMR and volatility of all losing teams
139-
const avgLoserMMR =
140-
loserStats.reduce((sum, ts) => sum + ts.avgMMR, 0) / loserStats.length
141-
const avgLoserVolatility =
142-
loserStats.reduce((sum, ts) => sum + ts.avgVolatility, 0) /
143-
loserStats.length
164+
// Update teamResults object
165+
player.elo = clamp(newMMR, 0, 9999)
166+
player.elo_change = parseFloat(mmrChange.toFixed(1))
167+
player.volatility = newVolatility
144168

145-
// Use overall average volatility for g factor
146-
const globalAvgVolatility =
147-
(winnerStats.avgVolatility + avgLoserVolatility) / 2
169+
// Set user queue role
170+
await setUserQueueRole(queueId, player.user_id)
171+
}
148172

149-
// Calculate rating change using the formula
150-
const ratingChange = calculateRatingChange(
151-
C_MMR_CHANGE,
152-
V_VARIANCE,
153-
avgLoserMMR,
154-
winnerStats.avgMMR,
155-
globalAvgVolatility,
156-
)
157-
158-
// Apply changes to all teams and players
159-
for (const ts of teamStats) {
160-
const isWinner = ts.isWinner
161-
const mmrChange = isWinner
162-
? ratingChange
163-
: -ratingChange / loserStats.length
164-
165-
for (const player of ts.team.players) {
166-
const oldMMR = player.elo ?? settings.default_elo
167-
const oldVolatility = player.volatility ?? 0
168-
169-
const newMMR = parseFloat((oldMMR + mmrChange).toFixed(1))
170-
const newVolatility = Math.min(oldVolatility + 1, 10)
171-
172-
// Update database
173-
await updatePlayerMmrAll(queueId, player.user_id, newMMR, newVolatility)
174-
175-
// Update teamResults object
176-
player.elo = clamp(newMMR, 0, 9999)
177-
player.elo_change = parseFloat(mmrChange.toFixed(1))
178-
player.volatility = newVolatility
179-
180-
// Set user queue role
181-
await setUserQueueRole(queueId, player.user_id)
173+
// Set team score
174+
ts.team.score = isWinner ? 1 : 0
182175
}
183176

184-
// Set team score
185-
ts.team.score = isWinner ? 1 : 0
177+
return teamResults
178+
} catch (err) {
179+
console.error('Error calculating new MMR:', err)
180+
return teamResults
186181
}
187-
188-
return teamResults
189182
}

0 commit comments

Comments
 (0)