<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[Werewolf Club]]></title><description><![CDATA[Werewolf Club]]></description><link>https://werewolfclub.hashnode.dev</link><image><url>https://cdn.hashnode.com/res/hashnode/image/upload/v1593680282896/kNC7E8IR4.png</url><title>Werewolf Club</title><link>https://werewolfclub.hashnode.dev</link></image><generator>RSS for Node</generator><lastBuildDate>Tue, 01 Sep 2026 13:03:44 GMT</lastBuildDate><atom:link href="https://werewolfclub.hashnode.dev/rss.xml" rel="self" type="application/rss+xml"/><language><![CDATA[en]]></language><ttl>60</ttl><item><title><![CDATA[Seven Invariants for Keeping an Eleven Player Browser Game in Sync]]></title><description><![CDATA[Disclosure: I founded Werewolf Club, the browser game used in this technical case study. Every player in the recorded session consented to the use of the gameplay recording, names, voices and images.
]]></description><link>https://werewolfclub.hashnode.dev/seven-invariants-for-keeping-an-eleven-player-browser-game-in-sync</link><guid isPermaLink="true">https://werewolfclub.hashnode.dev/seven-invariants-for-keeping-an-eleven-player-browser-game-in-sync</guid><category><![CDATA[Web Development]]></category><category><![CDATA[Game Development]]></category><category><![CDATA[distributed systems]]></category><category><![CDATA[multiplayer]]></category><category><![CDATA[TypeScript]]></category><dc:creator><![CDATA[Founder of Werewolf Club]]></dc:creator><pubDate>Thu, 27 Aug 2026 22:58:19 GMT</pubDate><content:encoded><![CDATA[<p>Disclosure: I founded Werewolf Club, the browser game used in this technical case study. Every player in the recorded session consented to the use of the gameplay recording, names, voices and images.</p>
<p>At 16:49 in a live Werewolf session, the interface reported that RS had lost connection during a nomination. The nomination panel showed that nine players were eligible to choose. Eleven seconds later, the interface reported that RS had reconnected. The same nomination remained open and seven of nine choices had been recorded.</p>
<p>The visible evidence is narrow but useful. The nomination did not restart when the connection changed. The room kept collecting choices in the same phase.</p>
<p>That is not a glamorous product moment, but it is a useful engineering result. Real time multiplayer software fails when the interface becomes the source of truth. A browser can be late, disconnected or stale. A media stream can recover independently of the game. A click can be retried after the server has already accepted it.</p>
<p>The session exposes seven invariants worth using when reviewing a multiplayer design. The recording proves the visible outcomes described below. The code examples are illustrative design sketches, not claims that the footage can prove an internal implementation.</p>
<h2>Invariant 1: the server owns the phase</h2>
<p>Every legal action depends on the active phase. A wolf may choose a target at night. A living player may nominate during nomination. An eligible voter may choose condemn or spare during judgment.</p>
<p>The browser can display those permissions, but it must not create them.</p>
<p>The authoritative check can be expressed as a small predicate:</p>
<pre><code class="language-ts">function canAcceptAction(room, player, action) {
  return action.roomId === room.id
    &amp;&amp; action.round === room.round
    &amp;&amp; action.phase === room.phase
    &amp;&amp; room.eligiblePlayerIds.includes(player.id)
    &amp;&amp; room.allowedActionTypes.includes(action.type)
    &amp;&amp; !room.acceptedActionByPlayerId.has(player.id)
}
</code></pre>
<p>The exact data model will vary. The important part is that room, round, phase, eligibility and prior acceptance are checked together. Hiding a button after a phase change improves the interface, but it does not make a late message invalid. Only the authoritative transition rule can do that.</p>
<h2>Invariant 2: a deadline is a fact, not a local countdown</h2>
<p>Eleven browsers will not update at precisely the same moment. Even if every clock is accurate, rendering and network delay will make countdowns drift.</p>
<p>A robust design can treat the phase deadline as a shared timestamp. Each client renders its own countdown from that value, while the server decides whether the phase is still open. A client may briefly show one second when another shows zero, but both submit to the same acceptance rule.</p>
<p>This also keeps narration separate from authority. Approved audio and matching captions explain what is happening. They do not decide when the room advances. Media playback can begin late or be suppressed by a browser. The phase deadline cannot depend on whether a particular device finished playing a sound.</p>
<p>The order is simple:</p>
<pre><code class="language-text">server deadline expires
state transition resolves
clients receive the new phase
interface presents the matching caption, audio and controls
</code></pre>
<p>If presentation drives state instead, each client can create its own timeline.</p>
<h2>Invariant 3: every player receives a valid projection</h2>
<p>Werewolf has one room state and several legitimate views of it.</p>
<p>All players can know the phase, deadline, living participants and public verdicts. A player can also know their own role and any private action currently available to that role. They must not receive another player's role or secret choice.</p>
<p>That suggests a projection boundary:</p>
<pre><code class="language-ts">function projectRoomFor(room, viewerId) {
  return {
    public: buildPublicSnapshot(room),
    private: buildPrivateSnapshot(room, viewerId),
  }
}
</code></pre>
<p>This is safer than sending the complete room state and asking the interface to hide sensitive fields. It also produces a clean reconnect contract. A returning player does not need a replay of every animation. They need a fresh public snapshot, their current private projection, the active deadline and whether their current action has already been accepted.</p>
<h2>Invariant 4: private inputs stay private until resolution</h2>
<p>In a judgment phase, visible partial results alter the decision being measured. If the first three voters publicly choose condemn, the fourth player is no longer answering only the question, “Do I believe the accused?” They are also reacting to the emerging majority.</p>
<p>A server can therefore accept each eligible vote privately and publish one result after the phase closes.</p>
<pre><code class="language-text">accept private choice
record one choice for that eligible player
wait for completion or deadline
resolve exactly once
publish the complete tally and verdict together
</code></pre>
<p>The second trial ended 4 condemn to 4 spare. The written tie rule resolved the verdict to spare, and the room received one shared result. The recording supports that visible outcome. It does not expose the internal storage path used to collect the choices.</p>
<p>Atomic revelation is not just visual polish. It preserves the intended game mechanic.</p>
<h2>Invariant 5: resolution is idempotent</h2>
<p>The most dramatic moments are common retry boundaries. A player taps as the timer closes. A socket reconnects after the server accepted a vote but before the browser received acknowledgement. Two workers notice that a completion condition is true.</p>
<p>The room must not resolve twice.</p>
<p>One practical design model is to give every phase instance a stable identity based on the room, round and phase occurrence. Resolution records that identity before the next state is broadcast.</p>
<pre><code class="language-ts">const resolutionKey = `${room.id}:${room.round}:${room.phaseInstance}`

