Skip to content

Perk store, streak bonus, and a shorter promo code - #173

Merged
kozalosev merged 5 commits into
mainfrom
feature/156-bonus-for-subsequent-growths
Aug 24, 2026
Merged

Perk store, streak bonus, and a shorter promo code#173
kozalosev merged 5 commits into
mainfrom
feature/156-bonus-for-subsequent-growths

Conversation

@kozalosev

@kozalosev kozalosev commented Aug 24, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds a generic, reusable state store for perks (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 inside Incrementor::new.
  • Adds the streak-bonus perk (Bonus for growing a dick several days in a row #156): a multiplier on the base increment (applied to shrinks too) that grows with consecutive days played, configurable via STREAK_BONUS_RATIO_PER_DAY / STREAK_BONUS_MAX_DAYS, on by default.
  • Perk state is written inside the same transaction as the length change, so a growth refused by the once-a-day trigger rolls its perk state back too.
  • Shortens the minimum promo code length from 4 to 3 characters (migration 42), fixing a real refusal in production for the 3-character code "КОТ" — the DB-side CHECK constraint 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 run applied against a fresh DB, including migrations 41 and 42
  • Manually: /grow on a backdated row shows the streak line, /stats shows the longest streak, /help renders the new sentence in every language
  • Confirm migration 42 is applied to production (or the manual ALTER TABLE promo_codes SQL is run) before relying on 3-character promo codes there

🤖 Generated with Claude Code

https://claude.ai/code/session_01ReYjiZno4se1p66mFvK6Bc

kozalosev and others added 3 commits August 17, 2026 18:59
…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
@kozalosev kozalosev linked an issue Aug 24, 2026 that may be closed by this pull request
@kozalosev kozalosev moved this to In Progress in DickGrowerBot Aug 24, 2026
@kozalosev kozalosev added the enhancement New feature or request label Aug 24, 2026
@kozalosev kozalosev added this to the v1.5.0 milestone Aug 24, 2026
kozalosev and others added 2 commits August 25, 2026 01:20
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
@kozalosev
kozalosev merged commit ba37f19 into main Aug 24, 2026
2 checks passed
@kozalosev
kozalosev deleted the feature/156-bonus-for-subsequent-growths branch August 24, 2026 22:40
@github-project-automation github-project-automation Bot moved this from In Progress to Done in DickGrowerBot Aug 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Bonus for growing a dick several days in a row

1 participant