Skip to content

atomicstack/chess-openings

Repository files navigation

chess openings

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.

what it does

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).

features

  • 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 Board so the forwarded move carries .capture(piece) and checkState, 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 .best when 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.

architecture

swiftui + swiftdata app targeting ios. one binary, one scheme, one persistence store.

layout

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

key pieces

  • DrillSession (core/drill/DrillSession.swift) — the drill state machine. holds a ChessKit.Board, the user's history, a parallel historyByUser: [Bool] audit (so undo can skip over autoplay/reply moves), and a DrillStatus (waitingForUser / evaluating / mistake / lineComplete). submit(move) compares the user's move against the MoveOracle, 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) — reads resources/openings.json, validates every ply by replaying it through a ChessKit.Board, and upserts into swiftdata. re-runs are idempotent: a version bump in the json wipes all rows with isSeed == true (cascading to lines and progress) and re-imports. user-created openings (isSeed == false) are never touched.
  • AudioService (core/audio/AudioService.swift) — lazy per-effect AVAudioPlayer cache over 14 chess-style sfx. the sound category is set to .ambient so the app mixes politely with music.
  • swiftdata modelsOpening has many Lines (cascade delete). Line stores plies as json-encoded Data (swiftdata doesn't handle arbitrary codable arrays natively) and has an optional LineProgress (1:1) and a LineSource (masters / open). UserSettings is a singleton row carrying drill mode, mastery threshold, sounds toggle, and seededVersion for seed migrations.
  • ChessKit is the only third-party swift dep. it handles san parsing, move legality, and board state.

seed pipeline

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 /masters and /lichess with 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.

build & test

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=....

tests

  • ChessCoreTests — san parsing, position construction, side bridging, seed dto decoding
  • DrillEngineTests — drill session transitions: strict vs show-and-retry, undo semantics, autoplay, streak increment, move applied callbacks
  • AudioTests — sound classifier + mute behaviour
  • SeedLoaderVersionTests — idempotent seed, wipe-on-version-bump, user-opening preservation
  • SeedIntegrityTests — every seeded line validates move-by-move and covers both sources

regenerating the seed

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.

About

ios chess opening drill trainer

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors