Skip to content

feat(planet): an account changes its flag once an hour - #423

Open
raphoester wants to merge 3 commits into
mainfrom
claude/country-change-rate-limit-e2b227
Open

raphoester wants to merge 3 commits into
mainfrom
claude/country-change-rate-limit-e2b227

Conversation

@raphoester

Copy link
Copy Markdown
Owner

What

An account may change the flag it plays for once an hour.

  • Server (planet/internal/allegiances): the flag each account plays for, and when it last changed it. The first flag starts no wait, and the first change is free, the way the first username is free: a player who clicked for its time zone's flag can still pick its own. After a change, a click or a shield for another flag within the hour is refused with FailedPrecondition and a planet.v1.Allegiance detail (the flag held and changeable_at_unix_ms).
  • Where it is checked: allegiance_click, between antibot_attempt_click and throttle_click, so a refused click spends no token, and a banned caller is refused exactly like any other (otherwise switching flags would tell it its clicks are dropped). place_shield_usecase checks before it spends the shield. Bombs, claims, quizzes and refills name a flag only for what they announce, and are not held.
  • Read: ClickService/GetAllegiance (identity token, from memory).
  • Kept like the charges: in memory, flushed every second to planet.allegiances (new migration). A row is never deleted, not even with its account: without the row the next flag is a first flag again, which would hand out a free change. The row names nobody once the account is gone.
  • Client: PlanetBackend reads the allegiance at load, on a new token and after a click for another flag. While the flag is held, the picked country snaps back to it (local storage, share link, another device), and the picker shows "You can play for another country at HH:MM" instead of the list. A refused click rolls back and plays the refusal sound, with no dialog. FakeBackend applies the same rule, and fakeBackend.unlockFlag() ends the wait in dev.

Why

Every click names a flag and nothing held a player to one: a player could paint for one country and defend another, click to click.

For the reviewer

  • The gate is a decorator in the click chain, beside throttle_click. I know decorators are meant for logging only. I kept it there because it has to sit outside the throttle and the shadow ban, and the click chain is where those gates live today. If you want it elsewhere, say where.
  • Guests are held per account: a private tab is a new guest, so a new first flag.
  • Deploy order: an old client that switches flags gets its clicks refused (and rolled back) until it reloads the new bundle. Shipping both together is enough.
  • Two e2e tests had one player click for three or four flags in a row; they now use one player per flag.

Tests

  • Backend: the rule, the book, both stores (postgres too), the click gate, the shield refusal, the handler's detail, the query, and an e2e test over HTTP. make test, make lint, make deadcode and make check-format pass.
  • Frontend: lockOf, the client's reads and refusal mapping, the globe's quiet refusal, the locked picker. 2043 tests, build and lint pass. Checked in the browser against the fake backend.

🤖 Generated with Claude Code

raphoester and others added 3 commits October 17, 2026 20:04
Every click and every shield names a flag, and nothing held a player to
one: a player could paint for one country and defend another, click to
click. The server now keeps the flag each account plays for. The first
flag starts no wait and the first change is free; after a change, a click
or a shield for another flag within the hour is refused with
FailedPrecondition and a planet.v1.Allegiance detail.

The check sits outside the throttle and the shadow ban, so a refused click
spends no token and a banned caller is refused like any other.
ClickService/GetAllegiance tells the client the flag held, and the picker
says when another country may be played.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-rate-limit-e2b227

# Conflicts:
#	apps/backend/CLAUDE.md
#	apps/backend/generated/proto/planet/v1/planet.pb.go
#	apps/backend/internal/planet/internal/planetv1controller/click_handler/errors.go
#	apps/backend/internal/planet/internal/planetv1controller/place_shield_handler/handler.go
#	apps/frontend/src/app/viewer/globe.test.ts
#	apps/frontend/src/app/viewer/globe.ts
#	apps/frontend/src/backends/fakeBackend.ts
#	apps/frontend/src/backends/planetBackend.test.ts
#	apps/frontend/src/backends/planetBackend.ts
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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