Skip to content

Feat/world map - #401

Merged
ElderEvil merged 23 commits into
masterfrom
feat/world-map
Aug 7, 2026
Merged

Feat/world map#401
ElderEvil merged 23 commits into
masterfrom
feat/world-map

Conversation

@ElderEvil

@ElderEvil ElderEvil commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Summary by CodeRabbit

  • New Features
    • Added an interactive World Map accessible from vault navigation.
    • Displayed wasteland locations, vault markers, linked dwellers, and location details.
    • Added procedural exploration discoveries with named locations.
    • Automatically recorded dweller origins, visited places, and newborn home origins on the map.
    • Added automatic map updates while viewing a vault.
  • Bug Fixes
    • Limited extended biographies to the supported length while preserving generation.
  • Documentation
    • Updated the changelog and roadmap for version 2.24.0.

@coderabbitai

coderabbitai Bot commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Warning

Review limit reached

@ElderEvil, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 12 minutes

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9a017c71-cb3a-457c-9ad6-f7c7416f01d1

📥 Commits

Reviewing files that changed from the base of the PR and between 6383af9 and 812b0f7.

⛔ Files ignored due to path filters (1)
  • frontend/pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (2)
  • .github/dependabot.yml
  • frontend/package.json
📝 Walkthrough

Walkthrough

Version 2.24.0 adds persisted wasteland locations, procedural discovery events, map APIs, resilient dweller-place registration, deterministic vault markers, and an authenticated frontend world-map experience with polling and marker details.

Changes

World Map and Discovery

Layer / File(s) Summary
Location persistence and map contracts
backend/app/models/*, backend/app/alembic/..., backend/app/crud/*, backend/app/schemas/wasteland_location.py, backend/app/utils/places.py, backend/app/tests/test_models/*, backend/app/tests/test_crud/*, backend/app/tests/test_utils/*
Added wasteland-location and dweller-location models, database tables, CRUD operations, API response schemas, deterministic coordinates, and seeded vault markers.
Map registration and assembly
backend/app/services/map_service.py, backend/app/services/dweller_ai.py, backend/app/services/breeding_service.py, backend/app/tests/test_services/test_map_service.py, backend/app/tests/test_services/test_dweller_ai_map.py, backend/app/tests/test_services/test_newborn_origin.py
Added home-marker creation, bio-place registration, discovery registration, dweller links, vault-map assembly, biography length enforcement, and newborn origin linking.
Discovery events and location metadata
backend/app/agents/dweller_agents.py, backend/app/schemas/dweller_ai.py, backend/app/schemas/exploration_event.py, backend/app/services/exploration/*, backend/app/models/exploration.py, backend/app/core/game_config.py, backend/app/data/exploration/*, backend/app/tests/test_services/test_discovery_events.py
Added structured AI place fields, discovery-event generation, configurable discovery probability, location persistence, discovery-name loading, and resilient exploration registration.
Map API access and response validation
backend/app/api/v1/api.py, backend/app/api/v1/endpoints/map.py, backend/app/tests/test_api/test_map.py
Registered authenticated map endpoints for vault maps and location details. Tests cover authorization, tenant isolation, missing locations, dweller references, and OpenAPI schemas.
Frontend map experience
frontend/src/modules/map/*, frontend/src/router/index.ts, frontend/src/core/components/common/SidePanel.vue, frontend/tests/unit/components/map/*, frontend/tests/unit/modules/map/*, frontend/tests/unit/stores/map.test.ts
Added the lazy map route, typed API service, Pinia state and polling, SVG map, marker components, detail modal, navigation entry, and unit coverage.
Release documentation and regression coverage
CHANGELOG.md, ROADMAP.md, backend/pyproject.toml, frontend/package.json, backend/app/tests/integration/test_quota_enforcement.py, backend/app/tests/test_api/test_system.py
Added release entries, roadmap milestones, aligned project versions, updated backstory fixtures, and changelog-version validation.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant MapView
  participant useMapStore
  participant MapAPI
  participant MapService
  participant Database

  User->>MapView: Open vault map
  MapView->>useMapStore: fetchMap(vaultId, token)
  useMapStore->>MapAPI: GET vault map
  MapAPI->>MapService: get_vault_map
  MapService->>Database: Load locations and dweller references
  Database-->>MapService: Map data
  MapService-->>MapAPI: VaultMapResponse
  MapAPI-->>useMapStore: VaultMapResponse
  useMapStore-->>MapView: Render locations and vault markers
Loading

Possibly related PRs

  • ElderEvil/falloutProject#197: Both PRs modify backend/app/services/breeding_service.py, although this PR adds newborn-origin map linking while the related PR refactors conception checks.
  • ElderEvil/falloutProject#366: Both PRs modify Alembic migrations and PostgreSQL enum synchronization guidance.
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 76.60% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the main change: adding the world map feature.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/world-map

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 11

🧹 Nitpick comments (6)
backend/app/tests/test_api/test_system.py (1)

31-37: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use the runtime version instead of a second literal.

The assertion hard-codes "2.24.0", while test_get_changelog_latest_matches_app_version already calls get_app_version(). A future version bump will require editing this assertion even when backend/pyproject.toml and CHANGELOG.md are consistent. Compare data["version"] with get_app_version() here as well.

Proposed change
+        from app.utils.version import get_app_version
+
+        expected_version = get_app_version()
-        assert data["version"] == "2.24.0", (
+        assert data["version"] == expected_version, (
🤖 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 `@backend/app/tests/test_api/test_system.py` around lines 31 - 37, Update
test_get_changelog_latest_matches_app_version to compare data["version"] with
the existing get_app_version() result instead of the hard-coded "2.24.0"
literal, while preserving the current mismatch error context and other
assertions.
backend/app/tests/test_utils/test_places.py (1)

17-139: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add return types to the test methods.

Each new test_* method omits -> None. Add the return annotation to every test method in this file.

As per coding guidelines, backend/**/*.py requires: “Add type hints to new functions and prefer UUID4/Pydantic types where applicable.”

🤖 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 `@backend/app/tests/test_utils/test_places.py` around lines 17 - 139, Add the
return annotation -> None to every test_* method shown in TestSchematicCoords,
TestCollisionNudge, and TestSeededVaultSpecs, as well as the preceding
normalization tests. Do not change test behavior or method parameters.

Source: Coding guidelines

backend/app/services/dweller_ai.py (1)

52-59: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Log the suppressed exception instead of discarding it.

with suppress(Exception) hides every failure in this helper without a log record. map_service.register_bio_places logs its own internal failures, but any error raised outside that scope (for example an attribute error on dweller_obj) disappears. Replace the suppression with an except clause that calls logger.exception.

As per coding guidelines: "log from services with logging.getLogger(__name__) and use logger.exception for unexpected errors."

♻️ Proposed change to log the failure
         """Register bio-extracted places on the world map — best-effort, never raises."""
-        with suppress(Exception):
-            await map_service.register_bio_places(
-                db_session,
-                dweller_obj,
-                origin_place=origin_place,
-                visited_places=visited_places,
-                explicit_origin=explicit_origin,
-            )
+        try:
+            await map_service.register_bio_places(
+                db_session,
+                dweller_obj,
+                origin_place=origin_place,
+                visited_places=visited_places,
+                explicit_origin=explicit_origin,
+            )
+        except Exception:
+            logger.exception("Map place registration failed for dweller %s", dweller_obj.id)

Remove the now unused from contextlib import suppress import at line 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 `@backend/app/services/dweller_ai.py` around lines 52 - 59, Replace the
suppress(Exception) wrapper around map_service.register_bio_places with explicit
exception handling that calls logger.exception for unexpected failures,
preserving the helper’s non-raising behavior. Ensure the module defines logger
via logging.getLogger(__name__), and remove the now-unused contextlib.suppress
import.

Source: Coding guidelines

backend/app/tests/test_services/test_discovery_events.py (1)

44-60: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert that the loops produce events, so the tests cannot pass vacuously.

Both loops call continue when generate_event returns None. If the generator gates on the time since the last recorded event, then add_event inside the loop can suppress every later draw. In that case test_no_discovery_when_chance_is_0 verifies far fewer than 200 draws, and test_discovery_event_generated_when_chance_is_1 can assert nothing at all. Count the generated events and assert the count is greater than zero.

💚 Proposed change for the chance-1.0 test
     with patch.object(game_config.exploration, "event_discovery_chance", 1.0):
+        generated = 0
         for _ in range(20):
             event = event_generator.generate_event(exploration)
             if event is None:
                 continue
+            generated += 1
             assert isinstance(event, DiscoveryEventSchema), f"Expected discovery, got {type(event).__name__}"
             exploration.add_event(
                 event_type=event.type,
                 description=event.description,
                 location_name=event.location_name,
             )
+    assert generated > 0, "No events were generated; the assertions never ran"

Apply the same counter to test_no_discovery_when_chance_is_0.

Also applies to: 121-132

🤖 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 `@backend/app/tests/test_services/test_discovery_events.py` around lines 44 -
60, Update both discovery-event tests around event_generator.generate_event in
the chance-1.0 and chance-0.0 loops to count non-None generated events,
incrementing the counter before continuing or processing the event, and assert
the count is greater than zero after each loop so neither test can pass
vacuously.
backend/app/services/breeding_service.py (1)

344-344: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse one vault lookup and extract the post-delivery side effects.

deliver_baby now loads the same vault twice: at line 438 through vault_crud_for_map and at line 461 through vault_crud. Both are the same CRUD object imported under two aliases with the same argument mother.vault_id. Load the vault once and pass it to both blocks.

The # noqa: PLR0915 at line 344 suppresses the statement-count rule instead of reducing the function. Extracting the two best-effort post-delivery blocks into private helpers removes the need for the suppression.

♻️ Proposed helper extraction
    `@staticmethod`
    async def _link_newborn_to_home(db_session: AsyncSession, child: Dweller, vault) -> None:
        """Link the newborn to the vault home marker — best-effort."""
        from app.services.map_service import map_service

        try:
            await map_service.link_home_origin(db_session, child, vault)
        except Exception:
            logger.exception(
                "Failed to link home origin for newborn: child=%s vault=%s", child.id, vault.id
            )
-        # Link newborn to home vault on world map (best-effort, non-critical)
-        try:
-            from app.crud.vault import vault as vault_crud_for_map
-            from app.services.map_service import map_service
-
-            vault_for_map = await vault_crud_for_map.get(db_session, mother.vault_id)
-            if vault_for_map:
-                await map_service.link_home_origin(db_session, child, vault_for_map)
-        except Exception:
-            logger.exception(
-                "Failed to link home origin for newborn: child=%s vault=%s",
-                child.id,
-                mother.vault_id,
-            )
+        from app.crud.vault import vault as vault_crud
+
+        vault = await vault_crud.get(db_session, mother.vault_id)
+        if vault:
+            await BreedingService._link_newborn_to_home(db_session, child, vault)

Then reuse the same vault value in the statistics block instead of loading it again.

Also applies to: 433-447

🤖 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 `@backend/app/services/breeding_service.py` at line 344, Update deliver_baby to
remove the PLR0915 suppression, load mother.vault_id once into a shared vault
value, and reuse it in both post-delivery blocks instead of calling
vault_crud_for_map and vault_crud separately. Extract the best-effort
home-linking and statistics side effects into private helpers, including
_link_newborn_to_home with exception logging, and invoke those helpers from
deliver_baby while preserving existing behavior.
backend/app/services/map_service.py (1)

99-106: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Widen or correct the dweller parameter type.

register_bio_places declares dweller: Dweller. The production caller in backend/app/services/dweller_ai.py (lines 42-59) passes dweller_obj: DwellerReadFull. The method only reads dweller.id and dweller.vault_id, so runtime works, but the annotation is incorrect for the main caller. Declare a Protocol with id and vault_id, or accept Dweller | DwellerReadFull, or accept the two identifiers directly.

As per coding guidelines: "Add type hints to new functions and prefer UUID4/Pydantic types where applicable."

♻️ Proposed change to accept identifiers explicitly
-    async def register_bio_places(
-        self,
-        db_session: AsyncSession,
-        dweller: Dweller,
-        origin_place: str,
-        visited_places: list[str],
-        explicit_origin: str | None = None,
-    ) -> None:
+    async def register_bio_places(
+        self,
+        db_session: AsyncSession,
+        dweller: Dweller | DwellerReadFull,
+        origin_place: str,
+        visited_places: list[str],
+        explicit_origin: str | None = None,
+    ) -> None:
🤖 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 `@backend/app/services/map_service.py` around lines 99 - 106, Correct the
dweller parameter typing in register_bio_places to match its production caller
and actual usage of only id and vault_id. Prefer a small Protocol exposing those
attributes, or explicitly support both Dweller and DwellerReadFull, while
preserving the existing method behavior.

Source: Coding guidelines

🤖 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
`@backend/app/alembic/versions/2026_08_06_2353-edb924d8dbeb_add_wasteland_locations.py`:
- Around line 47-48: Add named database CHECK constraints enforcing coord_x and
coord_y values from 0 through 100 in
backend/app/alembic/versions/2026_08_06_2353-edb924d8dbeb_add_wasteland_locations.py:47-48.
Add matching sa.CheckConstraint entries to WastelandLocation.__table_args__
alongside the unique constraint in
backend/app/models/wasteland_location.py:34-35, and add commit-level tests
rejecting negative and greater-than-100 coordinates in
backend/app/tests/test_models/test_wasteland_location.py:62-88.

In `@backend/app/api/v1/endpoints/map.py`:
- Around line 37-56: Move the CRUD lookup, vault ownership validation, dweller
reference retrieval, and WastelandLocationWithDwellers construction out of the
endpoint’s get_location_detail function into a map_service.get_location_detail
method. Keep the endpoint limited to parsing dependencies/parameters and
delegating to the service, preserving the existing not-found behavior and
response fields.

In `@backend/app/crud/wasteland_location.py`:
- Around line 72-103: Update the WastelandLocation model/schema to add a unique
constraint covering vault_id, coord_x, and coord_y, and ensure the corresponding
migration is included. In the creation flow around collision_nudge and the
IntegrityError handler, distinguish coordinate conflicts from normalized-name
conflicts, retry coordinate selection and insertion after rolling back, and
retain the existing get_by_normalized lookup for name conflicts.

In `@backend/app/schemas/dweller_ai.py`:
- Around line 20-24: Update both visited_places fields in the relevant schema to
apply a max_length of 64 to each string item using an item-level constrained
type such as Annotated[str, Field(max_length=64)]. Preserve the existing
list-level max_length of 5 and field defaults while ensuring names exceeding 64
characters are rejected before MapService.register_bio_places.

In `@backend/app/services/map_service.py`:
- Around line 76-85: The map helper transaction handling in
get_or_create/link_dweller must not commit or roll back the caller’s
AsyncSession, because register_discovery may have pending Exploration work. Move
map create/link operations to an independent session or an isolated nested
transaction so only map changes are committed or rolled back while the caller
transaction remains intact.

In `@backend/app/tests/test_services/test_newborn_origin.py`:
- Around line 78-86: Add UUID4 annotations to mother_id and father_id in
_create_due_pregnancy, and annotate its return value with the pregnancy model
type returned by BreedingService.create_pregnancy. Add -> None to both async
test functions in the affected section, preserving their existing behavior.

In `@frontend/src/modules/map/components/MapMarker.vue`:
- Around line 44-59: Make the map actions keyboard accessible: in
frontend/src/modules/map/components/MapMarker.vue lines 44-59, make the marker
<g> focusable and trigger its existing click emission on Enter and Space; in
frontend/src/modules/map/components/WorldMap.vue lines 45-46, remove the
whole-SVG role="img" aria-label treatment so interactive MapMarker children
remain accessible; in frontend/src/modules/map/components/MarkerDetailModal.vue
lines 92-100, replace each dweller navigation action with a button supporting
native Enter/Space activation.

In `@frontend/src/modules/map/stores/map.ts`:
- Around line 16-69: Update the polling callback created by useIntervalFn and
the startPolling/stopPolling lifecycle to prevent stale responses from
committing map state: capture the vault ID and token when each request starts,
assign it a monotonically increasing generation, and only update locations and
vaultMarkers when the generation and captured context still match the active
polling context. Invalidate the generation when polling is switched or stopped,
and add a deferred-promise test covering a request resolving after a context
change or stop.

In `@frontend/src/modules/map/views/MapView.vue`:
- Around line 77-82: Update the MapView template’s conditional rendering around
hasNoData to check mapStore.error before showing the empty state, displaying the
request failure message instead of “uncharted.” Add a retry action that invokes
the existing map-fetch flow, while preserving the current empty-state behavior
for successful responses with no map data.
- Around line 42-48: Update the MapView.vue lifecycle around onMounted to watch
vaultId changes, stopping the existing mapStore polling context before fetching
and starting polling for the new vault. Guard fetch results and mapStore commits
so delayed responses from a previous vault cannot overwrite the currently
selected vault’s map.

In `@ROADMAP.md`:
- Line 14: Update the v2.25.0 roadmap entry to describe the actual PostgreSQL
enum synchronization path: do not imply that Alembic’s compare_type=True
generates enum value migrations; document the explicit migration strategy or
enum autogenerate extension, including ALTER TYPE handling for added/renamed
labels and the required replacement approach for removed labels. Add regression
coverage for both added and removed enum labels.

---

Nitpick comments:
In `@backend/app/services/breeding_service.py`:
- Line 344: Update deliver_baby to remove the PLR0915 suppression, load
mother.vault_id once into a shared vault value, and reuse it in both
post-delivery blocks instead of calling vault_crud_for_map and vault_crud
separately. Extract the best-effort home-linking and statistics side effects
into private helpers, including _link_newborn_to_home with exception logging,
and invoke those helpers from deliver_baby while preserving existing behavior.

In `@backend/app/services/dweller_ai.py`:
- Around line 52-59: Replace the suppress(Exception) wrapper around
map_service.register_bio_places with explicit exception handling that calls
logger.exception for unexpected failures, preserving the helper’s non-raising
behavior. Ensure the module defines logger via logging.getLogger(__name__), and
remove the now-unused contextlib.suppress import.

In `@backend/app/services/map_service.py`:
- Around line 99-106: Correct the dweller parameter typing in
register_bio_places to match its production caller and actual usage of only id
and vault_id. Prefer a small Protocol exposing those attributes, or explicitly
support both Dweller and DwellerReadFull, while preserving the existing method
behavior.

In `@backend/app/tests/test_api/test_system.py`:
- Around line 31-37: Update test_get_changelog_latest_matches_app_version to
compare data["version"] with the existing get_app_version() result instead of
the hard-coded "2.24.0" literal, while preserving the current mismatch error
context and other assertions.

In `@backend/app/tests/test_services/test_discovery_events.py`:
- Around line 44-60: Update both discovery-event tests around
event_generator.generate_event in the chance-1.0 and chance-0.0 loops to count
non-None generated events, incrementing the counter before continuing or
processing the event, and assert the count is greater than zero after each loop
so neither test can pass vacuously.

