Skip to content

Merge dev → main (automated) - #770

Merged
SorraTheOrc merged 68 commits into
mainfrom
release/dev-to-main-20260729160734
Jul 29, 2026
Merged

Merge dev → main (automated)#770
SorraTheOrc merged 68 commits into
mainfrom
release/dev-to-main-20260729160734

Conversation

@SorraTheOrc

Copy link
Copy Markdown
Member

Automated release created by ship skill.\n\nIncludes CHANGELOG.md with work-item summaries from this release.

Sorra the Orc added 30 commits July 23, 2026 23:13
…r & Blend Spike gym scene

- Removed shaderAttempted and shaderResult private fields
- Removed [ Attempt Shader ] button from the button row in create()
- Removed attemptShader() method entirely
- Updated Controls help text to remove [ Attempt Shader ] line
- Updated Test Plan to remove step referencing Attempt Shader
- Remaining controls (Next Tint, Next Blend, Reset Tint) are untouched
- All 4388 unit tests and 393 browser tests pass
…rd Data PRD

Creates a comprehensive PRD at docs/main-street/prd-balance-process-and-tooling.md
defining:

- Structured balance review process (trigger events, review workflow,
  guardrail thresholds, decision gates)
- 7 micro-level per-card metrics (pick rate, win-rate delta, cost-to-income
  ratio, synergy utilization, upgrade adoption, event impact, survival rate)
  with formulas, data sources, and interpretation guidance
- 8 macro-level global metrics (win rate matrix, score distribution, economy
  health, synergy diversity, loss mode decomposition, card usage diversity,
  turn-by-turn snapshots, trap card prevalence)
- CLI tool architecture for 5 scripts (balance-report, balance-cards,
  balance-global, balance-capture-baseline, balance-check)
- Baseline management strategy for regression comparison
- Integration with existing Monte Carlo harness, playtest scenarios, CI,
  difficulty presets, AI strategies, and EconomyLedger
- Implementation roadmap (4 phases)
- Metric feasibility assessment with harness extension priorities

Updated cross-references in:
- README.md (added balance documentation links)
- docs/main-street/balancing-methodology.md
- docs/main-street/playtest-scenarios.md
- docs/main-street/monte-carlo-sample-results.md
- docs/main-street/card-catalog.md
- docs/main-street/prd-milestone-3.md
Commit b121b21 ('Sync work items and comments') deleted all 1195 project files
from the dev branch. Reverted via 49c8763 using --strategy-option=theirs,
then re-applied the cross-reference documentation updates from the Balance PRD
that were overwritten by the revert merge conflict resolution.

Restored files (1196 total):
- src/ (96 files): engine modules
- example-games/ (all 7 games)
- tests/ (all test files)
- public/assets/ (SVG cards, audio, thumbnails)
- scripts/, schemas/, tools/, config files
- .github/workflows/, .githooks/

Re-applied cross-references to prd-balance-process-and-tooling.md PRD in:
- README.md (2 references)
- docs/main-street/balancing-methodology.md
- docs/main-street/playtest-scenarios.md
- docs/main-street/monte-carlo-sample-results.md
- docs/main-street/card-catalog.md
- docs/main-street/prd-milestone-3.md
…date investment card timing guidance

- Add T8 step (buy Bookshop + auto-place) to unified tutorial flow
- Update scenario: replace biz-hardware with biz-bookshop ( Culture business)
- Updated coin budget: Laundromat (), Bookshop (), Local Festival () = 0
- Add auto-place logic in MainStreetTurnController for combined buy+place
- Update i18n strings: new T8 culture business step, updated T7 festival timing hint
- Renumber T8→T9 through T13→T14
- Update all tests for 14-step structure and new card references
- Fix browser overlay tests for renumbered step IDs

Work committed to dev
…valuation

Add estimatePositiveScoreProbability() function that evaluates whether
placing a card in a tableau column will lead to a positive final score.
The function considers:
- Remaining cards of that color in the draw pile
- Visible cards on both player and opponent expeditions
- How many follow-up cards can legally be placed in ascending order
- The current point deficit needed to overcome the -20 base cost

Integrate this into the GreedyStrategy scorePhase1Action() to award
bonuses for high-confidence plays and penalties for low-confidence ones.

New tests verify:
- Avoiding plays with low probability (opponent holds blocking cards)
- Playing low cards when many follow-ups are available
- Avoiding high cards with no follow-up potential
- Dynamic risk tolerance based on draw pile size
- Direct unit tests for the probability function

