feat(engine): independent (allow-repeats) multi-pick for the wildcard pool

The pool multi-pick (PICK min-max) now honors instance.pick_independent: when true it draws WITH replacement (repeats allowed) and the count is NOT clamped to the pool size, mirroring the inline ~ flag. Default (unique, pick_k_unique) path is byte-unchanged, so existing locked seeds reproduce. Foundation for the 'Allow repeats' pool toggle; schema (instance + library default) + TS parity + UI follow.
This commit is contained in:
DumiFlex
2026-06-09 23:44:25 +03:00
parent ba455fb098
commit 33b3600bf8
2 changed files with 50 additions and 3 deletions
+14 -3
View File
@@ -570,9 +570,20 @@ class WildcardHandler(ModuleHandler):
pool = [o for o in options if not o.get("is_null")]
weights = [max(0.0, float(o.get("weight", 1))) for o in pool]
pool_n = len(pool)
lo_c, hi_c = min(lo, pool_n), min(hi, pool_n)
count = lo_c if lo_c == hi_c else lo_c + int(rng.random() * (hi_c - lo_c + 1))
picks = pick_k_unique(pool, weights, count, rng)
independent = bool(instance.get("pick_independent", False))
if independent:
# SP2c: independent multi-pick draws WITH replacement (repeats
# allowed), mirroring the inline `~` flag — so the count is the
# requested range and is NOT clamped to the pool size.
count = lo if lo == hi else lo + int(rng.random() * (hi - lo + 1))
picks = [
p for p in (_pick_weighted(pool, rng) for _ in range(count))
if p is not None
]
else:
lo_c, hi_c = min(lo, pool_n), min(hi, pool_n)
count = lo_c if lo_c == hi_c else lo_c + int(rng.random() * (hi_c - lo_c + 1))
picks = pick_k_unique(pool, weights, count, rng)
sep = instance.get("pick_separator")
if not isinstance(sep, str):
sep = ", "
@@ -96,6 +96,42 @@ def test_multi_pick_binds_listvar_unique():
assert len(set(out["c"].items)) == 2
def test_multi_pick_independent_allows_repeats():
# SP2c: independent (with replacement) draws the FULL requested count even
# when it exceeds the pool size — 5 picks from a 2-option pool → 5 items.
from engine.syntax.types import ListVar
out = WildcardHandler.resolve(
_payload([
{"id": "a", "value": "alpha", "weight": 1},
{"id": "b", "value": "beta", "weight": 1},
]),
instance={
"variable_binding": "c", "pick_min": 5, "pick_max": 5,
"pick_independent": True, "pick_separator": ", ",
},
ctx=_ctx(seed=7),
)
assert isinstance(out["c"], ListVar)
assert len(out["c"].items) == 5
assert all(v in ("alpha", "beta") for v in out["c"].items)
def test_multi_pick_unique_clamps_to_pool():
# Default (unique): 5 requested from a 2-option pool clamps to 2 distinct.
from engine.syntax.types import ListVar
out = WildcardHandler.resolve(
_payload([
{"id": "a", "value": "alpha", "weight": 1},
{"id": "b", "value": "beta", "weight": 1},
]),
instance={"variable_binding": "c", "pick_min": 5, "pick_max": 5, "pick_separator": ", "},
ctx=_ctx(seed=7),
)
assert isinstance(out["c"], ListVar)
assert len(out["c"].items) == 2
assert len(set(out["c"].items)) == 2
def test_multi_pick_seed_deterministic():
a = WildcardHandler.resolve(
_payload(list(_MULTI)),