# 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 .venv/bin/python practice.py # vs the computer: http://127.0.0.1:8081 ``` 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, and everything else is static files. - `practice.py`: a separate server for playing against the computer. 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 cloudflared tunnel route dns moonchess practice.moonchess.example.com # if running practice.py ``` 3. `~/.cloudflared/config.yml`: ```yaml tunnel: moonchess credentials-file: /home/YOU/.cloudflared/.json ingress: - hostname: moonchess.example.com service: http://localhost:8080 - hostname: practice.moonchess.example.com # optional, if you run practice.py service: http://localhost:8081 - 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.