Building and Testing a Real Two-Player Game on a Distributed Network

by Jonathan Brown


A backgammon game is a deceptively good test of decentralized application design.

The rules are simple enough to explain in a few minutes, but a serious two-player implementation forces nearly every hard question into the open. Who is allowed to make a move? How do both players agree on the current board? How do they prevent a dishonest player from choosing favorable dice after seeing the other player’s secret? What happens when one machine loses power, one browser reloads, a node reconnects, or two people are separated by thousands of miles and unreliable network paths? And, just as importantly, how do ordinary human beings actually find one another and begin a game without becoming amateur cryptographers?

Freenet Backgammon began as an attempt to answer those questions by building a playable game on the current Freenet network. It is not a cloud-hosted web game with a decorative blockchain-shaped object behind it. The game state is held in Freenet contracts, actions are signed and verified, and each player’s browser reconstructs the board from the authoritative ledger retrieved through that player’s own Freenet node.

We have now played complete games between two independent nodes. In the first successful full game, one player was on a node in Florida and the other in Brazil. In the second, the apparent network locations were Stockholm and São Paulo through separate VPNs. The interface itself ran locally in each player’s browser, but the game actions were exchanged through Freenet and verified by both sides. The games were slow—slow enough that we sped up a recording sixteenfold—but they completed. The board logic held. The dice process held. Moves propagated. A machine power loss and reboot did not destroy the game.

That is an important threshold. It means the project has moved beyond “a backgammon interface that looks distributed” and into the much more interesting territory of a working distributed game with a real recovery story.

What Freenet Is, and Why It Is Interesting for Games

The modern Freenet project is not simply a place to host static files or a disguised conventional web server. It is an application platform organized around peer-to-peer communication, contracts, replicated state, and locally controlled identities. A user runs a Freenet node. Applications communicate with that local node, which participates in the wider network. The application does not need to put a conventional central server in the middle of every interaction.

That architecture invites a different way of thinking about multiplayer software.

In a normal online game, a central service is authoritative. It maintains the database, decides which move arrived first, stores account records, resolves disagreements, and generally acts as the referee. This model is familiar and often practical. It is also expensive to operate, vulnerable to outages, subject to platform control, and structurally dependent on the people who run the server.

A decentralized game has to move some of those responsibilities into a shared, verifiable protocol. The point is not that every game needs a decentralized ledger. A fast first-person shooter, for example, has demanding real-time requirements that make this a poor fit for slow global consensus or state propagation. But a turn-based game is different. Backgammon already has a natural rhythm: roll, move, wait, respond. Players can tolerate some delay if the result is trustworthy and recoverable.

Backgammon also exposes the limits of naïve decentralization. It is easy to store “a move happened.” It is harder to prove that the move was legal, made by the correct player, based on the correct previous board, and tied to fair dice. It is harder still to ensure that neither player can quietly rewrite history when a network interruption occurs.

That is the actual subject of Freenet Backgammon: not merely drawing a board in a browser, but creating a protocol that makes a distributed board game meaningful.

The Core Design: The Ledger Is the Game

The central principle of the project is straightforward: the authoritative game is not the picture of the board in either browser. It is the verified action history in the game contract.

Every game begins with an authenticated genesis action. That action establishes the game configuration, the two player identities, their roles, and the information required to derive the contract associated with that particular game. It is not enough for one player to say, “Here is a new game.” The genesis process is designed so that both parties contribute authenticated evidence before the game is treated as ready.

From there, the game progresses through signed actions. A browser may let a player click Roll, choose a checker, or choose a destination, but those clicks are only proposals. The local client verifies what it can verify, creates the appropriate signed protocol action, and sends a delta through the local Freenet node. The network eventually returns authoritative state. Each browser verifies and replays that state, then reconstructs the board.

This distinction matters enormously. A local UI can be wrong, stale, or temporarily disconnected. A player can refresh the page. A laptop can shut down. But if the authoritative ledger contains valid actions, the client can retrieve the ledger again and deterministically rebuild the same position.

