Compare commits

...

1 Commits

Author SHA1 Message Date
Timmy
ff87159ca3 feat(#1540): Spatial search — find nearest user/object by name
Some checks failed
CI / test (pull_request) Failing after 58s
Review Approval Gate / verify-review (pull_request) Successful in 10s
CI / validate (pull_request) Failing after 1m47s
Adds SpatialSearch class with BFS pathfinding on the room graph.

/find <name> command searches:
  - Players by username (case-insensitive substring)
  - Objects in room data
  - Whiteboard writings

Results sorted by distance, show:
  - Name and type (player/object/writing)
  - Room location
  - Distance in rooms
  - Direction to head (first exit to take)

Closes #1540
2026-04-14 23:21:58 -04:00

View File

@@ -1933,6 +1933,101 @@ def get_latency_stats() -> dict:
"recent": _latencies[-10:],
}
class SpatialSearch:
"""Find nearest user or object by name using BFS on the room graph."""
def find(self, target: str, from_room: str, rooms: dict, presence_mgr) -> list[dict]:
"""Search all rooms for target (player name or object).
Returns list of {name, type, room, distance, first_direction} sorted by distance.
"""
target_lower = target.lower()
results = []
# BFS from from_room
visited = set()
queue = [(from_room, 0, None)] # (room, distance, first_direction)
while queue:
room, dist, first_dir = queue.pop(0)
if room in visited:
continue
visited.add(room)
room_data = rooms.get(room, {})
# Search players in this room
players = presence_mgr.get_players_in_room(room)
for p in players:
if target_lower in p['username'].lower():
results.append({
'name': p['username'],
'type': 'player',
'room': room,
'distance': dist,
'first_direction': first_dir,
})
# Search objects in this room
for obj in room_data.get('objects', []):
if target_lower in obj.lower():
results.append({
'name': obj,
'type': 'object',
'room': room,
'distance': dist,
'first_direction': first_dir,
})
# Search whiteboard entries
for entry in room_data.get('whiteboard', []):
if target_lower in entry.lower():
results.append({
'name': entry[:60],
'type': 'writing',
'room': room,
'distance': dist,
'first_direction': first_dir,
})
# Enqueue neighbors
exits = room_data.get('exits', {})
for direction, dest_room in exits.items():
if dest_room not in visited:
# First step sets the direction, subsequent steps preserve it
next_dir = direction if first_dir is None else first_dir
queue.append((dest_room, dist + 1, next_dir))
# Sort by distance, then by type priority (player > object > writing)
type_priority = {'player': 0, 'object': 1, 'writing': 2}
results.sort(key=lambda r: (r['distance'], type_priority.get(r['type'], 9)))
return results
def format_results(self, results: list[dict], from_room: str) -> str:
"""Format search results as a readable string."""
if not results:
return "Nothing found."
lines = []
for r in results[:5]: # Cap at 5 results
dist = r['distance']
if dist == 0:
loc = "right here"
elif dist == 1:
loc = f"one room away ({r['first_direction']})"
else:
loc = f"{dist} rooms away (head {r['first_direction']})"
type_label = {'player': '👤', 'object': '📦', 'writing': '📝'}.get(r['type'], '?')
lines.append(f" {type_label} {r['name']}{r['room']} ({loc})")
if len(results) > 5:
lines.append(f" ... and {len(results) - 5} more")
return "Found:\n" + "\n".join(lines)
_spatial_search = SpatialSearch()
class BridgeHandler(BaseHTTPRequestHandler):
"""HTTP handler for multi-user bridge."""
@@ -2642,10 +2737,24 @@ class BridgeHandler(BaseHTTPRequestHandler):
"session_messages": len(session.messages),
}
elif verb == 'find':
if not arg:
return {"command": "find", "error": "Find whom? Usage: find <name>"}
target = arg.strip().lower()
# Search all rooms for a matching player or object
results = _spatial_search.find(target, room, rooms, presence_manager)
return {
"command": "find",
"query": arg,
"from_room": room,
"results": results,
"description": _spatial_search.format_results(results, room),
}
else:
return {
"command": verb,
"error": f"Unknown command: '{verb}'. Try: look, go <dir>, examine <obj>, say <msg>, ask <msg>",
"error": f"Unknown command: '{verb}'. Try: look, go <dir>, examine <obj>, say <msg>, ask <msg>, find <name>",
}
def _extract_exits(self, description: str) -> dict: