Game completion stopped being a message and became a system — Design · Blupoli

Game completion had become too important to leave inside every engine

For a long time a game could handle its own ending: detect a solved board, show a message, and offer another round. That works while each game is almost an island. It stopped being enough once completion began feeding statistics, personal records, achievements, streaks, and a shared visual identity across Blupoli Puzzles. PRs #168 and #175 turned that moment into a platform component.

The goal was not to make every game feel identical. We wanted a reliable sequence: keep the finished board visible, recognize the outcome, show a small set of useful metrics, apply category identity, celebrate only positive outcomes, and respect motion preferences. The engine still decides when the game ends; the shared shell decides how the ending is presented.

A solved puzzle should not disappear instantly. The final board is evidence of the player’s work and deserves a short moment before the interface moves on to statistics and next actions.

First iteration: centralize before making it spectacular

PR #168 was deliberately restrained. It kept the completed board visible for 1.2 seconds, added a common final-board animation and confetti for positive outcomes, introduced fallback performance stars, and began centralizing semantic player and board colors. The main value was proving that puzzle and competitive engines could converge on one completion surface.

Ataxx and Dots and Boxes were useful tests because neither ends in solved. Their visual convention was normalized so the human player is blue and the CPU is red. The exact colors are a small detail, but player identity should not become inconsistent just because a different engine is running.

Reduced motion was included from the start. The contemplation delay remains, while strong animation and confetti disappear when the operating system requests less motion. Accessibility was part of the contract rather than a patch added after the visual effect.

The second iteration changed the visual scale

PR #175 revisited the common base and made celebration much more visible. The completed board remains on screen for 1.7 seconds. Positive results then use eight bottom emitters on desktop with 22 particles each: 176 in total. Mobile uses five emitters with 16 each, for 80 particles.

The mobile reduction changes density rather than duration. Particles rise, reach an apex, and take several seconds to fall beyond the viewport. The implementation makes the rhythm explicit: 1,700 milliseconds of board reveal, 1,760 before confetti begins, and a 6,000-millisecond particle lifetime.

Those constants make experience review practical. A future usability pass can change shared timing in one place and validate it once instead of chasing unrelated timeouts across engines and local modals.

Visual completion sequence showing finished board, pause, celebration, and metric summary
The result is seen first, celebrated next, and summarized without hiding the animation.

A 1.7-second pause is an interaction decision

The pause prevents the application from feeling as if it steals the result at the instant the player earns it. The board temporarily changes role: it stops being only an interactive surface and becomes visual confirmation that the task is complete. That interval connects the final move to the finished state.

It also separates comprehension time from spectacle time. The player may need a moment to register the completed board even if no animation should run. That is why the pause remains under reduced motion while confetti can disappear without breaking meaning.

Centralizing this timing turns rhythm into part of the platform contract. Not everything worth sharing has to be an API; sometimes a consistent pause is what prevents dozens of games from developing unrelated senses of closure.

Not every finished outcome deserves confetti

The component distinguishes positive outcomes from outcomes that are merely complete. Solved and win form the positive set. A loss or draw is still stored, summarized, and included in statistics, but it does not receive the same celebration as a victory.

This is possible because GameResult carries outcome semantics. The completion component does not need to understand the rules of Ataxx or Dots and Boxes. It needs a correct result, then applies the presentation that belongs to that result.

Confetti therefore stops being a solver side effect. It becomes a visual interpretation of shared data. New competitive modes can reuse the same closure without copying animation logic, and animation can change without changing engines.

Stars needed a common fallback

Not every game had a native rating system. The shared component calculates stars when no game-specific strategy exists. Puzzle sessions start from five and may lose stars for hints, resets, or performance substantially worse than a comparable personal best.

Competitive sessions use outcome and, when available, score margin. A win can produce four or five stars, a draw sits in the middle, and a loss receives a lower rating. This provides a compact fallback where no richer model exists.

Game configuration can override that fallback. A title can define a rating based on moves relative to a reference, for example. Common behaviour fills gaps without erasing meaningful mechanics.

The summary metrics needed hierarchy too

GameResult can contain far more than belongs on a completion screen. Puzzle defaults prioritize time, moves, hints, difficulty, and size. Competitive defaults prioritize result, score, time, moves, and difficulty. The component deliberately stops before becoming a miniature analytics dashboard.

Games may configure a different list. The shared formatter understands reference moves, pieces, dual pivots, errors, backtracks, and values nested in metadata. The visual structure remains stable while the data reflects each mechanic.

This is consistency rather than uniformity. Consistency gives the player a familiar place to read the outcome and choose a next action. Uniformity would force every engine to show identical fields even when those fields are irrelevant.

Category identity belonged in CSS

The second iteration removed the CATEGORY_THEMES table from JavaScript. The host exposes the primary category through data-game-category, and CSS translates that semantic value into color. Every category has explicit light and dark values.

A second color table in JavaScript would create another source of truth beside the design system. A palette change would require synchronized edits and could drift in only one theme. Category is data; color is presentation.

Tests protect the boundary. They require light and dark category variants and prevent the JavaScript color table from returning. A styling decision is guarded as architecture rather than relying on team memory.

