search
Runs the search and returns the populated root node (one child per element of legal, plus whatever the tree expanded into). determinize supplies one fresh determinized State per iteration; a thrown exception from it is treated as a free retry (does not consume an iteration) — the caller is expected to advance its own RNG before rethrowing, mirroring a failed-sample retry. elapsedMillis is polled against SearchBudget.maxMillis.
A suspend function: cancelling the calling coroutine (a player moves on, a screen leaves composition) stops the loop at the next iteration boundary via ensureActive instead of burning the full budget. onSearchError receives every determinization/iteration failure that isn't a cancellation — a rules bug that always throws is now visible instead of silently producing a near-empty root; the default is a no-op so existing callers that don't care still compile.
determinize, iterate and the rules callbacks are all caller-supplied, which is why the catches below are broad: a search shell whose job is to survive a bad sample cannot narrow to types it does not own. Nothing is swallowed — every non-cancellation failure goes to onSearchError, and CancellationException is rethrown first so cancellation still works.