Skip to content

About

Offline chess app for Android and desktop - Stockfish, Maia, DGT Pegasus & Chessnut Air support. No account, no ads, GPLv3.

Topics

Resources

Stars

5 stars

Watchers

1 watching

Forks

Latest commit

 

History

190 Commits

Folders and files

Repository files navigation

MoveAPiece

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.

What it does

  • 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

Physical boards

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.

Tech stack

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

Building

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

Android

./gradlew :app:assembleDebug

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

Reproducible builds

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 file

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

Signed release builds (GitHub Release, Google Play)

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

Desktop

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

Testing

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

Project structure

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

License

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.

AI-assisted development

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.

Status

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.

About

Offline chess app for Android and desktop - Stockfish, Maia, DGT Pegasus & Chessnut Air support. No account, no ads, GPLv3.

Topics

Resources

Stars

5 stars

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages