Download hallway.py from alvations/hallway8: direct link, hf CLI and curl.
- Browser
- Download file 5.19 kB
-
https://huggingface.co/spaces/alvations/hallway8/resolve/161d0a9e03df610b4d5be4b3eaadbcc31163ba55/hallway.py
- Command line
-
hf download hf://spaces/alvations/hallway8@161d0a9e03df610b4d5be4b3eaadbcc31163ba55/hallway.py
-
curl -L -o hallway.py https://huggingface.co/spaces/alvations/hallway8/resolve/161d0a9e03df610b4d5be4b3eaadbcc31163ba55/hallway.py
5.19 kB
| """The corridor: building a loop and describing it. | |
| A "room" is not a paragraph. It is a set of properties, each currently at its | |
| baseline value or (at most one of them) at an anomalous value. The describer | |
| turns that into a handful of sentences drawn from rotating pools, so the same | |
| place reads differently every time while still being the same place. | |
| Content is organised into *arcs* (skins/backstories) under data/arcs/. Each arc | |
| file shares the same schema; only the theme, story, and vocabulary change. | |
| """ | |
| from __future__ import annotations | |
| import glob | |
| import json | |
| import os | |
| import random | |
| from typing import Dict, List, Optional | |
| import anomalies | |
| from memory import PlayerMemory | |
| ARCS_DIR = os.path.join(os.path.dirname(__file__), "data", "arcs") | |
| DEFAULT_ARC = "hallway-eight" | |
| # How many hallways/landings you must clear. Reaching this level wins. | |
| GOAL = 8 | |
| # How many details are described each loop. Enough to overload memory a little, | |
| # few enough to keep it readable. | |
| MIN_DETAILS = 4 | |
| MAX_DETAILS = 6 | |
| def _arc_path(arc_id: str) -> str: | |
| return os.path.join(ARCS_DIR, f"{arc_id}.json") | |
| def list_arcs() -> List[dict]: | |
| """Return the selectable arcs, described by their meta block. | |
| The default arc is listed first; the rest follow alphabetically. | |
| """ | |
| arcs = [] | |
| for path in sorted(glob.glob(os.path.join(ARCS_DIR, "*.json"))): | |
| with open(path, "r", encoding="utf-8") as f: | |
| meta = json.load(f).get("meta", {}) | |
| if meta.get("id"): | |
| arcs.append({ | |
| "id": meta["id"], | |
| "title": meta.get("title", meta["id"]), | |
| "tagline": meta.get("tagline", ""), | |
| "skin": meta.get("skin", "hallway"), | |
| }) | |
| arcs.sort(key=lambda a: (a["id"] != DEFAULT_ARC, a["id"])) | |
| return arcs | |
| class Hallway: | |
| def __init__(self, arc_id: str = DEFAULT_ARC): | |
| path = _arc_path(arc_id) | |
| if not os.path.exists(path): | |
| path = _arc_path(DEFAULT_ARC) | |
| with open(path, "r", encoding="utf-8") as f: | |
| self.data = json.load(f) | |
| self.properties: Dict[str, dict] = self.data["properties"] | |
| self.meta: dict = self.data.get("meta", {}) | |
| self.labels: Dict[str, str] = self.meta.get("labels", {}) | |
| def build(self, mem: PlayerMemory, rng: random.Random) -> dict: | |
| """Generate the next place for the player's current level. | |
| The probability of an anomaly stays near a coin-flip so neither | |
| "always continue" nor "always turn back" is a winning strategy. | |
| """ | |
| has_anomaly = rng.random() < 0.5 | |
| anomaly_prop: Optional[str] = None | |
| anomaly_val: Optional[str] = None | |
| if has_anomaly: | |
| anomaly_prop, anomaly_val = anomalies.pick_anomaly(self.properties, rng) | |
| # Decide which properties get described this loop. | |
| order = list(self.data["anchor_order"]) | |
| rng.shuffle(order) | |
| n = rng.randint(MIN_DETAILS, MAX_DETAILS) | |
| shown = order[:n] | |
| # If there is an anomaly, it must be visible this loop -- otherwise | |
| # the player is asked to notice something they were never shown. | |
| if anomaly_prop and anomaly_prop not in shown: | |
| shown[rng.randrange(len(shown))] = anomaly_prop | |
| # Re-sort shown into the canonical order so the "camera" of the prose | |
| # feels consistent even as content rotates. | |
| shown = [p for p in self.data["anchor_order"] if p in shown] | |
| sign_text = anomalies.drift_sign(self.data["sign_variants"], rng) | |
| sentences: List[str] = [] | |
| for prop in shown: | |
| spec = self.properties[prop] | |
| if prop == anomaly_prop: | |
| pool = spec["anomalies"][anomaly_val] | |
| else: | |
| baseline = spec["baseline"] | |
| pool = spec["values"][baseline] | |
| line = anomalies.render_sentence(rng.choice(pool), sign_text) | |
| sentences.append(line) | |
| # Adaptive touch: if the player keeps inspecting one thing, and it is | |
| # on show this loop, let the place notice. | |
| fav = mem.favorite | |
| adaptive_line = None | |
| if fav and fav in shown and rng.random() < 0.5: | |
| tmpl = rng.choice(self.data["adaptive"]["repeat_inspect"]) | |
| adaptive_line = tmpl.replace("{THING}", self.labels.get(fav, fav)) | |
| heading = anomalies.drift_heading( | |
| self.data["title_variants"], mem.loops, rng | |
| ) | |
| return { | |
| "heading": heading, | |
| "sentences": sentences, | |
| "adaptive": adaptive_line, | |
| "sign_text": sign_text, | |
| "shown": shown, | |
| # server-authoritative truth, never sent to the client: | |
| "_has_anomaly": has_anomaly, | |
| "_anomaly_prop": anomaly_prop, | |
| "_anomaly_val": anomaly_val, | |
| } | |
| def public(self, room: dict) -> dict: | |
| """Strip the answer before sending a room to the browser.""" | |
| return {k: v for k, v in room.items() if not k.startswith("_")} | |
| def intro(self) -> List[str]: | |
| return self.data["framing"]["intro"] | |
| def win(self) -> List[str]: | |
| return self.data["framing"]["win"] | |