| """ |
| ShufflerController — mirrors the JS makeShuffler() no-repeat logic. |
| |
| The seen-list for each named pool is kept inside a dict that lives in |
| gr.BrowserState (localStorage). The state dict shape: |
| |
| { |
| "jam": {"seen": ["...", ...]}, |
| "tt_easy": {"seen": [...]}, |
| "tt_medium": {"seen": [...]}, |
| "tt_hard": {"seen": [...]}, |
| "imp_all": {"seen": [...]}, |
| "imp_0": {"seen": [...]}, # per-category index |
| ... |
| "iv_all": {"seen": [...]}, |
| "iv_0": {"seen": [...]}, |
| ... |
| } |
| """ |
| import json |
| import random |
|
|
| from pydantic import BaseModel |
| from models.session_models import ShufflerState |
|
|
|
|
| def _serialise(item) -> str: |
| if isinstance(item, str): |
| return item |
| if isinstance(item, BaseModel): |
| return json.dumps(item.model_dump(), ensure_ascii=False, sort_keys=True) |
| return json.dumps(item, ensure_ascii=False) |
|
|
|
|
| def next_item(pool: list, key: str, raw_state: dict) -> tuple: |
| """ |
| Pick the next unseen item from pool. |
| Returns (item, updated_raw_state). |
| """ |
| if not pool: |
| return None, raw_state |
|
|
| state_dict = raw_state.get(key, {}) if isinstance(raw_state, dict) else {} |
| shuffler = ShufflerState.model_validate(state_dict) |
| seen_set = set(shuffler.seen) |
|
|
| unseen = [i for i, item in enumerate(pool) if _serialise(item) not in seen_set] |
| if not unseen: |
| |
| shuffler.seen = [] |
| seen_set = set() |
| unseen = list(range(len(pool))) |
|
|
| idx = random.choice(unseen) |
| picked = pool[idx] |
| shuffler.seen.append(_serialise(picked)) |
|
|
| updated = dict(raw_state) if isinstance(raw_state, dict) else {} |
| updated[key] = shuffler.model_dump() |
| return picked, updated |
|
|
|
|
| def reset_key(key: str, raw_state: dict) -> dict: |
| """Reset the seen list for a single key.""" |
| updated = dict(raw_state) if isinstance(raw_state, dict) else {} |
| updated[key] = ShufflerState().model_dump() |
| return updated |
|
|
|
|
| def imp_pool_key(cat: str, categories: list) -> str: |
| if cat == "All": |
| return "imp_all" |
| idx = next((i for i, c in enumerate(categories) if c.category == cat), 0) |
| return f"imp_{idx}" |
|
|
|
|
| def iv_pool_key(cat: str, categories: list) -> str: |
| if cat == "All": |
| return "iv_all" |
| idx = next((i for i, c in enumerate(categories) if c.category == cat), 0) |
| return f"iv_{idx}" |
|
|