Files
buildpath/match_collector/champion_stat.ts
vhaudiquet 8f9638ced5
All checks were successful
pipeline / build-and-push-images (push) Successful in 11s
pipeline / deploy (push) Successful in 6s
Big champion stats optim :)
2024-12-05 18:20:38 +01:00

328 lines
11 KiB
TypeScript

function sameArrays(array1 : Array<any>, array2 : Array<any>) {
if(array1.length != array2.length) return false;
for(let e of array1) {
if(!array2.includes(e)) return false;
}
return true;
}
import { MongoClient } from "mongodb";
import { ItemTree, treeInit, treeMerge, treeCutBranches, treeSort } from "./item_tree";
const itemDict = new Map()
async function itemList() {
const response = await fetch("https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/v1/items.json")
const list = await response.json()
return list
}
function arrayRemovePercentage(array: Array<{count:number}>, totalGames:number, percentage: number) {
let toRemove : Array<{count:number}> = []
for(let item of array) {
if((item.count/totalGames) < percentage) {
toRemove.push(item)
}
}
for(let tr of toRemove) {
array.splice(array.indexOf(tr), 1)
}
}
type Rune = {
count: number
primaryStyle: number
secondaryStyle: number
selections: Array<number>
pickrate?: number
};
type Builds = {
tree: ItemTree
start: Array<{data: number, count: number}>
bootsFirst: number
boots: Array<{data: number, count: number}>
lateGame: Array<{data: number, count: number}>
suppItems?: Array<{data: number, count: number}>
}
type Champion = {
id: Number
name: String
alias: String
}
type LaneData = {
data: string
count: number
winningMatches: number
losingMatches: number
winrate: number
pickrate: number
runes: Array<Rune>
builds: Builds
}
type ChampionData = {
champion: Champion
winningMatches: number
losingMatches: number
lanes: Array<LaneData>
}
function handleParticipantRunes(participant, runes: Array<Rune>) {
const primaryStyle = participant.perks.styles[0].style
const secondaryStyle = participant.perks.styles[1].style
const selections : Array<number> = []
for(let style of participant.perks.styles) {
for(let perk of style.selections) {
selections.push(perk.perk)
}
}
const gameRunes : Rune = {count:1, primaryStyle: primaryStyle, secondaryStyle: secondaryStyle, selections: selections};
let addRunes = true;
for(let rune of runes) {
if(rune.primaryStyle == gameRunes.primaryStyle
&& rune.secondaryStyle == gameRunes.secondaryStyle
&& sameArrays(rune.selections, gameRunes.selections)) {
rune.count++; addRunes = false; break;
}
}
if(addRunes) runes.push(gameRunes)
}
function handleMatchItems(timeline, participant: any, participantIndex : number, builds: Builds) {
const items : Array<number> = []
for(let frame of timeline.info.frames) {
for(let event of frame.events) {
if(event.participantId != participantIndex) continue;
if(event.type == "ITEM_UNDO") {
if(items.length > 0 && items[items.length - 1] == event.beforeId) {
items.pop()
}
continue;
}
let itemInfo = itemDict.get(event.itemId)
// Handle bounty of worlds destroy as upgrade
if(event.type == "ITEM_DESTROYED") {
if(event.itemId == 3867) {
let suppItem : number = itemInfo.to.find((x:number) =>
x == participant.item0
|| x == participant.item1
|| x == participant.item2
|| x == participant.item3
|| x == participant.item4
|| x == participant.item5
|| x == participant.item6 )
if(suppItem != undefined) {
const already = builds.suppItems.find((x) => x.data == suppItem)
if(already == undefined) builds.suppItems.push({count:1, data: suppItem})
else already.count += 1
}
}
}
if(event.type != "ITEM_PURCHASED") continue;
// Handle boots differently
if(itemInfo.categories.includes("Boots")){
if(itemInfo.to.length == 0 || event.itemId == 3006) {
// Check for bootsFirst
if(items.length < 2) {
builds.bootsFirst += 1
}
// Add to boots
const already = builds.boots.find((x) => x.data == event.itemId)
if(already == undefined) builds.boots.push({count:1, data:event.itemId})
else already.count += 1
}
continue;
}
// Check if item should be included
if(itemInfo.categories.includes("Consumable")) continue;
if(itemInfo.categories.includes("Trinket")) continue;
// Ignore Cull as not-first item
if(event.itemId == 1083 && items.length >= 1) continue;
// Ignore non-final items, except when first item bought
if(itemInfo.to.length != 0 && items.length >= 1) continue;
items.push(event.itemId)
}
}
// Core items
treeMerge(builds.tree, items.slice(1, 4))
// Start items
if(items.length >= 1) {
const already = builds.start.find((x) => x.data == items[0])
if(already == undefined) builds.start.push({count:1, data:items[0]})
else already.count += 1
}
// Late game items
for(let item of items.slice(4)) {
const already = builds.lateGame.find((x) => x.data == item)
if(already == undefined) builds.lateGame.push({count:1, data:item})
else already.count += 1
}
}
function handleMatch(match: any, champions : Map<number, ChampionData>) {
let participantIndex = 0;
for(let participant of match.info.participants) {
participantIndex += 1
const championId = participant.championId
const champion = champions.get(championId)
// Lanes
let lane = champion.lanes.find((x) => x.data == participant.teamPosition)
if(lane == undefined) {
const builds : Builds = {tree:treeInit(), start: [], bootsFirst: 0, boots: [], lateGame: [], suppItems: []}
lane = {count:1, data: participant.teamPosition, runes:[], builds:builds, winningMatches: 0, losingMatches: 0, winrate: 0, pickrate: 0}
champion.lanes.push(lane)
}
else lane.count += 1
// Winrate
if(participant.win) {
champion.winningMatches++;
lane.winningMatches++;
}
else {
champion.losingMatches++;
lane.losingMatches++;
}
// Runes
handleParticipantRunes(participant, lane.runes)
// Items
handleMatchItems(match.timeline, participant, participantIndex, lane.builds)
}
}
async function handleMatchList(client: MongoClient, patch: string, champions: Map<number, ChampionData>) {
const database = client.db("matches");
const matches = database.collection(patch)
const allMatches = matches.find()
const totalMatches: number = await matches.countDocuments()
let currentMatch = 0;
for await (let match of allMatches) {
console.log("Computing champion stats, game entry " + currentMatch + "/" + totalMatches + " ...")
currentMatch += 1;
handleMatch(match, champions)
}
return totalMatches
}
async function finalizeChampionStats(champion: ChampionData, totalMatches: number) {
let totalChampionMatches = champion.winningMatches + champion.losingMatches;
arrayRemovePercentage(champion.lanes, totalChampionMatches, 0.2)
champion.lanes.sort((a, b) => b.count - a.count)
// Filter runes to keep 3 most played
for(let lane of champion.lanes) {
const runes = lane.runes
runes.sort((a, b) => b.count - a.count)
if(runes.length > 3)
runes.splice(3, runes.length - 3)
// Compute runes pickrate
for(let rune of runes)
rune.pickrate = rune.count / lane.count;
}
for(let lane of champion.lanes) {
const builds = lane.builds
// Cut item tree branches to keep only 4 branches every time and with percentage threshold
builds.tree.count = lane.count;
treeCutBranches(builds.tree, 4, 0.05)
treeSort(builds.tree)
// Cut item start, to only 4 and with percentage threshold
arrayRemovePercentage(builds.start, lane.count, 0.05)
builds.start.sort((a, b) => b.count - a.count)
if(builds.start.length > 4)
builds.start.splice(4, builds.start.length - 4)
// Remove boots that are not within percentage threshold
arrayRemovePercentage(builds.boots, lane.count, 0.05)
builds.boots.sort((a, b) => b.count - a.count)
builds.bootsFirst /= lane.count
// Cut supp items below 2 and percentage threshold
arrayRemovePercentage(builds.suppItems, lane.count, 0.05)
builds.suppItems.sort((a, b) => b.count - a.count)
if(builds.suppItems.length > 2)
builds.suppItems.splice(2, builds.suppItems.length - 2)
// Delete supp items if empty
if(builds.suppItems.length == 0) delete builds.suppItems
builds.lateGame.sort((a, b) => b.count - a.count)
}
for(let lane of champion.lanes) {
lane.winrate = lane.winningMatches / lane.count
lane.pickrate = lane.count / totalMatches
}
return {name: champion.champion.name,
alias: champion.champion.alias.toLowerCase(),
id: champion.champion.id,
lanes: champion.lanes,
winrate: champion.winningMatches / totalChampionMatches,
gameCount: totalChampionMatches,
pickrate: totalChampionMatches/totalMatches,
};
}
async function championList() {
const response = await fetch("https://raw.communitydragon.org/latest/plugins/rcp-be-lol-game-data/global/default/v1/champion-summary.json");
const list = await response.json()
return list.slice(1)
}
async function makeChampionsStats(client: MongoClient, patch : string) {
var globalItems = await itemList()
for(let item of globalItems) {
itemDict.set(item.id, item)
}
const list = await championList()
console.log("Generating stats for " + list.length + " champions")
// Pre-generate list of champions
const champions: Map<number, ChampionData> = new Map()
for(let champion of list) {
champions.set(champion.id, {
champion: {id: champion.id, name: champion.name, alias: champion.alias},
winningMatches: 0,
losingMatches: 0,
lanes: []
})
}
// Loop through all matches to generate stats
const totalMatches = await handleMatchList(client, patch, champions)
// Finalize and save stats for every champion
const database = client.db("champions")
const collection = database.collection(patch)
for(let champion of list) {
const championInfo = await finalizeChampionStats(champions.get(champion.id), totalMatches)
await collection.updateOne({id: champion.id}, {$set: championInfo}, { upsert: true })
}
// Create alias-index for better key-find
await collection.createIndex({alias:1})
}
export default {makeChampionsStats}