Spaces:
Running
Running
Codex Claude Opus 4.8 commited on
Commit ·
b483ca7
1
Parent(s): 8f039b3
Trim prefetch to 1 pack, log model-pack failures, JSON-constrained cards, rotate fallback names
Browse files- ai_runtime.py +1 -1
- clients.py +2 -3
- generator.py +35 -7
- local_llm.py +3 -0
- tests/test_ai_runtime.py +1 -1
- tests/test_clients.py +2 -3
- tests/test_generator.py +6 -0
- tests/test_ui.py +29 -3
- ui.py +14 -4
ai_runtime.py
CHANGED
|
@@ -43,7 +43,7 @@ def local_ai_env(card_port: int = DEFAULT_CARD_PORT) -> dict[str, str]:
|
|
| 43 |
"TABRAS_CARD_ENDPOINT": chat_endpoint(card_port),
|
| 44 |
"TABRAS_CARD_MODEL": "minicpm-v-4.6-q4",
|
| 45 |
"TABRAS_CARD_TEMPERATURE": "0.7",
|
| 46 |
-
"TABRAS_CARD_MAX_TOKENS": "
|
| 47 |
"TABRAS_AI_BOSS": "1",
|
| 48 |
"TABRAS_BOSS_BACKEND": "mlx",
|
| 49 |
"TABRAS_BOSS_MODEL": DEFAULT_BOSS_MODEL,
|
|
|
|
| 43 |
"TABRAS_CARD_ENDPOINT": chat_endpoint(card_port),
|
| 44 |
"TABRAS_CARD_MODEL": "minicpm-v-4.6-q4",
|
| 45 |
"TABRAS_CARD_TEMPERATURE": "0.7",
|
| 46 |
+
"TABRAS_CARD_MAX_TOKENS": "256",
|
| 47 |
"TABRAS_AI_BOSS": "1",
|
| 48 |
"TABRAS_BOSS_BACKEND": "mlx",
|
| 49 |
"TABRAS_BOSS_MODEL": DEFAULT_BOSS_MODEL,
|
clients.py
CHANGED
|
@@ -51,7 +51,7 @@ def configure_mode() -> str:
|
|
| 51 |
os.environ.setdefault("TABRAS_ART_MODEL", DEFAULT_ART_MODEL)
|
| 52 |
os.environ.setdefault("TABRAS_AI_BOSS", "1")
|
| 53 |
os.environ.setdefault("TABRAS_CARD_TEMPERATURE", "0.7")
|
| 54 |
-
os.environ.setdefault("TABRAS_CARD_MAX_TOKENS", "
|
| 55 |
os.environ.setdefault("TABRAS_BOSS_MAX_TOKENS", "96")
|
| 56 |
os.environ.setdefault("TABRAS_ART_STEPS", "4")
|
| 57 |
return mode
|
|
@@ -61,13 +61,12 @@ def configure_mode() -> str:
|
|
| 61 |
def card_client_from_env() -> CardPackClient | None:
|
| 62 |
if os.environ.get("TABRAS_CARD_BACKEND") == "llamacpp":
|
| 63 |
return LlamaCppCardClient(
|
| 64 |
-
|
| 65 |
endpoint=os.environ.get("TABRAS_CARD_ENDPOINT", DEFAULT_LLAMA_CARD_ENDPOINT),
|
| 66 |
model=os.environ.get("TABRAS_CARD_MODEL", DEFAULT_CARD_MODEL),
|
| 67 |
timeout_seconds=int(os.environ.get("TABRAS_CARD_TIMEOUT", "60")),
|
| 68 |
temperature=float(os.environ.get("TABRAS_CARD_TEMPERATURE", "0.0")),
|
| 69 |
max_tokens=int(os.environ.get("TABRAS_CARD_MAX_TOKENS", "320")),
|
| 70 |
-
enable_thinking=False,
|
| 71 |
)
|
| 72 |
)
|
| 73 |
if os.environ.get("TABRAS_CARD_BACKEND") == "transformers":
|
|
|
|
| 51 |
os.environ.setdefault("TABRAS_ART_MODEL", DEFAULT_ART_MODEL)
|
| 52 |
os.environ.setdefault("TABRAS_AI_BOSS", "1")
|
| 53 |
os.environ.setdefault("TABRAS_CARD_TEMPERATURE", "0.7")
|
| 54 |
+
os.environ.setdefault("TABRAS_CARD_MAX_TOKENS", "256")
|
| 55 |
os.environ.setdefault("TABRAS_BOSS_MAX_TOKENS", "96")
|
| 56 |
os.environ.setdefault("TABRAS_ART_STEPS", "4")
|
| 57 |
return mode
|
|
|
|
| 61 |
def card_client_from_env() -> CardPackClient | None:
|
| 62 |
if os.environ.get("TABRAS_CARD_BACKEND") == "llamacpp":
|
| 63 |
return LlamaCppCardClient(
|
| 64 |
+
LocalJsonChatClient(
|
| 65 |
endpoint=os.environ.get("TABRAS_CARD_ENDPOINT", DEFAULT_LLAMA_CARD_ENDPOINT),
|
| 66 |
model=os.environ.get("TABRAS_CARD_MODEL", DEFAULT_CARD_MODEL),
|
| 67 |
timeout_seconds=int(os.environ.get("TABRAS_CARD_TIMEOUT", "60")),
|
| 68 |
temperature=float(os.environ.get("TABRAS_CARD_TEMPERATURE", "0.0")),
|
| 69 |
max_tokens=int(os.environ.get("TABRAS_CARD_MAX_TOKENS", "320")),
|
|
|
|
| 70 |
)
|
| 71 |
)
|
| 72 |
if os.environ.get("TABRAS_CARD_BACKEND") == "transformers":
|
generator.py
CHANGED
|
@@ -8,7 +8,7 @@ from pathlib import Path
|
|
| 8 |
from typing import Any, Protocol, Sequence
|
| 9 |
|
| 10 |
from budget import Card, CardSpec, EffectPlan, cost_card
|
| 11 |
-
from local_llm import
|
| 12 |
from primitives import PrimitiveId, School, school_bias, school_primitives
|
| 13 |
|
| 14 |
|
|
@@ -57,7 +57,7 @@ class CodexCardClient:
|
|
| 57 |
|
| 58 |
@dataclass(frozen=True)
|
| 59 |
class MiniCPMCardClient:
|
| 60 |
-
chat:
|
| 61 |
|
| 62 |
# Return one raw card payload from a local MiniCPM endpoint.
|
| 63 |
def create_card(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -74,7 +74,7 @@ class MiniCPMCardClient:
|
|
| 74 |
|
| 75 |
@dataclass(frozen=True)
|
| 76 |
class LlamaCppCardClient:
|
| 77 |
-
chat:
|
| 78 |
|
| 79 |
# Return one raw card payload from a llama.cpp MiniCPM endpoint.
|
| 80 |
def create_card(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
@@ -399,7 +399,7 @@ def llamacpp_card_prompt(payload: dict[str, Any], pack_cards: Sequence[dict[str,
|
|
| 399 |
|
| 400 |
|
| 401 |
# Generate one llama.cpp card with one corrective retry; quick mode takes the first card.
|
| 402 |
-
def generate_llamacpp_card(chat:
|
| 403 |
prompt = llamacpp_card_prompt(payload, pack_cards, focus)
|
| 404 |
card = parse_llamacpp_card_text(chat.complete(card_system_prompt(), prompt), payload, pack_cards, discard_bad_text=False)
|
| 405 |
need = llama_candidate_need(payload, pack_summary(pack_cards))
|
|
@@ -855,14 +855,42 @@ def fallback_art_prompt(payload: dict[str, Any]) -> str:
|
|
| 855 |
# Extract the first JSON object from model text.
|
| 856 |
def extract_json_object(text: str) -> str:
|
| 857 |
start = text.find("{")
|
| 858 |
-
|
| 859 |
-
if start == -1 or end == -1 or end < start:
|
| 860 |
raise ValueError("Codex response did not contain a JSON object")
|
| 861 |
-
|
|
|
|
| 862 |
json.loads(candidate)
|
| 863 |
return candidate
|
| 864 |
|
| 865 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 866 |
# Append missing JSON object or array closers.
|
| 867 |
def balance_json_closers(text: str) -> str:
|
| 868 |
stack: list[str] = []
|
|
|
|
| 8 |
from typing import Any, Protocol, Sequence
|
| 9 |
|
| 10 |
from budget import Card, CardSpec, EffectPlan, cost_card
|
| 11 |
+
from local_llm import ChatCompleter
|
| 12 |
from primitives import PrimitiveId, School, school_bias, school_primitives
|
| 13 |
|
| 14 |
|
|
|
|
| 57 |
|
| 58 |
@dataclass(frozen=True)
|
| 59 |
class MiniCPMCardClient:
|
| 60 |
+
chat: ChatCompleter
|
| 61 |
|
| 62 |
# Return one raw card payload from a local MiniCPM endpoint.
|
| 63 |
def create_card(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
|
| 74 |
|
| 75 |
@dataclass(frozen=True)
|
| 76 |
class LlamaCppCardClient:
|
| 77 |
+
chat: ChatCompleter
|
| 78 |
|
| 79 |
# Return one raw card payload from a llama.cpp MiniCPM endpoint.
|
| 80 |
def create_card(self, payload: dict[str, Any]) -> dict[str, Any]:
|
|
|
|
| 399 |
|
| 400 |
|
| 401 |
# Generate one llama.cpp card with one corrective retry; quick mode takes the first card.
|
| 402 |
+
def generate_llamacpp_card(chat: ChatCompleter, payload: dict[str, Any], pack_cards: Sequence[dict[str, Any]], focus: str = "") -> dict[str, Any]:
|
| 403 |
prompt = llamacpp_card_prompt(payload, pack_cards, focus)
|
| 404 |
card = parse_llamacpp_card_text(chat.complete(card_system_prompt(), prompt), payload, pack_cards, discard_bad_text=False)
|
| 405 |
need = llama_candidate_need(payload, pack_summary(pack_cards))
|
|
|
|
| 855 |
# Extract the first JSON object from model text.
|
| 856 |
def extract_json_object(text: str) -> str:
|
| 857 |
start = text.find("{")
|
| 858 |
+
if start == -1:
|
|
|
|
| 859 |
raise ValueError("Codex response did not contain a JSON object")
|
| 860 |
+
end = json_object_end(text, start)
|
| 861 |
+
candidate = balance_json_closers(text[start:end])
|
| 862 |
json.loads(candidate)
|
| 863 |
return candidate
|
| 864 |
|
| 865 |
|
| 866 |
+
# Return the end offset of the first balanced JSON object.
|
| 867 |
+
def json_object_end(text: str, start: int) -> int:
|
| 868 |
+
stack: list[str] = []
|
| 869 |
+
in_string = False
|
| 870 |
+
escaped = False
|
| 871 |
+
for index, char in enumerate(text[start:], start):
|
| 872 |
+
if escaped:
|
| 873 |
+
escaped = False
|
| 874 |
+
continue
|
| 875 |
+
if char == "\\" and in_string:
|
| 876 |
+
escaped = True
|
| 877 |
+
continue
|
| 878 |
+
if char == "\"":
|
| 879 |
+
in_string = not in_string
|
| 880 |
+
continue
|
| 881 |
+
if in_string:
|
| 882 |
+
continue
|
| 883 |
+
if char == "{":
|
| 884 |
+
stack.append("}")
|
| 885 |
+
elif char == "[":
|
| 886 |
+
stack.append("]")
|
| 887 |
+
elif stack and char == stack[-1]:
|
| 888 |
+
stack.pop()
|
| 889 |
+
if not stack:
|
| 890 |
+
return index + 1
|
| 891 |
+
return len(text)
|
| 892 |
+
|
| 893 |
+
|
| 894 |
# Append missing JSON object or array closers.
|
| 895 |
def balance_json_closers(text: str) -> str:
|
| 896 |
stack: list[str] = []
|
local_llm.py
CHANGED
|
@@ -51,6 +51,9 @@ class LocalJsonChatClient:
|
|
| 51 |
return parse_chat_response(json.loads(response.read().decode("utf-8")))
|
| 52 |
|
| 53 |
|
|
|
|
|
|
|
|
|
|
| 54 |
@dataclass(frozen=True)
|
| 55 |
class LocalCompletionClient:
|
| 56 |
endpoint: str
|
|
|
|
| 51 |
return parse_chat_response(json.loads(response.read().decode("utf-8")))
|
| 52 |
|
| 53 |
|
| 54 |
+
ChatCompleter = LocalChatClient | LocalJsonChatClient
|
| 55 |
+
|
| 56 |
+
|
| 57 |
@dataclass(frozen=True)
|
| 58 |
class LocalCompletionClient:
|
| 59 |
endpoint: str
|
tests/test_ai_runtime.py
CHANGED
|
@@ -24,7 +24,7 @@ def test_local_ai_env(monkeypatch) -> None:
|
|
| 24 |
env = local_ai_env(9001)
|
| 25 |
assert env["TABRAS_CARD_BACKEND"] == "llamacpp"
|
| 26 |
assert env["TABRAS_CARD_ENDPOINT"] == "http://127.0.0.1:9001/v1/chat/completions"
|
| 27 |
-
assert env["TABRAS_CARD_MAX_TOKENS"] == "
|
| 28 |
assert env["TABRAS_CARD_TEMPERATURE"] == "0.7"
|
| 29 |
assert env["TABRAS_AI_BOSS"] == "1"
|
| 30 |
assert env["TABRAS_BOSS_BACKEND"] == "mlx"
|
|
|
|
| 24 |
env = local_ai_env(9001)
|
| 25 |
assert env["TABRAS_CARD_BACKEND"] == "llamacpp"
|
| 26 |
assert env["TABRAS_CARD_ENDPOINT"] == "http://127.0.0.1:9001/v1/chat/completions"
|
| 27 |
+
assert env["TABRAS_CARD_MAX_TOKENS"] == "256"
|
| 28 |
assert env["TABRAS_CARD_TEMPERATURE"] == "0.7"
|
| 29 |
assert env["TABRAS_AI_BOSS"] == "1"
|
| 30 |
assert env["TABRAS_BOSS_BACKEND"] == "mlx"
|
tests/test_clients.py
CHANGED
|
@@ -2,7 +2,7 @@ from clients import art_client_from_env, boss_client_from_env, card_client_from_
|
|
| 2 |
from art import DiffusersImageClient, LazyArtClient
|
| 3 |
from boss import NemotronBossClient
|
| 4 |
from generator import LlamaCppCardClient, MiniCPMCardClient
|
| 5 |
-
from local_llm import
|
| 6 |
|
| 7 |
|
| 8 |
class FakeChat:
|
|
@@ -50,11 +50,10 @@ def test_card_client_from_env_llamacpp(monkeypatch) -> None:
|
|
| 50 |
monkeypatch.setenv("TABRAS_CARD_MODEL", "mini-q4")
|
| 51 |
client = card_client_from_env()
|
| 52 |
assert isinstance(client, LlamaCppCardClient)
|
| 53 |
-
assert isinstance(client.chat,
|
| 54 |
assert client.chat.endpoint == "http://cards"
|
| 55 |
assert client.chat.model == "mini-q4"
|
| 56 |
assert client.chat.max_tokens == 320
|
| 57 |
-
assert client.chat.enable_thinking is False
|
| 58 |
|
| 59 |
|
| 60 |
# Verify boss client is disabled unless enabled.
|
|
|
|
| 2 |
from art import DiffusersImageClient, LazyArtClient
|
| 3 |
from boss import NemotronBossClient
|
| 4 |
from generator import LlamaCppCardClient, MiniCPMCardClient
|
| 5 |
+
from local_llm import LocalCompletionClient, LocalJsonChatClient, nemotron_prompt
|
| 6 |
|
| 7 |
|
| 8 |
class FakeChat:
|
|
|
|
| 50 |
monkeypatch.setenv("TABRAS_CARD_MODEL", "mini-q4")
|
| 51 |
client = card_client_from_env()
|
| 52 |
assert isinstance(client, LlamaCppCardClient)
|
| 53 |
+
assert isinstance(client.chat, LocalJsonChatClient)
|
| 54 |
assert client.chat.endpoint == "http://cards"
|
| 55 |
assert client.chat.model == "mini-q4"
|
| 56 |
assert client.chat.max_tokens == 320
|
|
|
|
| 57 |
|
| 58 |
|
| 59 |
# Verify boss client is disabled unless enabled.
|
tests/test_generator.py
CHANGED
|
@@ -399,6 +399,12 @@ def test_extract_json_object() -> None:
|
|
| 399 |
extract_json_object("no json")
|
| 400 |
|
| 401 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 402 |
# Verify JSON extraction repairs missing closing delimiters.
|
| 403 |
def test_extract_json_object_repairs_missing_closer() -> None:
|
| 404 |
assert extract_json_object('{"cards": [{"name": "A"}]') == '{"cards": [{"name": "A"}]}'
|
|
|
|
| 399 |
extract_json_object("no json")
|
| 400 |
|
| 401 |
|
| 402 |
+
# Verify JSON extraction ignores later objects in chatty model output.
|
| 403 |
+
def test_extract_json_object_uses_first_balanced_object() -> None:
|
| 404 |
+
text = 'first {"name": "A", "effects": []} then {"name": "B"}'
|
| 405 |
+
assert extract_json_object(text) == '{"name": "A", "effects": []}'
|
| 406 |
+
|
| 407 |
+
|
| 408 |
# Verify JSON extraction repairs missing closing delimiters.
|
| 409 |
def test_extract_json_object_repairs_missing_closer() -> None:
|
| 410 |
assert extract_json_object('{"cards": [{"name": "A"}]') == '{"cards": [{"name": "A"}]}'
|
tests/test_ui.py
CHANGED
|
@@ -15,6 +15,7 @@ from ui import (
|
|
| 15 |
fallback_pack,
|
| 16 |
finish_opening_draft,
|
| 17 |
log_html,
|
|
|
|
| 18 |
mana_html,
|
| 19 |
new_run,
|
| 20 |
new_run_shell,
|
|
@@ -22,6 +23,7 @@ from ui import (
|
|
| 22 |
pass_turn_steps,
|
| 23 |
pending_tokens_html,
|
| 24 |
play_hand_card,
|
|
|
|
| 25 |
queue_next_pack,
|
| 26 |
refresh_art,
|
| 27 |
)
|
|
@@ -57,6 +59,15 @@ def test_fire_fallback_pack_rotates_names() -> None:
|
|
| 57 |
assert all("card" not in card.art_prompt.lower() for card in first + second)
|
| 58 |
|
| 59 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
# Verify a new run starts in draft state with three clickable cards.
|
| 61 |
def test_new_run_starts_draft() -> None:
|
| 62 |
state = new_run("Ada", "anime", "ice", seed=1)
|
|
@@ -446,6 +457,12 @@ class ForgePackClient:
|
|
| 446 |
return {"cards": [card(i) for i in range(payload["pack_size"])]}
|
| 447 |
|
| 448 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 449 |
class FakeArtClient:
|
| 450 |
# Return a stable URI for each generated art prompt.
|
| 451 |
def create_art(self, prompt: str) -> str:
|
|
@@ -464,18 +481,27 @@ class GatedArtClient:
|
|
| 464 |
|
| 465 |
|
| 466 |
# Verify a new run pre-forges candidate packs and the boss deck in the background.
|
| 467 |
-
def test_new_run_warms_forge() -> None:
|
|
|
|
| 468 |
forge.reset()
|
| 469 |
client = ForgePackClient()
|
| 470 |
state = new_run("Ada", "anime", "ice", client, seed=3)
|
| 471 |
forge.drain()
|
| 472 |
-
# pack 1 +
|
| 473 |
# so it no longer competes with the first visible pack.
|
| 474 |
-
assert client.calls ==
|
| 475 |
picked = choose_draft_card(state, 0, client)
|
| 476 |
assert all(card.name.startswith("Forged") for card in picked.current_pack)
|
| 477 |
|
| 478 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 479 |
# Verify battle start consumes the pre-forged boss deck.
|
| 480 |
def test_battle_uses_forged_boss_deck() -> None:
|
| 481 |
forge.reset()
|
|
|
|
| 15 |
fallback_pack,
|
| 16 |
finish_opening_draft,
|
| 17 |
log_html,
|
| 18 |
+
make_pack,
|
| 19 |
mana_html,
|
| 20 |
new_run,
|
| 21 |
new_run_shell,
|
|
|
|
| 23 |
pass_turn_steps,
|
| 24 |
pending_tokens_html,
|
| 25 |
play_hand_card,
|
| 26 |
+
prefetch_pack_limit,
|
| 27 |
queue_next_pack,
|
| 28 |
refresh_art,
|
| 29 |
)
|
|
|
|
| 59 |
assert all("card" not in card.art_prompt.lower() for card in first + second)
|
| 60 |
|
| 61 |
|
| 62 |
+
# Verify model pack failures are visible in server logs before fallback.
|
| 63 |
+
def test_make_pack_logs_model_failure(capsys) -> None:
|
| 64 |
+
pack = make_pack(BrokenPackClient(), None, "fire", "dark fantasy", (), 1)
|
| 65 |
+
captured = capsys.readouterr()
|
| 66 |
+
assert len(pack) == CARD_PANEL_COUNT
|
| 67 |
+
assert "Tabras model pack failed" in captured.err
|
| 68 |
+
assert "RuntimeError: bad json" in captured.err
|
| 69 |
+
|
| 70 |
+
|
| 71 |
# Verify a new run starts in draft state with three clickable cards.
|
| 72 |
def test_new_run_starts_draft() -> None:
|
| 73 |
state = new_run("Ada", "anime", "ice", seed=1)
|
|
|
|
| 457 |
return {"cards": [card(i) for i in range(payload["pack_size"])]}
|
| 458 |
|
| 459 |
|
| 460 |
+
class BrokenPackClient:
|
| 461 |
+
# Raise like a model response that cannot be parsed or costed.
|
| 462 |
+
def create_pack(self, payload):
|
| 463 |
+
raise RuntimeError("bad json")
|
| 464 |
+
|
| 465 |
+
|
| 466 |
class FakeArtClient:
|
| 467 |
# Return a stable URI for each generated art prompt.
|
| 468 |
def create_art(self, prompt: str) -> str:
|
|
|
|
| 481 |
|
| 482 |
|
| 483 |
# Verify a new run pre-forges candidate packs and the boss deck in the background.
|
| 484 |
+
def test_new_run_warms_forge(monkeypatch) -> None:
|
| 485 |
+
monkeypatch.delenv("TABRAS_PREFETCH_PACKS", raising=False)
|
| 486 |
forge.reset()
|
| 487 |
client = ForgePackClient()
|
| 488 |
state = new_run("Ada", "anime", "ice", client, seed=3)
|
| 489 |
forge.drain()
|
| 490 |
+
# pack 1 + one speculative next pack; the boss deck is deferred (draft_step >= 2)
|
| 491 |
# so it no longer competes with the first visible pack.
|
| 492 |
+
assert client.calls == 2
|
| 493 |
picked = choose_draft_card(state, 0, client)
|
| 494 |
assert all(card.name.startswith("Forged") for card in picked.current_pack)
|
| 495 |
|
| 496 |
|
| 497 |
+
# Verify the speculative draft fanout can be tuned without crashing.
|
| 498 |
+
def test_prefetch_pack_limit_env(monkeypatch) -> None:
|
| 499 |
+
monkeypatch.setenv("TABRAS_PREFETCH_PACKS", "0")
|
| 500 |
+
assert prefetch_pack_limit() == 0
|
| 501 |
+
monkeypatch.setenv("TABRAS_PREFETCH_PACKS", "bad")
|
| 502 |
+
assert prefetch_pack_limit() == 1
|
| 503 |
+
|
| 504 |
+
|
| 505 |
# Verify battle start consumes the pre-forged boss deck.
|
| 506 |
def test_battle_uses_forged_boss_deck() -> None:
|
| 507 |
forge.reset()
|
ui.py
CHANGED
|
@@ -1,6 +1,8 @@
|
|
| 1 |
from collections.abc import Callable, Iterator
|
| 2 |
from dataclasses import dataclass, replace
|
|
|
|
| 3 |
from random import Random
|
|
|
|
| 4 |
from typing import Sequence
|
| 5 |
|
| 6 |
import forge
|
|
@@ -281,11 +283,11 @@ def pack_maker(
|
|
| 281 |
return lambda: make_pack(client, None, state.school, state.world, state.player_deck, cost, state.draft_anchors)
|
| 282 |
|
| 283 |
|
| 284 |
-
# Pre-generate
|
| 285 |
def prefetch_next_packs(state: RunState, client: CardPackClient | None, art_client: ArtClient | None) -> None:
|
| 286 |
if client is None or state.draft_step >= len(state.draft_order):
|
| 287 |
return
|
| 288 |
-
for index in range(len(state.current_pack)):
|
| 289 |
nxt = apply_pick(state, index)
|
| 290 |
if nxt.draft_step >= len(nxt.draft_order):
|
| 291 |
continue
|
|
@@ -293,6 +295,14 @@ def prefetch_next_packs(state: RunState, client: CardPackClient | None, art_clie
|
|
| 293 |
forge.submit(pack_key(nxt), pack_maker(client, art_client, nxt, cost))
|
| 294 |
|
| 295 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 296 |
# Return the forge key for the boss deck of one run.
|
| 297 |
def enemy_deck_key(state: RunState) -> tuple:
|
| 298 |
return ("enemy", state.enemy_school, state.world, state.enemy_seed)
|
|
@@ -444,8 +454,8 @@ def make_pack(
|
|
| 444 |
try:
|
| 445 |
pack = generate_pack(client, school, world, current_deck, cost, draft_anchors=anchors, quick=quick)
|
| 446 |
return collect_ready_cards(pack)
|
| 447 |
-
except Exception:
|
| 448 |
-
|
| 449 |
pack = fallback_pack(school, world, cost, anchors, current_deck)
|
| 450 |
return collect_ready_cards(pack)
|
| 451 |
|
|
|
|
| 1 |
from collections.abc import Callable, Iterator
|
| 2 |
from dataclasses import dataclass, replace
|
| 3 |
+
import os
|
| 4 |
from random import Random
|
| 5 |
+
import sys
|
| 6 |
from typing import Sequence
|
| 7 |
|
| 8 |
import forge
|
|
|
|
| 283 |
return lambda: make_pack(client, None, state.school, state.world, state.player_deck, cost, state.draft_anchors)
|
| 284 |
|
| 285 |
|
| 286 |
+
# Pre-generate a bounded number of next-pack branches while the player reads.
|
| 287 |
def prefetch_next_packs(state: RunState, client: CardPackClient | None, art_client: ArtClient | None) -> None:
|
| 288 |
if client is None or state.draft_step >= len(state.draft_order):
|
| 289 |
return
|
| 290 |
+
for index in range(min(len(state.current_pack), prefetch_pack_limit())):
|
| 291 |
nxt = apply_pick(state, index)
|
| 292 |
if nxt.draft_step >= len(nxt.draft_order):
|
| 293 |
continue
|
|
|
|
| 295 |
forge.submit(pack_key(nxt), pack_maker(client, art_client, nxt, cost))
|
| 296 |
|
| 297 |
|
| 298 |
+
# Return how many possible next draft branches to forge speculatively.
|
| 299 |
+
def prefetch_pack_limit() -> int:
|
| 300 |
+
try:
|
| 301 |
+
return max(0, int(os.environ.get("TABRAS_PREFETCH_PACKS", "1")))
|
| 302 |
+
except ValueError:
|
| 303 |
+
return 1
|
| 304 |
+
|
| 305 |
+
|
| 306 |
# Return the forge key for the boss deck of one run.
|
| 307 |
def enemy_deck_key(state: RunState) -> tuple:
|
| 308 |
return ("enemy", state.enemy_school, state.world, state.enemy_seed)
|
|
|
|
| 454 |
try:
|
| 455 |
pack = generate_pack(client, school, world, current_deck, cost, draft_anchors=anchors, quick=quick)
|
| 456 |
return collect_ready_cards(pack)
|
| 457 |
+
except Exception as exc:
|
| 458 |
+
print(f"Tabras model pack failed; using fallback: {type(exc).__name__}: {exc}", file=sys.stderr, flush=True)
|
| 459 |
pack = fallback_pack(school, world, cost, anchors, current_deck)
|
| 460 |
return collect_ready_cards(pack)
|
| 461 |
|