Open it and play. MoveAPiece is a chess app for Android and desktop (Windows/macOS/Linux) that starts straight into a game against Stockfish, with no account, no network, no setup and no settings screen. One "New Game" dialog covers every choice there is - opponent, colour, strength, opening - and the app remembers it for next time.
If you own a DGT Pegasus or a Chessnut Air, Air+, Pro or Go, connect it over Bluetooth LE and play on the real board instead of the screen. The app works out which board it is on its own.
Written in Java, GPLv3, fully offline, no proprietary services. The Android
app is built for distribution via F-Droid; the
desktop app is a native, double-clickable application image made with the
JDK's own jpackage.
- Play against Stockfish (adjustable Elo 1320–3190), against Maia (a human-like opponent that plays like a player of a chosen rating, 800–2400, see the Tech stack table), or against another person
- Drill an opening: pick one of 20 well-known lines, the app plays the book side and accepts only the correct reply
- Look an opening up in a step-through library
- Ask for a hint, see a live evaluation if you switch it on, get a blunder warning, analyse the finished game
- Undo, redo, flip the board, import and export PGN (multi-game files included)
- Sound effects, six UI languages (English, German, French, Spanish, Italian, Dutch), portrait and landscape on Android
Moves made on the board are detected and applied to the game; the opponent's replies light up on the board's LEDs. The screen stays usable at the same time - the board is a second input, not a replacement - and picks back up correctly after a disconnect or a screen move made while it was away. Pick the board from the scan list and the app probes its GATT services to tell a Pegasus from a Chessnut; there is no board type to choose.
- DGT Pegasus senses occupancy only: moves are inferred from which squares emptied and filled, promotions are asked on screen, and a capture swapped too quickly on its destination is resolved with a banner hint. Hardware-verified on Android, macOS (Apple Silicon and Intel) and Windows; the Linux transport is implementation-complete but not yet hardware-verified.
- Chessnut Air, Air+, Pro, Go identify every piece, so captures and
promotions are recognised directly, a position set up on the board can be
taken over into the game, the board's speaker signals check, checkmate and
a piece put where it cannot go, and the NEW GAME button restarts what was
last played (two presses while a game is under way). Hardware-verified with a
Chessnut Air on Android and macOS; Windows and Linux share the same
transports and are expected to work but are not yet verified. The Air+,
Pro and Go speak the same BLE protocol according to the vendor's EasyLink
SDK and independent implementations, but have not been tested with
MoveAPiece yet - reports (and captures made with
tools/chessnut-sniffer/) are welcome. The Chessnut Move uses a different command set and is not supported; the Evo connects to third-party software over Wi-Fi rather than BLE.
| Area | Choice |
|---|---|
| Language | Java (no Kotlin) |
| Android UI | Material 3 Components, View Binding, ConstraintLayout |
| Desktop UI | JavaFX, styled with a custom stylesheet using the Android app's own Material 3 colors (desktop/.../app.css) |
| Chess rules | chesslib (MIT) |
| Engine | Stockfish (GPLv3), built from source, driven over UCI through ProcessBuilder — via the NDK on Android, via the host's native toolchain (Makefile COMP=gcc/clang/mingw) on desktop |
| Human-like opponent | Maia-3 (AGPL-3.0, the 5M model converted to ONNX, see MAIA_PROVENANCE.md), one bundled network that takes the rating as an input (offered: 800–2400), run in-process via ONNX Runtime (MIT) — a single forward pass per move, no subprocess/UCI involved unlike Stockfish |
| Physical boards | DGT Pegasus: vendored from a companion project's core/BLE-transport modules (GPLv3, own code — see Third-Party Notices). Chessnut (Air family): chessnut-core, own implementation of the board's BLE protocol, verified byte by byte against a real Chessnut Air with the capture tool in tools/chessnut-sniffer/ (protocol notes there), plus piece-identity-based move detection on top of pegasus-core's chess model. Transport implementations (shared by both boards through a per-board GATT profile): Android (android.bluetooth.*), desktop/macOS (CoreBluetooth via a small in-house Objective-C/JNI bridge, desktop/src/main/native/macos/), desktop/Windows (Windows Runtime Windows.Devices.Bluetooth via a small in-house C++/WinRT/JNI bridge, desktop/src/main/native/windows/, MSVC-built), desktop/Linux (bluez-dbus/dbus-java, both MIT — pure Java, no native code, since BlueZ's GATT client API is fully reachable over D-Bus). No third-party dependency for macOS/Windows: the one mature cross-platform BLE library (SimpleBLE) is BUSL-1.1-licensed, not GPL/FOSS |
| License | GPLv3 (required by the Stockfish dependency); the bundled Maia-3 network and the sound effects are AGPLv3, see Third-Party Notices |
Stockfish is pinned as a git submodule, so clone with submodules (or initialize them afterwards):
git clone --recurse-submodules <repo-url>
# or, in an existing checkout:
git submodule update --init./gradlew :app:assembleDebugThe NNUE evaluation network (~99 MB) is unpacked automatically at build
time and shipped as an APK asset rather than embedded per-ABI in the native
binary, to keep the APK size down — see app/stockfish.gradle.
Bit-identical, reproducible output is a hard requirement of the Android build: NDK/build-tools/compileSdk/targetSdk are pinned exactly, the native build uses fixed parallelism and disables the linker build-id, and no native library is stripped. Verify locally with two independent clean builds:
./gradlew clean :app:assembleDebug
shasum -a 256 app/build/outputs/apk/debug/*.apk
./gradlew clean :app:assembleDebug
shasum -a 256 app/build/outputs/apk/debug/*.apk
# hashes must match per fileassembleDebug produces one APK per ABI plus a universal one (see
app/build.gradle's splits.abi - each ABI split gets a distinct
versionCode so an F-Droid client always resolves the one matching the
device). .github/workflows/build.yml builds all of them on two
independent runners (build-a, build-b) and a third job (compare)
fails the workflow if any file's SHA-256 hash differs between the two.
Both jobs sign with the same fixed, committed app/debug.keystore (see
app/build.gradle's signingConfigs.debug) instead of AGP's
auto-generated per-machine keystore, which is what makes a whole-file
hash comparison meaningful - without it, the signing block alone would
differ between runners even when the app content is identical. This
reproducibility requirement is Android-specific (driven by F-Droid's
build process) and does not apply to the desktop app.
.github/workflows/release.yml builds the release variant on every
version tag, signed with the release key held in the repository's
secrets, and attaches the per-ABI and universal APKs plus the Android App
Bundle (MoveAPiece-<version>.aab) to the GitHub Release. The bundle is
what Google Play takes; the release key serves as the Play upload key.
To build the same locally, point the build at a keystore through the
environment (app/build.gradle only enables the release signing config
when all four are set):
RELEASE_KEYSTORE_PATH=/path/to/release.keystore \
SIGNING_KEY_ALIAS=... SIGNING_KEY_PASSWORD=... SIGNING_STORE_PASSWORD=... \
./gradlew -PabiSplits=false :app:bundleRelease
# -> app/build/outputs/bundle/release/app-release.aab-PabiSplits=false switches the per-ABI splits off for that invocation;
the bundle cannot be built with them on (see the splits block in
app/build.gradle).
./gradlew :desktop:run # run directly
./gradlew :desktop:jpackageAppImage # build a native app image (desktop/build/jpackage)
./gradlew :desktop:jpackageDmg # macOS installer (DMG)
./gradlew :desktop:jpackageDeb # Linux installer (DEB, needs dpkg-deb + fakeroot)
./gradlew :desktop:jpackageMsi # Windows installer (MSI, needs the WiX Toolset v3)Stockfish is built from the same pinned submodule and the same committed
NNUE network as Android, but for the host OS/architecture directly (no
NDK) — see desktop/stockfish.gradle. jpackageAppImage bundles that
binary plus a full JRE into a double-clickable .app/.exe/Linux binary
via the JDK's jpackage tool (desktop/packaging.gradle), using the same
launcher icon as the Android app. The app image (--type app-image) is the
plain, no-prerequisites output for local testing; the actual release
artifacts are one installer per OS (DMG, DEB, MSI — see the jpackageDmg/
jpackageDeb/jpackageMsi tasks in desktop/packaging.gradle for why not
a raw app image). None of them are code-signed, so the OS shows its usual
"unidentified developer" style warning on first launch/install.
.github/workflows/desktop.yml builds and packages the desktop app across
Linux (x86-64 and arm64), macOS (Apple Silicon and Intel), and Windows on
every push; .github/workflows/release.yml builds the same legs on a version
tag and attaches the installers (plus the signed Android APKs) to the GitHub
Release.
On macOS, the build additionally compiles a small Objective-C/JNI bridge to
CoreBluetooth for the physical-board feature (desktop/pegasus-ble-macos.gradle,
desktop/src/main/native/macos/) — needs Xcode's Command Line Tools (clang)
installed, which desktop.yml's macos-latest/macos-15-intel runners
already have preinstalled. The packaged app's Info.plist declares
NSBluetoothAlwaysUsageDescription (macOS kills any process outright that
touches CoreBluetooth without it - see desktop/src/main/jpackage-resources/Info.plist).
On Windows, the build likewise compiles a small C++/WinRT/JNI bridge to
Windows.Devices.Bluetooth (desktop/pegasus-ble-windows.gradle,
desktop/src/main/native/windows/) — needs MSVC (cl.exe) and the Windows
SDK's cppwinrt.exe on PATH (a "Developer Command Prompt for VS" locally;
ilammy/msvc-dev-cmd in CI, alongside the MinGW toolchain
desktop/stockfish.gradle already needs for Stockfish itself - two
toolchains in the same job, see desktop.yml's comments on that step).
./gradlew :core:test :pegasus-core:test :chessnut-core:test :desktop:test :app:testDebugUnitTest # JVM unit tests
./gradlew :app:connectedDebugAndroidTest # instrumented tests, needs a device/emulator:core:test covers the chess logic, Stockfish engine wrapper, opening
trainer library, Maia's ONNX position encoding/policy decoding and the two
physical-board bridges (against a fake transport) shared by both apps; :chessnut-core:test covers the Chessnut protocol (against frames
captured from the real board) and the identity-based move detection; :desktop:test additionally golden-tests MaiaEngine's actual
ONNX Runtime output against the reference PyTorch implementation for several
hundred positions (multi-ply history, castling, repetition, promotion) at five
ratings from 800 to 2400; :app:connectedDebugAndroidTest covers the
Android-only pieces (StockfishEngine against a real subprocess, BoardView
real measure/layout/touch) that can't run on the plain JVM.
The library modules compile with -Xlint:all. -Perrorprone adds
Error Prone to them and to :desktop and turns
warnings into errors (CI runs it on JDK 21 and 25; release builds do not, so
they never fail over a warning), and every test run of a library module writes a
JaCoCo coverage report to build/reports/jacoco/test.
core/ Platform-agnostic chess logic, shared by :app and
:desktop (plain java-library, no Android dependency)
├── src/main/java/de/schliweb/moveapiece/
│ ├── engine/ Stockfish process wrapper (UCI over stdin/stdout);
│ │ Maia (human-like opponent): ONNX position encoding/
│ │ policy decoding/engine + its bundled rating list
│ ├── logic/ chesslib integration (ChessGame), PGN helpers
│ ├── analysis/ EngineSearchFlow: everything the one Stockfish process is asked
│ │ to search (engine move, evaluation, hint, post-game analysis)
│ │ and the grading of its answers
│ ├── board/ Physical boards: PegasusGameBridge and ChessnutGameBridge
│ │ (board <-> ChessGame, on the platform's BoardScheduler) and
│ │ PhysicalBoardBridge, one interface over both
│ └── training/ Opening trainer: curated line library + session progress
app/ Android application module
├── src/main/java/de/schliweb/moveapiece/
│ ├── ui/ Board view, sound effects, opening library/preview screens
│ ├── board/ AndroidBoardScheduler: BoardScheduler on the main thread
│ └── MainActivity.java
├── src/main/java/de/schliweb/pegasus/bluetooth/ BLE transport (vendored, profile-aware)
├── src/main/cpp/stockfish/ Stockfish, pinned git submodule
└── stockfish.gradle Drives Stockfish's own Makefile via the NDK
desktop/ JavaFX desktop application module
├── src/main/java/de/schliweb/moveapiece/desktop/
│ ├── GameController.java Wires ChessGame + StockfishEngine + MaiaEngine + the board together
│ ├── BoardCanvas.java Board rendering + click-to-move (Canvas/GraphicsContext)
│ ├── GameSetupDialog.java, BoardConnectDialog.java, OpeningLibraryWindow.java,
│ │ OpeningPreviewWindow.java
│ ├── pegasus/ Per-OS BLE transports (macOS/Windows/Linux, profile-aware)
│ ├── FxBoardScheduler.java BoardScheduler on the JavaFX Application Thread
│ ├── Messages.java Localized strings (i18n/Messages*.properties)
│ └── DesktopApp.java, Launcher.java, Styles.java, MoveSoundPlayer.java, ...
├── src/main/native/macos/, src/main/native/windows/ Objective-C/JNI and C++/WinRT/JNI
│ bridges to CoreBluetooth / Windows.Devices.Bluetooth (no native
│ code needed for Linux - see the Tech stack table)
├── src/main/resources/de/schliweb/moveapiece/desktop/
│ ├── app.css Visual theme (Android's own Material 3 colors)
│ ├── i18n/ Messages.properties + _de/_fr/_es/_it/_nl
│ ├── pieces/, sounds/, icon.png Same artwork/audio as the Android app
├── stockfish.gradle Builds Stockfish for the host OS/arch (no NDK)
├── pegasus-ble-macos.gradle, pegasus-ble-windows.gradle Compile the native BLE bridges
└── packaging.gradle jpackage app-image + per-OS icon generation
pegasus-core/ DGT Pegasus protocol + chess-rules/move-detection
module (plain java-library, zero dependencies,
vendored — see Third-Party Notices); used by
:app and :desktop (Pegasus support), by
:chessnut-core (chess model, BoardState, BLE
profile) and by :core (board interface)
chessnut-core/ Chessnut protocol, Air family (frames, board reports with
piece identity, LEDs, beep, battery, button) and
the identity-based move detection/game flow shared
by :app and :desktop
help/ The in-app help, HELP.md plus one HELP.<lang>.md per UI
language; copied into both apps by their builds together
with PRIVACY.md and rendered by core's docs.MarkdownDocument
tools/chessnut-sniffer/ Python/bleak capture tool used to verify the
Chessnut protocol on hardware, with the protocol
notes (CHESSNUT_PROTOCOL.md); not part of the build
GPLv3 (see LICENSE), required by the Stockfish dependency. Third-party
components and their licenses are listed in
THIRD-PARTY-NOTICES.md. The app collects no
data; PRIVACY.md spells that out.
Most of this codebase is written by an AI coding agent (Claude Code) under a human developer's direction and review. See AI_POLICY.md for what that means in practice.
Android: core app and physical-board support (DGT Pegasus and Chessnut Air) are feature-complete and verified (automated tests + real-hardware testing). Desktop: covers the same feature set, including both boards on macOS, Windows, and Linux. Hardware-verified on macOS (Apple Silicon and Intel, both boards) and Windows (Pegasus); Linux is implementation-complete but not yet hardware-verified - see the Physical boards section above. All five desktop CI legs (Linux x86-64/arm64, macOS Apple Silicon/Intel, Windows) build and package in CI and ship installers with every GitHub Release.
Maia (human-like opponent) is feature-complete on both platforms, ratings
800–2400 (desktop: :desktop:test's golden tests against the reference
implementation's output).
What changed in each release is recorded in CHANGELOG.md.