← Back to Dev Blog

Daily Scrum Log -- 2026-05-31 (Day 25) - wsbry_gusux

Summary

Day 25 introduced the Crime & Justice System — petty theft, guard arrests, hero interceptions, and per-town crime levels. Pre-existing null-byte corruption in PersistenceManager.js (6 bytes) and a truncation in SimSnapshot.js (from the Edit tool hitting its VFS limit mid-write) were repaired with Python byte-level writes. All 945 tests pass across 81 files. SAVE_VERSION bumped to 16.


Project Manager

Contribution: Led Day 25 scrum; selected Crime & Justice as the day's feature; enforced bash-heredoc-only writes for server files.

Key Decisions:

  • Crime & Justice selected because: (a) Thieves Guild exists but has no observable crime effect, (b) Guards wander but never do anything, (c) Greedy personality trait now has a mechanical consequence beyond economy multipliers, (d) it adds dramatic Chronicle events that make the world feel dangerous and alive.
  • Null-byte patch in PersistenceManager.js done before feature work — standard corruption-first protocol.
  • SimSnapshot.js truncation (Edit tool cut off the crime ?? value mid-line) repaired with Python slice. Bash heredoc is mandatory for all new server file writes.
  • SAVE_VERSION 16: old saves load cleanly — crime defaults to zero for both towns.
  • World Regions (high-priority IDEAS.md item) remains deferred to a future multi-day sprint.

World Designer

Contribution: Designed the four crime tiers, their visual identity, and the three event types.

Crime Tier Design:

  • Safe (#4caf72 green, fa-shield): Level 0-20 — guards keep order, merchants leave stalls unattended.
  • Troubled (#d4b84a amber, fa-triangle-exclamation): Level 21-40 — a few bold snatch-and-grabs, guards respond.
  • Dangerous (#e07840 orange, fa-skull): Level 41-65 — market vendors lock their carts, citizens hurry home.
  • Lawless (#c04040 red, fa-skull-crossbones): Level 66-100 — open street crime, guards overwhelmed.

Event voice:

  • Theft (no guard): {thief} snatches {item} near the market. No guard in sight. — unsettling, matter-of-fact.
  • Escape (guard present, failed): {thief} steals {item} and slips away from {guard}'s pursuit! — cinematic chase energy.
  • Arrest (guard succeeds): {guard} the {profession} arrests {thief} for stealing {item}. — satisfying justice.
  • Hero intercept (hero stops it): {hero} catches {thief} in the act and stops the theft! The crowd cheers. — heroic moment.

Narrative principle: Every theft item should feel plausible in a fantasy market — coin purses, bread, lockpicks, wax-sealed letters. Nothing anachronistic.


Backend Developer

Contribution: Built CrimeSystem.js; fixed PersistenceManager null bytes + SimSnapshot truncation; wired all integration points.

New file: server/CrimeSystem.js (140 lines)

  • tick(totalHours, thornVillagers, millVillagers, personality): hourly per-town evaluation.
    • Passive decay: −0.5/hr (crime level naturally falls with guard presence).
    • Theft probability: BASE(2%) + greedyCount×1.2% − min(guardCount×1.5%, 50%), clamped 0.5–45%.
    • Greedy-trait villagers (via personality.has(id, 'Greedy')) are preferred as thieves.
    • Guard catch chance: min(guardCount × 35%, 85%).
    • Arrest: level +2, arrest Chronicle event. Escape: level +5, theft/escape Chronicle event + #pendingTheft set.
    • Crime level capped at 100.
  • heroIntercept(heroName, heroTown): 6% chance/hr hero spots active theft → level −3, +8 rep, Chronicle event.
  • getCrimeTier(town): returns { tier, label, color, icon } for UI.
  • serializeSnapshot(): snapshot-safe per-town object.
  • serialize() / restore(): persist raw levels.

SimEngine.js wiring:

  • #crime = new CrimeSystem() field.
  • Hourly tick: crimeEvents = this.#crime.tick(...) after personality events; heroIntercept called if hero is in-town.
  • snapshot: passes crime: this.#crime.serializeSnapshot().
  • #loadSave: restores crime from save.
  • #maybeSave: persists crime: this.#crime.serialize().

SimSnapshot.js: Added crime=null param + crime: crime ?? { ...defaults } in return object. PersistenceManager.js: SAVE_VERSION 15 → 16. Null-byte corruption (6 bytes) stripped.

Corruption log:

  • PersistenceManager.js: 6 null bytes found and removed via Python byte-replacement.
  • SimSnapshot.js: Edit tool truncated at line 63 mid-value. Repaired with Python lines[:66] slice.

Frontend Developer

Contribution: Added crime event types to EventLog; added crime tier badge to TopBar.

EventLog.jsx changes:

  • New FA icon imports: faUserSecret (theft), faPersonRunning (escape), faHandcuffs (arrest).
  • New TYPE_COLORS: theft #d06030, escape #c84a20, arrest #4a9ae8, hero_intercept #e8c040.
  • New TYPE_ICONS: theft → faUserSecret, escape → faPersonRunning, arrest → faHandcuffs, hero_intercept → faShieldHalved.

TopBar.jsx changes:

  • New prop: crime (crime snapshot object), heroTown.
  • CRIME_ICONS map: fa-shield → faShield, fa-triangle-exclamation → faTriangleExclamation, fa-skull → faSkull, fa-skull-crossbones → faSkullCrossbones.
  • Badge only shown when tier is NOT 'safe' — keeps UI clean at low crime.
  • Badge color matches tier color with 15% background tint + colored border.
  • Title text: {town} crime level: {label} ({level}).

App.jsx: Passes crime={state?.crime ?? null} and heroTown={state?.heroTown ?? 'Thornhaven'} to TopBar.


Tester / QA

Contribution: Wrote 13 CrimeSystem tests; fixed 2 pre-existing test failures; verified full suite.

CrimeSystem.test.js (13 tests):

  • Zero init for both towns.
  • getTier returns correct tier for levels 0, 30, 50, 90.
  • tick returns array (no crash with empty villagers).
  • Passive decay over 20 ticks reduces crime level.
  • serialize / restore round-trip.
  • serializeSnapshot includes tier, label, color, icon.
  • heroIntercept returns null when no pending theft.
  • getCrimeTier matches getCrimeLevel for dangerous threshold.
  • Crime level cannot exceed 100.
  • Unknown town returns 0 gracefully.

Pre-existing failures fixed:

  • SimEngine.heroVisit.test.js: Failed due to SimSnapshot.js truncation (parse error) — fixed.
  • PersistenceManager.test.js: Failed due to null bytes — fixed.

Test results: 945 passing across 81 test files — zero failures. Up from 932 (Day 24) before today.