← Policies

V2 — Ordered List

lesson_7/policies/v2 · rule bot · first deterministic strategy

V2 is the first policy that acts with intent. Every legal action arrives from the engine pre-classified into one of seven categories, and the category number doubles as a priority: resolve pending choices first, then buy, then use base abilities, then play cards, then attack bases, then attack the opponent, and only then end the turn. Within a category, a documented tie-break makes the choice fully deterministic (e.g. purchases: most expensive first, then alphabetical). The full design rationale lives in docs/lesson_7_action_ordering.md.

PriorityBucketTie-break
0Pending choices (forced discards, scraps, …)name, then index
1Purchasecost DESC, then name
2Base abilityname
3Play cardname
4Attack baseoutposts first, defense DESC, cost DESC, name DESC
5Attack opponent
6End turn

The policy module is a thin shell — it turns the chosen action into a one-hot distribution:

lesson_7/policies/v2/__init__.py

"""V2 -- Ordered List: deterministic priority play."""

from __future__ import annotations

from random import Random

import star_engine as se

from ..base import Actions, Policy
from ..common import one_hot, ordered_list_action


class V2(Policy):
    def __init__(self) -> None:
        super().__init__("V2 Ordered List")

    def get_action_probabilities(
        self, game: se.Game, actions: Actions, rng: Random
    ) -> list[float]:
        return one_hot(actions, ordered_list_action(game, actions))

The selection itself is a low-cardinality two-pass scan (the engine auto-resolves forced moves, so a decision typically offers ≤ 10 actions): find the highest-priority bucket present, then take the argmin of that bucket's tie-break key.

lesson_7/policies/common.py (excerpt)

def ordered_list_action(game: se.Game, actions: Actions) -> se.LegalAction:
    """V2: first action by fixed priority (lowest category number), tie-broken by
    that category's key."""
    return _select(actions, _by_category, lambda cat: _KEY_FOR[cat])


def _by_category(a: se.LegalAction) -> float:
    return a.category


def _select(
    actions: Actions,
    priority: Callable[[se.LegalAction], float],
    key_for: Callable[[float], Callable[[se.LegalAction], tuple]],
) -> se.LegalAction:
    """Low-cardinality selection: one O(n) pass to the highest-priority bucket
    present (smallest `priority` value), then a small argmin within it.
    `priority` maps an action -> its priority value; `key_for` maps a priority
    value -> the tie-break key fn for that bucket."""
    best = min(priority(a) for a in actions)
    members = [a for a in actions if priority(a) == best]
    return min(members, key=key_for(best))


# Per-category tie-break key, indexed by the category integer (0..6), which is
# also the priority. Mirrors docs/lesson_7_action_ordering.md.
_KEY_FOR: dict[int, Callable[[se.LegalAction], tuple]] = {
    se.CAT_PENDING: lambda a: (a.name, a.index),
    se.CAT_PURCHASE: _key_purchase,
    se.CAT_BASE_ABILITY: _key_name_asc,
    se.CAT_PLAY_CARD: _key_name_asc,
    se.CAT_ATTACK_BASE: _key_attack_base,
    se.CAT_ATTACK_OPPONENT: _key_index,
    se.CAT_END_TURN: _key_index,
}


def _key_purchase(a: se.LegalAction):
    # Most expensive first, then alphabetical: cost DESC, name ASC.
    return (-a.cost, a.name)


def _key_attack_base(a: se.LegalAction):
    # Outposts first, then defense (hit points) DESC, cost DESC, name DESC.
    return (not a.is_outpost, -a.defense, -a.cost, _RevStr(a.name))


# --- ordered-list tie-breaks -------------------------------------------------
class _RevStr:
    """Wrapper that reverses string ordering, so a `min()` key can express
    "name DESCENDING" inside an otherwise-ascending tuple key."""

    __slots__ = ("s",)

    def __init__(self, s: str) -> None:
        self.s = s

    def __lt__(self, other: "_RevStr") -> bool:
        return self.s > other.s

    def __eq__(self, other: object) -> bool:
        return isinstance(other, _RevStr) and self.s == other.s

All of common.py (shared by V2–V4) is on its own page.

← Policies · Home