Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/SdazaP/ruTournament/llms.txt

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

Each category in a ruTournament tournament is assigned exactly one format at creation time. The format is not cosmetic — it determines how individual solve times are aggregated into a score, how competitors are ranked within a round, and how advancement to subsequent rounds (or match-ups in a bracket) is calculated. Switching a category’s format after results have been entered will erase those results, so choose the format deliberately during tournament setup.
The WCA format mirrors the rules used at official World Cube Association competitions. Each round consists of a fixed number of solves per competitor, which are then averaged according to the round’s configured format.

Average formats

FormatSolvesCalculation
ao55Drop the single best and single worst time; average the middle 3
ao33Average all 3 times (mean of 3 — no drops)

Penalties

Every individual solve time is stored as a TimeRecord object with a base value (in seconds) and a penalty flag:
type Penalty = '' | '+2' | 'DNF';

interface TimeRecord {
  base: number;   // solve time in seconds
  penalty: Penalty;
}
PenaltyMeaningEffect on score
''Clean solveScore = base
'+2'Judge applied a +2 second penaltyScore = base + 2
'DNF'Did Not Finish — competitor did not complete the solveScore = -1 (treated as infinity in ranking)

Average calculation: calculateRulesStats

The following function, taken directly from ResultsWCA.tsx, computes both the single best and the official average for a set of times:
// src/pages/Tournament/ResultsWCA.tsx
export const calculateRulesStats = (
  times: TimeRecord[],
  format: 'ao3' | 'ao5'
) => {
  const solves = times.map(t => getSolveValue(t)); // DNF => -1
  const finishedSolves = solves.filter(s => s > 0);
  const dnfsCount = solves.filter(s => s < 0).length;

  let best = -1;
  if (finishedSolves.length > 0) best = Math.min(...finishedSolves);

  // Only calculate average once all slots are filled
  const allEntered = times.every(t => t.base > 0 || t.penalty === 'DNF');
  if (!allEntered) return { best, average: 0 };

  let average = 0;

  if (format === 'ao3') {
    // Any DNF in ao3 = DNF average
    if (dnfsCount > 0) average = -1;
    else average = solves.reduce((a, b) => a + b, 0) / 3;

  } else if (format === 'ao5') {
    // Two or more DNFs in ao5 = DNF average
    if (dnfsCount >= 2) average = -1;
    else {
      const valid = solves.filter(s => s > 0).sort((a, b) => a - b);
      if (dnfsCount === 1) {
        // The one DNF counts as the "worst" — drop only the best
        valid.shift();
        average = valid.reduce((a, b) => a + b, 0) / 3;
      } else {
        // Drop best and worst, average the middle 3
        valid.shift();
        valid.pop();
        average = valid.reduce((a, b) => a + b, 0) / 3;
      }
    }
  }

  return {
    best:    best    > 0 ? Math.round(best * 100) / 100 : -1,
    average: average > 0 ? Math.round(average * 100) / 100 : -1,
  };
};
Key rules to remember:
  • ao5 with 1 DNF: The DNF is treated as the worst time. Only the single best time is dropped; the remaining three (including the DNF-penalised slot treated as dropped) are averaged.
  • ao5 with 2+ DNFs: The entire average is DNF (-1).
  • ao3 with any DNF: The entire average is DNF (-1).

Ranking: sortWCA

Competitors within a round are ranked using the following comparator, also from ResultsWCA.tsx:
// src/pages/Tournament/ResultsWCA.tsx
export const sortWCA = (a: any, b: any) => {
  // Weight 1 = valid average, 2 = DNF average, 3 = incomplete
  const getWeight = (avg: number) => {
    if (avg > 0)  return 1;
    if (avg === -1) return 2;
    return 3;
  };

  const weightA = getWeight(a.average);
  const weightB = getWeight(b.average);

  // First: sort by average tier (valid < DNF < incomplete)
  if (weightA !== weightB) return weightA - weightB;

  // Within valid averages: lower average wins; tie-break on single best
  if (weightA === 1) {
    if (a.average !== b.average) return a.average - b.average;
    const bestA = a.best > 0 ? a.best : Infinity;
    const bestB = b.best > 0 ? b.best : Infinity;
    return bestA - bestB;
  }

  // Within DNF/incomplete: sort by single best
  const bestA = a.best > 0 ? a.best : (a.best === -1 ? Infinity - 1 : Infinity);
  const bestB = b.best > 0 ? b.best : (b.best === -1 ? Infinity - 1 : Infinity);
  if (bestA !== bestB) return bestA - bestB;

  return a.name?.localeCompare(b.name || '');
};
In plain terms: competitors with a valid (non-DNF) average are ranked first, sorted by that average ascending. Ties in the average are broken by the single best time. Competitors whose average is DNF rank below all valid averages, sorted again by their single best. Completely incomplete result sets rank last.

Multi-round advancement

WCA categories can have multiple rounds. Each non-final round has a competitorsToAdvance value that controls how many competitors move forward:
interface RoundLocal {
  num:                   number;
  format:                string;
  results:               ResultLocal[];
  competitorsToAdvance:  number | 'all';
}
After a preliminary round closes, the app sorts all competitors via sortWCA and slices the top competitorsToAdvance entries. Only those competitors appear in the next round’s result table. When competitorsToAdvance is 'all', every competitor with a result advances.
The number of rounds and the competitorsToAdvance value for each round are configured during tournament creation. You can also change them later in the Categories section while the tournament is in the activo state — just be aware that changing the advancement count on a round for which results already exist will immediately affect which competitors are visible in the next round.

Build docs developers (and LLMs) love