Skip to content

About

A Discord Bot for tracking game results in LoL/TFT.

Resources

Stars

4 stars

Watchers

1 watching

Forks

Repository files navigation

Tentrackule

A Discord bot that tracks League of Legends players and posts a rich match card to your server whenever a tracked player finishes a game.

The card is a modern HTML/CSS layout — champion splash, KDA, per-role performance grade, KP / damage share / vision breakdown, the lane matchup, team objectives, and either the ranked block (rank, LP delta, win-rate, recent-form streak) or a champion-mastery block for unranked queues.

How it works

Rendering is delegated to Cloudflare so the bot itself stays lean (256 MB, no browser, no font stack):

  tentrackule (bot, 256 MB)                     Cloudflare Browser Run
  ┌──────────────────────────┐  POST /screenshot ┌──────────────────────────────┐
  │ • poll Riot for new match │  { html }  ─────▶ │ Managed headless Chromium    │
  │ • build card HTML (Rust)  │                   │ • pre-installed CJK fonts    │
  │ • inline assets & fonts   │ ◀──── JPEG bytes  │ • crops #card-root           │
  │   as data URIs (cache)    │                   │ • billed per browser second  │
  └──────────────────────────┘                   └──────────────────────────────┘
            │ JPEG ─▶ Discord attachment
  • Card HTML is assembled in Rust (src/discord/scorecard.rs) from a fully resolved view-model — no template engine, every dynamic field HTML-escaped.
  • Assets (splash, spells, runes, items, opponent icon) are fetched once through a disk+memory cache (ImageCache) and inlined as data: URIs, like the Latin web fonts, so the render needs no network. CJK names use the fonts pre-installed in the browser.
  • Rendering POSTs the HTML to Cloudflare Browser Run, which waits for load and document.fonts.ready, then screenshots the #card-root element at deviceScaleFactor: 2. Rate limits honour Retry-After.
  • Fallback: if the renderer is unset, out of quota or unreachable, the match is still announced as a compact text embed, so a render outage never blocks tracking.

Data comes from Riot match-v5, league-v4 and champion-mastery-v4, plus Data Dragon for asset/version lookups. The performance grade is computed locally (src/discord/grade.rs); the recent-form streak is read from a local SQLite table (zero extra API calls).

Post-game AI analysis

