Cave Game is a distributed-systems simulation built with Pygame. It models a team of autonomous drones and rovers exploring a procedurally generated cave while coordinating through limited communication, local perception, and shared mission state. The repository is intentionally structured to show how concurrency, pathfinding, terrain generation, and UI composition work together in a small but non-trivial simulation.
The codebase is organized around a simple control chain: main.py creates the game shell, game.py manages menus and mission startup, and mission/control.py coordinates the live simulation. From there, multiple subsystems work together:
generation/map_generator.pybuilds the cave using multiprocessing and shared memory.agents/drone.pymodels local exploration, vision, and terrain knowledge.agents/rover.pyacts as a mobile or stationary support agent depending on mission setup.mapping/terrain_knowledge.pyowns terrain arrays, synchronization, snapshots, observation fusion, and merging.mapping/wall_mapping.pymeasures exposed cave/pillar/internal-wall surface coverage from combined occupied SLAM evidence.agents/exploration_policy.pyowns reproducible weighted-random selection.navigation/pathfinding.pyandnavigation/astar_pathfinder.pyprovide A* for cul-de-sac escape, homing, and the disabled rover flow.agents/graph.pytracks valid movement and exploration connectivity.ui/control_center/facade.pyis the UI facade,ui/control_center/controller.pyowns timer and input state, andui/control_center/renderer.pyowns Pygame layout and drawing.mission/presentation_adapter.pykeeps UI state and presentation toggles isolated from mission logic.asset_config/contains the enums and constants that keep gameplay, rendering, media, and map generation consistent.
The design goal is realism through constraint: agents do not start with omniscient knowledge, terrain is discovered incrementally, sharing is event-driven, and the visualization is built from the same distributed data model the agents use.
- Python 3.11 or newer
pipavailable in your Python installation- A desktop environment capable of running Pygame
Install dependencies into your system Python:
python -m pip install --upgrade pip
python -m pip install -r requirements.txtRun the simulation:
python main.pyRun the automated test suite:
python -m unittest discover -s tests -vSee TESTING.md for the test matrix, placement rules, and manual
smoke checklist.
The runtime flow is intentionally layered so that each file owns one part of the simulation lifecycle.
main.pyinstantiatesGameand starts the application.game.pycreates the UI window, handles menu navigation, and collects mission settings.- When the player starts a mission,
Gameprepares an immutable nestedSimulationConfig, generates the cave, and constructsMissionControl. GamecallsMissionControl.run(), which creates agents and runtime resources, launches worker threads, and enters the main loop.- A restart request cleanly shuts down that controller and constructs a new one over the same settings and generated cave.
MissionControl is the central coordinator during play. Construction prepares mission state without starting the simulation; run() explicitly owns runtime initialization and teardown. The main thread handles window events, sensing, and frame updates, while per-agent threads handle movement and nearby data sharing.
The compact square-icon control ends the run and returns to the menu. The
circular-arrow control restarts through the same cleanup path, then Game
creates fresh agents, mapping state, timers, threads, and pathfinding resources
without regenerating the cave. The pause/play control freezes agent movement,
mission updates, and elapsed mission time while keeping rendering and input
responsive. PAUSE closes a worker barrier and returns only after every agent
thread reaches a safe checkpoint; PLAY releases the barrier. Behavioral
cooldowns use a pause-aware simulation clock, so wall time spent paused does
not change sensing, sharing, or frontier timing.
That separation matters because the simulation mixes three different execution models:
- The main Pygame loop handles input, timing, and rendering.
- Drone and rover behavior can run concurrently in worker threads.
- Cave generation uses multiprocessing so the map can be carved efficiently at startup.
The main loop targets 15 FPS. FrameProfiler records smoothed frame, wait,
sharing, sensing, rendering, and display durations, which are exposed in the
control-center debug panel. SLAM surfaces rebuild at most every 0.1 seconds,
and rover terrain exchange runs at most every 0.5 seconds; cached visuals are
still blitted every frame.
agents/drone.py composes each drone's local mapping, weighted-random policy,
movement controller, sensor, and renderer. During ordinary exploration the
controller groups local unknown boundaries into connected components. It
weights collision-free headings toward sufficiently large wall-touching
components first, generic components second, and separation from nearby
teammates throughout. A cached 32-pixel coarse index periodically summarizes
frontiers across the drone's complete SLAM map. When its selected region lies
beyond the local scoring window, that bearing becomes the strategic signal
and local geometry remains a lower-weight tactical correction. Tiny components do not bias normal movement but remain
available to bounded stagnation cleanup. It then walks a ten-pixel raster step. If no
radius-length heading is open, it extracts border cells from the drone's local
SLAM and uses A* to reach the nearest viable border. Reaching an
escape border suppresses that exact target until its local frontier geometry
changes, then performs a recovery-only full-circle turn toward a physically
usable heading. A distance-based sensor-gain window also redirects stagnant
random walks toward local unknown SLAM boundaries. Its scan-only heading may
face a wall even though translation in that direction is invalid; movement
waits for one completed scan, then restores a collision-safe heading. It falls
back to a frontier outside the recent trail when A* is needed. A* is also used
to return home.
Every physical point is appended to the path history consumed by the renderer.
agents/rover.py serves as a support agent. In the current architecture it is primarily valuable as a rendezvous and accumulation point for terrain knowledge, which makes it useful for centralizing observations without replacing the distributed model.
navigation/pathfinding.py owns a cave-map-backed worker pool for drone escape
and homing routes. A search that reaches its fixed work cap returns a tagged
progress segment; the drone follows it and replans toward the unchanged goal.
The service also retains terrain-weighted routing for the disabled rover flow.
agents/graph.py remains the physical collision/history boundary, while
ui/control_center/facade.py and mission/presentation_adapter.py keep UI
concerns outside the simulation core.
The simulation works as a feedback loop:
- The map is generated.
- Agents are placed into the world.
- Drones move and scan every visible cell in their current cone.
- Dense visibility updates local SLAM, while sparse sample rays update the separate rover-oriented terrain knowledge.
- Nearby agents exchange data when the mission rules allow it.
- The UI reports exposed wall-surface coverage from combined occupied SLAM; roughness remains an optional terrain heatmap.
- Ordinary movement makes a seeded weighted-random choice inside the current vision cone. Connected wall-frontier components outrank generic components. Wall candidates combine continuation, size rank, and proximity with 2:2:1 weights; generic candidates combine size rank and proximity with 2:1 weights. A coarse whole-map cache applies the same tiering every two seconds and steers toward remote regions without rescoring every frontier pixel on every step. Positional separation spreads nearby drones.
- If a drone is boxed in, local-SLAM border cells are rebuilt and A* selects an escape. Exact team wall completion starts coordinated homing; local border exhaustion remains a compatibility fallback.
- Mission completion requires every drone to finish its return.
That flow is important because the game does not use a single global terrain oracle. Instead, knowledge is built from observations and exchanged through explicit events. This makes the heatmap, the agent behavior, and the mission state all consistent with one another.
Terrain state is represented by TerrainKnowledge. Mission control, every
drone, and every rover own separate instances containing roughness, confidence,
a floor mask, and synchronization. Drones update their local instance while
the same observations are separately recorded for rover routing and the
optional terrain heatmap. Terrain coverage does not define exploration
progress or completion.
Snapshots provide detached data for rendering and belief-scoped value
estimation. Sharing decides when knowledge moves between agents, while
TerrainKnowledge.merge_from() provides the single confidence-weighted merge
rule.
Local occupancy mapping follows the same ownership pattern. Each SlamMap
privately owns its occupancy grid, confidence grid, point cloud, lock, and
monotonic version. Sensing updates it through methods; sharing, frontier
selection, and rendering consume detached SlamSnapshot values. Rendering
tracks consumed versions so an update arriving during frame composition
remains pending for the next refresh.
Vision and terrain sampling deliberately have different resolutions.
VisionSensor.scan_cone() produces a collision-bounded VisionScan covering
every grid cell in the cone; those free and occupied observations are the only
sensor inputs to SLAM exploration gain and mission completion. The fixed ray
set remains available for the overlay and samples roughness every second ray
cell. Terrain may therefore remain interpolably sparse without creating
frontiers or delaying wall-mapping completion.
The distributed-semantics contract is:
- Agent-local knowledge drives active agent decisions.
- Mission-global terrain is telemetry and UI aggregation only.
- Exploration progress is occupied-SLAM coverage of exposed wall surfaces; buried solid rock is excluded.
- Exact team wall completion is a mission-level homing trigger. The exposed ground-truth wall mask remains confined to mission evaluation and is never supplied to a drone's heading scorer.
- SLAM-derived border selection is local. The physical cave map is consulted by collision checks, sensor simulation, communication line of sight, and the deliberately simple A* escape/homing service.
- Sharing is the only mechanism that transfers local knowledge between agents.
- Rover movement remains disabled. Its existing target and route code is disabled until it uses rover-local received knowledge before activation.
The sharing model is intentionally limited.
- Drone-to-drone exchange is triggered by proximity and throttled so the system does not spend all of its time copying arrays.
- SLAM is shared, but derived frontier coordinates are not. A recipient invalidates and rebuilds its own borders before mission-exhaustion checks.
- Rover sharing acts as a support mechanism for collecting knowledge near a persistent reference point.
- The heatmap refresh is also throttled so rendering stays responsive.
This makes the simulation feel distributed rather than centralized. Agents learn locally first, then synchronize when they actually meet.
The cave is produced by generation/map_generator.py, which uses parallel worker processes to erode an initially solid map into a navigable cave system. The generator uses shared memory so the workers can write into the same binary terrain buffer without copying the entire map between processes.
The generation pipeline is roughly:
- Create a shared map buffer.
- Spawn multiple erosion workers.
- Let those workers carve and refine the terrain concurrently.
- Apply post-processing to smooth or clean the result.
- Build the final terrain data used by the simulation.
The reason for this architecture is practical: cave generation is naturally parallel, and shared-memory multiprocessing is a good fit when several workers need to modify the same large array.
Drone movement intentionally uses a small baseline policy.
The agent typically does the following:
- scan nearby terrain and update private SLAM/terrain knowledge,
- test integer headings inside the current 60-degree vision cone for a clear radius-length look-ahead and ten-pixel short step,
- group local unknown boundaries into eight-connected components and exclude components below the configured 12-cell normal-heading size threshold,
- periodically aggregate the complete local SLAM into 32-pixel coarse frontier regions, retain one remote strategic target for two seconds, and bias the current collision-safe cone toward its bearing,
- keep wall-touching components as the strict first tier and score them from continuation alignment, normalized size rank, and proximity at 2:2:1; when none are actionable, score generic components from size rank and proximity at 2:1,
- add a positional separation score that repels nearby teammates and assigns deterministic launch sectors while the drones still overlap,
- choose by the resulting weights with a per-drone seeded random generator,
- walk the chosen line directly and record every point for path rendering,
- after 120 travelled pixels, keep weighted-random movement when sensor-local gain is productive; otherwise rotate directly toward an unknown cell beside a reachable SLAM border and hold position for exactly one completed scan,
- retain refreshed work after sensor gain, or suppress unchanged local frontier geometry after a zero-gain directed scan, then restore a collision-safe movement heading,
- when no local border can be viewed, use A* to reach a border outside the recent breadcrumb trail,
- when no local heading is available, rebuild known-free/unknown boundaries from local SLAM and use A* to reach the nearest viable border,
- after escape A*, retire the reached border while its local geometry is unchanged and rotate in place toward a usable outgoing heading,
- use the same A* service for homing after exact team wall completion or local border exhaustion; capped searches return a useful partial route and replan from its endpoint instead of being reported as unreachable.
Border extraction consumes local SLAM only. The true map remains the physical simulation boundary for collision checks, sensor observations, communication line of sight, and A* escape/homing routes.
The rendering path is deliberately separated from mission logic.
rendering/mission_renderer.py owns complete frame composition and the
stop-button visual. rendering/slam_view.py builds the selected SLAM or terrain
view. Each agent renderer owns paths, vision, and icons; the drone breadcrumb
path is the only navigation overlay. The control-center facade builds immutable frame
data, its controller owns timer/tab/input state, and its renderer owns all
Pygame resources and hit geometry. mission/presentation_adapter.py keeps
presentation state isolated so toggles do not contaminate the simulation
model.
Rendering is layered so the visual output stays readable:
- Black canvas and SLAM or terrain surface
- Agent paths
- Drone vision
- Agent icons
- Control center and stop button
Runtime code delegates frame composition directly to MissionRenderer.draw().
The project keeps its runtime settings and visual assets in predictable locations.
GameConfig/options.default.iniandGameConfig/simulation.default.inistore committed defaults. Navigation exposes local-SLAM border confidence, sampling stride, rebuild cooldown, frontier component threshold/proximity scale/score weights, coarse global cell size/refresh interval, the weightedrandompolicy, its wall/unexplored/separation biases, and its stagnation distance/gain threshold. Older waypoint and MCTS INI keys are ignored when loading existing files.GameConfig/options.local.iniandGameConfig/simulation.local.inistore user changes and are ignored by Git.Assets/contains the audio, fonts, images, backgrounds, and map resources used by the game.Assets/Map/contains cave images generated at runtime and ignored by Git.asset_config/provides typed constants and enums so gameplay values, colors, asset paths, and map-generation parameters stay consistent across modules.
This is a deliberate structural choice. Hard-coding file names and magic numbers across the codebase would make the simulation harder to tune and more brittle to change.
In menus:
Up/Down: move selectionLeft/Right: change selector or slider valuesEnter: confirm, open a submenu, or start a mission- Number keys
0-9: edit the seed field Backspace: return from submenus, except on the seed field where it deletes digits
Simulation settings available in-game:
- Objective:
ExplorationorSearch and Rescue - Cave size:
Small,Medium,Large - Seed: custom numeric seed or the default for the selected cave size
- Drones: from 3 to 8
| Feature | Status | Notes |
|---|---|---|
| Terrain roughness map | Implemented | Available in current simulation flow |
| Known map visualization | Implemented | Available in current simulation flow |
| Distributed terrain sharing | Implemented | Drone-to-drone and rover terrain sharing are active |
| POI and path sharing | Planned | POI model exists; runtime integration is deferred |
| Drone path rendering | Implemented | Each drone's complete travelled breadcrumb path is rendered incrementally |
| Battery management | Planned | Not yet implemented |
| Random exploration with A* escape | Implemented | Random local steps are direct; A* is reserved for cul-de-sac borders and homing |
| Search & Rescue mission logic | Planned | Objective exists in UI; starting it fails fast instead of running Exploration behavior |
| Drift modeling | Planned | Not yet implemented |
- If dependencies fail to install, upgrade
pipfirst and retry. - If
pygameaudio initialization fails, check that your system audio device is available and not locked by another app. - If
cv2import fails, reinstall OpenCV:
python -m pip install --force-reinstall opencv-python- This repository is under active development.
- Some modules contain extension points for future mission logic.