ScottWuMath / game /tasks.py
AdithyaSK's picture
AdithyaSK HF Staff
Card Arithmetic: OpenEnv environment and play page
50f2856 verified
Raw History Blame Contribute Delete
3.6 kB
"""Levels and task generation.
The levels follow the video: the 24 game with four cards, then "once that got too easy, you take
eight cards and you make the current year", then nine cards for any four-digit number someone
names. Three-digit with six cards sits between the two as a step up from 24.
Every generated task is solvable: the deal is checked with the solver and redealt if it fails.
"""
from __future__ import annotations
import datetime as _dt
import random
from dataclasses import dataclass
from .cards import Card, cards_from_ranks, deal
from .expr import Tree, steps, to_text
from .solver import solve
CURRENT_YEAR = _dt.date.today().year
@dataclass(frozen=True)
class Level:
name: str
title: str
n_cards: int
low: int
high: int
LEVELS = {
"24": Level("24", "24", 4, 24, 24),
"three-digit": Level("three-digit", "Three-digit", 6, 100, 999),
"year": Level("year", "Year", 8, 1900, 2100),
"four-digit": Level("four-digit", "Four-digit", 9, 1000, 9999),
}
DEFAULT_LEVEL = "year"
def cards_for_target(target: int) -> int:
"""How many cards a custom target gets: 4 for up to 99, 6 for three digits, 8 for 1000-2999, then 9."""
if target < 100:
return 4
if target < 1000:
return 6
if target < 3000:
return 8
return 9
def make_task(
rng: random.Random,
level: str | None = None,
target: int | None = None,
n_cards: int | None = None,
ranks: list[int] | None = None,
task_id: str | None = None,
time_limit: float = 2.0,
max_deals: int = 50,
) -> dict:
"""Deal a solvable task.
With no `target`, the level picks one from its range. A given `target` keeps the level's card
count, or without a level picks one by size (`cards_for_target`). Given `ranks` are dealt as
they are and may have no solution (then `solution` is None).
"""
if level is not None and level not in LEVELS:
raise ValueError(f"unknown level {level!r}; choose one of {', '.join(LEVELS)}")
if target is None:
lvl = LEVELS[level or DEFAULT_LEVEL]
level, target = lvl.name, rng.randint(lvl.low, lvl.high)
target = int(target)
if not 0 <= target <= 1_000_000:
raise ValueError("target must be a whole number from 0 to 1,000,000")
if ranks is not None:
cards = cards_from_ranks(list(ranks), rng)
tree = solve([c.rank for c in cards], target, time_limit)
return _task(cards, target, tree, level or "custom", task_id)
n = n_cards or (LEVELS[level].n_cards if level else cards_for_target(target))
if not 2 <= n <= 12:
raise ValueError("deal between 2 and 12 cards")
for _ in range(max_deals):
cards = deal(n, rng)
tree = solve([c.rank for c in cards], target, time_limit)
if tree is not None:
return _task(cards, target, tree, level or "custom", task_id)
raise RuntimeError(f"no solvable deal of {n} cards for {target} in {max_deals} tries")
def _task(cards: list[Card], target: int, tree: Tree | None, level: str, task_id: str | None) -> dict:
return {
"id": task_id or f"{level}-{target}-" + "".join(c.label for c in cards),
"level": level,
"target": target,
"cards": [c.to_dict() for c in cards],
"solution": None if tree is None else to_text(tree),
"solution_steps": [] if tree is None else steps(tree),
}
def public(task: dict) -> dict:
"""The task as an agent may see it: no reference solution."""
return {k: v for k, v in task.items() if k not in ("solution", "solution_steps")}