eleusis / game.py
nph4rd's picture
Upload game.py with huggingface_hub
d4494b3 verified
Raw
History Blame Contribute Delete
5.1 kB
"""Eleusis game state machine — pure logic, runs inside Pyodide.
Two variants share one engine:
- "hf": the 26-rule benchmark eval set; 10 turns; played card is replaced
by a draw (the recorded model games are rl4-consistency-step30).
- "small": the simple-rules era; 6 held-out rules x 4 deals; 8 turns; no
redraws (recorded games are the step-90 LoRA of the hosted run).
All rule evaluation and guess-checking uses the engine extracted verbatim
from the eleusis environment, so the human faces exactly the checks the
model faced.
"""
import json
import random
from cards import parse_card, deck
from engine import compile_python_rule, guess_matches_target
VARIANTS = {
"hf": {"max_turns": 10, "redraw": True},
"small": {"max_turns": 8, "redraw": False},
}
GAMES = {}
STATE = None
def load_games(variant: str, payload: str) -> int:
GAMES[variant] = json.loads(payload)
return len(GAMES[variant])
def new_game(variant: str) -> str:
global STATE
cfg = VARIANTS[variant]
games = GAMES[variant]
task_id = random.choice(list(games.keys()))
info = games[task_id]
starter, hand, draws = info["starter"], list(info["hand"]), list(info.get("draws", []))
if cfg["redraw"]:
used = {starter, *hand, *draws}
fallback = [c for c in deck() if c not in used]
random.Random(task_id).shuffle(fallback)
draws = draws + fallback
STATE = {
"variant": variant,
"task_id": task_id,
"hand": hand,
"mainline": [starter],
"board": [{"card": starter, "rejects": []}],
"turn": 0,
"draws": draws,
"solved": False,
"over": False,
"log": [],
}
return _public_state()
def _info():
return GAMES[STATE["variant"]][STATE["task_id"]]
def _cfg():
return VARIANTS[STATE["variant"]]
def play_turn(selected_card: str, guess_text: str, no_play: bool) -> str:
if STATE is None:
return json.dumps({"error": "no game"})
s = STATE
cfg = _cfg()
if s["over"]:
return _public_state()
if not no_play and (not selected_card or selected_card not in s["hand"]):
return _public_state("Pick a card from your hand first (or declare no-play).")
target = compile_python_rule(_info()["code"])
s["turn"] += 1
mainline_cards = [parse_card(c) for c in s["mainline"]]
guess = (guess_text or "").strip()
guess_correct = bool(guess) and guess_matches_target(guess, target)
guess_error = ""
if guess and not guess_correct:
try:
compile_python_rule(guess)
except Exception as exc:
guess_error = f"guess did not compile: {exc}"
if no_play:
would_accept = any(target(parse_card(c), mainline_cards) for c in s["hand"])
result = "missed_play" if would_accept else "correct_no_play"
card = None
else:
card = selected_card
accepted = target(parse_card(card), mainline_cards)
s["hand"].remove(card)
if accepted:
s["mainline"].append(card)
s["board"].append({"card": card, "rejects": []})
result = "accepted"
else:
s["board"][-1]["rejects"].append(card)
result = "rejected"
if cfg["redraw"] and s["draws"]:
s["hand"].append(s["draws"].pop(0))
s["log"].append({"card": card, "result": result, "guess": guess,
"guess_correct": guess_correct, "guess_error": guess_error})
if guess_correct:
s["solved"], s["over"] = True, True
elif s["turn"] >= cfg["max_turns"]:
s["over"] = True
return _public_state()
def give_up() -> str:
if STATE is None:
return json.dumps({"error": "no game"})
STATE["over"] = True
return _public_state()
def _public_state(message: str = "") -> str:
assert STATE is not None
s: dict = STATE
out = {k: s[k] for k in ("variant", "hand", "board", "turn", "solved", "over", "log")}
out["max_turns"] = _cfg()["max_turns"]
out["message"] = message
info = _info()
out["model_solved_count"] = sum(e["solved"] for e in info["episodes"])
out["model_attempts"] = [
{"solved": e["solved"], "turns_used": e["turns_used"]} for e in info["episodes"]
]
if s["over"]:
out["rule_label"] = info["label"]
out["rule_code"] = info["code"]
solved_turns = [e["turns_used"] for e in info["episodes"] if e["solved"]]
out["model_best_turns"] = min(solved_turns) if solved_turns else None
out["score"] = (_cfg()["max_turns"] + 1 - s["turn"]) if s["solved"] else 0
return json.dumps(out)
def reveal_attempt(idx: int) -> str:
if STATE is None:
return json.dumps({"error": "no game"})
info = _info()
ep = info["episodes"][int(idx)]
return json.dumps({
"index": int(idx),
"solved": ep["solved"],
"turns_used": ep["turns_used"],
"starter": info["starter"],
"turns": ep["turns"],
"model_solved_count": sum(e["solved"] for e in info["episodes"]),
})