Files
moonchess/README.md
T
tukaunuandClaude Opus 5.5 ce6b71eb35 Serve practice mode from the main server, add mode switch, fix touch
Practice mode is now at /practice on server.py instead of a separate
server, with a button in the bottom-right corner of each page to the
other mode. Touch: no double-tap zoom, no sticky hover after a tap, no
long-press selection on the board, and a pressed look on tapped squares.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-22 01:47:27 +02:00

109 lines
4.8 KiB
Markdown

# Moonchess
Tic-tac-toe where each player can have at most three pieces on the board. Placing a
fourth isn't possible: once you have three down, your oldest vanishes as your turn
starts (it's shown faded during your opponent's turn), so you can place right back on
its square. This follows Genshin Impact's Moon Chess.
## Running locally
```sh
python3 -m venv .venv
.venv/bin/pip install -r requirements.txt
.venv/bin/python server.py # http://127.0.0.1:8080
.venv/bin/python server.py --host 0.0.0.0 --port 8080 # reachable on your LAN
```
Practice mode (vs the computer) is at `/practice` on the same server. The switch in the
bottom-right corner of each page goes between the two modes.
Tests: `.venv/bin/python -m unittest discover -s tests -t .`
## How it works
- `game.py`: the rules, no networking.
- `server.py`: one aiohttp process. `/ws?game=CODE` is the live connection, `/practice`
and `POST /api/play` are practice mode, and everything else is static files.
- `practice.py`: practice mode's move endpoint, mounted by `server.py`. It holds no
state: the page sends the position to `POST /api/play` and gets the next one back.
- `solver.py`: solves every reachable position (~67k, about a second at startup) so the
computer knows the perfect move everywhere.
- `static/`: the pages. Plain HTML/CSS/JS, no build step. `board.js` is the board
drawing shared by `app.js` (online) and `practice.js`.
Practice mode: you start as X, which moves first. Click either side at any time to swap,
and the computer (Columbina, after the Genshin Impact minigame this game comes from)
plays whichever side you aren't. The accuracy slider is the chance each
computer move is perfect; otherwise it plays a random empty square. It can be changed
mid-game too. Undo (or Ctrl/Cmd+Z) goes back to your previous turn, taking back
Columbina's reply as well. Perfect play is a draw: you can only threaten one line
at a time, so you only lose by missing a block.
Seating: each game has a list of slots. Slots 0 and 1 are X and O, and the rest are
spectators. A new connection takes the earliest empty slot, so if a player drops,
whoever connects next gets their seat. Refreshing fast enough to grab it is allowed.
Heartbeat: the client pings every second. A seat whose player has been silent for 2
seconds shows as "Disconnected", and after 4 seconds they're dropped and the seat
shows as "Empty seat". The client dims the board and reconnects if it hasn't heard
from the server in 4 seconds. That automatic reconnect (unlike a refresh) comes from
the same page load, so it reclaims its own slot even if the server hasn't dropped the
old connection yet. A player who refreshes during the "Disconnected" window lands in
a spectator slot and has to refresh again once their seat shows "Empty seat".
Codes: any string of up to 32 characters works as a code, ignoring case and extra
whitespace. Joining a code that has no game creates one. "Generate code" just picks a
random 5-character code in the browser. Games live in memory: a game is deleted as soon as
nobody is connected to it (so if the last person in a game refreshes, it starts over),
and restarting the server ends all games.
## Deploying behind Cloudflare (Cloudflare Tunnel)
A tunnel lets Cloudflare reach the server without opening router ports or exposing
your home IP. WebSockets work through it without any extra configuration.
1. Run the server on the box, bound to localhost: `.venv/bin/python server.py --port 8080`
2. Install `cloudflared`, then:
```sh
cloudflared tunnel login
cloudflared tunnel create moonchess
cloudflared tunnel route dns moonchess moonchess.example.com
```
3. `~/.cloudflared/config.yml`:
```yaml
tunnel: moonchess
credentials-file: /home/YOU/.cloudflared/<tunnel-id>.json
ingress:
- hostname: moonchess.example.com
service: http://localhost:8080
- service: http_status:404
```
4. `cloudflared tunnel run moonchess` (or `sudo cloudflared service install` to run it at boot).
To keep the game server running, an example systemd unit (`/etc/systemd/system/moonchess.service`):
```ini
[Unit]
Description=Moonchess
After=network.target
[Service]
WorkingDirectory=/opt/moonchess
ExecStart=/opt/moonchess/.venv/bin/python server.py --port 8080
Restart=always
User=moonchess
[Install]
WantedBy=multi-user.target
```
If you'd rather use the regular orange-cloud proxy with port forwarding, point the
DNS record at your IP and forward a port to the server. That also works, because
Cloudflare proxies WebSockets on all plans. Cloudflare only proxies certain ports
(e.g. 80, 443, 8080, 8443), so bind to one of those or put a reverse proxy in front.
## Future work
- Seat-open alert: give everyone (especially spectators) an obvious signal when a
player seat becomes empty, so they can race to refresh and grab it.