feat: add ideal counts slider UI with smart validation

This commit is contained in:
matt 2025-10-14 16:45:49 -07:00
parent 9ab3835e2a
commit 35bff901d2
12 changed files with 217 additions and 11 deletions

View file

@ -96,6 +96,9 @@ WEB_AUTO_ENFORCE=0 # dockerhub: WEB_AUTO_ENFORCE="0"
# Build Stage Ordering
WEB_STAGE_ORDER=new # new|legacy. 'new' (default): creatures → spells → lands → fill. 'legacy': lands → creatures → spells → fill
# Ideals UI Mode
WEB_IDEALS_UI=slider # input|slider. 'slider' (default): range sliders with live value display. 'input': text input boxes
# Tagging Refinement Feature Flags
TAG_NORMALIZE_KEYWORDS=1 # dockerhub: TAG_NORMALIZE_KEYWORDS="1" # Normalize keywords & filter specialty mechanics
TAG_PROTECTION_GRANTS=1 # dockerhub: TAG_PROTECTION_GRANTS="1" # Protection tag only for cards granting shields

View file

@ -10,11 +10,18 @@ This format follows Keep a Changelog principles and aims for Semantic Versioning
## [Unreleased]
### Summary
- Enhanced deck building workflow with improved stage ordering, granular skip controls, and one-click Quick Build automation.
- New Ideal Counts section with interactive sliders or text inputs for customizing deck composition targets.
- Stage execution order now prioritizes creatures and spells before lands for better mana curve analysis.
- New wizard-only skip controls allow auto-advancing through specific stages (lands, creatures, spells) without approval prompts.
- Quick Build button provides one-click full automation with clean 5-phase progress indicator.
### Added
- **Ideal Counts UI**: Dedicated section in New Deck wizard for setting ideal card counts (ramp, lands, creatures, removal, wipes, card advantage, protection).
- **Slider Mode** (default): Interactive range sliders with live value display and expanded ranges (e.g., creatures: 0-70, lands: 25-45).
- **Input Mode**: Text input boxes with placeholder defaults (e.g., "10 (Default)").
- Smart validation warns when estimated total exceeds 99 cards (accounts for overlap: `Lands + Creatures + Spells/2`).
- Sliders start at recommended defaults and remember user preferences across builds.
- Configurable via `WEB_IDEALS_UI` environment variable (`slider` or `input`).
- **Quick Build**: One-click automation button in New Deck wizard with live progress tracking (5 phases: Creatures, Spells, Lands, Final Touches, Summary).
- **Skip Controls**: Granular stage-skipping toggles in New Deck wizard (21 flags: all land steps, creature stages, spell categories).
- Individual land step controls: basics, staples, fetches, duals, triomes, kindred, misc lands.
@ -24,9 +31,12 @@ This format follows Keep a Changelog principles and aims for Semantic Versioning
- **Stage Reordering**: New default build order executes creatures → spells → lands for improved pip analysis (configurable via `WEB_STAGE_ORDER` environment variable).
- Background task execution for Quick Build with HTMX polling progress updates.
- Mobile-friendly Quick Build with touch device confirmation dialog.
- Commander session cleanup: Commander selection automatically cleared after build completes.
### Changed
- **Default Stage Order**: Creatures and ideal spells now execute before land stages (lands can analyze actual pip requirements instead of estimates).
- **Ideal Counts Display**: Removed collapsible "Advanced options (ideals)" section; replaced with prominent fieldset with slider/input modes.
- Slider ranges expanded to support edge-case strategies (e.g., creature-heavy tribal, spell-heavy control).
- Skip controls only available in New Deck wizard (disabled during build execution for consistency).
- Skip behavior auto-advances through stages without approval prompts (cards still added, just not gated).
- Post-spell land adjustment automatically skipped when any skip flag enabled.
@ -35,6 +45,8 @@ This format follows Keep a Changelog principles and aims for Semantic Versioning
- Session context properly injected into Quick Build so skip configuration works correctly.
- HTMX polling uses continuous trigger (`every 500ms`) instead of one-time (`load delay`) for reliable progress updates.
- Progress indicator stops cleanly when build completes (out-of-band swap removes poller div).
- Ideal counts now properly populate from session state, allowing sliders to start at defaults and remember user preferences.
- Commander and commander_name cleared from session after build completes to prevent carryover to next build.
## [2.6.1] - 2025-10-13
### Summary

View file

@ -255,6 +255,7 @@ See `.env.example` for the full catalog. Common knobs:
| `SHOW_MUST_HAVE_BUTTONS` | `0` | Surface the must include/exclude buttons and quick-add UI (requires `ALLOW_MUST_HAVES=1`). |
| `THEME` | `dark` | Initial UI theme (`system`, `light`, or `dark`). |
| `WEB_STAGE_ORDER` | `new` | Build stage execution order: `new` (creatures→spells→lands) or `legacy` (lands→creatures→spells). |
| `WEB_IDEALS_UI` | `slider` | Ideal counts interface: `slider` (range inputs with live validation) or `input` (text boxes with placeholders). |
### Random build controls

View file

@ -92,6 +92,7 @@ Start here for interactive deck creation.
- `ALLOW_MUST_HAVES=1` (default) enables include/exclude enforcement.
- `WEB_AUTO_ENFORCE=1` re-runs bracket enforcement automatically after each build.
- `WEB_STAGE_ORDER=new` (default) runs creatures/spells before lands for better pip analysis. Use `legacy` for original lands-first order.
- `WEB_IDEALS_UI=slider` (default) shows interactive range sliders for ideal counts with live validation. Use `input` for traditional text boxes.
### Run a JSON Config
Execute saved configs without manual input.

View file

@ -1,10 +1,11 @@
# MTG Python Deckbuilder ${VERSION}
### Summary
- Enhanced deck building workflow with improved stage ordering, granular skip controls, and one-click Quick Build automation.
- Enhanced deck building workflow with improved stage ordering, granular skip controls, one-click Quick Build automation, and interactive Ideal Counts UI.
- Stage execution order now prioritizes creatures and spells before lands for better mana curve analysis.
- New wizard-only skip controls allow auto-advancing through specific stages (lands, creatures, spells) without approval prompts.
- Quick Build button provides one-click full automation with clean 5-phase progress indicator.
- Ideal Counts now feature interactive slider UI with live validation and smart overlap detection.
### Added
- **Quick Build**: One-click automation button in New Deck wizard with live progress tracking (5 phases: Creatures, Spells, Lands, Final Touches, Summary).
@ -14,6 +15,12 @@
- Creature stage controls: all creatures, primary, secondary, fill.
- Mutual exclusivity enforcement: "Skip All Lands" disables individual land toggles; "Skip to Misc Lands" skips early land steps.
- **Stage Reordering**: New default build order executes creatures → spells → lands for improved pip analysis (configurable via `WEB_STAGE_ORDER` environment variable).
- **Ideal Counts UI**: Interactive slider interface with live value display and smart validation (configurable via `WEB_IDEALS_UI` environment variable).
- Slider Mode (default): Range sliders for all ideal counts with expanded ranges (creatures: 0-70, lands: 25-45).
- Input Mode: Traditional text inputs with placeholder defaults showing recommended values.
- Smart Validation: Real-time deck size estimation using overlap-aware calculation (`Lands + Creatures + Spells/2`).
- Visual Warnings: Red alert (>99 cards), orange warning (90-99), no warning (<90).
- Session Persistence: Values persist across builds and initialize at defaults on first wizard load.
- Background task execution for Quick Build with HTMX polling progress updates.
- Mobile-friendly Quick Build with touch device confirmation dialog.

View file

@ -128,6 +128,7 @@ ENABLE_PRESETS = _as_bool(os.getenv("ENABLE_PRESETS"), False)
ALLOW_MUST_HAVES = _as_bool(os.getenv("ALLOW_MUST_HAVES"), True)
SHOW_MUST_HAVE_BUTTONS = _as_bool(os.getenv("SHOW_MUST_HAVE_BUTTONS"), False)
ENABLE_CUSTOM_THEMES = _as_bool(os.getenv("ENABLE_CUSTOM_THEMES"), True)
WEB_IDEALS_UI = os.getenv("WEB_IDEALS_UI", "slider").strip().lower() # 'input' or 'slider'
ENABLE_PARTNER_MECHANICS = _as_bool(os.getenv("ENABLE_PARTNER_MECHANICS"), True)
ENABLE_PARTNER_SUGGESTIONS = _as_bool(os.getenv("ENABLE_PARTNER_SUGGESTIONS"), True)
RANDOM_MODES = _as_bool(os.getenv("RANDOM_MODES"), True) # initial snapshot (legacy)

View file

@ -13,6 +13,7 @@ from ..app import (
_sanitize_theme,
ENABLE_PARTNER_MECHANICS,
ENABLE_PARTNER_SUGGESTIONS,
WEB_IDEALS_UI,
)
from ..services.build_utils import (
step5_base_ctx,
@ -1356,6 +1357,7 @@ async def build_new_modal(request: Request) -> HTMLResponse:
"allow_must_haves": ALLOW_MUST_HAVES, # Add feature flag
"show_must_have_buttons": SHOW_MUST_HAVE_BUTTONS,
"enable_custom_themes": ENABLE_CUSTOM_THEMES,
"ideals_ui_mode": WEB_IDEALS_UI, # 'input' or 'slider'
"form": {
"prefer_combos": bool(sess.get("prefer_combos")),
"combo_count": sess.get("combo_target_count"),
@ -1364,6 +1366,15 @@ async def build_new_modal(request: Request) -> HTMLResponse:
"use_owned_only": bool(sess.get("use_owned_only")),
"prefer_owned": bool(sess.get("prefer_owned")),
"swap_mdfc_basics": bool(sess.get("swap_mdfc_basics")),
# Add ideal values from session (will be None on first load, triggering defaults)
"ramp": sess.get("ideals", {}).get("ramp"),
"lands": sess.get("ideals", {}).get("lands"),
"basic_lands": sess.get("ideals", {}).get("basic_lands"),
"creatures": sess.get("ideals", {}).get("creatures"),
"removal": sess.get("ideals", {}).get("removal"),
"wipes": sess.get("ideals", {}).get("wipes"),
"card_advantage": sess.get("ideals", {}).get("card_advantage"),
"protection": sess.get("ideals", {}).get("protection"),
},
"tag_slot_html": None,
}

View file

@ -678,3 +678,52 @@ img.lqip.loaded { filter: blur(0); opacity: 1; }
0% { background-position: -200% 0; }
100% { background-position: 200% 0; }
}
/* Ideals Slider Styling */
.ideals-slider {
-webkit-appearance: none;
appearance: none;
height: 6px;
background: var(--border);
border-radius: 3px;
outline: none;
}
.ideals-slider::-webkit-slider-thumb {
-webkit-appearance: none;
appearance: none;
width: 18px;
height: 18px;
background: var(--ring);
border-radius: 50%;
cursor: pointer;
transition: all 0.15s ease;
}
.ideals-slider::-webkit-slider-thumb:hover {
transform: scale(1.15);
box-shadow: 0 0 0 4px rgba(96, 165, 250, 0.2);
}
.ideals-slider::-moz-range-thumb {
width: 18px;
height: 18px;
background: var(--ring);
border: none;
border-radius: 50%;
cursor: pointer;
transition: all 0.15s ease;
}
.ideals-slider::-moz-range-thumb:hover {
transform: scale(1.15);
box-shadow: 0 0 0 4px rgba(96, 165, 250, 0.2);
}
.slider-value {
display: inline-block;
padding: 0.25rem 0.5rem;
background: var(--panel);
border: 1px solid var(--border);
border-radius: 4px;
}

View file

