SwiftChessDemo is a demo app that demonstrates how to combine local chess libraries into a realistic, shippable SwiftUI chess experience. The code is intentionally small, readable, and heavily commented so you can trace how each module contributes to the final behavior. It is intended as a reference implementation for building an app with SwiftChessTools, not as a minimal sample.
Licensing note: SwiftChessDemo's original source code is licensed under the MIT
License so it can be reused as reference app code. The default app target links
with Stockfish through ../StockfishEmbedded; distributing that combined
Stockfish-linked app requires GPLv3 compliance. The app can also use the
permissively licensed ArasanEmbedded Swift package as a second embedded engine.
See LICENSE, LICENSES/, and THIRD_PARTY.md for details.
The demo is designed to show the same app-owned chess experience adapting between regular-width iPad layouts and compact iPhone layouts.
Public checkout layout:
SwiftChessDemo expects SwiftChessTools and StockfishEmbedded to be sibling
checkouts. It resolves ArasanEmbedded from GitHub through Swift Package
Manager. The parent folder can be any local directory; it does not need to be a
Git repo.
mkdir swift-chess-demo-dev
cd swift-chess-demo-dev
git clone https://github.com/Trickfest/SwiftChessDemo.git
git clone https://github.com/Trickfest/SwiftChessTools.git
git clone https://github.com/Trickfest/StockfishEmbedded.gitThe resulting layout should be:
swift-chess-demo-dev/
|-- SwiftChessDemo
|-- SwiftChessTools
`-- StockfishEmbedded
Required after clone: initialize the sibling ../StockfishEmbedded checkout
using its NNUE setup instructions. Those Stockfish neural-net files are not in
Git because they are large, but they are required to run the engine.
(cd StockfishEmbedded && Scripts/download-nnue.sh)
See ../StockfishEmbedded/README.md or
../StockfishEmbedded/Resources/NNUE/README.md for the authoritative engine
asset setup. SwiftChessDemo intentionally does not duplicate the exact NNUE
filename because it changes when StockfishEmbedded updates vendored Stockfish.
How it all fits together:
ChessCoreowns board state, legal move generation, move application, PGN parsing, FEN serialization, SAN move records, game status, and draw claims.ChessUIrenders the board and emits user move gestures. It also supplies the visible chessboard components used by the demo: piece sets, board themes, coordinate labels,ChessGameStatusView,ChessMoveListView,ChessEvaluationBar, and app-suppliedChessBoardArrowsuggestions.ChessUCIformats UCI command strings and parsesinfoandbestmovelines into typed values.- SwiftChessDemo owns app policy: view-model state, engine timing, move-provider selection, user display preferences, recoverable engine status feedback, error handling, and when validated moves should mutate the game.
StockfishMoveProviderwraps the embedded Stockfish lifecycle and serialized UCI searches for live play.ArasanMoveProviderwraps the embedded Arasan lifecycle and the same serialized UCI search contract for live play.ScenarioReplayMoveProvidersupplies deterministic non-live-engine moves for scenario replay and scenario-backed tests.- The sibling
../StockfishEmbeddedproject supplies engine moves over the UCI protocol viaSFEngine. - The
ArasanEmbeddedSwift package supplies an alternative engine over the UCI protocol viaArasanEngine.
Data flow at a glance:
- User moves on the board ->
ChessUI->GameViewModel.handleUserMove. - The move is validated/applied in
ChessCore, then serialized to FEN. - FEN is pushed back into
ChessUIto update the board UI. - Terminal game state and claimable draws are read from ChessCore's
Game.statusand draw-claim APIs, then rendered withChessGameStatusView. - Legal moves are also captured as
ChessMoveRecordvalues before they are applied, soChessMoveListViewcan render SAN without owning the game. - When it is the engine's turn, the selected live engine provider uses
ChessUCIto format the UCIpositionandgocommand strings sent to the embedded engine. - Opponent searches start immediately after the user's move. SwiftChessDemo keeps a minimum visible thinking interval before applying very fast replies, but it does not add that interval on top of slower real searches.
- The selected live engine streams
infolines through its provider, whereChessUCIparses them into White-positive evaluation values forChessEvaluationBar. - Live engine searches use UCI
go movetime, so the selected move-time value is the engine's intended thinking budget. SwiftChessDemo also keeps an app-side safety timeout slightly above that budget, asks the engine to stop if needed, applies the returnedbestmovewhen available, and uses the existing status display for a brief nonfatal notice before returning to the normal game status. - When suggestions are enabled, SwiftChessDemo asks the selected engine for up to three MultiPV analysis lines on the human player's turn, caches the ranked first moves, and filters the visible ChessUI arrows according to the user's suggestion-count picker. ChessUI renders the arrows but does not decide which moves to suggest.
- The engine returns
bestmove;ChessUCIparses it into aChessCore.Move. - Engine-vs-engine mode starts paused with default Stockfish-vs-Arasan settings. The game screen is the single place to choose White and Black engines, move-time budgets, pacing, and stress options before pressing Play or Step.
- Engine-vs-engine uses the same UCI
go movetimesearch policy as human-vs-engine play. The app derives its safety timeout from the selected move time, so there is no separate user-facing timeout setting. - Engine-vs-engine mode automatically claims ChessCore draw claims such as threefold repetition and the 50-move rule. Human-vs-engine games leave those claimable draws available to the player instead of ending automatically.
- Completed games keep the user on the game screen after the result alert is
dismissed, so the final board and move history can still be inspected. In
engine-vs-engine mode, the primary playback control becomes
Play Againand restarts from the initial position while preserving the current demo settings. - Engine-vs-engine stress mode can randomize the engine and/or move-time budget before each move within a configured range. Randomization happens before a search starts, never while an engine request is in flight, so the mode is useful for exercising engine switching and safety-timeout boundaries without changing the normal human-vs-engine game.
- Scenario replay uses a named JSON scenario plus a bundled PGN fixture. The
PGN is parsed through
ChessCore.PGNSerializer, concrete moves are held in memory, and the same move-application path updates the board, move list, and status UI without starting a live engine.
Reference-app boundaries:
- SwiftChessTools provides reusable chess building blocks; SwiftChessDemo shows one app-owned composition of those blocks.
- ChessUI renders app-supplied state. It does not decide legal policy, own the game model, run engines, choose suggestion moves, or apply moves on behalf of the app.
- ChessUCI formats and parses protocol text. It does not start an engine, serialize searches, choose move-time or MultiPV policy, or decide how analysis should affect UI.
- Stockfish integration lives in the default app target through
StockfishEmbedded. That linked distribution must comply with GPLv3.ArasanEmbeddedis also available from the same game screen as a permissively licensed engine option. - The scenario system is an app-level test and demonstration harness, not a SwiftChessTools public API.
Key files to read:
SwiftChessDemo/ContentView.swift: setup UI for choosing the game mode and human side before entering the game screen.SwiftChessDemo/GameView.swift: board UI, live piece-set, board-theme, engine-selection, and coordinate-label switching during play, visible ChessUI status and move-list components, status-row engine activity and timeout notices, optional evaluation-bar display, in-game engine move-time control, selectable move-suggestion arrows, engine-vs-engine playback controls, terminal-result handling, compact horizontal move-list layout on iPhone, and navigation flow.SwiftChessDemo/GameViewModel.swift: display state, safe move application, provider event handling, minimum-visible-thinking timing, recoverable timeout fallback, evaluation normalization, selected-engine MultiPV suggestion mapping, engine-vs-engine loop control, automatic draw-claim policy for engine-vs-engine games, engine-demo replay reset behavior, stress-mode randomization, and ChessCore game-status integration.SwiftChessDemo/EngineDemoConfiguration.swift: value types that describe engine-vs-engine mode, per-side engine/move-time settings, pacing, and optional deterministic stress randomization.SwiftChessDemo/StockfishMoveProvider.swift: embedded Stockfish lifecycle, serializedgo movetimesearch requests, UCI command formatting/parsing, safety-timeoutstophandling, and cancelled suggestion-output handling.SwiftChessDemo/ArasanMoveProvider.swift: embedded Arasan lifecycle, serializedgo movetimesearch requests, UCI command formatting/parsing, safety-timeoutstophandling, and cancelled suggestion-output handling.SwiftChessDemo/DemoEngineProvider.swift: app-local engine abstraction and shared request/event models used by live engine providers.SwiftChessDemo/GameScenario.swift: scenario-file loading and PGN validation for deterministic replay fixtures.SwiftChessDemo/GameScenarioIndex.swift: bundled scenario catalog loading and validation used to catch scenario-resource drift.SwiftChessDemo/GameMoveProvider.swift: deterministic move-provider abstraction used by scenario replay and scenario-backed UI tests.SwiftChessDemo/Scenarios/: checked-in scenario index, authoring guide, JSON definitions, and PGN fixtures.SwiftChessDemoTests/GameScenarioUnitTests.swift: fast unit coverage for scenario loading, index validation, deterministic move-provider behavior, live-analysis refreshes, and engine-vs-engine playback/restart/stress behavior.SwiftChessDemoUITests/SwiftChessDemoUITests.swift: UI coverage for available in-game piece-set selection, board-theme selection, coordinate-label toggling, live-engine selection, status, move-list, evaluation display options, selectable suggestion arrows, engine-vs-engine setup and game-screen controls, scenario replay including terminal-result dismissal, and four-full-move game flows from both white and black perspectives.
Automated tests:
- Run the suite from this repo root:
xcodebuild -project SwiftChessDemo.xcodeproj \
-scheme SwiftChessDemo \
-configuration Debug \
-destination 'platform=iOS Simulator,name=iPhone 17 Pro' \
-derivedDataPath .build/xcode-swiftchessdemo \
-clonedSourcePackagesDirPath .build/xcode-swiftchessdemo/SourcePackages \
test- GitHub Actions runs the app-hosted unit tests after checking out
SwiftChessDemo,SwiftChessTools, andStockfishEmbeddedas siblings, resolvingArasanEmbedded, and downloading the Stockfish NNUE file. The full UI suite is the local release gate because hosted simulator UI tests are slower and more environment sensitive. - The shared local scheme includes both fast scenario unit tests and full UI
tests. The unit tests run inside the demo app host so
Bundle.mainloads the same bundled scenarios the app uses at runtime. - Scenario replay tests set
SWIFT_CHESS_DEMO_SCENARIO=<scenario-id>and optionallySWIFT_CHESS_DEMO_SCENARIO_REPLAY_DELAY=0. Each scenario id maps to a JSON file inSwiftChessDemo/Scenarios; the JSON points at the PGN fixture that supplies the validated move list. - The game-flow UI tests run named scenarios in
testDrivesWhiteortestDrivesBlackmode so one side is driven by UI-test taps while the scenario supplies the opposing replies. This keeps move-flow coverage deterministic without starting a live engine. - The game-flow tests set
SWIFT_CHESS_DEMO_UI_TEST_ENGINE_MOVE_TIME_MS=250to keep simulator runs fast. UI tests that exercise live engine replies can also setSWIFT_CHESS_DEMO_UI_TEST_ENGINE_REPLY_DELAY=1.0to reduce the visible thinking pause. Normal app launches do not set these flags and default to Stockfish unless the player selects Arasan from the game screen. - Engine-vs-engine unit tests use fake engine providers to verify that Play, Pause, Step, per-side engine/move-time settings, safety-timeout propagation, automatic draw-claim policy, and seeded stress randomization behave deterministically without starting live engine processes.
- Engine-vs-engine UI coverage verifies that the setup mode keeps duplicate engine-demo controls off the setup screen, then launches into a paused game with demo-only playback controls visible and the normal live-game engine picker hidden.
- Evaluation-bar UI coverage can set
SWIFT_CHESS_DEMO_UI_TEST_EVALUATIONvalues such ascp:85,mate:white:3, ormate:black:2so the visual state is deterministic without live engine analysis. - Suggestion-arrow UI coverage uses scenario-backed move suggestions plus
optional
SWIFT_CHESS_DEMO_UI_TEST_SUGGESTION_ARROW_COUNTvalues from0through3so rendered arrows are deterministic without live engine analysis. - Scenario-index coverage sets
SWIFT_CHESS_DEMO_VALIDATE_SCENARIO_INDEX=1so the app validatesScenarios/index.json, bundled scenario JSON files, and PGN loading through the same bundle path used at runtime.
Scenario files:
SwiftChessDemo/Scenarios/index.jsonis the durable scenario catalog. It lists every scenario id, tags, purpose, selected metadata, and the PGN resource each scenario uses.- Scenario JSON is the per-scenario test description: id, title, PGN resource, playback mode, optional perspective, optional stop ply, and expected-status notes.
- PGN remains the readable source of moves. SwiftChessDemo does not check in a normalized move-list artifact; it parses and validates the PGN on launch.
- Supported playback modes are:
automaticReplay: replay both sides without user input or a live engine.testDrivesWhite: expose test-only buttons for White moves and let the scenario provide Black replies.testDrivesBlack: expose test-only buttons for Black moves and let the scenario provide White replies.
- See
SwiftChessDemo/Scenarios/README.mdfor scenario authoring steps, index-field expectations, and the manual launch environment variables.
Sibling dependencies:
../SwiftChessTools: public sibling checkout that provides theChessCore,ChessUI, andChessUCISwift package products.../StockfishEmbedded: public sibling checkout that provides theSFEngine-iOSXcode project product.ArasanEmbedded: public Swift package dependency resolved from GitHub that provides theArasanEmbeddedproduct.- The parent folder can be any local directory; it does not need to be a Git repo.
- Reference details live in
THIRD_PARTY.md.

