Spaces:
Sleeping
Sleeping
Codex Claude Opus 4.8 commited on
Commit ·
a33d5b8
1
Parent(s): 372c910
Pre-bake backbone card art (instant, no shimmer) + stop warming face-down enemy hand
Browse files- 18 bundled JPEGs (6 backbone cards x 3 worlds); backbone_card is born with its
art_uri, so the base Attack/Block/Draw cards never shimmer and cost no live SDXL.
- Skip art generation for the enemy hand (rendered as card backs), freeing the art
lane so visible player cards illustrate faster.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- README.md +26 -6
- assets/cards/anime__attack_medium.jpg +3 -0
- assets/cards/anime__attack_strong.jpg +3 -0
- assets/cards/anime__attack_weak.jpg +3 -0
- assets/cards/anime__block_medium.jpg +3 -0
- assets/cards/anime__block_weak.jpg +3 -0
- assets/cards/anime__draw_small.jpg +3 -0
- assets/cards/cyberpunk__attack_medium.jpg +3 -0
- assets/cards/cyberpunk__attack_strong.jpg +3 -0
- assets/cards/cyberpunk__attack_weak.jpg +3 -0
- assets/cards/cyberpunk__block_medium.jpg +3 -0
- assets/cards/cyberpunk__block_weak.jpg +3 -0
- assets/cards/cyberpunk__draw_small.jpg +3 -0
- assets/cards/darkfantasy__attack_medium.jpg +3 -0
- assets/cards/darkfantasy__attack_strong.jpg +3 -0
- assets/cards/darkfantasy__attack_weak.jpg +3 -0
- assets/cards/darkfantasy__block_medium.jpg +3 -0
- assets/cards/darkfantasy__block_weak.jpg +3 -0
- assets/cards/darkfantasy__draw_small.jpg +3 -0
- draft.py +23 -2
- tests/test_ui.py +2 -2
- ui.py +3 -2
README.md
CHANGED
|
@@ -102,25 +102,43 @@ Install dependencies:
|
|
| 102 |
python3 -m pip install -r requirements.txt
|
| 103 |
```
|
| 104 |
|
| 105 |
-
Run the Gradio app without
|
| 106 |
|
| 107 |
```bash
|
| 108 |
python3 app.py
|
| 109 |
```
|
| 110 |
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
```bash
|
| 114 |
python3 launch_ai.py
|
| 115 |
```
|
| 116 |
|
| 117 |
-
`launch_ai.py`
|
| 118 |
|
| 119 |
-
|
| 120 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 121 |
```
|
| 122 |
|
| 123 |
-
|
| 124 |
|
| 125 |
```bash
|
| 126 |
TABRAS_CARD_BACKEND=transformers
|
|
@@ -133,6 +151,8 @@ TABRAS_ART_MODEL=stabilityai/sdxl-turbo
|
|
| 133 |
python3 app_hf.py
|
| 134 |
```
|
| 135 |
|
|
|
|
|
|
|
| 136 |
## Project Structure
|
| 137 |
|
| 138 |
| File | Purpose |
|
|
|
|
| 102 |
python3 -m pip install -r requirements.txt
|
| 103 |
```
|
| 104 |
|
| 105 |
+
Run the Gradio app without model servers:
|
| 106 |
|
| 107 |
```bash
|
| 108 |
python3 app.py
|
| 109 |
```
|
| 110 |
|
| 111 |
+
That path is fully local and playable: it uses the same deterministic engine and fallback card generation, with no model server required.
|
| 112 |
+
|
| 113 |
+
For local AI card generation, install `llama-server` from llama.cpp, download the MiniCPM GGUF, and place it here:
|
| 114 |
+
|
| 115 |
+
```text
|
| 116 |
+
models/minicpm-v-4.6-gguf/MiniCPM-V-4.6-Q4_K_M.gguf
|
| 117 |
+
```
|
| 118 |
+
|
| 119 |
+
Then start Tabras through:
|
| 120 |
|
| 121 |
```bash
|
| 122 |
python3 launch_ai.py
|
| 123 |
```
|
| 124 |
|
| 125 |
+
`launch_ai.py` starts `llama-server` on `127.0.0.1:8090`, points Tabras at that OpenAI-compatible endpoint, uses local MLX for the Nemotron boss by default, and uses local Diffusers for SDXL-Turbo art.
|
| 126 |
|
| 127 |
+
You can also point any model role at a GPU machine you control. Set the endpoint/model environment variables before launching the app:
|
| 128 |
+
|
| 129 |
+
```bash
|
| 130 |
+
TABRAS_CARD_BACKEND=llamacpp
|
| 131 |
+
TABRAS_CARD_ENDPOINT=http://YOUR_GPU_HOST:8090/v1/chat/completions
|
| 132 |
+
TABRAS_CARD_MODEL=minicpm-v-4.6-q4
|
| 133 |
+
TABRAS_AI_BOSS=1
|
| 134 |
+
TABRAS_BOSS_BACKEND=openai
|
| 135 |
+
TABRAS_BOSS_ENDPOINT=http://YOUR_GPU_HOST:8081/v1/chat/completions
|
| 136 |
+
TABRAS_ART_BACKEND=modal
|
| 137 |
+
TABRAS_ART_ENDPOINT=http://YOUR_GPU_HOST:8082/generate
|
| 138 |
+
python3 app.py
|
| 139 |
```
|
| 140 |
|
| 141 |
+
For in-process local Transformers/Diffusers instead of OpenAI-compatible endpoints:
|
| 142 |
|
| 143 |
```bash
|
| 144 |
TABRAS_CARD_BACKEND=transformers
|
|
|
|
| 151 |
python3 app_hf.py
|
| 152 |
```
|
| 153 |
|
| 154 |
+
The submitted Space uses Modal GPU endpoints, but the same app can run with local CPU fallback, local model processes, in-process local models, or GPU endpoints that you configure.
|
| 155 |
+
|
| 156 |
## Project Structure
|
| 157 |
|
| 158 |
| File | Purpose |
|
assets/cards/anime__attack_medium.jpg
ADDED
|
Git LFS Details
|
assets/cards/anime__attack_strong.jpg
ADDED
|
Git LFS Details
|
assets/cards/anime__attack_weak.jpg
ADDED
|
Git LFS Details
|
assets/cards/anime__block_medium.jpg
ADDED
|
Git LFS Details
|
assets/cards/anime__block_weak.jpg
ADDED
|
Git LFS Details
|
assets/cards/anime__draw_small.jpg
ADDED
|
Git LFS Details
|
assets/cards/cyberpunk__attack_medium.jpg
ADDED
|
Git LFS Details
|
assets/cards/cyberpunk__attack_strong.jpg
ADDED
|
Git LFS Details
|
assets/cards/cyberpunk__attack_weak.jpg
ADDED
|
Git LFS Details
|
assets/cards/cyberpunk__block_medium.jpg
ADDED
|
Git LFS Details
|
assets/cards/cyberpunk__block_weak.jpg
ADDED
|
Git LFS Details
|
assets/cards/cyberpunk__draw_small.jpg
ADDED
|
Git LFS Details
|
assets/cards/darkfantasy__attack_medium.jpg
ADDED
|
Git LFS Details
|
assets/cards/darkfantasy__attack_strong.jpg
ADDED
|
Git LFS Details
|
assets/cards/darkfantasy__attack_weak.jpg
ADDED
|
Git LFS Details
|
assets/cards/darkfantasy__block_medium.jpg
ADDED
|
Git LFS Details
|
assets/cards/darkfantasy__block_weak.jpg
ADDED
|
Git LFS Details
|
assets/cards/darkfantasy__draw_small.jpg
ADDED
|
Git LFS Details
|
draft.py
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from random import Random
|
| 2 |
from typing import Callable
|
| 3 |
|
|
@@ -8,6 +11,24 @@ from primitives import School
|
|
| 8 |
|
| 9 |
PackChooser = Callable[[tuple[Card, ...], tuple[Card, ...]], int]
|
| 10 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
BACKBONE_CARDS: tuple[tuple[str, int, Effect], ...] = (
|
| 12 |
("Attack (weak)", 1, Effect("deal", amount=2)),
|
| 13 |
("Attack (medium)", 3, Effect("deal", amount=4)),
|
|
@@ -23,9 +44,9 @@ def backbone_deck(school: School, theme: str) -> tuple[Card, ...]:
|
|
| 23 |
return tuple(backbone_card(name, cost, effect, school, theme) for name, cost, effect in BACKBONE_CARDS)
|
| 24 |
|
| 25 |
|
| 26 |
-
# Build one standardized backbone card.
|
| 27 |
def backbone_card(name: str, cost: int, effect: Effect, school: School, theme: str) -> Card:
|
| 28 |
-
return Card(name, cost, school, theme, (effect,), flavor=f"Standard {name.lower()}.")
|
| 29 |
|
| 30 |
|
| 31 |
# Build a full 15-card text deck with nine generated synergy cards.
|
|
|
|
| 1 |
+
import base64
|
| 2 |
+
import re
|
| 3 |
+
from pathlib import Path
|
| 4 |
from random import Random
|
| 5 |
from typing import Callable
|
| 6 |
|
|
|
|
| 11 |
|
| 12 |
PackChooser = Callable[[tuple[Card, ...], tuple[Card, ...]], int]
|
| 13 |
|
| 14 |
+
_CARD_ART = Path(__file__).parent / "assets" / "cards"
|
| 15 |
+
_backbone_art_cache: dict[str, str] = {}
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
# Return the pre-baked art data URI for a backbone card. The six backbone cards are
|
| 19 |
+
# identical every run, so their art is generated once and bundled (no live SDXL call,
|
| 20 |
+
# no shimmer).
|
| 21 |
+
def prebaked_backbone_art(name: str, theme: str) -> str:
|
| 22 |
+
world = re.sub(r"[^a-z0-9]+", "", theme.lower())
|
| 23 |
+
card = re.sub(r"[^a-z0-9]+", "_", name.lower()).strip("_")
|
| 24 |
+
key = f"{world}__{card}"
|
| 25 |
+
if key not in _backbone_art_cache:
|
| 26 |
+
path = _CARD_ART / f"{key}.jpg"
|
| 27 |
+
_backbone_art_cache[key] = (
|
| 28 |
+
"data:image/jpeg;base64," + base64.b64encode(path.read_bytes()).decode("ascii") if path.exists() else ""
|
| 29 |
+
)
|
| 30 |
+
return _backbone_art_cache[key]
|
| 31 |
+
|
| 32 |
BACKBONE_CARDS: tuple[tuple[str, int, Effect], ...] = (
|
| 33 |
("Attack (weak)", 1, Effect("deal", amount=2)),
|
| 34 |
("Attack (medium)", 3, Effect("deal", amount=4)),
|
|
|
|
| 44 |
return tuple(backbone_card(name, cost, effect, school, theme) for name, cost, effect in BACKBONE_CARDS)
|
| 45 |
|
| 46 |
|
| 47 |
+
# Build one standardized backbone card, with pre-baked art so it never shimmers.
|
| 48 |
def backbone_card(name: str, cost: int, effect: Effect, school: School, theme: str) -> Card:
|
| 49 |
+
return Card(name, cost, school, theme, (effect,), flavor=f"Standard {name.lower()}.", art_uri=prebaked_backbone_art(name, theme))
|
| 50 |
|
| 51 |
|
| 52 |
# Build a full 15-card text deck with nine generated synergy cards.
|
tests/test_ui.py
CHANGED
|
@@ -547,6 +547,6 @@ def test_battle_warms_visible_card_art() -> None:
|
|
| 547 |
forge.drain()
|
| 548 |
state = refresh_art(state)
|
| 549 |
duel = state.duel
|
|
|
|
|
|
|
| 550 |
assert all(card.art_uri for card in duel.player.hand)
|
| 551 |
-
assert all(card.art_uri for card in duel.enemy.hand)
|
| 552 |
-
assert any(not card.art_uri for card in duel.player.deck)
|
|
|
|
| 547 |
forge.drain()
|
| 548 |
state = refresh_art(state)
|
| 549 |
duel = state.duel
|
| 550 |
+
# The player's hand is visible and fully illustrated; the enemy hand renders
|
| 551 |
+
# face-down (card backs), so its art is intentionally not warmed.
|
| 552 |
assert all(card.art_uri for card in duel.player.hand)
|
|
|
|
|
|
ui.py
CHANGED
|
@@ -385,8 +385,9 @@ def battle_opening_steps(
|
|
| 385 |
)
|
| 386 |
draw_cards(duel.player, OPENING_HAND_SIZE)
|
| 387 |
draw_cards(duel.enemy, OPENING_HAND_SIZE)
|
|
|
|
|
|
|
| 388 |
warm_card_art(art_client, duel.player.hand)
|
| 389 |
-
warm_card_art(art_client, duel.enemy.hand)
|
| 390 |
state = replace(
|
| 391 |
state,
|
| 392 |
enemy_deck=enemy_deck,
|
|
@@ -434,8 +435,8 @@ def collect_ready_battle(
|
|
| 434 |
def warm_battle_art(art_client: ArtClient | None, state: RunState) -> None:
|
| 435 |
if state.duel is None:
|
| 436 |
return
|
|
|
|
| 437 |
warm_card_art(art_client, state.duel.player.hand)
|
| 438 |
-
warm_card_art(art_client, state.duel.enemy.hand)
|
| 439 |
warm_card_art(art_client, tuple(card for _, card in state.showcase))
|
| 440 |
|
| 441 |
|
|
|
|
| 385 |
)
|
| 386 |
draw_cards(duel.player, OPENING_HAND_SIZE)
|
| 387 |
draw_cards(duel.enemy, OPENING_HAND_SIZE)
|
| 388 |
+
# Only warm the player's hand: the enemy hand renders face-down (card backs),
|
| 389 |
+
# so generating its art just starves the art lane and the visible cards.
|
| 390 |
warm_card_art(art_client, duel.player.hand)
|
|
|
|
| 391 |
state = replace(
|
| 392 |
state,
|
| 393 |
enemy_deck=enemy_deck,
|
|
|
|
| 435 |
def warm_battle_art(art_client: ArtClient | None, state: RunState) -> None:
|
| 436 |
if state.duel is None:
|
| 437 |
return
|
| 438 |
+
# Player hand + the played-card showcase are visible; the enemy hand is not.
|
| 439 |
warm_card_art(art_client, state.duel.player.hand)
|
|
|
|
| 440 |
warm_card_art(art_client, tuple(card for _, card in state.showcase))
|
| 441 |
|
| 442 |
|