d489e7a31b
"Winnable deals only" used to call `choose_winnable_seed` inline on the main thread inside `handle_new_game`. Each rejected attempt costs ~120 ms (`SolverConfig::default()` budget); the loop caps at `SOLVER_DEAL_RETRY_CAP` = 50, so a pathological run could stall the UI for ~6 s on a New Game click. Quat flagged this as the highest- impact UX regression left in the engine. Reorganised so the solver runs on `AsyncComputeTaskPool`: - New `PendingNewGameSeed` resource holds an `Option<PendingSeedTask>` carrying the in-flight `Task<u64>` plus the request's `mode` and `confirmed` flags so the polling system can replay them on a synthetic `NewGameRequestEvent` once the task resolves. - `handle_new_game` now writes to that resource (and `continue`s) for the winnable-only / Classic / random-seed branch, instead of calling `choose_winnable_seed` synchronously. - `poll_pending_new_game_seed` runs `.before(GameMutation)` so the synthetic event lands in the same frame's `handle_new_game` — the player sees no extra-frame visual lag once the solver completes. - Cancel-on-replace: when a fresh `NewGameRequestEvent` arrives while a previous task is in flight, `pending_seed.inner = None` drops the old task (Bevy's `Task` Drop cancels cooperatively at the next await point) before processing the new request. Two tests: - `winnable_seed_search_runs_async_and_completes_eventually` — spawns the task, drives `app.update()` in a wall-clock-bounded loop with `std::thread::yield_now()` so the shared `AsyncComputeTaskPool` gets a chance to schedule between polls. - `winnable_seed_search_drops_in_flight_task_on_new_request` — fires a winnable-only request, then before the task can complete fires an explicit-seed request that bypasses the solver entirely. Asserts the explicit seed wins, verifying the cancel-on-replace contract. Existing solver tests pass unchanged: explicit-seed paths skip the new branch and run synchronously like before. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>