Releases: YDW99/Regalia
Release list
Regalia v1.2.2
What's New in v1.2.2
Full Changelog: https://github.com/YDW99/Regalia/releases/tag/v1.2.2/compare/v1.2.1...v1.2.2
Contributors
Bug Fixes
Installation
Download the APK from the assets below and install it on your Android device.
Note: This app requires Stockfish chess engine binaries (libstockfish.so) to be built separately via NDK.
⚠️ Note: If you encounter any issues, please report them here.
Regalia v1.2.2 — Hardened by an 8-Skill Audit
A precision patch release: one real defect fixed, seven false positives cleanly dismissed, and the version bumped from
1.2.1→1.2.2. No new features, no new permissions, no new network access — just a sharper, better-defended build of the same chess experience you already trust.
📦 Download
| Asset | Size | Purpose |
|---|---|---|
Regalia-v1.2.2-release.apk |
~75 MB | Standalone installer for arm64-v8a devices |
Regalia-v1.2.2-manual-en.html |
~1.1 MB | Self-contained English user manual |
Regalia-v1.2.2-manual-zh.html |
~956 KB | Self-contained Chinese user manual |
💡 Bilingual parity — every user-facing string, every changelog entry, and every manual page is synchronized across Chinese (
zh) and English (en). The app auto-detects system language; switch manually anytime via the↔️中 / ↔️ENbutton in the header.
🔐 Integrity Checksums
APK file: Regalia-v1.2.2-release.apk
APK versionCode: 122
APK versionName: 1.2.2
APK signature: v1 ✅ v2 ✅ v3 ✅ (Xiaomi HyperOS 3 compatible)
Stockfish engine SHA-256 (must match all three locations):
Source binary: 8f7116d3f1a7004a6581d4fb0c1ff891ce095bab6d45e52f1578897cf23b61b5
jniLibs copy: 8f7116d3f1a7004a6581d4fb0c1ff891ce095bab6d45e52f1578897cf23b61b5
APK-embedded: 8f7116d3f1a7004a6581d4fb0c1ff891ce095bab6d45e52f1578897cf23b61b5
Verify on your own machine:
# APK signature (v1 + v2 + v3 must all be true)
apksigner verify --verbose Regalia-v1.2.2-release.apk
# Version info
aapt dump badging Regalia-v1.2.2-release.apk | head -2
# Engine SHA-256 (must equal the value above)
unzip -p Regalia-v1.2.2-release.apk lib/arm64-v8a/libstockfish.so | sha256sum🩺 What Happened in This Release
v1.2.2 is the direct result of subjecting v1.2.1 to an 8-skill comprehensive audit. Five sub-reports — totaling 4,199 lines of analysis — were produced using the following skill stack:
| # | Skill | What it does |
|---|---|---|
| 1 | secure-code-review |
OWASP Top 10 (A01–A10) full-spectrum review |
| 2 | code-vuln-audit |
Secret-leak scan + OWASP pattern matching |
| 3 | deep-module-refactor |
God Module friction-point analysis |
| 4 | code-arch-optimizer |
Coupling matrix + refactoring roadmap |
| 5 | git-repo-audit |
Hot-file analysis + contributor metrics |
| 6 | code-to-chart |
Module dependency graph generation |
| 7 | code-safety-audit |
OWASP pattern sweep (45 sites) |
| 8 | web-security-audit |
WebView / XSS / network review |
Every single finding from those reports was then verified line-by-line against the actual source tree — not against the stale snapshot the auditors saw. The result:
7 findings were false positives. 1 finding was real and is now fixed. The architecture recommendations are real but deferred.
This is what a disciplined patch release looks like.
✅ The One Real Fix
YELLOW-1 — FEN Parsing Lacks Length Limit
| Attribute | Value |
|---|---|
| OWASP category | A03 — Injection |
| Severity | 🟡 YELLOW (medium) |
| File | src/main/assets/chess.src/tablebase.js |
| Function | fenToState(fen) |
| Risk if unfixed | Pathologically long FEN strings could cause unnecessary string processing before validation rejected them — a low-impact DoS vector |
| Fix | Added a 200-character length limit at function entry |
Why 200 characters?
A standard FEN is at most ~87 characters. The generous 200 ceiling comfortably accommodates:
- ✅ Chess960 Shredder castling notation (e.g.
HAah) - ✅ En passant target square (e.g.
e3) - ✅ Extended fields (move counters, half-move clock)
- ✅ Reasonable user input noise (trailing whitespace, etc.)
// tablebase.js — fenToState()
function fenToState(fen){
if(!fen || typeof fen !== 'string') return null;
if(fen.length > 200) return null; // ← NEW: DoS guard
const parts = fen.trim().split(/\s+/);
// ...existing validation continues from here
}Why the audit's regex was rejected
The audit suggested a strict character whitelist:
// ❌ Audit's suggestion — REJECTED
if (!/^[1-8PNBRQKpnbrqk\/\s-]+$/.test(fen)) return null;This regex would break legitimate chess functionality:
| Use case | Legal characters | Caught by regex? |
|---|---|---|
Chess960 castling rights (KQkq or AHah) |
A-H, a-h |
❌ Breaks |
En passant target square (e3, g6) |
a-h, 1-8 |
❌ Breaks |
Field separator ( ) |
whitespace | ✅ Passes |
The existing per-character validation already rejects invalid piece characters by returning null — that is the correct whitelist approach, applied at the right granularity. Adding a coarse regex at the top would have created false negatives for valid Chess960 positions.
🚫 The Seven False Positives — and Why
Each of the following audit findings was carefully verified against the current codebase and confirmed to be either (a) based on stale code from previously deleted files, or (b) describing a defense that already exists.
RED-1 — getStatsPayload() returns unescaped PGN data (XSS)
| Audit's claim | StatsActivity.getStatsPayload() returns raw PGN strings that could contain <script> tags, causing XSS in stats.html. |
| Reality | stats.html already escapes every PGN header value (including White / Black player names) via _escFEN() at the rendering layer — see line 1770. |
| Why the audit's fix was rejected | The suggested Java-layer JSON escaping (" → \") would corrupt JSON syntax — getStatsPayload() returns a JSON string, and double-escaping quotes would break JSON.parse() on the JS side. |
| Status | ✅ False positive — existing defense confirmed. |
RED-2 — shouldOverrideUrlLoading doesn't block javascript: protocol
| Audit's claim | ChessWebViewClient.shouldOverrideUrlLoading() doesn't explicitly check for javascript: URLs. |
| Reality | The current implementation uses a fall-through blocker: it allows only file:///android_asset/ and http(s)://, then return true (block) for everything else — including javascript:, data:, intent:, content:, and about:blank. |
| Why the audit's fix was rejected | The audit's suggested early if (url.startsWith("javascript:")) return true; is functionally redundant — the final return true already catches it. Adding it would be harmless but misleading (implying the block list is opt-in rather than default-deny). |
| Status | ✅ False positive — default-de... |
Regalia v1.2.1
What's New in v1.2.1
Full Changelog: https://github.com/YDW99/Regalia/releases/tag/v1.2.1/compare/v1.2.0...v1.2.1
Contributors
Bug Fixes
Build / CI
- Update BUILDING.md with v1.2.1 notes and remove AI-GEN (@YDW99)
81cd0b6
Installation
Download the APK from the assets below and install it on your Android device.
Note: This app requires Stockfish chess engine binaries (libstockfish.so) to be built separately via NDK.
⚠️ Note: If you encounter any issues, please report them here.
Regalia v1.2.1 — Release Notes
A standalone, open-source chess app for Android — play offline against Stockfish 18, analyze your games, and explore openings. No account, no network, no tracking.
📦 Download
| File | Size | Description |
|---|---|---|
Regalia-v1.2.1-release.apk |
~75 MB | Release APK — arm64-v8a only, Android 6.0+ |
Regalia-v1.2.1-manual-zh.html |
~972 KB | Chinese user manual (self-contained HTML) |
Regalia-v1.2.1-manual-en.html |
~1.1 MB | English user manual (self-contained HTML) |
⚠️ The engine binary is NOT included in the source tarball.
If you build from source, download the official Stockfish 18 arm64-v8a-dotprod binary separately. SeeBUILDING.mdfor instructions.
✨ What's New in v1.2.1
v1.2.1 is a same-version refinement release — no new features, no new permissions, no versionCode bump. It is the result of 15 rounds of iterative quality improvement on top of the v1.2.0 architecture refactor, driven by first-principles code audits, SonarCloud static analysis, and guidance from three reference PDFs (AI Code Generation Defect Prevention, Android WebView Development, SonarCloud Perfect Review).
🎯 Highlights
- 3 SonarCloud Bugs fixed (PR #43): 2 real S3923 "if/else identical" issues + 1 S2757 false-positive refactor
- 152 empty
catchblocks eliminated (S108): 146 in chess.src/*.js + 6 in stats.html — all now log module-taggedconsole.warn - God Function refactors (S3776):
renderInternal347→23 lines,_renderDialogs181→31 lines,_renderReviewMode612→561 lines - 53
typeofmodernizations (S2703):typeof x === 'undefined'→x === undefinedfor declared variables; true globals (crypto,AndroidBridge) correctly preserved - 2 user-reported bugs fixed: review eval chart not refreshing + stats page data completeness
- 25 style unifications (S3523/S1154):
parseFloat→Number.parseFloat,String.fromCharCode→String.fromCodePoint - 4 nested ternary refactors (S3358) + 2 duplicate CSS selector merges (S3646)
♟️ Features
Core Gameplay
| Feature | Description |
|---|---|
| Stockfish 18 engine | World-class chess engine, arm64-v8a-dotprod build (ARMv8.6-A DOTPROD instruction set for accelerated NNUE inference) |
| 8 difficulty levels | Level 1 (beginner) → Level 7 (Skill Level mode) → Level 8 (⚙️ custom engine config) |
| Chess960 (Fischer Random) | Full Chess960 support with SP-ID selector, back-rank preview, Shredder-FEN castling rights |
| Time controls | Untimed / Sudden Death / Fischer Increment / Bronstein Delay / US Delay |
| Ponder mode | Engine thinks on opponent's time for faster response |
| Undo/Redo | Full move history navigation with redo stack |
| Setup mode | Custom board positions with manual castle-rights (🔁) and en-passant (⚡) markers |
Analysis & Review
| Feature | Description |
|---|---|
| Review mode | Step through your game move-by-move with eval bar, trend chart, and move slider |
| Analyze All | Batch-evaluate every move in the background; long-press to prioritize a specific step |
| Evaluation trend chart | SVG line chart showing eval progression; auto-centers on the active move |
| Visual annotations | [%csl] square highlights (B/R/G/Y) + [%cal] arrows; supports multi-color per square |
| Control heatmap | 🌗/🌈 Board coloring showing piece control influence per square |
| Critical moves | Auto-detected moves where evaluation changed significantly |
| Statistics page | 📊 Full-game statistics: move quality distribution, eval trend, opening classification, PGN text |
PGN Support
| Feature | Description |
|---|---|
| PGN import/export | Standard PGN (1994 spec) with NAG, [%csl]/[%cal], [%eval], [%clk], [%emt] |
| PGN cache manager | 📚 Save/load games locally with tags; partial-eval coverage dialog |
| FEN import/export | Copy/paste FEN positions; setup-mode FEN with Shredder castling for Chess960 |
| ECO opening classifier | 500+ openings with search, family filter, and AI book moves |
| Lichess Syzygy tablebase | 7-piece endgame tablebase queries (WDL/DTZ) via TLS-pinned API |
User Experience
| Feature | Description |
|---|---|
| Bilingual UI | 🇨🇳 Chinese / 🇬🇧 English, toggle from header; persists across sessions |
| Dark/Light theme | Auto-follows system setting; optimized palettes for both modes |
| Sensor anti-shake | Board stabilization via sensor fusion (optical-image-stabilization principle) |
| Haptic feedback | Distinct vibration patterns for piece move, capture, check, game-over |
| Sound system | Per-piece-type sound effects with move/capture/check/game-over variations |
| Coordinate labels | a-h file labels + 1-8 rank labels + per-square coordinates on all boards |
| Responsive layout | Portrait + landscape; notch/cutout/R-corner safe-area insets |
| SAF file picker | Storage Access Framework for PGN export/import (no storage permission needed on API 29+) |
🔒 Privacy & Security
Regalia is designed to be private by design.
- 🚫 No account — no registration, no login, no user identification
- 🚫 No tracking — no analytics, no telemetry, no crash reporting to remote servers
- 🚫 No ads — no advertising SDKs, no ad network connections
- 🌐 Offline-first — all gameplay is local; only optional tablebase queries use network (TLS-pinned)
- 🔐 WebView hardened:
setAllowFileAccess(false)setAllowFileAccessFromFileURLs(false)setAllowUniversalAccessFromFileURLs(false)@JavascriptInterfaceannotation on all exposed methods- URL scheme whitelist + sandbox path validation (
JsBridgeGateway) - TLS 1.2+ enforcement + SPKI SHA-256 certificate pinning (
TlsSecurityHelper)
- 📁 Scoped storage — all file I/O via SAF content URIs (no direct file path access on API 29+)
Permissions
| Permission | Why |
|---|---|
INTERNET |
Optional Lichess Syzygy tablebase queries (7-piece endgame) |
ACCESS_NETWORK_STATE |
Check network availability before tablebase queries |
WAKE_LOCK |
Keep engine alive during long analysis (foreground service, 30-min timeout) |
VIBRATE |
Haptic feedback on moves/captures/checks |
| `FOREGROUN... |
Regalia v1.2.0
What's New in v1.2.0
Full Changelog: https://github.com/YDW99/Regalia/releases/tag/v1.2.0/compare/v1.1.2...v1.2.0
Contributors
Bug Fixes
Installation
Download the APK from the assets below and install it on your Android device.
Note: This app requires Stockfish chess engine binaries (libstockfish.so) to be built separately via NDK.
⚠️ Note: If you encounter any issues, please report them here.
Regalia v1.1.2
What's New in v1.1.2
Full Changelog: https://github.com/YDW99/Regalia/releases/tag/v1.1.2/compare/v1.1.1...v1.1.2
Contributors
Features
- Add files via upload (@YDW99)
6c9ebcb
Bug Fixes
Documentation
- docs(CONTRIBUTING):完善细节 (@YDW99)
03b16a8 - docs(CONTRIBUTING);security(#34);build(#33) | (#35) (@YDW99)
50e7b5c
Other
- Delete CONTRIBUTING-en.md (@YDW99)
f9bf776 - Delete CONTRIBUTING-zh.md (@YDW99)
6a2fe04 - Delete Manual/Regalia-v1.1.1-manual-zh.html (@YDW99)
cb9176f - Delete Manual/Regalia-v1.1.1-manual-en.html (@YDW99)
455ab18
Installation
Download the APK from the assets below and install it on your Android device.
Note: This app requires Stockfish chess engine binaries (libstockfish.so) to be built separately via NDK.
⚠️ Note: If you encounter any issues, please report them here.
♟️ Regalia v1.1.2 ♔
A professional-grade Android chess app built around Stockfish 18 — now more robust, more secure, and more thoroughly audited than ever.
| Version | 1.1.2 (versionCode = 112) |
| Release Date | 2026-07-12 |
| Min Android | 6.0 (API 23) |
| Target Android | 15 (API 35) |
| Engine | Stockfish 18 arm64-v8a-dotprod |
| Architecture | arm64-v8a only |
| HyperOS 3 | ✅ Compatible |
| Source Size | ~34,000 lines across 8 JS modules + 9 Java files + 1 JNI + 1 stats page |
📦 Download
| Asset | Description |
|---|---|
Regalia-v1.1.2-release.apk |
Signed release APK (v1 + v2 + v3 signature schemes). 75 MB. |
Regalia-v1.1.2-manual-zh.html |
Self-contained Chinese user manual. |
Regalia-v1.1.2-manual-en.html |
Self-contained English user manual. |
⚠️ Engine binary is NOT in the source tarball. The 114 MB Stockfish 18arm64-v8a-dotprodbinary is excluded to keep the tarball small and avoid redistributing the engine with the source. Build instructions are inBUILDING.md(inside the tarball) — you'll download the engine separately and place it atsrc/main/jniLibs/arm64-v8a/libstockfish.so.
🎯 What's New in v1.1.2
v1.1.2 is a maintenance release that keeps the version number (versionCode = 112) unchanged while delivering 6 same-version revision phases (Phase 67 → 72). Each phase was driven by user feedback, first-principles code review, or security hardening — no new features were added, but the app is now significantly more reliable.
Phase Overview
| Phase | Date | Theme | Key Deliverable |
|---|---|---|---|
| 67 | 2026-07-07 | Version bump + PGN cache [%eval] root-cause fix + code review report |
New _pgnCacheShowPartialEvalDialog (3 options) |
| 68 | 2026-07-08 | Analyze All optimization + long-press priority + UI polish | Long-press to prioritize a step during batch analysis |
| 69 | 2026-07-09 | 4 bug fixes + Web Worker robustness + UCI optimization | Phase 69 UCI caps (MultiPV 8, Overhead 1000ms, Hash 50%, Threads 2×) |
| 70 | 2026-07-10 | First-principles code review cleanup | Edge-case [%eval] loss fix + console.log cleanup |
| 71 | 2026-07-11 | Stats-page move-selection bug fix + full code review | CSP 'unsafe-inline' + Chess960 0-distance castling + concurrency fixes |
| 72 | 2026-07-12 | Review analyze-all "false completion" bug fix + manual accuracy | Full-range completion scan + manual sync |
🐛 Bug Fixes (User-Reported)
1. Stats Page Move Selection Was Broken (Phase 71)
Symptom: Clicking a PGN move (e.g. 1. e4) in the 📊 Statistics page did nothing — no highlight, no board switch.
Root cause: The stats.html Content-Security-Policy used a SHA-256-hash-based script-src policy. CSP Level 2+ silently blocked all 23 inline onclick event handlers because the policy didn't include 'unsafe-inline' or 'unsafe-hashes'. The <script> block ran fine (hash matched), but every onclick="selectMove(0)" was silently dropped.
Fix: Switched script-src from 'sha256-<hash>' blob: to 'unsafe-inline' blob:. This is safe because stats.html is a local asset (file:///android_asset/) with no externally injected content and all JavaScript is inlined. Bonus: removing the hash eliminates the recurring "CSP hash mismatch" bug class (Phase 69 Bug 4 was the same root cause).
2. Review "Analyze All" Falsely Reported Completion After Long-Press Priority (Phase 72)
Symptom: During an "Analyze All" batch, long-pressing an uncached move to prioritize it would sometimes cause the batch to report "all analysis complete" — even though earlier steps remained un-analyzed.
Root cause: The _reviewAnalyzeAdvance() completion check only walked forward from _reviewAnalyzeStep+1. When a priority eval jumped ahead (e.g., user long-pressed step 50 during a batch at step 5) and subsequent steps were cached, the forward walk reached _lastStep and the completion branch fired — even though steps 5–49 were still uncached.
Fix: When the forward walk finds nothing, scan the entire range [0.._lastStep] for the lowest uncached step and resume the batch from there. The forward-only fast-path is preserved for the common (no-priority) case to avoid an O(n) full scan on every step.
// Phase 72 fix — full-range completion scan
if (nextStep > _lastStep) {
let _lowestUncached = -1;
for (let i = 0; i <= _lastStep; i++) {
if (!_reviewEvalCache.has(i)) { _lowestUncached = i; break; }
}
if (_lowestUncached >= 0) nextStep = _lowestUncached;
// else: truly all cached → completion branch
}3. PGN Cache Save Lost [%eval] Annotations (Phase 67 + 69 + 70)
Symptom: After running "Analyze All" in review mode, saving the game to the PGN cache would sometimes produce a PGN missing [%eval] annotations.
Root cause (three layers, fixed across three phases):
- Phase 67: The coverage check was gated on
!_useOriginal, but_useOriginalis almost alwaystruefor imported PGNs (becauseimportPGNsetstime:null). The check never ran, so the partial-eval dialog never appeared. - Phase 69: Race conditions in the PGN cache manager (save/import/delete/rename/tags) could corrupt state. Added
_pgnCacheOpInProgressguard. - Phase 70: Even after the above, exiting review mode before saving would still lose
[%eval]because the force-rebuild was gated on_inReview(nowfalse). Fix: check_reviewEvalCache.size > 0directly.
Fix: A new 3-option dialog (💾 Some steps not yet analyzed) appears when coverage is incomplete:
- 🔬 Analyze All first (recommended) — runs the batch, auto-saves on completion
- 💾 Save anyway — legacy behavior (evals will be missing)
- ✖️ Cancel — returns to cache manager
4. Chess960 0-Distance Castling Nulled the King (Phase 71)
Symptom: For Chess960 SP-IDs where the king starts on its castling target square (e.g. king on g1, kingside rook on h1 → UCI g1h1), castling would silently remove the king from the board.
Root cause: uciToCoords rewrote the destination to col 6, producing a 0-distance "move" g1g1. The _castleSide distance heuristic rejected it (0 < minDist), castling wasn't detected, and makeMv ran board[to] = board[from]; board[from] = null — self-copy then clear = king nulled.
Fix (defense-in-depth, 4 files):
ai-bridge.js uciToCoords— attachcastle='kingside'/'queenside'toresult.toui.js executeMove— checkto.castleas primary source (covers AI moves wherelegalMvsis empty)game-logic.js _castleSide— explicit 0-distance branchstats.html— mirror all three fixes in its independent code
🔒 Security & Robustness Hardening
XSS Hardening on Stats Page (Phase 71)
The CSP relaxation to 'unsafe-inline' (required for the onclick fix above) introduced a theoretical XSS vector: the renderPGNText movetext-walk fallback appended unrecognized characters raw to the HTML string. A malicious PGN like 1. e4 <img src=x onerror="..."> e5 * could execute.
Fix: All unrecognized movetext/variation/notation characters now route through _escFEN() (escapes `& < > " ' ``) before HTML insertion. Applied to:
renderPGNTextmovetext-walk fallback- Variation-text walk
firstMovesopening-plies list (defense-in-depth — notation is engine-generated)
Concurrency Fixes in StockfishNative.java (Phase 71)
| Race | Root Cause | Fix |
|---|---|---|
readyOkLatchHolder |
JS binder thread + executor thread both wrote the si... |
Regalia v1.1.1
What's Changed
Full Changelog: v1.1.0...v1.1.1
🚀 Regalia v1.1.1 — Release Notes
Version:
1.1.1· versionCode:111· Date: 2026-07-05
Engine: Stockfish 18 (arm64-v8a-dotprod, native build)
Min Android: 5.0 (API 21) · Target: Android 15 (API 35)
Overview
Regalia is a standalone, open-source chess app for Android — play offline against Stockfish 18, analyze your games, and explore openings. No account, no network, no tracking. Features full Chess960 (Fischer Random Chess) support, bilingual Chinese/English UI, personified move animations, and a comprehensive review/statistics system.
v1.1.1 is a quality-hardening release built on top of v1.1.0 (versionCode 110). While the version number increments, the focus is entirely on bug fixes, robustness, and architectural cleanup — no new user-facing features were added. Instead, 8 revision phases (Phase 59–66) were executed, each driven by user feedback and first-principles code audits.
What's New in v1.1.1
🔧 Bug Fixes
| # | Fix | Impact |
|---|---|---|
| 1 | Duplicate every-5-moves PGN annotation — re-exporting an imported PGN no longer duplicates [%eval] / eval-description annotations. Whitespace-tolerant regex dedup. |
PGN round-trip fidelity |
| 2 | Step-0 eval not cached — onEngineEval now caches stale callbacks for the original step even when the user navigates before the callback arrives. The eval trend chart's data point at step 0 is no longer missing. |
Review mode correctness |
| 3 | Resignation/timeout comments hardcoded English — PGN comments now follow the app's global language (T() with new i18n keys). |
Bilingual PGN export |
| 4 | "Analyze All" sometimes incomplete — batch session decoupled from reviewStep via _reviewAnalyzeStep / _reviewAnalyzeGen / _evalRequestBatchGen. User navigation during batch no longer invalidates in-flight callbacks. |
Review analysis reliability |
| 5 | Visual annotation [%csl]/[%cal] PGN pollution — TRUE root cause found and fixed: reviewStates from a previous game's review session was not cleared in _resetGameUIState(), causing _computeAndCacheVisualAnnotations to use the old game's board state at the same move index. Fix: clear reviewStates on every new-game entry + gate the shortcut on reviewMode === true. |
PGN export correctness |
| 6 | Imported PGN not saving to PGN cache — pure-import games (no live moves) now save the original PGN text, preserving all comments/tags/NAGs. | PGN cache manager |
| 7 | _savePGNYes timing bug — async export dialog could be open when the new game started. Fix: poll _pgnExportDialogActive before executing the pending action. |
Export flow reliability |
| 8 | Writer lock inconsistency — cleanupEngineResources() / shutdown() / cleanupFailedEngine() now use _writerLock (consistent with Phase 58 heartbeat path). |
Engine concurrency safety |
| 9 | onRenderProcessGone infinite loop — crash-count backoff (max 3 recreates per 60s window). |
OEM ROM resilience |
| 10 | Integer_bitcount negative-input infinite loop — n >>> 0 unsigned coercion. |
Defensive guard |
🆕 New Features (minor)
- Export annotation dialog — when exporting PGN (copy / file / cache save), a dialog asks whether to include special annotations (
[%csl]/[%cal]/[%eval]/eval descriptions). Three options: "Yes, include special annotations" / "No, exclude special annotations" / Cancel. Supports Android back-button and haptic feedback. - Statistics board navigation buttons —
⏮ ◀ ▶ ⏭below the stats board, context-aware (mainline vs variation). - Initial-position eval annotation — now a separate
{}comment before the first move (not attached to white's first move). - Step-0 eval annotation —
[Initial position]prefix, deduped on re-export.
🏗️ Architectural Improvements
- Centralized cache clearing —
_resetGameUIState()now clears 25+ state variables including_reviewEvalCache,_ecoRecCache,_pvCache,_visualAnnotationsCache,reviewStates,_preReviewSnapshot,setupHistory, and 7 dialog visibility flags. All 5 new-game entry points call it uniformly. importedflag on visual annotations — distinguishes human-authored PGN annotations (imported=true) from auto-generated UI display aids (imported=false). Onlyimported=trueentries are exported to PGN.includeAnnotationsparameter on_buildPGNString()— user-controlled annotation inclusion._requestBatchEval()— decoupled batch eval requests with generation counter, surviving user navigation and engine restarts._writerLockunification — allengineWriteraccess now goes through_writerLock, eliminating lock-inconsistency races.- Stale-state audit — following the
reviewStatesbug, audited all global state for similar patterns. Found and fixed_preReviewSnapshot,setupHistory/setupRedoStack, and 7 dialog flags not being cleared.
Phase-by-Phase Changelog
Phase 59 — Version bump + 4 bug fixes + analyze-all rewrite
- Version bumped to
versionCode=111,versionName="1.1.1" - Fix duplicate every-5-moves PGN
[%eval]annotation (dedup via whitespace-tolerant regex) - Fix step-0 eval not cached (stale callbacks now cached for original step)
- Add initial-position annotation to first move's
{}comment with[Initial position]prefix - Fix resignation/timeout comments hardcoded English →
T()i18n - First-principles rewrite of "Analyze All" batch logic:
_reviewAnalyzeStep/_reviewAnalyzeGen/_evalRequestBatchGendecoupled fromreviewStep
Phase 60 — Audit-driven fixes + stats board navigation
StockfishNative.javawriter-close paths unified to_writerLockInteger_bitcount()unsigned coerciononRenderProcessGone()crash-count backoff (3/60s)normalizeTagValue()tab filtering- Render-error page i18n (
render_error_titlekey) - Navigation buttons (
⏮ ◀ ▶ ⏭) below stats board (mainline/variation context-aware) - Static HTML export strips nav buttons (FEN-only); full-PGN export keeps them
Phase 61 — Cache pollution fix + PGN cache save + annotation move
_resetGameUIState()now centrally clears ALL game-related caches (15+ variables)- All 5 new-game entry points call
_resetGameUIState()uniformly _pgnCacheSaveCurrent()distinguishes pure-import vs live-play games- Initial-position eval annotation moved to separate
{}comment before first move - Stats-page PGN sync verified
Phase 62 — Visual annotation imported flag
_visualAnnotationsCacheentries now includeimportedboolean- Auto-generated annotations:
imported=false(UI display aids, not exported) - Imported PGN annotations:
imported=true(human-authored, exported) _buildPGNString()only exportsimported=trueentries
Phase 63 — Export annotation dialog
_buildPGNString()acceptsincludeAnnotationsparameter_showPGNExportAnnotationDialog()shown before copy/export/cache-save- Three options: Yes / No / Cancel
- 4 new i18n keys (bilingual zh/en)
Phase 64 — TRUE root cause fix: stale reviewStates
- Root cause:
_computeAndCacheVisualAnnotationsusedreviewStates[moveIdx+1]as a shortcut, butreviewStatesfrom a previous game's review session was not cleared in_resetGameUIState() - Fix: clear
reviewStates/reviewMode/reviewStep/reviewBaseStatein_resetGameUIState() - Defense-in-depth: gate
reviewStatesshortcut onreviewMode === true - Export dialog text update: 💾 emoji, "特殊注释" / "special annotations" terminology
Phase 65 — Dialog back-button + haptics + stale-state audit
- Export dialog: Android back-button support (equivalent to Cancel)
- All dialog buttons: explicit
HapticManager.fire('BUTTON_PRESS') - Stale-state audit:
_preReviewSnapshot,setupHistory/setupRedoStack, 7 dialog flags — all now cleared - Overlay click-outside fixed:
callback(null)(cancel) instead ofcallback(false)(No)
Phase 66 — Strict full-codebase audit
- Bug fix:
_savePGNYestiming (async dialog polling) - Redundancy: 2 stale
console.logremovals - Clarity:
openStatsPageexplicitincludeAnnotations=true - Audit confirmation: all caches, flags, and declarations verified correct
Key Features
♟️ Chess Engine
- Stockfish 18 (arm64-v8a-dotprod native build) — world-class engine
- 8 difficulty levels (Lv.1–6 by ELO, Lv.7 Skill Level, Lv.8 Custom)
- Real-time eval bar with depth, seldepth, nodes, NPS, and WDL
- MultiPV analysis with 🌿Line variation display
- Pondering support
🎨 User Interface
- Bilingual Chinese / English (toggle with one tap, persisted)
- Dark / Light mode (auto-follows system setting)
- Personified move animations — each piece type has unique motion characteristics
- Personified sound effects — each piece type has matching timbre
- Control heatmap — per-square control visualization with SVG arrows
- Eval trend chart — SVG line chart with pixel-perfect progress-bar alignment
- Review mode — full game replay with per-step eval, visual annotations, and critical-move markers
📝 PGN Support
- Full PGN import/export (1994 spec compliant)
[%eval]/[%csl]/[%cal]/[%emt]/[%clk]annotation support- Every-5-moves eval descriptions (bilingual, White-perspective)
- Initial-position eval annotation (separate
{}before first move) - Export dialog: user chooses whether to include special annotations
- PGN cache manager (📚) — save/load games to app-private storage
- Variations (RAV) import/export with 🌿Line display
🎯 Chess960
- Full Fischer Random Chess support
- ...
Regalia v1.1.0
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)
🎯 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-1or sum ≤ 0) → omit(%W/%D/%L). - Zero-sum WDL divide-by-zero guard — pathological
wdl 0 0 0engine output no longer producesNaN/Infinity. - Mate handling — uses
#+N/#-Nscore + 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-movesis stacked below.review-left(the board column, 256–320px tall), sooffsetTopincluded the board's full height —_targetwas way too large, clamped toscrollHeight - 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 previou...
Regalia v1.0.9
What's Changed
Full Changelog: v1.0.8...v1.0.9
Regalia v1.0.9
A precision bug-fix release. Two critical correctness issues resolved, two silent visual-annotation defects closed, and the review eval chart now speaks one consistent blue-vs-red language across both themes.
📦 Release Artifacts
| Asset | Size | Description |
|---|---|---|
Regalia-v1.0.9-release.apk |
~74 MB | Signed release APK (v1 + v2 + v3 signature schemes). Drop-in install on any Android 5.0+ arm64-v8a device. |
Regalia-v1.0.9-manual-en.html |
~813 KB | Self-contained English user manual (embedded CSS, no external resources). |
Regalia-v1.0.9-manual-zh.html |
~714 KB | Self-contained Chinese user manual (同步更新). |
Verifier's note: The APK is signed with all three legacy schemes so it installs cleanly on Xiaomi HyperOS 3, which enforces stricter signature validation than stock Android. Verify with:
apksigner verify --verbose Regalia-v1.0.9-release.apk # Expected: # 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
🎯 At a Glance
| Version | 1.0.9 (versionCode = 109) |
| Codename | Phase 52 — Correctness & Clarity |
| Engine | Stockfish 18 — arm64-v8a-dotprod (ARMv8.6-A DOTPROD instructions for NN inference acceleration) |
| Predecessor | v1.0.8 (Phase 51, 2026.7.2) |
| ABI | arm64-v8a only (modern 64-bit devices) |
| License | Dual: AGPL v3 (original work) + GPL v3 (DroidFish-derived & Stockfish) |
🔥 Headline Changes
This release concentrates on four root-cause fixes and one visual-design unification. No new features — every change is about making what already exists behave correctly.
🐛 Two CRITICAL Bug Fixes
1. PGN single-line import silently failed — 0 moves parsed, no error
Symptom
A PGN file where all headers and the movetext sit on one line (no \n between [Event "..."] and 1. e4) would import as zero moves. No toast, no warning — just an empty move list. The repro file PGN 2Kbug.pgn (71 half-moves, 11 variations, single line) triggered this 100% of the time.
Root cause
The tag-stripping regex used the multiline ^ anchor:
// BEFORE (broken)
let moveText = pgnText.replace(/^\[[^\]]*\]/gm, '').trim();With the m flag, ^ matches at the start of each line. But in a single-line PGN, the entire string is one line — so ^ matches only once (at the very start). Only the first tag ([Event "Regalia"]) was stripped; the remaining tags ([Site "?"], [Date "..."], …) leaked into the movetext as invalid SAN tokens. The tokenizer skipped them all, hit the 5-consecutive-skip safety limit, and aborted.
Fix
Replace the line-anchored pattern with a tag-format-specific pattern that matches [TagName value] where TagName starts with a letter:
// AFTER (fixed)
let moveText = pgnText.replace(/\[[A-Za-z]\w*\s+[^\]]+\]/g, '').trim();This is not anchored to line start, so it strips every tag regardless of PGN layout. It also cannot strip [%csl ...] / [%cal ...] / [%eval ...] inside brace comments — because % is not in [A-Za-z].
Companion fix — brace-comment stripping
The same single-line PGN exposed a second latent bug. The brace-comment stripper replaced {...} with empty string:
// BEFORE (broken) — e4{...}e5 became "e4e5" (one bogus token)
moveText = moveText.replace(/\{[^{}]*\}/g, '');In 1. e4{[%emt 6:25:53]}e5, there's no space between } and e5. Stripping to empty produced e4e5 — a single invalid token that failed SAN parsing and triggered the same cascade-failure path.
// AFTER (fixed) — replace with a SPACE; whitespace normalization collapses the doubles
moveText = moveText.replace(/\{[^{}]*\}/g, ' ');Verified: PGN 2Kbug.pgn now imports all 71 half-moves with all 11 variations intact.
2. Review & stats board showed "extra kings" — phantom pieces appearing during PGN replay
Symptom
After importing a PGN and entering review mode (or opening the stats page), the board would sometimes display more kings than legally exist — a white king on g1 and a white king on e1, for example. The corruption worsened as you stepped through the moves.
Root cause — two independent defects
Defect A — _castleSide read the wrong board (game-logic.js)
The castling-detection fallback (used when the explicit mv.castle flag is absent — the common case for moves reconstructed from moveRecords during review replay) checked the global gameState.board and gameState.castlingRights:
// BEFORE (broken) — reads the FINAL state, not the state being moved
const _cr = gameState && gameState.castlingRights ? gameState.castlingRights : null;
const _destEmpty = !gameState || !gameState.board[mv.to.row][mv.to.col];During PGN replay, the local state s being moved differs from gameState (which is the final state after all moves). Consider: if the white king ended the game on g1, then gameState.board[7][6] is non-empty (the king is there). When the replay reaches an earlier O-O move (king → g1), the _destEmpty check reads gameState (which has the king on g1) → returns false → castling detection is suppressed → only the king moves, the rook stays on h1.
Every subsequent move involving that misplaced rook then fails to replay, the board state diverges, and the user sees ghost pieces.
// AFTER (fixed) — accept an optional `s` parameter; use ITS board and rights
function _castleSide(mv, s) {
// …
const _st = s || (typeof gameState !== 'undefined' ? gameState : null);
const _cr = _st && _st.castlingRights ? _st.castlingRights : null;
const _destEmpty = !_st || !_st.board[mv.to.row][mv.to.row] || …;
// …
}
// Callers updated:
// makeMv(s, mv) → _castleSide(mv, s)
// makeMvInPlace(s, mv) → _castleSide(mv, s)
// moveAlg(s, mv, …) → _castleSide(mv, s)
// Animation-only callers in ui.js omit `s` and fall back to `gameState` (correct for live play).Defect B — stats.html executeMove false-positive castling (stats.html)
The stats page's independent move executor used an overly broad castling detector:
// BEFORE (broken) — ANY king move to col 6 or 2 was "castling"
if (piece.type === 'king' && (move.to.col === 6 || move.to.col === 2)) {
_isCastling = true;
// …treat the piece on the destination as the "castling rook" and reposition it…
}This caught normal king moves (Kf1-g1, Kg7-g6) and king captures (Kxg1). A king capture to col 6/2 would: (1) treat the captured piece as the "castling rook" and reposition it (silently losing the capture); (2) illegally displace the actual rook on h1/a1; (3) corrupt the board state — manifesting as extra pieces / extra kings.
// AFTER (fixed) — require ALL of: home row + correct distance + empty dest + right present
if (piece.type === 'king' && move.from.row === move.to.row) {
const _homeRow = piece.color === 'white' ? 7 : 0;
if (move.from.row === _homeRow && (move.to.col === 6 || move.to.col === 2)) {
const _destEmpty = !state.board[move.to.row][move.to.col];
const _cr = state.castlingRights || {};
const _dist = Math.abs(move.to.col - move.from.col);
const _is960 = (typeof gameVariant !== 'undefined' && gameVariant === 'chess960');
const _minDist = _is960 ? 1 : 2;
const _rightKey = …; // 'whiteKingside' | 'whiteQueenside' | 'blackKingside' | 'blackQueenside'
if (_dist >= _minDist && _destEmpty && _cr[_rightKey]) {
_isCastling = true;
// …find and move the actual participating rook…
}
}
}Verified: No extra kings appear at any review step for PGN 2Kbug.pgn (which contains 4 castling moves and many king moves to col 6/2).
🐛 Two HIGH-Severity Visual-Annotation Fixes
3. Variation comments contaminated main-line annotations
Symptom
After a variation containing [%csl] / [%cal] / [%eval] tags, the next main-line move would display the variation's squares/arrows/eval — as if those annotations described the main-line position. The review board was lying about the actual position state.
Root cause
The comment-extraction loop in _parsePGN read position-specific tags from {...} blocks but never checked the parenthesis depth _depth:
1. e4 (1. d4 {[%csl Bd4] [%eval 0.5]} d5 2. c4) e5 2. Nf3
↑ variation comment
↑ next main-line move — INHERITS the variation's an...
Regalia v1.0.8
What's Changed
Full Changelog: v1.0.7...v1.0.8
Regalia v1.0.8 — Personified Chess, Reimagined ♔
A standalone, open-source chess app for Android — play offline against Stockfish 18, analyze your games, and explore openings. No account, no network, no tracking.
versionCode 108 · versionName 1.0.8 · 30 development phases (Phase 22 → Phase 51)
🎯 What's New in v1.0.8
This is the biggest release in Regalia's history. v1.0.8 completely redesigns the move-animation and sound system around a single idea: every piece has a personality. A pawn shouldn't sound or move like a queen. Combined with full light/dark theme support and the Stockfish 18 arm64-v8a-dotprod engine, v1.0.8 is both the most expressive and the most robust version ever shipped.
✨ Headline Features
| Feature | What it does |
|---|---|
| 🎬 Personified Move Animations | Each of the 6 piece types has a unique motion characteristic via the Web Animations API — the pawn hesitates then darts, the knight traces an L-shape parabola, the queen glides in an elegant arc, the king steps with solemn weight. GPU-composited for sustained 120fps. |
| 🔊 Personified Sound Effects | A pure Web Audio API synth engine (zero audio files) gives every piece a matching timbre: pawn = triangle 3-stage, knight = sine sweep + ding, bishop = sawtooth + filter sweep, rook = square + noise + impact, queen = 3-freq harmony + LFO vibrato, king = bell partials + 4 footsteps. |
| 📳 Personified Haptics | Six dedicated haptic signatures (PAWN_MOVE → KING_MOVE) plus CASTLE and PROMOTION, each with its own throttle, amplitude, and temporal pattern. A single turn produces exactly one haptic (mutually-exclusive if/else if chain) — never a muddy double-buzz. |
| 🌓 Light / Dark Theme | Auto-switches with the system setting via dual-channel detection (Java UiModeManager + JS data-theme + CSS prefers-color-scheme). Light mode uses an elegant silver palette; dark mode keeps the warm brown-red + bright gold. The ♔/♚ king icon flips to match the on-board pieces. |
| 🧠 Stockfish 18 dotprod | The arm64-v8a-dotprod variant (ARMv8.6-A DOTPROD instructions for NN inference acceleration) — the strongest Stockfish build available for modern ARM devices. |
🎬 The Six Piece Personalities
Every piece now has a coherent animation + sound + haptic triad designed to feel like that piece's "character."
| Piece | Motion | Sound | Haptic | Duration |
|---|---|---|---|---|
| ♙ Pawn | Timid — hesitate back, then dart forward | Triangle wave, 3-stage | Three micro-trembles (low) | 260 ms |
| ♘ Knight | Agile — L-shape parabolic jump | Sine sweep + crisp ding | Lift-off + landing (mid) | 380 ms |
| ♗ Bishop | Sharp — quick diagonal glide | Sawtooth + filter sweep | Smooth bell curve (mid-low) | 270 ms |
| ♖ Rook | Fierce — charge → dash → impact + light shake | Square + noise + impact thud | Charge + heavy impact (mid-high) | 290 ms |
| ♕ Queen | Elegant — graceful arc + heavy shake | 3-freq harmony + LFO vibrato | Massive slam (highest) | 520 ms |
| ♔ King | Solemn — heavy single step + heavy shake | Bell partials + 4 footsteps | Four solemn thuds (high) | 560 ms |
Engineering note: Every animation frame is a pure
transformupdate — zero pixel ops. A single staticfilter: drop-shadowis cached on the composited layer, so the shadow travels with the piece "for free." Result: 120fps sustained even on mid-range devices.
🛡️ Robustness & Stability
v1.0.8 went through 5 independent first-principles code reviews (Phase 24, 28, 30, 46, 49), culminating in a 5-parallel-subagent audit of all ~27,000 lines. Every "避坑" (pitfall-avoidance) item is verified correct.
Bug fixes that mattered (Phase 49 highlights)
- Chess960 king-capture misclassified as castling — a king capturing to column 6/2 (e.g.
Kxg1with the king on f1) was silently destroying pieces. Fixed with an empty-destination guard. importPGNdata-loss on malformed[FEN]— an invalid FEN tag destroyed all persisted review evals across all games and corrupted the Chess960 mode, even though the import failed. Fixed by validatingstartStatebefore any side effects.onHintMoveapplied to the wrong position — if you moved between requesting a hint and the engine responding, the stale hint landed on the new position. Fixed with anisHintLoadingstaleness guard mirroringonBestMove's pattern.- Worker-pool Blob URL leak —
new Worker(url)failures leaked the Blob URL forever. Fixed by hoistingurland revoking on failure. - Heatmap cache-key collision — a 10-move variation and a 10-move mainline selection could produce identical cache keys → stale heatmap. Fixed with a content-based key.
The button-width saga (Phase 44 → 50)
A single UI bug — compact buttons stretching to full row width — survived seven fix attempts before the true root cause was found:
| Phase | Approach | Why it failed |
|---|---|---|
| 44 | width: fit-content |
Ignored in flex containers |
| 47 | Inline flex: 0 0 auto |
Correct for flex items — but the buttons were grid items |
| 48 | .btn-compact + !important flex |
Same flex approach, same failure |
| 49 | Added .btn-compact to click selector |
Robustness, not the root cause |
| 50 ✅ | .btn-row opts out of the grid transform |
The real fix |
Root cause: A portrait
@mediarule (.dlg-sec > div[style*="display:flex"]) was converting the button containers fromdisplay: flextodisplay: gridwithgrid-template-columns: 1fr auto— a layout designed for label+input rows. The buttons became grid items, and grid items ignore allflexproperties. The fix: a.btn-rowmarker class + a higher-specificity override (0,3,1>0,2,1, later source order) that restoresdisplay: flexand neutralizesgrid-template-columns.
Lesson: When flex properties have no visible effect, check whether the parent is secretly a CSS Grid.
🔧 Phase 51 — Three Final Fixes
After the button-width fix, three remaining issues were addressed:
1. PGN round-trip castling failure (HIGH)
Symptom: A game exported as PGN and re-imported would silently drop moves after any castling move (O-O / O-O-O). For example, a game ending 6.O-O O-O 7.Re1 a6 would import as 6.O-O O-O 7. _ a6 — the Re1 move vanished.
Root cause: pseudoMoves() attaches the castle flag to the to object ({row, col, castle}), and legalMoves() builds the full move as {from, to:{row,col,castle}, piece}. So mv.castle (top-level) is undefined for moves from legalMoves(). The UI path (executeMove) copies the flag to mv.castle before calling makeMv, but the PGN-replay path (_applySANMove) calls makeMvInPlace directly — mv.castle was undefined, so _castleSide() fell through to a heuristic that read the wrong board state. Result: castling only moved the king, not the rook. Subsequent rook moves failed to parse.
Fix: _castleSide() now checks both mv.castle (top-level, set by executeMove) and mv.to.castle (set by pseudoMoves). Verified: full Italian Game round-trips perfectly.
2. Move-classification label: "Book" → "Mediocre"
The review-mode move classification label for near-equal moves (eval delta < 50cp) was "Book"/"开局库". Changed to "Mediocre"/"平常" per user request — "Book" was misleading (it doesn't mean the move came from an opening book). The CSS class .book is unchanged.
3. Eval-chart dark-mode line invisibility
The review eval-trend chart's negative-eval line color (--chart-fill) was #1A1A2E (near-black) in dark mode — invisible against the #1a0a0a background. Changed to #5dade2 (light blue) for dark mode, clearly visible and hue-distinct from the positive-eval line (#E8E8F0 light cream). Light mode unchanged.
📱 Xiaomi HyperOS 3 Compatibility
Regalia is tested on Xiaomi HyperOS 3 and hardened against its aggressive process management:
- Foreground
EngineServicekeeps the Stockfish subprocess alive when the app goes to background - 5-second engine heartbeat detects zombie processes fast (OEM process killers are ruthless)
- Dual-channel theme detection —
prefers-color-schemealone is unreliable on HyperOS 3, so we also readUiModeManageron the Java side and mirror it to adata-themeattribute - 6-step WebView destroy prevents render-process crash loops
onRenderProcessGonerecovery- APK signed with v1 + v2 + v3 schemes — all three verified ✅
♟️ Full Feature List
Click to expand the complete feature list
Engine & Play
- Stockfish 18 —
arm64-v8a-dotprodnative build (ARMv8.6-A DOTPROD for NN acceleration) - 8 difficulty levels — 800 ELO (beginner) → 2800+ ELO (max strength) + Skill Level mode
- Chess960 / Fischer Random Chess — all 960 starting positions, proper castling rules, SP-ID selector, Shredder-FEN,
UCI_Chess960option - Ponder mode — engine thinks on your opponent's time
- WDL display — Win/Draw/Loss probability alongside the eval
- MultiPV analysis — 1–8 lines simultaneously
PGN & Analysis
- Standardized PGN — strict 1994-spec import/export, Seven-Tag Roster,
[%eval]/[%clk]/[%emt]annotations - NAG & visual annotations —
$1–$19NAGs; auto-cached[%csl]highlights +[%cal]arrows - Time-Control chess — Sudden Death / Fischer / Bronstein / US Delay, live clocks, auto-emit
[%clk] - Review mode — full replay, eval trend chart, move classification (brilliant / good / blunder), engine eval cache wit...
Regalia v1.0.7
What's Changed
Full Changelog: v1.0.6...v1.0.7
The source code files include an outdated README.md file along with the manuals. For detailed information about the relevant revisions (which encompass the manuals), please refer to the following:
Regalia v1.0.7 — Stability & Performance Maintenance Release
versionCode 107 · versionName "1.0.7" · 2026.6.28 – 2026.6.30
Stockfish 18 (arm64-v8a-dotprod) · Android 5.0+ (API 21) · arm64-v8a only
v1.0.7 is a code-quality and stability maintenance release based on three independent code-review reports + two comprehensive first-principles code-review passes (Phase 18/19) + two rounds of UX refinement (Phase 20/21). Across 21 phases, 52+ improvements were implemented. No new features were introduced, but numerous latent bugs were fixed, and the two remaining architectural suggestions from the Kimi audit report (LRU cache eviction + virtual list) were implemented.
📊 Release Overview
| Metric | Value |
|---|---|
| Total Phases | 21 |
| Fixes / Optimizations | 52+ |
| Code-Review Subagents | 7 (Phase 19) |
| Lines Reviewed | 28,281 |
| Critical Bug Fixes | 8 |
| Performance Breakthroughs | 3 |
| Compatibility | Xiaomi HyperOS 3 ✅ |
🎯 Key Highlights
⚡ Performance Breakthroughs
1. _reviewEvalCache LRU Eviction (Phase 18)
The old code commented "Unlimited cache size", arguing 60k entries ≈ 12MB was "negligible". First-principles analysis found three errors:
Old Claim Actual Reality
───────────────────────────── ─────────────────────────────
~200B per entry ~400B (JSON persistence overhead)
60k entries ≈ 12MB ≈ 24MB JSON
"negligible" main-thread block JSON.stringify blocks 100-300ms
localStorage quota ample WebView ~5MB → QuotaExceededError silently swallowed
Fix:
MAX_ENTRIES = 2000soft cap (~800KB JSON, well under any quota)_evictIfOverCap()evicts oldest entries by Map insertion order (= LRU order)get()/set()refresh LRU order via delete+set- Does NOT evict
_reviewEvalRequestedStep(the in-flight step) to avoid "analyzing..." flicker - Persistence preserves LRU order via
Array.from(m.entries()) - Backward-compatible: persisted files >2000 entries auto-evict on next
set()
2. Review Move-List Virtual List (Phase 18)
Threshold: 80 moves Overscan: 10 rows Debounce: 80ms
When move count > 80, windowed rendering is enabled — only the viewport + 10 rows of overscan above/below are rendered as DOM nodes; top/bottom spacer <div>s fill the total scroll height.
| Feature | Implementation |
|---|---|
| Shared render function | _buildReviewMovesInnerHTML(start, end) eliminates ~40 lines of landscape/portrait duplication |
| Scroll listener | Passive (passive:true), cannot block scrolling or interfere with scroll-restore |
| Partial refresh | _refreshReviewMovesOnly() replaces only .review-moves innerHTML |
| Dynamic row height | First render measures average height of first 10 rows via requestAnimationFrame |
| Window forcing | _forceReviewWindowToStep() ensures reviewGoTo() target is in window |
| State reset | _resetRvVirtualState() on enter/exit review, new game, import |
Performance: Not enabled under 80 moves (full render is faster for short lists); long-game render cost reduced from O(total moves) to O(visible + overscan).
3. Move Animation Slowed 30% (Phase 20, 120fps Preserved)
Piece Old Duration New Duration
─────────────────────────────────────────
Pawn 180ms 240ms
Knight 240ms 320ms
Bishop 210ms 280ms
Rook 180ms 240ms
Queen 260ms 340ms
King 210ms 280ms
- 120fps high-frame-rate preserved — GPU-composited via
will-change:transform+translate3d cubic-bezier(.25,.1,.25,1)easing curve unchanged; only duration is longer- CSS transition durations updated to match JS
durationstable
🐛 Critical Bug Fixes
Chess960 Castling "King Self-Capture" Fatal Bug (Phase 17)
Symptom: In Chess960 mode, when the king's starting position happens to be its castling destination square (e.g. an SP-ID where the white king starts on g1), castling caused the king to "capture itself" and vanish from the board.
Root cause: The makeMv/makeMvInPlace castling branch unconditionally executed:
ns.board[to.row][to.col] = ns.board[from.row][from.col]; // self-copy
ns.board[from.row][from.col] = null; // ← BUG: when from===to, this nulled the king's own squareFix: When castling AND from === to, skip the king move entirely (per the Fischer Random Chess "castling in place" rule). Also fixed unmakeMv and animateMove symmetrically. Added 20 Node-sandbox regression tests.
Chess960 unmakeMv Board Corruption (Phase 18, Critical)
Symptom: In ~half of all Chess960 SP-ID positions (e.g. white king on f1 castling kingside), the makeMvInPlace → unmakeMv round-trip corrupted the board.
Root cause: unmakeMv step 2 restores the king at f (king source), but the rook's destination cr.to may equal f (the rook moved to the king's old square) — step 2 already overwrote the rook with the king; step 4 s.board[cr.from] = s.board[cr.to] then read the king instead of the rook.
Fix: Save the rook piece itself in undo.castlingRook.piece, restoring from the saved piece rather than relying on board state.
_reviewEvalCache Corruption Race (Phase 19, Critical)
Symptom: requestEngineEval's cache-hit fast path and terminal-position fast path did not clear the pending debounce timer, allowing a stale debounce timer capturing a prior step's FEN to fire after the cache-hit return and overwrite the current step's correct cached eval.
Fix: Clear _reviewEvalDebounceTimer and increment _evalStaleGen on both fast paths to invalidate any in-flight callback.
Cross-Mode Stale Callback Race (Phase 19, Critical)
Symptom: onEngineEval's two filters are mode-exclusive; after exitReview, a review-mode callback still in flight could pass the normal-mode gen check and overwrite the correct eval for up to 30 seconds.
Fix: Capture _evalRequestReviewMode at request time; onEngineEval rejects cross-mode callbacks at the top.
PGN [%eval] Placeholder Offset (Phase 19, Critical)
Symptom: FEN-start PGNs where Black is to move had evals attach to the wrong reviewStep (off by 1).
Root cause: _parsePGN was unaware of the null placeholder that importPGN prepends after parsing.
Fix: Apply _placeholderOffset when populating the eval/annotation/comment caches.
StockfishNative Thread Safety (Phase 19)
Five fields changed from non-volatile to volatile:
private volatile Process engineProcess;
private volatile BufferedReader engineReader;
private volatile OutputStreamWriter engineWriter;
private volatile Thread readerThread;
private volatile ExecutorService _engineExecutor;Root cause: These fields are written from multiple threads (_engineExecutor worker, heartbeat thread, JS binder) without volatile/synchronization. A read on one thread may never observe a null write from another, producing NPEs or writes to a destroyed process's stream.
stopAndWaitForBestmove Timeout Stale-Bestmove Corruption (Phase 19)
Symptom: When the engine didn't respond to "stop" within 1 second, the late bestmove was processed as a real move, potentially generating an AI move for the wrong position.
Fix: On timeout, set _discardingPonderBestmove = true to discard the late bestmove.
ChessApp UncaughtExceptionHandler Swallowed Error (Phase 19)
Symptom: Worker-thread Errors (OOM/StackOverflow/NoClassDefFoundError) were silently swallowed, leaving the JVM in a corrupted state — e.g. if the SF-Reader thread died from OOM, the engine silently stopped responding while isProcessAlive() still returned true, causing a 30-120s apparent hang.
Fix: Error subclasses are now chained to the default handler so the process dies cleanly and restarts.
🎨 UX Refinements (Phase 20 + 21)
Review Eval Bar / Analyze Button Enlarged (Phase 20)
| Element | Old | New |
|---|---|---|
| Eval bar font (portrait/landscape) | .75rem / .7rem | .85rem / .8rem |
| Emoji size | .95rem | 1.1rem / 1.05rem |
| max-height | 1.9em | 2.4em |
| padding | 3px 8px | 5px 10px |
| Analyze button font | .7rem | .8rem |
| Analyze button min-height | 28px | 34px |
Text and emoji now display fully with comfortable breathing room — no longer cramped.
Portrait Dialog Positioning Precision Fix (Phase 21)
Phase 20 attempt → Phase 21 precision fix:
Phase 20: padding-top:18vh + padding-bottom:10px
→ center = 18 + (100-18-1)/2 ≈ 59vh ❌ BELOW center
Phase 21: padding-top:10px + padding-bottom:12vh
→ center = 10px + (100vh-10px-12vh)/2 ≈ 44vh ✅ slightly above center
The fullscreen dialog (New Game Settings) uses negative margins to cancel both paddings, maintaining true full-screen fill.
Landscape Anti-Shake Clipping Root-Cause Fix (Phase 21)
Phase 20 attempt (.bsec margin-right:10px) was ineffective — the clip happens at .bsec's own overflow:hidden boundary, not at the gap.
Phase 21 triple fix:
- Rolled back
margin-right:10px .bsecchanged tooverflow-x:visible; overflow-y:hidden_antiShakenow reserves 8px in BOTH orientation...
Regalia v1.0.6
What's Changed
Full Changelog: v1.0.5...v1.0.6
Regalia v1.0.6 — Release Notes
Version:
v1.0.6(versionCode 106)
Engine: Stockfish 18arm64-v8a-dotprod
Platform: Android 5.0+ (API 21) · ARM64 · Xiaomi HyperOS 3 compatible
License: AGPL v3 (application) + GPL v3 (DroidFish-derived code & Stockfish engine)
📥 Download
| File | Description |
|---|---|
Regalia-v1.0.6-release.apk |
Release APK (v1 + v2 + v3 signed) |
Regalia-v1.0.6-manual-zh.html |
Chinese user manual |
Regalia-v1.0.6-manual-en.html |
English user manual |
⚠️ Enable "Install from unknown sources" in system settings before installing.
🆕 What's New in v1.0.6
v1.0.6 is a feature-enhancement release with 10 major new features and 4 rounds of bug fixes (including critical Chess960 castling fixes).
♟️ Chess960 Improvements
ECO Opening Recognition Suppression
Chess960 (Fischer Random Chess) has no fixed opening theory. When Chess960 mode is active, the in-game ECO Opening info panel and Opening Recommendation bar are now fully suppressed — no more meaningless ECO lookups for non-standard start positions.
// game-logic.js — _startGameImpl()
_ecoEnabled = !(typeof dlgChess960 !== 'undefined' && dlgChess960);
// ui.js — ECO panels gated by gameVariant check
if (_ecoEnabled && !(gameVariant === 'chess960')) { /* show ECO panel */ }Chess960 Castling Detection Overhaul
Replaced the legacy Math.abs(to.col - from.col) === 2 pattern (which only works for standard chess) with an explicit castle flag on castling moves plus a unified _castleSide() helper:
| Detection Method | Standard Chess | Chess960 (1-col king move) |
|---|---|---|
Legacy === 2 |
✅ | ❌ |
mv.castle flag |
✅ | ✅ |
| Fallback (dest col + distance ≥ 2) | ✅ | ✅ (2+ col moves) |
Files updated: pseudoMoves(), makeMv(), makeMvInPlace(), moveAlg(), animateMove(), _uciToSimple(), tablebase.js applySANMove(), stats.html executeMove()
🏰 King-then-Rook Castling Gesture
A new castling operation method — essential for Chess960 where some positions can only be castled this way.
How it works:
- Select a king that can castle
- The castling-capable rook(s) are visually marked with a golden dashed ring (with pulse animation)
- Click the marked rook → castling executes instantly
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ ♖ │ │ │ ♔ │ │ │ │ ♖ │ ← Standard: king e1, rooks a1/h1
└───┴───┴───┴───┴───┴───┴───┴───┘
└──┐
▼ Select king → rook gets golden ring
┌───┬───┬───┬───┬───┬───┬───┬───┐
│ ♖ │ │ │ ╔═╗ │ │ │ ╔═╗ │ ♖ │ ← Click the ringed rook
└───┴───┴───┴───┴═╤═╩───┴───┴═╤═┘
└─ king→g1, rook→f1
Why it matters: In Chess960, when the rook's source square is the king's destination (e.g. rook on g1, king castles kingside to g1), the traditional "click the king's destination" gesture is ambiguous. The king-then-rook gesture resolves this.
🔧 Implementation Details
_computeCastlingRookSetForSelection(selPos, moves)— pure helper, no global state mutation_updateChangedSquares()— lightweight update path now syncs the marker (was only in full render)castle-ringCSS — golden dashed border +castlePulsekeyframe animation_getCastlingRookForClick()— click handler detects rook clicks and triggers castling
📊 Stats Page Per-Move Selection
Click any move or the initial FEN in the stats page's PGN Text panel to instantly view:
- The board position after that move (or the initial position)
- All statistics computed up to that move only — not including the effect of later moves
- Material balance, phase, pawn structure, eval trend, move quality distribution
When nothing is selected, the final complete statistics are shown.
🔄 PGN [SetUp]/[FEN] Round-Trip Preservation
Before: Importing a PGN with [SetUp "1"] and [FEN "..."] headers, then exporting, would drop both headers.
After: importPGN() now correctly assigns result.startFEN to _setupFEN, so _buildPGNString() regenerates both headers on export.
[SetUp "1"]
[FEN "rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR b KQkq e3 0 1"]
🎨 Unified Gray-Out Styling
When a toggle or stepper is grayed out (disabled), only the control itself is dimmed. Surrounding labels, descriptions, and explanations remain at full opacity.
| Element | Before | After |
|---|---|---|
| Toggle switch | Dimmed | Dimmed |
| Section heading | Dimmed | Full opacity |
| Description text | Dimmed | Full opacity |
| Explanation paragraph | Dimmed | Full opacity |
| Gray-out reason | Absent | New (red, same color as Engine Config warning) |
New gray-out explanations:
- "Chess960 is unavailable while AI Opening Book is enabled"
- "AI Opening Book is unavailable while Chess960 is enabled"
📱 Portrait-Optimized New Game Dialog
The New Game Settings dialog now has dedicated portrait-mode CSS while preserving the landscape layout:
| Property | Landscape | Portrait (<900px) | Narrow Portrait (<480px) |
|---|---|---|---|
| Max width | 500px | 460px | 96% |
| Padding | 24px | 18px 16px | 14px 12px |
| Time control input | 80px | 80px | 70px |
| ECO search row | Inline | Wrap | Stack vertically |
📜 Scroll-Position Preservation
Fixed a bug where scrollable windows would suddenly jump back to the top after a state change triggered a re-render.
Affected containers:
.dlg— dialogs (New Game, Engine Config, Import, About).panel— right-side info panel (landscape).review-moves— review mode move list.op-list— opening selectorstats.html#content— full document scroll
Mechanism: _savedContainerScrolls array captures scrollTop before app.innerHTML = h and restores it after (clamped to new scrollHeight).
⚙️ Engine Evaluation FEN Sanitization
Added _sanitizeFenForEngine() — strips inconsistent castling rights before sending to Stockfish.
Problem: The FEN 4k3/8/8/8/8/8/8/r1K4R w q - 1 3 has black queenside castling right (q) but no black rook on a8. Stockfish would enter a degraded state — no info lines, no bestmove, hanging until the 30s safety timer fired, then retrying indefinitely.
Solution:
function _sanitizeFenForEngine(fen) {
// Parse board, validate each castling right against actual piece placement
// K → white king on e1 AND white rook on h1
// Q → white king on e1 AND white rook on a1
// k → black king on e8 AND black rook on h8
// q → black king on e8 AND black rook on a8
// Shredder-FEN (Chess960) is passed through unchanged
}Applied to all engineEval, engineEvalDeep, engineHint, engineGo, engineGoTimed, and startPonder calls.
🏅 SL Mode Skill-Level Display
When the AI opponent is in SL (Skill Level) mode, the actual skill level value is now shown:
| Location | Before | After |
|---|---|---|
| AI opponent bar | SL |
SL20 |
| Top toolbar button | SL |
SL20 |
PGN [Black ""] tag |
AI Opponent SL |
AI Opponent SL20 |
New Java bridge method: getEngineSkillLevel() returns the current engineSkillLevel int.
🐛 Bug Fixes
Round 1 — Initial v1.0.6 Release
- White screen on launch — A backtick in a comment inside the
_WORKER_SOURCEtemplate literal (worker-pool.js) prematurely terminated the string, causing aSyntaxErrorthat blocked the entire script. Fixed by removing the backtick. executeMove()lostcastleflag — Themvobject was built as{from, to, piece, promotion}without copyingcastlefromlegalMvs, breaking Chess960 castling detection. Fixed by looking up the matching legal move._computeAndCacheVisualAnnotations()replay path — Samecastleflag loss during move replay. Fixed by restoring frommoveRecords.isCastling.
Round 2 — Castling Root Cause
executeMove()castle flag propagation — Comprehensive fix ensuringcastleflag flows fromlegalMvs→mv→makeMv()/moveAlg()/_castleSide()._castleSide()fallback logic — Improved to use destination column + distance (≥2 cols) instead of just=== 2, covering 2+ col Chess960 castling._uciToSimple()Chess960 detection — Simplified to distance-based (≥2 cols) to avoid 1-col ambiguity with normal king moves._sanitizeFenForEngine()Shredder detection — Simplified from double-regex to single/^[KQkq-]+$/._finishAnim()cleanup — Now clears_lastAnimMvto avoid stale references.
Round 3 — Chess960 Rook Capture & Analyze-All Off-by-One
-
Chess960 own rook captured during castling — In Chess960, when the rook's source square equals the king's destination (e.g. rook on g1, king castles kingside to g1),
makeMv()/makeMvInPlace()moved the king first (overwriting the rook), then tried to move the rook — but it was already gone.Fix: Detect castling before moving the king, save the rook piece reference, move the king, then place the saved rook at its destination. Also fixed
stats.html executeMove()(review mode replay).Test verification
Before: King e1, Rook g1 (Chess960) Kingside castling: king → g1, rook → f...