owenrusk.dev

casebench

test cases straight onto the board.

git clone https://owenrusk.dev/casebench.git

commit 1348d3064dbe12858c07a6ffbbd956b9c210b6ac
parent e12a67868ce846f0e8b72e67262a405d9c8810af
author Owen Rusk <owen@papermothgames.com>
date   2025-05-06 15:02:33 -0500
lock: lock a case behind an answer

only the hash is stored. the lock says what --text says, locked. by default.
casebench/cli.py+16-1
casebench/db.py+10-0
casebench/lock.py+14-0
diff --git a/casebench/cli.py b/casebench/cli.py
index d20c350..a493488 100644
--- a/casebench/cli.py
+++ b/casebench/cli.py
@@ -5,7 +5,7 @@ from pathlib import Path
 
 import psycopg
 
-from . import casefile, db
+from . import casefile, db, lock
 
 
 class Fail(Exception):
@@ -30,6 +30,13 @@ def main(argv: list[str] | None = None) -> int:
     note.add_argument("--item", help="the item to write on; the case's title if left out")
     note.set_defaults(run=run_note)
 
+    locker = sub.add_parser("lock", help="lock a case behind an answer")
+    locker.add_argument("case")
+    locker.add_argument("--player", required=True)
+    locker.add_argument("--answer", required=True)
+    locker.add_argument("--text", default="locked.", help="what the lock says (default: locked.)")
+    locker.set_defaults(run=run_lock)
+
     args = parser.parse_args(argv)
     dsn = os.environ.get("CASEBENCH_DSN")
     if not dsn:
@@ -69,3 +76,11 @@ def run_note(conn: psycopg.Connection, args: argparse.Namespace) -> None:
     text = args.file.read_text(encoding="utf-8") if args.file else args.text
     db.note(conn, who, args.case, args.item, text)
     print(f"{args.player}: noted {args.case}" + (f" / {args.item}" if args.item else ""))
+
+
+def run_lock(conn: psycopg.Connection, args: argparse.Namespace) -> None:
+    who = player(conn, args.player)
+    if db.source(conn, who, args.case) is None:
+        raise Fail(f"{args.player} doesn't have {args.case}")
+    db.lock(conn, who, args.case, args.text, lock.digest(args.answer))
+    print(f"{args.player}: locked {args.case}")
diff --git a/casebench/db.py b/casebench/db.py
index 5446104..54293ab 100644
--- a/casebench/db.py
+++ b/casebench/db.py
@@ -64,3 +64,13 @@ def note(conn: psycopg.Connection, player: str, case_id: str, item_id: str | Non
         (player, case_id, item_id, text),
     )
     conn.commit()
+
+
+def lock(conn: psycopg.Connection, player: str, case_id: str, text: str, answer_hash: str) -> None:
+    conn.execute(
+        "insert into board_locks (player_id, case_id, text, answer_hash) values (%s, %s, %s, %s)"
+        " on conflict (player_id, case_id) do update"
+        " set text = excluded.text, answer_hash = excluded.answer_hash, opened_at = null",
+        (player, case_id, text, answer_hash),
+    )
+    conn.commit()
diff --git a/casebench/lock.py b/casebench/lock.py
new file mode 100644
index 0000000..cce8057
--- /dev/null
+++ b/casebench/lock.py
@@ -0,0 +1,14 @@
+import hashlib
+
+
+def normalize(answer: str) -> str:
+    return " ".join(answer.casefold().split())
+
+
+def digest(answer: str) -> str:
+    return hashlib.sha256(normalize(answer).encode()).hexdigest()
+
+
+def opens(stored: str, typed: str) -> bool:
+    # the board calls this when someone types into a lock.
+    return digest(typed) == stored