This commit is contained in:
bumpsoo 2026-02-17 14:44:40 +00:00
commit 5962730120
11 changed files with 354 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.cache
build

24
CMakeLists.txt Normal file
View file

@ -0,0 +1,24 @@
cmake_minimum_required(VERSION 3.10)
project(LolAnalytics VERSION 1.0)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED True)
find_package(Boost REQUIRED COMPONENTS system)
find_package(OpenSSL REQUIRED)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
add_executable(lol_analytics
src/main.cpp
src/riot.cpp
src/network.cpp
)
target_include_directories(lol_analytics PRIVATE include)
target_link_libraries(lol_analytics PRIVATE
Boost::system
OpenSSL::SSL
OpenSSL::Crypto
)

58
README.md Normal file
View file

@ -0,0 +1,58 @@
# 📊 LoL Bio-Rhythm Analytics
## 1. Overview
A high-performance data analysis engine built with C++ to identify Champion Bio-Rhythms—specific hours of the day when a League of Legends champion's win rate peaks.
This project analyzes whether timing (e.g., 3:00 PM vs. 3:00 AM) significantly impacts win rates for specific champions by processing large-scale match data from the Riot Games API.
## 2. Architecture
- C++ + Boost(Asio, Beast)
- MySQL (might be changed to another one)
```mermaid
sequenceDiagram
participant Admin as User/Admin
participant Sys as lol_analytics
participant Riot as Riot API
participant DB as DB
%% ==========================================
%% API 1: Sync All Users by League
%% ==========================================
rect rgb(240, 248, 255)
Note over Admin, DB: [API 1] Fetch all users by league
Admin->>Sys: Request: Sync League Users (League ID)
loop Page 1 to End (Until empty)
Sys->>Riot: GET /lol/league/v4/entries/... (page=N)
Riot-->>Sys: User List Data
Sys->>DB: Save/Update Users (Summoner ID, PUUID, etc.)
end
Sys-->>Admin: User Sync Completed
end
%% ==========================================
%% API 2: Sync Matches within Time X ~ Y
%% ==========================================
rect rgb(255, 245, 238)
Note over Admin, DB: [API 2] Fetch matches within time X ~ Y
Admin->>Sys: Request: Fetch Matches (Time X ~ Y)
Sys->>DB: Query User IDs (PUUIDs)
DB-->>Sys: PUUID List
loop For each User
Sys->>Riot: GET /lol/match/v5/matches/by-puuid/{puuid}?startTime=X&endTime=Y
Riot-->>Sys: Match ID List
loop For each Match ID
Sys->>Riot: GET /lol/match/v5/matches/{matchId}
Riot-->>Sys: Match Detail Data
Sys->>DB: Save Match Result (win/loss, timestamp, champion)
end
end
Sys-->>Admin: Match Sync Completed
end
```

1
compile_commands.json Symbolic link
View file

@ -0,0 +1 @@
build/compile_commands.json

26
include/network.h Normal file
View file

@ -0,0 +1,26 @@
#pragma once
#include <boost/asio.hpp>
#include <boost/asio/co_spawn.hpp>
#include <boost/asio/executor_work_guard.hpp>
#include <memory>
#include <thread>
namespace asio = boost::asio;
class NetworkService : public std::enable_shared_from_this<NetworkService> {
public:
NetworkService(asio::io_context &, asio::ip::tcp::endpoint, int);
void start();
private:
asio::io_context &ctx_;
asio::ip::tcp::endpoint endpoint_;
std::vector<std::jthread> threads_;
asio::executor_work_guard<asio::io_context::executor_type> work_guard_;
asio::awaitable<void> listen();
asio::awaitable<void> session(asio::ip::tcp::socket);
};

42
include/riot.h Normal file
View file

@ -0,0 +1,42 @@
#pragma once
#include <boost/asio.hpp>
#include <cstdint>
#include <string>
#include <string_view>
#include <vector>
namespace asio = boost::asio;
namespace riot {
struct LeagueEntry {
std::string puuid;
};
enum class Division { I, II, III, IV };
enum class Tier { BRONZE, SILVER, GOLD, PLATINUM, DIAMOND };
enum class Region { KR };
class Client {
public:
Client(asio::io_context &, std::string, Region = Region::KR);
asio::awaitable<std::vector<LeagueEntry>> GetIdsInTier(Division, Tier,
uint8_t) const;
private:
asio::io_context &ctx_;
std::string api_key_;
Region region_;
static constexpr std::string_view BASE_URL = "api.riotgames.com";
std::string divisionToString(Division) const;
std::string tierToString(Tier) const;
std::string regionToString(Region) const;
};
} // namespace riot

14
schema.sql Normal file
View file

@ -0,0 +1,14 @@
CREATE DATABASE IF NOT EXISTS lol_analytics;
USE lol_analytics;
-- TODO: adjust size
CREATE TABLE IF NOT EXISTS user_ids (
puuid VARCHAR(256) PRIMARY KEY,
tier VARCHAR(256) NOT NULL,
);
CREATE TABLE IF NOT EXISTS matches (
match_id VARCHAR(256) PRIMARY KEY,
puuid VARCHAR(256) NOT NULL,
tier VARCHAR(256) NOT NULL,
FOREIGN KEY (puuid) REFERENCES user_ids(puuid)
);

