Skip to content

mormubis/swiss

Repository files navigation

Swiss

npm Test Coverage License: MIT

Swiss is a TypeScript library for Swiss chess tournament pairing and standings, following FIDE rules. Zero runtime dependencies.

Three FIDE-approved pairing systems are supported: Dutch (C.04.3), Dubov (C.04.4.1), and Burstein (C.04.4.2). Six built-in tiebreak functions are included, all pluggable and composable.

Installation

npm install @echecs/swiss

Quick Start

import { dutch, standings, buchholz, sonnebornBerger } from '@echecs/swiss';
import type { Game, Player } from '@echecs/swiss';

const players: Player[] = [
  { id: 'alice', rating: 2100 },
  { id: 'bob', rating: 1950 },
  { id: 'carol', rating: 1870 },
  { id: 'dave', rating: 1820 },
];

// Pair round 1 (no games played yet)
const round1 = dutch(players, [], 1);
console.log(round1.pairings);
// [{ whiteId: 'alice', blackId: 'carol' }, { whiteId: 'bob', blackId: 'dave' }]

// Submit results
const games: Game[] = [
  { whiteId: 'alice', blackId: 'carol', result: 1, round: 1 },
  { whiteId: 'bob', blackId: 'dave', result: 0.5, round: 1 },
];

// Pair round 2
const round2 = dutch(players, games, 2);

// Compute standings after round 1
const table = standings(players, games, [buchholz, sonnebornBerger]);
console.log(table[0]);
// { playerId: 'alice', rank: 1, score: 1, tiebreaks: [1, 1] }

API

Pairing functions

All three pairing systems share the same signature:

function dutch(players: Player[], games: Game[], round: number): PairingResult;
function dubov(players: Player[], games: Game[], round: number): PairingResult;
function burstein(
  players: Player[],
  games: Game[],
  round: number,
): PairingResult;
  • players — all registered players in the tournament
  • games — all completed games across all previous rounds
  • round — the round number to pair (1-based)

Throws RangeError for round < 1 or fewer than 2 players.

interface PairingResult {
  byes: Bye[]; // players with no opponent this round
  pairings: Pairing[]; // white/black assignments
}

interface Pairing {
  blackId: string;
  whiteId: string;
}

interface Bye {
  playerId: string;
}

Pairing systems

Function FIDE rule Description
dutch C.04.3 Default FIDE system — top half vs bottom half within each score group
dubov C.04.4.1 Adjacent pairing — rank 1 vs rank 2, rank 3 vs rank 4, etc.
burstein C.04.4.2 Rank 1 vs rank last, rank 2 vs rank second-to-last, etc.

standings()

function standings(
  players: Player[],
  games: Game[],
  tiebreaks: Tiebreak[],
): Standing[];

Returns players ranked by score, with tiebreaks applied in the order supplied. Each Standing entry includes the computed tiebreak values in tiebreaks[].

interface Standing {
  playerId: string;
  rank: number;
  score: number;
  tiebreaks: number[]; // one value per tiebreak function, in order
}

Built-in tiebreaks

All conform to the Tiebreak type and can be passed directly to standings():

type Tiebreak = (playerId: string, players: Player[], games: Game[]) => number;
Function Description
buchholz Sum of all opponents' final scores
buchholzCut Buchholz minus the single lowest opponent score
medianBuchholz Buchholz minus both lowest and highest opponent scores
sonnebornBerger Sum of (result × opponent's score) for each game
progressive Sum of cumulative scores after each round
directEncounter Score in games between tied players only

Custom tiebreaks

Any function matching the Tiebreak signature works:

import { standings } from '@echecs/swiss';
import type { Game, Player, Tiebreak } from '@echecs/swiss';

const numberOfWins: Tiebreak = (playerId, _players, games) =>
  games.filter(
    (g) =>
      (g.whiteId === playerId && g.result === 1) ||
      (g.blackId === playerId && g.result === 0),
  ).length;

const table = standings(players, games, [numberOfWins]);

Byes

A bye is represented as a Game with blackId: '' (empty string). The player in whiteId receives the bye point. Pass it in games alongside real games:

const games: Game[] = [
  { whiteId: 'alice', blackId: 'carol', result: 1, round: 1 },
  { whiteId: 'bob', blackId: '', result: 1, round: 1 }, // bye for bob
];

Using with @echecs/trf

To pair a tournament loaded from a TRF file, adapt the types:

import parse from '@echecs/trf';
import { dutch } from '@echecs/swiss';
import type { Tournament } from '@echecs/trf';
import type { Game, Player } from '@echecs/swiss';

function toPlayers(t: Tournament): Player[] {
  return t.players.map((p) => ({
    id: String(p.pairingNumber),
    rating: p.rating,
  }));
}

function toGames(t: Tournament): Game[] {
  const games: Game[] = [];
  for (const player of t.players) {
    for (const r of player.results) {
      if (r.color !== 'w' || r.opponentId === null) continue;
      let result: 0 | 0.5 | 1;
      if (r.result === '1' || r.result === '+') result = 1;
      else if (r.result === '0' || r.result === '-') result = 0;
      else if (r.result === '=') result = 0.5;
      else continue;
      games.push({
        blackId: String(r.opponentId),
        result,
        round: r.round,
        whiteId: String(player.pairingNumber),
      });
    }
  }
  return games;
}

const tournament = parse(trfString)!;
const pairings = dutch(toPlayers(tournament), toGames(tournament), 5);

Types

interface Player {
  id: string;
  rating?: number; // used for seeding in round 1
}

interface Game {
  blackId: string; // '' for a bye
  result: Result; // from white's perspective
  round: number;
  whiteId: string;
}

type Result = 0 | 0.5 | 1;

FIDE References

License

MIT

About

Swiss tournament pairing and standings algorithms following FIDE rules.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors