Skip to content

Login streak — the last empty slot in the daily layer (§5.3) - #210

Merged
0xward merged 1 commit into
mainfrom
claude/nullstate-onchain-text-q6g2a3
Jul 31, 2026
Merged

Login streak — the last empty slot in the daily layer (§5.3)#210
0xward merged 1 commit into
mainfrom
claude/nullstate-onchain-text-q6g2a3

Conversation

@0xward

@0xward 0xward commented Jul 31, 2026

Copy link
Copy Markdown
Owner

Fills the final [TARGET] in layer 2. §5.3 flips to [TODAY], and the four-layer summary in §3 no longer says the daily layer is empty.

No smart contract, ABI, or contract address is touched.

Why this is separate from Daily Contracts

Contracts answer why play today. They do not answer why open this at all today, and those are different questions.

A player with ten spare minutes plays. A player with one spare minute opens the app or does not — and if they do not, what breaks is the habit, not the session.

Half of this already existed as the Season Pass daily claim (+1 energy, +3 t1 shards per UTC day) — a login reward that was never framed as one, and only for pass holders. This is the version everyone gets, guests included: a guest id has the same 20-byte shape as a wallet and keys the same records, and a player who has not connected a wallet yet is exactly the player a retention mechanic is for.

The ladder

Day Pays
1 80 NullState Point
2 2 Glitch Shards (t1)
3 +1 energy
4 3 Glitch Shards (t1)
5 150 NullState Point
6 4 Glitch Shards (t1)
7 8 Glitch Shards (t1)

Every rung is t1 shards, energy or Point on purpose. Shard tier is act-gated — _shardTierForAct() drops t1 on acts 1–2, t2 on 3–4, t3 on act 5 — so paying a t2 shard would hand a new player a currency they cannot spend and did not earn.

Day 7 is 8 t1 shards because EVOLUTION_SHARD_COSTS[0] is 8: a full week is worth exactly one weapon evolution. That is the ratchet §4 asks for in so many words — "the weapon is tier 3, so next week is faster" — and it is a prize a player can name, which a scattering of shards is not.

Sized against Daily Contracts (200–400 Point or 2–4 t1 for real work): a whole week of merely opening the app is worth roughly one day of playing it. Showing up should be rewarded; it must never out-earn showing up and playing.

Trust

The day is the server's UTC day, the streak is derived from the stored last day rather than sent, and the whole decision — continuation, restart, or repeat? — happens inside one transaction, so two tabs cannot both advance it or both be paid.

A grant that throws still counts the day. A re-claimable day is worse than one missed grant; it is logged loudly because it is the only outcome here that costs the player something.

UI

No claim step, matching the decision Daily Contracts already made — opening the app is the event. The chip sits first on the daily bar because it is the one number there the player can lose, and loss aversion only works when the thing at risk is in front of them. A streak of 0 renders nothing rather than a "0" nobody can read.

The reward is announced once, on the visit that earned it, as a dismissible line above the bar — deliberately not a modal, so it never stands between the player and the ENTER button they came for.

Also: fixes a flaky test I shipped in #209

test-seeded-floors.js failed about one run in four on "the opened container stays open". A floor averages 1.6 lockable containers, so a fixed depth sometimes generated none and the container assertions had nothing to act on.

It now retries the mount until the floor has one. Probing and remounting does not work — every fresh mount draws a new RUN_SEED, so the probe's floor is not the floor the next mount builds. That subtlety is why the first fix for this flake failed. Stable across five consecutive runs.

Verification

Check Result
npm run test:streak (new) 31/31 — day boundaries against a stubbed RTDB, since they cannot be tested in a browser without waiting for midnight
npm run test:streak-ui (new) 16/16 in a real browser — POST on mount, chip first, announcement shown once and dismissible, day 7 styled as the payoff, streak 0 renders nothing
npm run test:seeded 15/15, five runs in a row
npm run test:raid 17/17
npm run test:contracts 13/13
npm run test:floors all passed
npm run test:inventory 7/7
npm run test:traversable 0 failures
tsc --noEmit clean
lint 34 errors — unchanged baseline
audit · check:copy · check:market · check:cssvars · check:attribution all pass
build succeeds

Docs: GAME-DESIGN.md §5.3 rewritten as shipped with the reasoning behind each rung, §3 and the build order updated, and game-mechanics.md gains a player-facing table.


Generated by Claude Code

Daily Contracts answer "why play today". They do not answer "why open this
at all today", and those are different questions. A player with ten spare
minutes plays; a player with one spare minute opens the app or does not —
and if they do not, what breaks is the habit, not the session.

Seven escalating days, breaking it resets to 1. Half of this already
existed as the Season Pass daily claim (+1 energy, +3 t1 shards per UTC
day) — a login reward that was never framed as one, and only for pass
holders. This is the version everyone gets, guests included: a guest id has
the same 20-byte shape as a wallet and keys the same records, and a player
who has not connected a wallet yet is exactly the player a retention
mechanic is for.

THE LADDER, AND WHY IT IS SHAPED LIKE THIS

  1: 80 Point   2: 2 t1   3: +1 energy   4: 3 t1
  5: 150 Point  6: 4 t1   7: 8 t1

Every rung is t1 shards, energy or Point on purpose. Shard TIER is
act-gated — _shardTierForAct() drops t1 on acts 1-2, t2 on 3-4, t3 on act
5 — so paying a t2 shard would hand a new player a currency they cannot
spend and did not earn.

Day 7 is 8 t1 shards because EVOLUTION_SHARD_COSTS[0] is 8: a full week is
worth exactly ONE weapon evolution. That is the ratchet §4 asks for in so
many words ("the weapon is tier 3, so next week is faster"), and it is a
prize a player can name, which a scattering of shards is not.

Sized against Daily Contracts (200-400 Point or 2-4 t1 for real work): a
whole week of merely opening the app is worth roughly one day of playing
it. Showing up should be rewarded; it must never out-earn showing up AND
playing.

TRUST

The day is the server's UTC day, the streak is derived from the stored last
day rather than sent, and the whole decision (continuation, restart, or
repeat?) happens inside ONE transaction — so two tabs cannot both advance
it or both be paid. A grant that throws still counts the day: a
re-claimable day is worse than one missed grant, and it is logged.

No claim step, matching the decision Daily Contracts already made — opening
the app IS the event. The chip sits first on the bar because it is the one
number there the player can lose, and loss aversion only works when the
thing at risk is in front of them. A streak of 0 renders nothing rather
than a "0" nobody can read.

ALSO: FIXES A FLAKY TEST I SHIPPED

test-seeded-floors.js failed about one run in four on "the opened container
stays open". A floor averages 1.6 lockable containers, so a fixed depth
sometimes generated none and the container assertions had nothing to act
on. It now retries the MOUNT until the floor has one — probing and
remounting does not work, because every fresh mount draws a new RUN_SEED,
so the probe's floor is not the floor the next mount builds. Stable across
five consecutive runs.

npm run test:streak      31 assertions against a stubbed RTDB (day
                         boundaries cannot be tested in a browser without
                         waiting for midnight)
npm run test:streak-ui   16 assertions in a real browser

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017A764RdnwpyWnG7uCNhMiQ
@chatgpt-codex-connector

Copy link
Copy Markdown

You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard.

@vercel

vercel Bot commented Jul 31, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
nullstate Ready Ready Preview Jul 31, 2026 6:25am

@0xward
0xward merged commit b30d2c6 into main Jul 31, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant