an ios app for drilling chess opening lines. pick an opening, pick a line, play it move-by-move against the book until it sticks.
the app ships with a catalogue of well-known openings (italian game, ruy lopez, scotch, vienna, king's gambit, danish gambit, london system, queen's gambit, caro-kann, scandinavian, french, sicilian, king's indian, nimzo-indian, queen's gambit declined, slav). for each opening it provides multiple concrete lines — the variations you actually see in play — and lets you practise them as either white or black.
each line is a sequence of plies (half-moves). the drill engine plays the book reply after your move, so you only have to think about your side. if you play the right move the board advances; if you play the wrong move you either get the piece snapped back (strict mode) or shown the book reply and given a chance to retry (show-and-retry mode).
- two drill modes
- strict: wrong moves bounce back, no feedback
- show-and-retry: the book move is revealed after a mistake so you can replay it correctly
- mastery tracking: each line records a correct-streak. hit the
mastery threshold (default 3) without mistakes and the line is marked
learned. once learned, a line stays learned even if you make a
mistake later — only the streak counter resets. the learned row
shows both the badge and the live streak so you can see permanent
mastery and current consistency at a glance. per-opening header in
the list reads
x/y lines learned. - hint and solution buttons: hint pulses the source square between its base grey and the blue selection shade (animation phase anchored to the moment you press the button, so every press starts identical). solution draws a blue arrow from source to destination, with the arrowhead tip centred on the target square — the arrow sits over the intermediate pieces on the board (so knight jumps are readable) and under the source piece. show-and-retry mistake feedback reuses the same arrow visual instead of a square tint. pressing solution while an unrelated piece is selected deselects it so the legal-target wash doesn't mix with the arrow.
- show line: a second-row purple play/pause button in drill mode autoplays the rest of the current line, one ply per second, so you can preview a line before drilling it. tapping the button again pauses; navigating away (back button) cancels the playback so audio doesn't continue behind your back. lines completed via show line don't earn the "perfect" or "speedy" medals.
- undo / reset: undo always lands at a position where it's your turn (never the opponent's) — even after the show-line preview applied an odd number of plies. reset returns to the starting position (or to the post-scaffold state for a black-side opening). show-line autoplay is cancelled by undo, reset, or any human move.
- board sounds: chess.com-style sfx for moves, captures, castles,
promotions, and checks, with a separate "opponent move" sound for
scripted replies. capture/check classification covers both user and
engine moves (the engine reply is run through chesskit's
Boardso the forwarded move carries.capture(piece)andcheckState, not just a bare quiet-move record). togglable in settings. - dual-source lines: every opening is seeded from two lichess explorer sources — masters database (games ≥50) and online 2200-2500 blitz/rapid/classical (games ≥500). lines are grouped by source in the opening detail view so you can see what the top humans prefer vs. what strong online players actually play.
- custom library: create your own openings and lines alongside the seeded ones; seed reloads only wipe seeded entries, custom entries survive app updates.
- black-side autoplay: when you're drilling a black-side opening, the app plays white's first move for you so the board is already waiting on your reply.
- rolling mistake log: the last 20 mistakes per line are kept for future review features.
- play it out: when a line is finished you can optionally play out
the rest of the position against a built-in stockfish (via
chesskit-engine). pick a difficulty in settings (0 = very weak, 20 =
full strength). during play you can ask for a hint (pulsing source
square), reveal the best move (arrow overlay), undo, offer a
draw, or resign. a small brain icon pulses in the bottom-right corner
while stockfish is thinking. stockfish is gpl-v3; the full license is
bundled at
Chess Openings/Resources/Stockfish/LICENSE-stockfish.txt. - move-quality badges: every user move during playout is graded by
stockfish at full strength and a chess.com-style badge floats over
the destination square (brilliant / best / good / inaccuracy /
mistake / blunder). brilliant detection composes the chess.com
criteria — sacrifice, not-already-winning, still-winning-after,
best-move, minimum-knight-value, lower-value-attacker — and only
promotes
.bestwhen all hold. analysis queries run before the engine's gameplay reply so the badge appears first, matching lichess/chess.com timing. - engine resignation: if stockfish's eval stays sufficiently below zero across a sliding window of replies, it offers to resign — the user accepts the win or declines and keeps playing. draws offered by the user are accepted only when stockfish's eval is within ±50 cp of equal.
- session resume after force-quit: both mid-drill and mid-playout state are persisted to swiftdata on every applied move. relaunching drops you back exactly where you were, on the right side, with hint toggles cleared so you re-enable explicitly for the next move.
- settings panel (gear icon, top right of the drill view): drill mode (strict / show-and-retry segmented picker), mastery threshold (1-10 correct-streak target), sounds toggle, engine difficulty slider, move-quality analysis depth, badge duration, and a "reset all progress" button with a destructive confirmation dialog.
swiftui + swiftdata app targeting ios. one binary, one scheme, one persistence store.
chess openings/
├── core/
│ ├── chess/ ChessTypes, Side, SanCodec, PositionBuilder
│ ├── drill/ DrillSession, DrillMode, DrillStatus, DrillProgress,
│ │ BookPly, BookCandidate, LineSource, MoveOracle,
│ │ LineBookOracle
│ └── audio/ AudioService, SoundEffect
├── data/
│ ├── models/ Opening, Line, LineProgress, UserSettings, Mistake
│ └── seed/ SeedLoader, SeedDTO
├── views/
│ ├── board/ BoardView, SquareView, HighlightKind,
│ │ PromotionPickerView
│ ├── train/ OpeningListView, OpeningDetailView, DrillView
│ ├── library/ LibraryListView, NewOpeningView, LineEditorView
│ ├── settings/ SettingsView
│ └── shared/ FlowLayout, ProgressBarView
├── resources/ openings.json (seed), Sounds/*.mp3
└── chess_openingsapp.swift
DrillSession(core/drill/DrillSession.swift) — the drill state machine. holds aChessKit.Board, the user's history, a parallelhistoryByUser: [Bool]audit (so undo can skip over autoplay/reply moves), and aDrillStatus(waitingForUser/evaluating/mistake/lineComplete).submit(move)compares the user's move against theMoveOracle, applies the scripted reply, and updates streak + status. onmoveapplied callback fans out to the audio layer.MoveOracle(core/drill/MoveOracle.swift) — strategy protocol for "what moves does the book accept here?". the default implementation,LineBookOracle, is a pure lookup against the line's ply list. swapping in a fuzzier oracle (e.g. "any main-line transposition") is a one-type change.SeedLoader(data/seed/SeedLoader.swift) — readsresources/openings.json, validates every ply by replaying it through aChessKit.Board, and upserts into swiftdata. re-runs are idempotent: a version bump in the json wipes all rows withisSeed == true(cascading to lines and progress) and re-imports. user-created openings (isSeed == false) are never touched.AudioService(core/audio/AudioService.swift) — lazy per-effectAVAudioPlayercache over 14 chess-style sfx. the sound category is set to.ambientso the app mixes politely with music.- swiftdata models —
Openinghas manyLines (cascade delete).Linestores plies as json-encodedData(swiftdata doesn't handle arbitrary codable arrays natively) and has an optionalLineProgress(1:1) and aLineSource(masters / open).UserSettingsis a singleton row carrying drill mode, mastery threshold, sounds toggle, andseededVersionfor seed migrations. ChessKitis the only third-party swift dep. it handles san parsing, move legality, and board state.
the built-in openings under resources/openings.json are generated by a
perl script, not hand-written.
scripts/seedbuilder/seed-catalogue.json— per-opening config: name, eco code, side, root san sequence, and desired line count.scripts/seedbuilder/build-seed.pl— walks each opening through the lichess explorer api (both/mastersand/lichesswith 2200-2500 blitz/rapid/classical filter) following the most-played reply at each ply until the line hits 10-20 plies or game-count drops below the threshold (50 masters / 500 online, with softer floors for early plies). md5-keyed disk cache avoids re-fetching during iterative runs. output is atomic (write-to-tmp + rename) and requests have exponential-backoff retry for 429s and 5xx.scripts/seedbuilder/annotations.json— optional per-ply text notes that get merged into the generated seed if the san matches.scripts/seedbuilder/check-seed.pl— sanity check that every opening has at least one line from each source and that plies stay within range.
the schema version (SeedDTO.version) is bumped whenever the catalogue
or structure changes; the next app launch notices the bump and re-seeds.
all builds and tests go through the makefile at the repo root so the scheme and simulator destination are defined once.
make build # compile
make test-all # all xctests + ui tests
make test T="Chess OpeningsTests/DrillEngineTests"
make test T="Chess OpeningsTests/DrillEngineTests/test_drillsession_undo_steps_back_one_full_move"
make clean
default destination is iPhone 16 Pro; override with DESTINATION=....
ChessCoreTests— san parsing, position construction, side bridging, seed dto decodingDrillEngineTests— drill session transitions: strict vs show-and-retry, undo semantics, autoplay, streak increment, move applied callbacksAudioTests— sound classifier + mute behaviourSeedLoaderVersionTests— idempotent seed, wipe-on-version-bump, user-opening preservationSeedIntegrityTests— every seeded line validates move-by-move and covers both sources
perl scripts/seedbuilder/build-seed.pl # writes chess openings/resources/openings.json
perl scripts/seedbuilder/check-seed.pl # sanity check
put a lichess api token in lichess-api-key.txt at the repo root to
raise the explorer rate limit (file is gitignored). without a token the
script falls back to anonymous requests with a 1s delay between calls.