match_collector: track gold advantage when items are bought

also add api.ts with Riot API types
This commit is contained in:
2026-04-17 10:51:21 +02:00
parent 19a9226dac
commit b7435f0884
3 changed files with 275 additions and 43 deletions

125
match_collector/api.ts Normal file
View File

@@ -0,0 +1,125 @@
type Match = {
metadata: {
dataVersion: string
matchId: string
participants: string[]
}
info: {
endOfGameResult: string
frameInterval: number
gameId: number
participants: Participant[]
teams: Team[]
}
timeline: Timeline
}
type Timeline = {
metadata: {
dataVersion: string
matchId: string
participants: string[]
}
info: {
endOfGameResult: string
frameInterval: number
gameId: number
participants: {
participantId: number
puuid: string
}[]
frames: Frame[]
}
}
type Team = {
bans: Ban[]
// eslint-disable-next-line @typescript-eslint/no-explicit-any
objectives: any
teamId: number
win: boolean
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Ban = any
type Participant = {
allInPing: number
assistMePings: number
assists: number
baronKills: number
bountyLevel: number
champExperience: number
champLevel: number
championId: number
championName: string
commandPings: number
championTransform: number
consumablesPurchased: number
// eslint-disable-next-line @typescript-eslint/no-explicit-any
challenges: any
damageDealtToBuildings: number
deaths: number
item0: number
item1: number
item2: number
item3: number
item4: number
item5: number
item6: number
itemsPurchased: number
kills: number
lane: string
participantId: number
// eslint-disable-next-line @typescript-eslint/no-explicit-any
perks: any
puuid: string
summoner1Id: number
summoner2Id: number
summonerId: string
teamId: number
teamPosition: string
win: boolean
}
type Frame = {
events: Event[]
participantFrames: {
'1': ParticipantFrame
'2': ParticipantFrame
'3': ParticipantFrame
'4': ParticipantFrame
'5': ParticipantFrame
'6': ParticipantFrame
'7': ParticipantFrame
'8': ParticipantFrame
'9': ParticipantFrame
'10': ParticipantFrame
}
timestamp: number
}
type ParticipantFrame = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
championStats: any
currentGold: number
// eslint-disable-next-line @typescript-eslint/no-explicit-any
damageStats: any
goldPerSecond: number
jungleMinionsKilled: number
level: number
minionsKilled: number
participantId: number
position: {
x: number
y: number
}
timeEnemySpentControlled: number
totalGold: number
xp: number
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type Event = any
export { Match, Timeline, Team, Ban, Participant, Frame, Event }

View File

@@ -1,14 +1,7 @@
function sameArrays(array1: Array<number>, array2: Array<number>) {
if (array1.length != array2.length) return false
for (const e of array1) {
if (!array2.includes(e)) return false
}
return true
}
import { MongoClient } from 'mongodb'
import {
ItemTree,
GoldAdvantageTag,
treeInit,
treeMerge,
treeCutBranches,
@@ -16,6 +9,16 @@ import {
treeMergeTree,
areTreeSimilars
} from './item_tree'
import { Match, Timeline, Participant, Frame } from './api'
function sameArrays(array1: Array<number>, array2: Array<number>) {
if (array1.length != array2.length) return false
for (const e of array1) {
if (!array2.includes(e)) return false
}
return true
}
const itemDict = new Map()
async function itemList() {
@@ -107,8 +110,7 @@ type ChampionData = {
}
// Helper function to create rune configuration from participant
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function createRuneConfiguration(participant: any): Rune {
function createRuneConfiguration(participant: Participant): Rune {
const primaryStyle = participant.perks.styles[0].style
const secondaryStyle = participant.perks.styles[1].style
const selections: Array<number> = []
@@ -126,8 +128,7 @@ function createRuneConfiguration(participant: any): Rune {
}
// Find or create a build for the given rune keystone
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function findOrCreateBuild(builds: Builds, participant: any): Build {
function findOrCreateBuild(builds: Builds, participant: Participant): Build {
const keystone = participant.perks.styles[0].selections[0].perk
const runeConfig = createRuneConfiguration(participant)
@@ -166,24 +167,71 @@ function findOrCreateBuild(builds: Builds, participant: any): Build {
return newBuild
}
// Calculate gold advantage at the time of item purchase
// Returns 'ahead', 'behind', or 'even' based on gold difference
function calculateGoldAdvantage(
match: Match,
frame: Frame,
participantIndex: number
): GoldAdvantageTag {
const GOLD_THRESHOLD = 1000 // 1000 gold difference threshold
const participantFrames = [
frame.participantFrames[1],
frame.participantFrames[2],
frame.participantFrames[3],
frame.participantFrames[4],
frame.participantFrames[5],
frame.participantFrames[6],
frame.participantFrames[7],
frame.participantFrames[8],
frame.participantFrames[9],
frame.participantFrames[10]
]
// Find the participant's team
const participantFrame = participantFrames[participantIndex - 1]
const participantGold = participantFrame.totalGold
if (!participantFrame) return 'even'
const participant = match.info.participants.find(
x => x.participantId == participantFrame.participantId
)!
const opponent = match.info.participants.find(
x => x.teamPosition === participant.teamPosition && x.teamId != participant.teamId
)
if (opponent == undefined) return 'even'
const opponentGold = participantFrames.find(
x => x.participantId == opponent.participantId
)!.totalGold
const goldDiff = participantGold - opponentGold
if (goldDiff >= GOLD_THRESHOLD) return 'ahead'
if (goldDiff <= -GOLD_THRESHOLD) return 'behind'
return 'even'
}
function handleMatchBuilds(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
timeline: any,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
participant: any,
match: Match,
participant: Participant,
participantIndex: number,
builds: Builds
) {
const timeline: Timeline = match.timeline
// Find or create the build for this participant's rune configuration
const build = findOrCreateBuild(builds, participant)
build.count += 1
const items: Array<number> = []
const items: Array<{ itemId: number; goldAdvantage: GoldAdvantageTag }> = []
for (const frame of timeline.info.frames) {
for (const event of frame.events) {
if (event.participantId != participantIndex) continue
if (event.type == 'ITEM_UNDO') {
if (items.length > 0 && items[items.length - 1] == event.beforeId) {
if (items.length > 0 && items[items.length - 1].itemId == event.beforeId) {
items.pop()
}
continue
@@ -251,7 +299,9 @@ function handleMatchBuilds(
if (itemInfo.to.length != 0 && (items.length >= 1 || participant.teamPosition === 'UTILITY'))
continue
items.push(event.itemId)
// Calculate gold advantage at time of purchase
const goldAdvantage = calculateGoldAdvantage(match, frame, participantIndex)
items.push({ itemId: event.itemId, goldAdvantage })
}
}
@@ -262,13 +312,12 @@ function handleMatchBuilds(
}
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function handleMatch(match: any, champions: Map<number, ChampionData>) {
function handleMatch(match: Match, champions: Map<number, ChampionData>) {
let participantIndex = 0
for (const participant of match.info.participants) {
participantIndex += 1
const championId = participant.championId
const champion = champions.get(championId)
const champion = champions.get(championId)!
// Lanes
let lane = champion.lanes.find(x => x.data == participant.teamPosition)
@@ -333,7 +382,7 @@ function handleMatch(match: any, champions: Map<number, ChampionData>) {
matchup.winrate = (matchup.winrate * (matchup.games - 1)) / matchup.games
}
} else {
const opponentChampion = champions.get(opponentChampionId)
const opponentChampion = champions.get(opponentChampionId)!
lane.matchups.push({
championId: opponentChampionId,
@@ -346,7 +395,7 @@ function handleMatch(match: any, champions: Map<number, ChampionData>) {
}
// Items and runes (builds)
handleMatchBuilds(match.timeline, participant, participantIndex, lane.builds)
handleMatchBuilds(match, participant, participantIndex, lane.builds)
}
}
@@ -366,7 +415,7 @@ async function handleMatchList(
'\rComputing champion stats, game entry ' + currentMatch + '/' + totalMatches + ' ... '
)
currentMatch += 1
handleMatch(match, champions)
handleMatch(match as unknown as Match, champions)
}
return totalMatches
@@ -389,10 +438,10 @@ function splitMergeOnStarterItem(build: Build, championName: string): BuildWithS
) {
const startItems = []
let items = build.items.children[0]
startItems.push({ data: build.items.children[0].data, count: build.items.children[0].count })
startItems.push({ data: build.items.children[0].data!, count: build.items.children[0].count })
build.items.children[0].data = undefined
if (build.items.children.length > 1) {
startItems.push({ data: build.items.children[1].data, count: build.items.children[1].count })
startItems.push({ data: build.items.children[1].data!, count: build.items.children[1].count })
build.items.children[1].data = undefined
items = treeMergeTree(build.items.children[0], build.items.children[1])
}
@@ -420,7 +469,7 @@ function splitMergeOnStarterItem(build: Build, championName: string): BuildWithS
items: c,
bootsFirstCount: build.bootsFirstCount,
count: c.count,
startItems: [{ data: c.data, count: c.count }],
startItems: [{ data: c.data!, count: c.count }],
suppItems: build.suppItems,
boots: build.boots
})
@@ -567,7 +616,7 @@ async function finalizeChampionStats(champion: ChampionData, totalMatches: numbe
// Summoner spells
lane.summonerSpells.forEach(x => (x.pickrate = x.count / lane.count))
lane.summonerSpells.sort((a, b) => b.count - a.count)
lane.summonerSpells = lane.summonerSpells.filter(x => x.pickrate >= 0.05)
lane.summonerSpells = lane.summonerSpells.filter(x => x.pickrate! >= 0.05)
// Cleaning up builds
cleanupLaneBuilds(lane)
@@ -674,7 +723,7 @@ async function makeChampionsStats(client: MongoClient, patch: string) {
const database = client.db('champions')
const collection = database.collection(patch)
for (const champion of list) {
const championInfo = await finalizeChampionStats(champions.get(champion.id), totalMatches)
const championInfo = await finalizeChampionStats(champions.get(champion.id)!, totalMatches)
await collection.updateOne({ id: champion.id }, { $set: championInfo }, { upsert: true })
}

View File

@@ -1,15 +1,35 @@
type GoldAdvantageTag = 'ahead' | 'behind' | 'even'
type ItemTree = {
data: number | undefined
count: number
children: Array<ItemTree>
// Gold advantage tracking
boughtWhen: {
aheadCount: number
behindCount: number
evenCount: number
meanGold: number
}
}
function treeInit(): ItemTree {
return { data: undefined, count: 0, children: [] }
return {
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: [] }
return {
data: data,
count: count,
children: [],
boughtWhen: { aheadCount: 0, behindCount: 0, evenCount: 0, meanGold: 0 }
}
}
/*
@@ -21,31 +41,49 @@ function nodeMerge(itemtree: ItemTree, node: ItemTree) {
let next: ItemTree | null = null
// Try to find an existing node in this tree level with same item
for (const node of itemtree.children) {
if (node.data == item) {
node.count += 1
next = node
for (const child of itemtree.children) {
if (child.data == item) {
child.count += 1
child.boughtWhen.aheadCount += node.boughtWhen.aheadCount
child.boughtWhen.evenCount += node.boughtWhen.evenCount
child.boughtWhen.behindCount += node.boughtWhen.behindCount
next = child
break
}
}
// If not found, add item node at this level
if (next == null) {
if (next == null && item !== undefined) {
next = treeNode(item, count)
itemtree.children.push(next)
}
return next
return next!
}
/*
* Merge a full build path with an existing item tree
*/
function treeMerge(itemtree: ItemTree, items: Array<number>) {
function treeMerge(
itemtree: ItemTree,
items: Array<{ itemId: number; goldAdvantage: GoldAdvantageTag }>
) {
let current = itemtree
for (const item of items) {
current = nodeMerge(current, { data: item, count: 1, children: [] })
current = nodeMerge(current, {
data: item.itemId,
count: 1,
boughtWhen: {
aheadCount: item.goldAdvantage == 'ahead' ? 1 : 0,
evenCount: item.goldAdvantage == 'even' ? 1 : 0,
behindCount: item.goldAdvantage == 'behind' ? 1 : 0,
meanGold: 0
},
children: []
})
}
}
@@ -54,7 +92,12 @@ function treeCutBranches(itemtree: ItemTree, thresholdCount: number, thresholdPe
while (itemtree.children.length > thresholdCount) {
const leastUsedBranch = itemtree.children.reduce(
(a, b) => (Math.min(a.count, b.count) == a.count ? a : b),
{ data: undefined, count: +Infinity, children: [] }
{
data: undefined,
count: +Infinity,
children: [],
boughtWhen: { aheadCount: 0, behindCount: 0, evenCount: 0, meanGold: 0 }
}
)
itemtree.children.splice(itemtree.children.indexOf(leastUsedBranch), 1)
}
@@ -88,7 +131,13 @@ function treeClone(tree: ItemTree): ItemTree {
return {
data: tree.data,
count: tree.count,
children: tree.children.map(child => treeClone(child))
children: tree.children.map(child => treeClone(child)),
boughtWhen: {
aheadCount: tree.boughtWhen.aheadCount,
behindCount: tree.boughtWhen.behindCount,
evenCount: tree.boughtWhen.evenCount,
meanGold: tree.boughtWhen.meanGold
}
}
}
@@ -174,4 +223,13 @@ function areTreeSimilars(t1: ItemTree, t2: ItemTree): number {
return Math.max(0, Math.min(1, similarity))
}
export { ItemTree, treeMerge, treeInit, treeCutBranches, treeSort, treeMergeTree, areTreeSimilars }
export {
ItemTree,
GoldAdvantageTag,
treeMerge,
treeInit,
treeCutBranches,
treeSort,
treeMergeTree,
areTreeSimilars
}