All 267 existing tests continue to pass.
…ll be used in a swap

Two changes prevent GreedyStrategy from wasting discard draws:

1.  now evaluates ALL legal moves (including column
   bonus) and only returns 'discard' when the best-scored move is a SWAP
   that is strictly better than discard-and-flip. Previously, a swap with
   a negative column bonus could trigger 'discard' even when the raw
   swap+bonus was still worse than doing nothing (DAF).

2.  now passes  to
    so column-building feasibility weighting is
   applied consistently between Phase 1 (draw source) and Phase 2 (move
   selection). Previously, Phase 2 had no column bonus, so the move
   chosen often differed from the Phase 1 rationale.

New edge-case tests verify:
- 'discard' is NOT chosen when the best legal move with the discard
  card would be a discard-and-flip
- 'discard' is NOT chosen when the column bonus exists but is
  insufficient to make a swap strictly better than discard-and-flip
- Column bonus scenarios are updated to use cases where the bonus
  actually makes a demonstrable difference
- E-1: Add cardsOwned tracking to MonteCarloRunSummary
- E-2: Add strategy×difficulty batch runner (runAllCombinations)
- E-3: Add marketOffers tracking to MonteCarloRunSummary
- E-4: Add getHistory() to EconomyLedger
- Add economyHistory field to MonteCarloRunSummary (E-4 integration)
- Add --sweep flag to CLI for batch runs
- Add ALL_STRATEGIES, ALL_DIFFICULTIES constants
- Add CombinationResult, RunAllCombinationsOptions interfaces
- Add monte-carlo-sweep npm script
- Update docs with sweep mode and extension fields

All 89 tests pass across 7 test files.
Modified takeScreenshot() to exclude HUD elements (help panel,
header chrome, nav buttons, event log) from the screenshot
RenderTexture using a Set-based blacklist by identity reference.

Added unit tests for the HUD exclusion logic in isolation.

- 4 new tests in tests/gym/GymSaveLoadScreenshotFilter.test.ts
- All 252 unit test files pass (4438 tests)
- npm run build succeeds
- Inject version at build time via Vite define (__APP_VERSION__ from package.json)
- Add createVersionLabel() factory in src/ui/versionDisplay.ts with shared constants
- Display version (v0.1.7) in GameSelectorScene bottom-left corner
- Display version on game canvas when SettingsPanel opens (hidden otherwise)
- Add type declaration for __APP_VERSION__ in vite-env.d.ts
- Update DEVELOPER.md with Build-Time Version Injection documentation
- Add unit tests for GameSelectorScene version label
- Add browser tests for SettingsPanel version label display

Closes CG-0MR89YKN0000FK5W
Creates a reusable GymButtonBar class that provides:
- Left/center/right zone alignment
- Even spacing within zones
- Automatic row wrapping when buttons exceed zone width
- Support for 1..n rows
- Preserves existing button styling (colors, hover effects, font)
- Returns button references for setVisible(), setText(), etc.
- Supports multiple bars at different Y positions
- refresh() and destroy() lifecycle methods

Includes 19 unit tests verifying zone positioning, spacing, wrapping,
styling, button references, and lifecycle management.

Part of CG-0MRDGW0DH001AA58 (Gym button bar)
This restores all source files that were deleted by commit c6281fb
(worklog sync). Files are restored from commit 292d6df which had
the correct state of all source files.
Adds initButtonBar() method and buttonBar property to GymSceneBase
so Gym scenes can easily create auto-laying-out button bars.

- initButtonBar(y, opts?) creates a GymButtonBar at the given Y position
- buttonBar property exposes the bar for addButton() calls
- addButton() marked as @deprecated in favor of buttonBar.addButton()
- addButtonAtAnchor() preserved for migration
- 8 integration tests verifying source code structure
- Imports GymButtonBar from src/ui/GymButtonBar

Part of CG-0MRDGW0DH001AA58 (Gym button bar)
Migrates simple Gym scenes (single-row button groups) from manual
this.addButton(x, y, ...) to this.buttonBar.addButton(label, callback, opts).

