treefmt / nix fmt (pull_request) Successful in 6s
test ebook search / test-ebook-search (pull_request) Successful in 39s
pytest / pytest (pull_request) Successful in 40s
build_systems / build-brain (pull_request) Successful in 1m0s
build_systems / build-bob (pull_request) Successful in 1m2s
build_systems / build-rhapsody-in-green (pull_request) Successful in 1m33s
build_systems / build-jeeves (pull_request) Successful in 2m25s
test ebook search / test-ebook-search (push) Successful in 35s
build_systems / build-brain (push) Successful in 37s
build_systems / build-bob (push) Successful in 37s
build_systems / build-jeeves (push) Successful in 2m17s
treefmt / nix fmt (push) Successful in 5s
pytest / pytest (push) Successful in 29s
build_systems / build-rhapsody-in-green (push) Successful in 49s
- implement FastAPI and HTMX lobby and game interfaces - support up to four human and AI-controlled players - add configurable rules, victory conditions, and expansion modules - support validated JSON cards, patrons, objectives, and outposts
59 lines
2.4 KiB
Python
59 lines
2.4 KiB
Python
"""Requirement evaluation shared by patrons, objectives, outposts, and AI."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from .models import Requirement, RequirementKind
|
|
|
|
|
|
def requirements_met(requirements: list[Requirement], bonuses: dict[str, int]) -> bool:
|
|
"""Return whether one assignment satisfies all fixed and wildcard clauses."""
|
|
chosen: dict[str, str] = {}
|
|
|
|
def visit(index: int) -> bool:
|
|
if index == len(requirements):
|
|
return True
|
|
requirement = requirements[index]
|
|
if requirement.kind == RequirementKind.COLOR:
|
|
if bonuses.get(requirement.resource or "", 0) < requirement.count:
|
|
return False
|
|
chosen[requirement.id] = requirement.resource or ""
|
|
return visit(index + 1)
|
|
|
|
forbidden = set(requirement.exclude)
|
|
forbidden.update(chosen[item] for item in requirement.distinct_from if item in chosen)
|
|
for resource, amount in bonuses.items():
|
|
if resource in forbidden or amount < requirement.count:
|
|
continue
|
|
chosen[requirement.id] = resource
|
|
if visit(index + 1):
|
|
return True
|
|
chosen.pop(requirement.id, None)
|
|
return False
|
|
|
|
fixed = [item for item in requirements if item.kind == RequirementKind.COLOR]
|
|
flexible = [item for item in requirements if item.kind == RequirementKind.ANY_COLOR]
|
|
return visit_ordered([*fixed, *flexible], bonuses, chosen)
|
|
|
|
|
|
def visit_ordered(requirements: list[Requirement], bonuses: dict[str, int], chosen: dict[str, str]) -> bool:
|
|
"""Backtracking evaluator kept separate for straightforward unit testing."""
|
|
if not requirements:
|
|
return True
|
|
requirement, *rest = requirements
|
|
if requirement.kind == RequirementKind.COLOR:
|
|
resource = requirement.resource or ""
|
|
if bonuses.get(resource, 0) < requirement.count:
|
|
return False
|
|
chosen[requirement.id] = resource
|
|
return visit_ordered(rest, bonuses, chosen)
|
|
forbidden = set(requirement.exclude)
|
|
forbidden.update(chosen[item] for item in requirement.distinct_from if item in chosen)
|
|
for resource, amount in bonuses.items():
|
|
if resource in forbidden or amount < requirement.count:
|
|
continue
|
|
chosen[requirement.id] = resource
|
|
if visit_ordered(rest, bonuses, chosen):
|
|
return True
|
|
chosen.pop(requirement.id, None)
|
|
return False
|