Purvakalpa Studio

Docs / Chess: Knight Fever

The engine

The from-scratch rules engine and search behind every bot: negamax with alpha-beta pruning and quiescence, the Bot Ladder, Game Review and Chess960.

Knight Fever plays with its own engine, written from scratch in Swift. It has two layers: ChessEngine knows the rules, and ChessAI searches for moves. The bots, Game Review, the coach’s facts and the trainers all sit on these two layers, so standard chess has one definition of a legal move across the app. Endless runs on an engine of its own, described in Endless.

The rules layer

ChessEngine holds the position (GameState) and everything that follows from the rules: legal move generation, making moves, check and checkmate, and the draws. Castling, en passant, promotion, threefold repetition, the fifty-move rule and insufficient material are all implemented, and move generation is tested against the standard reference move counts.

The same layer provides the helpers the rest of the app uses instead of keeping its own copy of the rules:

  • SAN. GameState.san(for:) writes any move in standard algebraic notation. The opening trainer resolves the book move and checks yours through it, so the two cannot disagree.
  • FEN. GameState(fen:) builds a position from FEN, which is how the endgame trainer and the puzzles set up their positions.
  • Attack maps. GameState.attackers(of:by:) lists the pieces attacking a square. The coach uses it to spot a hanging piece, and VoiceOver’s “Read attackers” action reads it aloud.
  • Hashing. Every position has a hash key, which indexes the search’s transposition table.
  • PGN. PGN.swift reads and writes games for the Theater’s import and export.

ChessAI is the playing strength: a negamax search with alpha-beta pruning and a quiescence search.

  • Negamax scores each position for the side to move. What is good for one side is equally bad for the other, so one function serves both colours by negating the score at each level.
  • Alpha-beta pruning abandons a line as soon as it is proven worse than an alternative already found. The result is the same as searching every line, while visiting far fewer positions.
  • Quiescence search keeps going past the nominal depth until the position is quiet, so a score is never taken in the middle of an exchange.

Two entry points serve the rest of the app:

// The move for a bot level; .impossible is Boss Fight
let move = ChessAI.bestMove(in: state, level: .impossible)

// Every legal move at the root, best first, each with a score in centipawns
let scored = ChessAI.analyze(in: state, depth: depth, timeBudget: budget)

analyze is a public wrapper over the search’s scored root moves. It was added for Game Review, and the Bot Ladder is built on it too.

The search runs off the main thread, in a detached task. Its move is applied only if the position is still the one it was asked about and the task has not been cancelled, so undoing or starting a new game while the bot thinks can never let a stale move land on the board.

The opponents

OpponentHow it plays
Warm-Up BotA friendly engine level for practice
Boss FightThe engine at full strength: iterative deepening instead of a fixed depth, and no deliberate mistakes
Bot LadderFive rungs from beginner up to the Boss, weakened on purpose (below)
Bot BrawlTwo bots play each other while you watch
SandboxAny position you set up, played out against the Boss

On Home, Play the computer offers Easy (Warm-Up Bot), Hard (Boss Fight) and Pick level (the Bot Ladder). The endgame and opening trainers hand their positions to the Boss, so the engine defends properly, and trainer games never count towards your rating.

The Bot Ladder

The ladder is a thin layer over the search, not a second engine. Each rung is a persona that takes the scored list from analyze and turns it into a move with three settings:

  • Depth: how far analyze looks.
  • Error rate: the chance of not playing the top move on a given turn.
  • Slip window: when the persona errs, it picks at random among the moves within this many centipawns of the best. A wider window makes a weaker, more random bot.

In simplified form:

func move(in state: GameState) -> Move? {
    if useIterative { return ChessAI.bestMove(in: state, level: .impossible) }  // top rung
    let scored = ChessAI.analyze(in: state, depth: depth, timeBudget: budget)
    guard let best = scored.first else { return nil }
    if slipWindow == 0 || Double.random(in: 0..<1) >= errorRate { return best.move }
    let pool = scored.filter { best.score - $0.score <= slipWindow }
    return pool.randomElement()?.move ?? best.move
}
Target EloDepthError rateSlip window (centipawns)
60010.55300
100020.35180
140030.18110
180040.0760
2200Iterative (the Boss)00

The Elo figures are design targets for each rung, not measured ratings. Low rungs pair a shallow search with frequent, bounded slips, which makes opponents that miss things and can be beaten. The top rung is the Boss itself. The ladder is opt-in: with it off, Warm-Up Bot and Boss Fight play exactly as before.

Game Review

Open a finished game in the Theater and choose Game Review. The reviewer replays the game, runs analyze at every ply (depth 2, with a small time budget) and grades each move by its centipawn loss against the engine’s best: best, good, inaccuracy, mistake or blunder. Each side’s accuracy falls off exponentially as its average centipawn loss grows. The summary is written from templates, so the same game always gets the same words.

The review shows an evaluation bar, the accuracy scores, a badge on every move and the engine’s suggested better move, with controls to step through the game. It runs on the device, off the main thread, and covers standard games only.

Chess960

Chess960 maps a Scharnagl id (0 to 959; id 518 is the standard RNBQKBNR) to a back rank, and the mapping was checked against python-chess for all 960 ids. A Chess960 game reuses the engine and the board unchanged and is played pass-and-play.

Castling is not in yet. The engine’s castling code is written for the standard king and rook squares, so Chess960 games start without castling rights rather than risk an illegal castle. Castling from any start files is on the roadmap.