Scenes migrated:
- GymOverlayUiScene (4 buttons)
- GymTranscriptScene (6 buttons, preserving btnHit/btnStick/btnNewHand refs)
- GymUndoRedoScene (5 buttons, undo/redo buttons unaffected)
- GymSaveLoadScene (8 buttons across 2 rows)
- GymTooltipScene (4 buttons with dynamic toggle label)
- GymMarketOfferEngineScene (4 buttons)
- GymParameterizedOverlayScene (3 buttons)
- GymGraphicsLightingSpikeScene (2 buttons)
- GymGraphicsShaderSpikeScene (3 buttons)
- GymSllScene (3 buttons with custom colors at 2 Y positions)

All button styling (colors, hover effects, font) preserved via opts.
Button references preserved for dynamic show/hide (btnHit), setText (toggleBtnRef).

Part of CG-0MRDGW0DH001AA58 (Gym button bar)
Migrates multi-row and mixed-layout Gym scenes from manual
this.addButton(x, y, ...) to this.buttonBar.addButton(label, callback, opts).

Scenes migrated:
- GymHandPileScene (17 buttons across 4 rows with labels)
- GymRuleEngineScene (13 buttons in 2 sections with wrapping)
- GymSpatialRulesScene (14 buttons across 3 rows with labels)
- GymHudComponentsScene (6 buttons across 2 rows)
- GymDeckRngScene (4 buttons, design buttons left manual)
- GymI18nScene (5 buttons across 2 rows)
- GymAiStrategyScene (8 buttons across 3 sections)
- GymAudioFeedbackScene (4 buttons, dynamic buttons left manual)
- GymSvgHelpersScene (6 buttons across 2 rows)

All button styling preserved. Button references maintained for
dynamic show/hide and text changes (dragButton, enBtn, etc.).

Part of CG-0MRDGW0DH001AA58 (Gym button bar)
- Updated EVENT_TEMPLATE_COUNT from 36 to 37 in game-state.test.ts
- Updated expanded-card-pool.test.ts event template count (36→37) and deck size (108→111)
- Updated market.test.ts unique template count (36→37)
- Added evt-recession to expanded-card-manifest.json
- Restore evt-recession card in card-data.csv (was reverted in d0a6f4a)
- Add missing evt-recession.svg card asset
- Update csv-checksum.json to match new CSV content
- Fix meta-progression.test.ts card counts (83->84, 81->82)
- Fix turnflow.test.ts coin assertion to be seed-resilient
…uring deck-back fly-in

Adds pendingRefillSlots infrastructure to FeudalismRenderer (following
the same pattern as patronAnimationCache) so that market slots being
refilled render as empty during the deck-back fly-in animation. After
the animation completes, a fresh refreshAll() call re-renders the
actual card.

Changes:
- FeudalismRenderer: add pendingRefillSlots set, addPendingRefillSlots(),
  clearPendingRefillSlots(), isPendingRefillSlot(). refreshMarket() checks
  the set before rendering cards, showing empty rectangles for flagged slots.
- FeudalismTurnController: add onSetPendingRefillSlots and
  onClearPendingRefillSlots callbacks. executeAction() and executeAiTurn()
  set pending refill slots before onRefreshAll(), clear them in the
  onRefreshMarket callback (called after deck-back lands).
- FeudalismScene: wire up new callbacks in both create() and
  restoreFromCheckpoint().
- Tests: FeudalismRefillAnimation.browser.test.ts with 4 tests verifying
  the pending refill infrastructure.

ACs covered:
1. Slot appears visually empty during refill animation
2. Deck-back flies to empty slot, card appears after landing
3. Existing animations unchanged
4. Works for all tiers, purchase and reserve-from-market
5. Reduced motion mode correctly skips animation, shows card immediately
6. Code comments updated
7. Full test suite passes (205 tests, 16 feudalism test files)
- Removed redundant [ Toggle HelpPanel ] and [ Toggle Settings ] buttons
- Repositioned status text lines on separate Y offsets to avoid overlap
- Updated help content (Controls, Test Plan) to reflect toggle button removal
- Updated browser tests: toggle assertions replaced with Open/Close assertions

All 20 GymHudComponentsScene browser tests pass.
- Updated setMaxRotationDegrees() clamp from >= 0 to [0, 359] range
- Updated JSDoc to document the 0-359 range
- Updated Gym slider maxValue from 45 to 359
- Added 11 boundary tests: 0, 359, negative, NaN, Infinity, and range invariant
…Animation test

Fix 5 TS6133 errors (unused imports/variables) in the refill animation
browser test file that were causing build failures.

- Removed unused 'beforeEach' import
- Removed unused GAME_W, GAME_H constants
- Restored needed 'renderer' variable in tier test
- Removed unused 'renderer' variable in callback test
CG-0MRDL6LSS001LPCG

Moves the celebration sound (SFX_KEYS.PATRON_VISIT) and toast
messages from immediately after executeTurn() to just before
playCardAnimation(), for both human (executeAction) and AI
(executeAiTurn) paths. Also removes the premature onRefreshAll()
that rendered the market slot empty before the flying animation
began.

Changes:
- executeAction(): remove early sound+toast and refreshAll, defer
  them to the animation block
- executeAiTurn(): same reordering for AI turn consistency
- New test file (11 tests) verifying source code call ordering
…ing legacy addButton calls, remove deprecated methods

- Migrate GymAudioFeedbackScene dynamic buttons from this.addButton() to
  this.buttonBar.addButton() with left zone for both sound event and
  visual feedback auto-generated buttons
- Migrate GymTokenPileViewScene 9 buttons (8 grid + 1 Reset All) from
  this.addButton() to this.buttonBar.addButton() with center zone
- Remove deprecated addButton() method from GymSceneBase (migration complete)
- Remove deprecated addButtonAtAnchor() method from GymSceneBase (migration complete)
- Update GymSceneBaseButtonBar tests to verify removal instead of presence
- All legacy this.addButton() calls eliminated from all Gym scenes

Part of CG-0MRDGW0DH001AA58 (Gym button bar)
- Add GymButtonBar section to docs/DEVELOPER.md under Shared HUD Components
  with API reference, zone documentation, per-button overrides, and
  GymSceneBase integration details
- Update README.md to mention GymButtonBar in the Shared HUD Components list

Documentation covers:
  - Constructor configuration (y, zone, padding, buttonGap, rowSpacing, width)
  - Left/center/right zone behavior and wrapping
  - Per-button overrides (zone, fontSize, color, hoverColor)
  - Instance methods (addButton, refresh, destroy)
  - GymSceneBase integration via initButtonBar()

Relates to CG-0MRDGW0DH001AA58 (Gym button bar)
Two bugs fixed:

1. Identity references: The exclusion set used this.helpPanel and
   this.helpButton (wrapper class instances), not the actual Phaser
   scene children. Added getSceneChildren() accessors to HelpPanel
   (returns the container) and HelpButton (returns circle, label,
   hitArea) so scenes can exclude them by identity.

2. Position override: rt.draw(drawables, 0, 0) temporarily set every
   object's position to (0,0) — Phaser 4's DynamicTexture x/y
   parameters replace object positions, not add as offsets. Only
   containers rendered children at correct relative positions,
   making non-container objects (cards, text, buttons) all stack
   at the origin while the HelpPanel container appeared normally.
   Fixed by calling rt.draw(drawables) without x/y.

Test: 18/18 SaveLoad tests pass, 441/441 gym tests pass, build clean.
Sorra the Orc added 28 commits July 27, 2026 10:27
The scroll mask was created at scene origin (0,0), but the settings panel slides in from the right at (canvasWidth - panelWidth, 0). The mask covered (0, 0, panelWidth, canvasHeight) in scene space, which is the wrong area, clipping all scroll content away.

Fix: add the mask graphics to 'this.container' so its coordinates are in container-local space, where the panel occupies (0, 0, panelWidth, canvasHeight). The mask now correctly clips the scrollable content to the visible panel area.
Root cause: the mask graphics was added to the container, rendering as a
visible filled rectangle on top of everything, obscuring all content.

Fix: match the HelpPanel pattern — create the mask at scene root, set
visible=false, position it to cover the panel area when slid in from the
right (canvasWidth - panelWidth, 0, panelWidth, canvasHeight). Use
'new Phaser.Display.Masks.GeometryMask()' instead of createGeometryMask().

Also ensures mask stays correctly positioned regardless of container
position, even during slide-in animation.
When the expanded tree exceeds the dialog content area, the tree
now scrolls via mouse wheel instead of clipping at ~20 extra lines.
Uses the same pattern as HelpPanel: invisible geometry mask in scene
space covering the content area, with scrollable inner container.
Scroll position is persisted across re-renders (filter changes, node
expand/collapse) via state.scrollY.
- Expand tree content area: removed spurious REFRESH_BTN_HEIGHT (36px)
  subtraction (refresh button sits at the top with the filter, not the
  bottom) and reduced BOTTOM_PADDING from 60 to 10, reclaiming ~86px
  of vertical tree space.
- State Inspector wheel handler now checks pointer is within dialog
  bounds before scrolling, preventing scroll events from leaking to
  other overlays.
- Settings Panel wheel handler also checks pointer is within the
  panel bounds, preventing dual-scrolling when the inspector is open
  on top.
Create src/ui/Dialog.ts — reusable modal dialog with:
- Backdrop + centered overlay box
- Title with close button (✕)
- Scrollable content area with invisible geometry mask
- Bounds-checked wheel handler (pointer-in-box)
- Proper cleanup (event listener removal, object destruction)
- onClose callback for caller cleanup
- Auto-calculates content height for scroll clamping

Refactor all three debug overlays to use it:
- StateInspectorOverlay: ~80 lines removed (backdrop, title, close, mask,
  wheel handler, hudContainer parenting all handled by Dialog)
- GameEventLogOverlay: ~70 lines removed, now scrollable
- AiDecisionOverlay: ~70 lines removed, now scrollable

All dialogs now share identical scrolling behaviour (mask, wheel,
bounds checking) via the single Dialog component.
The separate Dialog.ts component duplicated the existing overlay
infrastructure. This commit removes it and integrates the scrollable
dialog functionality directly into Overlay.ts as
createScrollableOverlay(), keeping everything in one place.

Changes:
- Overlay.ts: Add createScrollableOverlay() factory + types. Updated
  module doc to highlight overlays are suitable for dialogs.
- ParameterizedOverlay.ts: Add module doc noting that scrollable
  overlays are the right choice for dialog-style content.
- Dialog.ts: Deleted (functionality merged into Overlay.ts).
- Debug overlays (StateInspector, GameEventLog, AiDecision): Import
  from Overlay.ts instead of Dialog.ts.
- ui/index.ts: Remove Dialog exports, add ScrollableOverlay exports.
ScrollableOverlay → OverlayDialog:
- createScrollableOverlay → createOverlayDialog
- ScrollableOverlayOptions → OverlayDialogOptions
- ScrollableOverlayHandle → OverlayDialogHandle

The key differentiator is that this is a managed dialog overlay
(lifecycle, content container, close/refresh) versus a static
config-driven overlay (ParameterizedOverlay). The scrolling is
an implementation detail.
The scroll container was created at (contentX, contentY) but
refresh() called scrollContainer.setY(-scrollY), which overrode
the container's Y position. With scrollY = 0, setY(0) moved the
container to y=0 — above the mask area at contentY — hiding all
content.

Fix: scrollContainer.y = contentY - scrollY (preserves contentY,
only offsets by scroll amount).
… counting, add port-based orphan cleanup

Replaced the fragile reference-counting pattern in dev-server-utils.ts
with a straightforward start-stop-per-call lifecycle:

- ensureDevServer() now kills any existing process on port 3000 before
  starting a fresh server (belt-and-suspenders via fuser/lsof).
- killDevServer() unconditionally kills the child process and cleans
  up any remaining process on port 3000.
- Lock file simplified to store only PID (no refCount).
- Added killProcessOnPort() helper for cross-platform port-based
  process termination (fuser on Linux, lsof on macOS).
- Signal handlers use the same port-based cleanup as fallback.
- Updated tests for simplified API.
- Updated docs/DEVELOPER.md troubleshooting sections.
…esolve cold-start timeout

The e2e replay test (replay-main-street.e2e.test.ts) failed when run alongside
the parallel unit test pool because Vite's cold compilation under CPU contention
exceeded the 30s (and later 60s/120s) waitForGameBoot timeout.

Root cause (two issues):

1. Signal handler overreach in dev-server-utils.ts: installDevServerCleanupHandlers()
   called killProcessOnPort(3000) in its SIGTERM/SIGINT/exit handlers. When other
   test workers finished (or timed out), their exit handlers killed ANY process on
   port 3000 — including the dev server spawned by the replay script's child process.

2. CPU contention: the replay e2e test shared the 'unit' Vitest project which runs
   all test files in parallel. The fresh Vite dev server's initial lazy compilation
   had to compete for CPU with hundreds of parallel unit tests.

Fixes:
- dev-server-utils.ts: Remove killProcessOnPort(3000) from signal handlers.
  Port-based cleanup is still performed by ensureDevServer() (before starting
  fresh) and killDevServer() (when our own child finishes) — these are the
  correct, scoped locations.