9
setup.sh Normal file
View file

@ -0,0 +1,9 @@
#!/bin/bash
set -e
cmake -B build -S .
ln -sf build/compile_commands.json compile_commands.json
echo "Setup completed"

34
src/main.cpp Normal file
View file

@ -0,0 +1,34 @@
#include "network.h"
#include "riot.h"
#include <boost/asio.hpp>
#include <boost/asio/awaitable.hpp>
#include <boost/asio/co_spawn.hpp>
#include <iostream>
#include <thread>
int main() {
uint32_t thread_cnt = std::thread::hardware_concurrency();
if (thread_cnt == 0) {
thread_cnt = 1;
}
boost::asio::io_context ctx;
const std::string address = "0.0.0.0";
const uint16_t port = 8080;
try {
auto service = std::make_shared<NetworkService>(
ctx, boost::asio::ip::tcp::endpoint(boost::asio::ip::tcp::v4(), port),
thread_cnt);
service->start();
ctx.run();
} catch (const std::exception &e) {
std::cerr << "Error: " << e.what() << "\n";
return 1;
}
return 0;
}

68
src/network.cpp Normal file
View file

@ -0,0 +1,68 @@
#include "network.h"
#include <boost/asio.hpp>
#include <boost/asio/strand.hpp>
#include <boost/beast.hpp>
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <iostream>
namespace beast = boost::beast;
namespace asio = boost::asio;
NetworkService::NetworkService(asio::io_context &ctx,
asio::ip::tcp::endpoint endpoint, int thread_cnt)
: ctx_(ctx), endpoint_(endpoint), work_guard_(ctx.get_executor()) {
threads_.reserve(thread_cnt);
for (int i = 0; i < thread_cnt; ++i) {
threads_.emplace_back([ctx = &ctx_]() { ctx->run(); });
}
}
void NetworkService::start() {
asio::co_spawn(
asio::make_strand(ctx_),
[self = shared_from_this()]() { return self->listen(); }, asio::detached);
}
asio::awaitable<void> NetworkService::listen() {
auto executor = co_await asio::this_coro::executor;
asio::ip::tcp::acceptor acceptor(executor, endpoint_);
std::cout << "Server listening on port: " << endpoint_.port() << std::endl;
while (true) {
asio::ip::tcp::socket socket =
co_await acceptor.async_accept(asio::use_awaitable);
asio::co_spawn(
asio::make_strand(ctx_),
[self = shared_from_this(),
socket = std::move(socket)]() mutable -> asio::awaitable<void> {
co_await self->session(std::move(socket));
},
asio::detached);
}
}
asio::awaitable<void> NetworkService::session(asio::ip::tcp::socket socket) {
try {
beast::flat_buffer buffer;
beast::http::request<beast::http::string_body> req;
co_await beast::http::async_read(socket, buffer, req, asio::use_awaitable);
beast::http::response<beast::http::string_body> res{beast::http::status::ok,
req.version()};
res.set(beast::http::field::content_type, "text/plain");
res.keep_alive(req.keep_alive());
res.body() = "Bye, World!";
res.prepare_payload();
co_await beast::http::async_write(socket, res, asio::use_awaitable);
} catch (std::exception &e) {
std::cerr << "Session error: " << e.what() << "\n";
}
co_return;
}

76
src/riot.cpp Normal file
View file

@ -0,0 +1,76 @@
#include "riot.h"
#include <boost/beast/core.hpp>
#include <boost/beast/http.hpp>
#include <boost/beast/ssl.hpp>
#include <boost/beast/version.hpp>
#include <format>
#include <iostream>
#include <stdexcept>
namespace beast = boost::beast;
namespace http = beast::http;
namespace riot {
std::string Client::divisionToString(Division division) const {
switch (division) {
case Division::I:
return "I";
case Division::II:
return "II";
case Division::III:
return "III";
case Division::IV:
return "IV";
default:
throw std::invalid_argument("invalid division");
}
}
std::string Client::tierToString(Tier tier) const {
switch (tier) {
case Tier::BRONZE:
return "BRONZE";
case Tier::SILVER:
return "SILVER";
case Tier::GOLD:
return "GOLD";
case Tier::PLATINUM:
return "PLATINUM";
case Tier::DIAMOND:
return "DIAMOND";
default:
throw std::invalid_argument("invalid tier");
}
}
std::string Client::regionToString(Region region) const {
switch (region) {
case Region::KR:
return "kr";
default:
throw std::invalid_argument("invalid region");
}
}
Client::Client(boost::asio::io_context &ctx, std::string key, Region region)
: ctx_(ctx), api_key_(std::move(key)), region_(region) {}
asio::awaitable<std::vector<LeagueEntry>>
Client::GetIdsInTier(Division division, Tier tier, uint8_t page) const {
auto div_str = divisionToString(division);
auto tier_str = tierToString(tier);
auto region_str = regionToString(region_);
auto request_url =
std::format("https://{}.{}/lol/league/v4/entries/RANKED_SOLO_5x5/{}/"
"{}?api_key={}&page={}",
region_str, BASE_URL, tier_str, div_str, api_key_, page);
std::cout << "[DEBUG] Fetching from: " << request_url << std::endl;
// TODO: Implement JSON parsing
co_return std::vector<LeagueEntry>{};
}
} // namespace riot