Initial commit

This commit is contained in:
2024-11-21 19:39:22 +01:00
commit 56d1075459
32 changed files with 10966 additions and 0 deletions

1
match_collector/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
node_modules

View File

@@ -0,0 +1,8 @@
FROM node:lts-alpine
RUN mkdir -p /home/node/app/node_modules && chown -R node:node /home/node/app
WORKDIR /home/node/app
USER node
COPY --chown=node:node package*.json ./
RUN npm install
COPY --chown=node:node . .
CMD [ "node", "index.js" ]

View File

@@ -0,0 +1,105 @@
function sameArrays(array1, array2) {
if(array1.length != array2.length) return false;
for(let e of array1) {
if(!array2.includes(e)) return false;
}
return true;
}
async function championInfos(client, patch, championId) {
const database = client.db("matches");
const matches = database.collection(patch)
const allMatches = matches.find()
let winningMatches = 0;
let losingMatches = 0;
let totalMatches = 0;
const runes = [];
for await (let match of allMatches) {
totalMatches += 1;
for(let participant of match.info.participants) {
if(participant.championId != championId) continue;
if(participant.win)
winningMatches += 1;
else
losingMatches += 1;
const primaryStyle = participant.perks.styles[0].style
const secondaryStyle = participant.perks.styles[1].style
const selections = []
for(let style of participant.perks.styles) {
for(let perk of style.selections) {
selections.push(perk.perk)
}
}
const gameRunes = {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)
break;
}
}
// Filter runes to keep 3 most played
const maxes = [0, 0, 0] // 24, 2, 1 -> 18 > 1 -> 24, 2, 2 -> 18 > 2 -> 24, 24, 2
const maxRunes = [null, null, null]
for(let rune of runes) {
let maxcount = 2;
if(rune.count <= maxes[maxcount]) continue;
while(maxcount > 0 && rune.count > maxes[maxcount]) {
maxes[maxcount] = maxes[maxcount - 1];
maxRunes[maxcount] = maxRunes[maxcount - 1];
maxcount--;
}
rune.pickrate = rune.count / (winningMatches + losingMatches)
if(rune.count <= maxes[maxcount]) maxcount++;
maxes[maxcount] = rune.count
maxRunes[maxcount] = rune
}
return {id: championId,
winrate:winningMatches / (winningMatches + losingMatches),
gameCount:(winningMatches + losingMatches),
pickrate:(winningMatches + losingMatches)/totalMatches,
runes: maxRunes.filter((x) => x != null)
};
}
async function makeChampionStat(client, patch, championId) {
const championInfo = await championInfos(client, patch, championId)
const database = client.db("champions")
const collection = database.collection(patch)
await collection.updateOne({id: championInfo.id}, {$set: championInfo}, { upsert: true })
}
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, patch) {
const list = await championList()
console.log("Generating stats for " + list.length + " champions")
let i = 0;
for(let champion of list) {
console.log("Entry " + i + "/" + list.length + " ...")
await makeChampionStat(client, patch, champion.id)
i += 1
}
const database = client.db("champions")
const collection = database.collection(patch)
await collection.createIndex({id:1})
}
module.exports = {makeChampionsStats}

156
match_collector/index.js Normal file
View File

