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

aptx-match-engine

v1.0.7

Published

A tournament engine for competitive esports brackets (Swiss, Round Robin, Single/Double Elimination, GSL)

Readme

aptx-match-engine

Pure Stage Tournament Engine - 专注于单赛制阶段状态管理的竞技赛事引擎。

v2.0 Breaking Change: 从 v1 的多阶段 TournamentState 架构迁移到 Pure Stage 架构。引擎现在只负责单个赛制的状态管理,多阶段关系由业务层管理。

安装

npm install aptx-match-engine
# or
pnpm add aptx-match-engine

核心概念

Pure Stage Engine 架构

┌─────────────────────────────────────────────────────────┐
│                    业务层 (Backend)                      │
│  - 多阶段管理 (Tournament → Stages)                      │
│  - 阶段间晋级关系                                        │
│  - duration、赛程时间等纯业务数据                          │
│  - Slot ↔ Team 映射 (通过 seedOrder)                      │
└─────────────────────────────────────────────────────────┘
                           │
                           ▼
┌─────────────────────────────────────────────────────────┐
│              Pure Stage Engine (本包)                    │
│  - 单赛制阶段结构管理                                     │
│  - 比赛结果录入 (games[].winnerSlot)                      │
│  - 派生状态计算 (比分、晋级、排名)                         │
│  - 输出 effects 供业务层处理                              │
└─────────────────────────────────────────────────────────┘

数据分离原则

| 数据类型 | 存储位置 | 说明 | |---------|---------|------| | games[].winnerSlot | Engine State | 引擎管理的原始比赛结果 | | duration | SQL (业务层) | 纯业务字段,引擎无关 | | scheduledAt | SQL (业务层) | 纯业务字段,引擎无关 | | 多阶段晋级关系 | SQL (业务层) | 业务层管理阶段间关联 |

支持的赛制

| 赛制 | 类型标识 | 说明 | |-----|---------|------| | 循环赛 | ROUND_ROBIN | 小组内循环对战,支持多分组 | | 单败淘汰 | SINGLE_ELIM | 经典淘汰赛,输一场即出局 | | 双败淘汰 | DOUBLE_ELIM | 胜者组/败者组双轨制 | | GSL 小组赛 | GSL_GROUP | 双败小组赛,每组4队 | | 瑞士轮 | SWISS | 动态配对积分制 |

快速开始

1. 创建阶段结构

import { createStage, type StageConfig } from 'aptx-match-engine';

// 定义赛制配置
const config: StageConfig = {
  type: 'SINGLE_ELIM',
  elimination: {
    seedOrder: [1, 2, 3, 4, 5, 6, 7, 8], // 8队单败淘汰
  },
  format: 'BO3', // 默认 BO3
};

// 创建阶段(仅结构,无比赛结果)
const stage = createStage('SINGLE_ELIM', config);

console.log(stage.series); // 查看生成的系列赛结构
console.log(stage.slots);  // [1, 2, 3, 4, 5, 6, 7, 8]

2. 录入比赛结果

import { recordGameResult, getAllSeriesViews } from 'aptx-match-engine';

// 记录系列赛的第一局比赛,胜者为 slotId=1 的队伍
const { stage: newStage, effects } = recordGameResult(stage, {
  seriesId: 'series-001',
  gameNumber: 1,
  winnerSlot: 1,
});

console.log(effects); // [{ type: 'game_recorded', ... }]

// 获取派生状态(解析后的比分、晋级等)
const views = getAllSeriesViews(newStage);
console.log(views[0].homeScore); // 1
console.log(views[0].isCompleted); // false (BO3 需要 2 胜)

3. 录入弃权

import { recordWalkover } from 'aptx-match-engine';

// 记录 slotId=2 的队伍弃权
const { stage: newStage, effects } = recordWalkover(stage, {
  seriesId: 'series-001',
  forfeitSlot: 2,
});

console.log(effects); // [{ type: 'series_completed', winnerSlot: 1 }, ...]

4. 计算排名

import { calculateStandings } from 'aptx-match-engine';

// 计算循环赛/瑞士轮排名
const standings = calculateStandings(stage, { win: 3, draw: 1, loss: 0 });

