Spaces:
Running
Running
Deploy a9d45ca
Browse files- server/deckbuilder.py +9 -0
- web/src/api.ts +7 -0
- web/src/components/DeckView.tsx +13 -3
- web/src/index.css +9 -0
server/deckbuilder.py
CHANGED
|
@@ -74,6 +74,14 @@ class DeckCard:
|
|
| 74 |
category: str
|
| 75 |
oracle_text: str
|
| 76 |
locked: bool = False # kept from a previous completion at the user's request
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
# Per-face power/toughness, loyalty, or defense ("4/3", "Loyalty 3", None), see server/card_stats.py.
|
| 78 |
stats: list[str | None] = field(default_factory=list)
|
| 79 |
# Cheapest paper printing: {"usd": 0.25, "set": "M21", "finish": "foil"}, see server/card_prices.py.
|
|
@@ -291,6 +299,7 @@ class DeckBuilder:
|
|
| 291 |
type_line=card.type_line, mana_cost=card.mana_cost, cmc=card.cmc,
|
| 292 |
category=category_of(card.type_line), oracle_text=card.oracle_text,
|
| 293 |
stats=self.card_stats.get(card.oracle_id, []), price=self.prices.get(card.oracle_id),
|
|
|
|
| 294 |
)
|
| 295 |
|
| 296 |
def _basic_card(self, name: str, quantity: int, source: str) -> DeckCard:
|
|
|
|
| 74 |
category: str
|
| 75 |
oracle_text: str
|
| 76 |
locked: bool = False # kept from a previous completion at the user's request
|
| 77 |
+
# Real-deck-frequency classification ("staple" | "mid" | "long_tail"),
|
| 78 |
+
# None for basics (outside the tokenizer vocab, so untiered). Distinct
|
| 79 |
+
# from `source`: the synergy phase runs unrestricted and routinely
|
| 80 |
+
# picks real staples (Sol Ring, Command Tower) on their own merit, so
|
| 81 |
+
# a card can be source="synergy" and tier="staple" at once -- without
|
| 82 |
+
# this field the frontend had no way to show that, and a staple could
|
| 83 |
+
# read as "just a synergy pick" with nothing marking it as universal.
|
| 84 |
+
tier: str | None = None
|
| 85 |
# Per-face power/toughness, loyalty, or defense ("4/3", "Loyalty 3", None), see server/card_stats.py.
|
| 86 |
stats: list[str | None] = field(default_factory=list)
|
| 87 |
# Cheapest paper printing: {"usd": 0.25, "set": "M21", "finish": "foil"}, see server/card_prices.py.
|
|
|
|
| 299 |
type_line=card.type_line, mana_cost=card.mana_cost, cmc=card.cmc,
|
| 300 |
category=category_of(card.type_line), oracle_text=card.oracle_text,
|
| 301 |
stats=self.card_stats.get(card.oracle_id, []), price=self.prices.get(card.oracle_id),
|
| 302 |
+
tier=self.tiers.get(self.tok.token_to_id[self.tok.oracle_id_to_token[card.oracle_id]]),
|
| 303 |
)
|
| 304 |
|
| 305 |
def _basic_card(self, name: str, quantity: int, source: str) -> DeckCard:
|
web/src/api.ts
CHANGED
|
@@ -2,10 +2,17 @@
|
|
| 2 |
|
| 3 |
export type CardSource = 'input' | 'synergy' | 'staple' | 'basic'
|
| 4 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
export interface DeckCard {
|
| 6 |
name: string
|
| 7 |
quantity: number
|
| 8 |
source: CardSource
|
|
|
|
| 9 |
score: number | null
|
| 10 |
type_line: string
|
| 11 |
mana_cost: string
|
|
|
|
| 2 |
|
| 3 |
export type CardSource = 'input' | 'synergy' | 'staple' | 'basic'
|
| 4 |
|
| 5 |
+
/** Real-deck-frequency classification, independent of `source` -- the
|
| 6 |
+
* synergy phase is unrestricted and routinely picks real staples on their
|
| 7 |
+
* own merit, so a card can be source: 'synergy' and tier: 'staple' at
|
| 8 |
+
* once. null for basics (outside the tokenizer vocab, so untiered). */
|
| 9 |
+
export type CardTier = 'staple' | 'mid' | 'long_tail'
|
| 10 |
+
|
| 11 |
export interface DeckCard {
|
| 12 |
name: string
|
| 13 |
quantity: number
|
| 14 |
source: CardSource
|
| 15 |
+
tier: CardTier | null
|
| 16 |
score: number | null
|
| 17 |
type_line: string
|
| 18 |
mana_cost: string
|
web/src/components/DeckView.tsx
CHANGED
|
@@ -132,9 +132,18 @@ function CardRow({
|
|
| 132 |
const adjustable = !isCommander && isAdjustable(card)
|
| 133 |
const isBanned = adjustable && banned.has(card.name)
|
| 134 |
const locked = adjustable && !isBanned && lockedNames.has(card.name)
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
const classes = [
|
| 139 |
'card-row',
|
| 140 |
`source-${isCommander ? 'commander' : card.source}`,
|
|
@@ -171,6 +180,7 @@ function CardRow({
|
|
| 171 |
</span>
|
| 172 |
)}
|
| 173 |
{isCommander && <span className="tag">Commander</span>}
|
|
|
|
| 174 |
<ManaCost cost={card.mana_cost} />
|
| 175 |
{/* Fixed-width, right-aligned last column so prices line up down each
|
| 176 |
group; an empty cell keeps the column when a card has no price. */}
|
|
|
|
| 132 |
const adjustable = !isCommander && isAdjustable(card)
|
| 133 |
const isBanned = adjustable && banned.has(card.name)
|
| 134 |
const locked = adjustable && !isBanned && lockedNames.has(card.name)
|
| 135 |
+
// A card picked by the synergy phase can still be a real staple by
|
| 136 |
+
// real-deck frequency -- the synergy phase is unrestricted and often
|
| 137 |
+
// picks staples (Sol Ring, Command Tower) on their own merit. Call that
|
| 138 |
+
// out specifically: it's the one case where `source` alone would read
|
| 139 |
+
// as "just a synergy pick" when the card is actually universal.
|
| 140 |
+
const isUnlabeledStaple = card.source === 'synergy' && card.tier === 'staple'
|
| 141 |
+
const sourceLabel = card.source === 'input' || isCommander ? SOURCE_LABELS.input : SOURCE_LABELS[card.source]
|
| 142 |
+
const title = [
|
| 143 |
+
sourceLabel,
|
| 144 |
+
isUnlabeledStaple && 'also a staple',
|
| 145 |
+
!isCommander && card.score != null && `model score ${card.score.toFixed(3)}`,
|
| 146 |
+
].filter(Boolean).join(' · ')
|
| 147 |
const classes = [
|
| 148 |
'card-row',
|
| 149 |
`source-${isCommander ? 'commander' : card.source}`,
|
|
|
|
| 180 |
</span>
|
| 181 |
)}
|
| 182 |
{isCommander && <span className="tag">Commander</span>}
|
| 183 |
+
{isUnlabeledStaple && <span className="tag tag-staple" title="Also a staple by real-deck frequency">Staple</span>}
|
| 184 |
<ManaCost cost={card.mana_cost} />
|
| 185 |
{/* Fixed-width, right-aligned last column so prices line up down each
|
| 186 |
group; an empty cell keeps the column when a card has no price. */}
|
web/src/index.css
CHANGED
|
@@ -740,6 +740,15 @@ textarea {
|
|
| 740 |
color: var(--warn-text);
|
| 741 |
}
|
| 742 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 743 |
/* ---------- mana symbols ---------- */
|
| 744 |
|
| 745 |
.mana-cost {
|
|
|
|
| 740 |
color: var(--warn-text);
|
| 741 |
}
|
| 742 |
|
| 743 |
+
/* Outlined, not filled like .tag/.tag-warn -- this flags a secondary fact
|
| 744 |
+
about the card (it's also a staple) alongside its primary source dot,
|
| 745 |
+
not a replacement for it. */
|
| 746 |
+
.tag-staple {
|
| 747 |
+
background: transparent;
|
| 748 |
+
border: 1px solid var(--src-staple);
|
| 749 |
+
color: var(--src-staple);
|
| 750 |
+
}
|
| 751 |
+
|
| 752 |
/* ---------- mana symbols ---------- */
|
| 753 |
|
| 754 |
.mana-cost {
|