Same-tier overlapping entities merge into the next tier instead of bouncing, one pair per sweep pass
Context
Phase 2 of the approved merge-puzzler plan: the core game mechanic. Two same-tier, overlapping entities should combine into a single entity of the next tier instead of bouncing off each other, and each merge should add to a running score. This has to compose with the existing pairwise collision sweep (resolve_all_collisions/resolve_against_rest, both length-preserving) without breaking it, and needs a clear answer for what happens when three or more same-tier entities mutually overlap in the same frame.
Options considered
Decision
glemy/pe/tier.gleam gained color(tier), score_for(tier), next(tier) -> Option(Int) (None at the highest tier), and merge(a, b) -> Entity (next tier, midpoint position, averaged velocity, tier-derived radius). glemy/pe.gleam's Model gained a score: Int field. The collision sweep was restructured around a new private ResolveOutcome type (Survived(final_target, resolved_rest) | Merged(merged, remaining_rest, score)): resolve_target_against_rest checks each entity in rest against target in order, merging immediately (via tier.merge) on the first same-tier, overlapping, not-already-max-tier match it finds, otherwise falling back to the existing collision.resolve bounce and recursing. resolve_all_collisions threads the merged-or-survived outcome back into a shrinking entity list plus a total score-gained for the pass; pe.update adds that to model.score.
Verification
106 Erlang / 139 JavaScript tests passing (up from 101/134 pre-Phase-2, purely additive: 9 new pe/tier_test.gleam tests plus 5 new pe_test.gleam integration tests). New integration tests cover: two overlapping same-tier entities merge into the correct next-tier entity with the correct score; two overlapping max-tier (unmergeable) entities bounce instead; two overlapping different-tier entities never merge; a three-entity same-tier mutual-overlap cluster merges only the first pair this pass, leaving the third entity completely untouched; score accumulates correctly across two independent merges in one update call. Six pre-existing bounce-behavior tests that happened to use overlapping same-tier (tier 0) entities were updated to use the max tier (5, deliberately unmergeable) instead so they keep exercising pure bounce behavior unchanged rather than being broken by the new merge path.
Consequences
A dense same-tier cluster's full collapse now visibly spreads across multiple frames rather than happening instantaneously within one -- an intentional, documented simplification, not a bug. Any future code reasoning about 'how many entities exist after this frame' must account for the list shrinking on a merge. Model.score is now authoritative running game score, feeding directly into Phase 7's HTML score overlay.