Skip to Content
Technical Implementation

Last Updated: 3/10/2026


Technical Implementation

This article provides a comprehensive technical overview of how the TFT Rolling Odds Calculator is implemented, covering the architecture, key algorithms, and code structure.

Architecture Overview

The calculator is a client-side web application with no backend dependencies:

┌─────────────────────────────────────────┐ │ index.html │ │ (UI Structure & Input Controls) │ └──────────────┬──────────────────────────┘ ├──> style.css (Styling) ├──> Chart.js (Visualization) └──> script.js (Core Logic) ├─ Event Handlers ├─ Probability Calculations ├─ Matrix Operations └─ Chart Updates

Technology Stack

  • HTML5: Structure and input controls
  • CSS3: Styling and layout
  • Vanilla JavaScript: All logic (no frameworks)
  • Chart.js: Bar chart visualization
  • Mathematics: Linear algebra (matrix operations)

File Structure

TFT-Rolling-Odds-Calculator/ ├── index.html # Main HTML structure ├── style.css # Styling ├── script.js # All JavaScript logic ├── markov.png # Diagram for README └── README.md # Project documentation

Core Data Structures

Game Configuration Constants

const totalUnits = [22, 20, 17, 10, 9]; const distinctChamps = [13, 13, 13, 13, 8];
  • totalUnits[i]: Number of copies of each champion at cost tier i+1
  • distinctChamps[i]: Number of unique champions at cost tier i+1

Shop Odds Table