if (await store.hasResolution(resolutionKey)) {
  return store.getResolution(resolutionKey)
}

const result = resolvePhase(room)
await store.commitResolution(resolutionKey, result)
return result
</code></pre>
<p>Production storage needs an atomic write or transaction around that check. The sample shows the contract, not a complete concurrency solution. Repeating the request should return the existing result or fail predictably. It should never condemn a player twice or run the win check twice.</p>
<h2>Invariant 6: media presence does not change game eligibility</h2>
<p>Usually only two to four of the eleven cameras were active at once in the recorded session. The conversation remained live, and the game completed.</p>
<p>That matters architecturally. Camera state belongs to the communication layer. Player life, role, voting eligibility and phase completion belong to the game layer. A camera turning off must not remove a player from a vote. A video tile reconnecting must not reinsert a player who was already eliminated.</p>
<p>The two systems can inform the interface without sharing authority. A connection indicator can help the group understand a delay. It should not silently rewrite game state.</p>
<p>The 16:49 evidence does not prove every recovery guarantee in this article. It does show one practical outcome: a visible disconnect and reconnect happened while the same nomination continued collecting choices.</p>
<h2>Invariant 7: every transition produces an explainable room</h2>
<p>A valid state is not enough. Players must understand why they are in it.</p>
<p>After a verdict, the room should show the complete tally, the spare or condemn result and any public role reveal required by the rules. After a win check, it should identify the winning side and reveal enough information for the group to reconstruct the game.</p>
<p>The session produced five verdict paths:</p>
<ol>
<li>A real wolf was spared 3 to 5.</li>
<li>The same wolf was spared on a 4 to 4 tie.</li>
<li>An innocent player was spared 3 to 4.</li>
<li>The wolf was finally condemned 4 to 2.</li>
<li>The village condemned the innocent player at the final trial, which handed the remaining wolves parity and the game.</li>
</ol>
<p>The visible procedure was consistent, but the social meaning changed every time. That is the desired boundary. Software should make the procedure dependable without deciding whom the players should trust.</p>
<h2>Test the invariants as properties</h2>
<p>Example based tests are useful, but invariants invite broader checks.</p>
<p>For any generated room state and action sequence, test that:</p>
<ol>
<li>An action for the wrong phase is never accepted.</li>
<li>An ineligible player never changes the phase result.</li>
<li>A player contributes at most one accepted input to a phase.</li>
<li>Private inputs never appear in another player's projection before resolution.</li>
<li>Resolving the same phase instance twice returns one public outcome.</li>
<li>A reconnect receives the current snapshot rather than an older phase.</li>
<li>Camera and microphone changes never alter game eligibility.</li>
<li>Every completed transition has one explainable public result.</li>
</ol>
<p>A full playtest then checks the human layer that properties cannot measure. Did eleven people understand when to act? Could they tell why a tie spared the accused? Did the final reveal explain why the game ended? Did a reconnect create confusion even though the stored state remained correct?</p>
<h2>Boring rules make room for surprising people</h2>
<p>The best sign that a multiplayer state model works is that players stop discussing the software and start arguing about each other.</p>
<p>In our session, the village accused the same real wolf three times. It spared him twice, caught him later, then condemned an innocent player and lost. A multiplayer design does not need to make that story interesting. It needs rules that preserve private information, accept each legal action once, reveal shared outcomes together and recover the current truth when a browser returns.</p>
<p>You can watch the consented gameplay cut here:</p>
<p><a class="embed-card" href="https://www.youtube.com/watch?v=psyGiV8dea0">https://www.youtube.com/watch?v=psyGiV8dea0</a></p>

<p><a href="https://www.werewolfclub.com/?utm_source=hashnode&amp;utm_medium=owned&amp;utm_campaign=seo_aeo_wave2_2026">Werewolf Club</a> is the browser based live video game used for this case study. It supports 4 to 16 friends. The built in moderator privately assigns roles, runs the night, guides discussion and voting, and presents approved narration and captions so everyone gets to play.</p>
<p>Closing disclosure: I founded Werewolf Club and have a commercial interest in the product discussed here.</p>
]]></content:encoded></item></channel></rss>