Skip to content

Bot Ladder

The bot ladder is the player-facing ranking system for games against AI bots. It runs separately from any PvP rating — your bot ladder rating does not affect your PvP rating, and vice versa. Ladder data is opt-in: a user must explicitly enable it before their games count.

Ten strength levels, defined in apps/game-web/src/pages/profile/bot-ladder/lib/levels.ts:32 and sourced from the authoritative catalog at packages/katago-server/src/bot-level.ts:142.

CodeDisplay bandApprox strength
lvl_30kBeginner30 kyu
lvl_20kBeginner20 kyu
lvl_10kBeginner10 kyu
lvl_5kBeginner5 kyu
lvl_1kIntermediate1 kyu
lvl_5dIntermediate5 dan
lvl_1dIntermediate1 dan
lvl_3dAdvanced3 dan
lvl_7dAdvanced7 dan
lvl_10dElite10 dan

Each level config bundles a KataGo model, a visits budget, a family (band), and a baseline Elo used by BotRankService to compute deltas.

All paths are /api-prefixed (see REST API for the gateway-wide table). The bot-ladder subset:

MethodPathPurpose
GET/game/bot/ladder/me{ rating, optIn, today } — current user state.
POST/game/bot/ladder/opt-in{ optIn: boolean } — toggle ladder participation.
GET/game/bot/ladder/history?limit=50Most recent rating deltas.
GET /api/game/bot/ladder/me
Cookie: better-auth.session_token=<...>
{
"rating": 1482,
"optIn": true,
"today": {
"gamesPlayed": 3,
"wins": 2,
"losses": 1,
"energyUsed": 12
}
}
POST /api/game/bot/ladder/opt-in
Content-Type: application/json
Cookie: better-auth.session_token=<...>
{ "optIn": false }

Drizzle schema lives next to the rest of weiqi-server’s tables.

  • botRatings — one row per user. Columns: userId, rating, botRatingOptIn, gamesPlayed, wins, losses, lastPlayedAt, …
  • botRankHistory — append-only. One row per rating change. Columns: userId, rating (post-update), opponentLevel, expected, actual, createdAt, …

The rating column on botRatings is denormalised — botRankHistory is the authoritative audit log.

BotRankService uses classic Elo with a per-level expected score:

expected = 1 / (1 + 10 ^ ((opponentRating - userRating) / 400))
delta = K * (actual - expected)

K is fixed at 32 for bot games. The opponent rating comes from BOT_LEVELS[opponentLevel].rating — a stable, level-specific baseline that does not change as more users play.

BotDailyUsageService.getUsage returns today’s usage against the user’s entitlement (packages/authz):

  • BOTS_HINT_TOP3, BOTS_LIVE_EVAL, BOTS_REVIEW_FULL — feature gates.
  • dailyGameCap — total bot games per UTC day for free tier.
  • PRO tier has no daily cap and full hint access.

The player profile page (apps/game-web/src/pages/profile/bot-ladder/bot-ladder.page.tsx:39) shows a live RatingChart and a HistoryTable. Both consume the useBotLadder hook (apps/game-web/src/entities/bot-game/hooks/useBotLadder.ts), which in turn talks to botGameApi (src/entities/bot-game/api/bot-game.api.ts:46) over a plain fetch.

Socket.IO is not used on the ladder page — the data set is small and the user reloads infrequently. The page is rendered with SSR + hydration; the initial rating comes from the server, updates poll every 30 s.

Hindsight is via the same endpoints as a normal bot game, with an entitlement check:

  • /game/bot/games/:id/hint — requires BOTS_HINT_TOP3 (free) or higher.
  • /game/bot/games/:id/review — full KataGo review. Requires BOTS_REVIEW_FULL. Backed by runFullReview; results are cached on board_ai_reviews.

A free-tier player with the daily cap exhausted still sees the page, but the “play” button is replaced with an upgrade prompt.

The shared entity types live in apps/game-web/src/entities/bot-game/types/bot-game.ts:1-178:

export type BotLevelCode = "lvl_30k" | "lvl_20k" | "lvl_10k" | "lvl_5k"
| "lvl_1k" | "lvl_5d" | "lvl_1d" | "lvl_3d" | "lvl_7d" | "lvl_10d";
export type BotLadderRating = {
rating: number;
optIn: boolean;
today: { gamesPlayed: number; wins: number; losses: number; energyUsed: number };
};

These types are referenced from the docs whenever a bot-related endpoint is documented, so they stay in sync with the actual client API.