Skip to main content

battle.sh

Terminal two-player Battleship over an operator-owned WebSocket Relay.

Install

pip install battle-sh

This installs a single battle-sh command. You do not need to clone the repo, run uv, or invoke Python modules to play.

Play (battle-sh)

The whole experience is one executable:

battle-sh                  # open the Textual player app (Host / Join / Exit)
battle-sh --relay URL      # same, with a Relay URL
battle-sh relay            # run the Match Relay server

Point clients at a relay with --relay (or the BATTLE_SH_RELAY environment variable); it defaults to ws://127.0.0.1:8765. Reconnect grace is --grace-seconds (CLI-only).

# Terminal A — Relay
battle-sh relay --bind-host 127.0.0.1 --port 8765
# Terminal B — Player (Host)
battle-sh --relay ws://127.0.0.1:8765
# Choose Host; share the Invite shown on the waiting screen.
# Terminal C — Player (Guest)
battle-sh --relay ws://127.0.0.1:8765
# Choose Join; paste the Invite in-app.

Against a hosted relay, use the wss:// URL, e.g. battle-sh --relay wss://relay.example.com.

The Match UI shows a live scoreboard: active/remaining ships per side, hits, current turn, match state, and a connection indicator for both players.

Local development (ws://, no Caddy)

Checks

Same gates as CI:

uv sync --group dev
uv run pyright
uv run pytest

Play

Run the Relay on loopback, then open two player terminals:

uv sync
uv run python -m battle_sh.networking.relay_cli --bind-host 127.0.0.1 --port 8765
# Terminal A — Host
uv run battle-sh --relay ws://127.0.0.1:8765
# Choose Host; share the Invite.
# Terminal B — Guest
uv run battle-sh --relay ws://127.0.0.1:8765
# Choose Join; paste the Invite in-app.

Live Match UI

Fixed three-band layout: top = Match/role/turn + Match time; middle = wide board | phase-aware controls; bottom = status/errors. Match time starts when the Guest joins (not during Host lobby wait) and freezes on the Winner/Abandoned end screen. After end presentation, the app returns to the opening Host / Join / Exit menu.

Key map

Phase Keys
Placement 1–5 or Tab / Shift+Tab select ship; w/a/s/d or arrows move; e / r flip H↔V; t re-roll; y lock
Combat w/a/s/d or arrows Aim (skips fired cells); f / Enter / Space fire
Any Match phase first Ctrl+C warns (status), second confirms Abandon and sends leave_match (both sides end immediately; no reconnect grace; arm auto-clears)

Waiting turns show a spinner; only Ctrl+C is honored for quit. q never quits. Back is available on Join and Host-waiting only (not mid-Match). Invite for Guest is entered in-app on Join. Caddy and TLS are not required for local play.

Manual Host/Guest smoke checklist

On a local Relay (commands above), in two terminals:

  1. Host creates a Match from the opening menu; confirm lobby shows waiting-for-Guest (no Match time yet).
  2. Guest joins with the Invite in-app; both see Match time start; three-band chrome stays stable.
  3. Placement: move/rotate/re-roll with keys, then y to lock; opponent wait shows spinner.
  4. Combat: Aim with arrows/WASD, fire with f; confirm skip over already-fired cells.
  5. Quit path: two-step Ctrl+C Abandons immediately for both players (leave_match); both see Match Abandoned with frozen Match time, then return to the opening menu.
  6. Optional: play through to Winner and confirm frozen Match time on the end screen before returning to opening.

Run a Relay on any cloud VM (wss://)

You rent (or already have) a normal Linux virtual machine somewhere — DigitalOcean, Hetzner, AWS, a home box, whatever. This project does not create the VM for you and is not tied to one cloud. You point DNS at the machine, SSH in with our scripts, and they install the Relay.

Works best on Debian/Ubuntu (the script can install Caddy with apt). On other distros, install Caddy yourself first, then run provision again.

1. Create a VM

In your cloud console: start a small Ubuntu VM, note its public IP, and make sure you can SSH as root (or a user that can sudo — today the scripts assume you SSH as the user who can write /opt and manage systemd; root is simplest).

Open ports 22 (SSH), 80, and 443 on the firewall / security group.

2. Point a domain at the VM

Pick a hostname players will use, e.g. relay.example.com.

In your DNS provider, create an A record (and AAAA if you use IPv6) from that name to the VM’s public IP. Wait until dig +short relay.example.com shows the right address.

Do this before provisioning. Caddy asks Let’s Encrypt for a real certificate for that name; if DNS is wrong, certs fail and wss:// will not work.

3. Install the Relay from your laptop

On your laptop (this repo), with SSH access to the VM:

./scripts/provision-relay \
  --host root@YOUR_VM_IP \
  --domain relay.example.com \
  --email you@example.com

That copies the app to /opt/battle-sh, installs uv and Caddy if needed, writes a systemd unit (battle-sh-relay.service), and starts Relay + Caddy. Caddy terminates HTTPS and forwards WebSockets to the Relay on 127.0.0.1:8765.

Players then connect with:

battle-sh --relay wss://relay.example.com

Preview files without touching a machine:

./scripts/provision-relay \
  --host root@YOUR_VM_IP \
  --domain relay.example.com \
  --email you@example.com \
  --dry-run \
  --output-dir /tmp/battle-sh-deploy

4. Tear it down

./scripts/deprovision-relay --host root@YOUR_VM_IP

Stops Relay and Caddy, removes the unit, Caddyfile, and /opt/battle-sh. The Caddy package may stay installed on the OS.

Practice the same flow in Docker (no real cloud)

Want to rehearse provision without renting a VM? scripts/practice-vm boots a systemd Ubuntu container with SSH — a stand-in “cloud box” on your machine — then runs the real provision script against it.

Docker is not how we deploy in production (uv + Caddy + systemd on a real host is). This is only a local practice VM. Because there is no public DNS, provision uses --tls-internal (Caddy’s local certificate). Real internet VMs should not pass that flag.

./scripts/practice-vm doctor    # up → provision → status → WebSocket smoke
# or step by step:
./scripts/practice-vm up
./scripts/practice-vm provision
./scripts/practice-vm status
./scripts/practice-vm smoke
./scripts/practice-vm down

SSH into the practice box: ssh -i .scratch/practice-vm/id_ed25519 -p 2222 root@127.0.0.1

Metadata

Release files for battle-sh 0.1.5

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for battle-sh 0.1.5
File Size Uploaded Provenance
battle_sh-0.1.5.tar.gz 42.5 kB With provenance Details

Built distribution (wheel)

Table of built distributions (wheels) for battle-sh 0.1.5
File Interpreter ABI Platform
battle_sh-0.1.5-py3-none-any.whl Python 3 none any Details

Total release size: 98.4 kB

Release files / battle_sh-0.1.5.tar.gz

Download URL battle_sh-0.1.5.tar.gz
Size 42.5 kB
Tags Source
SHA-256 checksum
How to use checksums
77e1e86446b9fa960029df58e7fdd5ca47422ede36de41b5a55db388c02a86b4
BLAKE2b-256 checksum
How to use checksums
0f739ec09b36e0fa155cdc4cd864c1c650e3d7ec69cbc4f0d64cf36dee7d556e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 31, 2026.

Transparency log

Release files / battle_sh-0.1.5-py3-none-any.whl

Download URL battle_sh-0.1.5-py3-none-any.whl
Size 55.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
fd37f6d0d94167913fa68f2bfb2c4275d483d4a5ab0f569c17826d2ed1e2164a
BLAKE2b-256 checksum
How to use checksums
7e322f9294a164da6f541a8e96714b01b84ae870b63a0c3f9ba481bf4aaa817e
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 31, 2026.

Transparency log

Security for battle-sh 0.1.5

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

Full provenance

All 2 files in this release have been uploaded with provenance metadata.

Select from 2 files

Attestation for battle_sh-0.1.5.tar.gz

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 31, 2026.

Transparency log

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page