In `@backend/app/tests/test_utils/test_places.py`:
- Around line 17-139: Add the return annotation -> None to every test_* method
shown in TestSchematicCoords, TestCollisionNudge, and TestSeededVaultSpecs, as
well as the preceding normalization tests. Do not change test behavior or method
parameters.
🪄 Autofix

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: 721bd47d-a588-405b-8daa-a16d60da2492

📥 Commits

Reviewing files that changed from the base of the PR and between 1e7b9e5 and 8afec34.

⛔ Files ignored due to path filters (2)
  • backend/uv.lock is excluded by !**/*.lock
  • frontend/src/core/types/api.generated.ts is excluded by !**/*.generated.*
📒 Files selected for processing (53)
  • CHANGELOG.md
  • ROADMAP.md
  • backend/app/agents/dweller_agents.py
  • backend/app/alembic/versions/2026_08_06_2353-edb924d8dbeb_add_wasteland_locations.py
  • backend/app/api/v1/api.py
  • backend/app/api/v1/endpoints/map.py
  • backend/app/core/game_config.py
  • backend/app/crud/__init__.py
  • backend/app/crud/wasteland_location.py
  • backend/app/data/exploration/discovery_names.json
  • backend/app/models/__init__.py
  • backend/app/models/exploration.py
  • backend/app/models/wasteland_location.py
  • backend/app/schemas/dweller_ai.py
  • backend/app/schemas/exploration_event.py
  • backend/app/schemas/wasteland_location.py
  • backend/app/services/breeding_service.py
  • backend/app/services/dweller_ai.py
  • backend/app/services/exploration/coordinator.py
  • backend/app/services/exploration/data_loader.py
  • backend/app/services/exploration/event_generator.py
  • backend/app/services/map_service.py
  • backend/app/tests/integration/test_quota_enforcement.py
  • backend/app/tests/test_api/test_map.py
  • backend/app/tests/test_api/test_system.py
  • backend/app/tests/test_crud/test_wasteland_location.py
  • backend/app/tests/test_models/test_wasteland_location.py
  • backend/app/tests/test_services/test_discovery_events.py
  • backend/app/tests/test_services/test_dweller_ai_map.py
  • backend/app/tests/test_services/test_exploration_service.py
  • backend/app/tests/test_services/test_map_service.py
  • backend/app/tests/test_services/test_newborn_origin.py
  • backend/app/tests/test_utils/test_places.py
  • backend/app/utils/places.py
  • backend/pyproject.toml
  • frontend/package.json
  • frontend/src/core/components/common/SidePanel.vue
  • frontend/src/modules/exploration/models/exploration.ts
  • frontend/src/modules/map/README.md
  • frontend/src/modules/map/components/MapMarker.vue
  • frontend/src/modules/map/components/MarkerDetailModal.vue
  • frontend/src/modules/map/components/WorldMap.vue
  • frontend/src/modules/map/index.ts
  • frontend/src/modules/map/models/map.ts
  • frontend/src/modules/map/routes/index.ts
  • frontend/src/modules/map/services/mapService.ts
  • frontend/src/modules/map/stores/map.ts
  • frontend/src/modules/map/views/MapView.vue
  • frontend/src/router/index.ts
  • frontend/tests/unit/components/map/MarkerDetailModal.test.ts
  • frontend/tests/unit/components/map/WorldMap.test.ts
  • frontend/tests/unit/modules/map/routes.test.ts
  • frontend/tests/unit/stores/map.test.ts

Comment thread backend/app/api/v1/endpoints/map.py Outdated
Comment thread backend/app/crud/wasteland_location.py Outdated
Comment on lines +72 to +103
# Gather occupied coordinates for this vault
occupied_result = await db_session.execute(
select(WastelandLocation.coord_x, WastelandLocation.coord_y).where(WastelandLocation.vault_id == vault_id)
)
occupied: set[tuple[float, float]] = {(rx, ry) for rx, ry in occupied_result.all()}

coord_x, coord_y = collision_nudge((base_x, base_y), occupied)

obj = WastelandLocation(
name=name[:64],
normalized_name=normalized,
type=type,
coord_x=coord_x,
coord_y=coord_y,
description=description,
vault_id=vault_id,
exploration_id=exploration_id,
)
db_session.add(obj)
try:
await db_session.commit()
await db_session.refresh(obj)
return obj
except IntegrityError:
# Race: another request already inserted this name
await db_session.rollback()
# Re-fetch the existing row
existing = await self.get_by_normalized(db_session, vault_id, normalized)
if existing is not None:
return existing
# Should not happen — re-raise if we still can't find it
raise

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make coordinate allocation atomic.

Lines 72-78 select a coordinate from an unreserved snapshot. Concurrent requests with different names that hash to the same coordinate can select the same point. Without a database uniqueness constraint, the map stores overlapping markers. With a uniqueness constraint, Lines 95-103 reselect only by normalized name and re-raise the coordinate conflict for one request.

