File size: 3,602 Bytes
50f2856
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
"""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")}