{"id":51173219,"url":"https://github.com/ydw99/regalia","last_synced_at":"2026-06-27T02:01:27.677Z","repository":{"id":365070730,"uuid":"1269313166","full_name":"YDW99/Regalia","owner":"YDW99","description":"Regalia — an open-source chess app. As of now, all the original codes are AI-generated.","archived":false,"fork":false,"pushed_at":"2026-06-25T03:57:05.000Z","size":4123,"stargazers_count":2,"open_issues_count":1,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2026-06-25T05:06:26.230Z","etag":null,"topics":["agpl-v3","ai-generated","android-app","chess","multilanguage","single-game","stockfish-chess","webview-app"],"latest_commit_sha":null,"homepage":"","language":"HTML","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/YDW99.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE\u0026NOTICE.zip","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":"AUTHORS-stockfish","dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":"NOTICE","maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-06-14T15:02:12.000Z","updated_at":"2026-06-25T03:16:14.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/YDW99/Regalia","commit_stats":null,"previous_names":["ydw99/regalia"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/YDW99/Regalia","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/YDW99%2FRegalia","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/YDW99%2FRegalia/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/YDW99%2FRegalia/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/YDW99%2FRegalia/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/YDW99","download_url":"https://codeload.github.com/YDW99/Regalia/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/YDW99%2FRegalia/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34839005,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-27T02:00:06.362Z","response_time":126,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["agpl-v3","ai-generated","android-app","chess","multilanguage","single-game","stockfish-chess","webview-app"],"created_at":"2026-06-27T02:01:26.856Z","updated_at":"2026-06-27T02:01:27.666Z","avatar_url":"https://github.com/YDW99.png","language":"HTML","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Regalia ♔\n\n\u003c!-- AI-GEN: AI assisted\n     This document was AI-assisted and has been reviewed for AGPL v3 compliance. --\u003e\n\nA standalone, open-source chess app for Android — play offline against Stockfish 18, analyze your games, and explore openings. No account, no network, no tracking. Now with **Chess960 (Fischer Random Chess)** support (v1.0.4).\n\n\"Regalia\" is used solely as a project name for this open-source chess app. No trademark rights are claimed. Anyone is free to fork and rename their own version.\n\n## Screenshots\n\n\u003cp align=\"center\"\u003e\n  \u003cimg src=\"assets/screenshot.jpg\" alt=\"Regalia gameplay screenshot\" width=\"280\"\u003e\n\u003c/p\u003e\n\n*Portrait mode — evaluation bar, move history, AI opponent display with ponder info, and Control heatmap*\n\n**Control Heatmap** — Tap the 🌗/🌈 button on the toolbar to toggle the control heatmap. Each square is dynamically colored by HSL to indicate which side controls it: blue-purple = your control, red = opponent's control, purple = contested. Hovering a square shows SVG arrows from each controlling piece to that square (warm gold for your pieces, cool silver-blue for opponent's). The info card below the board shows per-piece control contributions with position labels.\n\n**🌿Line** — In the move record, 🌿 lines appear below each move showing engine analysis variations (MultiPV) and PGN import variations (RAV). Each variation is labeled 🌿Line 1, 🌿Line 2, etc., assigned sequentially by display order. PGN import variations are automatically parsed and displayed as 🌿Lines with proper move numbering. Toggle the Variations switch to show or hide them.\n\n## Features\n\n- **Stockfish 18 Engine** — arm64-v8a-dotprod variant with NEON acceleration for optimal performance on modern devices\n- **Chess960 / Fischer Random Chess** (v1.0.4 NEW) — full support for the 960 starting positions with proper castling rules, SP-ID selector in New Game dialog, Shredder-FEN castling rights, and `UCI_Chess960` engine option\n- **Standardized PGN** (v1.0.4 NEW) — import/export follows the 1994 PGN spec strictly: Seven-Tag Roster always emitted, `[%eval]` / `[%clk]` / `[%emt]` annotations embedded, Result terminator enforced, tolerant parser auto-corrects malformed input\n- **NAG \u0026amp; Visual Annotations** (v1.0.4 NEW) — NAG ($1-$19) support; automatic selection \u0026amp; caching of `[%csl ...]` (square highlights) and `[%cal ...]` (arrows) per move: Square highlights — Blue=player net-control strong squares, Red=AI net-control strong squares, Yellow=high total-control squares, Green=neutral center squares; Arrows — Blue=multi-threat (one piece threatens 2+ enemy pieces), Red=check path, Yellow=queen-threat path, Green=escape squares\n- **Time-Control Chess** (v1.0.4 NEW) — Sudden Death / Fischer Increment / Bronstein Delay / US Delay modes; live clock display with low-time warning; auto-emits `[TimeControl \"...\"]` header and `[%clk HH:MM:SS]` per-move annotations; for untimed games, emits `[%emt HH:MM:SS]` (elapsed move time)\n- **Web Worker Pool** (v1.0.4 NEW) — `worker-pool.js` offloads PGN parsing, statistics computation, and control-map computation to a background thread; falls back to inline execution on devices without Worker support\n- **8 Difficulty Levels** — from beginner (800 ELO) to maximum strength (2800+ ELO), plus Skill Level mode\n- **PGN Import** — paste PGN from clipboard, or select a PGN file from your device\n- **Review Mode** — full game replay with evaluation trend chart, move-by-move analysis, move classification (brilliant/good/blunder), and engine evaluation cache\n- **MultiPV Analysis** — 1–8 lines of analysis simultaneously\n- **ECO Opening Classification** — 500+ standard openings with search, category filtering, and book move recommendations\n- **Syzygy Endgame Tablebases** — 7-piece endgame lookup via Lichess Tablebase API (requires network; auto-disables when offline)\n- **Position Setup** — custom board editing with FEN copy/import\n- **Ponder Mode** — engine thinks on opponent's time for stronger play\n- **WDL Display** — Win/Draw/Loss probability shown alongside evaluation\n- **Heatmap Control Statistics** (v1.0.4 NEW) — per-square average control across all positions, strongest/weakest square detection, center-control trend\n- **Bilingual UI** — full Chinese/English toggle via the ↔️ button on the toolbar, with automatic system language detection\n- **Landscape Support** — adaptive layout for both portrait and landscape orientation\n- **Engine Configuration** — full UCI parameter control with export/import\n- **Haptic Feedback** — responsive touch feedback throughout the interface\n\n## Download\n\nDownload the latest APK from [GitHub Releases](https://github.com/YDW99/Regalia/releases). Enable \"Install from unknown sources\" to install.\n\n## Requirements\n\n- Android 5.0 (API 21) or later\n- ARM64 device (arm64-v8a)\n- ~80 MB storage\n\n## Building\n\n### Prerequisites\n\n- Android SDK with API 35 (Android 15)\n- Gradle 8.x (wrapper included)\n- Stockfish 18 engine binary for arm64-v8a-dotprod\n\n### Build Steps\n\n1. Place the Stockfish engine binary at:\n   ```\n   /tmp/stockfish/stockfish-android-armv8-dotprod\n   ```\n\n2. Build the chess.html asset:\n   ```bash\n   cd src/main/assets/chess.src/\n   bash build-chess.sh\n   ```\n\n3. Build the APK:\n   ```bash\n   ./gradlew assembleRelease\n   ```\n\nThe signed APK will be at `build/outputs/apk/release/`.\n\n## Project Structure\n\n```\nRegalia/\n├── src/main/\n│   ├── assets/\n│   │   ├── chess.src/          # Source files (JS + CSS + HTML template)\n│   │   │   ├── game-logic.js   # Chess rules, move generation, i18n\n│   │   │   ├── chess960.js     # Chess960 SP-ID, Shredder-FEN, 960 castling rules (v1.0.4 NEW)\n│   │   │   ├── pgn-standard.js # Standardized PGN encoder/decoder, NAG, [%csl]/[%cal], TimeControl (v1.0.4 NEW)\n│   │   │   ├── worker-pool.js  # Web Worker pool for PGN/stats/control-map offloading (v1.0.4 NEW)\n│   │   │   ├── ai-bridge.js    # Engine communication, eval display, PGN export\n│   │   │   ├── ui.js           # Rendering, dialogs, interaction, review mode, time-control UI\n│   │   │   ├── eco-data.js     # ECO opening classification data\n│   │   │   ├── tablebase.js    # Lichess Syzygy tablebase queries + PGN import\n│   │   │   ├── index.html.tpl  # CSS template\n│   │   │   └── build-chess.sh  # Build script → chess.html\n│   │   ├── chess.html          # Built output (combined JS+CSS+HTML)\n│   │   ├── stats.html          # Statistics page (📊统计) — fullscreen WebView\n│   │   ├── AGPLv3_Logo.svg     # AGPL logo for About page\n│   │   └── GPLv3_Logo.svg      # GPL logo for 💾HTML export dialog\n│   ├── java/com/Regalia/\n│   │   ├── MainActivity.java   # WebView host, immersive mode, lifecycle\n│   │   ├── StockfishNative.java # Engine process management, UCI protocol, SAF file I/O\n│   │   │                       # v1.0.4: setChess960Mode() / isChess960Mode() bridge methods\n│   │   ├── StatsActivity.java  # Fullscreen WebView for 📊统计 statistics page\n│   │   ├── ChessWebViewClient.java # Page load handler\n│   │   ├── EngineService.java  # Foreground service for engine stability\n│   │   ├── ChessApp.java       # Application class, crash protection\n│   │   ├── TlsSecurityHelper.java # TLS 1.2+ enforcement for tablebase API\n│   │   └── RootDetector.java   # Informational root detection (About dialog)\n│   ├── cpp/\n│   │   ├── engine_jni.cpp      # JNI native chmod/renice (from DroidFish)\n│   │   └── CMakeLists.txt\n│   ├── res/\n│   │   └── values/strings.xml\n│   └── AndroidManifest.xml\n├── NOTICE                      # Third-party component notices\n├── NOTICE-DroidFish            # Original DroidFish notice\n├── NOTICE-gradle               # Gradle notice (Apache v2.0)\n├── AUTHORS-stockfish           # Stockfish project authors list\n├── LICENSE-AGPL v3             # AGPL v3 full text (application)\n├── LICENSE-GPL v3              # GPL v3 full text (engine components)\n├── LICENSE-Apache v2.0         # Apache v2.0 full text (Gradle)\n├── PRIVACY.md                  # Privacy policy\n├── build-chess.py              # Python build script (alternative to build-chess.sh)\n├── Regalia-v1.0.5-manual-zh.html  # Chinese user manual\n├── Regalia-v1.0.5-manual-en.html  # English user manual\n└── README.md\n```\n\n## Licensing\n\nRegalia is a **combined work** under dual licensing:\n\n| Component | License | File |\n|-----------|---------|------|\n| Original application code (UI, WebView, services, build scripts) | AGPL v3 | LICENSE-AGPL v3 |\n| DroidFish-derived code (engine management, game logic, PGN parsing, UI patterns) | GPL v3 | LICENSE-GPL v3 |\n| Stockfish 18 engine binary | GPL v3 | LICENSE-GPL v3 |\n| ECO opening data | CC0 (data) / AGPL v3 (code) | — |\n| Application icons | AI-generated / AGPL v3 | — |\n\nPer GPL v3 Section 13, these licenses are compatible for combination. Each component retains its original license. Since AGPL v3 imposes stricter network interaction provisions (Section 13), its obligations effectively extend to the entire combined work, ensuring users who access the work over a network retain the right to obtain source code.\n\n**Source code**: Available at https://github.com/YDW99/Regalia\n\n### GPL v3 Files (DroidFish-derived)\n\n- `StockfishNative.java` — Engine management logic\n- `engine_jni.cpp` — Native chmod/renice from DroidFish\n- `game-logic.js` — PGN disambiguation and SAN notation\n- `ai-bridge.js` — Engine communication patterns\n- `ui.js` — UI layout and interaction patterns\n- `tablebase.js` — PGN parsing (GameTree/PgnToken/PgnScanner)\n\n### AGPL v3 Files (original)\n\n- `MainActivity.java`, `ChessApp.java`, `ChessWebViewClient.java`, `EngineService.java`\n- `eco-data.js`, `index.html.tpl`, `CMakeLists.txt`\n- `build-chess.py`, `build-chess.sh`\n- All other project files not listed above\n\n### Third-Party Components\n\n- **DroidFish** — Engine management, game logic, PGN parsing, UI patterns (Copyright © Peter Österlund, GPL v3)\n- **Stockfish 18** — Chess engine (Copyright © T. Romstad, M. Costalba, J. Kiiski, G. Linscott, GPL v3)\n- **Lichess Tablebase API** — Endgame tablebase queries (public API, requires network)\n- **lichess-org/chess-openings** — ECO opening classification data (CC0)\n\nSee [NOTICE](NOTICE) for full attribution details. More declaration documents are preserved in [NOTICE-DroidFish](NOTICE-DroidFish) and [AUTHORS-stockfish](AUTHORS-stockfish).\n\n## Contributing\n\nContributions are welcome! Please ensure:\n\n1. All contributions to the application layer are licensed under AGPL v3\n2. Any modifications to DroidFish-derived or Stockfish code remain under GPL v3\n3. Code is tested on physical Android devices (especially Xiaomi HyperOS 3)\n\n## Version\n\n**v1.0.5** (versionCode 105) — current release\n\nThe v1.0.5 release adds (Round-6 Revision 49):\n\n1. **High aspect-ratio screen adaptation** — every interface scrolls vertically only,\n   never horizontally, on any aspect ratio (ultra-tall phones, foldable inner screens,\n   ultra-wide tablets).\n2. **Notch/cutout/R-corner adaptation** — `shortEdges` cutout mode in AndroidManifest,\n   `viewport-fit=cover` + `env(safe-area-inset-*)` CSS, auto-avoids notches/cutouts/R-corners.\n3. **Sensor-fusion board anti-shake (OIS-style)** — new `StabilizationHelper.java` fuses\n   `TYPE_LINEAR_ACCELERATION` (gravity-free translation) + `TYPE_GAME_ROTATION_VECTOR`\n   (preferred, no magnetometer) / `TYPE_ROTATION_VECTOR` (fallback) sensors;\n   board counter-shifts in real time to cancel device shake, plus ±2° roll-only\n   tilt compensation. Toggle by long-pressing any board square (Toast in zh/en).\n4. **Stats page \"PGN Text\" → \"PGN Text (After processing)\"** — clarifies the PGN is\n   reconstructed/processed, not raw.\n5. **Review arrow shrink** — arrows shrunk (4→2px stroke, 8×6→5×4 head) with per-arrow\n   angular offset when multiple arrows share a target, so they stay distinguishable.\n6. **Phase analysis precision** — rewritten per 国际象棋三阶段精确区分.md: multi-criteria\n   detection (undeveloped minors, king castled, queen present, king advanced) replaces\n   the old fixed material threshold.\n7. **Manual chapter reorder** — Legal chapter moved to ch.2; manual changelog newest-first.\n\n### Patch revisions under v1.0.5 (version number unchanged — still v1.0.5 / versionCode 105)\n\nMultiple patch revisions have been applied under the same v1.0.5 version number.\nThe most significant recent revisions are:\n\n- **Round-6 Revision 50** — Version-number residual cleanup (`v1.0.4` → `v1.0.5`\n  in 3 UI locations); anti-shake Y-axis direction correction; PGN comment-merge\n  fix (comments now attach to the PRECEDING move per PGN spec; only same-move\n  comments merge with \" | \").\n- **Round-6 Revision 51** — App-launcher icon label synchronized to\n  \"Regalia v1.0.5\"; anti-shake Y-axis direction further corrected.\n- **Round-6 Revision 52** — Anti-shake screen-orientation auto-adapt (4 display\n  rotations: 0°/90°/180°/270°); visual-annotation null-reference fix when\n  `moveRecords[moveIdx]` is null (now uses `{csl:[],cal:[]}`).\n- **Round-6 Revision 53** — Precise sensor re-selection: only\n  `TYPE_LINEAR_ACCELERATION` + `TYPE_ROTATION_VECTOR` + `Display.getRotation()`\n  (removed accelerometer/gyroscope/gravity/game-rot/geomagnetic-rot sensors).\n  ±2° tilt compensation via `getRotationMatrixFromVector()`+`getOrientation()`.\n  Yellow arrows extended to bidirectional queen threats (both sides' queens).\n  Blue arrows also bidirectional. New `_computeInitialPositionAnnotations()`\n  for the initial position (reviewStep 0).\n- **Round-6 Revision 54** — Six improvements per user spec:\n  1. **Yellow arrow extension** — when the mover's just-moved piece is a queen\n     and its destination is attacked by an opp piece (\"主动把皇后移动到受威胁\n     格\"), that yellow arrow's priority is BOOSTED to survive the cap-6 dedup.\n  2. **Blue arrow extension** — when the mover's moved piece is itself at a\n     threatened square (\"主动把任意棋子移到受威胁格\") AND this causes an opp\n     piece to have 2+ threats on mover pieces, those blue arrows are BOOSTED.\n  3. **Per-color arrow offsets** — each color has a FIXED diagonal offset\n     (Blue→Upper-Left, Yellow→Lower-Left, Red→Upper-Right, Green→Lower-Right)\n     applied to both start and end so overlapping arrows of different colors\n     are visually separable.\n  4. **Yellow square rounded-rectangle style** — yellow squares now render as\n     a separate rounded-rectangle overlay (2px solid border, ~15% border-\n     radius, ~10% inset) above the cell, leaving four-corner gaps so other\n     colors' box-shadow insets remain visible underneath.\n  5. **Roll-only board rotation** — `StabilizationHelper` rotation restricted\n     to SCREEN-ROLL only (the steering-wheel tilt). Screen-pitch (nodding)\n     and screen-yaw (door-like horizontal turn) are NOT compensated.\n  6. **First-principles anti-shake audit** — cross-checked every sensor\n     choice against the two uploaded sensor reference docs. Key change:\n     PREFER `TYPE_GAME_ROTATION_VECTOR` (no magnetometer → no magnetic\n     drift near laptops/metal desks) with `TYPE_ROTATION_VECTOR` fallback.\n     Verified: `SENSOR_DELAY_GAME`, α=0.15 low-pass filter, 0.3° dead zone,\n     decay-based drift prevention, lifecycle pattern, null-sensor fallback.\n     Confirmed: `TYPE_ORIENTATION` / `device_orientation` is deprecated —\n     we correctly use `Display.getRotation()`.\n- **Round-6 Revision 55** — Line-by-line code audit of every source\n  file, prioritized: bug fixes \u003e features \u003e perf \u003e redundancy \u003e simplification.\n  1. **StatsActivity security parity** (bug) — the stats WebView was missing\n     the defense-in-depth security flags that MainActivity has\n     (`setAllowFileAccess(false)`, `setAllowContentAccess(false)`,\n     `setAllowFileAccessFromFileURLs(false)`,\n     `setAllowUniversalAccessFromFileURLs(false)`,\n     `setMixedContentMode(NEVER_ALLOW)`, `setFilterTouchesWhenObscured(true)`).\n     Added all of them so the stats page has the same security posture as the\n     main game.\n  2. **StatsActivity FLAG_FULLSCREEN fix** (bug) — was using the deprecated\n     `FLAG_FULLSCREEN` unconditionally, which conflicts with Edge-to-Edge\n     enforcement on Android 15+ (same black-screen bug MainActivity fixed in\n     v18.4.6). Now gated on `SDK_INT \u003c R`, matching MainActivity.\n  3. **StatsActivity immersive mode** (feature) — the stats page now hides\n     system bars (status + navigation) just like the main game, using the\n     same platform-aware approach (`WindowInsetsController` on API 30+,\n     legacy flags on API 21-29).\n  4. **engineGoDepth eval-mode options** (bug) — `StockfishNative.engineGoDepth`\n     (used for deep eval / analyze-all) was missing the\n     `applyEvalModeOptions()` call that `engineEval` has. Without it, deep\n     eval ran with gameplay `Contempt=24` (biased toward avoiding draws) and\n     the user's `MultiPV` setting (potentially \u003e1, reducing depth). Fixed by\n     adding the call; the bestmove handler already restores gameplay settings.\n  5. **getCtrlMap perf** (perf) — hoisted the attacker-position object out of\n     the inner loop. Was allocating ~256 objects per ctrl-map (per render\n     tick when heatmap is on); now allocates max 32 (one per piece) and\n     reuses.\n  6. **EngineService notification builder** (redundancy) — extracted shared\n     `_buildNotificationWithContent()` helper used by both\n     `buildNotification()` and `updateNotification()`, eliminating ~50 lines\n     of duplicated builder configuration.\n  7. **worker-pool fenToState consistency** — worker's check was\n     `parts.length\u003c4`, main thread's is `\u003c2`. Changed worker to `\u003c2` so\n     minimal FENs (board + side-to-move only) are accepted by both paths.\n  8. **chess960.js stale comment cleanup** — removed a misleading comment\n     that claimed a line was \"dead code\" (it had been consolidated in\n     v1.0.4). Code was correct; only the comment was stale.\n- **Round-6 Revision 56** — Audit of `ai-bridge.js` MultiPV\n  processing path and `ui.js` dirty-flag render pipeline. Six improvements:\n  1. **MultiPV toggle-off stale lines** (bug) — `setConfigMultiPV(1)` mid-search\n     left stale secondary PV lines (index 2..N) visible until the next\n     `onBestMove` (seconds away). Now `_multiPVLines` and the display cache\n     are cleared immediately, and the display refreshes instantly.\n  2. **MultiPV display signature cache** (perf) — `_updateMultiPVDisplay()`\n     was re-converting ALL PV lines from UCI to SAN on every progress tick\n     (10-50×/sec), wasting 80+ `makeMv` calls per tick when the PV content\n     was identical. Added `_multiPVDisplayCache` (per-line signature) to\n     skip the conversion + DOM write when nothing changed.\n  3. **MultiPV sort skip** (perf) — `onMultiPVProgress()` +\n     `onEngineProgress()` sorted `_multiPVLines` unconditionally on every\n     tick. Now only sort when a new line was appended (indices don't change\n     on in-place update).\n  4. **Eval display signature cache** (perf) —\n     `_updateEvalDisplayIncremental()` rebuilt `innerHTML` on every\n     `DIRTY_EVAL` tick even when eval values were unchanged (common during\n     deep search). Added `_evalDispPrevSig` signature check to skip the DOM\n     write; invalidated on full render.\n  5. **MultiPV cache invalidation** (robustness) — wired\n     `_clearMultiPVDisplayCache()` into all three MultiPV-reset paths\n     (`onBestMove`, `restartCurrentEngine`, engine-recovery) so a recovered\n     engine doesn't inherit stale display-cache signatures.\n  6. **Dirty-flag pipeline audit** — confirmed `_performDirtyRender`'s\n     flag-routing is correct; the TOOLBAR/PANEL/MOVES → full-render branches\n     are technically dead code (always accompanied by bitcount\u003e2) but left\n     as-is for intent clarity and future-proofing.\n- **Round-6 Revision 57** — CRITICAL FIX for blank-screen-on-launch\n  regression introduced in Rev55. The Rev55 comment added to\n  `worker-pool.js fenToState()` (inside the `_WORKER_SOURCE` template literal)\n  used backticks around `parts.length\u003c4`. Since `_WORKER_SOURCE` is a JS\n  template literal (delimited by backticks), the backtick inside the comment\n  **prematurely terminated** the template literal, causing a `SyntaxError`\n  that crashed the entire `chess.html` bundle at parse time. Symptom: app\n  launches to a blank screen showing only the background color. Fix: removed\n  the backticks from the comment text. The actual code change from Rev55\n  (`parts.length\u003c4` → `parts.length\u003c2`) is correct and unchanged. Verified\n  with `node --check` on the bundled JS (exit code 0).\n- **Round-6 Revision 58** — Two improvements per user spec:\n  1. **Review-board arrow offset redesign** (ui.js) — Redesigned the arrow\n     offset logic so: (a) each color's start offset = end offset (same bias\n     direction, arrow direction preserved); (b) reverse-overlap (A→B and\n     B→A) only happens for same-color arrows (they land on the exact same\n     line; different-color reverse arrows are parallel but never collinear);\n     (c) different-color arrows never overlap on the same line (guaranteed\n     by four distinct diagonal biases: B→UL, Y→LL, R→UR, G→LR). Removed the\n     Rev49 perpendicular \"fan\" offset that broke same-color reverse-arrow\n     overlap. Added arrow deduplication by (color, from, to).\n  2. **Magnetic-field-aware sensor switching** (StabilizationHelper.java) —\n     On start, uses `TYPE_ROTATION_VECTOR` (accurate baseline via\n     magnetometer). Background-monitors `TYPE_MAGNETIC_FIELD`; when magnetic\n     disturbance detected (|B| deviation \u003e 15 µT from rolling baseline),\n     switches to `TYPE_GAME_ROTATION_VECTOR` (no magnetometer, immune to\n     interference). When field calms (\u003c 8 µT for 2 seconds), switches back\n     to `TYPE_ROTATION_VECTOR`. Hysteresis prevents oscillation. Both sensors\n     registered simultaneously (no re-registration gap).\n\n- **Round-6 Revision 59** — CRITICAL FIX for rotation-axis bug\n  in landscape orientations. The board's tilt-compensation rotation was\n  controlled by the WRONG axis — it responded to screen pitch (front-back nod)\n  instead of screen roll (steering-wheel tilt). Root cause:\n  `SensorManager.getOrientation()` was called on the raw device-frame rotation\n  matrix, which returns angles in the DEVICE's natural coordinate system, not\n  the screen's. In landscape (ROTATION_90/270), device pitch and roll are\n  swapped relative to the screen. Fix: Added\n  `SensorManager.remapCoordinateSystem()` to transform the rotation matrix\n  from device frame to screen frame BEFORE calling `getOrientation()`. After\n  remapping, `orientationAngles[2]` is ALWAYS screen-roll (steering-wheel)\n  regardless of display rotation. Simplified `dispatchTransform()` rotation\n  to `-(smoothRoll - baselineRoll)` for ALL 4 orientations. Verified correct\n  for 0°/90°/180°/270°. Also verified the Rev58 arrow offset logic already\n  satisfies the user's requirements (same color = same bias for start+end;\n  different colors = different biases).\n\n- **Round-6 Revision 70** (current, 2026.6.27 net-control algorithm consistency fix) —\n  Per user spec: the [%csl] blue/red net-control algorithm must match the\n  \"Square Control\" info panel's net control. One fix:\n  1. **Net-control consistency** (ui.js, BUG FIX) — blue/red candidates changed\n     from one-sided dominance (`pAtk\u003e0 \u0026\u0026 aAtk===0` / `aAtk\u003e0 \u0026\u0026 pAtk===0`) to\n     netCtrl-based (`netCtrl\u003e0` / `netCtrl\u003c0`), where `netCtrl = pAtk - aAtk`.\n     This matches the info panel's `myCtrl - opCtrl`. A square with 3 white +\n     1 black (netCtrl=+2) now correctly marks blue; previously it didn't (AI\n     had an attacker, failing the one-sided condition). Score ranks by\n     `|netCtrl|`. Both `_computeAndCacheVisualAnnotations()` and\n     `_computeInitialPositionAnnotations()` updated. Spec comment items 3-4\n     updated.\n\n- **Round-6 Revision 69** (2026.6.27 multi-color per square + heatmap text deletion) —\n  Per user spec: allow multi-color highlights on the same square; delete the\n  heatmap conditional-display text from manuals/code. Two changes:\n  1. **Multi-color per square** (ui.js, FEATURE) — `_rvCslMap` changed from\n     `{square: {color}}` (single-color overwrite) to `{square: [colors]}`\n     (array preserving all colors). Render code iterates colors: B/R/G accumulate\n     into comma-separated box-shadow inset; Y emits yellow rounded-rect overlay.\n     A square can now show both blue box-shadow AND yellow rounded-rect.\n  2. **Heatmap text deletion** (manuals + stats.html, DOC) — deleted\n     \"不含热力图控制统计数据的 PGN → 不显示热力图控制统计区块\" / English\n     equivalent from v1.0.5 + v1.0.4 manuals (zh+en) and stats.html code comment.\n\n- **Round-6 Revision 68** (2026.6.27 review yellow rounded-rect position fix) —\n  Per user report: review-board yellow [%csl] rounded-rect overlay mispositioned.\n  One fix + documentation audit:\n  1. **Yellow rounded-rect position fix** (ui.js, CRITICAL BUG) — the yellow\n     overlay div was emitted AFTER the cell div's `\u003c/div\u003e` instead of before,\n     so `position:absolute` anchored to `.bgrid` (via `transform:translateZ(0)`)\n     instead of the cell (`position:relative`). All yellow overlays stacked at\n     the board origin. Fixed by moving the overlay emission to before the cell's\n     `\u003c/div\u003e` so it becomes a child of the cell div. Parameters unchanged.\n  2. **README/LICENSE/NOTICE audit** — checked all 14 matching files; added\n     Rev68 entries to chess.src/README.license, java/README.license,\n     Manual/README.license, NOTICE, README.md; added Rev54-68 summary entries\n     to src/main/README.license, src/main/assets/README.license,\n     src/main/res/README.license, src/main/cpp/README.license (were stalled at\n     Rev53). LICENSE-* and NOTICE-* original texts need no update.\n\n- **Round-6 Revision 67** (2026.6.27 landscape translation direction fix) —\n  Per user request: rigorously verify translation anti-shake direction via first\n  principles. Found ROTATION_90/270 boardPxX sign inverted (landscape horizontal\n  shake amplified instead of cancelled). One fix:\n  1. **Landscape boardPxX sign fix** (StabilizationHelper.java, CRITICAL BUG) —\n     ROTATION_90: `boardPxX = -dispY` → `+dispY`; ROTATION_270:\n     `boardPxX = +dispY` → `-dispY`. ROTATION_0/180 already correct. Derived\n     per-orientation from first principles: device-natural Dx/Dy → world-axis\n     mapping, then OIS (device world-right → board screen-left CSS -x; device\n     world-up → board screen-down CSS +y). Comment rewritten as derivation\n     table. Impact: landscape anti-shake horizontal direction now correct;\n     portrait unaffected.\n\n- **Round-6 Revision 66** (2026.6.27 yellow overlap + comment cleanup) —\n  Per user spec: allow yellow squares to overlap with blue/red; comprehensively\n  delete outdated/redundant comments. Two changes:\n  1. **Yellow overlap allowed** (ui.js, FEATURE) — Rev65 EXCLUDED blue/red\n     squares from yellow contention. Rev66 removes that exclusion — yellow now\n     simply takes the top 3 by total attacker count, regardless of whether\n     they're also blue or red. Rendering layer (box-shadow inset + yellow\n     rounded-rect overlay) already supports multi-color highlights. Both\n     `_computeAndCacheVisualAnnotations()` and\n     `_computeInitialPositionAnnotations()` updated. Spec comment item 5 updated.\n  2. **Comment cleanup** (10 files, REDUNDANCY) — audited and cleaned ~120\n     lines of stale comments: StabilizationHelper.java Rev64 tombstones +\n     Rev61 Y-axis fix; MainActivity.java wrong public-contract; ui.js\n     arrow-rendering Rev49/54/58 narrative + Rev65/66 yellow explanations +\n     Rev62 resign-sound + Rev63 arrow markers; index.html.tpl CSS multi-rev;\n     worker-pool.js Rev57 backtick; chess960.js Rev62 array-swap + Rev55 meta;\n     pgn-standard.js Rev62 lineLen; ai-bridge.js Rev62 _importedStartColor +\n     mate:0/null + depth\u003e0. Principle: retained license headers, current-\n     behavior docs, algorithm explanations, i18n, pitfall warnings; deleted\n     tombstones describing deleted code / fixed bugs / multi-rev narratives.\n\n- **Round-6 Revision 65** (2026.6.27 yellow-square logic fix) —\n  Per user spec: yellow squares (non-arrow) should be the squares with the\n  highest total control count (total attackers, regardless of piece color).\n  One change:\n  1. **Yellow-square logic fix** (ui.js, BUG FIX) — Rev61's condition\n     `pAtk\u003e0 \u0026\u0026 aAtk\u003e0` (both sides must have attackers) was too strict,\n     excluding high-total one-sided squares. Rev65 changes it to `total\u003e=2`\n     (all squares with 2+ total attackers are candidates), then excludes\n     squares already chosen as blue/red (one-sided dominance), and picks the\n     top 3 by total. This correctly implements \"黄色格子=双方总控制数量最多的\n     那几个格子\". Both `_computeAndCacheVisualAnnotations()` and\n     `_computeInitialPositionAnnotations()` updated. Spec comment item 5 updated.\n\n- **Round-6 Revision 64** (2026.6.27 remove rotation + undo cache sync) —\n  Per user spec: remove the rotation part of anti-shake (keep only translation),\n  delete all rotation code/comments; and after undo, revert the corresponding\n  content in the background real-time cached PGN. Two changes:\n  1. **Anti-shake rotation removed** (StabilizationHelper.java + index.html.tpl,\n     FEATURE) — `StabilizationHelper.java` completely refactored. Deleted\n     sensors: `TYPE_ROTATION_VECTOR`, `TYPE_GAME_ROTATION_VECTOR`,\n     `TYPE_MAGNETIC_FIELD` (only `TYPE_LINEAR_ACCELERATION` retained). Deleted\n     methods: `processRotationVector()`, `processMagneticField()`. Deleted all\n     rotation state fields, constants, scratch arrays. CSS `.bwrap.stabilized`\n     `transform` changed from `translate3d(...) rotate(var(--stab-rot,0deg))` to\n     `translate3d(var(--stab-x,0px),var(--stab-y,0px),0)` only. `applyTransform()`\n     no longer sets `--stab-rot`; added `removeProperty('--stab-rot')` to clear\n     residual values. File slimmed from 720 → 337 lines (53% reduction).\n     Benefits: 4 sensors → 1 sensor, lower battery, eliminates the entire class\n     of rotation-direction bugs from Rev59-63.\n  2. **Undo reverts cached PGN content** (ui.js, BUG FIX) — `undoMove()` now\n     calls new `_invalidateCachesForUndoneMoves(currentMoveCount)` after\n     restoring `moveRecords`. Clears three caches: `_reviewEvalCache` (key \u003e\n     currentMoveCount), `_visualAnnotationsCache` (key \u003e= currentMoveCount,\n     '_initial' sentinel kept), `_cachedOriginalPGN` (set to null). This\n     ensures stale eval/annotation data for undone moves doesn't leak into\n     re-played moves, review mode, or PGN export. `_redoStack` is NOT cleared\n     (undo must remain reversible).\n\n- **Round-6 Revision 63** (2026.6.27 anti-shake + arrow optimization) —\n  Per user spec: limit anti-shake rotation to ±10°, optimize arrow\n  positioning and redesign arrow shape. Three changes:\n  1. **Anti-shake rotation limit ±10°** (StabilizationHelper.java, FEATURE) —\n     `MAX_ROTATION_DEG` reduced from ±30° (Rev61) to ±10°. First-principles:\n     sensor floor ~1°, human perception ~0.5-1°, handheld tilt rarely \u003e8°,\n     1:1 mapping preserved in 1°-10° range. Dead zone 1.0° unchanged.\n  2. **Arrow offset optimization** (ui.js, FEATURE) — offset magnitude changed\n     from `Math.min(3, cellSize*0.08)` (3px cap + 8%) to `cellSize*0.12` (12%\n     no cap). Scales proportionally with cell size so offset fraction is\n     constant across screens. Verified safe for cellSize 20-60px (max radial\n     offset = cellSize/2 - 2.75, we use 12% which is well within bound).\n     Cell-center formula `col*cellSize + cellSize/2` confirmed exact for all\n     cell sizes.\n  3. **Arrow shape redesign** (ui.js, VISUAL) — minimal arrow: thin 1.5px\n     line (was 2px) + compact 4×3 triangular arrowhead (was 5×4) + butt\n     linecap + no origin dot. Line shorten 9→4px (matches new arrowhead).\n     stroke-opacity 0.95→0.9 (pieces underneath remain readable). The minimal\n     representation that still communicates origin, target, direction, type.\n\n- **Round-6 Revision 62** (2026.6.27 second-pass audit) — Second-pass\n  first-principles code audit. One critical bug fix + 5 bug fixes + 3 perf\n  improvements + redundancy cleanup:\n  1. **Chess960 SP-ID bishop-color array swap fix** (chess960.js, CRITICAL BUG)\n     — `_CH960_LIGHT_BISHOP_FILES` and `_CH960_DARK_BISHOP_FILES` were swapped.\n     a1/c1/e1/g1 are DARK squares (not light), b1/d1/f1/h1 are LIGHT (not dark).\n     This broke 720/960 SP-IDs; SP-ID 518 produced \"RNQBBKNR\" instead of\n     \"RNBQKBNR\". After fix, all 960 SP-IDs pass round-trip test. The parity\n     checks in `backRankToSPID()` were fixed in sync.\n  2. **Tablebase 404 misidentified as server-down** (tablebase.js, BUG) — 404\n     means \"position not in tablebase\", not \"server down\". 3 × 404 falsely set\n     `_tbOffline=true` for 60s. Now only 5xx + network errors count.\n  3. **PGN move-number regex missing 'O'** (worker-pool.js + tablebase.js, BUG)\n     — `(?=[a-hKQRBN])` didn't match 'O', so \"1 O-O\" wasn't normalized to\n     \"1. O-O\" and \"1\" was mis-tokenized. Fixed to `[a-hKQRBNO]`.\n  4. **Resign sound never played** (ui.js, BUG) — `_resignGame()` called\n     undefined `_playSound('lose')`. Now calls `playSound('gameover')`.\n  5. **mate:0 vs mate:null distinction lost** (ai-bridge.js, BUG) — `c.mate||0`\n     lost the checkmate-now distinction. Fixed to `c.mate!=null?c.mate:0`.\n  6. **worker-pool Map delete-during-iteration** (worker-pool.js, BUG) —\n     `for...of` + `.delete()` risked skipping entries on some engines. Fixed\n     to snapshot keys via `Array.from()` first.\n  7. **ECO recommendation LRU no-refresh-on-hit** (game-logic.js, PERF) — cache\n     hit returned without delete+re-insert, so hot entries could be evicted\n     before cold ones. Fixed to refresh LRU on hit (matching `_tbCache` pattern).\n  8. **MultiPV display wasted work during AI thinking** (ai-bridge.js, PERF) —\n     computed hintParts/signatures/cache checks 10-50×/sec during AI thinking\n     but discarded the result. Early-exit when `!isHintLoading`.\n  9. **Eval chart label loop LRU churn** (ui.js, PERF) — used `get()` (refreshes\n     LRU) instead of `peek()` in a read-only iteration. Fixed to `peek()`.\n  10. **Redundancy cleanup** — removed dead `_importedStartColor` ternary\n      (variable never declared, guard always true); removed dead `let lineLen=0`\n      in composePGN; simplified redundant `depth\u003e0` check; simplified two\n      unreachable ternary branches.\n  11. **chess960.js comment correction** — `randomSPID()` comment claimed\n      \"P(\u003e8 retries) ≈ 0.4%\"; actual is (256/65536)^8 ≈ 1.3e-19. Code correct,\n      comment fixed.\n  12. **Known Chess960 castling detection limitation** (DOCUMENTED, NOT FIXED)\n      — `Math.abs(to.col-from.col)===2` only detects castling when king starts\n      on col 4. In Chess960 (king can start on cols 1-6), ~78.8% of positions\n      have broken castling detection. HIGH risk to fix (4 coordinated locations\n      + Zobrist hash); deferred to a dedicated task. Standard chess unaffected.\n\n- **Round-6 Revision 61** (2026.6.27) — Per the 2026.6.27\n  development plan. Two critical bug fixes + one feature refinement +\n  four additional bug fixes from a first-principles code audit:\n  1. **Board anti-shake Y-axis direction reversal fix** (StabilizationHelper.java,\n     CRITICAL BUG) — all 4 screen-orientation cases (ROTATION_0/90/180/270)\n     in `dispatchTransform()` had the WRONG sign on `boardPxY`. OIS requires\n     that when the device moves UP, the board moves DOWN (CSS +Y) to\n     compensate; the original code moved the board UP — amplifying the shake.\n     User report: \"屏幕突然向上移动，棋盘不但不向下位移来抵消，反而也向上\".\n     X-axis signs were already correct.\n  2. **Board rotation sensitivity adjustment** (StabilizationHelper.java,\n     FEATURE COMPLETION) — `ROTATION_DEADZONE_DEG` 0.3° → 1.0° (filter\n     sensor noise + sub-degree hand tremor); `MAX_ROTATION_DEG` 45° → 30°\n     (covers normal handheld tilt, avoids visual abruptness). 1:1 ratio\n     (|board rotation| = |screen roll delta|) strictly preserved within\n     1°–30° range. User report: \"棋盘旋转角度过于敏感，屏幕小幅度转动，\n     棋盘就大幅度转动\".\n  3. **Review-board yellow-square logic fix** (ui.js, CRITICAL BUG) —\n     yellow-square selection changed from `total\u003e=2` to `pAtk\u003e0 \u0026\u0026 aAtk\u003e0`\n     (both sides must have attackers). Previously, single-side-dominated\n     squares (e.g. 5 white + 0 black = total 5) were misclassified as\n     yellow — they should be BLUE. User spec: \"黄色格=双方总控制高格(当前\n     未正确实现)\".\n  4. **Arrow stroke-linecap changed to butt** (ui.js, VISUAL REFINEMENT) —\n     changed from \"round\" to \"butt\" in review-board SVG arrows. The round\n     linecap created a small semi-circle at the arrow's start that read as\n     an \"extra dot\" — contradicting user spec \"箭头末端没有多余的圆点\".\n  5. **Engine extraction progress percentage fix** (StockfishNative.java,\n     BUG FIX) — `total / 114115752L * 13` truncated to 0 for total \u003c 114 MB\n     (Java left-to-right evaluation). Fixed to `total * 13L / 114115752L`.\n  6. **stats.html unclosed-brace infinite-loop fix** (stats.html, CRITICAL\n     BUG) — `while(moveText.includes('{'))...` would ANR on malformed PGN\n     with unclosed `{`. Capped to 10 iterations.\n  7. **StatsActivity stream leak fix** (StatsActivity.java, BUG FIX) —\n     `loadAssetAsBase64()` switched to try-with-resources to guarantee\n     InputStream closure on exception paths.\n\n- **Round-6 Revision 60** — Three changes per user spec:\n  1. **Arrow origin dot removed** (ui.js) — removed the small filled circle\n     at each arrow's origin in the review-board SVG. The arrowhead suffices.\n  2. **Arrow offset calibrated** (ui.js) — reduced per-color diagonal offset\n     from 14%/6px-max to 8%/3px-max to prevent arrows from exceeding cell\n     boundaries on small screens.\n  3. **Rotation limit expanded** (StabilizationHelper.java) — increased\n     from ±2° to ±45°. The board can now counter-rotate\n     up to 45° to compensate for large device tilt.\n     (Note: Rev61 later reduced this back to ±30° — see Rev61 entry above.)\n\n**v1.0.4** (versionCode 104) — previous major release\n\nThe v1.0.4 release adds:\n\n1. **Chess960 (Fischer Random Chess) mode** — full implementation with SP-ID selector,\n   Shredder-FEN castling rights, 960 castling rules, `UCI_Chess960` engine option,\n   and `[Variant \"Chess960\"]` PGN tag.\n2. **Standardized PGN import/export** — Seven-Tag Roster always emitted, `[%eval]`\n   / `[%clk]` annotations embedded, Result terminator enforced, tolerant parser\n   auto-corrects malformed input.\n3. **Heatmap-based control statistics** — per-square average control across all\n   positions, strongest/weakest square detection, center-control trend.\n4. **Version sync** — versionCode, versionName, AndroidManifest, build.gradle,\n   every in-app UI display, Java file VERSION constants, README, NOTICE, every\n   README.license file, and HTML manuals all synchronized to v1.0.4.\n\n### Patch revisions (version number unchanged — still v1.0.4 / versionCode 104)\n\nMultiple patch revisions have been applied under the same v1.0.4 version number.\nThe most significant recent revisions are:\n\n- **Round-5 Revision 17** — Xiaomi HyperOS 3 cache-clearing fix: added a\n  Java-side persistent storage layer (`AndroidBridge.persistentGet/Set/Remove`)\n  backed by `SharedPreferences`, dual-written alongside WebView localStorage so\n  the review-eval cache, language preference, and crash-recovery state survive\n  HyperOS 3's aggressive cache wipes. Also added `AndroidManifest.xml` backup\n  rules (`backup_rules.xml` + `data_extraction_rules.xml`) declaring\n  SharedPreferences and WebView Local Storage as user data (not cache).\n- **Round-5 Revision 18** — 📚 PGN Cache Manager: new modal dialog\n  accessible from the review toolbar (📚 button next to 🌗/🌈). Lets users\n  save the current PGN to a named entry, list all saved entries (with size +\n  mtime), import any entry with one tap (syncs both review and main move\n  list), and select multiple entries for batch deletion. Backed by\n  `AndroidBridge.listPGNCaches/savePGNCache/getPGNCache/deletePGNCaches`\n  writing to `/data/data/com.Regalia/files/pgn_cache/` (HyperOS-proof).\n  Also removed the 200-entry cap on `_reviewEvalCache` — now unlimited.\n- **Round-5 Revision 19** — onclick syntax fix + click-empty-moves-to-review:\n  fixed HTML attribute-escaping bug that broke the cache manager's click\n  handlers. Also made the \"No moves yet\" text in the main move list\n  clickable to enter review mode (useful for users who just installed the\n  app and want to import a PGN before playing any move).\n- **Round-5 Revision 20** (current) — HyperOS 3 eval-cache root-cause fix +\n  PGN cache rename/tags + review toolbar reorder:\n  • **Eval cache**: first-principles analysis revealed that\n    SharedPreferences.apply() is async — HyperOS 3 SIGKILLs the app before\n    the disk flush, losing data. Fix: dedicated file\n    (`/data/data/com.Regalia/files/eval_cache.json`) with atomic write\n    (tmp + fsync + rename) + synchronous `saveEvalCacheSync()` Java\n    interface. Critical-event flush on onPause/onStop/onUserLeaveHint +\n    pagehide/visibilitychange/beforeunload. Debounce reduced 500ms→150ms.\n    Eval cache now loads synchronously at JS module construction — **instant\n    recovery** of all previously-analyzed evals when entering review mode.\n  • **PGN Cache Manager**: new ✏️ Rename and 🔖 Add Tags buttons per entry.\n    Tags stored in `\u003cname\u003e.tags.json`, displayed as gold capsule chips.\n    Max 10 tags per entry, each ≤30 chars, case-insensitive dedup.\n    listPGNCaches() returns all tags in one call.\n  • **Review toolbar button order**: 🌗/🌈 moved to right of 💬Vars toggle,\n    left of 📝 PGN — groups display buttons (Vars, Heatmap) before action\n    buttons (PGN, Save, Cache, FEN).\n  • New Java interfaces: persistentSetSync, persistentFlush,\n    loadEvalCacheSync, saveEvalCacheSync, renamePGNCache, setPGNCacheTags,\n    getPGNCacheTags.\n  • backup_rules.xml + data_extraction_rules.xml: added eval_cache.json\n    and pgn_cache/ as user data for cloud backup / device migration.\n  • Dead code cleanup: removed unused `_pgnCacheToast` variable.\n- **Round-5 Revision 21** — Eval cache writes now synchronous +\n  PGN cache tag filter/search:\n  • **Eval cache**: `_reviewEvalCache.set()` / `.delete()` / `.clear()` now\n    trigger **immediate synchronous write** to `eval_cache.json` (no\n    debounce). Each eval arrives ~1/sec via `onEngineEval`; sync write\n    overhead is ~1-5ms — imperceptible. Completely eliminates the 150ms\n    debounce window where new eval data could be lost if HyperOS 3\n    SIGKILLed the app mid-debounce.\n  • **PGN Cache Manager**: new 🔍 text search box + tag-chip quick-filter\n    row at the top of the modal. Search matches entry names AND tags\n    (case-insensitive substring). Tag chips collect all unique tags across\n    all entries; click a chip to filter by that tag. Tag chips below each\n    entry name are also clickable. Smart Select All/None operates only on\n    visible (filtered) entries. Filter status bar shows match count.\n    Auto-clears filter on close/reopen.\n  • New JS functions: `_pgnCacheOnSearchInput`, `_pgnCacheApplyFilter`,\n    `_pgnCacheClearFilter`, `_pgnCacheFilterByTag`,\n    `_pgnCacheEntryMatchesFilter`, `_pgnCacheCollectAllTags`.\n  • New i18n keys: `pgn_cache_search_placeholder`, `pgn_cache_search_apply`,\n    `pgn_cache_search_clear`, `pgn_cache_filter_all`, `pgn_cache_filter_by_tag`,\n    `pgn_cache_filter_status`, `pgn_cache_filter_no_match` (all zh/en).\n- **Round-5 Revision 22** — New Game dialog layout + HyperOS 3\n  defense strengthened + PGN cache search UX:\n  • **New Game dialog**: Chess960 toggle moved to be directly below the\n    \"Play Color\" section (was below AI Book Moves). Removed the redundant\n    \"Classic Openings (Optional)\" residual title from the Chess960-OFF\n    branch. The Chess960-ON dedicated-settings section heading is now\n    `T('chess960_label')` (was `T('classic_openings')`).\n  • **HyperOS 3 eval-cache defense**: `MainActivity.onDestroy()` now also\n    calls `_flushReviewEvalCache()` + `persistentFlush()` before destroying\n    the WebView. Previously onDestroy skipped the flush — any eval data\n    that arrived between onStop and onDestroy could be lost. The four-layer\n    defense is now: (1) dedicated file `eval_cache.json` with atomic write;\n    (2) synchronous `saveEvalCacheSync()` using `FileDescriptor.sync()`;\n    (3) critical-event flush on Java onPause/onStop/onUserLeaveHint/\n    onDestroy AND JS pagehide/visibilitychange/beforeunload; (4) Rev21's\n    per-set synchronous write (no debounce).\n  • **PGN Cache Manager search**: removed the redundant 🔍 apply button\n    (soft-keyboard Enter now triggers search directly via\n    `enterkeyhint=\"search\"`). Input type changed to `search` for native\n    affordances. The ✏️/🔖/📥 buttons per entry now share the same row\n    (was a vertical column).\n  • **HTML manuals**: removed the obsolete \"Stockfish engine binary\"\n    paragraph from the v1.0.3 changelog in both zh and en manuals.\n- **Round-5 Revision 23** — First-principles code review fixes:\n  • **Critical bug fix**: Engine restart (`onEngineRestarting()` /\n    `restartCurrentEngine()`) no longer calls `_reviewEvalCache.clear()`,\n    which previously destroyed the ENTIRE persisted eval cache (all games'\n    data). Now only resets `_reviewEvalRequestedStep=-1` — Stockfish is\n    deterministic, so other games' evals remain valid.\n  • **Performance**: Eval cache writes restored to 150ms debounce (Rev21's\n    per-set synchronous write serialized the entire ~12MB Map on every eval\n    callback). `_flushSync()` from lifecycle handlers still forces immediate\n    write, so the debounce window is covered by Rev22's onDestroy flush.\n  • **Bug fix**: `saveEvalCacheSync()` now uses `Files.move()` with\n    `ATOMIC_MOVE` + `REPLACE_EXISTING` on API 26+ — the old delete+rename\n    sequence had a brief window where neither file existed.\n  • **Redundancy**: Removed dead second cache check in `requestEngineEval()`,\n    dead `moveNum` variable in `_buildPGNString()`, stale 🔍 comment.\n- **Round-5 Revision 24** (current) — Analyze-all root-cause fix + instant\n  PGN-comment eval recovery + player rename + PGN cache completeness:\n  • **Analyze-all interruption root-cause fix**: First-principles analysis\n    revealed FIVE distinct causes of \"分析全部 interrupted at a certain\n    point\":\n    (1) The old `startStep=0` fallback when all steps were cached caused\n    `requestEngineEval()` to early-return on cache hit WITHOUT calling\n    `_reviewAnalyzeAdvance()`, stalling the batch until the safety timer\n    fired. Now `reviewAnalyzeAll()` detects the all-cached case up front\n    and short-circuits to completion INSTANTLY (no engine calls, no timer).\n    (2) The safety timer was set ONCE with `min(N*8s, 300s)` — capped at 5\n    minutes. For long games with depth-22 evals (5-15s/step), the 5min cap\n    fired mid-analysis. Now the per-step safety timer RESETS on every\n    advance (60s/step, no global cap), and a stuck step is SKIPPED (not\n    aborting the whole batch).\n    (3) If the engine auto-recovered (`onEngineReady` after Java\n    `recoverEngine()`), the batch was silently dropped. Now `onEngineReady`\n    resumes the batch if `_reviewAnalyzeAllActive` is still true.\n    (4) If `requestEngineEval()` hit cache during batch (rare race or\n    re-entry), it returned without advancing. Now it detects\n    `_reviewAnalyzeAllActive + cache hit` and calls `_reviewAnalyzeAdvance`.\n    (5) `onEngineError` no longer clears `_reviewAnalyzeAllActive` — the\n    per-step safety timer covers the recovery window, and the batch resumes\n    on `onEngineReady`.\n  • **Instant cache restoration from PGN comments**: `tablebase.js _parsePGN()`\n    now extracts `[%eval ...]` tags from `{}` comments BEFORE stripping them,\n    and `importPGN()` populates `_reviewEvalCache` from the extracted evals.\n    When the user enters review mode after import, all positions with\n    `[%eval ...]` annotations are shown INSTANTLY with their cached eval —\n    the engine is only invoked for positions WITHOUT `[%eval ...]`. This\n    implements the \"instant recovery from cache (including PGN comments)\"\n    requirement. Supports all Lichess eval formats: `[%eval 0.35]`,\n    `[%eval -1.5]`, `[%eval #5]`, `[%eval #-3]`, `[%eval M5]`.\n  • **PGN cache archive completeness**: `_pgnCacheSaveCurrent()` now ALWAYS\n    uses `_buildPGNString(true)` (forceIncludeVariations=true). The previous\n    behavior preferred `_cachedOriginalPGN` (the imported text), which lost\n    NEW moves played after import. `_buildPGNString()` now includes: all\n    moveRecords (including post-import moves), variations (forced on\n    independent of showVariations UI toggle), `[%eval ...]` from the review\n    cache, `[%emt]/[%clk]/[%csl]/[%cal]`, Seven-Tag Roster + supplementary\n    tags. The saved PGN archive contains the COMPLETE game record.\n  • **Player rename feature**: Clicking the \"你\"/\"You\" name on the main\n    interface player bar opens a rename prompt. The new name is:\n    - Persisted via `AndroidBridge.persistentSet('Regalia_humanName', ...)`\n      (HyperOS 3-proof)\n    - Reflected in all PGN text (`[White \"...\"]` / `[Black \"...\"]`)\n    - Reflected in PGN cache archives and clipboard copies\n    - Loaded synchronously at JS module init (available before first render)\n    Entering an empty string or the default name resets to \"你\"/\"You\".\n    PGN import with explicit `[White \"...\"]`/`[Black \"...\"]` headers also\n    populates `_humanPlayerName` if the human player's slot has a non-default\n    name.\n  • **Redundant 📥 button removed**: The 📥 Import button in the PGN Cache\n    Manager was redundant — clicking the entry's name already imports it.\n    Removed to save horizontal space; ✏️ Rename and 🔖 Tags buttons remain.\n  • **New i18n keys**: `rename_player_hint`, `rename_player_prompt`,\n    `rename_player_saved`, `rename_player_reset` (all zh/en).\n  • **New globals**: `_humanPlayerName`, `playerWhite`, `playerBlack`.\n  • **Stockfish 18 arm64-v8a-dotprod**: Engine binary updated to the\n    official sf_18 release (NDK r27c build, 114 MB, NEON+dotprod\n    acceleration).\n- **Round-5 Revision 25** (current) — Stats page promotion symbols + Chess960\n  dialog heading cleanup + first-principles review:\n  • **Stats page promotion symbol fix (bug fix)**: In `stats.html\n    renderPGNText()`, the `renderSAN()` function previously only prepended\n    the pawn symbol for pawn moves (e.g., \"e8=Q\" → \"♙e8=Q\") but left the\n    \"=Q\"/\"=R\"/\"=B\"/\"=N\" promotion suffix as a literal letter — inconsistent\n    with non-pawn moves where the piece letter IS converted to a symbol.\n    Now every promotion piece (Q/R/B/N after '=') is converted to its\n    side-colored symbol (♕/♛, ♖/♜, ♗/♝, ♘/♞), regardless of whether the\n    move is a pawn move or a piece move. Examples: \"e8=Q+\" → \"♙e8=♕+\"\n    (White) / \"♟e8=♛+\" (Black); \"exd8=R\" → \"♙exd8=♖\" (White) / \"♟exd8=♜\"\n    (Black). A shared `_replacePromo()` helper handles both the pawn-move\n    and piece-move branches uniformly, including the rare theoretical case\n    of a piece-letter move with a promotion suffix (not legal in standard\n    chess, but defensive).\n  • **Chess960 dialog heading removed (redundancy cleanup)**: In the New\n    Game dialog, when Chess960 is enabled, the dedicated settings section\n    (SP-ID input, back-rank preview, note) previously had a redundant\n    `\u003ch3\u003e` heading showing \"菲舍尔任意制象棋\" / \"Fischer Random Chess\". The\n    Chess960 toggle above (with its own label) already makes it clear what\n    mode the user is in. Rev25 removes the heading so the settings appear\n    directly under the toggle, saving vertical space. The heading is\n    removed in both Chinese and English modes.\n- **Round-5 Revision 26** — Stats page promotion symbol root-cause\n  fix:\n  • **Root cause of \"e8=Q still shows letter\" found and fixed**: Rev25 added\n    a `_replacePromo()` helper to convert `=Q`/`=R`/`=B`/`=N` suffixes to\n    piece symbols, but the promotion STILL showed as a letter because the\n    underlying SAN regex in `stats.html renderPGNText()` never captured the\n    promotion suffix for non-capture pawn promotions. The regex's pawn\n    non-capture alternative was `[a-h][1-8][+#]?` (NO promotion group), so\n    for \"e8=Q\" it matched only \"e8\" — leaving \"=Q\" to be rendered as raw\n    literal characters via the \"pass through other characters\" fallback.\n    Rev26 fixes the regex alternative to `[a-h][1-8](?:=[QRBN])?[+#]?`,\n    which captures the full \"e8=Q\" so `renderSAN()` (and its `_replacePromo`\n    helper) can convert the promotion piece to a symbol. The same fix is\n    applied to the variation-move regex. Now ALL promotion formats render\n    correctly: `e8=Q` → ♙e8=♕, `exd8=R` → ♙exd8=♖, `a8=N#` → ♟a8=♞#,\n    `b1=B+` → ♙b1=♗+. Verified via regex test against 12 sample moves.\n- **Round-5 Revision 27** — Hyperlinks open in system browser +\n  Resign feature + Stats board display + multi-report P0/P1 fixes:\n  • **Clickable hyperlinks (About page + everywhere)**: The About page's\n    \"Source code: https://github.com/YDW99/Regalia\" is now a real `\u003ca\u003e`\n    hyperlink (previously plain text). ALL hyperlinks anywhere in the app\n    (About dialog's AGPL/GPL license links, stats.html links, etc.) are\n    intercepted by a new global click handler that calls\n    `AndroidBridge.openUrlInBrowser(url)` — a new `@JavascriptInterface`\n    method in StockfishNative.java and StatsActivity.java that launches\n    the system default browser via `Intent.ACTION_VIEW`. Defense-in-depth:\n    `ChessWebViewClient.shouldOverrideUrlLoading()` and the stats WebView's\n    WebViewClient also intercept http(s) URLs at the navigation layer, so\n    links triggered by any means (JS `window.open`, meta refresh, etc.)\n    are routed to the system browser. URLs are strictly validated to be\n    http(s) only — file:, content:, javascript:, intent: schemes are\n    silently rejected.\n  • **🏳️ Resign feature (DeepSeek review 2.1)**: A new 🏳️ Resign button\n    appears in the player bar next to the \"Your turn\" indicator. Tapping\n    it opens a confirmation dialog; confirming ends the game with the\n    opponent winning. PGN export follows the 元宝 PGN report convention:\n    `[Result \"0-1\"]` if White resigns / `[Result \"1-0\"]` if Black resigns,\n    `[Termination \"Resignation\"]` supplementary tag, and a\n    `{White resigns.}` / `{Black resigns.}` comment on the last move.\n    Stops the engine (`stop` command + isAIThinking=false) and the game\n    clock. The game-over overlay shows the correct resign text and\n    win/lose emoji.\n  • **Stats page board display (DeepSeek review)**: A small chess board\n    now appears ABOVE the PGN text panel in 📊统计, showing the position\n    after the currently-selected move (or initial position if nothing is\n    selected). The board uses the SAME color scheme as the main\n    chess.html board (SQ_LIGHT/SQ_DARK gradients, coordinate labels with\n    stroke, piece symbols with color/stroke/glow), with White at the\n    bottom and Black at the top. Width is responsive (max 360px, aspect\n    ratio 1:1).\n  • **Kimi stats.html P0 fix — Backspace global interceptor**: The\n    `keydown` listener that turns Backspace into \"return to main game\"\n    now checks whether the key event originated from an INPUT, TEXTAREA,\n    or contentEditable element. Previously, pressing Backspace while\n    editing the PGN paste field or any prompt input would (1) prevent\n    the character deletion AND (2) immediately bounce the user back to\n    the main game — making text editing impossible.\n  • **Kimi stats.html P0 fix — applySANMove king-safety validation**:\n    `applySANMove` now validates that the moving side's king is NOT in\n    check AFTER the candidate move, by calling a new `_isKingSafe()`\n    helper. Previously, `canMoveTo` only checked pseudo-legal moves, so\n    a pinned piece could \"move\" and leave its own king in check —\n    corrupting the replayed position and all downstream statistics.\n    Castling also now checks post-castle king safety. If a disambiguated\n    piece fails the check, the scanner continues to the next matching\n    piece (correct SAN disambiguation behavior).\n  • **Kimi stats.html P1 fix — game complexity threshold**: Changed\n    `validEvals.length\u003e=3` to `\u003e=2`. With 2 evals, `changes` has 1\n    element — `avgChange` and `variance` are both well-defined\n    (variance=0 for a single sample). The previous \u003e=3 check skipped\n    the entire complexity section for 2-eval games.\n  • **Kimi stats.html P1 fix — _escFEN escaping**: Added `\"` and `'`\n    escaping to `_escFEN()`, matching chess.html's `_esc()` behavior.\n    Closes a potential XSS vector where a malicious FEN imported via\n    PGN could break out of HTML attribute context.\n  • **Kimi stats.html P1 fix — exportFullHTML regex**: Replaced the\n    lazy `[\\s\\S]*?\\n}` regex (which could match a nested object\n    literal's closing brace instead of the function's) with a new\n    `_stripFnBody()` helper that does brace-depth counting to find the\n    function's true closing brace. Prevents orphaned code from leaking\n    into the exported HTML.\n  • **Stats.html hyperlink interceptor**: New global click handler in\n    stats.html routes `\u003ca href=\"http(s)://...\"\u003e` clicks to the new\n    `AndroidBridge.openUrlInBrowser()` bridge method.\n  • **formatEval resign fix**: The resign case in `formatEval()` now\n    uses `_resignWinnerColor` directly (set by `_resignGame()`) instead\n    of trying to detect \"White wins\" / \"Black wins\" in the gameOver\n    text (which only contains \"resigns\"). Fixes a bug where the player\n    always appeared to lose even when they won (e.g. AI resigns).\n- **Round-5 Revision 28** (current) — Stats↔main PGN sync + resign emoji\n  fix + stats board promotion fix:\n  • **Stats↔main PGN sync**: Every time the user enters the stats page\n    (📊 button), `openStatsPage()` now ALWAYS rebuilds the PGN from the\n    current `moveRecords` via `_buildPGNString(true)` (forceIncludeVariations=true).\n    Previously, it used `_cachedOriginalPGN` (the text from the last\n    importPGN/importPGNFile call), which became STALE when the user played\n    new moves after importing — the stats page would show the imported\n    PGN's moves, not the current game's moves. Now the stats page always\n    reflects the current main/review game state.\n  • **Stats→main PGN import-back prompt**: When the user imports a PGN on\n    the stats page (via 🗃️ Paste PGN or 📂 Select PGN File) and then\n    returns to the main activity (back button or 返回对局 button), a new\n    \"🗃️ Import PGN to game?\" dialog appears with Yes/No/Cancel:\n    - **Cancel**: Stay on the stats page (don't return to main).\n    - **No**: Return to main without importing (main keeps its current game).\n    - **Yes**: Return to main AND trigger the existing \"💾 Save PGN file?\"\n      prompt (to avoid losing the main/review's current PGN), then import\n      the stats-page PGN via `importPGN()`.\n    The dialog UI matches the app's existing dialog style (.dov/.dlg/.dlg-btns).\n    The Android back button is intercepted: if the import-back dialog is\n    visible, back = Cancel (dismiss); otherwise back = returnToGame()\n    (which may show the import-back dialog if a PGN was imported).\n  • **Resign game-over emoji fix**: The game-over overlay's emoji was\n    hardcoded to show 🤝 for all non-checkmate terminations. Now it shows\n    🏳️ when `_gameOverStatusKey==='resign'` (matching the resign button's\n    icon), and 🤝 only for genuine draws (stalemate, 50-move, repetition,\n    insufficient material, agreement).\n  • **Stats board promotion fix**: The stats page board display (added in\n    Rev27) showed a pawn on the back rank instead of the promoted piece\n    (queen/rook/bishop/knight) for promotion moves. Root cause: the\n    `moveRecords.push({...})` in `tablebase.js _parsePGN()` (the PGN import\n    path) was missing the `promotion` field, so `openStatsPage()` serialized\n    move records without promotion info, and the stats page's\n    `executeMove()` couldn't apply the promotion. Fixed by adding\n    `promotion: move.promotion||null` to the pushed move record.\n- **Round-5 Revision 29** — PGN import failure root-cause fix + stats page\n  en passant + comprehensive bug fixes:\n  • **CRITICAL: PGN import failure fix**: Rev28's `promotion:\n    move.promotion||null` referenced an undefined variable `move` (loop\n    variable is `parsedMove`). The ReferenceError was caught by the\n    surrounding try/catch and shown to the user as \"Invalid PGN format\" —\n    EVERY PGN import failed. Fixed to `(parsedMove\u0026\u0026parsedMove.move\u0026\u0026\n    parsedMove.move.promotion)||null`.\n  • **CRITICAL: Stats page en passant support**: The simplified SAN parser's\n    `canMoveTo()` and `executeMove()` did not handle en passant. Importing\n    a PGN with en passant (e.g. move 48 `gxf6` in the user's\n    Regalia_game 3Ω.pgn) caused parsing to fail at move 94 → side-to-move\n    desync → 25 subsequent moves all rejected → stats page showed only 98\n    moves. Added en passant branch to `canMoveTo()` and captured-pawn\n    removal to `executeMove()`. Stats page now parses all 123 moves.\n  • **Stats page checkmate symbol fix**: `buildSAN()` previously never\n    emitted `#` (checkmate) — all checks shown as `+`. Added full\n    checkmate detection via in-place simulation (mutate→check king\n    safety→revert) to avoid the recursive executeMove→buildSAN→\n    executeMove loop that caused stack overflow with the naive approach.\n  • **NAG Black-side fix**: `ai-bridge.js _buildPGNString()` and\n    `stats.html classifyMove()` used raw White-POV delta for mate\n    thresholds. A BLACK mating move was misclassified as $4 (blunder)\n    instead of $3 (brilliant). Fixed to use side-relative moverDelta.\n  • **Stats page Black-to-move-start caption fix**: `_renderStatsBoard()`\n    caption used `lastIdx%2===0` for White/Black, ignoring\n    `parsed.isBlackToMoveStart`. For black-to-move-start games, the color\n    was inverted. Fixed to respect `parsed.isBlackToMoveStart`.\n  • **PGN parsing tolerance improvement**: Unclosed `{...}` comments\n    previously survived the multi-iteration regex removal, and the\n    residual content was tokenized into bogus move tokens generating\n    meaningless NULL placeholders. Now follows PGN spec's tolerant\n    behavior: an unclosed `{` consumes the rest of the movetext.\n  • **Variation prefix number falsy-0 fix**: `v.prefixEllipsisNum||vmn`\n    in `_buildPGNString()` incorrectly fell back to vmn when\n    prefixEllipsisNum===0 (legitimate rare value). Fixed to explicit\n    null check.\n  • **stats.html PGN parse error i18n**: Was hardcoded English \"PGN parse\n    error\". Added i18n key `pgn_parse_error` (zh: \"PGN 解析失败\").\n  • **PGN import high-tolerance**: Invalid moves are skipped with NULL\n    placeholders (shown as dimmed \"—\") and side-to-move is auto-advanced\n    to avoid cascade failure. Nested variations supported via recursive\n    descent parser. App self-exported PGN re-imports perfectly (123\n    moves → export → re-import → 123 moves, lossless).\n- **Round-5 Revision 30** (current) — Robustness hardening + scroll\n  restoration bug fix:\n  • **ROBUSTNESS: Scroll position restoration fix**: The .mlist move list\n    repeatedly jumped back to top during re-renders. Root cause: with\n    `scroll-behavior:smooth` on .mlist, programmatic `scrollTop`\n    assignment during restore fired intermediate 'scroll' events that\n    overwrote `_mlistScrollState.scrollTop`, causing the NEXT render to\n    restore to a stale intermediate position. Fixed with a\n    `_scrollRestoreGuard` flag that suppresses `_onMlistScroll` during\n    programmatic restore, plus temporarily switching `scroll-behavior`\n    to 'auto' for instant restoration. The save phase now also reads\n    `scrollTop` DIRECTLY from the live DOM (not just from the event\n    handler) for the most reliable snapshot. Same pattern applied to\n    `.review-body` in review mode. `_scrollRestoreGuard` is reset on\n    new game.\n  • **ROBUSTNESS: ChessWebViewClient URL overload**: Now also overrides\n    the newer `WebResourceRequest`-based `shouldOverrideUrlLoading`\n    overload (API 24+). The deprecated String-based overload only fires\n    on API 21-23; on API 24+ only the `WebResourceRequest` overload is\n    invoked. Without this override, http(s) links clicked on API 24+\n    devices would NOT be redirected to the system browser. The new\n    overload delegates to the String overload for consistent behavior.\n  • **ROBUSTNESS: chess960.js randomSPID() modulo bias elimination**:\n    Previously `buf[0]%960` had a slight bias (65536 isn't a multiple of\n    960; values 0..255 were ~0.03% more likely than 256..959). Now uses\n    rejection sampling: only accept values \u003c 65280 (the largest multiple\n    of 960 ≤ 65536) before applying %960. Bounded retry count (8)\n    prevents infinite loops; fallback to %960 if all retries exceed\n    (extremely unlikely — P ≈ 0.4%).\n\nSee `NOTICE` (VERSION HISTORY SUMMARY → v1.0.4) and the per-module\n`README.license` files for full details.\n- **Round-5 Revision 31** (current) — Stats board piece sizing + review\n  orientation scroll fix + UCI timed-ponder compliance + dead code cleanup:\n  • **Stats board piece sizing fix**: `stats.html _renderStatsBoard()` piece\n    font-size was `5.2vw` (viewport-relative). In landscape, the viewport is\n    wide (e.g. 800px+) but the board container is capped at 360px, so pieces\n    overflowed their squares. Fixed: piece font-size now uses a CSS variable\n    `--piece-size` set via JS to `(boardWidth / 8 * 0.72)`. This scales\n    correctly with the ACTUAL board container in both portrait and landscape,\n    on all Android WebView versions (no container query support needed).\n    Added `_updateStatsBoardPieceSize()` called after render and on\n    resize/orientationchange.\n  • **Review mode orientation-change scroll bug fix**: After orientation\n    change, the review move list jumped to a wrong scroll position because\n    the save phase read `scrollTop` from the OLD layout and the restore\n    phase applied that pixel offset to the NEW layout (where it points to a\n    different move). The review-moves-list scroll-into-view logic didn't\n    fire either (no `reviewStep` change). Fixed: (1) on orientationchange,\n    force `_lastReviewStepScrolled=-2` so the next render re-centers the\n    active move in the NEW layout; (2) invalidate `_mlistScrollState.valid`\n    so the main move list re-snapshots from the new DOM; (3) set\n    `_skipReviewBodyScrollRestore` flag so the review-body doesn't restore\n    the stale scrollTop (the active-move scroll-into-view handles\n    re-centering instead).\n  • **UCI COMPLIANCE: Timed-game ponder fix**: Per UCI spec, `go ponder`\n    MUST include time params (wtime/btime/winc/binc) so the engine can\n    switch to normal time management when `ponderhit` fires. Previously\n    `startPonder()` sent only \"go ponder\" with no time params — breaking\n    timed-game ponder (engine searched indefinitely or panicked after\n    ponderhit). Fixed: `startPonder()` now accepts 4 time params; JS side\n    passes current `gameClocks` values. For untimed games, all zeros are\n    passed and Java side sends plain \"go ponder\". Added `@deprecated`\n    legacy single-arg `startPonder()` overload for backward compat.\n  • **Dead code cleanup**: Removed unused `_unclosed` flag in\n    `tablebase.js _parsePGN` (was set but never read). Removed dead\n    `oppKing` variable in `stats.html buildSAN()` (was always null, never\n    read — leftover from abandoned check-detection). Removed dead\n    `ch==='\\s'` comparison in `pgn-standard.js` (a single char can never\n    equal the 2-char string \"\\s\" — typo for regex \\s class). Fixed\n    `composePGN()` to avoid leading blank lines when tagPairs is empty.\n- **Round-5 Revision 32** (current) — ECO book move engine-unresponsive\n  bug fix + UCI eval optimization:\n  • **CRITICAL: ECO book move causing engine unresponsive**: When \"AI\n    prefers ECO opening book moves\" is enabled, after 3 consecutive\n    book-served AI moves, the engine appeared \"unresponsive\" — no AI\n    move was made. Root cause: `doAIMove()` increments `_aiRetryCount`\n    on every call (expecting it to be reset by `onBestMove`). When the\n    ECO book provides a move, the engine is NOT called, so `onBestMove`\n    never fires, and `_aiRetryCount` accumulates. After 3 book moves,\n    `_aiRetryCount\u003e=3` → `doAIMove()` falsely concludes \"AI move failed\n    after 3 consecutive timeouts\" → shows `ai_timeout` toast and\n    RETURNS WITHOUT CALLING THE ENGINE. Fix: reset `_aiRetryCount=0`\n    on a successful book move (and tablebase move) — the \"request\" was\n    satisfied, just by the book instead of the engine.\n  • **UCI EVAL OPTIMIZATION (per SF18 best-practices doc)**:\n    `engineEval()` and `engineEvalDeep()` now set `Contempt=0` (objective\n    eval; default 24 biases scores by avoiding draws, distorting\n    analysis), `MultiPV=1` (max search depth; MultiPV\u003e1 reduces depth),\n    `UCI_ShowWDL=true` (Win/Draw/Loss probability output) BEFORE the\n    eval search. After the eval, `handleBestMove()`'s STATE_EVAL case\n    calls `restoreGameplayOptions()` to restore `Contempt=24` (aggressive\n    gameplay) and the user's MultiPV setting. New methods:\n    `applyEvalModeOptions()`, `restoreGameplayOptions()`.\n- **Round-5 Revision 33** (current) — Seldepth (SD) display throughout:\n  • **Seldepth parsing \u0026 display**: Per SF18 eval best-practices doc, added\n    seldepth (selective search depth / tactical depth) parsing and display.\n    Seldepth reflects the actual max depth reached in tactical variations\n    (usually \u003e= depth). Displayed as \"SD\u003cN\u003e\" right after \"D\u003cN\u003e\" to match\n    the existing abbreviated depth style (e.g., \"D15 SD22\").\n  • **Main UI eval bar + AI opponent bar**: Both now show \"D15 SD22\" during\n    engine search. SD only shown when \u003e 0 AND \u003e depth (seldepth == depth\n    is redundant).\n  • **Review mode eval bar**: Now shows real-time depth (D), seldepth (SD),\n    nodes, and nps during analysis — previously only showed D with no\n    nodes/nps. This gives the user full visibility into the engine's\n    search progress while reviewing a position.\n  • **PGN eval annotation**: formatEvalTag() now includes \"SD\u003cN\u003e\" in the\n    `{SF18 D15 SD22 +0.5 50%W/30%D/20%L}` annotation when seldepth \u003e depth.\n  • **Implementation**: StockfishNative.java new SELDEPTH_PATTERN +\n    processInfoLine() parsing + 9th/6th/7th params to onEngineProgress/\n    onPonderProgress/onEngineEval. ai-bridge.js new _sfSeldepth global +\n    cache storage. ui.js updated all 3 eval bar render paths.\n- **Round-5 Revision 34** (current) — Chess960 castling own-piece capture\n  fix + heatmap coordinate labels:\n  • **CRITICAL: Chess960 castling could capture own pieces**:\n    `isChess960CastlingLegal()` only checked squares BETWEEN the king and\n    rook, not the king's/rook's destinations. In Chess960, the king and\n    rook can start close together (e.g., king b1, rook c1), so the king's\n    path extends far beyond the rook through occupied squares. `makeMv()`\n    would overwrite those squares, destroying own pieces. Fix: now checks\n    the UNION of the king's path and rook's path — every square must be\n    empty except the king's and rook's own starting positions.\n  • **stats.html Chess960 castling**: `applySANMove()` no longer hardcodes\n    king from col 4 (e1) — scans the back rank for the king's actual\n    column. `executeMove()` castling rook move is now Chess960-aware,\n    finding the nearest rook on the castling side instead of always\n    using col 7/0.\n  • **stats.html heatmap coordinate labels**: Each heatmap cell now\n    displays its coordinate (e.g., \"a8\", \"e4\") at the top, centered.\n- **Round-5 Revision 35** (current) — Timed-game engine stop fix + AI bar\n  seldepth label localization:\n  • **CRITICAL: Timed-game engine search exceeded clock time**: Two root\n    causes fixed:\n    (1) Clock expiry didn't send \"stop\" to the engine. The engine kept\n        searching past the GUI's 0-second mark. Fix: `_onGameClockExpired()`\n        now calls `AndroidBridge.engineStop()` for an immediate hard-stop.\n    (2) `wtime`/`btime` sent to the engine were stale (measured at JS call\n        time, but \"go\" is sent up to 6s later after stopAndWait + ucinewgame\n        + readyok). The engine over-allocated search time. Fix: `engineGoTimed()`\n        now deducts the setup overhead + a safety margin from the clock\n        values before sending \"go\".\n  • **New @JavascriptInterface methods**: `engineStop()` (hard-stop any\n    engine state, discards bestmove), `sendToEngine(String)` (raw UCI\n    command fallback).\n  • **AI opponent bar SD label localization**: The AI opponent bar now\n    shows \"选深\" (Chinese) / \"SelDepth\" (English) instead of \"SD\". The\n    eval bar keeps \"SD\" for compactness. New i18n key: `seldepth_label`.\n- **Round-5 Revision 36** (current) — AI bar layout restructure + seldepth\n  colon + critical bug fixes:\n  • **AI opponent bar layout restructure**: Engine search real-time info\n    (depth/seldepth/nodes/nps) moved to line 2 right-aligned. Ponder info\n    moved to line 3 right-aligned. Line 1 shows only compact status.\n  • **Seldepth label colon**: Added ':' between seldepth label and value\n    (选深:22 / SelDepth:22) for format consistency.\n  • **CRITICAL: engineStop() idle-state fix**: No longer sets\n    _discardingPonderBestmove when engine is idle — prevents the next\n    game's first bestmove from being silently discarded.\n  • **CRITICAL: onPonderProgress score perspective fix**: was inverted\n    (playerColor==='black' → should be 'white'). Ponder scores now display\n    with correct sign.\n  • **stats.html Chess960 castling detection**: Fixed to use destination\n    column (6/2) instead of distance (===2) for Chess960 compatibility.\n  • **Redundancy**: _updateAIThinkDisplay() no longer duplicates the\n    \"thinking\" placeholder on line 2.\n- **Round-5 Revision 37** (current) — AI bar captured pieces split at 7:\n  • **AI opponent bar captured pieces split**: When the AI's captured pieces\n    count exceeds 7, pieces 8+ are displayed on line 3 (left-aligned),\n    sharing the line with ponder info (right-aligned). Pieces 1-7 remain\n    on their original row. The player bar keeps the original single-row\n    wrap behavior (unchanged). `capturedPiecesHtml()` gains a `splitAt`\n    parameter (7 for AI bar, 0/omitted for player bar).\n- **Round-5 Revision 38** (current) — AI opponent bar overflow fix:\n  • **AI opponent bar overflow fix**: Some information was exceeding the AI\n    bar's display width. Fixed by adding `overflow:hidden` to the inner\n    column div, `max-width:100%` + `box-sizing:border-box` to\n    `#ai-search-info`, `min-width:0` + `overflow:hidden` to the line-3\n    container, `min-width:0` to `#ai-ponder-info`, and `max-width:100%` +\n    `overflow:hidden` to `.pbar` CSS. All text now properly truncates with\n    ellipsis when the bar is too narrow.\n- **Round-5 Revision 39** (current) — Ponder info not displayed when captured\n  pieces \u003e 7:\n  • **CRITICAL: Ponder info not displayed when captured pieces \u003e 7**: The\n    `#ai-cap-overflow` div had `width:100%` + `flex:0 0 auto`, causing it to\n    take ALL available width in the line-3 flex container, pushing\n    `#ai-ponder-info` off-screen. Fix: removed `width:100%`, changed to\n    `flex:0 1 auto` (shrink-to-fit) + `min-width:0`. Now the overflow pieces\n    only take the space they need, leaving room for ponder info on the right.\n- **Round-5 Revision 40** (current) — Overflow pieces horizontal fix +\n  ponder alignment fix:\n  • **Overflow pieces vertical wrapping fix**: `#ai-cap-overflow` used\n    `flex-wrap:wrap` causing pieces to stack vertically when container\n    narrowed. Changed to `flex-wrap:nowrap` + `overflow:hidden` — pieces\n    always stay horizontal. Reduced font-size to 0.85rem for overflow pieces.\n  • **Ponder alignment fix**: Line-3 container used\n    `justify-content:space-between` which put `#ai-ponder-info` at LEFT\n    when no overflow (single child). Changed to `margin-left:auto` on\n    `#ai-ponder-info` for always-right alignment.\n- **Round-5 Revision 41** (current) — Player bar captured pieces font-size\n  + stats board coordinate labels rework:\n  • **Player bar captured pieces font-size**: Reduced from 1.1rem to 0.85rem\n    to match AI opponent bar (both bars now visually consistent).\n  • **Stats page board coordinate labels rework**: File labels (a-h) moved\n    outside board on TOP, rank labels (1-8) moved outside board on LEFT,\n    per-cell coordinate labels (e.g. \"e4\") added inside each cell — all\n    matching main board style exactly (color, stroke, glow, paint-order).\n- **Round-5 Revision 42** (current) — AI bar overflow pieces not displayed:\n  • **CRITICAL: Overflow captured pieces not displayed**: `#ai-cap-overflow` had\n    `overflow:hidden` (clipped pieces) + `flex:0 1 auto` (shrank to 0 width).\n    Fix: removed `overflow:hidden`, changed to `flex:0 0 auto` (natural width,\n    no shrink). All overflow pieces now display correctly.\n- **Round-5 Revision 43** (current) — Exported stats HTML stuck at \"Loading...\" fix:\n  • **CRITICAL: Exported stats HTML stuck at \"Loading...\"**: The `_stripFnBody()`\n    function's brace-counting logic matched nested function closing braces\n    (at depth 0 + next char \\n/space), truncating the strip of `_exportFullHTML`\n    mid-function. This left orphaned code in the exported JS → syntax error →\n    page never executed → stuck at \"Loading...\". Fix: changed end condition to\n    depth===0 AND '}' at start of line (preceded by \\n) — standard JS convention\n    for top-level function closing braces. All 9 stripped functions now strip\n    correctly; full exported JS passes syntax validation.\n- **Round-5 Revision 44** (current) — Eval bar stuck fix + Worker memory leak\n  fix + _stripFnBody simplification:\n  • **Eval bar stuck mid-analysis**: Added JS-side eval safety timer (10s) that\n    resets `_evalLoading` if the engine doesn't respond. Prevents the eval bar\n    from getting stuck at \"分析中\" when `stopAndWaitForBestmove` blocks or the\n    engine fails to respond.\n  • **Worker memory leak fix (review report item 1)**: Added `terminateWorker()`\n    registered on `pagehide`/`beforeunload` — terminates the Web Worker and\n    revokes its Blob URL on page exit, preventing OOM from accumulated Worker\n    threads in Android WebView.\n  • **_stripFnBody simplification (review report item 18)**: Replaced fragile\n    brace-counting with simple regex `function name(){...}` → `function name(){}`\n    via non-greedy match to next `\\n}` at column 0. Verified for all 9 functions.\n- **Round-5 Revision 45** (current) — Engine timeout threshold extended:\n  • **AI safety timer**: Extended from 15s to 30s to accommodate timed-game\n    mode where the engine may need 20-25s for complex positions at high\n    difficulty levels.\n  • **Eval safety timer**: Extended from 10s to 15s to match (eval searches\n    at depth 15/22 can take 10-12s on complex positions).\n- **Round-5 Revision 46** (current) — Timeout emoji fix + engine timeout 360s:\n  • **Timeout emoji fix**: Game-over overlay and eval bar now show ⌛ for timeout\n    wins (was 🤝 — fell through to default draw emoji).\n  • **Engine timeout extended**: AI safety timer 30s→360s (6 min); eval safety\n    timer 15s→30s. Fully accommodates long timed games.\n- **Round-5 Revision 47** (current) — Timeout game-over text re-localization:\n  • **Timeout text not re-localizing**: The timeout game-over text was set\n    directly using `_lang` instead of `T()`, so switching language after\n    game-over didn't update the text. Fix: added `'timeout'` branch to\n    `_gameOverStrFromStatus()` with new `_timeoutWinnerColor` variable and\n    new i18n key `timeout_win_suffix`. Now re-localizes correctly.\n- **Round-5 Revision 48** (current) — PGN comment completeness + Blue/Yellow\n  arrows + review board visual annotations + stats conditional display:\n  • **PGN comment/visual-annotation completeness fix**: First-principles\n    review revealed that `_parsePGN` in `tablebase.js` STRIPPED all brace\n    comments during parsing — only `[%eval ...]` tags were extracted\n    beforehand. This meant `[%csl ...]` (square highlights), `[%cal ...]`\n    (arrows), AND free-text comments from imported PGNs were LOST. The user\n    requirement \"all valid () variations and {} comments must be fully\n    received\" was not met. Fix: added a pre-strip extraction pass that\n    walks the movetext character-by-character (tracking paren depth for\n    variation-internal comments), extracts `[%csl]`/`[%cal]` tags AND\n    free-text comment bodies, and attaches them to the next main-line\n    move. `importPGN()` now populates `_visualAnnotationsCache` and\n    `mr.comment` from the extracted data. `_buildPGNString()` now includes\n    `mr.comment` in the comment parts (with literal `{`/`}` escaped to\n    Unicode full-width braces to avoid premature comment termination).\n    PGN round-trip is now lossless for comments and visual annotations.\n  • **NEW: Blue and Yellow arrows in visual annotations**: Per user spec\n    (备忘1.md), added two new arrow colors:\n    - Blue arrow = one side's piece simultaneously threatens multiple\n      (\u003e1) enemy pieces (arrows from threatening piece to each\n      threatened piece)\n    - Yellow arrow = one side's threat to the other side's queen\n      (arrow from threatening piece to queen's square)\n    `_computeAndCacheVisualAnnotations()` in `ui.js` now computes these\n    from the post-move control map. Caps: top 3 attackers by threat\n    count (blue), top 3 attackers by piece value (yellow), max 4 blue\n    arrows per attacker.\n  • **Stats page visual annotations section enhancement**: The visual\n    annotations section in `stats.html` now displays all 4 arrow colors\n    (Blue, Red, Yellow, Green) in the SAME left-to-right order as the 4\n    square colors — so same colors align VERTICALLY (per user spec).\n    New i18n keys: `blue_arrows_threats`, `yellow_arrows_queen` (zh+en).\n    Updated `visual_annotations_desc` to mention all 4 arrow types.\n  • **Stats page conditional display**: Per user spec, the visual\n    annotations section is now ONLY shown if the PGN actually contains\n    `[%csl]` or `[%cal]` tags. Similarly, the heatmap control statistics\n    section heading is now inside the `_posCount\u003e0` check.\n  • **Review board visual annotations overlay**: Per user spec (备忘1.md),\n    when the control heatmap is OFF in review mode, the review board now\n    displays `[%csl]`/`[%cal]` annotations:\n    - Square highlights via CSS box-shadow inset (3px, matching last-move\n      hint width). Colors: B=#4a90d9, R=#e74c3c, Y=#f1c40f, G=#27ae60.\n    - Arrows drawn in an SVG overlay layer (same style as the main board's\n      `_updateArrows()`: 4px stroke, round linecap, 0.85 opacity,\n      triangular arrowhead marker per color).\n    The `.review-board` CSS rule now has `position:relative` so the SVG\n    overlay's `position:absolute` anchors correctly.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fydw99%2Fregalia","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fydw99%2Fregalia","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fydw99%2Fregalia/lists"}