Skip to content

Commit 6578303

Browse files
committed
feat: full migration to 3d points
1 parent 709a023 commit 6578303

110 files changed

Lines changed: 15946 additions & 1062 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,51 @@
44
![Zero Dependencies](https://img.shields.io/badge/Dependencies-Zero-brightgreen.svg)
55
![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)
66
![CI](https://github.com/tmcarmichael/compass-core/actions/workflows/ci.yml/badge.svg)
7-
![Coverage](<https://img.shields.io/badge/Coverage-78%25_(20.5K_measured)-blue.svg>)
7+
![Coverage](<https://img.shields.io/badge/Coverage-88%25_(20.5K_measured)-blue.svg>)
88

99
A layered decision architecture for intelligent behavior in a real-time 3D world.
1010

11+
> Compass is a cleaned public case study of a real autonomous agent architecture originally developed against a legacy 3D MMORPG emulator.
12+
>
13+
> This repository is intentionally published as an architecture reference and proof of work, not as a turnkey botting tool, live client integration package, or runnable game-automation release. The focus here is the system design: perception, decision-making, routines, navigation, learning, observability, and real session telemetry.
14+
15+
## What This Repo Is
16+
17+
- A modern Python implementation of a layered real-time agent architecture
18+
- A systems case study built against a noisy, high-pressure 3D sandbox
19+
- A reference for control-loop design, rule systems, utility scoring, GOAP planning, navigation, and learned adaptation
20+
- A record of real decision traces, forensics, plans, and session outputs from the underlying private system
21+
22+
## What This Repo Is Not
23+
24+
- A ready-to-run bot
25+
- A packaged live integration for the target game client
26+
- A dump of private configs, assets, or operational glue
27+
- A turnkey automation release intended for direct operational use
28+
29+
Compass treats a legacy MMORPG as a demanding autonomy sandbox rather than as the point of the project. The interesting problem is not the game itself; it is the combination of partial observability, noisy state, real-time interruption, 3D navigation, competing goals, social threat, and long-horizon autonomy.
30+
31+
The public release preserves the architecture, model code, test strategy, and representative telemetry from that system. It is meant to be read, inspected, and evaluated as a systems architecture project.
32+
1133
<p align="center">
1234
<img src="docs/compass-hero.png" alt="Compass" width="720">
1335
</p>
1436

37+
## How To Evaluate This Repo
38+
39+
- Read [docs/architecture.md](docs/architecture.md) for the execution model and subsystem boundaries
40+
- Read [docs/design-decisions.md](docs/design-decisions.md) for the rationale behind the main architectural choices
41+
- Browse [docs/samples/](docs/samples/) for real decision traces, GOAP plans, forensics dumps, and session telemetry
42+
- Run `just check` to verify the public codebase, tests, typing, and linting
43+
- Start with [`src/perception/state.py`](src/perception/state.py), [`src/brain/decision.py`](src/brain/decision.py), and [`src/brain/runner/loop.py`](src/brain/runner/loop.py) to understand the core loop
44+
1545
Compass defines a control architecture for agents that must perceive, decide, and navigate a 3D open world in real time. Live world state flows through a forward-only pipeline that separates perception, decision-making, routines, and movement, while a typed world model gives each layer a shared view of the world.
1646

1747
Decision-making is layered by design. Priority rules enforce the safety envelope, utility scoring ranks immediate choices, and goal-oriented planning coordinates multi-step behavior across time. Routines execute those decisions without blocking and remain interruptible under threat. Encounter history, spatial memory, and navigation over parsed world geometry help the agent adapt and plan in an unpredictable environment.
1848

1949
A legacy 3D MMORPG emulator provided the demanding sandbox that forced this architecture to evolve: partial observability, noisy state, spatial hazard, resource pressure, and constant interruption. What began as reactive rules and state-machine routines grew into a layered system that preserves survival guarantees while adding learned adaptation and anticipatory planning. The result is an architecture shaped by the demands of the world it was built in.
2050

21-
The system operates autonomously in multi-hour sessions. In [recorded sessions](docs/samples/learned-encounter-data.md), average fight duration for a given entity type dropped from 29.5s to 15.9s as encounter history accumulated, session grades improved from B to A, and operational parameters auto-tuned from defaults. See [docs/samples/](docs/samples/) for full session telemetry: [decision traces](docs/samples/decision-trace.md), [GOAP plans with cost tracking](docs/samples/goap-planner.md), [forensic ring buffer dumps](docs/samples/forensics-ring-buffer.md), and [4-tier log output](docs/samples/session-tiers.md) from live sessions.
51+
In the underlying private system, this architecture operated autonomously in multi-hour sessions. In [recorded sessions](docs/samples/learned-encounter-data.md), average fight duration for a given entity type dropped from 29.5s to 15.9s as encounter history accumulated, session grades improved from B to A, and operational parameters auto-tuned from defaults. See [docs/samples/](docs/samples/) for full session telemetry: [decision traces](docs/samples/decision-trace.md), [GOAP plans with cost tracking](docs/samples/goap-planner.md), [forensic ring buffer dumps](docs/samples/forensics-ring-buffer.md), and [4-tier log output](docs/samples/session-tiers.md) from live sessions.
2252

2353
---
2454

@@ -219,7 +249,7 @@ GOAP Goal planner, spawn prediction, trajectory forecasting
219249

220250
---
221251

222-
## Why a Legacy 3D MMORPG Emulator
252+
## Why an MMORPG Sandbox
223253

224254
A legacy 3D MMORPG emulator concentrates most of the hard problems in autonomous agent design into a single domain:
225255

@@ -236,7 +266,7 @@ A legacy 3D MMORPG emulator concentrates most of the hard problems in autonomous
236266

237267
## Reading the Code
238268

239-
Start with [`perception/state.py`](src/perception/state.py): the frozen `GameState` and `SpawnData` dataclasses that every layer depends on. Then [`brain/decision.py`](src/brain/decision.py): the coordinator that evaluates rules and manages routine lifecycles. Pick any rule module in [`brain/rules/`](src/brain/rules/) to see how conditions and score functions are written. The GOAP planner lives in [`brain/goap/planner.py`](src/brain/goap/planner.py). Combat strategies are in [`routines/strategies/`](src/routines/strategies/).
269+
After the top-level overview, read by subsystem. Start with any rule module in [`brain/rules/`](src/brain/rules/) to see how conditions and score functions are written. Then inspect [`brain/goap/planner.py`](src/brain/goap/planner.py) for goal-directed sequencing, [`brain/world/model.py`](src/brain/world/model.py) for derived world intelligence, and [`brain/runner/loop.py`](src/brain/runner/loop.py) for the 10 Hz execution path. Combat strategies live in [`routines/strategies/`](src/routines/strategies/).
240270

241271
For architecture details beyond the README, see [`docs/architecture.md`](docs/architecture.md). For design rationale, [`docs/design-decisions.md`](docs/design-decisions.md). For the full evolutionary arc, [`docs/evolution.md`](docs/evolution.md).
242272

@@ -268,10 +298,6 @@ docs/ Architecture, design decisions, evolution history, retr
268298

269299
</details>
270300

271-
> **Note:** This repository is an architecture and implementation reference. Running the full system requires the target environment and supporting assets described in the docs. This is a cleaned extraction from a private working repo; the evolution described in [docs/evolution.md](docs/evolution.md) reflects the actual development arc, published here as a single snapshot.
272-
273-
---
274-
275301
Built with Python 3.14, zero runtime dependencies, and the standard library. See [docs/testing.md](docs/testing.md) for the test strategy and coverage philosophy.
276302

277303
---

docs/retrospective.md

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -97,8 +97,6 @@ The emulator was the concrete proof point, but this architecture applies to any
9797

9898
- **Other game environments**: any application with readable process memory can be perceived the same way. The perception layer is the only part that changes.
9999

100-
- **Robotics control loops**: perception -> decision -> action -> motor is the standard robotics architecture. The 10 Hz priority-rule engine, the non-blocking tick contract, and the hysteresis patterns apply directly. GOAP planning with learned cost functions is directly applicable to warehouse robots, delivery drones, and autonomous vehicles.
101-
102100
- **Autonomous testing agents**: navigating complex UIs, handling error states, recovering from unexpected conditions. The routine state machine pattern handles multi-step UI flows cleanly.
103101

104102
- **Any long-running autonomous system**: the 4-tier logging pipeline, the forensic buffer, and the scorecard system are useful wherever an agent operates unattended and needs post-hoc debugging.

docs/testing.md

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
<!-- last_modified: 2026-03-30 -->
2+
13
# Testing
24

35
How the test suite is structured, what it covers, and what it deliberately omits.
@@ -15,7 +17,7 @@ Hypothesis profiles: `dev` (50 examples, fast) and `ci` (200 examples, thorough)
1517

1618
## Coverage strategy
1719

18-
Coverage is measured in CI with `pytest-cov` and enforced with a `fail_under` floor in `pyproject.toml`. The measured surface is ~17.7K of ~22K total source lines (80% of the codebase). Only 7 files are omitted; all require a live game client or game asset files.
20+
Coverage is measured in CI with `pytest-cov` and enforced with a `fail_under` floor in `pyproject.toml`. The measured surface is the full 20.5K source lines with zero omissions.
1921

2022
### What is covered
2123

@@ -48,21 +50,17 @@ set_backend(recorder)
4850

4951
This makes every routine testable without mocking, monkeypatching, or environment access. Tests assert on `recorder.actions` to verify motor commands.
5052

51-
### What is omitted (and why)
53+
### Previously omitted modules
5254

53-
The `[tool.coverage.run] omit` list in `pyproject.toml` excludes only modules that require external hardware or binary assets:
55+
Seven modules that were previously excluded from coverage measurement are now fully measured and tested via mock-based testing:
5456

55-
| Omitted path | Reason |
56-
|---|---|
57-
| `src/perception/*` | Win32 `ReadProcessMemory`: requires a live game client process |
58-
| `src/eq/s3d.py`, `wld.py`, `zone_chr.py` | Binary asset parsers: require S3D/WLD game files |
59-
| `src/runtime/orchestrator.py` | Session lifecycle: thread coordination, logging wiring |
60-
| `src/runtime/server.py` | Web server: HTTP/WebSocket infrastructure |
61-
| `src/runtime/agent.py` | Process management: Win32 admin check, process spawning |
57+
- **Perception layer** (`src/perception/*`): Win32 `ReadProcessMemory` calls are tested through mock readers that return crafted SPAWNINFO buffers. `read_state()` assembly, profile chain resolution, struct validation, log parsing, and all sub-readers (char, spawn, inventory) are covered.
58+
- **Binary asset parsers** (`src/eq/s3d.py`, `wld.py`, `zone_chr.py`): tested with synthetic binary fixtures built via `struct.pack`.
59+
- **Runtime orchestration** (`src/runtime/orchestrator.py`, `agent.py`): config building, feature toggles, log buffer, session pruning tested with mock dependencies.
6260

63-
These 7 files (~4K lines) are validated through runtime invariants, the forensics ring buffer, and session-level observability.
61+
### Compensating controls for deeply-coupled code
6462

65-
### Compensating controls for omitted code
63+
The remaining uncovered lines are concentrated in multi-phase routine tick handlers (motor-coupled state machines) and the 10 Hz brain runner loop (threading, I/O). These are validated through:
6664

6765
1. **Runtime invariants** (`src/util/invariants.py`): assertions that fire during operation and flush diagnostics on violation
6866
2. **Forensics buffer** (`src/util/forensics.py`): 300-tick ring buffer that dumps to disk on death or crash, providing post-hoc debugging for the perception/routine layers

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ dev = [
2727
"hypothesis>=6.100",
2828
"pre-commit>=4.0",
2929
"pytest-benchmark>=5.0",
30+
"pytest-xdist>=3.5",
3031
]
3132

3233
# ── Tool configuration ───────────────────────────────────────────────
@@ -99,6 +100,6 @@ markers = [
99100
source = ["src"]
100101

101102
[tool.coverage.report]
102-
fail_under = 78
103+
fail_under = 91
103104
show_missing = true
104105
skip_empty = true

src/brain/combat_eval.py

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,8 @@
1111
from enum import StrEnum, unique
1212
from typing import Any
1313

14-
from core.types import Disposition
14+
from core.types import Disposition, Point
1515
from eq.strings import normalize_mob_name
16-
from nav.geometry import distance_2d
1716
from perception.queries import (
1817
is_pet,
1918
)
@@ -274,7 +273,7 @@ def find_targets(
274273

275274
if not is_resource and not is_valid_target(spawn, player_level):
276275
continue
277-
dist = distance_2d(player_x, player_y, spawn.x, spawn.y)
276+
dist = Point(player_x, player_y, 0.0).dist_to(spawn.pos)
278277
if dist > max_distance:
279278
continue
280279
con = con_color(player_level, spawn.level)

src/brain/context.py

Lines changed: 20 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@
3737
from brain.state.threat import ThreatState
3838
from brain.state.zone import ZoneState
3939
from core.constants import XP_SCALE_MAX
40-
from nav.geometry import distance_2d
40+
from core.types import Point
4141
from perception.state import GameState
4242
from util.thread_guard import assert_brain_thread
4343

@@ -283,16 +283,16 @@ def has_unlootable_corpse(self, state: GameState, max_dist: float = 120.0) -> bo
283283
for spawn in state.spawns:
284284
if not spawn.is_corpse:
285285
continue
286-
dist = distance_2d(state.x, state.y, spawn.x, spawn.y)
286+
dist = state.pos.dist_to(spawn.pos)
287287
if dist > max_dist:
288288
continue
289-
defeat = self.find_unlootable_kill(spawn.name, spawn.x, spawn.y, corpse_spawn_id=spawn.spawn_id)
289+
defeat = self.find_unlootable_kill(spawn.name, spawn.pos, corpse_spawn_id=spawn.spawn_id)
290290
if defeat:
291291
return True
292292
# Log WHY matching failed
293293
for k in self.defeat_tracker.defeat_history:
294294
if not k.looted and time.time() - k.time < 300:
295-
kdist = distance_2d(spawn.x, spawn.y, k.x, k.y)
295+
kdist = spawn.pos.dist_to(Point(k.x, k.y, 0.0))
296296
name_match = k.name in spawn.name or spawn.name.startswith(k.name)
297297
log.debug(
298298
"Loot match: corpse='%s'@(%.0f,%.0f) vs defeat='%s'@(%.0f,%.0f) "
@@ -318,25 +318,29 @@ def clear_recent_kills(self) -> None:
318318
(sid, t) for sid, t in self.defeat_tracker.recent_kills if now - t < 60
319319
]
320320

321-
def record_kill(self, spawn_id: int, name: str = "", x: float = 0.0, y: float = 0.0) -> None:
321+
_ORIGIN = Point(0.0, 0.0, 0.0)
322+
323+
def record_kill(self, spawn_id: int, name: str = "",
324+
pos: Point = _ORIGIN) -> None:
322325
"""Delegate to defeat_tracker."""
323326
assert_brain_thread("record_kill")
324-
self.defeat_tracker.record_kill(spawn_id, name=name, x=x, y=y)
327+
self.defeat_tracker.record_kill(spawn_id, name=name, pos=pos)
325328

326-
def update_stationary_kills(self, player_x: float, player_y: float) -> None:
329+
def update_stationary_kills(self, pos: Point) -> None:
327330
assert_brain_thread("update_stationary_kills")
328331
if self.metrics.last_kill_x == 0 and self.metrics.last_kill_y == 0:
329-
self.metrics.last_kill_x = player_x
330-
self.metrics.last_kill_y = player_y
332+
self.metrics.last_kill_x = pos.x
333+
self.metrics.last_kill_y = pos.y
331334
self.metrics.stationary_kills = 1
332335
return
333-
dist = distance_2d(player_x, player_y, self.metrics.last_kill_x, self.metrics.last_kill_y)
336+
last_kill_pos = Point(self.metrics.last_kill_x, self.metrics.last_kill_y, 0.0)
337+
dist = pos.dist_to(last_kill_pos)
334338
if dist < 30:
335339
self.metrics.stationary_kills += 1
336340
else:
337341
self.metrics.stationary_kills = 1
338-
self.metrics.last_kill_x = player_x
339-
self.metrics.last_kill_y = player_y
342+
self.metrics.last_kill_x = pos.x
343+
self.metrics.last_kill_y = pos.y
340344

341345
def should_reposition(self) -> bool:
342346
if self.metrics.stationary_kills < 2:
@@ -351,14 +355,13 @@ def should_reposition(self) -> bool:
351355
def find_unlootable_kill(
352356
self,
353357
corpse_name: str,
354-
corpse_x: float,
355-
corpse_y: float,
358+
pos: Point,
356359
max_dist: float = 60.0,
357360
corpse_spawn_id: int = 0,
358361
) -> DefeatInfo | None:
359362
"""Delegate to defeat_tracker."""
360363
return self.defeat_tracker.find_unlootable_kill(
361-
corpse_name, corpse_x, corpse_y, corpse_spawn_id=corpse_spawn_id, max_dist=max_dist
364+
corpse_name, pos, corpse_spawn_id=corpse_spawn_id, max_dist=max_dist
362365
)
363366

364367
def defeats_in_window(self, window_seconds: float) -> int:
@@ -424,7 +427,7 @@ def nearest_player_dist(self, state: GameState) -> float:
424427
best = 9999.0
425428
for spawn in state.spawns:
426429
if spawn.spawn_type == 0 and spawn.name != state.name:
427-
dist = distance_2d(state.x, state.y, spawn.x, spawn.y)
430+
dist = state.pos.dist_to(spawn.pos)
428431
if dist < best:
429432
best = dist
430433
return best
@@ -433,7 +436,7 @@ def nearby_player_count(self, state: GameState, radius: float = 200.0) -> int:
433436
count = 0
434437
for spawn in state.spawns:
435438
if spawn.spawn_type == 0 and spawn.name != state.name:
436-
dist = distance_2d(state.x, state.y, spawn.x, spawn.y)
439+
dist = state.pos.dist_to(spawn.pos)
437440
if dist <= radius:
438441
count += 1
439442
return count

src/brain/goap/spawn_predictor.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ def _cell_center(cx: int, cy: int) -> Point:
3838
return Point(
3939
x=(cx + 0.5) * CELL_SIZE,
4040
y=(cy + 0.5) * CELL_SIZE,
41+
z=0.0,
4142
)
4243

4344

src/brain/goap/world_state.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,9 +72,9 @@ def build_world_state(state: GameState, ctx: AgentContext) -> PlanWorldState:
7272
# At camp
7373
at_camp = True
7474
if ctx.camp.roam_radius > 0:
75-
from nav.geometry import distance_2d
75+
from core.types import Point
7676

77-
d = distance_2d(state.x, state.y, ctx.camp.camp_x, ctx.camp.camp_y)
77+
d = state.pos.dist_to(Point(ctx.camp.camp_x, ctx.camp.camp_y, 0.0))
7878
at_camp = d <= ctx.camp.roam_radius * 1.2 # slight buffer
7979

8080
return PlanWorldState(

0 commit comments

Comments
 (0)