File size: 5,188 Bytes
161d0a9 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 | """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"]
|