Spaces:
Running
Running
|
Download README.md from chesstb/explorer: direct link, hf CLI and curl.
- Browser
- Download file 59.1 kB
-
https://huggingface.co/spaces/chesstb/explorer/resolve/main/README.md
- Command line
-
hf download hf://spaces/chesstb/explorer/README.md
-
curl -L -o README.md https://huggingface.co/spaces/chesstb/explorer/resolve/main/README.md
59.1 kB
| title: ChessTB Explorer | |
| emoji: ♟️ | |
| colorFrom: green | |
| colorTo: blue | |
| sdk: docker | |
| pinned: true | |
| app_port: 7860 | |
| license: gpl-3.0 | |
| short_description: "Interactive tablebase probing: WDL/DTZ/DTC/DTM/DTM50" | |
| # ChessTB Explorer ♟️ | |
| Interactive tablebase probing interface with WDL/DTZ/DTC/DTM/DTM50 metrics. | |
| This Hugging Face Space is deployed from | |
| [PunisherChess/ChessTB-Explorer-Render](https://github.com/PunisherChess/ChessTB-Explorer-Render) | |
| at commit `4969e401fa41ef1b86458cac4b6a31f9b11e08d0`. | |
| The original authors' license and third-party notices are retained. | |
| The application probes the public `chesstb/chesstb` bucket over HTTP byte | |
| ranges, using the explorer's existing bounded page cache. Sources are | |
| searched per table with raw generator output under `full/` preferred over | |
| shipping tables at the bucket root. Castling materials use `full/castling/` | |
| and `castling/`, respectively. | |
| Availability of each metric depends on the published tables for that material. | |
| `ADMIN_TOKEN` and `FLASK_SECRET_KEY` are configured as Space secrets. | |
| The probing library follows the maintained `add-chesstb-tablebases` branch | |
| of `noobpwnftw/python-chess`. | |
| [](https://chesstb-explorer-render.onrender.com) | |
| A single-page web application for exploring **ChessTB** chess endgame | |
| tablebases. Paste or build any position on an interactive board and | |
| instantly see every legal move ranked by **Distance to Zeroing (DTZ)**, | |
| **Distance to Conversion (DTC)**, **Distance to Mate (DTM)**, and **DTM | |
| under the 50-move rule (DTM50)** - straight from the tablebase, with no | |
| chess engine involved. | |
| The backend is a small Flask application that probes the tablebase through | |
| a modified fork of [`python-chess`](https://github.com/noobpwnftw/python-chess/tree/add-chesstb-tablebases) | |
| (the `chess.chesstb` module - see [Installation](#installation)). The frontend is | |
| a React app built around | |
| [Chessground](https://github.com/lichess-org/chessground) (lichess.org's | |
| board component) and [chessops](https://github.com/niklasf/chessops) for | |
| move generation/validation, supporting both Standard chess and Chess960 | |
| (Fischer Random). | |
| > This tool only *displays* tablebase results - it does not generate, | |
| > verify, or ship any tablebase data itself. See | |
| > [Getting the tablebase files](#getting-the-tablebase-files) below. | |
| > The probe endpoints are unauthenticated by design - evaluating a | |
| > position is this app's public functionality. The `/admin` cache | |
| > dashboard is gated behind `ADMIN_TOKEN`, a secret configured by the deployment | |
| > (see [Security notes](#security-notes)). | |
| --- | |
| ## Features | |
| - **Interactive board**: drag-and-drop moves, click-to-place pieces from | |
| the spare-piece trays, pawn promotion dialog, board flip (orientation | |
| persisted locally across visits), undo/redo. | |
| - **Chess960 (Fischer Random) support**: a **Chess960 Mode** switch in | |
| the settings panel changes castling interpretation without altering the | |
| current board position: dragging a king onto its own rook castles the | |
| Chess960 way, and X-FEN/Shredder-FEN rook-file castling-rights letters | |
| (e.g. `HAha`) are accepted in the FEN box. Carried through the shared | |
| URL (`&variant=chess960`), the saved in-progress game, and PGN | |
| import/export (`[Variant "Chess960"]`). Off by default. | |
| - **Lock**: a padlock button beside the FEN box that restricts board | |
| interaction to legal moves only: dragging, click-to-move, and the | |
| spare-piece trays are limited to the side to move's legal destinations, | |
| and off-board drops are disabled. Unlocked by default; a manual toggle | |
| is remembered locally across visits. Auto-play also engages Lock for | |
| the duration of a run (unless already locked by hand), and disables the | |
| padlock button until the run stops. | |
| - **Four ranked move tables side by side**: DTZ, DTC, DTM, and DTM50 | |
| columns, each independently sorted best-move-first. Exact metric ties | |
| prefer immediate checkmate, then use SAN order with promotions along | |
| the same path ordered queen, rook, bishop, knight. A better-scoring | |
| underpromotion still ranks first. Includes: | |
| - score text colour-coded by outcome (win / cursed win / draw / | |
| blessed loss / loss) | |
| - a **warning dot** on any move flagged cursed win or blessed loss, | |
| with a hover tooltip explaining the 50-move-rule nuance; a DTZ score | |
| that may still be a ply off after the moves are cross-checked shows a | |
| second tooltip line | |
| - an **info dot** on a draw by insufficient material, keeping the score | |
| text itself reading as a plain "Draw" | |
| - dot messages on touch or pen input: tapping a dot shows its tooltip | |
| text as a notice and does not play the move | |
| - an outcome summary (wins / draws / losses / unknown) across legal moves, | |
| respecting the halfmove clock and 50-move rule. It uses DTM50 outcomes, | |
| or DTC when DTM50 is unavailable. Without either, clock-independent | |
| draws, cursed wins and blessed losses count as draws; unresolved | |
| wins/losses count as unknown. | |
| - an optional pinned **Root Row** above rank 1, showing the current | |
| position's own score for each metric as a reference point - toggled | |
| from the settings panel (**Show Root Row**, off by default) and | |
| persisted locally | |
| - **Best-move arrows**: the top DTZ, DTC, DTM, and DTM50 moves are drawn | |
| as colour-coded arrows directly on the board. | |
| - **Auto-play**: automatically plays the best move for any of the four | |
| metrics on a timer. The per-move delay | |
| is set from the settings panel (**Autoplay Delay**, 0s–2.5s in 50ms | |
| steps, 1.25s by default). The countdown starts after the current | |
| position's tablebase results are displayed, giving you the full delay | |
| to read them before the next move. A metric's button is disabled when its | |
| table isn't present for the current material, or when the position is | |
| already a draw. | |
| - **PGN import/export**: paste a PGN to load its full move tree, including | |
| variations, at the starting position. Step through it or click any move | |
| in the PGN panel, or download the current game as a PGN file. | |
| - **CSV export** of the current move table, including the Root Row line | |
| ahead of rank 1 when that setting is on. | |
| - **Move-list / PGN panel** with full undo/redo and click-to-jump - jumping | |
| to an earlier point in the line re-uses the cached probe instead of | |
| re-querying the tablebase. | |
| - **Shareable positions**: the FEN (and, in Chess960 Mode, the active | |
| variant) is written to the URL hash on every move, so a link to the | |
| page reproduces the exact position and ruleset. | |
| - **Session persistence**: the in-progress game (not just the position) | |
| survives a trip to the admin dashboard and back. | |
| - **Board and piece set**: Libre Brown board, CBurnett piece set. | |
| - **Streaming probes**: `/probe/stream` reports progress via | |
| Server-Sent Events while child positions are being probed, so the UI | |
| shows a live progress bar instead of a blank pause on slower lookups. | |
| - **Hover pre-fetch**: hovering a move in the table warms the cache for | |
| the resulting position before selection. | |
| - **Admin cache dashboard** (`/admin`, behind `ADMIN_TOKEN`) - live | |
| hit-rate stats for both LRU caches, thread-pool configuration, and a | |
| one-click cache-clear button. | |
| - **Machine-readable API**: the full HTTP API is described by an OpenAPI | |
| 3.0 document served at `/openapi.yaml`. | |
| - **Keyboard shortcuts**: `Enter` Apply, `F` Flip, `C` Clear, `←`/`→` | |
| Back/Forward. The Board Settings panel lists them on devices with hover. | |
| --- | |
| ## Architecture at a glance | |
| ``` | |
| Browser (React app - see frontend/) | |
| ├─ engine/board.js - chessops-backed position/rules state, chessground | |
| │ wrapper (drag/drop, history, arrows), active variant | |
| ├─ engine/tablebase.js - talks to /probe/stream, exposes results as state | |
| ├─ engine/theme.js - board/piece-set theming | |
| ├─ hooks/useExplorer.js - FEN box, PGN, auto-play, settings, keyboard shortcuts | |
| └─ components/ - the rendered page (board, results table, PGN panel, dialogs) | |
| │ HTTP (JSON) / SSE (text/event-stream) - {fen, variant} | |
| ▼ | |
| Flask app (app.py) | |
| ├─ /probe, /probe/stream - evaluate a FEN under a variant, rank every legal move | |
| ├─ /admin, /admin/cache/* - cache dashboard + stats API | |
| └─ /openapi.yaml - API specification | |
| │ | |
| ▼ | |
| tablebase_router.py - searches TABLEBASE_PATH in order for every material, | |
| including castling positions and recursive dependencies | |
| │ | |
| ▼ | |
| chesstb.open_tablebase(...) per source (noobpwnftw's modified python-chess fork) | |
| │ | |
| ▼ | |
| ChessTB tablebase files - local disk, or a remote http(s) URL (probed | |
| in place over byte ranges by default, or downloaded and cached to local | |
| disk per material - see remote/remote_direct.py, remote/remote_fallback.py, | |
| and "Getting the tablebase files" below) | |
| ``` | |
| The React app is built with Vite into `static/dist/` (`app.js`, | |
| `app.css`), which the Flask template loads as same-origin assets, as the | |
| Content-Security-Policy (`script-src 'self'`) requires. See | |
| [Frontend build](#frontend-build) below. | |
| This covers the main explorer page's (`index.html`) module graph - see | |
| [Project structure](#project-structure) below for the complete file set, | |
| including `admin.js` (the `/admin` dashboard's client script, a plain | |
| server-rendered page with no build step). | |
| The Python modules and the frontend `engine/` modules carry a file-level | |
| docstring/header comment describing their responsibilities, and the | |
| move-ranking algorithm itself - including | |
| its treatment of the 50-move rule - is documented inline in `app.py` | |
| above `evaluate_all_moves()` and the `_effective_move_wdl`/ | |
| `_effective_distance`/`_order_rank` helpers it calls. | |
| --- | |
| ## Frontend build | |
| The explorer page is a React app (`frontend/`) built with Vite into | |
| `static/dist/`, which the Flask app serves alongside its own | |
| static assets. The build output is checked in, so running the app needs | |
| no frontend server or build step. Rebuild after changing anything under | |
| `frontend/src/`: | |
| ```bash | |
| cd frontend | |
| npm ci | |
| npm run build # writes static/dist/app.js, app.css, admin.css, fonts/ | |
| ``` | |
| `frontend/src/engine/` and `frontend/src/vendor/` hold the position/rules | |
| engine (chessops + Chessground) and the tablebase client, consumed by | |
| the React components in `frontend/src/components/`. Shared UI primitives | |
| (`Icon`, `Button`, `Segmented`, `Panel`) are in `frontend/src/components/ui/`. | |
| Piece SVGs and the piece-set stylesheet are in `static/` and are linked | |
| directly from the page template. | |
| ### Styling | |
| Stylesheets are in `frontend/src/styles/` and are bundled by Vite into | |
| `static/dist/app.css` (explorer page) and `static/dist/admin.css` (`/admin`). | |
| `tokens.css` defines the colour, type, spacing, radius, elevation, motion, | |
| and control-size custom properties; other files reference them instead of | |
| literal values. Files are imported into the `reset`, `vendor`, and `app` | |
| cascade layers by `index.css` and `admin.css`. | |
| | Token group | Custom properties | | |
| |---|---| | |
| | Type scale | `--text-xs` 12 px, `--text-sm` 13 px, `--text-md` 14 px, `--text-lg` 16 px, `--text-xl` 20 px | | |
| | Spacing | `--space-1` to `--space-6` (4 px to 32 px) | | |
| | Controls | `--control-h`, `--control-h-sm` (44 px on touch-primary devices) | | |
| | Board | `--board-light`, `--board-dark`, `--board-highlight`, read by `engine/theme.js` | | |
| Sizes are authored in `rem`, so the large-screen root font size scaling | |
| applies to all text and controls. | |
| ### Page layout | |
| | Viewport | Layout | | |
| |---|---| | |
| | Above 1180 px | Three-column CSS grid (board, results, PGN). The results and PGN columns take the board column's height and scroll internally. | | |
| | 768 to 1180 px | Board row above a results and PGN row; the page scrolls. | | |
| | Below 768 px | Three horizontally swipeable screens (board, results, PGN) with a bottom navigation bar (`ScreenNav`). Header and navigation heights include the device safe-area insets, and screen heights use `dvh`. The header shows the subtitle below the title, and the board is sized to the visible height so the whole board screen fits. | | |
| The side-to-move toggle and the board form one group (`.board-group`) as wide | |
| as the board, with a fixed small gap between them at every screen size. Free | |
| vertical space in the board column sits above the group. | |
| The results panel is a `results` size container. At 49 rem wide and above it | |
| shows the four metric columns; below that it shows one metric at a time, | |
| selected with the metric tabs, regardless of viewport width. The desktop | |
| layout reserves 50 rem for this panel before growing the board, so the | |
| four-metric view can appear as soon as the three columns fit. The results | |
| table is a fixed header table above a scrolling body table, so the scrollbar | |
| starts at the first moves row. The body reserves no scrollbar gutter: while | |
| its rows overflow, its table is widened by the scrollbar's measured width, so | |
| columns keep the header's position and width whether or not a scrollbar shows. | |
| The PGN move-number column width is set by `--pgn-rank-w` in `tokens.css`. | |
| ### Display scaling | |
| Sizing is CSS-only and is defined in `frontend/src/styles/layout.css`. | |
| - **Text:** above 1180 px, the root font size of the explorer page | |
| (`html.explorer`) is 100% of the browser default up to a 1536 x 780 window | |
| and rises to at most 125% (20 px), following the window's width and | |
| height. Browser zoom and the browser default font size apply. | |
| - **Board:** above 1180 px, `--board-size` fills the window height in | |
| multiples of 8 px, from 20.5 rem up to `--board-size-max` (720 px, defined | |
| in `tokens.css`), and leaves at least 40 rem for the results table. The | |
| left column is as wide as the board, so it is capped at 720 px as well. | |
| The width the board column does not use goes to the results column, so | |
| the total content width stays constant. | |
| - **Tablet layout:** at 1180 px and below, the root font size is the | |
| browser default and `--board-size` is `--board-size-cap`, defined in | |
| `tokens.css` as | |
| `min(720px, clamp(18.75rem, min(23.4375vw, 41.6667vh), 31.5rem))`. | |
| - **Phone layout:** below 768 px, `--board-size` is the smaller of | |
| `--board-size-cap` (`min(360px, 88vw)` up to 480 px wide) and the height | |
| that remains after the header, bottom navigation, and the controls | |
| above the board, with a floor of 14 rem. The board screen therefore | |
| fits the visible height regardless of browser toolbar size. Below the | |
| floor, the screen scrolls from its top. | |
| - `--board-fit-h` includes the fixed height above the board in the left | |
| column and requires an update when that height changes. This applies to | |
| both the desktop definition in `layout.css` and the phone definition in | |
| `responsive.css`. | |
| - `html.explorer` is set in `templates/index.html` and `frontend/index.html`. | |
| `/admin` does not use it. | |
| --- | |
| ## Prerequisites | |
| - **Python 3.10+** | |
| - **Git**, available on `PATH` - `pip install -r requirements.txt` fetches the | |
| `python-chess` fork directly from GitHub (`chess @ git+https://...`), | |
| which requires `git` to be installed even though the package itself is | |
| Python. | |
| - **A ChessTB tablebase directory or URL**: either a local directory on | |
| disk, or an `http(s)://` base URL serving the same layout remotely with | |
| no local download (see [Getting the tablebase files](#getting-the-tablebase-files)). | |
| The app starts without one, but every probe will fail until | |
| `TABLEBASE_PATH` points at the required sources. See | |
| [Configuration reference](#configuration-reference). | |
| - A modern browser (the frontend uses `fetch`, `ReadableStream`, and CSS | |
| container queries). | |
| --- | |
| ## Installation | |
| 1. **Create and activate a virtual environment:** | |
| ```bash | |
| python -m venv .venv | |
| source .venv/bin/activate # Windows: .venv\Scripts\activate | |
| ``` | |
| 2. **Install dependencies:** | |
| ```bash | |
| pip install -r requirements.txt | |
| ``` | |
| This pulls in the **modified `python-chess` fork** (not the plain | |
| PyPI `chess` package; see the comments in `requirements.txt` for why | |
| that distinction matters). | |
| 3. **Get the tablebase files**: see the next section. When using the | |
| remote (no-download) option requires no fetch; continue to step 4 and | |
| point `TABLEBASE_PATH` at the URL. | |
| 4. **Configure `config.py`**: open it and set `TABLEBASE_PATH` to the | |
| directory (or URL) from step 3: | |
| ```python | |
| TABLEBASE_PATH = "/data/chesstb" | |
| ``` | |
| or, for the remote option: | |
| ```python | |
| TABLEBASE_PATH = "https://huggingface.co/buckets/chesstb/chesstb/resolve" | |
| ``` | |
| This alone covers every piece count. See | |
| [Getting the tablebase files](#getting-the-tablebase-files) for | |
| the ordered source list, which includes shipping tables for both ordinary | |
| and castling materials. | |
| Every other setting in `config.py` has a default and a comment | |
| explaining what it does - see | |
| [Configuration reference](#configuration-reference) for the complete | |
| list. Edit the values directly. To enable the header's source repository button, | |
| set `SOURCE_REPOSITORY_URL` there too. | |
| 5. **Run the app:** | |
| ```bash | |
| python app.py | |
| ``` | |
| By default (`DEBUG = False` in `config.py`) this serves the app via | |
| **waitress**, a production-grade pure-Python WSGI server - see | |
| [Running in production](#running-in-production) below. Set | |
| `DEBUG = True` in `config.py` to use Flask's development server | |
| (auto-reload, interactive debugger) for local development. | |
| 6. Open **http://127.0.0.1:7860** in a browser. | |
| The built frontend (`static/dist/`) is checked in; see | |
| [Frontend build](#frontend-build) when changing `frontend/src/`. | |
| --- | |
| ## Getting the tablebase files | |
| Where the tablebase *data* lives is a separate question from where this | |
| *app* runs - Option A below points `TABLEBASE_PATH` at a plain | |
| `http(s)://` URL, so it works identically regardless of which platform | |
| serves the app itself (Render, Hugging Face Spaces, a local machine, | |
| etc.); `remote/remote_source.py` speaks plain HTTP byte-range requests, | |
| nothing more, and has no dependency on any one host. See | |
| [Deploying to Render](#deploying-to-render) for the app side of that | |
| distinction. | |
| ChessTB tablebase files are available two ways: | |
| ### Option A: Remote, no local download (recommended) | |
| Tables are available in the [ChessTB bucket](https://huggingface.co/buckets/chesstb/chesstb). | |
| Shipping tables are grouped by metric; raw generator output is under `full/`. | |
| Both use `castling/` for tables with castling rights. | |
| WDL/DTZ cover 3–8 pieces; DTC/DTM/DTM50 cover 3–6 pieces. | |
| Set `TABLEBASE_PATH` to the bucket root: | |
| ```python | |
| TABLEBASE_PATH = "https://huggingface.co/buckets/chesstb/chesstb/resolve" | |
| ``` | |
| The explorer prefers raw generator output under `full/`, falls back to | |
| shipping tables, and selects `castling/` paths automatically, including | |
| for recursive dependencies. This applies to Standard chess and Chess960. | |
| Local roots use the same layout. An explicit list of source directories | |
| is also supported. | |
| `REMOTE_MODE` selects the remote access mode, which stays fixed for the | |
| life of the process: | |
| - **`"direct"` (default)**: `remote/remote_direct.py` probes the remote | |
| tables **in place**, fetching only the `REMOTE_PAGE_SIZE_BYTES`-sized | |
| byte ranges each probe reads and keeping them in an in-memory | |
| LRU bounded by `REMOTE_PAGE_CACHE_BYTES`. Nothing is written to disk. | |
| This uses `chess.chesstb`'s table-source seam | |
| (`Tablebase.WDL_FILE` / `_TableFile._open_source`), which the pinned | |
| fork (see `requirements.txt`) provides. | |
| - **`"download"`**: `remote/remote_fallback.py` fetches each table | |
| **in full** the first time a probe touches its material and caches it | |
| in a temporary local directory (bounded by | |
| `REMOTE_PAGE_CACHE_BYTES` as an on-disk budget, evicting | |
| least-recently-used files, removed entirely when the app stops). Every | |
| probe after the first is then a local mmap read. | |
| `"direct"` is the better default for browsing across many materials, which | |
| is what this app does. `"download"` is preferable when a long session | |
| repeatedly probes a small set of materials, or on a high-latency link: a | |
| ChessTB probe is not a narrow read - a dropped-frame table is reconstructed by walking | |
| its children, and pawn positions reach promotion sub-tables - so one cold | |
| probe can open several materials and issue several fetches against each. | |
| That is also why `REMOTE_PAGE_SIZE_BYTES` should stay large. See each | |
| module's docstring for the full design. | |
| Both remote modes cache successful signed CDN redirects until 60 seconds | |
| before expiry, allowing subsequent byte-range reads to bypass the resolver. | |
| A failed cached URL is retried through the original URL to obtain a fresh | |
| redirect. Resolver and CDN connection pools stay open for reuse. | |
| Complete cached responses are returned immediately by `/probe/stream`, | |
| without rebuilding the board or queuing child probes. Queued child work is | |
| cancelled when a stream disconnects or its pre-warm deadline expires; | |
| already running probes can finish and warm the shared cache. | |
| Table contents and paths are assumed to remain fixed; existence | |
| and size metadata stay cached for the process lifetime, including missing | |
| files. Restart the app after adding or moving tables. | |
| Remote access suits a machine without a couple of terabytes of local disk | |
| for the full tablebase set. Its costs are probe latency on first touch of | |
| any given material (depending on the connection to the CDN), temporary | |
| local disk usage in `"download"` mode, and the requirement of a network | |
| connection. Remote access uses the `requests` package (see | |
| `requirements.txt`). | |
| ### Option B: Local directory, over FTP | |
| The same tables are also distributed over FTP for a fully local, offline | |
| setup: | |
| ``` | |
| ftp://chessdb:chessdb@ftp.chessdb.cn/pub/chesstb/ | |
| ``` | |
| Any FTP client works, for example: | |
| ```bash | |
| # lftp | |
| lftp -e "mirror --parallel=4 /pub/chesstb/ /data/chesstb; quit" \ | |
| ftp://chessdb:chessdb@ftp.chessdb.cn | |
| # curl (single file) | |
| curl "ftp://chessdb:chessdb@ftp.chessdb.cn/pub/chesstb/<path-to-file>" -o <local-file> | |
| ``` | |
| Tablebase sets are large and grow quickly with piece count - check | |
| available disk space before mirroring the full archive, and consider | |
| mirroring only the required subsets (piece counts). Point | |
| `TABLEBASE_PATH` at the selected directory. | |
| --- | |
| Regardless of how it is configured, if `TABLEBASE_PATH` is unset, missing/unreachable, or | |
| points at a location with no usable tables, the app still starts (with a | |
| warning in the logs) - every `/probe` request will then return an error, | |
| and `/health` reports `"degraded"`. | |
| --- | |
| ## Configuration reference | |
| All configuration lives in **`config.py`**, as plain Python values. Open | |
| it and edit the settings directly - each one has a comment above it | |
| explaining what it does. The table below is the complete reference. | |
| The defaults target a single process on Hugging Face CPU Basic | |
| (2 vCPU, 16 GB RAM). The decoded-block cache has a shared 512 MiB | |
| budget, and each of the four default remote sources has a 2 GiB page | |
| cache budget, totaling 8.5 GiB of byte-cache budgets. Caches fill on | |
| demand; this is not a cap on process memory. Root-response and child-probe | |
| caches are limited by entry count, with memory use depending on response | |
| size. The remaining RAM accommodates those results, table/index metadata, | |
| Python object overhead, and active requests. Revisit the budgets if you | |
| add remote sources or run multiple application processes. | |
| | Setting | Default | Description | | |
| |-----------------------------|-------------------|-------------| | |
| | `TABLEBASE_PATH` | Public bucket root (also readable from a `TABLEBASE_PATH` env var) | Directory containing the ChessTB tablebase files, **or** an `http(s)://` base URL serving the same layout remotely (e.g. a Hugging Face storage bucket - see [Getting the tablebase files](#getting-the-tablebase-files)). Required for probing to work. A list of directories and URLs, tried left-to-right (a hybrid source), is accepted as a literal in `config.py` only. On Render, set this from the service's Environment tab (or `render.yaml`'s prompt) instead of editing the file. | | |
| | `DEBUG` | `False` | `True` runs `app.py` via Flask's own dev server (auto-reload, detailed tracebacks) instead of waitress. Leave `False` - which serves via waitress - for anything reachable from another machine. See [Running in production](#running-in-production). | | |
| | `HOST` | `"0.0.0.0"` | Interface the server (waitress, or the dev server if `DEBUG = True`) binds to. | | |
| | `PORT` | `7860` (also readable from a `PORT` env var) | Port the server listens on. Render injects its own `PORT` at deploy time and this picks it up automatically - nothing to set for a Render deployment. | | |
| | `WAITRESS_THREADS` | `4` | Worker threads in waitress's request-handling pool. Only relevant when `DEBUG = False`. Needs to be more than 1 so a long-lived `/probe/stream` connection can't block other requests. | | |
| | `PROBE_THREADS` | `2` | Worker threads in the probe thread pool for evaluating a position's legal moves in parallel. Set an explicit integer to match the host's CPU count, or `None` to scale automatically (`min(16, cpu*2)`). | | |
| | `PROBE_PARALLEL_THRESHOLD` | `4` | Minimum number of child positions before probing switches from sequential to the thread pool. | | |
| | `PROBE_TIMEOUT_SECS` | `30` | Wall-clock timeout for each probe phase. `/probe` gives the root probe and the child-move batch independent budgets, so worst-case `/probe` latency can approach `2 × PROBE_TIMEOUT_SECS`. Also bounds how long a request waits on another thread's in-flight probe of the same FEN before giving up with a retryable `probe_timeout` error. | | |
| | `EVALUATE_CACHE_SIZE` | `16384` | Max entries in the root-FEN result cache (full JSON responses). | | |
| | `PROBE_CACHE_SIZE` | `262144` | Max entries in the child-position probe cache (raw `chess.chesstb.ProbeResult` objects - WDL/DTZ/DTC/DTM/DTM50 together). | | |
| | `BLOCK_CACHE_BYTES` | `536870912` (512 MiB) | Size, in bytes, of `chesstb`'s own internal cache of decoded/decompressed tablebase blocks (shared across the WDL/DTZ/DTC/DTM/DTM50 tables). Raising this trades RAM for fewer repeated disk reads/HTTP fetches + decompressions across a session - most worthwhile when `PROBE_PARALLEL_THRESHOLD` is set high enough that probing runs mostly serially. | | |
| | `REMOTE_MODE` | `"direct"` | **Remote tablebase entries only.** `"direct"` probes the remote tables in place over byte ranges (nothing written to disk); `"download"` fetches each table in full on first touch and caches it on local disk. See [Getting the tablebase files](#getting-the-tablebase-files). | | |
| | `REMOTE_PAGE_CACHE_BYTES` | `2147483648` (2 GiB) | **Remote tablebase entries only.** Soft budget, in bytes, per remote source in `TABLEBASE_PATH`, shared across that source's tables: the in-memory page cache in `"direct"` mode, the on-disk cache of whole downloaded files in `"download"` mode. See [Getting the tablebase files](#getting-the-tablebase-files). | | |
| | `REMOTE_PAGE_SIZE_BYTES` | `262144` (256 KiB) | **Remote tablebase entries only.** Size, in bytes, of one page. In `"direct"` mode this is the granularity of every fetch, so it trades over-fetching against round trips per probe; in `"download"` mode it is only the chunk size used while streaming a full file down. | | |
| | `REMOTE_TIMEOUT_SECS` | `20` | **Remote tablebase entries only.** Per-HTTP-request timeout for existence/size checks and the download itself. | | |
| | `REMOTE_MAX_RETRIES` | `3` | **Remote tablebase entries only.** Attempts for a single remote request before it's treated as failed. | | |
| | `REMOTE_POOL_MAXSIZE` | `None` | **Remote tablebase entries only.** Size of each remote backend's HTTP connection pool. `None` computes `max(PROBE_THREADS * 2, 20)`. | | |
| | `PROBE_RATE_LIMIT` | `"60 per minute"` | Per-client-IP request limit on `/probe` and `/probe/stream`, using [flask-limiter](https://flask-limiter.readthedocs.io/)'s string syntax. Left empty, rate limiting is disabled entirely. See [Security notes](#security-notes). | | |
| | `ADMIN_LOGIN_RATE_LIMIT` | `"5 per minute"` | Per-client-IP request limit on `/admin/login`, same string syntax as `PROBE_RATE_LIMIT`, applied independently of it. Covers both failed and successful login attempts. Left empty, login attempts are not rate limited. See [Security notes](#security-notes). | | |
| | `PREFETCH_RATE_LIMIT` | `"30 per minute"` | Per-client-IP request limit for hover prefetch requests marked with `X-Prefetch: 1`. Left empty, prefetch requests share `PROBE_RATE_LIMIT`. See [Security notes](#security-notes). | | |
| | `TRUSTED_PROXY_COUNT` | `1` (also readable from a `TRUSTED_PROXY_COUNT` env var) | Number of trusted reverse-proxy hops in front of this app. In production this configures [waitress's own `trusted_proxy_count`](https://docs.pylonsproject.org/projects/waitress/en/stable/proxy-headers.html) (waitress parses `X-Forwarded-For`/`X-Forwarded-Proto` and corrects `REMOTE_ADDR`/`wsgi.url_scheme` itself, ahead of Flask); [Werkzeug's `ProxyFix`](https://werkzeug.palletsprojects.com/en/latest/middleware/proxy_fix/) makes the same correction for the `DEBUG = True` dev-server path, which doesn't run under waitress. Both rate limiters key on the corrected address, and `request.scheme`/`request.is_secure` read the corrected scheme. `1` matches this project's own `render.yaml` topology (Render's edge/load-balancer, once). Set it to match the deployment's reverse-proxy chain, or set it to `0` for a deployment with no reverse proxy in front of it at all. See [Security notes](#security-notes). | | |
| | `ADMIN_TOKEN` | `""` (from `ADMIN_TOKEN` env var) | Shared secret required to reach `/admin` and `/admin/cache/*`. Read from the environment rather than hardcoded, so it can be set as a platform secret. Left unset, admin routes respond `503` rather than running unprotected. See [Security notes](#security-notes). | | |
| | `FLASK_SECRET_KEY` | `""` (from `FLASK_SECRET_KEY` env var) | Key used to sign the admin session cookie. Read from the environment for the same reason as `ADMIN_TOKEN`. Left unset, a random key is generated at process startup instead, so every restart requires logging in again. | | |
| | `SESSION_COOKIE_SECURE` | `False` (also readable from a `SESSION_COOKIE_SECURE` env var, `"true"`/anything else) | Whether the admin session cookie set by `/admin/login` requires HTTPS. Independent of `DEBUG`. Defaults `False` to match a plain-HTTP local installation; set to `"true"` for any deployment the browser reaches over HTTPS (including behind a TLS-terminating reverse proxy such as Render's - `render.yaml` already sets this explicitly for that deployment). See [Security notes](#security-notes). | | |
| | `SOURCE_REPOSITORY_URL` | `""` (also readable from a `SOURCE_REPOSITORY_URL` env var) | Repository URL for the source repository button shown in the header of every page. Edited directly like any other non-secret value here, or set as an env var on a given deployment. Left empty, the button is omitted. | | |
| Invalid values (wrong type, out of range) cause the app to log an error | |
| and exit at startup rather than run with a silently-wrong configuration. | |
| --- | |
| ## Running in production | |
| `python app.py` serves the app via **[waitress](https://docs.pylonsproject.org/projects/waitress/)**, | |
| a production-grade, pure-Python WSGI server, whenever `DEBUG` in | |
| `config.py` is `False` (the default) - no separate `gunicorn`/`waitress` | |
| command or extra process is needed on top of the app itself. Setting | |
| `DEBUG = True` in `config.py` switches `app.py` over to Flask's own dev | |
| server instead (auto-reload + interactive debugger), which is meant for | |
| local development only and should not be used for anything reachable | |
| from another machine. | |
| ```bash | |
| # Production (default): DEBUG = False in config.py: serves via waitress | |
| python app.py | |
| # Local development: set DEBUG = True in config.py first, then run the | |
| # same command: Flask dev server, auto-reload + debugger | |
| python app.py | |
| ``` | |
| - `HOST` / `PORT` control the interface and port waitress binds to, same | |
| as for the dev server. `PORT` is also readable from a `PORT` | |
| environment variable (see [Configuration reference](#configuration-reference)), | |
| which is what lets the same image bind correctly on Render - or any | |
| other platform that assigns the port at deploy time - with no edits. | |
| - `WAITRESS_THREADS` (default `4`) sizes waitress's own pool of | |
| request-handling threads. This needs to be more than 1 for the same | |
| reason the dev server needs `threaded=True`: `/probe/stream` holds a | |
| connection open via Server-Sent Events for the whole duration of a | |
| probe, and a single-threaded server would let that one connection block | |
| every other request - including the browser's own concurrent requests | |
| for CSS/JS/piece images on first page load. It's independent of | |
| `PROBE_THREADS`, which sizes the thread pool used internally to | |
| parallelise tablebase probing rather than to serve HTTP requests. | |
| - `TRUSTED_PROXY_COUNT` (default `1`) configures waitress's own | |
| `trusted_proxy_count`, so `request.remote_addr` and `request.scheme` | |
| reflect the real client IP and scheme behind Render's edge/load-balancer | |
| rather than that proxy's own address/hop - see | |
| [Security notes](#security-notes). | |
| - Waitress handles `/probe/stream`'s streamed response natively; no | |
| additional configuration is needed for SSE to work correctly. | |
| - The probe endpoints are unauthenticated by design; only `/admin` and | |
| `/admin/cache/*` require a credential (see | |
| [Security notes](#security-notes)). | |
| --- | |
| ## Deploying to Render | |
| This project deploys to [Render](https://render.com) as a single Docker | |
| web service, built straight from this GitHub repository. Render watches | |
| the connected branch and rebuilds/redeploys automatically on every push. | |
| There are two ways to set it up: | |
| ### Option 1: Blueprint (`render.yaml`, recommended) | |
| This repository ships a [`render.yaml`](render.yaml) at its root | |
| describing the service (Docker runtime, health check, environment | |
| variables). Render finds it automatically: | |
| 1. Push this repository to GitHub. | |
| 2. In the [Render Dashboard](https://dashboard.render.com), click | |
| **New → Blueprint** and connect the repo. | |
| 3. Render reads `render.yaml` and requests the one setting required | |
| before the first deploy can run: **`TABLEBASE_PATH`** - the | |
| `http(s)://` base URL of the remote tablebase (see | |
| [Getting the tablebase files](#getting-the-tablebase-files)). | |
| `ADMIN_TOKEN` and `FLASK_SECRET_KEY` are generated automatically. | |
| 4. Click **Apply** / **Create**. Render builds the image from this | |
| repo's `Dockerfile` and deploys it. | |
| Every subsequent push to the connected branch redeploys automatically. | |
| To change `TABLEBASE_PATH`, `ADMIN_TOKEN`, or any other setting later, | |
| edit it from the service's **Environment** tab in the Render Dashboard; | |
| see [Configuration reference](#configuration-reference) for which | |
| settings are env-overridable this way versus which live only in | |
| `config.py`. | |
| ### Option 2: Manual Web Service (no `render.yaml`) | |
| The service can also be configured manually without the Blueprint file: | |
| 1. Push this repository to GitHub. | |
| 2. In the Render Dashboard, click **New → Web Service** and connect the | |
| repo. | |
| 3. Set **Runtime** to **Docker** (Render should detect the `Dockerfile` | |
| at the repo root automatically). | |
| 4. Under **Environment**, add: | |
| - `TABLEBASE_PATH`: required; same as Option 1 above. | |
| - `ADMIN_TOKEN`: optional but recommended; any secret string, or | |
| use Render's "Generate" button. While it is unset, `/admin` | |
| responds `503`. | |
| - `FLASK_SECRET_KEY`: optional; when omitted, a random key is | |
| generated at process startup and admin sessions don't survive a | |
| restart. | |
| - `SESSION_COOKIE_SECURE`: set to `true`. Render always terminates | |
| TLS at its edge, so the browser reaches this deployment over HTTPS | |
| even though the app itself just speaks plain HTTP to Render's | |
| proxy; `config.py`'s own default (`false`) is tuned for a local | |
| plain-HTTP installation instead. (Option 1's `render.yaml` sets | |
| this automatically - it is a manual step here because Option 2 | |
| skips that file.) | |
| - `SOURCE_REPOSITORY_URL`: optional; the repository URL, which enables the | |
| header's source repository button. | |
| - Leave `PORT` alone - Render sets it itself, and `config.py` already | |
| reads it (see [Configuration reference](#configuration-reference)). | |
| 5. Under **Health Check Path**, set `/health` (matches the route in | |
| `app.py`; Render won't mark a deploy healthy until this returns `200`, | |
| which itself only happens once the sources in `TABLEBASE_PATH` have | |
| opened successfully). | |
| 6. Click **Create Web Service**. | |
| ### Choosing a plan | |
| Probing is CPU-bound (block decompression on every request), so a | |
| dedicated vCPU - Render's **Starter** tier or above - gives lower probe | |
| latency than the **Free** tier's shared/limited CPU. Free works | |
| for casual or low-traffic use; the service spins down after inactivity | |
| (a slow first request after waking up) and probes are slower under load. Either way, `REMOTE_MODE = "direct"` (the default) | |
| means no persistent disk is needed regardless of plan - see | |
| `config.py`'s "Remote (URL) tablebase entries only" section and | |
| [Getting the tablebase files](#getting-the-tablebase-files). | |
| --- | |
| ## Usage guide | |
| - **Set a position**: type or paste a FEN into the FEN box and press | |
| `Enter` or **Apply**. The move rankings and best-move arrows update | |
| automatically. The castling-availability field accepts `-` or any | |
| combination of `K`, `Q`, `k`, `q` (each requiring a matching king and | |
| rook on their home squares); in **Chess960 Mode** it additionally | |
| accepts X-FEN/Shredder-FEN rook-file letters (`a`-`h` / `A`-`H`). A | |
| position with a castling right is evaluated the same as any other, | |
| reported as not covered if the loaded tablebase set has no table for it | |
| (see `TABLEBASE_PATH` in | |
| [Configuration reference](#configuration-reference)). | |
| - **Chess960 (Fischer Random)**: toggle **Chess960 Mode** in the settings | |
| panel to switch castling interpretation. This does not move any pieces | |
| or randomize a new starting position - it only changes how castling | |
| moves/rights on the *current* board are interpreted, and drops the | |
| current move line the same way a board edit does (a line built under | |
| one variant isn't reinterpreted as the other). With it on, drag a king | |
| onto its own rook to castle; the destination square and resulting rook | |
| position follow standard Chess960 rules regardless of where the king | |
| and rook started. The setting is carried in the shared URL | |
| (`&variant=chess960`), the saved in-progress game, and PGN | |
| import/export (a `[Variant "Chess960"]` header). | |
| - **Edit the board directly**: drag pieces around the board, drag a piece | |
| off the board to remove it, or click a spare piece in either tray and | |
| then click a square to place it (click again / press `Esc` to cancel). | |
| Board edits reset the current move line the same way **Clear** does. | |
| - **Lock the board**: click the padlock button beside the FEN box to | |
| restrict drag-and-drop, click-to-move, and the spare-piece trays to | |
| legal moves for the side to move; the button turns gold while active. | |
| Click again to unlock. The choice is remembered locally across visits. | |
| Starting auto-play locks the board automatically for the run (unless | |
| already locked) and disables the padlock button until it stops. | |
| - **Play moves**: click any move in one of the four ranked tables to | |
| play it, or drag a piece on the board through a legal move. | |
| - **Navigate history**: the Undo/Redo arrow buttons or `←`/`→`, or click | |
| any move in the PGN panel to jump straight to that point in the line. | |
| - **Auto-play**: click the ▶ button above the DTZ, DTC, DTM, or DTM50 | |
| columns | |
| to have the app play that metric's best move on a timer (delay set by | |
| **Autoplay Delay** in settings, 1.25s by default); click again (the | |
| button shows ■ while running) to stop. Any manual navigation stops auto-play. | |
| - **Import/export a game**: **Import** in the PGN panel opens a dialog to | |
| paste PGN text. **Import Game** loads the full game and its variations | |
| into the PGN panel, starting before the first move; **Export** | |
| downloads the full game and its variations as `chesstb_game.pgn` with | |
| standard headers. | |
| - **Export the move table**: the **CSV** button downloads the current | |
| DTZ/DTC/DTM/DTM50 rankings as a CSV file. | |
| - **Share a position**: the URL updates live with `#fen=...`; sending | |
| that link reproduces the exact position. | |
| - **Change settings**: the ⚙ button in the header opens a **Show Root | |
| Row** switch for the results table, a **Chess960 Mode** switch, and an | |
| **Autoplay Delay** slider; the choices are persisted across visits. | |
| - **Admin dashboard**: the gauge icon in the header opens `/admin`, prompting for the | |
| `ADMIN_TOKEN` on first visit, then showing live hit-rate stats for both | |
| caches and the thread-pool configuration, auto-refreshing every 5 | |
| seconds. | |
| --- | |
| ## API | |
| The full HTTP API - `/probe`, `/probe/stream`, `/health`, | |
| `/admin/cache/stats`, `/admin/cache/clear`, `/admin/login`, | |
| `/admin/logout` - is documented as an OpenAPI 3.0 specification served | |
| live by the running app at: | |
| ``` | |
| http://127.0.0.1:7860/openapi.yaml | |
| ``` | |
| `openapi.yaml` documents every endpoint's request/response shapes; the | |
| ranking algorithm behind `moves_dtz` / `moves_dtc` / `moves_dtm` / | |
| `moves_dtm50` is documented inline in `app.py`'s own module docstring and | |
| its `evaluate_all_moves()` function. `/admin/cache/*` require an | |
| `Authorization: Bearer <ADMIN_TOKEN>` header or an authenticated session | |
| cookie from `/admin/login` - see [Security notes](#security-notes). | |
| Quick example: | |
| ```bash | |
| curl -X POST http://127.0.0.1:7860/probe \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"fen": "4k3/8/8/8/8/8/8/4K2R w - - 0 1"}' | |
| ``` | |
| `variant` is optional and defaults to `"standard"` when omitted. It is | |
| never inferred from the FEN itself - the orthodox starting array is | |
| itself a legal Chess960 starting position, so the same FEN text is | |
| ambiguous without it: | |
| ```bash | |
| curl -X POST http://127.0.0.1:7860/probe \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"fen": "r3k2r/8/8/8/8/8/8/R3K2R w KQkq - 0 1", "variant": "chess960"}' | |
| ``` | |
| --- | |
| ## Project structure | |
| ``` | |
| . | |
| ├── LICENSE # GPL-3.0-or-later license text for this project | |
| ├── THIRD_PARTY_LICENSES.md # Licenses and attributions for bundled third-party code, fonts, and the piece set | |
| ├── Dockerfile # Container build used for Render (and any other Docker host) | |
| ├── render.yaml # Render Blueprint: service definition, health check, env vars | |
| ├── app.py # Flask backend: routes, probing, caching | |
| ├── config.py # All configuration, as plain Python values - see "Configuration reference" | |
| ├── openapi.yaml # OpenAPI 3.0 specification, served at /openapi.yaml | |
| ├── requirements.txt | |
| ├── README.md | |
| ├── .gitattributes, .gitignore, .dockerignore | |
| ├── tablebase_router.py # Ordered local/remote source lookup for every material | |
| ├── tablebase_opening.py # Per-material table-opening lock policy, shared by tablebase_router.py and both remote backends | |
| ├── dtz_anchor.py # Resolves rounded DTZ values by cross-checking the moves of a position | |
| ├── remote/ | |
| │ ├── remote_source.py # Generic HTTP byte-range client shared by both remote backends - see its own module docstring | |
| │ ├── remote_direct.py # REMOTE_MODE="direct": probe remote tables in place over byte ranges - see its own module docstring | |
| │ └── remote_fallback.py # REMOTE_MODE="download": whole-file download, cached to local disk on first touch - see its own module docstring | |
| ├── templates/ | |
| │ ├── index.html # Main explorer UI - mounts the React app (see frontend/) | |
| │ └── admin.html # Cache dashboard | |
| ├── frontend/ # React app for the explorer page - see "Frontend build" | |
| │ ├── package.json, package-lock.json | |
| │ ├── vite.config.js # Builds into ../static/dist with fixed filenames (no cache-busting hash) | |
| │ ├── index.html, admin.html # Build entries (script and stylesheet) | |
| │ └── src/ | |
| │ ├── main.jsx, App.jsx | |
| │ ├── engine/ # Position/rules engine and tablebase client (chessops + Chessground) | |
| │ ├── vendor/ # Vendored Chessground (script and base stylesheet) + chessops, bundled by the build | |
| │ ├── hooks/useExplorer.js # FEN box, PGN, auto-play, settings, keyboard shortcuts | |
| │ ├── components/ # Board, results table, PGN panel, dialogs, settings panel | |
| │ │ └── ui/ # Icon, Button, Segmented, Panel | |
| │ ├── styles/ # Tokens, base, layout, component and chess stylesheets - see "Styling" | |
| │ └── assets/fonts/ # Inter and JetBrains Mono (woff2) | |
| └── static/ | |
| ├── dist/ # Built frontend (see "Frontend build"): app.js, app.css, admin.css, fonts/ | |
| ├── css/ | |
| │ └── pieces-cburnett.css # Piece-set stylesheet linked by templates/index.html | |
| ├── js/ | |
| │ └── admin.js # Admin dashboard client | |
| └── pieces/cburnett/ # Piece-set SVGs | |
| ``` | |
| --- | |
| ## Security notes | |
| - The `/probe` and `/probe/stream` endpoints are intentionally | |
| unauthenticated - evaluating a chess position is this app's public, | |
| core functionality. `/admin` and `/admin/cache/*` are gated behind | |
| `ADMIN_TOKEN` (see below); every other route requires no credential. | |
| - **Rate limiting** on `/probe` and `/probe/stream` is controlled by | |
| `PROBE_RATE_LIMIT`, and on `/admin/login` separately by | |
| `ADMIN_LOGIN_RATE_LIMIT` (both in `config.py`'s "Rate limiting" | |
| section), each applied per client IP via | |
| [flask-limiter](https://flask-limiter.readthedocs.io/). A client over | |
| either limit gets a `429` with a JSON error body. `PROBE_RATE_LIMIT`'s | |
| default, `"60 per minute"`, covers normal browsing and | |
| autoplay at its default delay - raise it if legitimate autoplay at a | |
| fast delay setting gets throttled, lower it if the deployment sees | |
| abuse, or set either limit to `""` to disable it entirely. | |
| `ADMIN_LOGIN_RATE_LIMIT` defaults to `"5 per minute"`, tight enough to | |
| slow down repeated `ADMIN_TOKEN` guesses. Both limiters key on | |
| `request.remote_addr` as corrected by `TRUSTED_PROXY_COUNT` (see | |
| below) and their state lives in-process (`memory://`), which is | |
| sufficient for a single-container deployment (e.g. one Render Web | |
| Service instance) but isn't shared across multiple replicas. | |
| - **`TRUSTED_PROXY_COUNT`** (`config.py`'s "Rate limiting" section) is | |
| what `request.remote_addr`, and therefore both rate limiters above, | |
| is corrected against, so it reads the real client IP from | |
| `X-Forwarded-For` rather than the address of the reverse proxy | |
| in front of this app. The same setting also governs whether | |
| `X-Forwarded-Proto` is trusted, correcting `request.scheme` / | |
| `request.is_secure` (and any externally-generated URL) when TLS | |
| terminates at that reverse proxy rather than at this app itself. In | |
| production (`DEBUG = False`) this configures [waitress's own `trusted_proxy_count`](https://docs.pylonsproject.org/projects/waitress/en/stable/proxy-headers.html): | |
| waitress parses both headers and corrects `REMOTE_ADDR`/`wsgi.url_scheme` | |
| itself, before Flask ever sees the request, so it takes priority here. | |
| [Werkzeug's `ProxyFix`](https://werkzeug.palletsprojects.com/en/latest/middleware/proxy_fix/) | |
| makes the equivalent correction for the `DEBUG = True` Flask-dev-server | |
| path, which doesn't run under waitress at all. Defaults to `1`, | |
| matching this project's own `render.yaml` topology; set it to match | |
| the deployment if that differs, or to `0` for a deployment with no | |
| reverse proxy in front of it at all. | |
| - **Admin access** is controlled by `ADMIN_TOKEN` (`config.py`'s "Admin" | |
| section), read from the environment rather than hardcoded - set it as | |
| a platform secret (e.g. a Render environment variable, generated | |
| automatically by `render.yaml`'s `generateValue: true`) rather than | |
| committing it. Visiting `/admin` with no active session shows a login | |
| form; a correct token there starts a signed, `HttpOnly`, | |
| `SameSite=Lax` session cookie (`Secure` too, where configured - see | |
| `SESSION_COOKIE_SECURE` below). API clients can instead send | |
| `Authorization: Bearer <ADMIN_TOKEN>` directly, no session needed. | |
| Token comparison uses `hmac.compare_digest` (constant-time, resistant | |
| to timing attacks). Leaving `ADMIN_TOKEN` unset does **not** open the | |
| admin panel to everyone - every admin route responds `503` instead. | |
| - **`SESSION_COOKIE_SECURE`** (`config.py`'s "Admin" section) controls | |
| whether the admin session cookie above requires HTTPS, independently | |
| of `DEBUG` - `DEBUG` is a development/runtime behavior switch, not a | |
| reliable indicator of the deployment's transport security, so | |
| `DEBUG = False` alone does not imply HTTPS is in front of this | |
| app. Read from a `SESSION_COOKIE_SECURE` env var (`"true"` or anything | |
| else, case-insensitive), defaulting `False` to match this project's own | |
| plain-HTTP local installation - a browser won't send a | |
| `Secure` cookie back over a plain HTTP connection, so `/admin/login` | |
| would otherwise appear to succeed while every subsequent authenticated | |
| `/admin/*` request came back `401`. Set to `"true"` for any deployment | |
| the browser reaches over HTTPS (including behind a TLS-terminating | |
| reverse proxy - the browser's own connection is what the `Secure` | |
| attribute governs, not the internal hop between the proxy and this | |
| process); `render.yaml` already sets this explicitly for the Render | |
| deployment it configures, since Render always terminates TLS at its | |
| edge. | |
| - The app serves via **waitress**, a production-grade WSGI server, by | |
| default (`DEBUG = False` in `config.py`) - see [Running in production](#running-in-production). | |
| Only set `DEBUG = True` (which switches to Flask's own dev server) for | |
| local development, never for anything reachable from another machine. | |
| - A `Content-Security-Policy` is set on every response: scripts and fonts | |
| are restricted to `'self'` (no inline scripts, no third-party sources); | |
| styles allow `'self'` plus `'unsafe-inline'` (needed for inline | |
| `style` attributes set by the React app and in `admin.html`); images | |
| allow `'self'` plus `data:`; `connect-src`/`default-src` are `'self'` | |
| too, so `fetch`/SSE calls can only reach this same origin. `object-src`, | |
| `base-uri`, and `form-action` are locked to `'none'`/`'self'`/`'self'`, | |
| and `frame-ancestors 'self' https://huggingface.co` allows the Hugging | |
| Face Space page to embed the app. `X-Content-Type-Options: | |
| nosniff` and `Referrer-Policy: strict-origin-when-cross-origin` are set | |
| too. Request bodies are capped at 4 KB. | |
| - `config.py` itself holds no secrets - `ADMIN_TOKEN` and | |
| `FLASK_SECRET_KEY` are the only two secrets, and they are read only from | |
| the environment so the file stays safe to commit as-is. Every other | |
| setting is a non-sensitive value. | |
| --- | |
| ## Troubleshooting | |
| | Symptom | Likely cause | | |
| |---|---| | |
| | Every probe returns "Position not covered by the loaded tablebase." | Check `TABLEBASE_PATH` and the startup logs. Set the tablebase root; `full/` and `castling/` directories are resolved automatically. For an explicit source list, include every required directory. | | |
| | A FEN is rejected with "Invalid castling availability" | The submitted castling-availability field is something other than `-` or a combination of `K`, `Q`, `k`, `q` (or, in Chess960 Mode, rook-file letters `a`-`h`/`A`-`H`), or names a castling right without a matching king and rook on the board. | | |
| | A FEN with `a`-`h`/`A`-`H` castling-rights letters is rejected even though the position looks fine | Those letters are only accepted with **Chess960 Mode** on (settings panel, or `variant: "chess960"` on the API) - Standard mode only accepts `K`, `Q`, `k`, `q`, matching Standard chess's fixed castling-rook squares. | | |
| | Dragging a king onto its own rook doesn't castle | **Chess960 Mode** is off - enable it in the settings panel (or send `variant: "chess960"` on the API) for Chess960-style king-onto-rook castling; in Standard mode a king can only reach its rook's square by regular king movement. | | |
| | `/health` returns `503 degraded` | The tablebase failed to open - check `TABLEBASE_PATH` and file permissions. | | |
| | `ModuleNotFoundError: No module named 'chess.chesstb'` | Plain PyPI `chess` was installed instead of the fork. Re-run `pip install -r requirements.txt` and confirm it pulled `chess` from `noobpwnftw/python-chess` (the `add-chesstb-tablebases` branch) rather than plain PyPI `chess` - see the note at the top of `requirements.txt`. | | |
| | `ModuleNotFoundError: No module named 'config'` | `config.py` is missing from alongside `app.py`. It ships with the repository - if it was deleted or moved, restore it from the repo (or re-clone) and re-apply any edits, such as `TABLEBASE_PATH`. | | |
| | App logs `Configuration error: ...` and exits immediately | A value in `config.py` is the wrong type or out of range for that setting (e.g. a string where an integer is expected, or a negative cache size) - the error message names which setting and why. Fix it in `config.py` and re-run `python app.py`. | | |
| | Admin dashboard panels stuck on "Loading…" | Check the browser console for a CSP violation and verify that the app is running and reachable at the `/admin` URL. | | |
| | A drag, drop, or spare-piece placement on the board is silently rejected | The board is Locked - the padlock button beside the FEN box is highlighted gold. Lock restricts board interaction to legal moves and disables the spare-piece trays and off-board drops; click the padlock again to unlock and edit freely. | | |
| | Probing feels slow on positions with many legal moves, or doesn't scale with `PROBE_THREADS` | Confirm `lz4` is installed (`pip show lz4`, or `python -c "import lz4"`) - without it, `chess.chesstb`'s block decompression runs in pure Python and holds the GIL, so `PROBE_THREADS` can't achieve real parallelism. `pip install lz4` (already in `requirements.txt`) and restart. Otherwise, tune `PROBE_THREADS`, `PROBE_PARALLEL_THRESHOLD`, and `PROBE_TIMEOUT_SECS` in `config.py`. Note that `tablebase_router.py` guards each material's first open with a lock keyed by `(kind, material)`, so two threads racing to open the *same* never-before-seen material collapse onto one read, while distinct materials of the same kind open concurrently. If any configured tier uses `REMOTE_MODE="download"`, opens fall back to one lock shared per kind, so several distinct never-before-seen materials of that kind then open one at a time instead - either way, this only affects the very first probe against any given material. | | |
| | `ModuleNotFoundError: No module named 'waitress'` | The active environment's dependencies are out of date - re-run `pip install -r requirements.txt` to pick up `waitress`. `app.py` imports it unconditionally at the top of the file, before `config.DEBUG` is even read, so setting `DEBUG = True` does **not** avoid this - waitress has to be installed either way. | | |
| | Every probe on a remote `TABLEBASE_PATH` returns "Position not covered..." or `/health` is `degraded` | Confirm the URL is reachable and correct (try opening `TABLEBASE_PATH/wdl/KQK.lzw` - or any small material's `.lzw` - directly in a browser). Check the startup log's "TABLEBASE_PATH opened remotely at: ..." line (or a warning in its place) for the actual failure. | | |
| | A probe occasionally returns `503` with `error_code: "probe_timeout"` | Another concurrent request was already probing the same position and didn't finish within `PROBE_TIMEOUT_SECS`. This is transient and retryable - the original probe has usually landed in the child-probe cache before a retry, so the retry is inexpensive. Frequent occurrences suggest raising `PROBE_TIMEOUT_SECS`, or that a slow remote `TABLEBASE_PATH` is the underlying bottleneck. | | |
| | Remote probing feels slow, or repeated probes against the same material keep hitting the network | Check `REMOTE_TIMEOUT_SECS`/`REMOTE_MAX_RETRIES` aren't causing retries on a slow link, and consider raising `REMOTE_PAGE_CACHE_BYTES` - a too-small budget evicts cached pages (`"direct"`) or downloaded files (`"download"`) before later probes can reuse them. `GET /admin/cache/stats` surfaces this per tier as `tablebase_cache.<low|high|castling>.remote_page_cache` or `.remote_disk_cache` respectively. On a high-latency link also try raising `REMOTE_PAGE_SIZE_BYTES`, or `REMOTE_MODE = "download"`. See [Configuration reference](#configuration-reference). | | |
| | On Render, the deploy never goes live / stays stuck "deploying" | Render's health check (`healthCheckPath: /health` in `render.yaml`, or the equivalent field on a manually-created service - see [Deploying to Render](#deploying-to-render)) only passes once `/health` returns `200`, which itself requires the sources in `TABLEBASE_PATH` to have opened successfully. Check the service's **Logs** tab for the same "opened remotely at: ..." / warning line described above. | | |
| | On Render, the service responds but the page never loads / connection refused | Typically a `PORT` mismatch. Confirm `config.py` reads `PORT` from the environment (see [Configuration reference](#configuration-reference)) and that nothing else in a custom start command overrides `HOST`/`PORT`. | | |
| --- | |
| ## Credits | |
| - Tablebase data & format: [ChessTB / chessdb.cn](https://www.chessdb.cn/) | |
| - Tablebase probing support: [noobpwnftw/python-chess (`add-chesstb-tablebases` branch)](https://github.com/noobpwnftw/python-chess/tree/add-chesstb-tablebases), a fork of [niklasf/python-chess](https://github.com/niklasf/python-chess) | |
| - Remote (URL) tablebase support for the fork's `chess.chesstb` module - see `remote/remote_source.py`, `remote/remote_direct.py` and `remote/remote_fallback.py` | |
| - Board UI: [Chessground](https://github.com/lichess-org/chessground) | |
| - Move generation/validation on the client, for both Standard chess and | |
| Chess960: [chessops](https://github.com/niklasf/chessops) | |
| - Production WSGI server: [waitress](https://docs.pylonsproject.org/projects/waitress/) | |
| - Piece set: [cburnett](https://github.com/lichess-org/lila/blob/master/COPYING.md), a commonly-used community set found in most web chess UIs. | |
| ## License | |
| ChessTB Explorer is licensed under `GPL-3.0-or-later`; see [`LICENSE`](LICENSE). | |
| Bundled third-party components and their licenses are listed in | |
| [`THIRD_PARTY_LICENSES.md`](THIRD_PARTY_LICENSES.md). | |