Semantic tokens replace theme patches

The work introduced semantic roles such as --game-control-*. Without them, a control can begin with dark-theme values and later accumulate a light-theme patch. Repeated across a catalogue, that pattern creates brittle CSS.

With semantic tokens, components ask for a control background, text, or border; the active theme supplies concrete values. game-shell.css can express the same intent in both modes instead of treating one theme as primary and the other as a repair.

PR #175 did not claim to finish every per-engine color problem. The exhaustive audit remained tied to issue #174. The shared completion system was a foundation, not a declaration that the whole catalogue was already visually normalized.

Confetti had to live above the summary

Earlier celebration could become almost hidden as soon as the completion screen appeared. The new effect is a viewport-level layer rather than decoration trapped inside the result card. Emitters start at the bottom and particles remain visible while the summary is already on screen.

That forced stacking, duration, and density to be designed together. More particles are useless if they fall behind an overlay. A longer lifetime is harmful if it blocks interaction. A wide spread feels generous on desktop but can overwhelm a narrow phone.

The composition changes the meaning of the effect. Celebration belongs to the whole completed game, not to a button or a small card. It occupies the context of the outcome and reads as closure for the challenge itself.

Mobile should not be an impoverished version

Desktop uses eight emitters across the lower width; mobile uses five and fewer particles, but the six-second lifetime remains. Time and trajectory are preserved while density adapts to available space and performance.

A phone experience should not simply be desktop shrunk down, but it also should not lose every expressive moment. The hierarchy remains: recognize the board, celebrate if appropriate, show the summary, and offer the next action.

Puzzle boards already consume a large share of a small viewport. Celebration has to feel substantial without competing with controls or making result information unreadable.

Reduced motion preserves meaning while removing spectacle

When prefers-reduced-motion is active, intense animation and confetti disappear. The board contemplation interval and the result summary remain. Two players receive the same semantic content with different amounts of motion.

This makes celebration progressive enhancement. If a device cannot animate smoothly, a preference disables the effect, or a future surface uses another rendering approach, completion still works. HTML owns the outcome and actions; spectacle is optional.

Implementing the policy once also stops every engine from inventing a different interpretation of reduced motion. One user preference applies across the catalogue.

Sound, copy, and actions belong to the shared layer

The component keeps one local preference for feedback sound and centralizes result tones. Engines no longer need their own global audiovisual settings. Common copy covers solved, win, draw, and loss titles, time and move labels, personal records, sound controls, and navigation actions.

That copy exists in Spanish, English, Italian, Portuguese, French, and German. The most emotionally visible point of the game should not fall back to one default language. A translation fix in the common component benefits every engine that uses it.

Primary actions such as play another, view statistics, and close do not depend on animation finishing. They remain available even when motion is reduced or confetti is not rendered.

Completion combines session facts with historical context

The summary contains two classes of information. Time, moves, outcome, and score describe the session that just ended. A personal record or achievement relates that session to history. They can appear together, but the interface should keep their meaning distinct.

The component receives GameResult and can also read progress context. Star fallback may compare against an existing best and a label may announce your best time, while base metrics remain facts about this session. A first-ever game still renders correctly without invented history.

This boundary keeps persistence ownership clean. The completion component does not become the record database; it consumes what the progress system already knows.

Achievements arrived later without forcing a rewrite

The definitive achievement system later integrated with the shared completion screen by grouping new unlocks and linking to the collection. The completion component does not evaluate achievement rules; it renders unlock information produced elsewhere.

The achievement engine likewise does not know how the completion screen animates. Both systems meet at a narrow event boundary. That separation is why achievement integration did not require rebuilding post-game UI inside every engine.

One completion surface becomes an integration point for personal records, achievements, and future post-game context without multiplying implementations.

Why the primary category drives the theme

A game may belong to several categories, but one result screen needs a coherent visual identity. The component uses the primary category exposed by the host as its theme anchor. That avoids mixing multiple palettes in a single celebration and keeps the ending visually connected to the game’s other surfaces.

The category does not alter GameResult or persistence. It selects visual variables. Taxonomy can evolve without inserting presentation details into the result schema: data still describes the session, while DOM context provides theming information.

It also reinforces category color as a navigation aid across Explore, progress, and game surfaces. Completion becomes another place where a stable visual language helps orientation.

A shared component reduces the QA surface

Before centralization, validating a completion change meant reasoning about several slightly different implementations. A fix in one game did not guarantee equivalent behaviour elsewhere. With a shared component, most visual regression coverage can live in one set of tests plus representative engine cases.

Every engine still needs to emit the right outcome and metrics. But once those values arrive correctly, panel layout, confetti, themes, sound, accessibility, and actions are not rebuilt dozens of times.

Reducing the number of implementations reduces the number of places where the same bug can return in a slightly different form. Consistency produces a direct maintenance and QA benefit.

Visual tests protect intent, not only syntax

The suite verifies timing, confetti, fallback stars, shared tokens, and category variants. It also protects the CSS-only color boundary and the intended celebration intensity and duration. Those are unusual assertions for UI code, but they map to product decisions that can otherwise regress silently.

An automated test cannot determine whether an animation feels excellent. It can detect that a refactor reduced eight emitters to one, removed the contemplation interval, or dropped a light-theme category definition. Tests preserve structural invariants; human review judges perceptual quality.

That combination scales better than manually replaying every game ending after each shared stylesheet or runtime change.

What this iteration deliberately did not solve

The common system does not force every game to show identical metrics or use the same rating strategy. Configuration permits useful specialization. It also does not mean the full catalogue color audit is finished; PR #175 explicitly left exhaustive per-engine cleanup outside scope and linked that work to issue #174.

That boundary is healthy. A shared component becomes fragile when it tries to anticipate every future exception. It is better for it to own sequence, outcome, density, accessibility, themes, actions, and extension points while genuine differences enter through configuration.

The goal was not to make every game indistinguishable. It was to remove accidental duplication from the one thing they all do: finish.

The broader lesson: completion is a platform surface

The work began as a confetti improvement and ended up reinforcing architecture. Completion connects data, accessibility, theming, localization, progress, and navigation. Keeping it inside every engine repeated all of those concerns and made every cross-cutting feature more expensive to add.

Now the engine reports an outcome; the shared component decides how to acknowledge it; CSS supplies category identity; user preferences control motion and sound; progress and achievements add context; and tests protect timing and structure. Games can still specialize without giving up a coherent platform experience.

The visible improvement is more cannons, more particles, and a celebration that remains visible above the result screen. The durable improvement is that the next post-game feature can be built once and reach the catalogue without redesigning the ending from scratch.

Why completion timing must stay independent from persistence

The visual sequence begins only after the game has produced a valid result. That ordering matters. Saving progress should not depend on whether confetti finished, whether the panel rendered successfully, or whether the player immediately navigated away. Persistence and presentation happen around the same moment, but they are not the same responsibility.

This separation protects data from animation changes. We can tune the 1.7-second reveal, replace the particle system, or disable celebration entirely without changing when a session becomes complete. The repository records the event because the game ended, not because the UI reached a particular frame.

It also protects the UI from storage details. The completion component receives normalized information and can focus on hierarchy and feedback. It does not need to know how IndexedDB, the durable queue, or aggregate progress are implemented behind the shared result contract.

Category theming is a navigation aid, not only decoration

Using the primary category as the completion theme does more than make the panel look different. Blupoli is increasingly using category colors across discovery, progress, and game surfaces so players can recognize families at a glance. The final screen continues that visual thread at the moment when attention is highest.

Because the identity is expressed through semantic CSS variables, the same category can remain recognizable in light and dark modes without duplicating runtime logic. The design system can evolve shades while the host continues exposing only the category identifier.

This is important as games begin to belong to multiple categories. Storage can keep the full taxonomy, while a single primary category provides one clear visual anchor. The completion component does not need to blend several palettes or decide taxonomy on its own.

The post-game surface has a strict information budget

Centralization made another problem visible: once a shared surface exists, every new platform feature wants to place something there. Personal bests, achievements, streak updates, daily-challenge status, sharing, recommendations, and replay actions can all make plausible claims on the same space.

The component therefore needs an information budget. The current design limits primary metrics, groups achievement unlocks, and keeps next actions obvious. A new integration should earn its place by helping the player understand the result or choose what to do next, not simply because the platform happens to have more data available.

This constraint is architectural as well as visual. A shared surface becomes valuable when it concentrates cross-cutting features; it becomes harmful if it turns into an unbounded dumping ground. Centralization makes that trade-off explicit and gives us one place to enforce it.

Shared presentation makes future native packaging easier

Blupoli Puzzles is still a web experience, but the project is also preparing for Android distribution through Capacitor. A consistent browser-level completion contract helps that path because post-game behaviour is implemented in the same shared web layer rather than being fragmented across game pages.

This does not mean Capacitor requires this architecture, or that the completion component was built only for Android. The benefit is simply that a wrapped application inherits one predictable ending across the catalogue. Device-specific work can focus on shell integration instead of normalizing dozens of different completion flows.

The same principle applies to any future surface. A stable shared completion contract travels better than a collection of engine-specific modals because the platform has one place where lifecycle, accessibility, theming, and navigation meet.

What we learned from making the celebration deliberately excessive

The request for more cannons and a longer visible fall was useful because it forced us to distinguish visual intensity from architectural quality. It is easy to add another burst of particles; it is harder to make sure those particles appear in the correct layer, respect reduced motion, adapt to mobile, and never interfere with the actual result.

Once those constraints were explicit, “more confetti” became a test of the shared system rather than a one-off flourish. Eight emitters, 176 desktop particles, five mobile emitters, and six seconds of lifetime are implementation choices that can evolve. The durable part is the structure that lets them evolve safely.

That is why this work belongs in the Devlog as more than a visual tweak. The celebration made a deeper architectural boundary visible: engines own game rules, while the platform owns the way a completed game is acknowledged.

Related reading

The data layer feeding the summary is covered in Progress starts when you play. The later reward integration appears in From scattered badges to a platform-wide achievement system. Earlier visual context is in The shared UI system.