That is precisely what we observed in live testing. One machine shut down unexpectedly from power loss during the first full game. After reboot, the game state could be retrieved and restored. In the second game, Pots was rebooted in the middle of a live match. The exact board, move history, active roles, and pending turn returned from the contract. No human had to reconstruct the position from memory. No one had to trust the other player’s account of what had happened.

The architecture therefore separates three things that ordinary online games often blend together:

  • The local presentation: the board, buttons, dice images, and move history in the browser.
  • The player’s local durable state: identity keys, pending actions, dice material, and handshake data retained in browser storage.
  • The shared authoritative state: the verified action ledger held through the Freenet contract system.

The browser is important, but it is not the referee. The ledger is.

Why the Protocol Work Was the Real Work

Most of the visible interface can be understood at a glance: checker points, the bar, borne-off counters, a roll button, a pass button, player panels, and move history. The difficult engineering sits underneath it.

The protocol now uses authenticated actions and a canonical representation of those actions. Canonical encoding is not an aesthetic preference. If two clients represent “the same move” in subtly different byte forms, exact comparison, signing, recovery, and verification become fragile. The project therefore treats the encoded action history as a security boundary, not simply an implementation detail.

Each action is verified against the prior state. The client replays the verified history deterministically rather than trusting a convenient summary. That means an invalid action cannot simply be inserted because a UI displays it. If the history does not decode, does not verify, or does not replay correctly, the client rejects it rather than presenting an invented board as authoritative.

The game’s dice process requires particular care. In a centralized backgammon service, the operator can run a random-number generator and tell both players the result. In a peer-to-peer game, that is not satisfactory: neither player should be able to select the roll unilaterally after learning what would benefit them.

Freenet Backgammon uses a protocol built around player-controlled secret material, commitments, requests, and reveals. The exact mechanism is less important to a casual player than its purpose: both players contribute to the information used to produce an outcome, and the protocol prevents a player from changing a committed contribution after learning the other side’s. The client stores the relevant local material durably and recovers it when possible. A browser refresh must not silently generate a different secret or a different action identifier for the same already-pending turn.

This was a substantial part of the engineering process. “Roll the dice” sounds like a button. In a distributed game, it is a state machine with failure modes.

The same is true of every network action. The client records pending actions exactly, verifies their structure, and attempts recovery rather than casually generating replacements. If a request reached the local node but the browser did not receive confirmation before a connection dropped, the client must not create a second contradictory request. It must be able to recognize the durable pending action, retry it where appropriate, and wait for authoritative confirmation.

That is the difference between a demo and a game someone can trust with an hour of play.

From a Local Board to a Freenet-Connected Client

The visible client is a Rust/WASM browser application. It is built and served locally during development, then opened in Firefox. The browser connects to the player’s local Freenet node through the node’s loopback WebSocket API at 127.0.0.1:7509. This is a useful boundary: the UI does not need broad network privileges or direct exposure to another player’s machine. Each player runs a local client talking to a local node; the Freenet nodes carry the contract traffic.

During our tests, the UI on the EliteBook was served through a local SSH tunnel from Pots. That arrangement kept the development server private while allowing the EliteBook browser to load the exact same built client artifacts. Importantly, this did not make the game itself “local” in the ordinary two-player-on-one-computer sense. The EliteBook’s browser connected to the EliteBook’s own Freenet node, and Pots connected to the Pots node. The browser files were merely delivered across a tunnel for development convenience.

This distinction became clearer as the test progressed. The UI was local to each user, but game state travelled through the network and returned as verified contract state. The game was not a remote desktop session or a shared browser tab. It was two independent browser clients running the same protocol against independent nodes.

The first major task was therefore not cosmetic UI work. It was connecting the browser safely to the node API, receiving contract responses, classifying them, checking authoritative ledgers, and keeping asynchronous network activity bound to the correct active game. A browser client can have old callbacks running after a player changes selected games, reloads, or begins another challenge. Without careful scoping, a late response for a previous game can overwrite the visible state of the current game.