Add a unique vault-coordinate constraint. Retry coordinate selection after a coordinate conflict. Keep the existing normalized-name lookup for name conflicts.

🤖 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 `@backend/app/crud/wasteland_location.py` around lines 72 - 103, Update the
WastelandLocation model/schema to add a unique constraint covering vault_id,
coord_x, and coord_y, and ensure the corresponding migration is included. In the
creation flow around collision_nudge and the IntegrityError handler, distinguish
coordinate conflicts from normalized-name conflicts, retry coordinate selection
and insertion after rolling back, and retain the existing get_by_normalized
lookup for name conflicts.

Comment thread backend/app/schemas/dweller_ai.py Outdated
Comment on lines +76 to +85
try:
await db_session.commit()
await db_session.refresh(obj)
return obj
except IntegrityError:
await db_session.rollback()
existing = await wl_crud.get_by_normalized(db_session, vault.id, normalized)
if existing is not None:
return existing
raise

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect commit/rollback usage in the wasteland_location CRUD and its callers.
set -euo pipefail

fd -t f 'wasteland_location.py' backend/app/crud --exec cat -n {}

echo "--- map_service call sites ---"
rg -n -C 4 'map_service\.(ensure_home_marker|link_home_origin|register_bio_places|register_discovery|get_vault_map)' backend/app --glob '!**/tests/**'

Repository: ElderEvil/falloutProject

Length of output: 11529


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- map_service outline/size ---"
wc -l backend/app/services/map_service.py
ast-grep outline backend/app/services/map_service.py 2>/dev/null || true
echo "--- relevant map_service sections ---"
sed -n '1,180p' backend/app/services/map_service.py | cat -n
echo "--- breeding_service around ensure_dweller_location/link_home_origin call ---"
sed -n '380,470p' backend/app/services/breeding_service.py | cat -n
echo "--- exploration coordinator around discovery call ---"
sed -n '1,150p' backend/app/services/exploration/coordinator.py | cat -n
echo "--- dweller_ai map registration ---"
sed -n '1,100p' backend/app/services/dweller_ai.py | cat -n

echo "--- session dependencies and current autocommit/pending status ---"
rg -n -C 3 'create_async_session|async_session|AsyncSession|get_async_session|commit\(|rollback\(\)|begin|begin_nested|autocommit|expire_no_pending|is_active|pending' backend/app --glob '*.py' --glob '!**/tests/**'

Repository: ElderEvil/falloutProject

Length of output: 50381


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- locate db_session.py ---"
fd -t f 'session.py' backend/app/db

echo "--- get_async_session implementation ---"
while IFS= read -r file; do
  echo "### $file"
  sed -n '1,220p' "$file" | cat -n
done < <(fd -t f 'session.py' backend/app/db)

echo "--- breeding_service critical section ---"
sed -n '340,455p' backend/app/services/breeding_service.py | cat -n

echo "--- exploration coordinator critical context ---"
sed -n '1,140p' backend/app/services/exploration/coordinator.py | cat -n

echo "--- dweller_ai critical context ---"
sed -n '1,80p' backend/app/services/dweller_ai.py | cat -n

echo "--- focused map_service methods ---"
rg -n -C 8 'ensure_home_marker|link_home_origin|register_bio_places|register_discovery|get_vault_map' backend/app/services/map_service.py backend/app/services --glob '!**/tests/**'

echo "--- focused rollback/commit/transaction helpers ---"
rg -n -C 3 'async def .*rollback|rollback\(\)|begin\(|begin_nested\(|expire_no_pending|pending|is_active|autocommit|AsyncEngine|AsyncSession' backend/app/db backend/app --glob '*.py' --glob '!**/tests/**' --max-count 200

Repository: ElderEvil/falloutProject

Length of output: 311


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "--- get_async_session implementation ---"
sed -n '1,220p' backend/app/db/session.py | cat -n

echo "--- breeding_service critical section ---"
sed -n '340,455p' backend/app/services/breeding_service.py | cat -n

echo "--- exploration coordinator critical context ---"
sed -n '1,140p' backend/app/services/exploration/coordinator.py | cat -n

echo "--- dweller_ai critical context ---"
sed -n '1,80p' backend/app/services/dweller_ai.py | cat -n

echo "--- focused map_service methods ---"
rg -n -C 8 'ensure_home_marker|link_home_origin|register_bio_places|register_discovery|get_vault_map' backend/app/services/map_service.py backend/app/services --glob '!**/tests/**'

echo "--- focused transaction helpers ---"
rg -n -C 3 'rollback\(\)|begin\(|begin_nested\(|expire_no_pending|pending|is_active|autocommit|AsyncEngine|AsyncSession' backend/app/db backend/app --glob '*.py' --glob '!**/tests/**' --max-count 200

Repository: ElderEvil/falloutProject

Length of output: 50381


🏁 Script executed:

