10/29 2026
394
Series: "Writing High-Quality Code with AI: Development Record of AI Werewolf" This article corresponds to version: v1.2 → v1.3 | Date: Noon, October 22, 2026
Preface
In the previous article, I removed the 'fake AI,' and for the first time, every sentence in the first version was genuinely generated by the model. I was quite proud and sent the link to myself, planning to 'observe' the game in another window.
Then I discovered something more serious: I could see everyone's identities and even change my own card in the console.
The issue was that not a single line of code in this version was wrong—it was just all running where it shouldn't have been. This article discusses exactly that: why 'correctly written code' can still lead to cheating, and what the real differences are between JS and Node.js.
Introduction: Three Windows, Three Ways to Cheat
I opened two windows first: one for playing and the other for observing. The observer window could see the identities of all six seats because the state was naturally shared within the same browser.
Then I pressed F12. Three paths, each easier than the last:
Refresh the page, and progress is lost. Since the entire game state was stored in memory, refreshing the page reset everything. Modify the memory to win at will. Operations like game.players[0].role = 'wolf' in the console were like the hand of God for 'rules running in the browser.' Check the network panel, and all prompts are visible. Every AI prompt, call parameter, and request was clearly listed.
The third point was the most critical: if the prompts were assembled in the browser, then 'what the AI should know' was determined by the player.

Figure: The 'Event Log' panel in v1.2.1. After refactoring, all content on this screen is replayed from the server's event stream—before refactoring, the same information was compiled by the browser itself.
Problem: Rules Run on 'Player's Territory'
In short: the browser is the player's territory, and the server is the referee.
Anything that 'should not be modifiable'—identity assignments, wolf kill results, vote counts, victory determinations—if calculated in the browser, is equivalent to handing the referee's seat to the opponent. They can't modify your server, but they can modify their own browser.
While this may seem like mere 'self-cheating' in a single-player game, it comes with two real costs:
Progress loss on refresh: State is in memory and disappears on page reload. Exposure of AI calls: Model calls are sent directly from the browser, meaning prompts, parameters, and call frequencies are all in the player's hands.
Thus, the goal of refactoring was not just to 'add a backend' but to move the 'truth' from the browser to the server.
Root Cause: What's the Real Difference Between JS and Node.js?
Figure: A dividing line. On the left is everything the player can modify—cards in memory, request parameters, prompts in the network panel; on the right is only one thing: the truth calculated by the server. The judgment criterion is simple—can this rule be modified by the player? If yes, it must be on the right.
Many people (including myself at the time) think of 'Node.js as server-side JavaScript' as one thing, but it's actually three things mixed together:
Language: JavaScript, with identical syntax. Runtime: Who is executing this code—the browser's built-in engine or the Node program on your computer? Accessible resources: Is there a DOM? Can files be read/written? Can ports be listened to?
The same function sum(a, b) { return a + b } will produce the same result on both sides. But once 'environment' is involved, differences immediately emerge:
DimensionJS in BrowserNode.jsWho is executingBrowser JS engineLocal Node runtimeIs there a DOM?Yes (document, window)No, accessing document throws an errorGlobal objectwindowglobalCan read/write files?No (sandboxed)Yes (fs module)Can listen to ports?NoYes (http.createServer)Module systemHandled uniformly by bundlersESM (import) / CJS (require)Dependency management—npm + package.json + node_modulesFinal outputBundled static resources (dist/)Runnable .js files (dist-server/)
Take the smallest example. The same logic, almost identical syntax on both sides, but the way 'data is fetched' differs vastly:
// In the browser: Data comes from the page or API const players = JSON.parse(localStorage.getItem('players') || '[]'); // In Node.js: Data comes from the server's own memory import { readFile } from 'node:fs/promises'; const players = JSON.parse(await readFile('./data/players.json', 'utf8'));
Solution: Define Protocols First, Then Move Logic
This time, I didn't just say, 'Help me rewrite this for the server-side.' That approach would let the AI rewrite all files at once, and you wouldn't know where things went wrong.
My request was structured like this:
Background: 1. The web version of Werewolf currently writes rules, random numbers, and victory conditions in the front-end `assets/ai.js`. Goals: 1. Move the entire game logic to the server-side, with the front-end only handling rendering and interaction. 2. Behavior must remain identical (this is a refactor, not a rewrite). Process: 1. First, define the API protocols: list every interface the front-end will call (methods, paths, request bodies, response bodies), without implementing the code. 2. After I confirm the protocols, you move the logic according to them. Requirements: 1. Behavior must remain exactly the same; no 'convenient' optimizations allowed. 2. If unsure where something belongs or if external behavior needs changing, list it for me to decide—don't decide yourself.
Analysis: There are three 'gates' in this approach—
'Define protocols first, don't touch implementation' → Split one large change into two smaller ones for easier error localization. 'Behavior must remain identical' → Prohibit the AI from 'convenient' optimizations (its favorite thing). 'Ask me first if unsure' → Keep decision-making power in my hands.
The result is this directory structure:

Figure: Two clusters after refactoring. On the left are rules and state machines; on the right is only rendering. The dashed line in the middle is the entire interface—one request in, a series of events out.
Another key design: The front-end no longer 'calculates,' only 'displays.' Every player action becomes a request, and the server calculates the new state before pushing it back:
POST /api/games/:id/action { value: 3 } ← I vote for player 3 GET /api/games/:id/events?since=42 ← Fetch new events since event 42
Using polling instead of WebSocket was a deliberate shortcut: the cloud database lacks subscription capabilities, and a game generates at most a few events per second. Polling with a 'since' cursor is sufficient and cuts the code volume in half.
Outcome: All Three Cheating Paths Were Blocked
After deploying the changes, I replayed the three original cheating methods—all were blocked:
How I cheated beforeWhat happens nowRefresh the page, check if progress remainsProgress is preserved—state is on the server, so opening it in another tab continues the gameModify players' cards in the consoleCan't be done—front-end receives only a projection; modifying your own copy doesn't affect the refereeView prompts in the network panelCan't see them—prompts are assembled on the server; the browser only receives text
Two bonus features were also gained: reconnection after disconnection and multi-end synchronization. Because 'state is on the server,' observing in the 9th article was almost a free addition.
Content Summary
Refactoring should be split into two steps: 'define protocols first, then move implementation,' and explicitly prohibit the AI from 'convenient' optimizations. One large change is easier to lose control of than ten small ones. Moving to the server-side doesn't automatically mean security; security depends on whether each interface validates identity and permissions. When 'code looks correct but doesn't work,' print real data first—don't keep guessing. Such issues often lie not in the code but in layers you're unaware of. Is this transferable? This is a necessary path for all AI programming projects: for any application requiring 'authoritative state' (accounting, approvals, quizzes, ordering), rules must reside on the server-side.
References
