Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: CI

on:
push:
branches: [main]
pull_request:
workflow_dispatch:

jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
node-version: ['18.x', '20.x', '22.x']
steps:
- uses: actions/checkout@v4

- uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'npm'

- run: npm ci

- name: Typecheck
run: npm run typecheck

- name: Test
run: npm test

- name: Build
run: npm run build

- name: Verify published entry points
run: npm run test:package
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,6 @@ coverage/
.env
.env.local


# TypeScript build info
*.tsbuildinfo
46 changes: 0 additions & 46 deletions .npmignore

This file was deleted.

5 changes: 5 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"semi": false,
"singleQuote": true,
"trailingComma": "es5"
}
95 changes: 94 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,100 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [2.0.0] - 2026-09-12

Three formats instead of one. `double-elimination` now generates single
elimination and round robin tournaments as well, all returning the same match
shape, plus a standings calculator for league and group play.

### Added

- **Round robin** via `generateRoundRobin`: circle-method fixtures seeded so the
top two seeds meet in the final round, sides balanced as evenly as the round
count allows, rests instead of empty matches for odd fields, multi-leg seasons
(`legs`) that replay fixtures with the sides swapped, and snake-seeded group
stages (`groupCount`).
- **Standings** via `calculateStandings`: points, wins, draws, losses, scores
and ranks from whatever results exist so far, with a configurable points table
and tiebreakers (`headToHead`, `scoreDifference`, `scoreFor`, `wins`, `seed`)
applied in order, each only to the rows the previous one left level.
Participants nothing separates share a rank.
- **Single elimination** via `generateSingleElimination`, with an optional
`thirdPlaceMatch`. Previously this was only reachable through
`losersStartRoundsBeforeFinal: 0`.
- **`generateTournament({ format, ... })`**, a discriminated union over all three
formats, for applications that store the format as data.
- **Grand final support** for double elimination via the `grandFinal` option:
- `'none'` (default) — the winners final decides 1st/2nd and the losers final
decides 3rd/4th, as in 1.x
- `'single'` — the winners final loser drops to the losers final and the two
bracket winners meet once for the title
- `'reset'` — as `'single'`, plus a bracket reset match, played only when the
losers bracket representative wins the first grand final
- A `LICENSE` file, a CI workflow running typecheck, tests, build and a
published-entry-point check on Node 18, 20 and 22, and an `npm run demo`
script that prints any tournament the package can generate.

### Fixed

- **Byes no longer stall the losers bracket**: byes were only resolved in the
first winners round, so with any participant count that isn't a power of 2 the
losers bracket could contain matches that never became playable — a match
waiting on a loser that does not exist, or a match with no entrants at all —
and everything downstream of them stalled. Byes are now resolved through the
whole bracket: walkover winners are pre-placed, a walkover no longer reserves a
losers bracket slot (`loserTo` is `null`), and matches that could only ever
receive one player are bypassed so the feeding match points at what came next.
Unreachable matches are kept in the returned array with empty slots and no
routing, so round and position numbering stays stable for rendering.
- **The published package now loads in Node**: the build emitted ES module syntax
into a package with no `"type": "module"` and extensionless relative imports,
so both `require('double-elimination')` and `import` failed outside a bundler.
The package now ships separate ESM and CommonJS builds behind an `exports` map,
verified on every CI run.
- **Rematch prevention in large brackets**: losers from winners round 3 onwards
kept their bracket position, which let players meet opponents they had already
beaten as early as losers round 4. Each wave of losers is now reordered on a
rotating cycle (reverse, reverse-and-half-shift, half-shift, unchanged). Over
200 random 64-player tournaments this moves the first possible rematch from
losers round 4 to losers round 7 and cuts rematches from ~2.8 to ~0.4 per
tournament. Brackets of 16 or fewer are unchanged.
- **Invalid participants are rejected instead of corrupting the bracket**: seeds
that were not exactly `1..N` silently left participants out of the bracket, and
duplicate seeds dropped players without warning. Participants are now ranked by
seed, so any unique ascending seed values work, and duplicate seeds, duplicate
`registrationId`s, non-numeric seeds, an `idFactory` that repeats ids, a
missing `eventId`, and a fractional `losersStartRoundsBeforeFinal` all throw.
- **Exact bracket sizing**: `nextPowerOf2` used `Math.log2`, which is not
guaranteed to be exact for large inputs; sizing is now integer arithmetic.

### Changed

- **`bracketType` is now `'winners' | 'losers' | 'grandFinal' | 'roundRobin'`.**
TypeScript code that exhaustively narrows on it needs the new cases, even
though neither value appears unless you ask for it.
- **Every match carries `group` and `leg`**, `null` and `1` respectively outside
a round robin. Persisting matches with a strict schema means adding the
columns or dropping the fields.
- **Types renamed**, with the old names kept as deprecated aliases:
`BracketMatch` → `TournamentMatch`, `BracketType` → `MatchType`,
`GeneratorOptions` → `DoubleEliminationOptions`.
- Brackets of 32 or more participants have a different losers bracket layout
because of the routing fix above. A bracket generated with an earlier version
will not match one regenerated with this version — finish in-flight
tournaments on the version that created them.
- The package is published as ESM with a CommonJS fallback; `src` is published
alongside `dist` so source maps resolve.
- The package description and keywords now cover all three formats. The npm
name stays `double-elimination`.

### Migration

`generateDoubleElimination` takes the same options and produces the same
brackets for 16 participants or fewer, so most upgrades are just the install.
See [Migrating from 1.x](README.md#migrating-from-1x) for the four things to
check.

## [1.2.2] - 2024-12-16

### Changed
Expand Down Expand Up @@ -87,4 +181,3 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Winners and losers bracket routing
- TypeScript support with full type definitions
- Simplified format: WB Finals winner = 1st, loser = 2nd; LB Finals winner = 3rd, loser = 4th

35 changes: 31 additions & 4 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ Enhancement suggestions are tracked as GitHub issues. When creating an enhanceme
2. **Make your changes** following the coding standards below
3. **Add tests** for any new functionality
4. **Update documentation** if needed
5. **Ensure all tests pass** (`npm test`)
5. **Ensure all tests pass** (`npm test`), the package still typechecks
(`npm run typecheck`), and the built entry points load
(`npm run build && npm run test:package`)
6. **Commit your changes** with clear commit messages
7. **Push to your fork** and submit a pull request

Expand Down Expand Up @@ -126,13 +128,38 @@ are in opposite halves for large brackets.
```
double-elimination/
├── src/ # Source TypeScript files
├── dist/ # Compiled JavaScript (generated)
├── tests/ # Test files
│ ├── types.ts # The shared match shape and every option type
│ ├── participants.ts # Shared validation, seeding ranks, match factory
│ ├── generateTournament.ts # Format dispatcher
│ ├── planBracket.ts # Option validation and derived bracket sizes
│ ├── createWinnersBracket.ts # Winners bracket skeleton and wiring
│ ├── createLosersBracket.ts # Losers bracket skeleton and wiring
│ ├── createGrandFinal.ts # Grand final and bracket reset
│ ├── wireLoserRouting.ts # Winners -> losers bracket routing
│ ├── resolveByes.ts # Walkover resolution across the bracket
│ ├── roundRobinSchedule.ts # Circle method and snake seeding
│ ├── generateRoundRobin.ts # Round robin fixtures, legs and groups
│ ├── calculateStandings.ts # League tables and tiebreakers
│ ├── generateSingleElimination.ts
│ └── generateDoubleElimination.ts
├── tests/ # Test files (helpers.ts holds a bracket simulator)
├── scripts/ # Build and package verification scripts
├── website/ # Interactive demo site
├── dist/ # Compiled JavaScript, ESM and CJS (generated)
├── package.json # Package configuration
├── tsconfig.json # TypeScript configuration
├── tsconfig.json # TypeScript configuration (ESM build)
├── tsconfig.cjs.json # CommonJS build overrides
└── README.md # Documentation
```

Changes to bracket structure should come with a test in
`tests/invariants.test.ts`: it plays every bracket size out with random results
and checks that nothing stalls, that every player but the champion accumulates
two losses, and that rematches stay rare and late. Round robin scheduling has
the equivalent in `tests/roundRobin.test.ts`, which checks across field sizes
that every pair meets exactly once per leg, nobody is scheduled twice in a
round, and the sides stay balanced.

## Areas for Contribution

- **Bug fixes**: Fix reported issues
Expand Down
Loading
Loading