npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@anatolykhelmer/judo-core

v0.1.0

Published

Framework-agnostic tournament engine for judo competitions: IJF repechage, double elimination and round-robin brackets as pure, serializable data.

Readme

@anatolykhelmer/judo-core

A tournament engine for judo competitions — brackets, seeding, promotion and standings — with no database, no clock and no framework attached.

A competition is plain data. Every operation is a pure function from one state to the next, so you can store a competition as JSON, send it over the wire, replay it, diff it, or drive it from React, a worker or a CLI without the engine caring which.

npm install @anatolykhelmer/judo-core

Quick start

import {
  createCompetition,
  applyResult,
  getNextMatch,
  getStandings,
} from '@anatolykhelmer/judo-core';

const competitors = [
  { id: '1', name: 'Ono' },
  { id: '2', name: 'Riner' },
  { id: '3', name: 'Krpálek' },
  { id: '4', name: 'Abe' },
  { id: '5', name: 'Heydarov' },
];

// Seeded draws are reproducible — the same seed always yields the same bracket.
let competition = createCompetition(competitors, {
  format: 'olympic',
  seed: 'worlds-2026',
});

let match = getNextMatch(competition);
while (match !== null) {
  competition = applyResult(competition, match.id, {
    kind: 'win',
    winner: 'white',
    by: 'ippon',
  });
  match = getNextMatch(competition);
}

console.log(getStandings(competition));
// { kind: 'elimination', complete: true, places: [ { place: 1, competitorId: '3' }, … ] }

Byes are handled for you: createCompetition settles every match the draw already decides, so the first match you are offered is a real contest.

Formats

| Format | What it is | Minimum field | | --- | --- | --- | | olympic | Single elimination with the judo repechage: quarterfinal losers get a second chance, two bronze medals | 5 | | double-elimination | Full repechage — one loss drops you to the losers bracket, two put you out | 5 | | round-robin | Everybody fights everybody, ranked by a points table | 2 |

See docs/formats.md for bracket diagrams, promotion rules and the exact repechage pairing.

Core concepts

State is data. CompetitionState is { schemaVersion, format, competitors, matches } — no classes, no cycles, no object identity. JSON.stringify it and you have lost nothing. Field order is stable, so stored competitions diff and hash cleanly.

Results are descriptive. A result says who advances and why:

applyResult(competition, 'W_2_1', {
  kind: 'win',
  winner: 'blue',
  by: 'waza-ari',
  score: { white: { wazaAri: 0, yuko: 0, shido: 1 }, blue: { wazaAri: 1, yuko: 0, shido: 0 } },
  time: '4:00',
  technique: 'uchi-mata',
});

Beyond kind: 'win' there is kind: 'walkover' (bye, fusen-gachi, kiken-gachi) and kind: 'void' for a double disqualification or a match neither competitor reached. Results are validated: you cannot declare an empty corner the winner, or have someone win with three shido against them.

Nothing is mutated. applyResult returns a new state and leaves the old one untouched, so undo and time travel are free:

const before = competition;
const after = applyResult(competition, matchId, result);
// `before` is still the state before the result

Draws are reproducible. Randomness is injected, never reached for. Pass a seed (or your own rng) and the draw is auditable and repeatable.

API

Building and running

  • createCompetition(competitors, { format, seed?, rng?, shuffle? }) — draw a competition
  • applyResult(state, matchId, result) — record a result; promotions and walkovers cascade before it returns
  • autoResult(match) — the result a match settles into on its own, if any

Reading

  • getNextMatch(state) — the next match to call to the mat, or null
  • getReadyMatches(state) — everything that could be fought right now (parallel mats)
  • getMatch(state, id) / findMatch(state, id) / getCompetitor(state, id)
  • getMatchesByRound(state) — matches grouped into consecutive same-round blocks
  • getStandings(state, { points? }) — the podium, or the round-robin table
  • isComplete(state)
  • formatBracket(state) — the whole draw as plain text, for terminals and test failures

Storing

  • toJSON(state) / fromJSON(data)fromJSON validates untrusted input and throws InvalidStateError with the offending path

Also exported

createSeededRng, shuffle, normalizeDraw, nextPowerOfTwo, validateResult, resultPoints, IJF_POINTS, winnerId, loserId, isReady, buildTopology, and every error class.

Class facade

If you prefer methods to threading state through calls:

import { Competition } from '@anatolykhelmer/judo-core';

const competition = Competition.create(competitors, { format: 'olympic', seed: 1 });

let match = competition.getNextMatch();
while (match !== null) {
  competition.finishMatch(match.id, { kind: 'win', winner: 'white', by: 'ippon' });
  match = competition.getNextMatch();
}

console.log(competition.getStandings());
console.log(String(competition));

competition.state hands back an immutable snapshot; snapshots taken before a result stay valid afterwards.

Standings

Knockout formats return the judo podium — gold, silver, two bronzes and two fifth places — truncated when the field is too small to fill every place:

{ kind: 'elimination', complete: true, places: [{ place: 1, competitorId: '3' }, …] }

Round-robin returns a table ranked by classification points (ippon and hansoku-make 10, waza-ari 7, yuko 5, decision 1 by default), then by the result between tied competitors, then by fewest points conceded, then by most wins by ippon. Pass your own points table if your federation counts differently.

Notes

  • Zero runtime dependencies. ESM and CJS builds, full type declarations. Node 20+, and it runs unchanged in a browser.
  • applyResult is O(matches) per call, since it rebuilds the match list. A 64-competitor knockout plays out in about 2 ms; an all-play-all of the same size — 2016 matches, which nobody actually runs — takes about 400 ms.
  • The engine knows nothing about weight categories, age groups, mats or check-in. Attach whatever you need to a competitor through meta; it is carried around untouched.

License

MIT