console.log(standings);
// [{
//   groupId: undefined,
//   entries: [
//     { slotId: 1, wins: 3, losses: 0, draws: 0, points: 9, ... },
//     { slotId: 2, wins: 2, losses: 1, draws: 0, points: 6, ... },
//     ...
//   ]
// }]

5. 瑞士轮特殊处理

import { generateSwissRound } from 'aptx-match-engine';

// 当前轮次所有比赛完成后,生成下一轮对阵
const { updatedStage, isCompleted, effects } = generateSwissRound(stage);

if (isCompleted) {
  console.log('瑞士轮全部结束');
} else {
  console.log('新生成的对阵:', updatedStage.series.filter(s => s.round === currentRound + 1));
}

API 文档

核心类型

// 基础类型
type SlotId = number;
type StageType = 'ROUND_ROBIN' | 'SINGLE_ELIM' | 'DOUBLE_ELIM' | 'GSL_GROUP' | 'SWISS';
type SeriesFormat = 'BO1' | 'BO2' | 'BO3' | 'BO5';

// Slot 引用
type SeedSlot = { type: 'seed'; slotId: SlotId };
type AdvanceSlot = {
  type: 'advance';
  sourceSeriesId: string;
  position: 'winner' | 'loser';
};
type SlotRef = SeedSlot | AdvanceSlot;

// Stage 结构(引擎核心状态)
interface Stage {
  id: string;
  type: StageType;
  format: SeriesFormat;
  config: StageConfig;
  slots: SlotId[];
  series: Series[];
}

// 系列赛
interface Series {
  id: string;
  round: number;
  sortOrder: number;
  homeSlot: SlotRef;
  awaySlot: SlotRef;
  format: SeriesFormat;
  games: { gameNumber: number; winnerSlot: SlotId | null }[];
  byeSlot?: SlotId; // 轮空标记
}

// 录入输入
interface GameResultInput {
  seriesId: string;
  gameNumber: number;
  winnerSlot: SlotId;
}

interface WalkoverInput {
  seriesId: string;
  forfeitSlot: SlotId;
}

// 录入结果
interface RecordResult {
  stage: Stage;      // 更新后的阶段状态
  effects: Effect[]; // 触发的效果(供业务层处理)
}

type Effect =
  | { type: 'game_recorded'; seriesId: string; gameNumber: number; winnerSlot: SlotId }
  | { type: 'series_completed'; seriesId: string; winnerSlot: SlotId; loserSlot?: SlotId }
  | { type: 'bye_advanced'; seriesId: string; advancedSlot: SlotId }
  | { type: 'stage_completed' };

// 系列赛派生视图
interface SeriesView {
  series: Series;
  resolvedHome: SlotId | null;  // 解析后的主队 slot
  resolvedAway: SlotId | null;  // 解析后的客队 slot
  homeScore: number;
  awayScore: number;
  isCompleted: boolean;
  winnerSlot: SlotId | null;
  isDraw: boolean;
  isReady: boolean;
}

// 排名
interface StandingEntry {
  slotId: SlotId;
  wins: number;
  losses: number;
  draws: number;
  gamesWon: number;
  gamesLost: number;
  points: number;
}

interface StageStandings {
  groupId?: string;
  entries: StandingEntry[];
}

主要函数

创建阶段

function createStage(
  type: StageType,
  config: StageConfig
): Stage

创建新的阶段结构。config 根据 type 不同而变化:

循环赛配置:

{
  type: 'ROUND_ROBIN',
  roundRobin: {
    groups: [
      { name: 'A组', slots: [1, 2, 3, 4] },
      { name: 'B组', slots: [5, 6, 7, 8] },
    ],
  },
  format: 'BO3',
}

单败淘汰配置:

{
  type: 'SINGLE_ELIM',
  elimination: {
    seedOrder: [1, 2, 3, 4, 5, 6, 7, 8],
    roundFormats: { 1: 'BO1', 2: 'BO3', 3: 'BO5' }, // 各轮赛制覆盖
  },
  format: 'BO3', // 默认赛制
}

双败淘汰配置:

