Added chatterbox connections
This commit is contained in:
1 parent
41ef12f954
commit
e58ceeb192
58 files changed
+8931
-8
No files matched your search
@@ -0,0 +1,87 @@
|
||||
"""Persistent, revision-checked hint library for the lab. Text is data, never executable."""
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import threading
|
||||
import uuid
|
||||
|
||||
ROOT = Path(__file__).resolve().parent
|
||||
PATH = ROOT / ".lab-data" / "hints.json"
|
||||
LOCK = threading.Lock()
|
||||
PHASES = ["ALL","BRIEFING","ARCADE","BOSS","REVEAL","CELLS","CALIBRATION_TUTORIAL","CALIBRATION","LAUNCH_ARM","COUNTDOWN","DEBRIEF"]
|
||||
TARGETS = ["room"] + ["P01","P02A","P02B","P03","P04","P05","P06"] + [f"cabinet_{i:02}" for i in range(1,7)]
|
||||
def defaults():
|
||||
hints=[]
|
||||
for id,title,text in [
|
||||
("P01","Claw controls","Start by investigating the claw machine controls."),
|
||||
("P02A","First claw reward","Now that the controls work, try retrieving the first token."),
|
||||
("P02B","Second claw reward","The first token has arrived. The claw still has another reward."),
|
||||
("P03","Records and sleeves","Look at the records together with their sleeves."),
|
||||
("P04","Service panel","Compare the symbols on the service panel."),
|
||||
("P05","Ticket values","Look closely at the values on the tickets."),
|
||||
("P06","Poster clues","There may be more to the poster when you look at its layers.")]:
|
||||
hints.append({"id":"default-"+id,"title":title,"text":text,"categories":["Discovery"],"phase":"ARCADE","target":id,"condition":"incomplete","level":1})
|
||||
for id,title,text,phase,category in [
|
||||
("tokens","Matching tokens","Each token belongs to one machine. Look for the cabinet name.","ARCADE","Arcade"),
|
||||
("cells","Power cells","Charge each cell, remove it from its bay, then place it in its matching ship socket.","CELLS","Ship"),
|
||||
("practice","Practice together","Name the station, repeat the target, hold it steady, press Ready, then Execute.","CALIBRATION_TUTORIAL","Calibration"),
|
||||
("calibration","Team coordination","Read back the target before pressing Ready. The navigator presses Execute when the team is ready.","CALIBRATION","Calibration"),
|
||||
("launch","Final launch","Arm the stations in order. Confirm each group is ready before the navigator commits the launch.","LAUNCH_ARM","Finale")]:
|
||||
hints.append({"id":"default-"+id,"title":title,"text":text,"categories":[category],"phase":phase,"target":"room","condition":"any","level":1})
|
||||
return {"revision":0,"hints":hints}
|
||||
|
||||
def read():
|
||||
with LOCK:
|
||||
return _read()
|
||||
def _read():
|
||||
if not PATH.exists(): return defaults()
|
||||
data=json.loads(PATH.read_text(encoding="utf-8"))
|
||||
if not isinstance(data,dict) or not isinstance(data.get("revision"),int) or not isinstance(data.get("hints"),list):
|
||||
raise ValueError("Hint file is invalid; it was not overwritten")
|
||||
return data
|
||||
|
||||
class Conflict(ValueError): pass
|
||||
|
||||
def validate(hint):
|
||||
if not isinstance(hint,dict): raise ValueError("Missing hint")
|
||||
clean={}
|
||||
for key,limit in (("title",100),("text",1500)):
|
||||
value=hint.get(key)
|
||||
if not isinstance(value,str) or not 1<=len(value.strip())<=limit:
|
||||
raise ValueError(f"{key} must contain 1-{limit} characters")
|
||||
clean[key]=value.strip()
|
||||
categories=hint.get("categories",[])
|
||||
if not isinstance(categories,list) or not 1<=len(categories)<=8 or any(not isinstance(x,str) or not 1<=len(x.strip())<=40 for x in categories):
|
||||
raise ValueError("Use 1-8 category names, each up to 40 characters")
|
||||
clean["categories"]=list(dict.fromkeys(x.strip() for x in categories))
|
||||
for key,allowed in (("phase",PHASES),("target",TARGETS),("condition",["any","incomplete","complete"]),("level",[1,2,3])):
|
||||
if hint.get(key) not in allowed: raise ValueError("Invalid "+key)
|
||||
clean[key]=hint[key]
|
||||
return clean
|
||||
|
||||
def save(body):
|
||||
if not isinstance(body,dict): raise ValueError("Expected a hint object")
|
||||
hint=validate(body.get("hint"))
|
||||
with LOCK:
|
||||
data=_read()
|
||||
if body.get("revision")!=data["revision"]: raise Conflict("Hints changed on another device. Reload the library, then save your draft again.")
|
||||
id=body["hint"].get("id","")
|
||||
if id:
|
||||
index=next((i for i,h in enumerate(data["hints"]) if h["id"]==id),None)
|
||||
if index is None: raise ValueError("Hint no longer exists")
|
||||
data["hints"][index]={"id":id,**hint}
|
||||
else:
|
||||
if len(data["hints"])>=1000: raise ValueError("Library is limited to 1000 hints")
|
||||
data["hints"].append({"id":uuid.uuid4().hex,**hint})
|
||||
data["revision"]+=1
|
||||
PATH.parent.mkdir(parents=True,exist_ok=True)
|
||||
temp=PATH.with_suffix(".tmp")
|
||||
with temp.open("w",encoding="utf-8") as stream:
|
||||
json.dump(data,stream,ensure_ascii=False,indent=2)
|
||||
stream.flush()
|
||||
os.fsync(stream.fileno())
|
||||
if PATH.exists():
|
||||
PATH.with_suffix(".bak").write_bytes(PATH.read_bytes())
|
||||
temp.replace(PATH)
|
||||
return data
|
||||
|
||||
Reference in new issue
Block a user