mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-16 07:30:13 +01:00
feat: add ideal counts slider UI with smart validation
This commit is contained in:
parent
9ab3835e2a
commit
35bff901d2
12 changed files with 217 additions and 11 deletions
|
|
@ -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
|
||||
|
|
|
|||
12
CHANGELOG.md
12
CHANGELOG.md
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
124
code/web/templates/build/_new_deck_ideals.html
Normal file
124
code/web/templates/build/_new_deck_ideals.html
Normal 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>
|
||||
|
|
@ -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;">
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue