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:
@@ -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)),
|
||||
|
||||
Reference in New Issue
Block a user