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