{"_id":"@anatolykhelmer/judo-core","name":"@anatolykhelmer/judo-core","dist-tags":{"latest":"0.1.0"},"versions":{"0.1.0":{"name":"@anatolykhelmer/judo-core","version":"0.1.0","description":"Framework-agnostic tournament engine for judo competitions: IJF repechage, double elimination and round-robin brackets as pure, serializable data.","keywords":["judo","tournament","bracket","repechage","double-elimination","round-robin","single-elimination","seeding","sports"],"license":"MIT","author":{"name":"Anatoly Khelmer"},"repository":{"type":"git","url":"git+https://github.com/anatolykhelmer/judo-core.git"},"bugs":{"url":"https://github.com/anatolykhelmer/judo-core/issues"},"homepage":"https://github.com/anatolykhelmer/judo-core#readme","type":"module","sideEffects":false,"exports":{".":{"import":{"types":"./dist/index.d.mts","default":"./dist/index.mjs"},"require":{"types":"./dist/index.d.cts","default":"./dist/index.cjs"}},"./package.json":"./package.json"},"main":"./dist/index.cjs","module":"./dist/index.mjs","types":"./dist/index.d.mts","engines":{"node":">=20"},"publishConfig":{"access":"public"},"scripts":{"build":"tsdown","typecheck":"tsc --noEmit && tsc --noEmit -p tsconfig.examples.json","test":"vitest run","test:watch":"vitest","coverage":"vitest run --coverage","lint":"biome check .","lint:fix":"biome check --write .","example":"tsx examples/simulate.ts","prepublishOnly":"npm run build"},"devDependencies":{"@biomejs/biome":"^2.3.14","@types/node":"^26.2.0","@vitest/coverage-v8":"^4.0.19","fast-check":"^4.5.0","tsdown":"^0.16.1","tsx":"^4.20.7","typescript":"^5.9.3","vitest":"^4.0.19"},"gitHead":"b6969175426cc3b1d7febc977fd5fc7b964a4265","_id":"@anatolykhelmer/judo-core@0.1.0","_nodeVersion":"26.4.0","_npmVersion":"11.17.0","dist":{"integrity":"sha512-GmX8Fu2qFGacd1hIXtYEtQvXMOm/LYgxphPmyFP4KSjjWKZoz+OmKVOS+h6/hHjTOqnmAc+S5xoZ5P+F7Aghxw==","shasum":"3fc3bf96175ca85591e1419f422960764dcbd3f3","tarball":"https://registry.npmjs.org/@anatolykhelmer/judo-core/-/judo-core-0.1.0.tgz","fileCount":9,"unpackedSize":313996,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEQCIFwbCiUP+AWDmb7eLkNPPIp6ZLO5fHtlipllQ+LeHxkXAiABuEmIja9RmRVtBgpa3SpmPrqG95CTHsuhA10RSeR7PA=="}]},"_npmUser":{"name":"anatolykhelmer","email":"anatoly.khelmer@gmail.com"},"directories":{},"maintainers":[{"name":"anatolykhelmer","email":"anatoly.khelmer@gmail.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/judo-core_0.1.0_1786347573928_0.9006250371729081"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-10T07:39:33.701Z","0.1.0":"2026-08-10T07:39:34.104Z","modified":"2026-08-10T07:39:34.361Z"},"maintainers":[{"name":"anatolykhelmer","email":"anatoly.khelmer@gmail.com"}],"description":"Framework-agnostic tournament engine for judo competitions: IJF repechage, double elimination and round-robin brackets as pure, serializable data.","homepage":"https://github.com/anatolykhelmer/judo-core#readme","keywords":["judo","tournament","bracket","repechage","double-elimination","round-robin","single-elimination","seeding","sports"],"repository":{"type":"git","url":"git+https://github.com/anatolykhelmer/judo-core.git"},"author":{"name":"Anatoly Khelmer"},"bugs":{"url":"https://github.com/anatolykhelmer/judo-core/issues"},"license":"MIT","readme":"# @anatolykhelmer/judo-core\n\nA tournament engine for judo competitions — brackets, seeding, promotion and\nstandings — with no database, no clock and no framework attached.\n\nA competition is plain data. Every operation is a pure function from one state\nto the next, so you can store a competition as JSON, send it over the wire,\nreplay it, diff it, or drive it from React, a worker or a CLI without the\nengine caring which.\n\n```bash\nnpm install @anatolykhelmer/judo-core\n```\n\n## Quick start\n\n```ts\nimport {\n  createCompetition,\n  applyResult,\n  getNextMatch,\n  getStandings,\n} from '@anatolykhelmer/judo-core';\n\nconst competitors = [\n  { id: '1', name: 'Ono' },\n  { id: '2', name: 'Riner' },\n  { id: '3', name: 'Krpálek' },\n  { id: '4', name: 'Abe' },\n  { id: '5', name: 'Heydarov' },\n];\n\n// Seeded draws are reproducible — the same seed always yields the same bracket.\nlet competition = createCompetition(competitors, {\n  format: 'olympic',\n  seed: 'worlds-2026',\n});\n\nlet match = getNextMatch(competition);\nwhile (match !== null) {\n  competition = applyResult(competition, match.id, {\n    kind: 'win',\n    winner: 'white',\n    by: 'ippon',\n  });\n  match = getNextMatch(competition);\n}\n\nconsole.log(getStandings(competition));\n// { kind: 'elimination', complete: true, places: [ { place: 1, competitorId: '3' }, … ] }\n```\n\nByes are handled for you: `createCompetition` settles every match the draw\nalready decides, so the first match you are offered is a real contest.\n\n## Formats\n\n| Format | What it is | Minimum field |\n| --- | --- | --- |\n| `olympic` | Single elimination with the judo repechage: quarterfinal losers get a second chance, two bronze medals | 5 |\n| `double-elimination` | Full repechage — one loss drops you to the losers bracket, two put you out | 5 |\n| `round-robin` | Everybody fights everybody, ranked by a points table | 2 |\n\nSee [docs/formats.md](docs/formats.md) for bracket diagrams, promotion rules\nand the exact repechage pairing.\n\n## Core concepts\n\n**State is data.** `CompetitionState` is `{ schemaVersion, format, competitors, matches }`\n— no classes, no cycles, no object identity. `JSON.stringify` it and you have\nlost nothing. Field order is stable, so stored competitions diff and hash cleanly.\n\n**Results are descriptive.** A result says who advances *and why*:\n\n```ts\napplyResult(competition, 'W_2_1', {\n  kind: 'win',\n  winner: 'blue',\n  by: 'waza-ari',\n  score: { white: { wazaAri: 0, yuko: 0, shido: 1 }, blue: { wazaAri: 1, yuko: 0, shido: 0 } },\n  time: '4:00',\n  technique: 'uchi-mata',\n});\n```\n\nBeyond `kind: 'win'` there is `kind: 'walkover'` (`bye`, `fusen-gachi`,\n`kiken-gachi`) and `kind: 'void'` for a double disqualification or a match\nneither competitor reached. Results are validated: you cannot declare an empty\ncorner the winner, or have someone win with three shido against them.\n\n**Nothing is mutated.** `applyResult` returns a new state and leaves the old one\nuntouched, so undo and time travel are free:\n\n```ts\nconst before = competition;\nconst after = applyResult(competition, matchId, result);\n// `before` is still the state before the result\n```\n\n**Draws are reproducible.** Randomness is injected, never reached for. Pass a\n`seed` (or your own `rng`) and the draw is auditable and repeatable.\n\n## API\n\n### Building and running\n\n- `createCompetition(competitors, { format, seed?, rng?, shuffle? })` — draw a competition\n- `applyResult(state, matchId, result)` — record a result; promotions and\n  walkovers cascade before it returns\n- `autoResult(match)` — the result a match settles into on its own, if any\n\n### Reading\n\n- `getNextMatch(state)` — the next match to call to the mat, or `null`\n- `getReadyMatches(state)` — everything that could be fought right now (parallel mats)\n- `getMatch(state, id)` / `findMatch(state, id)` / `getCompetitor(state, id)`\n- `getMatchesByRound(state)` — matches grouped into consecutive same-round blocks\n- `getStandings(state, { points? })` — the podium, or the round-robin table\n- `isComplete(state)`\n- `formatBracket(state)` — the whole draw as plain text, for terminals and test failures\n\n### Storing\n\n- `toJSON(state)` / `fromJSON(data)` — `fromJSON` validates untrusted input and\n  throws `InvalidStateError` with the offending path\n\n### Also exported\n\n`createSeededRng`, `shuffle`, `normalizeDraw`, `nextPowerOfTwo`,\n`validateResult`, `resultPoints`, `IJF_POINTS`, `winnerId`, `loserId`,\n`isReady`, `buildTopology`, and every error class.\n\n### Class facade\n\nIf you prefer methods to threading state through calls:\n\n```ts\nimport { Competition } from '@anatolykhelmer/judo-core';\n\nconst competition = Competition.create(competitors, { format: 'olympic', seed: 1 });\n\nlet match = competition.getNextMatch();\nwhile (match !== null) {\n  competition.finishMatch(match.id, { kind: 'win', winner: 'white', by: 'ippon' });\n  match = competition.getNextMatch();\n}\n\nconsole.log(competition.getStandings());\nconsole.log(String(competition));\n```\n\n`competition.state` hands back an immutable snapshot; snapshots taken before a\nresult stay valid afterwards.\n\n## Standings\n\nKnockout formats return the judo podium — gold, silver, two bronzes and two\nfifth places — truncated when the field is too small to fill every place:\n\n```ts\n{ kind: 'elimination', complete: true, places: [{ place: 1, competitorId: '3' }, …] }\n```\n\nRound-robin returns a table ranked by classification points (ippon and\nhansoku-make 10, waza-ari 7, yuko 5, decision 1 by default), then by the result between\ntied competitors, then by fewest points conceded, then by most wins by ippon.\nPass your own `points` table if your federation counts differently.\n\n## Notes\n\n- Zero runtime dependencies. ESM and CJS builds, full type declarations. Node 20+, and\n  it runs unchanged in a browser.\n- `applyResult` is O(matches) per call, since it rebuilds the match list. A\n  64-competitor knockout plays out in about 2 ms; an all-play-all of the same\n  size — 2016 matches, which nobody actually runs — takes about 400 ms.\n- The engine knows nothing about weight categories, age groups, mats or check-in.\n  Attach whatever you need to a competitor through `meta`; it is carried around untouched.\n\n## License\n\nMIT\n","readmeFilename":"README.md","_rev":"1-76c99c7f63f8fc73ca4892e4cdbe5e03"}