JSPM

@echecs/swiss

1.0.0
  • ESM via JSPM
  • ES Module Entrypoint
  • Export Map
  • Keywords
  • License
  • Repository URL
  • TypeScript Types
  • README
  • Created
  • Published
  • Downloads 32
  • Score
    100M100P100Q78909F
  • License MIT

Swiss chess tournament pairings and standings following FIDE rules. Implements Dutch, Dubov, and Burstein systems with Buchholz, Sonneborn-Berger, and progressive tiebreaks. Zero dependencies.

Package Exports

  • @echecs/swiss
  • @echecs/swiss/burstein
  • @echecs/swiss/double
  • @echecs/swiss/dubov
  • @echecs/swiss/dutch
  • @echecs/swiss/lim
  • @echecs/swiss/team

Readme

Swiss

npm Test Coverage License: MIT

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

Six FIDE-approved pairing systems are supported: Dutch (C.04.3), Dubov (C.04.4.1), Burstein (C.04.4.2), Lim (C.04.4.3), Double-Swiss (C.04.5), and Swiss Team (C.04.6). 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.
lim C.04.4.3 Lim system — bi-directional pairing with strict color rules
doubleSwiss C.04.5 Two-game match Swiss — each pairing is a two-game match
swissTeam C.04.6 Team Swiss — teams as players, Type A color preferences

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]);

Double-Swiss matches

In Double-Swiss (doubleSwiss), each pairing is a two-game match. Record both games with the same round number:

import { doubleSwiss } from '@echecs/swiss';

const round1 = doubleSwiss(players, [], 1);
// round1.pairings[0] = { whiteId: 'alice', blackId: 'bob' }

// Record both games of the match
const games: Game[] = [
  { whiteId: 'alice', blackId: 'bob', result: 1, round: 1 }, // game 1
  { whiteId: 'bob', blackId: 'alice', result: 0.5, round: 1 }, // game 2
];
// Alice scored 1 + 0.5 = 1.5 points for this match

A Double-Swiss bye awards 1.5 points (one win + one draw), recorded as two game entries with blackId: '':

const byeGames: Game[] = [
  { whiteId: 'carol', blackId: '', result: 1, round: 1 },
  { whiteId: 'carol', blackId: '', result: 0.5, round: 1 },
];

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