diff --git a/match_collector/api.ts b/match_collector/api.ts new file mode 100644 index 0000000..482bad1 --- /dev/null +++ b/match_collector/api.ts @@ -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 } diff --git a/match_collector/champion_stat.ts b/match_collector/champion_stat.ts index b6c292f..a26645f 100644 --- a/match_collector/champion_stat.ts +++ b/match_collector/champion_stat.ts @@ -1,14 +1,7 @@ -function sameArrays(array1: Array, array2: Array) { - 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, array2: Array) { + 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 = [] @@ -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 = [] + 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) { +function handleMatch(match: Match, champions: Map) { 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) { 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) { } // 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 }) } diff --git a/match_collector/item_tree.ts b/match_collector/item_tree.ts index f527b00..5d95fca 100644 --- a/match_collector/item_tree.ts +++ b/match_collector/item_tree.ts @@ -1,15 +1,35 @@ +type GoldAdvantageTag = 'ahead' | 'behind' | 'even' + type ItemTree = { data: number | undefined count: number children: Array + + // 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) { +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 +}