const costProbs = [ [1, 0, 0, 0, 0], // Level 1 [1, 0, 0, 0, 0], // Level 2 [0.75, 0.25, 0, 0, 0], // Level 3 // ... (continues for all 11 levels) ];

A 2D array where costProbs[level-1][cost-1] gives the probability of rolling a unit of that cost at that level.

Headliner Probabilities

const headlinerProbs = [ [0.077, 0, 0, 0, 0], // Level 2 [0.077, 0, 0, 0, 0], // Level 3 // ... (continues through level 10) ];

Probability of getting a headliner of each cost at each level.

Key Algorithms

1. Main Probability Calculation

function getProbs(cost, lvl, a, b, gold) { var mat = getCombOfMatrices(cost, lvl, a, b, gold) const pprob = mat[0] // Exact probabilities // Convert to cumulative probabilities let cprob = [1]; for (let i = 1; i < 10; i++) { let p = 1; for (let j = 0; j < i; j++) { p -= pprob[j]; } cprob.push(p.toFixed(2)); } return [pprob, cprob]; }

Flow:

  1. Generate combined transition matrix for all possible scenarios
  2. Extract probability distribution from first row
  3. Convert exact probabilities to cumulative (“at least X”)
  4. Return both formats

2. Transition Matrix Generation

function getTransitionMatrix(cost, lvl, a, b){ const mat = []; for (let i = 0; i < 10; i++) { const newRow = []; const p = getTransitionProb(cost, lvl, a+i, b+i); for (let j = 0; j < 10; j++) { if (i == 9 && j == 9) { newRow.push(1); // Absorbing state continue; } if (j == i) { newRow.push(1 - p); // Stay in same state } else if (j == i + 1) { newRow.push(p); // Transition to next state } else { newRow.push(0); // No other transitions } } mat.push(newRow); } return mat; }

Matrix Structure:

  • 10×10 matrix representing states (0-9 units found)
  • Row i = currently have i units
  • Column j = end with j units after one shop
  • mat[i][j] = probability of transitioning from state i to state j

Key Properties:

  • Tri-diagonal structure (can only stay or advance by 1)
  • State 9 is absorbing (represents 9+ units)
  • Transition probability decreases as pool depletes

3. Single Shop Transition Probability

function getTransitionProb(cost, lvl, a, b){ const howManyLeft = Math.max(0, totalUnits[cost - 1] - a); const poolSize = totalUnits[cost - 1] * distinctChamps[cost - 1] - b; return getCostProb(lvl, cost) * (howManyLeft / poolSize) }

Formula:

P(hit unit) = P(roll this cost tier) × (copies left / total units of this cost left)

4. Headliner Integration

function getCombOfMatrices(cost, lvl, a, b, gold){ const rolls = Math.floor(gold/2); var p = [10×10 zero matrix]; const allTransitionMatrices = getAllTransitionMatrices(cost, lvl, a, b, gold); const headlinerWhichShopProbs = getHeadlinerWhichShopProbs(cost, lvl, gold); // Weighted sum over all possible headliner scenarios for (let i = 0; i < rolls; i++){ const combined = multiplyScalar(allTransitionMatrices[i], headlinerWhichShopProbs[i]); p = addMatrices(p, combined); } // Add scenario where no headliner appears const noHeadlinerM = power(getTransitionMatrix(cost, lvl, a, b), 5*rolls); const prNoHeadliner = (1-headlinerProbs[lvl-2][cost-1])**(rolls); const c = multiplyScalar(noHeadlinerM, prNoHeadliner); p = addMatrices(p, c); return p; }

Logic:

  1. For each possible roll where headliner appears:
    • Calculate transition matrix up to that point
    • Apply headliner transition (instant +3 copies)
    • Continue normal rolling after
    • Weight by probability of headliner appearing at that roll
  2. Add the scenario where no headliner appears at all
  3. Sum all weighted scenarios using law of total probability

5. Headliner Transition Matrix

const headlinerTransitionMatrix = [ [0, 0, 0, 1, 0, 0, 0, 0, 0, 0], // 0 → 3 units [0, 0, 0, 0, 1, 0, 0, 0, 0, 0], // 1 → 4 units [0, 0, 0, 0, 0, 1, 0, 0, 0, 0], // 2 → 5 units // ... (each row shifts right by 3) ];

When a headliner appears, you instantly get +3 copies, so this matrix jumps 3 states forward.

6. Matrix Operations

Matrix Multiplication

function multiply(a, b){ var aNumRows = a.length, aNumCols = a[0].length, bNumRows = b.length, bNumCols = b[0].length, m = new Array(aNumRows); for (var r = 0; r < aNumRows; ++r) { m[r] = new Array(bNumCols); for (var c = 0; c < bNumCols; ++c) { m[r][c] = 0; for (var i = 0; i < aNumCols; ++i) { m[r][c] += a[r][i] * b[i][c]; } } } return m; }

Standard matrix multiplication: O(n³) complexity.

Matrix Power

function power(a, n){ var newmat = a for (let i = 0; i < n-1; i++) { newmat = multiply(newmat, a); } return newmat; }

Usage: M^n represents n consecutive shops.

Note: This is a naive implementation. For large n, could be optimized with exponentiation by squaring.

UI and Event Handling

Input Controls

document.getElementById("costRange").oninput = function(){ updateCost(); updateGraph(); } document.getElementById("lvlRange").oninput = function(){ updateLvl(); updateGraph(); } document.getElementById("copiesText").oninput = function(){ updateGraph(); } // ... (similar for other inputs)

All inputs trigger immediate recalculation and chart update.

Chart Update

function updateGraph() { chart.data.datasets = [{ label: 'Probability of getting at least x units', data: getProbs( parseInt(cost.value), parseInt(lvl.value), parseInt(copies.value), parseInt(pool.value), parseInt(gold.value) )[1].slice(1) // Use cumulative probs, skip "at least 0" }] chart.update(); }

Chart Configuration

var chart = new Chart(ctx, { type: 'bar', data: { labels: ['1','2','3','4','5','6','7','8','9'], datasets: [{ label: 'Probability of getting at least x units', data: [0, 0, 0, 0, 0, 0, 0, 0, 0] }] }, options: { scales: { y: { suggestedMin: 0, suggestedMax: 1, beginAtZero: true } }, plugins: { tooltip: { displayColors: false, bodyFont: { size: 20 }, // Custom callbacks for clean display } } } });

Performance Considerations

Computational Complexity

For gold amount g (number of rolls = g/2):

  • Matrix size: 10×10 (fixed)
  • Matrix multiplications: O(g) operations
  • Each multiplication: O(10³) = O(1000) operations
  • Total: O(g) - linear in gold amount

Optimization Opportunities

  1. Matrix Exponentiation by Squaring

    • Current: O(n) multiplications for M^n
    • Optimized: O(log n) multiplications
    • Benefit: Significant for large gold amounts (100+)
  2. Sparse Matrix Operations

    • Transition matrices are tri-diagonal
    • Could use specialized sparse matrix algorithms
    • Benefit: ~3x speedup
  3. Memoization

    • Cache transition matrices for common states
    • Benefit: Faster UI updates when changing only gold amount
  4. Web Workers

    • Move heavy calculations off main thread
    • Benefit: Smoother UI during computation

Current Performance

On modern hardware:

  • Typical calculation (50 gold): < 10ms
  • Large calculation (200 gold): < 100ms
  • UI remains responsive throughout

Testing and Validation

Manual Test Cases

Test 1: Level 1, 1-cost unit, full pool

Input: cost=1, lvl=1, copies=0, pool=0, gold=10 Expected: High probability (100% shop odds × full pool)

Test 2: Impossible scenario

Input: cost=5, lvl=1, copies=0, pool=0, gold=100 Expected: 0% (can't roll 5-costs at level 1)

Test 3: Depleted pool

Input: cost=1, lvl=8, copies=22, pool=X, gold=50 Expected: 0% (all copies already taken)

Validation Against Known Probabilities

The mathematics can be validated against:

  1. Single-shop probabilities (simple formula)
  2. Binomial approximations (for large pools)
  3. Community-verified edge cases

Code Quality

Strengths

  • ✅ Clear function names
  • ✅ Modular design
  • ✅ No external dependencies (except Chart.js)
  • ✅ Works offline after initial load

Areas for Improvement

  • ⚠️ No input validation (can enter negative numbers)
  • ⚠️ No unit tests
  • ⚠️ Magic numbers could be constants
  • ⚠️ Could benefit from JSDoc comments

Extending the Calculator

Adding New TFT Sets

  1. Update totalUnits array
  2. Update distinctChamps array
  3. Update costProbs table if shop odds changed
  4. Update headlinerProbs if mechanic changed
  5. Test with known scenarios

Adding New Features

Possible enhancements:

  • Multiple unit tracking
  • Expected value calculations
  • Save/load scenarios
  • Probability heatmaps
  • Mobile-optimized UI

See the Development Guide for detailed instructions.

Dependencies

Chart.js

<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
  • Version: Latest from CDN
  • Purpose: Bar chart visualization
  • License: MIT
  • Docs: chartjs.org 

No Build Process

The application requires no build step:

  • No npm/yarn
  • No webpack/bundler
  • No transpilation
  • Just open index.html in a browser!

Browser Compatibility

Required Features

  • ES6 JavaScript (arrow functions, const/let)
  • HTML5
  • CSS3
  • Canvas API (for Chart.js)

Supported Browsers

  • Chrome 60+
  • Firefox 55+
  • Safari 11+
  • Edge 79+
  • Mobile browsers (iOS Safari, Chrome Mobile)

Deployment

The application is deployed using GitHub Pages:

  1. Code pushed to main branch
  2. GitHub Pages serves from root directory
  3. Accessible at: https://wongkj12.github.io/TFT-Rolling-Odds-Calculator/

No server required - purely static hosting.