root 2b5bf86b0f fix: bump maxTokens again (800/500) and add conciseness instructions
Post-restart logs show model still hitting 600-token ceiling on verbose
responses — truncated JSON at exactly 599-600 output tokens. Average
successful response was ~418 tokens but outliers reach 600+. Added
explicit conciseness instructions to backstory prompt to reduce variance.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 18:42:56 +00:00

DFlike

A multiplayer browser game inspired by Dwarf Fortress. Watch autonomous NPCs with needs-driven AI wander, eat, rest, and socialize in a shared world. Each NPC has unique stats (physical and personality) that influence their behavior, needs, and social interactions. Connect as an observer or take control of an avatar.

Architecture

  • Server: Node.js + Socket.io, server-authoritative ECS simulation at 10 ticks/sec
  • Client: Phaser 3 + TypeScript, Vite bundler
  • Shared: TypeScript types and constants used by both server and client
dflike/
  shared/    -- Types and constants (no runtime code)
  server/    -- Node.js + Socket.io game server with ECS
  client/    -- Phaser 3 + TypeScript renderer
  chars/     -- Character sprite assets
  docs/      -- Design and implementation docs

Prerequisites

  • Node.js >= 20 (tested with v22)
  • npm >= 10

Quick Start (Development)

npm install

# Copy character assets into the client public directory
cp -r chars client/public/assets/

# Terminal 1: Start server (port 3001)
npm -w server run dev

# Terminal 2: Start client dev server (port 3000)
npm -w client run dev

Open http://localhost:3000. Open multiple tabs for multiplayer.

Controls

  • WASD / Arrow Keys -- pan camera (camera mode), move avatar (avatar mode), or cycle NPCs (follow mode)
  • TAB -- toggle between camera, avatar, and follow modes

Follow Mode

In follow mode, the camera tracks an NPC and a panel slides in from the right showing:

  • Composited character portrait (layered from skin + accessories + facial features)
  • NPC name (randomly generated fantasy names)
  • Current activity (Wandering, Eating, Resting, Socializing, Idle)
  • Live-updating needs bars (Hunger, Energy) with color-coded fill levels
  • NPC stats in a two-column grid (STR, DEX, CON, INT, PER / SOC, COU, CUR, EMP, TMP)
  • Relations tab showing relationships grouped by tier with slider visualization

The panel uses an EarthBound/SNES RPG-inspired aesthetic with doubled borders, pixel font, and smooth slide transitions.

Deployment on Debian LXC

These instructions cover running DFlike on a headless Debian (or Ubuntu) LXC container, serving the game on your local network.

1. System Setup

apt update && apt upgrade -y
apt install -y curl git

2. Install Node.js

Install Node.js 22.x via NodeSource:

curl -fsSL https://deb.nodesource.com/setup_22.x | bash -
apt install -y nodejs
node --version  # should print v22.x.x

3. Clone and Install

cd /opt
git clone <your-repo-url> dflike
cd dflike
npm install

4. Copy Character Assets

The character sprite assets in chars/ need to be copied into the client's public directory. They are excluded from the client build via .gitignore.

cp -r chars client/public/assets/

5. Build the Client

By default the client connects to the game server on the same hostname it was loaded from (port 3001). To override, set the VITE_SERVER_URL environment variable before building:

# Default: auto-detects from browser hostname (works for most setups)
npm -w client run build

# Or specify an explicit server URL
VITE_SERVER_URL=http://192.168.1.50:3001 npm -w client run build

For development, you can also create a client/.env.local file:

VITE_SERVER_URL=http://192.168.1.50:3001

6. Serve the Client Build

Install a simple static file server to serve the built client:

npm install -g serve

Or use nginx (recommended for production):

apt install -y nginx

nginx config (/etc/nginx/sites-available/dflike):

server {
    listen 80;
    server_name _;

    root /opt/dflike/client/dist;
    index index.html;

    location / {
        try_files $uri $uri/ /index.html;
    }
}

Enable and restart:

ln -s /etc/nginx/sites-available/dflike /etc/nginx/sites-enabled/
rm -f /etc/nginx/sites-enabled/default
nginx -t && systemctl restart nginx

7. Run the Game Server

Quick start:

cd /opt/dflike
npm -w server run start

The server listens on port 3001 by default. Override with the PORT environment variable:

PORT=4000 npm -w server run start

8. Run as a systemd Service

Create /etc/systemd/system/dflike.service:

[Unit]
Description=DFlike Game Server
After=network.target

[Service]
Type=simple
WorkingDirectory=/opt/dflike
ExecStart=/usr/bin/npx tsx server/src/main.ts
Restart=on-failure
RestartSec=5
Environment=PORT=3001
Environment=NODE_ENV=production

[Install]
WantedBy=multi-user.target

Enable and start:

systemctl daemon-reload
systemctl enable dflike
systemctl start dflike
systemctl status dflike   # verify it's running
journalctl -u dflike -f   # view logs

9. Firewall

If you have a firewall enabled, open the necessary ports:

# Game server (Socket.io)
ufw allow 3001/tcp

# Web server (nginx)
ufw allow 80/tcp

Deploying with Traefik (Reverse Proxy)

If you're exposing the game through Traefik on a domain (e.g. game.example.com), you need two routes: one for the static client and one for the Socket.io game server.

Traefik dynamic config (e.g. game.yaml):

http:
  routers:
    game-ws:
      rule: "Host(`game.example.com`) && PathPrefix(`/socket.io`)"
      entryPoints:
        - websecure
      service: game-ws
      tls:
        certResolver: letsencrypt
    game:
      rule: "Host(`game.example.com`)"
      entryPoints:
        - websecure
      service: game
      tls:
        certResolver: letsencrypt
  services:
    game:
      loadBalancer:
        servers:
          - url: "http://<LXC_IP>:3000"
    game-ws:
      loadBalancer:
        servers:
          - url: "http://<LXC_IP>:3001"

Set VITE_SERVER_URL to the public domain so Socket.io connects through the proxy:

# In client/.env.local
VITE_SERVER_URL=https://game.example.com

Security note: Do not expose vite dev to the internet. Use vite preview or serve client/dist/ with a static file server (nginx, serve, etc.) for production.

Summary of Ports

Service Port Protocol
Game server 3001 TCP
Client (dev) 3000 TCP
Client (prod) 80 TCP

NPC Stats

Each NPC is generated with 10 stats using 3d6 rolls (range 3-18, bell curve centered at ~10.5):

Physical Personality
Strength -- physical tasks Sociability -- social initiation, cooldown
Dexterity -- movement, precision Courage -- exploration, threat response
Constitution -- needs decay rate Curiosity -- wander variety, distance
Intelligence -- learning/drift rate Empathy -- social outcome bias
Perception -- awareness radius Temperament -- emotional stability

Stats influence gameplay systems:

  • Constitution scales hunger/energy decay rates
  • Perception extends or shrinks the social awareness radius
  • Sociability reduces social interaction cooldowns
  • Empathy biases social interactions toward positive outcomes
  • Courage influences willingness to interact with disliked NPCs
  • Curiosity extends awareness radius for NPCs not yet encountered

Stats also have transient modifiers (e.g., low hunger debuffs Intelligence) and baseline drift (tiny permanent shifts from experience over time).

Relationships

NPCs form relationships through social interactions. Each relationship is tracked independently per NPC (asymmetric — A's view of B can differ from B's view of A).

How it works:

  • Relationships are lazy-initialized on first encounter (NPCs don't "know" each other until they interact)
  • Each interaction updates relationship values (-100 to 100) based on outcome, with diminishing returns on repeated interactions
  • Deltas are blended asymmetrically: 30% shared outcome + 70% individual perception
  • Stats influence relationships: Empathy boosts positive gains, Temperament amplifies negative losses, Sociability adds a general bonus and determines friend capacity

Classification tiers (derived from value):

Range Tier
80 to 100 Partner
50 to 79 Close Friend
20 to 49 Friend
5 to 19 Acquaintance
-4 to 4 Stranger
-19 to -5 Wary
-49 to -20 Rival
-79 to -50 Enemy
-100 to -80 Nemesis

Caps: Partner cap defaults to 1 (2 for high sociability + empathy NPCs). Friend cap scales with sociability.

Despawn behavior: Strong relationships (|value| >= 20) persist as memories when an NPC is removed. Weaker relationships fade toward zero and are cleaned up.

All relationship tuning values are config-driven in server/src/config/relationshipConfig.ts, designed for future admin UI integration.

Game Constants

All tunable game constants live in shared/src/constants.ts:

Constant Default Purpose
MAX_NPC_COUNT 50 Maximum NPCs in the world
TICK_RATE 10 Server ticks per second
WORLD_WIDTH / WORLD_HEIGHT 64 World size in tiles
MOVE_SPEED 0.75 NPC movement speed (tiles/tick)
HUNGER_DECAY_PER_TICK 0.05 Hunger drain rate
ENERGY_DECAY_PER_TICK 0.03 Energy drain rate
HUNGER_THRESHOLD 30 Below this, NPC seeks food
ENERGY_THRESHOLD 20 Below this, NPC seeks rest
AWARENESS_RADIUS 5 Base social detection range (tiles)
SOCIAL_GLOBAL_COOLDOWN 50 Ticks between social interactions
SOCIAL_PAIR_COOLDOWN 300 Ticks before same pair can interact again

Development Commands

Command Description
npm install Install all workspace dependencies
npm -w server run dev Start server with hot reload
npm -w server run start Start server (production)
npm -w server run test Run server tests
npm -w server run test:watch Run server tests in watch mode
npm -w client run dev Start client dev server
npm -w client run build Build client for production
npx -w shared tsc Rebuild shared types
Description
Multiplayer NPC simulation game (Dwarf Fortress-inspired)
Readme 5.4 MiB
Languages
TypeScript 99.9%