@@ -0,0 +1,156 @@
const base = "https://euw1.api.riotgames.com"
const api_key = process.env.RIOT_API_KEY
const sleep_minutes = 3
var champion_stat = require("./champion_stat")
main()
async function main() {
console.log("MatchCollector - Hello !");
const client = await connectToDatabase();
const [latestPatch, latestPatchTime] = await fetchLatestPatchDate(client);
console.log("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 !")
const challengerLeague = await fetchChallengerLeague();
console.log("ChallengerLeague: got " + challengerLeague.entries.length + " entries");
const gameList = [];
let i = 0;
for(let challenger of challengerLeague.entries) {
console.log("Entry " + i + "/" + challengerLeague.entries.length + " ...")
const puuid = await summonerPuuid(challenger.summonerId);
const challengerGameList = await summonerGameList(puuid, latestPatchTime);
for(let game of challengerGameList) {
if(!gameList.includes(game) && !alreadySeenGameList.includes(game)) {
gameList.push(game)
}
}
i++;
}
console.log("Games: got " + gameList.length + " entries");
i = 0;
for(let 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("Generating stats...");
await champion_stat.makeChampionsStats(client, latestPatch)
console.log("All done. Closing client.");
await client.close()
}
async function handleRateLimit(url) {
let response = await fetch(url)
if(response.status == 429) {
await new Promise(resolve => setTimeout(resolve, sleep_minutes * 60 * 1000))
response = handleRateLimit(url)
}
return response
}
function handleError(response) {
if(!response.ok) {
console.log("Error during fetch(" + response.url + "): STATUS " + response.status + " (" + response.statusText + ")");
process.exit(1);
}
}
async function connectToDatabase() {
const { MongoClient, ServerApiVersion } = require("mongodb");
// Create a MongoClient with a MongoClientOptions object to set the Stable API version
const client = new MongoClient(`mongodb://${process.env.MONGO_USER}:${process.env.MONGO_PASS}@mongo:27017`)
await client.connect()
return client
}
async function fetchLatestPatchDate(client) {
const database = client.db("patches");
const patches = database.collection("patches");
const latestPatch = await patches.find().limit(1).sort({date:-1}).next()
return [latestPatch.patch, Math.floor(latestPatch.date.valueOf() / 1000)]
}
async function fetchChallengerLeague() {
const queue = "RANKED_SOLO_5x5"
const endpoint = `/lol/league/v4/challengerleagues/by-queue/${queue}`
const url = `${base}${endpoint}?api_key=${api_key}`
const challengerLeagueResponse = await handleRateLimit(url);
handleError(challengerLeagueResponse)
const challengerLeague = await challengerLeagueResponse.json();
return challengerLeague;
}
async function summonerPuuid(summonerId) {
const endpoint = `/lol/summoner/v4/summoners/${summonerId}`
const url = `${base}${endpoint}?api_key=${api_key}`
const puuidResponse = await handleRateLimit(url);
handleError(puuidResponse)
const puuidJson = await puuidResponse.json();
return puuidJson.puuid;
}
async function summonerGameList(puuid, startTime) {
const base = "https://europe.api.riotgames.com"
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 gameListResponse = await handleRateLimit(url);
handleError(gameListResponse)
const gameList = await gameListResponse.json();
return gameList;
}
async function match(matchId) {
const base = "https://europe.api.riotgames.com"
const endpoint = `/lol/match/v5/matches/${matchId}`
const url = `${base}${endpoint}?api_key=${api_key}`
const matchResponse = await handleRateLimit(url)
handleError(matchResponse)
const match = await matchResponse.json();
return match;
}
async function matchTimeline(matchId) {
const base = "https://europe.api.riotgames.com"
const endpoint = `/lol/match/v5/matches/${matchId}/timeline`
const url = `${base}${endpoint}?api_key=${api_key}`
const timelineResponse = await handleRateLimit(url)
handleError(timelineResponse)
const timeline = await timelineResponse.json();
return timeline
}
async function alreadySeenGames(client, latestPatch) {
const database = client.db("matches")
const matches = database.collection(latestPatch)
const alreadySeen = await matches.distinct("metadata.matchId")
return alreadySeen
}
async function saveMatch(client, match, latestPatch) {
const database = client.db("matches")
const matches = database.collection(latestPatch)
await matches.insertOne(match)
}

163
match_collector/package-lock.json generated Normal file
View File

@@ -0,0 +1,163 @@
{
"name": "stat_collector",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "stat_collector",
"version": "1.0.0",
"license": "ISC",
"dependencies": {
"mongodb": "^6.10.0"
}
},
"node_modules/@mongodb-js/saslprep": {
"version": "1.1.9",
"resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.9.tgz",
"integrity": "sha512-tVkljjeEaAhCqTzajSdgbQ6gE6f3oneVwa3iXR6csiEwXXOFsiC6Uh9iAjAhXPtqa/XMDHWjjeNH/77m/Yq2dw==",
"license": "MIT",
"dependencies": {
"sparse-bitfield": "^3.0.3"
}
},
"node_modules/@types/webidl-conversions": {
"version": "7.0.3",
"resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
"integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==",
"license": "MIT"
},
"node_modules/@types/whatwg-url": {
"version": "11.0.5",
"resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.5.tgz",
"integrity": "sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==",
"license": "MIT",
"dependencies": {
"@types/webidl-conversions": "*"
}
},
"node_modules/bson": {
"version": "6.9.0",
"resolved": "https://registry.npmjs.org/bson/-/bson-6.9.0.tgz",
"integrity": "sha512-X9hJeyeM0//Fus+0pc5dSUMhhrrmWwQUtdavaQeF3Ta6m69matZkGWV/MrBcnwUeLC8W9kwwc2hfkZgUuCX3Ig==",
"license": "Apache-2.0",
"engines": {
"node": ">=16.20.1"
}
},
"node_modules/memory-pager": {
"version": "1.5.0",
"resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
"integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
"license": "MIT"
},
"node_modules/mongodb": {
"version": "6.10.0",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.10.0.tgz",
"integrity": "sha512-gP9vduuYWb9ZkDM546M+MP2qKVk5ZG2wPF63OvSRuUbqCR+11ZCAE1mOfllhlAG0wcoJY5yDL/rV3OmYEwXIzg==",
"license": "Apache-2.0",
"dependencies": {
"@mongodb-js/saslprep": "^1.1.5",
"bson": "^6.7.0",
"mongodb-connection-string-url": "^3.0.0"
},
"engines": {
"node": ">=16.20.1"
},
"peerDependencies": {
"@aws-sdk/credential-providers": "^3.188.0",
"@mongodb-js/zstd": "^1.1.0",
"gcp-metadata": "^5.2.0",
"kerberos": "^2.0.1",
"mongodb-client-encryption": ">=6.0.0 <7",
"snappy": "^7.2.2",
"socks": "^2.7.1"
},
"peerDependenciesMeta": {
"@aws-sdk/credential-providers": {
"optional": true
},
"@mongodb-js/zstd": {
"optional": true
},
"gcp-metadata": {
"optional": true
},
"kerberos": {
"optional": true
},
"mongodb-client-encryption": {
"optional": true
},
"snappy": {
"optional": true
},
"socks": {
"optional": true
}
}
},
"node_modules/mongodb-connection-string-url": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.1.tgz",
"integrity": "sha512-XqMGwRX0Lgn05TDB4PyG2h2kKO/FfWJyCzYQbIhXUxz7ETt0I/FqHjUeqj37irJ+Dl1ZtU82uYyj14u2XsZKfg==",
"license": "Apache-2.0",
"dependencies": {
"@types/whatwg-url": "^11.0.2",
"whatwg-url": "^13.0.0"
}
},
"node_modules/punycode": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
"integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
"license": "MIT",
"engines": {
"node": ">=6"
}
},
"node_modules/sparse-bitfield": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
"integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
"license": "MIT",
"dependencies": {
"memory-pager": "^1.0.2"
}
},
"node_modules/tr46": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz",
"integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==",
"license": "MIT",
"dependencies": {
"punycode": "^2.3.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/webidl-conversions": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
"integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=12"
}
},
"node_modules/whatwg-url": {
"version": "13.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz",
"integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==",
"license": "MIT",
"dependencies": {
"tr46": "^4.1.1",
"webidl-conversions": "^7.0.0"
},
"engines": {
"node": ">=16"
}
}
}
}

View File

@@ -0,0 +1,15 @@
{
"name": "match_collector",
"version": "1.0.0",
"main": "index.js",
"type": "commonjs",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"description": "",
"dependencies": {
"mongodb": "^6.10.0"
}
}