import re COMMAND_VERBS = { "go", "head", "walk", "run", "visit", "check", "rest", "help", "tell", "make", "ask", "send", "move", "bring", "look", } BASE_LOCATION_ALIASES = { "clinic": "nia_clinic", "hospital": "nia_clinic", "nurse": "nia_clinic", "cafe": "cafe", "café": "cafe", "coffee": "cafe", "school": "school", "class": "school", "park": "park", "fountain": "fountain", "square": "square", "plaza": "square", "office": "priya_office", "city hall": "priya_office", "shop": "shop", "store": "shop", } def classify(text, world): raw = (text or "").strip() if not raw: return {"type": "noop", "text": raw, "addressees": [], "instruction": "", "goto": None} if not re.search(r"[A-Za-z0-9]", raw): return {"type": "ambient", "text": raw, "addressees": [], "instruction": raw, "goto": None} cast = world.get("cast", []) names = _matched_names(raw, cast) lowered = raw.lower() looks_command = bool(names) and ( _starts_with_name_command(lowered, names) or lowered.startswith("tell ") or lowered.startswith("make ") or lowered.startswith("ask ") or any(re.search(rf"\b{verb}\b", lowered) for verb in COMMAND_VERBS) ) if looks_command: instruction = _strip_command_prefix(raw, names) goto = resolve_location(instruction, world, cast, names) return { "type": "directed_command", "text": raw, "addressees": names, "instruction": instruction, "goto": goto, } return {"type": "world_event", "text": raw, "addressees": [], "instruction": raw, "goto": None} def resolve_location(text, world, cast=None, addressees=None): lowered = (text or "").lower() hotspots = ((world.get("board", {}) or {}).get("hotspots_tile") or {}) if "home" in lowered and cast and addressees and len(addressees) == 1: char = next((c for c in cast if c["name"] == addressees[0]), None) home = char.get("home") if char else None if home in hotspots: return home aliases = dict(BASE_LOCATION_ALIASES) if "nurse_office" in hotspots: aliases["clinic"] = "nurse_office" aliases["nurse"] = "nurse_office" aliases["nurse office"] = "nurse_office" if "quad" in hotspots: aliases["quad"] = "quad" aliases["campus"] = "quad" if "riverside_walk" in hotspots: aliases["river"] = "riverside_walk" aliases["riverside"] = "riverside_walk" for key in hotspots: aliases[key] = key aliases[key.replace("_", " ")] = key aliases[key.replace("_", "")] = key for phrase, hotspot in sorted(aliases.items(), key=lambda item: len(item[0]), reverse=True): if phrase in lowered and hotspot in hotspots: return hotspot return None def _matched_names(text, cast): lowered = text.lower() matches = [] for char in cast: first = char["name"].split()[0].lower() if re.search(rf"\b{re.escape(first)}\b", lowered): matches.append(char["name"]) return matches def _starts_with_name_command(lowered, names): first_names = [name.split()[0].lower() for name in names] for first in first_names: if re.match(rf"^\s*{re.escape(first)}\s*[,:\-]\s*", lowered): return True return False def _strip_command_prefix(text, names): instruction = text.strip() first_names = "|".join(re.escape(name.split()[0]) for name in names) patterns = [ rf"^\s*(?:tell|ask|make|send)\s+(?:{first_names})\s+(?:to\s+)?", rf"^\s*(?:{first_names})\s*[,:\-]\s*", ] for pattern in patterns: instruction = re.sub(pattern, "", instruction, flags=re.IGNORECASE).strip() return instruction or text.strip()