Skip to content

Regalia v1.1.0

Choose a tag to compare

@YDW99 YDW99 released this 05 Jul 06:47
· 20 commits to main since this release
f2727bb

What's Changed

Full Changelog: v1.0.9...v1.1.0


Regalia v1.1.0

A production-grade Android chess app built around Stockfish 18, with Chess960, time control, personified animations, bilingual UI, and a full review/statistics pipeline.

versionCode 110 · versionName 1.1.0 · Min SDK 21 · Target SDK 35 · arm64-v8a (dotprod)

License: AGPL v3
License: GPL v3
Engine: Stockfish 18
Platform: Android 5.0+
Zero dependencies


🎯 Release Highlights

This release ships 8 development phases (Phase 53 → 58) layered on top of the v1.0.9 baseline, delivering:

Category Count Notable Items
🐛 Bug fixes 20+ Chess960 castling rook-loss, PGN single-line parse, portrait review scroll, visual-annotation cache residue
🔒 Concurrency hardening 3 P0 stopLatch TOCTOU race, heartbeat deadlock, postJsCallback lifecycle guard
✨ New features 1 Every-5-moves PGN {} eval annotation (bilingual, White-perspective)
🎨 Visual refinements 6+ Check-response arrows, pixel-perfect chart alignment, nav-button centering
📝 Documentation 7 README.license + 2 HTML manuals + UBIQUITOUS_LANGUAGE.md All synced to v1.1.0

No version bump within the phase series — all changes ship under versionCode=110, versionName="1.1.0".


✨ What's New

Phase 58 — PGN Eval Annotation + P0 Concurrency Hardening

🆕 Every-5-moves PGN {} eval annotation

At moves 5, 10, 15, 20, …, the PGN {} comment now carries a human-readable fragment mirroring the in-app evaluation bar. The fragment is auto-localized via T() reading the global _lang variable, so it switches between Chinese and English based on the app's current language mode.

Format:

<White-perspective label> (<score>) D<depth> SD<seldepth> (<W%>W/<D%>D/<L%>L)

Examples:

Language Example
🇨🇳 Chinese 均势 (-0.10) D22 SD34 (1%W/96%D/3%L)
🇬🇧 English Equal (-0.10) D22 SD34 (1%W/96%D/3%L)
🇨🇳 Chinese (mate) 白方将杀 (#+3) D15 SD20 (100%W/0%D/0%L)
🇬🇧 English (mate) White mates (#+3) D15 SD20 (100%W/0%D/0%L)

Design choices:

  • White-perspective, not player-perspective — the PGN comment is unambiguous regardless of which side the human played. This matches standard PGN convention where [%eval] values are always from White's POV.
  • Missing components gracefully omitted — no depth → omit D## SD##; no WDL (all -1 or sum ≤ 0) → omit (%W/%D/%L).
  • Zero-sum WDL divide-by-zero guard — pathological wdl 0 0 0 engine output no longer produces NaN/Infinity.
  • Mate handling — uses #+N / #-N score + a localized "White mates" / "Black mates" label.
  • Position in {} comment — placed after the structured [%eval] tag (so [%xxx] tags remain first per PGN spec) and before free-text / resign / timeout annotations.

New API surface:

// pgn-standard.js
function formatEvalAnnotation(cached)  string
// cached: {eval, mate, depth, seldepth, wdlW, wdlD, wdlL}

// game-logic.js — new i18n keys (11)
pgn_white_winning, pgn_white_huge_adv, pgn_white_advantage,
pgn_white_slight_adv,, pgn_equal,
pgn_black_slight_adv, pgn_black_advantage, pgn_black_huge_adv,
pgn_black_winning, pgn_mate_white, pgn_mate_black

🔒 P0 Concurrency Fix 1 — stopLatch TOCTOU race

Symptom: In rare timing windows, the bestmove from a stopped ponder search could incorrectly arm the _discardingPonderBestmove flag, causing the next legitimate bestmove to be silently dropped — corrupting the engine state machine.

Root cause: The bestmove handler in readEngineOutput() read _stopLatch (a volatile field) without holding _stopLatchLock. This created a classic time-of-check-to-time-of-use race with stopAndWaitForBestmove's timeout path:

T1: bestmove handler reads _stopLatch = X (non-null)
T2: stopAndWaitForBestmove.await() times out
T2: stopAndWaitForBestmove sets _discardingPonderBestmove = true
T2: stopAndWaitForBestmove.finally{} clears _stopLatch = null
T1: bestmove handler calls X.countDown() and returns
    → discard flag is now stuck TRUE
    → NEXT legitimate bestmove is incorrectly discarded

Fix: The bestmove handler now atomically captures-and-clears _stopLatch under _stopLatchLock:

synchronized (_stopLatchLock) {
    stopLatch = _stopLatch;
    if (stopLatch != null) {
        _stopLatch = null;  // claim ownership
    }
}

The timeout path only arms the discard flag if it still owns the latch (_stopLatch == stopLatch under the lock). Exactly one consumer "owns" the latch — no race.

🔒 P0 Concurrency Fix 2 — Heartbeat deadlock

Symptom: shutdown() could block for the full 1-second join timeout when the heartbeat thread was mid-engineWriter.write(), leaving the heartbeat in a zombie state on some OEM ROMs.

Root cause: The heartbeat thread's engineWriter.write("quit\n") call (inside the zombie-detection branch) was synchronized on StockfishNative.this — the same monitor used by startHeartbeat() (which is synchronized). If shutdown() ran while the heartbeat held the this monitor inside the writer I/O call, shutdown's _heartbeatThread.join(1000) would wait for the heartbeat to release this — but the heartbeat was blocked on I/O.

Fix: Introduced a dedicated _writerLock (private final Object) for engineWriter access in the heartbeat path. _writerLock is decoupled from the this monitor, so shutdown's interrupt/join is not blocked by heartbeat's writer access.

// Before (deadlock risk):
synchronized (StockfishNative.this) {
    engineWriter.write("quit\n");
    engineWriter.flush();
}

// After (decoupled):
synchronized (_writerLock) {
    engineWriter.write("quit\n");
    engineWriter.flush();
}

cleanupEngineResources() and recoverEngine() use their own locks (_restartLock, _stopLatchLock) and do not hold _writerLock — no lock-ordering inversion.


Phase 57+ — Code-Review-Driven Preventive Hardening

Six preventive fixes surfaced by a comprehensive 12-skill parallel code review of the v1.1.0 source:

# File Fix
1 pgn-standard.js parseStandardPGN single-line PGN tag-stripping regex — /^\[[\s\S]*?\]/gm only matched the first tag when all tags + movetext were on ONE line (the ^ anchor with gm flags only matches the very start of the string). Replaced with format-strict, unanchored /\[[A-Za-z]\w*\s+"[^"]*"\]/g. Preventive — this parser is not on the main code path.
2 chess960.js isChess960CastlingLegal king-position lookup — was scanning the entire back rank (up to 8 board reads); now reads the cached s.wk/s.bk fields directly (maintained by syncHash() and cloneS()), with a defensive board-scan fallback.
3 ai-bridge.js WDL percentage display divide-by-zero guard — if(total > 0) check before dividing _sfWdlW/_sfWdlD/_sfWdlL by their sum.
4 StockfishNative.java postJsCallback activity-lifecycle guard — skips evaluateJavascript when host Activity isFinishing()/isDestroyed(). Prevents IllegalStateException crash on HyperOS 3 during engine-init retries after user exit.
5 EngineService.java Wake-lock bounded 30-minute timeout — wakeLock.acquire(30L * 60L * 1000L). Prevents indefinite CPU wake if OEM silently kills the service and onDestroy never runs.
6 res/README.license LIC-2 stale version reference — line 14 strings.xml description updated from v1.0.8 to v1.1.0.

Also added: UBIQUITOUS_LANGUAGE.md (pure-English, 80+ terms) at the project root — a domain-terminology glossary for developer/domain-expert conversations.


Phase 57 — Portrait Review Scroll + Visual-Annotation Cache

🐛 Portrait review move-list scroll positioning

Symptom: In portrait review mode, clicking a move in the move list scrolled to the wrong position (the active move was nowhere near centered, often clamped to the bottom).

Root cause: The Phase 56 fix (which replaced scrollIntoView with manual scrollTop computation) used _rAct.offsetTop to compute the active move's position. However, offsetTop returns the distance from the element's outer border to the top of its offsetParent's inner border — and .rmv-block's offsetParent is .review-overlay (position:fixed), not _rList (.review-moves has no position set).

  • Landscape: the error was small (~24px header).
  • Portrait: .review-moves is stacked below .review-left (the board column, 256–320px tall), so offsetTop included the board's full height — _target was way too large, clamped to scrollHeight - clientHeight (scrolled to bottom).

Fix: Replaced offsetTop with getBoundingClientRect()-based calculation relative to _rList:

_actTop = (_actRect.top - _listRect.top) + _rList.scrollTop;
_target = _actTop + (_actH/2) - (_listH/2);

Works identically for standard chess and Chess960 (pure DOM/layout fix, no game-logic changes).

🐛 Visual-annotation cache residue at review entry

Symptom: Stale [%csl]/[%cal] annotations from a previous game occasionally rendered on the initial-position board when entering review.

Root causes:

  1. _computeInitialPositionAnnotations read gameState (the live mid-game state) instead of reviewStates[0].state (the actual initial position shown at step 0).
  2. The '_initial' cache key was never cleared by _invalidateCachesForUndoneMoves (which only deletes numeric keys). It was cleared by _resetGameState (new game / import / setup-complete / FEN import), but if the user re-entered review without one of those entry points, the stale cache persisted.

Fix:

  • _computeInitialPositionAnnotations now reads reviewStates[0].state with a fallback chain: reviewStates[0].state → reviewBaseState → gameState (defensive).
  • enterReview() explicitly deletes the '_initial' key from _visualAnnotationsCache at entry, forcing fresh computation each review session. Numeric keys (0..N-1) are deliberately preserved.

Phase 56 — Landscape Review Scroll + PGN Timeout + Timing Sync

🐛 Landscape review nav-button scroll-to-top

Symptom: In landscape review mode, clicking the navigation buttons (◀ ▶ ⏮ ⏭) caused the entire page to scroll back to the top.

Root cause: scrollIntoView({block:'center'}) scrolls all scrollable ancestors. In the nested .review-body > .review-moves layout, this dragged the outer container back to the top.

Fix: Replaced scrollIntoView with manual scrollTop computation on the inner .review-moves container only — preserves outer .review-body scroll position.

✨ PGN timeout annotations

Timeout games now emit proper PGN termination metadata, parallel to the existing resign logic:

[Termination "Time forfeit"]

1. e4 e5 2. Nf3 Nc6 ... {Black wins by timeout} 0-1

🐛 First-move timing synchronization

_turnStartTime and gameClocks are now reset in _resetGameUIState() (called by all game-start entry points). Previously _turnStartTime was only set once at module load — the first move's [%emt]/{Xs} annotation could be wildly inaccurate if the user spent time configuring the new game.

🐛 UCI command ordering refinement

setGameDifficulty()'s setoption UCI_LimitStrength/UCI_Elo/Skill Level commands are now sent before position fen in both engineGoTimed and engineGoInternal. Ensures the engine applies the difficulty settings to the search from the first move.


Phase 55 — Chess960 Castling Rook-Loss Fix

🐛 Critical: Rook disappears after O-O/O-O-O in certain Chess960 starting positions

Symptom: In Chess960 starting positions where the participating rook's source square IS the king's castling destination (e.g. King on d1, queenside rook on c1: O-O-O puts the King on c1, which is the rook's source), the rook would silently disappear from the statistics page board after castling.

Root cause: The _destEmpty check incorrectly rejected the Chess960 case where the king's destination square holds a same-color rook participating in the castling.

Fix: Replaced _destEmpty with _destValid — allows the destination square to be empty OR hold a same-color rook participating in the castling. Applied to both stats.html executeMove/buildSAN and game-logic.js _castleSide.

// Before:
const _destEmpty = !_destPiece;
if (_destEmpty && (!_is960 || _cr[color + 'Kingside'])) return 'kingside';

// After:
const _destValid = !_destPiece ||
    (_destPiece.type === 'rook' && _destPiece.color === mv.piece.color);
if (_destValid && (!_is960 || _cr[color + 'Kingside'])) return 'kingside';

Phase 54 — Custom Slider + 7 Code-Audit Fixes

✨ Pixel-perfect review progress bar / eval chart alignment

Replaced the native <input type="range"> with a custom slider (div elements for track/fill/thumb). The custom slider shares the same CSS container as the eval chart, and the thumb center is positioned via calc(ratio * 100%) — achieving true pixel-level alignment between the progress bar and the chart's first/last data points.

⚠️ Do NOT regress to native <input type="range"> — WebKit's slider position is unreliable across Android WebView versions.

Other Phase 54 fixes

  • Move-list scroll-into-view — only scroll when the active move is not visible, using block:'nearest'.
  • executeMove async callback — wrapped in try-catch to prevent async exceptions from corrupting the render state.
  • ChessAudioEngine partial-init resetsetEnabled/setVolume now null-guard against partial initialization.
  • Engine heartbeat all-callbacks fix — heartbeat timestamp now updated in all engine callbacks (onEngineProgress, onBestMove, onHintMove, onPonderProgress), not just onEngineEval.
  • MultiPV secondary-variation divergence — distinguish alternative vs. continuation variations correctly.
  • PGN cascade-skip threshold — raised from 5 to max(15, mainTokens.length * 0.1) to avoid skipping short legitimate games.
  • Render retry-loop guard_animRetryCount capped at 10 to prevent infinite retry loops if setTimeout never fires (WebView suspended).

Phase 53 — Visual Annotation Overhaul + Version Bump

Version bumped to versionCode=110, versionName="1.1.0".

✨ Green arrow redefined: "escape path" → "check-response path"

The green arrow visual annotation now includes two categories:

  1. King escape moves — all legal king moves that escape check (correctly handling pins and discovered-check escapes). If the king has no legal escape, no green arrow is drawn from the king's starting square (fixes a v1.0.9 bug).
  2. Legal captures of the checking piece — all legal moves by the defending side that capture the checker (including legal en passant).

Chess960 compatibility: isChess960CastlingLegal already checks "king not in check", so legalMoves never generates castling moves while in check — green arrows never incorrectly include castling.

✨ Red check arrow uses actual checker position

The red check arrow now points from the actual checking piece's position (correctly handling discovered checks and double checks), not from an inferred "last move origin".

Other Phase 53 changes

  • Stats visual-annotation cutoff — the stats page's visual-annotation counts now respect the selected-move cutoff (was showing counts for the entire game regardless of selection).
  • King-control-arrow legality filter — king-move arrows in the control heatmap / "square control info" panel / visual annotations are now filtered for legality (no arrows into pinned squares, no arrows that would leave the king in check).
  • Nav-button center-align — review navigation button text is now center-aligned.
  • King-position staleness fix_resetGameUIState() now clears all stale UI state, including cached king positions from a previous game.
  • FEN-import state pollution fix — importing a FEN no longer leaves residual state from the previous game.
  • exitSetup state pollution fix — exiting setup mode no longer carries setup-only state into the game.
  • Portrait/landscape review layout unification — both orientations now use the .review-top (board + moves) + .review-bottom (full-width controls) structure.

📦 Download

Artifact Size Description
Regalia-v1.1.0-release.apk ~77 MB Signed release APK (v1+v2+v3 signing schemes)
Regalia-v1.1.0-manual-zh.html ~818 KB Chinese user manual (self-contained)
Regalia-v1.1.0-manual-en.html ~924 KB English user manual (self-contained)
UBIQUITOUS_LANGUAGE.md ~32 KB Pure-English domain terminology glossary (80+ terms)

APK signing

✅ Verified using v1 scheme (JAR signing):           true
✅ Verified using v2 scheme (APK Signature Scheme v2): true
✅ Verified using v3 scheme (APK Signature Scheme v3): true

Compatible with Xiaomi HyperOS 3 (Android 15) and all Android 5.0+ devices.

Engine binary (NOT in the source tarball)

The Stockfish 18 arm64-v8a-dotprod binary is excluded from the source tarball to keep it small and avoid redistributing the 114 MB engine binary. Download separately:

curl -sL -o sf.tar \
  https://github.com/official-stockfish/Stockfish/releases/download/sf_18/stockfish-android-armv8-dotprod.tar
tar -xf sf.tar
mkdir -p src/main/jniLibs/arm64-v8a
cp stockfish/stockfish-android-armv8-dotprod src/main/jniLibs/arm64-v8a/libstockfish.so
chmod +x src/main/jniLibs/arm64-v8a/libstockfish.so

See BUILDING.md for full build instructions.


🔧 Build & Install

Requirements

  • JDK 21 (Temurin 21.0.5+11 recommended; JRE-only is insufficient — javac required)
  • Android SDK: API 35, Build-Tools 34.0.0, NDK 27.2.12479018, CMake 3.22.1
  • Gradle 8.11.1 (wrapper included)
  • Python 3 (for build-chess.py — merges the 8 JS modules into chess.html)

Quick start

# 1. Place the engine binary (see above)
# 2. Configure local.properties
echo "sdk.dir=/path/to/android-sdk" > local.properties
# 3. Place a keystore at ../debug.keystore (storepass=android, alias=debug)
#    or update signingConfigs.release in build.gradle
# 4. Build chess.html (merges 8 JS modules)
python3 build-chess.py
# 5. Build the release APK
./gradlew assembleRelease
# 6. Verify signatures
/path/to/android-sdk/build-tools/34.0.0/apksigner verify --verbose \
  /tmp/regalia_build/Regalia/outputs/apk/release/Regalia-release.apk

The signed APK will be at /tmp/regalia_build/Regalia/outputs/apk/release/Regalia-release.apk (per buildDir in build.gradle).

Build troubleshooting

Symptom Fix
CMake/ninja "manifest still dirty after 100 tries" find . -name "*.txt" -o -name "*.cpp" -o -name "*.cmake" | xargs touch (normalize future timestamps from unzip/tar)
./gradlew: Permission denied chmod +x gradlew
Aliyun Maven mirror 502 build.gradle/settings.gradle already place google()/mavenCentral() before the Aliyun mirror; if still 502, temporarily comment out the Aliyun blocks

📁 Project Structure

Regalia/
├── src/main/
│   ├── assets/
│   │   ├── chess.src/              # 8 JS modules (merged by build-chess.py → chess.html)
│   │   │   ├── game-logic.js       # Chess rules, move generation, i18n, castling detection
│   │   │   ├── chess960.js         # Chess960 SP-ID, Shredder-FEN, 960 castling rules
│   │   │   ├── pgn-standard.js     # PGN encoder/decoder, NAG, [%csl]/[%cal], formatEvalAnnotation
│   │   │   ├── worker-pool.js      # Web Worker pool (Blob-URL, CSP-safe)
│   │   │   ├── ai-bridge.js        # Engine communication, eval display, PGN export
│   │   │   ├── tablebase.js        # Lichess Syzygy tablebase + PGN import
│   │   │   ├── eco-data.js         # ECO opening classification data
│   │   │   ├── ui.js               # Rendering, dialogs, review mode, ChessAudioEngine
│   │   │   └── index.html.tpl      # CSS template (theme variables, responsive layout)
│   │   ├── chess.html              # Built output (combined JS+CSS+HTML)
│   │   └── stats.html              # Statistics page (fullscreen WebView)
│   ├── java/com/Regalia/
│   │   ├── MainActivity.java       # WebView host, immersive mode, lifecycle
│   │   ├── StockfishNative.java    # Engine process management, UCI protocol, 60+ JS interfaces
│   │   ├── StatsActivity.java      # Fullscreen WebView for statistics
│   │   ├── ChessWebViewClient.java # Page load handler, render-process crash recovery
│   │   ├── EngineService.java      # Foreground service for engine stability
│   │   ├── ChessApp.java           # Application class, crash protection
│   │   ├── StabilizationHelper.java # Sensor-fusion board anti-shake (OIS principle)
│   │   ├── TlsSecurityHelper.java  # TLS 1.2+ enforcement for tablebase API
│   │   └── RootDetector.java       # Informational root detection (About dialog)
│   ├── cpp/
│   │   ├── engine_jni.cpp          # JNI native chmod/renice (from DroidFish)
│   │   └── CMakeLists.txt
│   └── res/                        # Android resources (strings, XML configs, launcher icons)
├── Manual/                         # HTML user manuals (Chinese + English)
├── UBIQUITOUS_LANGUAGE.md          # Domain terminology glossary (80+ terms, English)
├── NOTICE                          # Third-party component notices + version history
├── LICENSE-AGPL v3 / LICENSE-GPL v3 / LICENSE-Apache v2.0
├── PRIVACY.md / BUILDING.md / README.md
├── build.gradle / settings.gradle / gradle.properties
└── build-chess.py                  # Python build script (merges JS modules → chess.html)

📜 Licensing

Regalia is a combined work under dual licensing:

Component License Files
Application (original) AGPL v3 MainActivity.java, ChessWebViewClient.java, EngineService.java, ChessApp.java, StabilizationHelper.java, RootDetector.java, TlsSecurityHelper.java, chess960.js, eco-data.js, build-chess.py, AndroidManifest.xml, strings.xml, build configs
Engine + DroidFish-derived GPL v3 StockfishNative.java, engine_jni.cpp, game-logic.js, ai-bridge.js, ui.js, tablebase.js, stats.html, index.html.tpl, pgn-standard.js, worker-pool.js, libstockfish.so
Gradle Apache v2.0 gradle/wrapper/*

Per GPL v3 Section 13, these licenses are compatible for combination. Each component retains its original license. Since AGPL v3 imposes stricter requirements (including network interaction provisions under Section 13), its obligations effectively extend to the entire combined work.

7 README.license files provide per-directory license classification. See NOTICE for the full version history and third-party component attribution.


⚖️ Privacy

Regalia is a fully offline chess application. No personal data is collected, transmitted, or stored.

  • No analytics, advertising SDKs, or tracking
  • No user registration or accounts
  • Only network-dependent feature: Syzygy endgame tablebase queries to the public Lichess API (tablebase.lichess.ovh) — sends only FEN strings, no personal data. Forced over TLS 1.2+ via TlsSecurityHelper.java.
  • All game data stored locally in app-private storage (/data/data/com.Regalia/)
  • Engine binary integrity: ELF magic check + SHA-256 hash verification on first launch

See PRIVACY.md for the full policy.


🧪 Verification

Automated tests

Test suite Tests Status
test-phase57-plus.js 18 ✅ All pass
test-phase58-pgn-annotation.js 22 ✅ All pass

Test coverage includes:

  • PGN tag-stripping regex (single-line + multi-line)
  • isChess960CastlingLegal king-position lookup (cached / no-cache / stale-cache / blocked-path / no-rights)
  • formatEvalAnnotation (centipawn zh+en, positive/negative eval, mate White+Black, missing WDL, zero-sum WDL, missing depth, falsy cached, language toggle)
  • Built chess.html presence of all fixes

APK verification

✅ apksigner verify: v1 + v2 + v3 schemes all true
✅ aapt2 dump badging: versionCode=110, versionName=1.1.0
✅ AndroidManifest: no android:debuggable="true" (release build)
✅ chess.html: formatEvalAnnotation present, _moveNum%5===0 hook present, pgn_white_advantage i18n key present
✅ classes.dex: "race resolved" string present (stopLatch fix), "_writerLock" present (heartbeat fix)

🚀 What's Next

Items intentionally postponed to future versions (with rationale documented in NOTICE):

Item Reason
ui.js split into 6 submodules (7,497 lines) Requires 6+ weeks of architectural work + dedicated test coverage. Not appropriate for a same-version hardening phase.
StockfishNative.java split into focused helper classes (5,212 lines) Same as above — 60+ @JavascriptInterface methods must remain on the main class.
PGN parser unification (4 duplicate parsers) stats.html is standalone (can't import from chess.src/); worker-pool.js runs in a Web Worker (separate scope). Unification requires careful extraction of a shared tokenizer module.
Additional P0 concurrency hardening (_pendingBestMoveInfo 2s timer race, cleanupEngineResources sync granularity) Theoretical races that have not manifested in production across v1.0.8–v1.1.0. Require dedicated stress-testing.

💬 Feedback & Contributions

  • Bug reports & feature requests: Open an issue on GitHub
  • Pull requests welcome — see README.md for the contribution guide
  • Domain terminology questions: Refer to UBIQUITOUS_LANGUAGE.md (80+ terms with definitions, ambiguity notes, and example conversations)

🙏 Acknowledgments

  • Stockfish — the world-class open-source chess engine (GPL v3)
  • DroidFish — the Android chess app from which Regalia derives its engine management, PGN handling, and core chess logic (GPL v3)
  • Lichess — for the public Syzygy tablebase API
  • The chess community for decades of specification work (PGN, FEN, SAN, UCI, Syzygy)

📖 Full changelog (Phase 53 → 58)

Phase 58 (2026.7.5) — Feature + P0 concurrency hardening

  • ✨ Every-5-moves PGN {} eval annotation (bilingual, White-perspective)
  • 🔒 stopLatch TOCTOU race fix (StockfishNative.java)
  • 🔒 Heartbeat deadlock fix — dedicated _writerLock (StockfishNative.java)

Phase 57+ (2026.7.5) — Code-review-driven preventive hardening

  • 🐛 pgn-standard.js parseStandardPGN single-line PGN tag-stripping regex
  • chess960.js isChess960CastlingLegal king-position lookup optimization
  • 🐛 ai-bridge.js WDL percentage display divide-by-zero guard
  • 🐛 StockfishNative.java postJsCallback activity-lifecycle guard
  • 🐛 EngineService.java wake-lock bounded 30-minute timeout
  • 📝 res/README.license LIC-2 stale version reference
  • 📝 Added UBIQUITOUS_LANGUAGE.md (pure-English, 80+ terms)

Phase 57 (2026.7.4) — Portrait review scroll + visual-annotation cache

  • 🐛 Portrait review move-list scroll positioning (offsetTopgetBoundingClientRect)
  • 🐛 Visual-annotation cache residue at review entry ('_initial' key cleared in enterReview)

Phase 56 (2026.7.4) — Landscape review scroll + PGN timeout + timing sync

  • 🐛 Landscape review nav-button scroll-to-top (scrollIntoView → manual scrollTop)
  • ✨ PGN timeout annotations ([Termination "Time forfeit"] + {<color> wins by timeout})
  • 🐛 First-move timing sync (_turnStartTime + gameClocks reset in _resetGameUIState)
  • 🐛 UCI command ordering refinement (setGameDifficulty before position fen)

Phase 55 (2026.7.4) — Chess960 castling rook-loss fix

  • 🐛 _destEmpty_destValid (allows king destination to hold participating rook)

Phase 54 (2026.7.4) — Custom slider + 7 code-audit fixes

  • ✨ Custom slider for pixel-perfect review progress bar / eval chart alignment
  • 🐛 Move-list scroll-into-view (only when not visible, block:'nearest')
  • 🐛 executeMove async callback try-catch
  • 🐛 ChessAudioEngine partial-init reset
  • 🐛 Engine heartbeat all-callbacks fix
  • 🐛 MultiPV secondary-variation divergence fix
  • 🐛 PGN cascade-skip threshold (5 → max(15, len*0.1))
  • 🐛 Render retry-loop guard (_animRetryCount max 10)

Phase 53 (2026.7.3) — Visual annotation overhaul + version bump

  • 🏷️ Version bumped to versionCode=110, versionName="1.1.0"
  • ✨ Green arrow redefined: "escape path" → "check-response path" (king escape + capture checker)
  • ✨ Red check arrow uses actual checker position (discovered check support)
  • 🐛 Stats visual-annotation cutoff fix
  • ✨ King-control-arrow legality filter
  • ✨ Nav-button center-align
  • 🐛 King-position staleness fix
  • 🐛 FEN-import state pollution fix
  • 🐛 exitSetup state pollution fix
  • ✨ Portrait/landscape review layout unification

AI-GEN: AI assisted — this release note was AI-assisted and has been reviewed for AGPL v3 compliance.

Copyright (C) 2026 Regalia. Licensed under GNU Affero General Public License v3 (AGPL v3).