Perk store, streak bonus, and a shorter promo code - #173
Merged
Conversation
…y day Closes #156. A streak has to be remembered somewhere, and a perk had nowhere to remember anything: the two that exist either compute from the length they are given or borrow another feature's table. Rather than bolt two columns onto Dicks for one perk, migration 41 gives every perk a place of its own — Perks(id, name) and Perk_States(chat_id, uid, perk_id, state jsonb), whose blob only its owner understands. Adding a perk now costs a row in Perks rather than a migration. The names are resolved to ids once, at startup, in a single statement. A perk does not write. apply() returns its new state beside its change, and create_or_grow writes the length and every blob in one transaction. That ordering is the point: the once-a-day rule is a trigger that raises GD0E1 after the perks have run, and a rolled-back growth must leave no perk believing it happened. LoanPayoutPerk predates this and still pays inside apply. Two things a perk is handed rather than fetching itself: ChangeSource, because a Dick of the Day award shares the pipeline but is not a day of playing, and today, which is the database's current_date — the same calendar the daily trigger compares against. The streak is the first user of it. It stores {streak, max, last_grow} and multiplies the base increment by STREAK_BONUS_RATIO_PER_DAY for each consecutive day, up to STREAK_BONUS_MAX_DAYS; a shrink is multiplied too. Unlike help-pussies it is on by default, so it works without a change in production. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ReYjiZno4se1p66mFvK6Bc
КОТ is a fine promo code and the bot would not take it. The minimum was stated in six places, and the one that actually refuses a code is the `promo_code_format` constraint of migration 18, tightened in 23 — the promo-manager writes into this database directly, so that check is what a code meets first. Migration 42 loosens it to three; every code already stored satisfies the looser rule, so the constraint can be re-added without a scan failing. The rest is the same number written down elsewhere: PROMO_CODE_MIN_LENGTH, the `error_message` of PromoCode, and `commands.promo.errors.invalid_format` in all six locales, which tells the user the range out loud. A repository test now creates a three-character code, so the type and the column cannot drift apart again without something going red. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ReYjiZno4se1p66mFvK6Bc
Two things about how the streak shows up:
`/stats` put both numbers of "Days in a row" on one line; splitting them,
the way the "Length" line above it already does, reads better once the
second one is a full sentence of its own rather than a parenthetical.
"The following perks affected the result" named the perk but not why its
share was what it was — a "+3" from "day after day" said nothing about the
streak behind it. `PerkOutcome` gains a `note` field a perk may set beside
its state, threaded through a new `lang_code` on `PerkContext` so the note
can be localized where it's built rather than guessed at render time.
`StreakPerk` fills it with the streak length itself, so the line reads
"day after day (+3, 5 days in a row)".
Both the note and the perk's own name are `PerkName`-keyed values a perk
hands back, so the note gets the same kind of wrapper: `PerkNote`. And
`Increment` keeps one map instead of two kept in step by hand — `by_perks`
now holds a `PerkEffect { change, note }` per perk rather than a bare change
in one map and its note in a second.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01ReYjiZno4se1p66mFvK6Bc
Perks.id is a smallserial, and nextval() builds that default before ON CONFLICT gets a look at it — so the INSERT ... ON CONFLICT DO UPDATE register_all ran on every start spent a sequence value on every already- registered perk, every single restart, whether or not the row changed. Three perks and enough restarts is how one of them ended up at id 23. EXCEPT keeps an already-known name out of the INSERT's row source entirely, so a restart that adds nothing now costs nothing. ON CONFLICT DO NOTHING stays as a safety net for two instances racing to register the same brand-new name — the loser's attempt is discarded rather than erroring — and the second statement is what keeps that safe: it reads every requested name back from the table itself, so the loser still gets the winner's id instead of coming back short a perk. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ReYjiZno4se1p66mFvK6Bc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Perks/Perk_States, migration 41) so a new perk needs no schema change, only a registration entry — registered once at startup in a single statement insideIncrementor::new.STREAK_BONUS_RATIO_PER_DAY/STREAK_BONUS_MAX_DAYS, on by default.CHECKconstraint had been missed when the Rust-side validator was first written and was still enforcing the old minimum.Test plan
cargo build && cargo clippy --tests && cargo test(296 tests passing locally)cargo sqlx migrate runapplied against a fresh DB, including migrations 41 and 42/growon a backdated row shows the streak line,/statsshows the longest streak,/helprenders the new sentence in every languageALTER TABLE promo_codesSQL is run) before relying on 3-character promo codes there🤖 Generated with Claude Code
https://claude.ai/code/session_01ReYjiZno4se1p66mFvK6Bc