Repository navigation
G12: Harvest Streak Counter - #1
Conversation
11 TDD tasks covering types, engine, migration, records, HUD chip, DailyLog rows, BankruptcyScreen integration, and final sweep. Notes the schema bump is v5→v6 (records v1→v2) — the spec's v3→v4 reference predates the current schema baseline. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Initialize harvestStreak and peakHarvestStreak to 0 in initialGameState, and add STREAK_BONUS_PER_LEVEL and STREAK_BONUS_CAP constants. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Resets harvestStreak to 0 when a season is cleared (season_passed or season_4_won) but preserves peakHarvestStreak. Skips reset on season_failed since the run is ending and the final log should reflect the as-played streak. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Updated migrateState() to handle schema v5 → v6 migration by adding harvestStreak and peakHarvestStreak fields defaulted to 0. Also updated v4 → v6 and v3 → v6 migration chains to include the new streak fields. Updated existing migration tests to expect schema v6 with all migrated fields properly initialized. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add bestHarvestStreak to PersonalBests and integrate with recordRunEnd to track harvest streak peaks. Schema bumped 1→2; legacy records degrade gracefully with bestHarvestStreak=0. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add harvestStreak prop to HUD component and render a flame emoji chip (🔥 ×N) when the streak count exceeds 0. The chip uses aria-label for accessibility and is hidden entirely when harvestStreak === 0. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Add two new rows to the Daily Log modal: - Streak bonus row when streakBonus > 0, showing multiplier (capped at 4) and coins awarded - Streak reset note when a streak breaks (streakBefore > 0 && streakAfter === 0) Includes comprehensive test coverage for both conditions and accessibility checks. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Adds peakHarvestStreak prop to BankruptcyScreen, displays it in a new StatRow with new-best badge support, and includes it in Personal Records grid. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Bonus is now based on streakBefore (prior streak count), so the first harvest in a streak earns nothing. Day 2 of a streak earns +5, day 3 +10, etc., still capped at +20. The DailyLog chip multiplier and all affected tests are updated to match.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
✅ Files skipped from review due to trivial changes (2)
🚧 Files skipped from review as they are similar to previous changes (1)
📝 WalkthroughWalkthroughThis PR implements G12: adds per-run streak fields and per-turn streak logging, computes capped per-harvest coin bonuses applied before bankruptcy, persists bestHarvestStreak in records (schema v2), migrates saves to schema v6, and surfaces streak UI in HUD, DailyLog, and Bankruptcy screens with tests and docs. ChangesHarvest Streak Counter (G12)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
src/engine/gameEngine.ts (1)
348-365:⚠️ Potential issue | 🟠 Major | ⚡ Quick winInclude
streakBonusinnetChange.Both log builders add the streak bonus into
closingBalance, butnetChangestill excludes it. On every bonus-paying harvest day,openingBalance + netChange !== closingBalance, so the log underreports the turn’s actual gain.Proposed fix
taxRate: TAX_RATE, taxDeducted: 0, - netChange: totalHarvestIncome, + netChange: totalHarvestIncome + streakBonus, closingBalance: coinBalance, exhaustedPlots, pestDestroyedPlots, flashDroughtDaysAfter: flashDroughtDaysAfterEvent, streakBefore, @@ taxRate: TAX_RATE, taxDeducted, - netChange: totalHarvestIncome - landLeaseDeducted - taxDeducted, + netChange: totalHarvestIncome + streakBonus - landLeaseDeducted - taxDeducted, closingBalance: coinBalance, exhaustedPlots, pestDestroyedPlots, flashDroughtDaysAfter: flashDroughtDaysRemaining, streakBefore,Also applies to: 437-454
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/engine/gameEngine.ts` around lines 348 - 365, The DailyLogEntry construction currently sets netChange to totalHarvestIncome but omits adding streakBonus, causing openingBalance + netChange to differ from closingBalance when a streakBonus is applied; update the netChange calculation in the log builders (where DailyLogEntry is created and netChange is assigned) to include streakBonus (e.g., netChange = totalHarvestIncome + streakBonus or more generally netChange = closingBalance - openingBalance) so netChange reflects the streakBonus and matches closingBalance minus openingBalance; apply the same change in both places (around the blocks that define streakBonus, openingBalance, closingBalance, and netChange).
🧹 Nitpick comments (4)
tests/components/BankruptcyScreen.test.tsx (1)
100-108: ⚡ Quick winStrengthen streak recap assertions beyond label presence.
Line 100 verifies headings only. Also assert streak values and the new-best badge so the new wiring is actually validated.
✅ Suggested assertions
-import { render, screen } from '`@testing-library/react`'; +import { render, screen, within } from '`@testing-library/react`'; @@ it('shows Longest streak stat row with peakHarvestStreak', () => { @@ expect(screen.getByText('Longest streak')).toBeInTheDocument(); expect(screen.getByText(/Best streak/i)).toBeInTheDocument(); + expect(screen.getAllByLabelText('new personal best')).toHaveLength(1); + const recordsPanel = screen.getByLabelText(/personal records across all runs/i); + expect(within(recordsPanel).getByText('Best streak:')).toBeInTheDocument(); + expect(within(recordsPanel).getByText('6')).toBeInTheDocument(); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/components/BankruptcyScreen.test.tsx` around lines 100 - 108, The test currently only checks for the presence of the "Longest streak" and "Best streak" labels; update the spec that uses renderScreen(...) (with peakHarvestStreak: 6, records and newBests) to also assert the rendered numeric streak value (e.g., "6" or a string containing the peakHarvestStreak) and assert the "new best" indicator is present when newBests includes 'bestHarvestStreak' so the wiring for peakHarvestStreak and newBests is actually validated; locate the test using renderScreen, peakHarvestStreak, records and newBests and add assertions for the streak number and the new-best badge element.tests/components/HUD.test.tsx (1)
71-76: ⚡ Quick winAdd a tooltip/cap assertion for the streak chip.
Line 71 currently validates visibility/text only. Add a
titleassertion so capped next-bonus behavior is protected too.✅ Suggested test extension
it('shows the streak chip with ×N when harvestStreak > 0', () => { render(<HUD {...baseProps} currentDay={5} coinBalance={100} harvestStreak={7} />); const chip = screen.getByLabelText(/harvest streak/i); expect(chip).toBeInTheDocument(); expect(chip).toHaveTextContent('×7'); + expect(chip).toHaveAttribute('title', expect.stringMatching(/\+20🪙 bonus \(capped at \+20\)/i)); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/components/HUD.test.tsx` around lines 71 - 76, The test "shows the streak chip with ×N when harvestStreak > 0" in HUD.test.tsx should also assert the streak chip's tooltip/cap behavior: after rendering HUD with harvestStreak (the existing render call using HUD and props like currentDay, coinBalance, harvestStreak), grab the chip via getByLabelText(/harvest streak/i) (the existing chip variable) and add an assertion that the chip has a title attribute (and, if you know the exact capped message, assert its value) — e.g. expect(chip).toHaveAttribute('title') or expect(chip.getAttribute('title')).toBe('EXPECTED_TOOLTIP') to protect the capped next-bonus behavior.tests/engine/gameEngine.test.ts (1)
1534-1546: ⚡ Quick winThis
season_failedtest never exercises the streak-preservation path.Here the turn has no harvest, so the streak becomes
0even without any season-end reset. The test would still pass ifseason_failedincorrectly cleared a harvested streak. Use a season-end turn that harvests but still misses the target.Example adjustment
- it('does NOT reset streak on season_failed (run is ending)', () => { - const base = { - ...initialGameState(), - currentDay: 20, - coinBalance: 16, - harvestStreak: 2, - peakHarvestStreak: 2, - }; - const { state: after, log } = processTurn(base, 'sunny'); + it('does NOT reset streak on season_failed when the final turn harvests', () => { + const seeded = seedAndPlant({ + ...initialGameState(), + currentDay: 20, + coinBalance: 21, + harvestStreak: 2, + peakHarvestStreak: 2, + }); + const { state: after, log } = processTurn(seeded, 'sunny'); expect(after.phase).toBe('season_failed'); - expect(log.streakAfter).toBe(0); // no harvest → miss-day reset (independent rule) - expect(after.harvestStreak).toBe(0); + expect(log.streakBefore).toBe(2); + expect(log.streakAfter).toBe(3); + expect(after.harvestStreak).toBe(3); });🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/engine/gameEngine.test.ts` around lines 1534 - 1546, The test "does NOT reset streak on season_failed (run is ending)" currently uses a turn with no harvest so the streak resets for an unrelated reason; change the base state in this test so the turn produces a harvest but still results in phase 'season_failed' (so we exercise the season-end code path). In practice, adjust the initialGameState() seed values (e.g., crop/harvest readiness or coinBalance) used in the test before calling processTurn('sunny') so processTurn returns after.phase === 'season_failed' while a harvest occurred, then assert that after.harvestStreak and log.streakAfter preserve the pre-turn streak (harvestStreak remains 2). Target the test case, initialGameState(), processTurn, and the assertions around after.phase, log.streakAfter and after.harvestStreak.tests/engine/records.test.ts (1)
157-158: ⚡ Quick winUse the imported constant instead of hardcoded string.
Line 158 uses the hardcoded string
'pixel-parsnips-records'instead of the importedRECORDS_KEYconstant. Line 67 in this file correctly uses the constant. Using the constant prevents maintenance issues if the key value changes.♻️ Proposed fix
- localStorage.setItem( - 'pixel-parsnips-records', + localStorage.setItem( + RECORDS_KEY, JSON.stringify({🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/engine/records.test.ts` around lines 157 - 158, Replace the hardcoded localStorage key string with the imported RECORDS_KEY constant: in the test where localStorage.setItem('pixel-parsnips-records', ...) is used, swap the literal for RECORDS_KEY and ensure the test imports/uses the same RECORDS_KEY referenced earlier in this file (the constant used at line 67) so both occurrences consistently use RECORDS_KEY.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@backlog.md`:
- Line 105: Update the footer timestamp chain line that currently reads
"*Generated 2026-06-02 from p1–p6 analyses. Updated 2026-06-03 after shipping
006-season-system, then 2026-06-04 after shipping 007-enriched-run-summary, then
2026-06-05 after deferring G4 (Daily Objectives / Milestones / Contracts).*" by
appending the G12 ship note for 2026-06-06 so it reflects the new update (e.g.,
add ", then 2026-06-06 after shipping G12"). Ensure the exact footer sentence is
updated consistently with the document references to G12 in Phase 2 and line 33.
In `@src/engine/useGameEngine.ts`:
- Around line 22-60: The migrateState function currently only checks
parsed.state truthiness so malformed saves like {schemaVersion:5, state:42} get
cast/spread into a GameState; update each recognized-schema branch in
migrateState to first validate parsed.state is a non-null object with the
minimal required GameState properties (e.g. check typeof parsed.state ===
'object' && parsed.state !== null && 'phase' in parsed.state && 'plots' in
parsed.state) before spreading/casting; if the guard fails, fall through to the
discard/malformed-save path and return null so corrupted payloads are not
migrated into invalid GameState objects.
---
Outside diff comments:
In `@src/engine/gameEngine.ts`:
- Around line 348-365: The DailyLogEntry construction currently sets netChange
to totalHarvestIncome but omits adding streakBonus, causing openingBalance +
netChange to differ from closingBalance when a streakBonus is applied; update
the netChange calculation in the log builders (where DailyLogEntry is created
and netChange is assigned) to include streakBonus (e.g., netChange =
totalHarvestIncome + streakBonus or more generally netChange = closingBalance -
openingBalance) so netChange reflects the streakBonus and matches closingBalance
minus openingBalance; apply the same change in both places (around the blocks
that define streakBonus, openingBalance, closingBalance, and netChange).
---
Nitpick comments:
In `@tests/components/BankruptcyScreen.test.tsx`:
- Around line 100-108: The test currently only checks for the presence of the
"Longest streak" and "Best streak" labels; update the spec that uses
renderScreen(...) (with peakHarvestStreak: 6, records and newBests) to also
assert the rendered numeric streak value (e.g., "6" or a string containing the
peakHarvestStreak) and assert the "new best" indicator is present when newBests
includes 'bestHarvestStreak' so the wiring for peakHarvestStreak and newBests is
actually validated; locate the test using renderScreen, peakHarvestStreak,
records and newBests and add assertions for the streak number and the new-best
badge element.
In `@tests/components/HUD.test.tsx`:
- Around line 71-76: The test "shows the streak chip with ×N when harvestStreak
> 0" in HUD.test.tsx should also assert the streak chip's tooltip/cap behavior:
after rendering HUD with harvestStreak (the existing render call using HUD and
props like currentDay, coinBalance, harvestStreak), grab the chip via
getByLabelText(/harvest streak/i) (the existing chip variable) and add an
assertion that the chip has a title attribute (and, if you know the exact capped
message, assert its value) — e.g. expect(chip).toHaveAttribute('title') or
expect(chip.getAttribute('title')).toBe('EXPECTED_TOOLTIP') to protect the
capped next-bonus behavior.
In `@tests/engine/gameEngine.test.ts`:
- Around line 1534-1546: The test "does NOT reset streak on season_failed (run
is ending)" currently uses a turn with no harvest so the streak resets for an
unrelated reason; change the base state in this test so the turn produces a
harvest but still results in phase 'season_failed' (so we exercise the
season-end code path). In practice, adjust the initialGameState() seed values
(e.g., crop/harvest readiness or coinBalance) used in the test before calling
processTurn('sunny') so processTurn returns after.phase === 'season_failed'
while a harvest occurred, then assert that after.harvestStreak and
log.streakAfter preserve the pre-turn streak (harvestStreak remains 2). Target
the test case, initialGameState(), processTurn, and the assertions around
after.phase, log.streakAfter and after.harvestStreak.
In `@tests/engine/records.test.ts`:
- Around line 157-158: Replace the hardcoded localStorage key string with the
imported RECORDS_KEY constant: in the test where
localStorage.setItem('pixel-parsnips-records', ...) is used, swap the literal
for RECORDS_KEY and ensure the test imports/uses the same RECORDS_KEY referenced
earlier in this file (the constant used at line 67) so both occurrences
consistently use RECORDS_KEY.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: eca11247-485c-46d4-a969-b3dfb072bc36
📒 Files selected for processing (19)
backlog.mdspecs/008-harvest-streak/plan.mdsrc/App.tsxsrc/components/BankruptcyScreen.tsxsrc/components/DailyLog.tsxsrc/components/GameBoard.tsxsrc/components/HUD.tsxsrc/engine/constants.tssrc/engine/gameEngine.tssrc/engine/records.tssrc/engine/types.tssrc/engine/useGameEngine.tstests/components/BankruptcyScreen.test.tsxtests/components/DailyLog.test.tsxtests/components/GameBoard.test.tsxtests/components/HUD.test.tsxtests/engine/gameEngine.test.tstests/engine/records.test.tstests/engine/useGameEngine.test.ts
vitest 4 requires vite >=6, which caused npm to install a nested vite 8 alongside the pinned vite 5 — leaving package-lock.json out of sync with the esbuild 0.27 that nested vite required and breaking `npm ci`. Aligning vite to ^7 lets vitest share the root vite and produces a clean lockfile. Also bumps checkout/setup-node to v5 and the runner to Node 22 to clear the Node 20 deprecation warning. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Truthy check on parsed.state let non-object values (e.g. 42, "x") through to spread/cast as GameState. Add isGameStateShape structural guard so corrupted saves fall through to the discard path. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
netChange omitted streakBonus, so openingBalance + netChange diverged from closingBalance on streak-bonus turns. Compute as closingBalance - openingBalance in both the bankruptcy and normal log builders to preserve the invariant. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- gameEngine: season_failed test now seeds a harvest so it exercises the season-end code path (was passing via miss-day reset for unrelated reasons). - HUD: assert streak chip tooltip including capped next-bonus message. - BankruptcyScreen: assert rendered streak value and New Best badge. - records: use RECORDS_KEY constant instead of hardcoded key string. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 2
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In @.github/workflows/ci.yml:
- Line 23: Replace the moving tag uses: actions/setup-node@v5 with the specific
v5 commit SHA at both places where "uses: actions/setup-node@v5" appears so the
workflow is pinned; update the action step inputs to account for v5's automatic
caching by adding/adjusting package-manager-cache when packageManager is present
(or explicitly disable it) and verify any packageManager fields remain correct;
also confirm the workflow runner and any Node-version-specific steps are
compatible with Node 24 (update node-version targets or environment assumptions
if needed).
- Line 20: The workflow currently uses mutable tags actions/checkout@v5 and
actions/setup-node@v5; update the CI workflow to pin both actions to their full
commit SHAs (replace the `@v5` tags with the corresponding commit SHA refs for
actions/checkout and actions/setup-node) and set the node-version input to a
deterministic 22.x floor that satisfies the lockfile (e.g., "22.x" with a
minimum of >=22.12.0) so the runner and Node requirements are explicit and
supply-chain risk is reduced.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 02fb8bc8-3310-4961-9bf7-4329fbfc3f23
⛔ Files ignored due to path filters (1)
package-lock.jsonis excluded by!**/package-lock.json
📒 Files selected for processing (8)
.github/workflows/ci.ymlpackage.jsonsrc/engine/gameEngine.tssrc/engine/useGameEngine.tstests/components/BankruptcyScreen.test.tsxtests/components/HUD.test.tsxtests/engine/gameEngine.test.tstests/engine/records.test.ts
✅ Files skipped from review due to trivial changes (1)
- package.json
🚧 Files skipped from review as they are similar to previous changes (4)
- tests/components/BankruptcyScreen.test.tsx
- src/engine/useGameEngine.ts
- src/engine/gameEngine.ts
- tests/engine/records.test.ts
Replace mutable @v5 tags for actions/checkout and actions/setup-node with full commit SHA refs to reduce supply-chain risk. Switch node-version from "22" to "22.x" for explicitness. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… meta description Lighthouse mobile audit flagged two WCAG AA contrast failures and a missing meta description. - HUD season chip label: text-farm-stone/60 was 2.34:1 on the dark chip; switch to text-farm-parchment/70 (~7.7:1). - farm.grass token #3D7A2B against farm-parchment was 4.44:1 (just under 4.5:1); darken to #357028 (~5.1:1). Ripples to the few primary action buttons that use bg-farm-grass text-farm-parchment. - index.html: add meta description and replace the default Vite title with "Pixel Parsnips". Scores: a11y 94 -> expected 100, SEO 90 -> expected 100. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Summary
season_failed). Persistent best (bestHarvestStreak) lives onrecords.ts(schema v2), per-run high-water mark onGameState.peakHarvestStreak.Spec: specs/008-harvest-streak/spec.md · Plan: specs/008-harvest-streak/plan.md
Per user feedback during review: first harvest of a streak earns no bonus (bonus is based on the prior streak count), so day 2 is the first paying day.
Test Plan
npm test— 328/328 passingnpm run lint— cleannpx tsc -b— cleanSummary by CodeRabbit
New Features
Documentation