The project now uses active game scope snapshots to prevent that. Network requests, callbacks, and recovery tasks are tied to the game contract and scope that created them. If the player changes games, stale work is not allowed to mutate the new game’s UI. This is not glamorous engineering, but it is exactly the sort of work that stops a distributed game from becoming haunted by delayed messages.

Lobby, Presence, Challenges, and Acceptance

A playable game needs more than an authoritative ledger. Players have to discover one another.

The project therefore includes a separate lobby contract and client-side lobby logic. A player can create a local identity, choose a display name, publish presence, and mark themselves available for new challenges. Other clients retrieve and verify the lobby state, project the valid presence records, and show eligible players.

A challenge is not simply a chat message saying “play me.” It contains signed evidence: challenger identity, challenged identity, game information, expiry, and the data needed to establish a corresponding game contract. The receiving client verifies that the challenge is addressed to its local identity and that the relevant contract evidence is present before the player signs acceptance.

This acceptance step was another major milestone. The client now has a durable, verified path from an incoming challenge to an accepted game. It can derive the game identifier, expected game contract, peer identity, and local player role from authoritative accepted-challenge state. It can retain exact local acceptance evidence through browser storage and reconnects. It can then begin the genesis-share exchange required for authenticated game creation.

This sounds complicated because it is. The goal is to avoid “trust me, we are playing now” logic. A player should not be able to trick another browser into joining an arbitrary contract, accept a challenge not actually addressed to that identity, or replace the agreed genesis with a different game configuration.

The result of this work is that the accepted game is not merely a UI selection. It is an authenticated relationship between two cryptographic identities, tied to a deterministic contract identity and a verifiable game genesis.

What Live Play Taught Us

The two live games have been more valuable than a large number of purely local tests, not because the code was untested, but because real users immediately expose the difference between protocol correctness and usable software.

The first full game completed from beginning to end. The players were on independent nodes. The game logic appeared sound. The board state remained consistent. Pass Turn worked correctly. The players were able to make legal moves, hit checkers, enter from the bar, bear off, and finish a game. The test also demonstrated recovery after the EliteBook lost power and restarted.

The second game deliberately tested messier conditions. The two nodes were operating through separate VPN exit locations—Stockholm and São Paulo. Pots was rebooted during the game. The client reported connection trouble, then recovered through the Reconnect control. The authoritative game state came back after reboot, and play continued.

Several important conclusions emerged.

First, the durable game design is working. The ledger survived power loss, browser reloads, node reconnection, and a full host reboot. That is perhaps the most important success criterion for this kind of application.

Second, a browser’s matchmaking availability is not the same thing as game connectivity. The UI may show a player as “Unavailable” because they are not advertising themselves for new challenges, while that same player is fully connected and actively participating in an existing game. The protocol behavior is sensible; the wording is not. The interface should say “Not accepting new challenges,” not “Unavailable,” because the latter sounds like a network failure.

Third, connection status needs clearer semantics. During testing, a red “Freenet connection failed” badge could coexist with a game that was still propagating verified state or whose local client could recover with a single Reconnect action. The client’s status handling is real—it can reconnect successfully—but it needs to distinguish a WebSocket error, a failed contract request, a recovery attempt, an inactive subscription, and a restored connection. A single red badge is too blunt an instrument for that job.

Fourth, the present UI makes the right information hard to see. Accepted games persist, correctly, because persistence is required for recovery. But the game list currently presents cryptographic identifiers in a narrow right-hand rail, where they wrap vertically and can look duplicated. Newest, completed, active, and archived games are not yet separated clearly. Choosing the correct accepted game before activation requires more attention than it should. These are not protocol failures. They are precisely the kind of user-experience work that becomes obvious only after people use the system for real.

How to Get Started with Freenet

For a technically minded reader, the first step is to understand that Freenet applications are normally used through a local node. You are not merely visiting a distant web server. You install and run the node on your own machine, then applications communicate with it locally.

The general path is:

  1. Obtain Freenet from the official project resources and run a local node.
  2. Confirm that the node is healthy and that its local API is available.
  3. Explore the project’s developer documentation and examples before exposing services or changing node configuration.
  4. For Freenet Backgammon specifically, clone the project repository and check out the active development branch, network-actions-0.1.
  5. Install the Rust toolchain, the WASM target, and Trunk or the equivalent client build tooling.
  6. Build and serve the browser client locally.
  7. Open the local client in a modern browser and allow it to create or restore its local identity and durable browser state.
  8. Use the lobby to publish presence, discover another test player, challenge or accept a challenge, select the authenticated accepted game, activate it, and play.

At the moment, this is a developer and advanced-tester path, not a consumer installation path. The live UI source is in the repository, but it is not yet packaged as a one-click Freenet application that ordinary users can discover, install, and launch without a Rust/WASM development environment.

A developer such as Ian Clarke could nevertheless test it from the repository today. He could run his own local node, build the client, generate a new identity, join the lobby, and challenge one of the existing test identities. He would not need access to Pots or the EliteBook. He would be operating an independent client against the same public protocol and contract environment.

That is a meaningful stage of openness. The source is not trapped on a single machine. The protocol is not imaginary. But it is not yet the finished public distribution experience.

Where Freenet Backgammon Stands Now

The project is at the point where the underlying distributed game is more mature than the product experience around it.

The important layers are in place:

  • A protocol for authenticated, replayable game actions.
  • Canonical action encoding and verification.
  • Deterministic reconstruction of the board from authoritative history.
  • A durable pending-action and local-secret recovery strategy.
  • A challenge and acceptance flow tied to verified lobby state.
  • A dual-party authenticated genesis process.
  • Deterministic game-contract derivation from game identity.
  • A browser client that connects to a player’s local Freenet node.
  • Real cross-node games played to completion.
  • Demonstrated recovery from browser disruption, node reconnection, host reboot, and power loss.

What remains is substantial, but it is now product work built on a working foundation rather than speculative protocol work.

The immediate next phase should focus on the client experience:

  • Redesign the accepted-game list into clear cards or rows with opponent name, color, status, time, and a readable short identifier.
  • Separate active, pending, completed, and archived games.
  • Make “You are White” or “You are Black” prominent before and after activation.
  • Replace ambiguous “Unavailable” language with “Not accepting new challenges.”
  • Separate node/WebSocket status, lobby subscription status, game-contract status, and recovery status.
  • Make reconnect behavior explicit: what failed, whether an automatic retry is in progress, when it succeeded, and what state was recovered.
  • Make the right-hand status area scrollable or collapsible rather than allowing raw identifiers to stretch down the screen.
  • Provide an intentional rematch flow, including color swap and a clean path to start a new challenge without confusing it with an old accepted game.
  • Improve presentation of move history and game completion.
  • Add more human-readable diagnostics while preserving the exact technical evidence needed for developers.

After that comes distribution. The client needs to be packaged and published in a way that allows a Freenet user to discover it, obtain it, run it against their local node, and understand what they are being asked to approve. The published application must also make its compatible protocol and contract versions clear. A user should not need to know what a genesis share is in order to play backgammon, even though the client still needs to handle one correctly.

Finally, the project should undergo broader testing and review. A game involving identities, signatures, dice commitments, durable pending actions, and shared contracts deserves independent inspection. The fact that two people have now played complete games is powerful evidence of functional progress. It is not, by itself, a security audit or a claim of finished production readiness.

Still, something real has happened.

A local browser interface on two separate machines has become the front end to a game whose moves are signed, propagated, verified, persisted, recovered, and replayed through Freenet. The system has survived the ordinary violence of actual computing: Wi-Fi changes, VPNs, stale browser state, reloads, reconnection failures, a power-depleted laptop, and a host reboot. It has also survived the more subtle violence of human use: people selecting the wrong old game, looking at confusing cryptographic identifiers, and asking the natural question, “What exactly am I supposed to click now?”

Those are the right problems to have. They mean the board is no longer hypothetical. The next task is to make the experience worthy of the protocol underneath it.


Jonathan Brown for Border Cyber Group