@ -0,0 +1,124 @@
<fieldset>
<legend>Ideal Counts</legend>
<div class="muted" style="font-size:12px; margin-bottom:0.75rem;">
Sliders start at recommended defaults. Adjust as needed for your deck strategy.
<span id="ideals-validation-warning" style="color:#f59e0b; display:none; margin-left:0.5rem;"></span>
</div>
<div style="display:grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap:1rem;">
{% set use_sliders = ideals_ui_mode == 'slider' %}
{% set ideals_data = [
('ramp', labels.ramp, defaults.ramp, 0, 30),
('lands', labels.lands, defaults.lands, 25, 45),
('basic_lands', labels.basic_lands, defaults.basic_lands, 0, 40),
('creatures', labels.creatures, defaults.creatures, 0, 70),
('removal', labels.removal, defaults.removal, 0, 30),
('wipes', labels.wipes, defaults.wipes, 0, 15),
('card_advantage', labels.card_advantage, defaults.card_advantage, 0, 30),
('protection', labels.protection, defaults.protection, 0, 20)
] %}
{% for field_name, field_label, default_val, min_val, max_val in ideals_data %}
<div>
<label style="display:block; margin-bottom:0.25rem;">
<span>{{ field_label }}</span>
{% if use_sliders %}
<div style="display:flex; align-items:center; gap:0.5rem; margin-top:0.25rem;">
<input
type="range"
name="{{ field_name }}"
min="{{ min_val }}"
max="{{ max_val }}"
value="{{ form[field_name] if form and form[field_name] is not none else default_val }}"
class="ideals-slider"
data-field="{{ field_name }}"
style="flex:1; cursor:pointer;"
/>
<output
id="{{ field_name }}_value"
class="slider-value"
style="min-width:2.5rem; text-align:center; font-weight:600; color:var(--accent); font-size:14px;"
>
{{ form[field_name] if form and form[field_name] is not none else default_val }}
</output>
</div>
{% else %}
<input
type="number"
name="{{ field_name }}"
min="{{ min_val }}"
max="100"
value="{{ form[field_name] if form and form[field_name] is not none else '' }}"
placeholder="{{ default_val }} (Default)"
style="width:100%; margin-top:0.25rem;"
/>
{% endif %}
</label>
</div>
{% endfor %}
</div>
<script>
// Validate ideal counts to prevent over-allocation
function validateIdealCounts() {
const warning = document.getElementById('ideals-validation-warning');
if (!warning) return;
// Get all ideal values
const getValue = (name) => {
const input = document.querySelector('[name="' + name + '"]');
return input ? parseInt(input.value) || 0 : 0;
};
const lands = getValue('lands');
const creatures = getValue('creatures');
const ramp = getValue('ramp');
const removal = getValue('removal');
const wipes = getValue('wipes');
const cardAdvantage = getValue('card_advantage');
const protection = getValue('protection');
// Calculate estimated total with overlap factor
// lands + creatures + (spells / 2) since spells often overlap with creatures
const spells = ramp + removal + wipes + cardAdvantage + protection;
const estimatedTotal = lands + creatures + Math.ceil(spells / 2);
// Check if estimated total exceeds deck size (99 for EDH)
if (estimatedTotal > 99) {
warning.textContent = '⚠ Estimated total ~' + estimatedTotal + ' cards (max 99). Reduce counts to avoid build issues.';
warning.style.display = 'inline';
warning.style.color = '#ef4444'; // Red for hard conflict
} else if (estimatedTotal > 90) {
warning.textContent = '⚠ Estimated total ~' + estimatedTotal + ' cards. Deck may be tight on slots.';
warning.style.display = 'inline';
warning.style.color = '#f59e0b'; // Orange for warning
} else {
warning.style.display = 'none';
}
}
// Attach validation to all ideal inputs
{% if use_sliders %}
document.querySelectorAll('.ideals-slider').forEach(slider => {
const fieldName = slider.dataset.field;
const output = document.getElementById(fieldName + '_value');
// Update display on input
slider.addEventListener('input', function() {
output.textContent = this.value;
validateIdealCounts();
});
// Ensure initial value is displayed
output.textContent = slider.value;
});
{% else %}
document.querySelectorAll('[name="ramp"], [name="lands"], [name="basic_lands"], [name="creatures"], [name="removal"], [name="wipes"], [name="card_advantage"], [name="protection"]').forEach(input => {
input.addEventListener('input', validateIdealCounts);
input.addEventListener('change', validateIdealCounts);
});
{% endif %}
// Run initial validation
validateIdealCounts();
</script>
</fieldset>

View file

@ -110,6 +110,7 @@
</div>
</div>
</fieldset>
{% include "build/_new_deck_ideals.html" %}
{% if allow_must_haves %}
<fieldset>
<legend>Include/Exclude Cards</legend>
@ -213,16 +214,6 @@
{% endif %}
{% endif %}
{% include "build/_new_deck_skip_controls.html" %}
<details style="margin-top:.5rem;">
<summary>Advanced options (ideals)</summary>
<div style="display:grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap:.5rem; margin-top:.5rem;">
{% for key, label in labels.items() %}
<label>{{ label }}
<input type="number" name="{{ key }}" value="{{ defaults[key] }}" min="0" />
</label>
{% endfor %}
</div>
</details>
<div class="modal-footer" style="display:flex; gap:.5rem; justify-content:space-between; margin-top:1rem;">
<button type="button" class="btn" onclick="this.closest('.modal').remove()">Cancel</button>
<div style="display:flex; gap:.5rem;">

View file

@ -103,6 +103,9 @@ services:
# Build Stage Ordering
WEB_STAGE_ORDER: "new" # new|legacy. 'new' (default): creatures → spells → lands → fill. 'legacy': lands → creatures → spells → fill
# Ideals UI Mode
WEB_IDEALS_UI: "slider" # input|slider. 'slider' (default): range sliders. 'input': text boxes
# Tagging Refinement Feature Flags
TAG_NORMALIZE_KEYWORDS: "1" # 1=normalize keywords & filter specialty mechanics (recommended)
TAG_PROTECTION_GRANTS: "1" # 1=Protection tag only for cards granting shields (recommended)

View file

@ -105,6 +105,9 @@ services:
# Build Stage Ordering
WEB_STAGE_ORDER: "new" # new|legacy. 'new' (default): creatures → spells → lands → fill. 'legacy': lands → creatures → spells → fill
# Ideals UI Mode
WEB_IDEALS_UI: "slider" # input|slider. 'slider' (default): range sliders. 'input': text boxes
# Tagging Refinement Feature Flags
TAG_NORMALIZE_KEYWORDS: "1" # 1=normalize keywords & filter specialty mechanics (recommended)
TAG_PROTECTION_GRANTS: "1" # 1=Protection tag only for cards granting shields (recommended)