ndza

Project update - Battleship - P2P and web-browser support

In my previous post, I introduced my hobby project, a two-player battleship game, playable through TUIs, where one terminal hosts the game, and another terminal joins it. I did want to take it further than just a simple client-server application driven through a TUI. So, here I mention some progress, along with learnings. The updated codebase is v0.0.2.

Connecting the apps over Internet using P2P

For the game to work right now, the joining-client needs to be able to connect to the hosting-server. Therefore, the server has to be reachable from the client, in terms of IP-address and port. It is a WebSocket over TCP/IP connection. There might be some firewall configuration required on the server machine to allow connections to the incoming port. This is a relatively simple setup, and works nicely for most LANs.

One thing I'd like to support is not just this sort of connectivity, but also making it possible to connect over the Internet from any client to any server. I want to take this opportunity to learn about P2P networks.

But, there are some challenges along the way.

NATs

Most domestic users' networks are behind routers doing NAT. The user's machine IP-address (usually private, allocated internally) is different than their public IP-address (assigned by the ISP). For outgoing connections, ports are usually remapped as well.

Applications can use STUN servers to try to somewhat discover these public endpoints and NAT types, followed by UDP hole-punching, for example, to establish direct peer connections.

This is not always possible. Modern or commercial grade routers can also block incoming packets, unless it's a reply to a connection initiated by it. Symmetric NATs can always be problematic, as in my case. That's where, TURN relay servers come in. We have to use them as a fallback.

And then we have ICE, which is a framework meant to help figure out the best approach (direct or STUN hole punching or TURN relay) by testing every path quickly and picking the fastest route.

So, I elected to use coturn, which looks like a nice way to help this. I'll need to host this publicly.

To clarify, I could just build a bespoke dedicated centralised game-server, that all players (hosters and joiners) connect to as clients and communicate through. But, this can be hard to run/manage and get right in the first place. It'll be is very specific to my game. And over here I want to support P2P networking too.

Signalling server

We also need some sort of signalling server. This is a small server that acts like a matchmaker, allowing joining-clients to find hosting-servers. In the case of battleship, a UUID is used for matching. I've written this in Rust.

Web-browser mode

Another thing I'd like to support is to be able to play the game in the web-browser. I'd like this to be similar to the TUI setup, where one player hosts a game and another player joins it. But what's more, I'd like to support a way for both terminals and web-browsers to play together.

For the battleship GUI, I went with Leptos in client-side rendering mode, this allows me to build a WebAssembly based client in Rust. And it means I get to learn a popular Rust GUI framework. I also went with the same terminal look in the browser, which I thought was nice.

For the P2P networking connectivity mentioned in the previous section, we have to use WebRTC, as that's the technology available in web-browsers for this sort of networking.

Screenshots/Traces

I haven't hosted anything publicly, yet. But, here are some screenshots that allow me to play locally.

The /etc/turnserver.conf has this:

listening-port=3478
realm=battleship.local
use-auth-secret
static-auth-secret=nolan
allow-loopback-peers
cli-password=nolan

Status after restart shows a sensible output:

nolan@nolan-veed:~/projects/battleship$ sudo systemctl restart coturn
nolan@nolan-veed:~/projects/battleship$ sudo systemctl status coturn
● coturn.service - coTURN STUN/TURN Server
     Loaded: loaded (/usr/lib/systemd/system/coturn.service; enabled; preset: enabled)
     Active: active (running) since Tue 2026-11-03 08:02:38 GMT; 2s ago
 Invocation: 85821083156e4d66997063ef18b26a94
       Docs: man:coturn(1)
             man:turnadmin(1)
             man:turnserver(1)
   Main PID: 897772 (turnserver)
      Tasks: 27 (limit: 37498)
     Memory: 12.6M (peak: 13.3M)
        CPU: 38ms
     CGroup: /system.slice/coturn.service
             └─897772 /usr/bin/turnserver -c /etc/turnserver.conf --pidfile=

Nov 03 08:02:38 nolan-veed turnserver[897772]: 0: : IO method (auth thread): epoll (with changelist)
Nov 03 08:02:38 nolan-veed turnserver[897772]: 0: : IO method (auth thread): epoll (with changelist)
Nov 03 08:02:38 nolan-veed turnserver[897772]: 0: : IO method (auth thread): epoll (with changelist)
Nov 03 08:02:38 nolan-veed turnserver[897772]: 0: : IO method (auth thread): epoll (with changelist)
Nov 03 08:02:38 nolan-veed turnserver[897772]: 0: : IO method (auth thread): epoll (with changelist)
Nov 03 08:02:38 nolan-veed turnserver[897772]: 0: : IO method (auth thread): epoll (with changelist)
Nov 03 08:02:38 nolan-veed turnserver[897772]: 0: : IO method (auth thread): epoll (with changelist)
Nov 03 08:02:38 nolan-veed turnserver[897772]: 0: : IO method (admin thread): epoll (with changelist)
Nov 03 08:02:38 nolan-veed turnserver[897772]: 0: : SQLite DB connection success: /var/lib/turn/turndb
Nov 03 08:02:38 nolan-veed systemd[1]: Started coturn.service - coTURN STUN/TURN Server.
nolan@nolan-veed:~/projects/battleship$ cargo run --release -p battleship-signal -- --turn-secret nolan
    Finished `release` profile [optimized] target(s) in 0.17s
     Running `target/release/battleship-signal --turn-secret nolan`
2026-11-03T08:04:57.822311Z  INFO battleship_signal: signaling server listening on 0.0.0.0:3536

nolan@nolan-veed:~/projects/battleship/crates/web$ ~/.cargo/bin/trunk serve
2026-11-03T08:06:47.242180Z  INFO 🚀 Starting trunk 0.22.1
2026-11-03T08:06:47.242968Z  INFO 📦 starting build
2026-11-03T08:06:47.243056Z  INFO node packages []
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.12s
2026-11-03T08:06:48.667422Z  INFO applying new distribution
2026-11-03T08:06:48.668784Z  INFO ✅ success
2026-11-03T08:06:48.668819Z  INFO 📡 serving static assets at -> /
2026-11-03T08:06:48.669063Z  INFO 📡 server listening at:
2026-11-03T08:06:48.669069Z  INFO     🏠 http://127.0.0.1:8080/
2026-11-03T08:06:48.669073Z  INFO     🏠 http://[::1]:8080/
2026-11-03T08:06:48.669124Z  INFO     🏠 http://localhost.:8080/
2026-11-03T08:06:48.669135Z  INFO     🏠 http://ip6-loopback.:8080/
2026-11-03T08:06:48.669138Z  INFO     🏠 http://ip6-localhost.:8080/
A screenshot of battleship-web waiting
nolan@nolan-veed:~/projects/battleship$ cargo run --release -p battleship-tui -- join 8ffe82e8-46a8-4fa1-bcd5-74d300876232 --signal ws://localhost:3536 --turn turn:localhost:3478

Signalling server says:

2026-11-03T08:08:03.872762Z  INFO battleship_signal::rooms: host registered room=8ffe82e8
2026-11-03T08:11:14.393155Z  INFO battleship_signal::rooms: joiner arrived room=8ffe82e8
A screenshot of battleship-web placing A screenshot of battleship-tui placing A screenshot of battleship-web playing A screenshot of battleship-tui playing

Signalling server says:

2026-11-03T08:19:55.404952Z  INFO battleship_signal::rooms: joiner left room=8ffe82e8
2026-11-03T08:20:42.741570Z  INFO battleship_signal::rooms: joiner arrived room=8ffe82e8
A screenshot of player2 rejoining through battleship-web

Looking ahead

This has been good for learning. The claude has made it easy to figure out the moving pieces. I've made sure the code structure is good, and the code is not too messy.

I need to figure out a couple of things though. I need a better way to host these coturn and signaling servers. I have found some cloud-based providers for TURN servers and for signaling. So, watch this space for updates there. It'll make it easier for me to host battleship on this website.