With ANTHROPIC_API_KEY set, every Summoner's Rift card gets an 🧠 Analyse it! button. A click posts a coach-style breakdown of the tracked player's game under the card (in French): a third-person recap for the server, whether the result came down to the player or the team, then coaching for the player (a read of both drafts and the matchup, strengths, turning points, mistakes with a concrete fix, a build & runes verdict, one focus for the next game). The prompt asks for causes and counterfactuals, not a restatement of the stats the card already shows. The button is then disabled. The analysis is posted folded (recap and verdict only, so it doesn't bury the cards around it); its 🔽 Voir le coaching / 🔼 Replier button unfolds or folds it for the whole channel, read back from SQLite with no new Claude call.

  • Same shape every game. Three fixed parts (src/analysis/): a digest built in Rust from match-v5 and the match timeline (lane diffs every 5 min, every allied lane's gold diff at 10/15 min, gold swings, each death with its zone, fight context, unspent gold and the enemy objectives that followed, where the player stood when objectives fell, objective trades, level 6/11/16 and item timings against the lane opponent, skill order); every player's classes, runes, spells, damage split (physical/magic/true), healing, CC and final build; a versioned system prompt (prompt.md) followed by the patch's item catalog from Data Dragon (stats and effects of every completed Rift item, so build advice matches the live shop); and a structured-output JSON schema the reply must follow, rendered into a fixed embed. All numbers are computed locally; the only identity sent is the analysed player's game name (for the third-person summary), never a tag or puuid.
  • Patch meta from OP.GG. Before the Claude call, the bot queries OP.GG's public MCP server (https://mcp-api.op.gg/mcp, no key; src/analysis/opgg.rs) for the Emerald+ reference on the player's champion and role (core build, boots, 4th/5th item options, runes, skill max order, each with games and win rate; best and worst matchups), OP.GG's read of the exact lane matchup, and every champion's tier in its role (tier list cached 6 h). It is a fixed set of calls made from Rust, not tools Claude drives, and only champion names and roles are sent. If OP.GG fails, the analysis runs without that section.
  • Claude Opus 5.5 at high effort, over raw HTTP (no official Rust SDK), with server-side refusal fallbacks enabled. The instructions and item catalog (~20k tokens) are prompt-cached, shared by every analysis within 5 minutes; the digest adds ~7k, and the output 5k tokens with thinking, i.e. **$0.15–0.25 per analysis** and about a minute of wait (the exact cost is logged per call).
  • Paid once per game. Analyses are cached in SQLite per (match, player, prompt version), so later clicks and other guilds are served for free, and simultaneous clicks share one call. Token counts are stored with each row (SELECT SUM(input_tokens), SUM(output_tokens) FROM match_analyses). Bump PROMPT_VERSION after changing the prompt to regenerate on the next click.

Commands

Command Description
/track <game_name> <tag_line> <region> Start tracking a player
/untrack … Stop tracking a player
/list List players tracked in this server
/config channel <#channel> Set the alert channel (needs Manage Server)
/dev_test_alert … Render a sample card for any queue/outcome (dev, bot owner only)
/dev_analyze <game_name> <tag_line> <region> [match_id] [fresh] Run the AI analysis on any game (dev, bot owner only)

Configuration

Set via environment (a .env file is loaded in dev — see .env.example):

Variable Required Default Notes
DISCORD_TOKEN ✅ — Bot token
RIOT_API_KEY ✅ — Riot API key
DATABASE_URL sqlite:tentrackule.db SQLite connection string
POLLING_INTERVAL_SECS 60 Base poll tick / fastest cadence; idle players are polled less often (activity-adaptive)
RIOT_RATE_LIMIT_PER_SECOND 20 Client-side rate limit (fast window)
RIOT_RATE_LIMIT_PER_TWO_MIN 100 Client-side rate limit (2-minute window)
DDRAGON_VERSION (auto) Data Dragon patch. Unset → resolved from versions.json at boot and refreshed every 24h; set to pin a patch
CLOUDFLARE_ACCOUNT_ID (unset) Cloudflare account running Browser Run; unset → text-embed fallback
CLOUDFLARE_API_TOKEN (unset) API token with the Browser Run - Edit permission (secret)
ANTHROPIC_API_KEY (unset) Claude API key for the post-game analysis button; unset → no button (secret)
RUST_LOG / LOG_FORMAT info / — Tracing filter; LOG_FORMAT=json for prod

Development

A Nix flake provides the toolchain (nix develop / direnv), or use a local Rust toolchain (edition 2024).

cargo run                 # run the bot (needs DISCORD_TOKEN + RIOT_API_KEY)
cargo test                # unit tests
cargo clippy --all-targets

The card builder is unit-testable without any infrastructure. Tests write sample cards to target/scorecard-samples/*.html (open them in a browser, or screenshot #card-root, to review visually). The HTML is also locked by golden snapshots under src/discord/snapshots/:

UPDATE_SNAPSHOTS=1 cargo test   # regenerate goldens after an intentional markup change

To render real cards on Cloudflare (Latin, CJK and Cyrillic names), add the two CLOUDFLARE_* variables to .env, then review the JPEGs written to target/scorecard-samples/:

cargo test renders_real_cards_on_cloudflare -- --ignored --nocapture

To check the analysis on a real game, pick a player and run the free digest check (Riot only), then the full report (one paid Claude call, needs ANTHROPIC_API_KEY). Outputs land in target/analysis-samples/:

export ANALYSIS_TEST_RIOT_ID="Name#TAG" ANALYSIS_TEST_PLATFORM=EUW1
cargo test real_match_digest -- --ignored --nocapture
cargo test real_match_analysis -- --ignored --nocapture

Deployment

  • tentrackule — the bot on Fly.io (fly.toml, 256 MB, persistent volume for the DB).
  • Cloudflare Browser Run — the managed renderer, with nothing to deploy. Setup, limits and failure modes are in docs/renderer-deploy.md.

Docs

About

A Discord Bot for tracking game results in LoL/TFT.

Resources

Stars

4 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages