Allow collecting data from EUNE, NA, KR on top of EUW
All checks were successful
pipeline / lint-and-format (push) Successful in 4m44s
pipeline / build-and-push-images (push) Successful in 4m7s

- match_collector: query API and build collections for each platform
- match_collector: aggregate champion stats of each platform in one collection with platform annotations
- frontend: replace stats to count matches in platform-specific collections
- frontend: replace "EUW Challengers" with all supported platforms
- dev: adapted scripts to count match in platforms
This commit is contained in:
2026-04-17 16:25:19 +02:00
parent 0f84b9a707
commit dae65c8fa2
7 changed files with 342 additions and 104 deletions

View File

@@ -218,7 +218,8 @@ function handleMatchBuilds(
match: Match,
participant: Participant,
participantIndex: number,
builds: Builds
builds: Builds,
platform?: string
) {
const timeline: Timeline = match.timeline
@@ -226,7 +227,7 @@ function handleMatchBuilds(
const build = findOrCreateBuild(builds, participant)
build.count += 1
const items: Array<{ itemId: number; goldAdvantage: GoldAdvantageTag }> = []
const items: Array<{ itemId: number; goldAdvantage: GoldAdvantageTag; platform?: string }> = []
for (const frame of timeline.info.frames) {
for (const event of frame.events) {
if (event.participantId != participantIndex) continue
@@ -301,7 +302,7 @@ function handleMatchBuilds(
// Calculate gold advantage at time of purchase
const goldAdvantage = calculateGoldAdvantage(match, frame, participantIndex)
items.push({ itemId: event.itemId, goldAdvantage })
items.push({ itemId: event.itemId, goldAdvantage, platform })
}
}
@@ -312,7 +313,7 @@ function handleMatchBuilds(
}
}
function handleMatch(match: Match, champions: Map<number, ChampionData>) {
function handleMatch(match: Match, champions: Map<number, ChampionData>, platform?: string) {
let participantIndex = 0
for (const participant of match.info.participants) {
participantIndex += 1
@@ -395,17 +396,19 @@ function handleMatch(match: Match, champions: Map<number, ChampionData>) {
}
// Items and runes (builds)
handleMatchBuilds(match, participant, participantIndex, lane.builds)
handleMatchBuilds(match, participant, participantIndex, lane.builds, platform)
}
}
async function handleMatchList(
client: MongoClient,
patch: string,
champions: Map<number, ChampionData>
champions: Map<number, ChampionData>,
platform?: string
) {
const database = client.db('matches')
const matches = database.collection(patch)
const collectionName = platform ? `${patch}_${platform}` : patch
const matches = database.collection(collectionName)
const allMatches = matches.find()
const totalMatches: number = await matches.countDocuments()
@@ -415,7 +418,7 @@ async function handleMatchList(
'\rComputing champion stats, game entry ' + currentMatch + '/' + totalMatches + ' ... '
)
currentMatch += 1
handleMatch(match as unknown as Match, champions)
handleMatch(match as unknown as Match, champions, platform)
}
return totalMatches
@@ -696,7 +699,7 @@ async function championList() {
return list.slice(1)
}
async function makeChampionsStats(client: MongoClient, patch: string) {
async function makeChampionsStats(client: MongoClient, patch: string, platforms: string[] = []) {
const globalItems = await itemList()
for (const item of globalItems) {
itemDict.set(item.id, item)
@@ -705,7 +708,7 @@ async function makeChampionsStats(client: MongoClient, patch: string) {
const list = await championList()
console.log('Generating stats for ' + list.length + ' champions')
// Pre-generate list of champions
// Pre-generate list of champions (shared across all platforms)
const champions: Map<number, ChampionData> = new Map()
for (const champion of list) {
champions.set(champion.id, {
@@ -716,10 +719,18 @@ async function makeChampionsStats(client: MongoClient, patch: string) {
})
}
// Loop through all matches to generate stats
const totalMatches = await handleMatchList(client, patch, champions)
// Process matches from all platforms, merging into the same champions map
let totalMatches = 0
for (const platform of platforms) {
console.log(`\n=== Processing matches from platform: ${platform} ===`)
const platformMatches = await handleMatchList(client, patch, champions, platform)
totalMatches += platformMatches
console.log(`Processed ${platformMatches} matches from ${platform}`)
}
// Finalize and save stats for every champion
console.log(`\n=== Total matches processed: ${totalMatches} ===`)
// Finalize and save stats to a single champions collection
const database = client.db('champions')
const collection = database.collection(patch)
for (const champion of list) {
@@ -729,6 +740,7 @@ async function makeChampionsStats(client: MongoClient, patch: string) {
// Create alias-index for better key-find
await collection.createIndex({ alias: 1 })
console.log(`Stats saved to collection: ${patch}`)
}
export default { makeChampionsStats }

View File

@@ -1,4 +1,3 @@
const base = 'https://euw1.api.riotgames.com'
const api_key = process.env.RIOT_API_KEY
const sleep_minutes = 12
@@ -7,6 +6,22 @@ import { MongoClient } from 'mongodb'
import champion_stat from './champion_stat'
import { Match } from './api'
// Region configuration: platform -> regional routing value
const PLATFORMS: Record<string, string> = {
EUW1: 'EUROPE',
EUN1: 'EUROPE',
NA1: 'AMERICAS',
KR: 'ASIA'
}
function getPlatformBaseUrl(platform: string): string {
return `https://${platform.toLowerCase()}.api.riotgames.com`
}
function getRegionalBaseUrl(region: string): string {
return `https://${region.toLowerCase()}.api.riotgames.com`
}
main()
async function main() {
@@ -25,42 +40,51 @@ async function main() {
'Connected to database, latest patch ' + latestPatch + ' was epoch: ' + latestPatchTime
)
const alreadySeenGameList = await alreadySeenGames(client, latestPatch)
console.log('We already have ' + alreadySeenGameList.length + ' matches for this patch !')
console.log('Using RIOT_API_KEY: ' + api_key)
if (api_key != null && api_key != undefined && api_key != '') {
const challengerLeague = await fetchChallengerLeague()
console.log('ChallengerLeague: got ' + challengerLeague.entries.length + ' entries')
// Iterate through all platforms
for (const [platform, region] of Object.entries(PLATFORMS)) {
console.log(`\n=== Processing platform: ${platform} (region: ${region}) ===`)
const gameList : string[] = []
let i = 0
for (const challenger of challengerLeague.entries) {
console.log('Entry ' + i + '/' + challengerLeague.entries.length + ' ...')
const puuid = challenger.puuid
const challengerGameList = await summonerGameList(puuid, latestPatchTime)
for (const game of challengerGameList) {
if (!gameList.includes(game) && !alreadySeenGameList.includes(game)) {
gameList.push(game)
const alreadySeenGameList = await alreadySeenGames(client, latestPatch, platform)
console.log(
'We already have ' + alreadySeenGameList.length + ' matches for this patch/platform !'
)
const challengerLeague = await fetchChallengerLeague(platform)
console.log('ChallengerLeague: got ' + challengerLeague.entries.length + ' entries')
const gameList: string[] = []
let i = 0
for (const challenger of challengerLeague.entries) {
console.log('Entry ' + i + '/' + challengerLeague.entries.length + ' ...')
const puuid = challenger.puuid
const challengerGameList = await summonerGameList(puuid, latestPatchTime, region)
for (const game of challengerGameList) {
if (!gameList.includes(game) && !alreadySeenGameList.includes(game)) {
gameList.push(game)
}
}
i++
}
i++
}
console.log('Games: got ' + gameList.length + ' entries')
i = 0
for (const game of gameList) {
console.log('Entry ' + i + '/' + gameList.length + ' ...')
const gameMatch = await match(game)
const gameTimeline = await matchTimeline(game)
gameMatch.timeline = gameTimeline
await saveMatch(client, gameMatch, latestPatch)
i++
console.log('Games: got ' + gameList.length + ' entries for ' + platform)
i = 0
for (const game of gameList) {
console.log('Entry ' + i + '/' + gameList.length + ' ...')
// Determine region from matchId (format: REGION_matchId)
const matchRegion = game.split('_')[0]
const gameMatch = await match(game, matchRegion)
const gameTimeline = await matchTimeline(game, matchRegion)
gameMatch.timeline = gameTimeline
await saveMatch(client, gameMatch, latestPatch, platform)
i++
}
}
}
console.log('Generating stats...')
await champion_stat.makeChampionsStats(client, latestPatch)
await champion_stat.makeChampionsStats(client, latestPatch, Object.keys(PLATFORMS))
console.log('All done. Closing client.')
await client.close()
@@ -113,10 +137,11 @@ async function fetchLatestPatchDate(client: MongoClient) {
return [latestPatch!.patch, Math.floor(latestPatch!.date.valueOf() / 1000)]
}
async function fetchChallengerLeague() {
async function fetchChallengerLeague(platform: string) {
const queue = 'RANKED_SOLO_5x5'
const endpoint = `/lol/league/v4/challengerleagues/by-queue/${queue}`
const url = `${base}${endpoint}?api_key=${api_key}`
const baseUrl = getPlatformBaseUrl(platform)
const url = `${baseUrl}${endpoint}?api_key=${api_key}`
const challengerLeagueResponse = await handleRateLimit(new URL(url))
@@ -126,10 +151,10 @@ async function fetchChallengerLeague() {
return challengerLeague
}
async function summonerGameList(puuid: string, startTime: string) {
const base = 'https://europe.api.riotgames.com'
async function summonerGameList(puuid: string, startTime: string, region: string) {
const baseUrl = getRegionalBaseUrl(region)
const endpoint = `/lol/match/v5/matches/by-puuid/${puuid}/ids`
const url = `${base}${endpoint}?queue=420&type=ranked&startTime=${startTime}&api_key=${api_key}`
const url = `${baseUrl}${endpoint}?queue=420&type=ranked&startTime=${startTime}&api_key=${api_key}`
const gameListResponse = await handleRateLimit(new URL(url))
handleError(gameListResponse)
@@ -138,10 +163,10 @@ async function summonerGameList(puuid: string, startTime: string) {
return gameList
}
async function match(matchId: string) {
const base = 'https://europe.api.riotgames.com'
async function match(matchId: string, region: string) {
const baseUrl = getRegionalBaseUrl(region)
const endpoint = `/lol/match/v5/matches/${matchId}`
const url = `${base}${endpoint}?api_key=${api_key}`
const url = `${baseUrl}${endpoint}?api_key=${api_key}`
const matchResponse = await handleRateLimit(new URL(url))
handleError(matchResponse)
@@ -150,10 +175,10 @@ async function match(matchId: string) {
return match
}
async function matchTimeline(matchId: string) {
const base = 'https://europe.api.riotgames.com'
async function matchTimeline(matchId: string, region: string) {
const baseUrl = getRegionalBaseUrl(region)
const endpoint = `/lol/match/v5/matches/${matchId}/timeline`
const url = `${base}${endpoint}?api_key=${api_key}`
const url = `${baseUrl}${endpoint}?api_key=${api_key}`
const timelineResponse = await handleRateLimit(new URL(url))
handleError(timelineResponse)
@@ -162,17 +187,19 @@ async function matchTimeline(matchId: string) {
return timeline
}
async function alreadySeenGames(client: MongoClient, latestPatch: string) {
async function alreadySeenGames(client: MongoClient, latestPatch: string, platform: string) {
const database = client.db('matches')
const matches = database.collection(latestPatch)
const collectionName = `${latestPatch}_${platform}`
const matches = database.collection(collectionName)
const alreadySeen = await matches.distinct('metadata.matchId')
return alreadySeen
}
async function saveMatch(client: MongoClient, match: Match, latestPatch: string) {
async function saveMatch(client: MongoClient, match: Match, latestPatch: string, platform: string) {
const database = client.db('matches')
const matches = database.collection(latestPatch)
const collectionName = `${latestPatch}_${platform}`
const matches = database.collection(collectionName)
await matches.insertOne(match)
}
@@ -187,10 +214,15 @@ async function runWithPreloadedData() {
const [latestPatch] = await fetchLatestPatchDate(client)
console.log(`Latest patch: ${latestPatch}`)
// Check if we have matches for this patch
// Check if we have matches for this patch (including platform-specific collections)
const matchesDb = client.db('matches')
const collections = await matchesDb.listCollections().toArray()
const patchCollections = collections.map(c => c.name).filter(name => name === latestPatch)
const collectionNames = collections.map(c => c.name)
// Find collections for this patch (both global and platform-specific)
const patchCollections = collectionNames.filter(
name => name === latestPatch || name.startsWith(`${latestPatch}_`)
)
if (patchCollections.length === 0) {
console.error(`❌ No match data found for patch ${latestPatch}`)
@@ -199,13 +231,21 @@ async function runWithPreloadedData() {
return
}
console.log(`Found ${patchCollections.length} match collection(s)`)
console.log(
`Found ${patchCollections.length} match collection(s): ${patchCollections.join(', ')}`
)
// Generate stats for each patch with data
for (const patch of patchCollections) {
console.log(`Generating stats for patch ${patch}...`)
await champion_stat.makeChampionsStats(client, patch)
console.log(`Stats generated for patch ${patch}`)
// Extract platforms from collection names (e.g., "15.1_EUW1" -> "EUW1")
const platforms = patchCollections
.filter(name => name.startsWith(`${latestPatch}_`))
.map(name => name.replace(`${latestPatch}_`, ''))
// Generate stats for each platform
if (platforms.length > 0) {
await champion_stat.makeChampionsStats(client, latestPatch, platforms)
} else {
// Fallback for old-style collections without platform suffix
await champion_stat.makeChampionsStats(client, latestPatch)
}
console.log('🎉 All stats generated successfully!')

View File

@@ -1,5 +1,12 @@
type GoldAdvantageTag = 'ahead' | 'behind' | 'even'
type PlatformCounts = {
euw: number
eun: number
na: number
kr: number
}
type ItemTree = {
data: number | undefined
count: number
@@ -12,6 +19,13 @@ type ItemTree = {
evenCount: number
meanGold: number
}
// Platform tracking
platformCount: PlatformCounts
}
function initPlatformCounts(): PlatformCounts {
return { euw: 0, eun: 0, na: 0, kr: 0 }
}
function treeInit(): ItemTree {
@@ -19,16 +33,8 @@ function treeInit(): ItemTree {
data: undefined,
count: 0,
children: [],
boughtWhen: { aheadCount: 0, behindCount: 0, evenCount: 0, meanGold: 0 }
}
}
function treeNode(data: number, count: number): ItemTree {
return {
data: data,
count: count,
children: [],
boughtWhen: { aheadCount: 0, behindCount: 0, evenCount: 0, meanGold: 0 }
boughtWhen: { aheadCount: 0, behindCount: 0, evenCount: 0, meanGold: 0 },
platformCount: initPlatformCounts()
}
}
@@ -49,6 +55,12 @@ function nodeMerge(itemtree: ItemTree, node: ItemTree) {
child.boughtWhen.evenCount += node.boughtWhen.evenCount
child.boughtWhen.behindCount += node.boughtWhen.behindCount
// Merge platform counts
child.platformCount.euw += node.platformCount.euw
child.platformCount.eun += node.platformCount.eun
child.platformCount.na += node.platformCount.na
child.platformCount.kr += node.platformCount.kr
next = child
break
}
@@ -56,7 +68,13 @@ function nodeMerge(itemtree: ItemTree, node: ItemTree) {
// If not found, add item node at this level
if (next == null && item !== undefined) {
next = treeNode(item, count)
next = {
data: item,
count: count,
children: [],
boughtWhen: { ...node.boughtWhen },
platformCount: { ...node.platformCount }
}
itemtree.children.push(next)
}
@@ -68,11 +86,12 @@ function nodeMerge(itemtree: ItemTree, node: ItemTree) {
*/
function treeMerge(
itemtree: ItemTree,
items: Array<{ itemId: number; goldAdvantage: GoldAdvantageTag }>
items: Array<{ itemId: number; goldAdvantage: GoldAdvantageTag; platform?: string }>
) {
let current = itemtree
for (const item of items) {
const platformKey = item.platform ? item.platform.toLowerCase() : null
current = nodeMerge(current, {
data: item.itemId,
count: 1,
@@ -82,7 +101,13 @@ function treeMerge(
behindCount: item.goldAdvantage == 'behind' ? 1 : 0,
meanGold: 0
},
children: []
children: [],
platformCount: {
euw: platformKey === 'euw1' ? 1 : 0,
eun: platformKey === 'eun1' ? 1 : 0,
na: platformKey === 'na1' ? 1 : 0,
kr: platformKey === 'kr' ? 1 : 0
}
})
}
}
@@ -96,7 +121,8 @@ function treeCutBranches(itemtree: ItemTree, thresholdCount: number, thresholdPe
data: undefined,
count: +Infinity,
children: [],
boughtWhen: { aheadCount: 0, behindCount: 0, evenCount: 0, meanGold: 0 }
boughtWhen: { aheadCount: 0, behindCount: 0, evenCount: 0, meanGold: 0 },
platformCount: initPlatformCounts()
}
)
itemtree.children.splice(itemtree.children.indexOf(leastUsedBranch), 1)
@@ -137,6 +163,12 @@ function treeClone(tree: ItemTree): ItemTree {
behindCount: tree.boughtWhen.behindCount,
evenCount: tree.boughtWhen.evenCount,
meanGold: tree.boughtWhen.meanGold
},
platformCount: {
euw: tree.platformCount.euw,
eun: tree.platformCount.eun,
na: tree.platformCount.na,
kr: tree.platformCount.kr
}
}
}
@@ -148,6 +180,17 @@ function treeMergeTree(t1: ItemTree, t2: ItemTree): ItemTree {
// Merge counts for the root
t1.count += t2.count
// Merge platform counts
t1.platformCount.euw += t2.platformCount.euw
t1.platformCount.eun += t2.platformCount.eun
t1.platformCount.na += t2.platformCount.na
t1.platformCount.kr += t2.platformCount.kr
// Merge boughtWhen
t1.boughtWhen.aheadCount += t2.boughtWhen.aheadCount
t1.boughtWhen.evenCount += t2.boughtWhen.evenCount
t1.boughtWhen.behindCount += t2.boughtWhen.behindCount
// Merge children from t2 into t1
for (const child2 of t2.children) {
// Find matching child in t1 (same data value)
@@ -225,6 +268,7 @@ function areTreeSimilars(t1: ItemTree, t2: ItemTree): number {
export {
ItemTree,
PlatformCounts,
GoldAdvantageTag,
treeMerge,
treeInit,