Skip to content

Repository files navigation

The Honest Idle Game

An idle game that dissects its own manipulation mechanics while they are working on you.

Auf Deutsch: README.de.md — das Spiel selbst ist zweisprachig.

The whole game sits in index.html — embedded CSS, vanilla JavaScript, no frameworks, no build tools, no external dependencies, no network requests. Double-click the file and it runs; a server is not required.

What is in it

Layer 1 — the idle game. A factory for attention, resource: impulses. A click button, six generators (Stimulus, Habit, Loop, Trigger, Ritual, System), prestige via synapses, offline progress up to eight hours.

Layer 2 — the leaflet. Thirty-one mechanics work on you first and get dissected afterwards.

Fundamentals: number rush, oven-glance (Zeigarnik), variable reward (Skinner), loss timer (Kahneman/Tversky), streak (commitment), FOMO event (scarcity), sunk cost.

Reward and chance: near-misses with a faked rate, a loss dressed up as a win, a machine with a pity counter, ratio schedule instead of interval schedule, a hit rate quietly adjusted behind your back.

Time and rhythm: energy with return appointments, login calendar with a reset, season with an expiry date, artificial waiting with a speed-up button, overflowing buffer.

Progress and ownership: pre-stamped loyalty card, goal gradient in the bars, a catalogue with gaps, achievement list, a plant you name yourself (IKEA effect).

Social: invented fellow players, a parasocial character, a neighbour with a constant lead, leaderboard on a rubber band.

Attention and money: premium currency, exaggerated feedback, blinking window title, endless history with no stopping cue, artificial maintenance outages (need frustration).

Every leaflet names its source, shows the player's real statistics and stays readable in the archive. Where the evidence is thin — the Zeigarnik memory advantage, the 66-day rule, phantom vibration, the parasocial purchase effect, screen time and wellbeing — the leaflet says so explicitly.

Invented numbers come from a deterministic pseudo-random generator, so the glass house panel can recompute them instead of merely asserting them. It even predicts what the next pull on the machine will yield, and names the actual hit rate while the gift button still displays the nominal one.

Clarity is the meta-currency for understanding. You do not buy production with it; you switch parts of the game off — up to glass-box mode and the closing credits.

Language

The game exists in full in German and English — interface, all 31 leaflets including sources, store, mirror report, messages and window title. The starting language follows the browser language, the switch sits at the top right, and the choice is stored in the save. Number and date formats change with it (decimal comma against decimal point).

Technically the German original remains the template: the EN object is laid over it. Where an entry is missing there, the German text still appears instead of leaving a gap.

Data

Everything lives exclusively in the browser's localStorage. There is no server and no request to the outside. The Mirror tab evaluates this local data; "delete save" removes it completely.

index.html?debug=1 blendet ein Dev-Panel ein: Zeitraffer ×60, Sprung um einen Tag, Ressourcen-Cheats und ein manueller Auslöser für jede der 31 Mechaniken. Ohne den Parameter ist davon nichts sichtbar.

Development and testing

index.html?debug=1 reveals a dev panel: time-lapse ×60, jump one day forward, resource cheats and a manual trigger for each of the seven mechanics. Without the parameter none of it is visible.

Legal notice

impressum.html carries the details required under § 5 ECG / § 25 MedienG, laid out as on lichtenberger.dev. It is linked from the game's bottom bar and from the article; in the container it is additionally reachable without an extension at /impressum. The link in the game is relative so that it also works when the file is opened by double-click.

Container and deployment

For running on a server a container is included: nginx on Alpine, containing only index.html, the legal notice and the article. The process runs as user nginx on port 8080, the filesystem is read-only (read_only), only /tmp is writable, all capabilities are dropped.

The delivered Content-Security-Policy allows 'unsafe-inline' for script and style — both are in the file, after all — plus data: for the favicon drawn via canvas, and nothing else. connect-src 'none' means: no fetch, no XHR, no WebSocket. That turns the game's claim that no byte leaves the device from a promise in the footer into a rule the browser enforces.

Routing via Traefik. docker-compose.yml publishes no port; it joins the host's existing Traefik network and describes itself through labels. One router serves both domains equally, www. is redirected per domain to the bare address, HSTS sits at the TLS termination.

To try it without Traefik the bare container is enough:

docker build -t honest-idle-game .
docker run --rm -p 8080:8080 honest-idle-game
# http://localhost:8080  and  http://localhost:8080/healthz

/healthz answers with ok and is also used by the image's HEALTHCHECK.

Delivery via GitHub Actions

The same host and the same Traefik as lichtenberger.dev.

.github/workflows/deploy.yml runs on every push to main and on manual dispatch:

  1. build — builds the image and puts it in GHCR (ghcr.io/d1gl3/the-honest-idle-game, tags latest and sha-<commit>). The built-in GITHUB_TOKEN is enough for that.
  2. deploy — copies docker-compose.yml to the host over SSH, logs in to GHCR there, docker compose pull, up -d. It then waits until the container reports healthy; if that does not happen the run fails and shows the last log lines.

As long as DEPLOY_SSH_KEY is missing the second step is skipped cleanly instead of failing red — the image still lands in GHCR.

.github/workflows/ci.yml runs on all other branches and in pull requests: build the image, start the container, check every path (game, legal notice, article, images, /healthz, 404), verify the security headers — explicitly including connect-src 'none', so the game's promise cannot fall out of the delivery unnoticed — and confirm the process is not running as root.

One-time setup

1. DNS — point all four names at the host:

honest-idle-game.de.       A   217.154.81.233
www.honest-idle-game.de.   A   217.154.81.233
honest-idle-game.com.      A   217.154.81.233
www.honest-idle-game.com.  A   217.154.81.233

2. Host — create the directory for the service; the user deploy and the network traefik already exist from lichtenberger.dev:

sudo -u deploy mkdir -p /home/deploy/the-honest-idle-game

The directory name has to match nothing — it ends up in DEPLOY_PATH and is needed nowhere else. On the existing host it is still called the-honnest-idle-game, from before the repository was renamed; docs/OPEN-SOURCE.md explains how to move it safely, or why you can leave it alone.

3. Secrets and variables — under Settings → Secrets and variables → Actions. The existing deploy key can be reused; it merely has to be stored once more in this repository, because secrets apply per repository:

Name Kind Value
DEPLOY_SSH_KEY Secret private SSH key of the user deploy
DEPLOY_HOST Secret or variable address of the target host
DEPLOY_USER Secret or variable deploy
DEPLOY_PATH Secret or variable /home/deploy/the-honest-idle-game
DEPLOY_PORT Secret or variable (optional) SSH port, if not 22

All of it applies per repository. If one of the four mandatory fields is missing, the workflow skips the deploy with a notice naming the missing one — the image still lands in GHCR. If sshd on the host is not on 22, DEPLOY_PORT has to be set here specifically, even if another repository already delivers to the same host successfully — otherwise scp aborts with dial tcp …:22: i/o timeout. The workflow therefore checks reachability up front and says so clearly instead of running into a timeout.

The four are read as ${{ secrets.X || vars.X }}; a secret that is set wins. As a secret they are masked in the Actions logs, as a variable they stand there in clear text — and the logs of a public repository can be read by anyone. Which value belongs where, and why DEPLOY_USER should specifically not be a secret, is in docs/OPEN-SOURCE.md.

Either way they are gone from the file, so that a public repository does not ship a finished map of the server and a fork does not accidentally run against someone else's infrastructure.

The GHCR package is pulled during deploy with the run's GITHUB_TOKEN and therefore works even while it is private.

Article

blog/the-honest-idle-game.html describes the project from a media-psychology perspective: all 31 effects explained briefly, the fact-check of the sources and a verified bibliography. It is in German.

Support

https://buymeacoffee.com/d1gl3

Voluntary and without consequence. There is no currency for it in the game, no advantage, no badge and no mention in the credits — a game that dissects monetisation should not introduce it through the back door. The link sits in the bottom bar next to the legal notice and the source code, deliberately quiet: no graphic, no blinking, no appearance after the tenth minute.

It also changes nothing about the game's promise. A link sends nothing; only the click opens a new page. Referrer-Policy: no-referrer ensures buymeacoffee.com does not learn where the visit came from, and the CSP stays untouched — connect-src 'none' still applies, no badge and no script is loaded from there.

Open source

Licence: MIT. Rules for contributions: CONTRIBUTING.md. Security reports: SECURITY.md.

What has to be configured in the repository for opening it to be safe is in docs/OPEN-SOURCE.md — visibility, default branch, token permissions, fork pull requests, secret scanning. That document is in German.

About

An idle game that dissects its own manipulation mechanics while they work on you. 31 dark patterns, each with a sourced leaflet. One HTML file, no server, no tracking, no byte leaves your device.

Topics

Resources

Contributing

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Contributors

Languages