- vite.config.ts: Move replay e2e tests to a dedicated 'replay-e2e' Vitest
  project with pool: 'forks' + singleFork: true. This provides clean OS
  process isolation with no leftover signal handlers or stale lock files,
  and uncontested CPU for Vite's cold compilation.
- replay.ts: Add page.setDefaultTimeout + explicit page.goto timeout as
  belt-and-suspenders. Revert SCENE_READY_TIMEOUT to 30_000 (no longer needed).
- test config: Increase runReplay() timeout to 180s for headroom.
- docs: Add replay-e2e project to test project documentation.

Refs: CG-0MS35J1FX005QYDV (AC6)
Implement five strategic improvements to the Lost Cities AI:

1. Opponent Card Denial (Block Play) - Bonus for playing numbered cards
   the opponent could use to continue their expedition, denying them
   the card. Applies when extending existing expeditions.

2. Optimal Investment Timing - Prefer playing investments before numbered
   cards in the same color. Bonus for early investments with numbered
   cards to follow. Strong penalty for late investments (4+ cards).

3. Opponent Expedition Blocking - New scoreBlockingPlay() function
   rewards playing cards that fill gaps in opponent expeditions,
   preventing them from scoring highly.

4. Endgame / Deck-Count Awareness - When draw pile < 10: avoid starting
   new expeditions without enough cards, reduce probability penalties,
   increase discard penalties for opponent-wanted cards.

5. Score-Aware Multi-Column Strategy - Prioritize completing existing
   columns when none are scored. More willing to start new columns
   when at least one column is already positive.

All improvements maintain backward compatibility with existing
GreedyStrategy interface.

Tests: 57 tests pass (52 existing + 17 new)
…onEventCards

CG-0MS0Z9XDF001TAD4: Tests for generalized duration-mitigation

- Remove hardcoded dEvent.id === 'evt-flu-outbreak' guard so
  computeDurationWithClinicReduction applies to ALL DurationEventCards
- Add non-flu DurationEventCard tests in turnflow.test.ts:
  - Clinic reduces duration by 2
  - Medical Center reduces duration by 3
  - No clinic leaves duration unchanged
- All existing flu-outbreak integration tests still pass with identical behaviour

Build: npm run build ✓
Tests: 260 test files, 4537 passed, 10 skipped ✓
…Gini, HHI, CI)

- Implement median(), iqr(), gini(), hhi(), confidenceInterval() in scripts/balance/engine/statistics.ts
- 41 unit tests covering edge cases (empty arrays, single-element, negative values, floating point)
- All functions have typed signatures and JSDoc
- IQR uses Moore & McCabe exclusive method
- Gini uses standard formula returning 0 (equal) to (n-1)/n (max inequality)
- HHI accepts raw counts (fromCounts=true) or proportional shares
- confidenceInterval uses population variance with configurable z-score
… baseline module

- Implement guardrail thresholds (PRD §3.3) with evaluateGuardrails() in scripts/balance/guards/thresholds.ts
- Implement baseline capture/validation/loading in scripts/balance/engine/baseline.ts
- Create barrel files: scripts/balance/engine/index.ts, scripts/balance/index.ts, scripts/balance/utils/statistics.ts
- 21 unit tests for thresholds and baseline modules
- M1 (Pick Rate): computes purchases/market appearances, null when marketOffers absent
- M2 (Win-Rate Delta): computes winRate(owned) - winRate(not owned), null when cardsOwned absent
- M3 (Cost-to-Income Ratio): static computation from card CSV data, handles zero income (Infinity)
- M4 (Synergy Utilization): actual/max synergy bonuses, null when incomeBreakdown absent
- M5 (Upgrade Adoption): upgrades/parent purchases, null when cardsOwned absent
- M6 (Event Impact Score): avg(coinDelta + repDelta*5), fallback to CSV static deltas
- M7 (Survival Rate): wins(owned)/runs(owned), null when cardsOwned absent
- 29 unit tests across all 7 metrics
- Updated barrel file
- G1 (Win Rate × Difficulty): strategy/difficulty matrix entry
- G2 (Score Distribution): median, mean, Q1, Q3, IQR, stdDev, min, max
- G3 (Economy Health): avg coins/turn, bankruptcy rate, tightness index
- G4 (Synergy Diversity): HHI of synergy type shares, null when finalGrid absent
- G5 (Loss Mode Decomposition): bankruptcy/reputation/timeout shares
- G6 (Card Usage Diversity): Gini coefficient from won-run appearances
- G7 (Turn-by-Turn Snapshots): avg economy trajectory per turn
- G8 (Trap Card Prevalence): cards with winRateDelta < -10% AND pickRate > 20%
- 27 unit tests across all 8 metrics
- Phase 1 dependent metrics (G3, G4, G6, G7, G8) degrade to null
- Updated barrel file
…evaluation