#!/bin/bash
tmplog=$(mktemp)
tmplog2=$(mktemp)
set -euo pipefail

echo "--- get_async_session implementation ---"
sed -n '1,220p' backend/app/db/session.py | cat -n > "$tmplog"
cat "$tmplog"

echo "--- targeted breeding_service context ---"
sed -n '390,450p' backend/app/services/breeding_service.py | cat -n > "$tmplog"
cat "$tmplog"

echo "--- targeted exploration coordinator context ---"
sed -n '55,130p' backend/app/services/exploration/coordinator.py | cat -n > "$tmplog"
cat "$tmplog"

echo "--- targeted dweller_ai context ---"
sed -n '40,65p' backend/app/services/dweller_ai.py | cat -n > "$tmplog"
cat "$tmplog"

echo "--- focused map_service methods ---"
rg -n -C 10 'ensure_home_marker|link_home_origin|register_bio_places|register_discovery|get_vault_map' backend/app/services/map_service.py > "$tmplog2"
cat "$tmplog2"

echo "--- focused transaction helpers ---"
rg -n -C 3 'rollback\(\)|begin\(|begin_nested\(|expire_no_pending|pending|is_active|autocommit|AsyncEngine|AsyncSession' backend/app/db backend/app/db/*.py backend/app -g '*.py' -g '!**/tests/**' | head -n 240

Repository: ElderEvil/falloutProject

Length of output: 28940


Do not issue or discard transactions on the caller session inside best-effort map helpers.

get_async_session() yields a manual AsyncSession, and get_or_create() / link_dweller() commit. register_discovery() is called after _handle_loot_event adds an Exploration, and that added exploration is not yet committed. An IntegrityError in the map helper then rolls back the caller session and loses the pending exploration event. Move map create/link operations out of the caller transaction, or use a nested scope that only rolls back the map work.

🤖 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 `@backend/app/services/map_service.py` around lines 76 - 85, The map helper
transaction handling in get_or_create/link_dweller must not commit or roll back
the caller’s AsyncSession, because register_discovery may have pending
Exploration work. Move map create/link operations to an independent session or
an isolated nested transaction so only map changes are committed or rolled back
while the caller transaction remains intact.

Comment thread frontend/src/modules/map/components/MapMarker.vue Outdated
Comment thread frontend/src/modules/map/stores/map.ts
Comment thread frontend/src/modules/map/views/MapView.vue Outdated
Comment thread frontend/src/modules/map/views/MapView.vue
Comment thread ROADMAP.md Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 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 `@backend/app/tests/test_utils/test_dweller_ai_schemas.py`:
- Around line 9-35: Add a -> None return annotation to each new test function:
test_dweller_backstory_visited_places_65_chars_rejected,
test_dweller_backstory_visited_places_64_chars_valid, and
test_extended_bio_visited_places_65_chars_rejected. Do not alter their test
logic.
🪄 Autofix

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: b3b83c20-ba0d-4e55-9871-5410f65753c2

📥 Commits

Reviewing files that changed from the base of the PR and between 8afec34 and 6383af9.

📒 Files selected for processing (24)
  • ROADMAP.md
  • backend/app/alembic/versions/2026_08_07_1023-4408ea93bd5b_add_wasteland_location_coord_constraints.py
  • backend/app/api/v1/endpoints/map.py
  • backend/app/crud/wasteland_location.py
  • backend/app/models/wasteland_location.py
  • backend/app/schemas/dweller_ai.py
  • backend/app/services/breeding_service.py
  • backend/app/services/dweller_ai.py
  • backend/app/services/map_service.py
  • backend/app/tests/test_api/test_system.py
  • backend/app/tests/test_crud/test_wasteland_location.py
  • backend/app/tests/test_models/test_wasteland_location.py
  • backend/app/tests/test_services/test_discovery_events.py
  • backend/app/tests/test_services/test_newborn_origin.py
  • backend/app/tests/test_utils/test_dweller_ai_schemas.py
  • backend/app/tests/test_utils/test_places.py
  • frontend/src/modules/map/components/MapMarker.vue
  • frontend/src/modules/map/components/MarkerDetailModal.vue
  • frontend/src/modules/map/components/WorldMap.vue
  • frontend/src/modules/map/stores/map.ts
  • frontend/src/modules/map/views/MapView.vue
  • frontend/tests/unit/components/map/MarkerDetailModal.test.ts
  • frontend/tests/unit/components/map/WorldMap.test.ts
  • frontend/tests/unit/stores/map.test.ts
🚧 Files skipped from review as they are similar to previous changes (13)
  • frontend/tests/unit/components/map/WorldMap.test.ts
  • frontend/src/modules/map/views/MapView.vue
  • frontend/tests/unit/components/map/MarkerDetailModal.test.ts
  • backend/app/api/v1/endpoints/map.py
  • backend/app/tests/test_api/test_system.py
  • backend/app/models/wasteland_location.py
  • backend/app/schemas/dweller_ai.py
  • frontend/src/modules/map/components/MarkerDetailModal.vue
  • backend/app/crud/wasteland_location.py
  • frontend/src/modules/map/components/WorldMap.vue
  • ROADMAP.md
  • frontend/src/modules/map/stores/map.ts
  • backend/app/tests/test_services/test_discovery_events.py

Comment on lines +9 to +35
def test_dweller_backstory_visited_places_65_chars_rejected():
"""visited_places item > 64 chars raises pydantic.ValidationError."""
with pytest.raises(pydantic.ValidationError):
DwellerBackstory(
bio="A test bio for backstory validation.",
origin_place="Megaton",
visited_places=["x" * 65],
)


def test_dweller_backstory_visited_places_64_chars_valid():
"""visited_places item == 64 chars is valid (edge case)."""
result = DwellerBackstory(
bio="A test bio for backstory validation.",
origin_place="Megaton",
visited_places=["x" * 64],
)
assert result.visited_places == ["x" * 64]


def test_extended_bio_visited_places_65_chars_rejected():
"""visited_places item > 64 chars raises pydantic.ValidationError on ExtendedBio."""
with pytest.raises(pydantic.ValidationError):
ExtendedBio(
extended_bio="More details about the dweller.",
visited_places=["y" * 65],
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Add return annotations to the new test functions.

Add -> None to all three test functions.

Proposed fix
-def test_dweller_backstory_visited_places_65_chars_rejected():
+def test_dweller_backstory_visited_places_65_chars_rejected() -> None:
@@
-def test_dweller_backstory_visited_places_64_chars_valid():
+def test_dweller_backstory_visited_places_64_chars_valid() -> None:
@@
-def test_extended_bio_visited_places_65_chars_rejected():
+def test_extended_bio_visited_places_65_chars_rejected() -> None:

As per coding guidelines, backend/**/*.py requires: “Add type hints to new functions and prefer UUID4/Pydantic types where applicable.”

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
def test_dweller_backstory_visited_places_65_chars_rejected():
"""visited_places item > 64 chars raises pydantic.ValidationError."""
with pytest.raises(pydantic.ValidationError):
DwellerBackstory(
bio="A test bio for backstory validation.",
origin_place="Megaton",
visited_places=["x" * 65],
)
def test_dweller_backstory_visited_places_64_chars_valid():
"""visited_places item == 64 chars is valid (edge case)."""
result = DwellerBackstory(
bio="A test bio for backstory validation.",
origin_place="Megaton",
visited_places=["x" * 64],
)
assert result.visited_places == ["x" * 64]
def test_extended_bio_visited_places_65_chars_rejected():
"""visited_places item > 64 chars raises pydantic.ValidationError on ExtendedBio."""
with pytest.raises(pydantic.ValidationError):
ExtendedBio(
extended_bio="More details about the dweller.",
visited_places=["y" * 65],
)
def test_dweller_backstory_visited_places_65_chars_rejected() -> None:
"""visited_places item > 64 chars raises pydantic.ValidationError."""
with pytest.raises(pydantic.ValidationError):
DwellerBackstory(
bio="A test bio for backstory validation.",
origin_place="Megaton",
visited_places=["x" * 65],
)
def test_dweller_backstory_visited_places_64_chars_valid() -> None:
"""visited_places item == 64 chars is valid (edge case)."""
result = DwellerBackstory(
bio="A test bio for backstory validation.",
origin_place="Megaton",
visited_places=["x" * 64],
)
assert result.visited_places == ["x" * 64]
def test_extended_bio_visited_places_65_chars_rejected() -> None:
"""visited_places item > 64 chars raises pydantic.ValidationError on ExtendedBio."""
with pytest.raises(pydantic.ValidationError):
ExtendedBio(
extended_bio="More details about the dweller.",
visited_places=["y" * 65],
)
🤖 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 `@backend/app/tests/test_utils/test_dweller_ai_schemas.py` around lines 9 - 35,
Add a -> None return annotation to each new test function:
test_dweller_backstory_visited_places_65_chars_rejected,
test_dweller_backstory_visited_places_64_chars_valid, and
test_extended_bio_visited_places_65_chars_rejected. Do not alter their test
logic.

Source: Coding guidelines

… compat

typescript 7.0.2 (native TS) removed the ts.factory API that both
openapi-typescript (peerDep ^5.x) and vue-tsc depend on, breaking
pnpm types:generate and pnpm run typecheck. Restore the pre-dependabot
known-good state (^6.0.3) until the toolchain supports TS7.
…orts it

openapi-typescript (peerDep ^5.x) and vue-tsc break on typescript 7
(native TS removed ts.factory). Prevent dependabot re-bumping past 6.x.
@ElderEvil
ElderEvil merged commit 221da64 into master Aug 7, 2026
4 of 5 checks passed
This was referenced Aug 7, 2026
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