Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/ValveSoftware/counter-strike_regional_standings/llms.txt

Use this file to discover all available pages before exploring further.

This page walks through the complete pipeline that transforms raw match data into the published CS2 Regional Standings. Each stage builds on the previous one: data is loaded and cleaned, teams are identified and seeded, ratings are updated match by match, and the final ranked tables are written to disk. The entry point is a single function call:
const [matches, teams] = Ranking.generateRanking(-1, filename);
Passing -1 as the version timestamp tells the model to use the most recent match in the dataset as the cutoff. The function returns the filtered match list and the fully-ranked team list, which are then handed to Report.generateOutput to produce the markdown files.
1

Data ingestion

The model reads matchdata.json from disk and applies a series of filters to remove data that should not influence rankings.
const data = fs.readFileSync(filename);
const dataJson = JSON.parse(data);
let matches = dataJson.matches;
Filters applied in order:
  1. Incomplete matches — any match where either team does not have exactly 5 players recorded is dropped. This ensures only full 5v5 results are counted.
  2. Unranked matches — matches after 1 January 2025 must carry an explicit valveRanked: true flag. Earlier matches are included unconditionally.
  3. Showmatches — any event whose name contains the word “showmatch” (case-insensitive) is excluded entirely.
  4. In-progress events — matches belonging to events where finished is false are removed. Only completed events count.
These filters run before the time window is applied, so they affect which matches are even considered when computing the rolling window boundaries.
2

Time windowing

After filtering, the model determines the date range of matches to include.The end time is either the explicit version timestamp passed to generateRanking, or the timestamp of the most recent remaining match. The start time is exactly 6 months (6 × 30 × 24 × 3600 seconds) before the end time.
this.filterWindow = 6 * 30 * 24 * 3600; // ~6 months in seconds
Matches outside this window are discarded. Within the window, a grace period of one month is applied to the time-decay weighting: matches in the final month before the cutoff receive full weight, while older matches decay toward zero as they approach the start of the window.
let graceperiod = 30 * 24 * 3600; // 1 month
rankingContext.setTimeWindow(startTime, endTime - graceperiod);
The grace period prevents a cliff-edge effect where a match played one day before the window boundary is worth nearly nothing. Matches near the cutoff still carry close to full weight.
3

Team initialization and roster deduplication

Teams are not tracked by a static team ID. Instead, the model builds roster identities from the players who actually played in each match.Matches are first sorted in reverse chronological order so that the most recent lineup is treated as the canonical roster for a given team identity. As the model walks backward through matches, a new roster entry is created whenever no existing roster shares at least 3 players with the incoming match lineup.
const TEAM_OVERLAP_TO_SHARE_ROSTER = 3;

sharesRoster(players) {
    let overlap = 0;
    players.forEach(pNew => {
        this.players.forEach(pExisting => {
            if (pNew.playerId === pExisting.playerId) overlap += 1;
        });
    });
    return overlap >= TEAM_OVERLAP_TO_SHARE_ROSTER;
}
This mirrors the Major qualification rules: a roster that retains at least 3 players from a previous lineup is treated as the same competitive entity and inherits that lineup’s match history.After all rosters are identified, their active rosters are determined by looking at the most recent 10 matches and selecting up to 5 players who each appeared in at least 5 of those matches.Once roster identity is established, matches are re-sorted into forward chronological order for the rating run.
4

Seeding calculation

Before any head-to-head results are processed, every roster receives a seed rank based on their historical performance. Seeding uses four factors, each weighted equally:
FactorDescription
Bounty offeredPrize winnings earned, scaled by event age and capped at the 5th-highest value across all teams
Bounty collectedQuality of opponents defeated, measured by their own prize winnings
Opponent networkBreadth of the opponent pool beaten, counting only the most recent win against each opponent
LAN winsWins recorded at LAN events, scaled by event age; top 10 results only
All factors cap their raw values against the Nth highest value across all teams (where N = 5), preventing a single outlier team from compressing the rest of the field.The four modifier values are averaged into a single seedValue. That value is then linearly remapped onto a 400–2000 point scale:
seedRank = 400 + ((seedValue - minSeedValue) / (maxSeedValue - minSeedValue)) * 1600
Only the top 10 results contribute to bounty collected, opponent network, and LAN wins. Playing in a low-stakes match can never hurt a team’s seed — it can only add a small positive contribution if it ranks in the top 10.
5

Glicko rating run

With seed ranks established, the model processes every match in forward chronological order through a modified Glicko rating system. The rating deviation (RD) is fixed at 75, which reduces the system to standard Elo-style updates with consistent step sizes.
const glicko = new Glicko();
glicko.setFixedRD(75); // glicko -> elo
For each match, the winner gains rating points and the loser loses them. The magnitude of the adjustment depends on the difference between the two teams’ current ratings and the match’s information content — a multiplier between 0 and 1 that reflects how recent the match is within the time window.
matches.forEach(match => {
    const [winTeam, loseTeam] = (match.winningTeam === 1)
        ? [match.team1, match.team2]
        : [match.team2, match.team1];

    glicko.singleMatch(winTeam.glickoTeam, loseTeam.glickoTeam, match.informationContent);
});
After all matches are processed, each team’s final Glicko rank is stored as their rankValue.
6

Filtering ranked teams

Two filters determine which teams appear in the published standings:
  1. No wins — teams with distinctTeamsDefeated === 0 are removed entirely. A team must have beaten at least one opponent to appear at any rank.
  2. Ranking criteria — teams must have played at least 5 matches to receive a global or regional rank number. Teams with fewer matches remain in the data (their rank value is computed) but are not assigned a standing position.
teams = teams.filter(t => t.distinctTeamsDefeated > 0);

teams.forEach(t => {
    t.satisfiesRankingCriteria = (t.matchesPlayed >= 5);
});
7

Regional assignment

Each team is assigned to a region based on the plurality nationality of its active roster players. The model counts how many active roster players belong to each of the three regions (Europe, Americas, Asia), and assigns the team to whichever region has the most representation.Players whose nationality is listed as "world" or maps to no known region are assigned to the lowest-priority region with any other representation on the roster.Teams can appear in more than one region if the representation is exactly tied — the region array stores a 1 for each region the team belongs to.Global and regional rank numbers are then assigned by iterating over teams sorted by rankValue descending, incrementing each counter only for teams that satisfy the ranking criteria.
8

Output generation

The final step writes the standings to disk as markdown files. Report.generateOutput produces:
  • Four standings tables per run: global, Europe, Americas, and Asia. Each is a markdown table with columns for standing, points, team name, active roster, and a link to the per-team detail page.
  • Per-team detail pages for every team with a global rank, saved under details/<date>/. Each page shows the team’s starting rank value, the formula breakdown, and a full match-by-match table listing age weight, event weight, bounty collected, opponent network, LAN wins, and Glicko adjustment for every match played.
Files written to the live/<year>/ directory are always updated. Files written to the invitation/<year>/ directory are only updated when the run date falls in the first 7 days of the month — this is the periodic snapshot used for invitation decisions.
Report.generateOutput(teams, regions, strDate);
The per-team filename encodes global rank, team name, and active roster player nicks — for example, 0001--natus-vincere--b1t-electronic-iм-jl-w0nderful.md. This makes individual pages directly linkable and diff-friendly in version control.

Build docs developers (and LLMs) love