- compareMetrics(current, baseline, thresholds?) returns structured ComparisonReport
- Per-metric comparison includes: current, baseline, delta, deltaPct, status
- Guardrail evaluation using thresholds.ts severity levels (pass/flag/fail)
- Zero baseline returns ±Infinity deltaPct
- Empty inputs produce structured empty report
- Threshold overrides supported via optional parameter
- Output matches PRD §6.5 format: meta, summary, comparisons
- 15 unit tests covering all edge cases
- Updated barrel file
- Integration tests (11 tests) covering: statistics→global-metrics pipeline,
  statistics→card-metrics pipeline, card-metrics+global-metrics→comparison,
  full pipeline with synthetic Phase 1 data, Phase 1 data absent (graceful
  degradation), non-overlapping comparison, static metrics, CIs
- API reference docs at docs/main-street/balance-analysis-api.md (all modules)
- PRD updated with actual file paths, implementation status, deviations notes
- 144 total balance tests passing
Rename checkPatronVisit to collectQualifyingPatrons, returning PatronTile[]
instead of a single PatronTile | null. All qualifying patrons are now moved
to the player's collection in the same turn.

Changes:
- TurnResult.patronVisit → patronVisits: PatronTile[] (array, possibly empty)
- checkPatronVisit() → collectQualifyingPatrons() returns all qualifying patrons
- finishTurn() accepts PatronTile[] parameter
- discardTokens() passes empty array for patron visits
- GameTranscript: FeudalismTurnRecord.patronVisit → patronVisits
- Turn controller: handles array, animates first patron, shows combined toast
- Animator: playCardAnimation accepts PatronTile[] array
- New tests: 0, 1, 2, 3+ patron scenarios, partial qualification, dedup
- All 4687 tests pass, build succeeds
Adds a '[ GitHub ]' link in the bottom-right corner of the game home
page that opens https://github.com/TheWizardsCode/Tableau-Card-Engine
in a new browser tab when clicked.

- Clickable text label styled consistently with the existing UI
  (monospace font, muted green color #88ff88, alpha 0.6)
- Positioned at bottom-right (GAME_W - 8, GAME_H - 12) with
  bottom-right anchor to avoid overlap with version label
- Uses Phaser text.on('pointerdown') with window.open() to open URL

Build: success
Tests: 33/33 passing in GameSelectorScene.test.ts
…at logo

Fixes two issues from review feedback:
1. Added setInteractive() to enable pointer events (missing before)
2. Replaced '[ GitHub ]' text label with the GitHub Octocat SVG icon
   loaded from an inline data URI in preload()
3. Added hidden alt text 'GitHub repository' for accessibility

Build: success
Tests: 35/35 passing in GameSelectorScene.test.ts
- Moved from bottom-right to top-right corner with 10px margin
- SVG fill changed from #88ff88 to #ffffff (white octocat)
- Icon size increased to 28x28 for better visibility at top
- Removed alpha transparency (full opacity white on dark bg)
- Changed origin from (0.5,0.5) to (1,0) for top-right anchoring
- Removed unused VERSION_ALPHA import

Tests: 36/36 passing
… multi-patron visibility)

When multiple patrons qualify in a single turn:
- Previously: only the first patron was animated; others were collected silently
- Fixed: all qualifying patrons now get individual fly-in animations chained
  sequentially via new chainAllPatronAnimations() method

Tests added:
- Purchase qualifying two patrons simultaneously
- Take-different with two pre-qualified patrons
- Patron collection surviving token-over-limit discard flow
- Confirms collectQualifyingPatrons returns all qualifying patrons
@SorraTheOrc
SorraTheOrc merged commit b6867d5 into main Jul 29, 2026
@SorraTheOrc
SorraTheOrc deleted the release/dev-to-main-20260729160734 branch July 29, 2026 16:08
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