{
  type: 'DOUBLE_ELIM',
  elimination: {
    seedOrder: [1, 2, 3, 4, 5, 6, 7, 8],
    winnersRoundFormats: { 1: 'BO1', 2: 'BO3', 3: 'BO5' },
    losersRoundFormats: { 1: 'BO1', 2: 'BO3' },
    grandFinalFormat: 'BO5',
  },
  format: 'BO3',
}

GSL 小组赛配置:

{
  type: 'GSL_GROUP',
  groups: [
    { name: 'A组', slots: [1, 2, 3, 4] as [SlotId, SlotId, SlotId, SlotId] },
  ],
  format: 'BO3',
}

瑞士轮配置:

{
  type: 'SWISS',
  swiss: {
    rounds: 5,
    advanceThreshold: 3,
    eliminateThreshold: 0, // 0 表示不自动淘汰
  },
  format: 'BO3',
}

录入比赛结果

function recordGameResult(
  stage: Stage,
  input: GameResultInput
): RecordResult

function recordGameResults(
  stage: Stage,
  inputs: GameResultInput[]
): RecordResult

function recordWalkover(
  stage: Stage,
  input: WalkoverInput
): RecordResult

查询派生状态

function getAllSeriesViews(stage: Stage): SeriesView[]

function getSeriesView(
  stage: Stage,
  seriesId: string,
  resolvedSlots?: Map<string, { home: SlotId | null; away: SlotId | null }>
): SeriesView | null

function resolveSlot(
  slotRef: SlotRef,
  getSeriesResult: (seriesId: string) => { winnerSlot: SlotId | null } | null
): SlotId | null

排名计算

function calculateStandings(
  stage: Stage,
  pointRules?: { win: number; draw: number; loss: number }
): StageStandings[]

function getStageRanking(stage: Stage): SlotId[]

瑞士轮

function generateSwissRound(stage: Stage): {
  updatedStage: Stage;
  isCompleted: boolean;
  effects: Effect[];
}

function isSwissCompleted(stage: Stage): boolean

function initSwissStage(
  teamCount: number,
  format: SeriesFormat,
  rounds?: number
): Stage

调整操作

function overrideGameResult(
  stage: Stage,
  input: GameResultInput
): RecordResult

function swapSeeds(
  stage: Stage,
  slotA: SlotId,
  slotB: SlotId
): Stage

function cascadeReset(
  stage: Stage,
  seriesId: string
): { stage: Stage; resetIds: string[] }

辅助函数

function getTotalGames(format: SeriesFormat): number
function generateSeriesLabel(stage: Stage, series: Series): string
function isValidSlot(stage: Stage, slotId: SlotId): boolean

从 v1 迁移到 v2

主要变更

| v1 (旧) | v2 (新) | 说明 | |--------|--------|------| | TournamentState | Stage | 引擎只管理单阶段 | | createTournament() | createStage() | 创建单阶段结构 | | recordGameResult(state, seriesId, gameNum, winnerSlot) | recordGameResult(stage, { seriesId, gameNumber, winnerSlot }) | 参数改为对象 | | state.stages[0].series | stage.series | 直接访问 | | series.homeScore | getAllSeriesViews(stage)[0].homeScore | 派生状态需通过 view 获取 | | series.duration | 移除 | 纯业务字段,引擎无关 | | series.status | 移除 | 派生状态,通过 view.isCompleted 获取 |

迁移示例

v1 代码:

import { createTournament, recordGameResult } from 'aptx-match-engine';

const { state } = createTournament({ teamCount: 8, stages: [config] });
const { state: newState } = recordGameResult(state, seriesId, 1, 2);
console.log(newState.stages[0].series[0].homeScore);

v2 代码:

import { createStage, recordGameResult, getAllSeriesViews } from 'aptx-match-engine';

const stage = createStage('SINGLE_ELIM', config);
const { stage: newStage } = recordGameResult(stage, { seriesId, gameNumber: 1, winnerSlot: 2 });
const views = getAllSeriesViews(newStage);
console.log(views[0].homeScore);

本地开发

构建

cd packages/tournament-engine
pnpm install
pnpm build

测试

pnpm test

可视化调试

pnpm preview:dev

访问 http://localhost:3333 查看交互式对阵图。

License

MIT