Halloween deserves a small detour from serious business software. Tonight, our mission is to guide a pumpkin basket through a moonlit town, catch falling candy, and keep the ghosts away.
Meet Pumpkin Patrol, a small browser game built for October 31, 2026. A round takes up to 45 seconds of active game time. You have three lanes, three lives, and one very reasonable ambition: bring home more sweets than the ghosts do.
The complete game runs in a single HTML file. There is no account, installation, game server, or AI service to connect. Behind the playful surface is a useful engineering exercise: separate the rules from the browser, make time explicit, and test what happens when a player pauses, restarts, or loses their final life.
Play Pumpkin Patrol
Move with the left and right arrow keys or A and D while the playfield has focus. On a phone or tablet, tap a lane in the scene or use the three buttons below it. Catch the striped candies for ten points each. Avoid the white ghosts: each one costs a life.
Survive until the timer reaches zero to complete the midnight mission. The game pauses when you leave the window or hide its tab, and waits for you to resume. You can also pause with P or the Pause button. Restart returns to the opening screen.
Download the game, source code, tests, and editable diagrams. Extract the ZIP and open examples/pumpkin-patrol.html in your browser. The downloaded game also works without a network connection. This describes the HTML artifact; it does not claim that the surrounding WordPress page is an offline application.
The web trends behind the idea
Homann Software's recent articles focus on architecture, Java, AI, and tested applications. A seasonal game is a lighter way to explore the same habits: small responsibilities, visible behavior, and evidence that the example works.
Three developments in the current web landscape are relevant. They are context for our design choice, rather than evidence that Halloween games are the largest commercial opportunity in software.
First, browser capabilities continue to converge. The September 2026 web platform roundup, published by web.dev on October 2, describes several features reaching Baseline Newly available across browsers, including CSS progress() and lazy-loaded image sizing with sizes="auto". For a developer, the practical question is which features the audience can use. Baseline distinguishes newly interoperable features from widely available features; the latter reach that milestone after 30 months. It provides a useful compatibility signal alongside testing on the browsers your visitors actually use.
Second, browser graphics keep advancing. The Chrome 155–156 WebGPU update listed in the October 2026 release notes discusses texture compression and additional canvas color spaces. It also flags a temporarily disabled shader extension. That is a useful reminder to distinguish an announcement, an experiment, and a capability that can be relied on for a particular audience. WebGPU is interesting for demanding graphics. Our little two-dimensional game has a much smaller rendering requirement.
Third, web development increasingly has explicit ways to describe support and input behavior. We can combine a conservative graphics choice with Pointer Events for mouse, pen, and touch input, rather than building a different interaction system for each device. The third point is an engineering interpretation of these platform capabilities, not a measured adoption trend.
Our Halloween choice is therefore a small, portable browser experience built on established APIs. Canvas 2D is enough for this game. The example uses no WebGPU, framework, runtime AI, package manager dependency, or remotely loaded sprite library. That keeps the scope understandable and gives readers a working artifact they can inspect.
A tiny game with explicit rules
Before drawing pumpkins, define what a correct round means.
| Situation | Result |
|---|---|
| A new round is created | Middle lane, score zero, three lives, 45 seconds remaining |
| Candy crosses the catch line in the player's lane | Add ten points and remove that candy |
| A ghost crosses the catch line in the player's lane | Remove one life and remove that ghost |
| An object crosses in another lane | No score or damage; it continues off screen |
| The third life is lost | End the round immediately |
| 45 seconds of simulation time pass with lives remaining | Complete the round |
| The final hit and the timer finish in the same step | The final hit takes precedence |
| The game is paused | No simulation time, movement, spawning, scoring, or damage advances |
This is a lane game, not a physics simulation. The basket changes lanes immediately, and a catch is decided when an object crosses a horizontal line. There are no pixel-perfect collisions or promises that every generated pattern can be collected without risk. Candy and ghosts have different shapes as well as different colors.
Each new round starts with the same pseudo-random seed. That gives the game the same sequence of object types and lanes when it receives the same sequence of inputs. It is useful for reproducing bugs and comparing attempts. It does not make the score secure or prevent someone from modifying the downloaded code.
Architecture: let the rules live outside the browser
The project contains three small authoring files. engine.mjs owns the rules and state transitions. app.js connects browser events, drives the game loop, draws the scene, and updates the text interface. shell.html defines the page layout and controls. A build script embeds the first two into the shell and produces pumpkin-patrol.html.
The engine does not import the DOM, Canvas, or a timer. It receives a command or a time step and updates its own state. It does mutate that state; “deterministic” here means reproducible for the same seed, step sequence, and commands, rather than purely functional or immutable.
For example, moving the basket requires a running round and an integer lane. The allowed result is clamped to the three valid lanes:
export function move(game, lane) {
if (game.phase === 'running' && Number.isInteger(lane)) {
game.lane = Math.max(0, Math.min(2, lane));
}
}
This short function handles several real cases. Pressing left at the left edge leaves the basket in place. A paused round ignores movement. A fractional value or NaN cannot become the player's lane. The same rule applies whether the command comes from a keyboard event, a native button, or a pointer position.
Scoring uses a crossing test rather than checking whether an object is still near the basket:
if (previousY < HIT_Y && item.y >= HIT_Y && item.lane === game.lane) {
if (item.kind === 'candy') game.score += 10;
else game.lives = Math.max(0, game.lives - 1);
return false;
}
The false return value removes the caught object from the array's filter operation. That prevents one candy from awarding points repeatedly over several frames. An object already past the catch line cannot be collected by moving underneath it later. These details are small enough to overlook and important enough to test.
The complete source is in the download. The snippets in this article are extracted from that source and checked during packaging, so the explanation and the runnable example use the same rules.
A predictable game loop
Browsers schedule display work with requestAnimationFrame(). The callback frequency generally follows the display refresh rate, and background tabs commonly stop receiving callbacks. Moving every object a fixed number of pixels per callback would therefore make refresh rate part of the game rules.
Pumpkin Patrol instead advances the engine in steps of one sixtieth of a second. The browser adapter accumulates elapsed time, runs as many complete steps as it has available, and then renders the current state:
accumulator += Math.min((now - last) / 1000, 0.1);
while (accumulator + 1e-12 >= STEP && game.phase === 'running') {
tick(game); accumulator -= STEP;
}
STEP is 1 / 60. The tiny tolerance handles floating-point arithmetic near a step boundary. Rendering and rule updates now have separate responsibilities: a frame may draw after one step, several steps, or no complete new step.
The 100-millisecond cap is a deliberate compromise. After a long main-thread stall, the game accepts at most six new simulation steps from that frame's elapsed time. It drops the excess rather than fast-forwarding through seconds of missed objects. Consequently, 45 simulation seconds may take longer than 45 wall-clock seconds on a struggling device. This is a casual single-player game, with no real-time or competitive fairness guarantee.
The seed alone does not guarantee identical play across devices. Inputs must also reach the same simulation steps. The tests establish repeatability for a controlled command sequence; they do not claim that two people pressing keys at approximately the same moments will produce identical replays.
Keyboard, touch, and a considerate pause
The playfield takes keyboard focus when the player starts or resumes. It handles arrows, A/D, and P locally, so ordinary page navigation outside the game remains available. The lane buttons are native HTML buttons, with visible focus outlines and an aria-pressed value indicating the selected lane.
Pointer coordinates need one conversion. A phone may display the 600-pixel-wide canvas at a much smaller CSS width, so the event's horizontal position must be mapped back into logical game coordinates:
const rect = canvas.getBoundingClientRect();
const x = (event.clientX - rect.left) * canvas.width / rect.width;
move(game, Math.floor(x / (canvas.width / 3)));
The game accepts a primary pointer press and leaves vertical touch scrolling available. A tap near the right edge selects the right lane even when the displayed canvas is narrower than its internal coordinate system. Tests exercise this on desktop and an emulated touch viewport.
We also listen for window blur and the Page Visibility API. Either event can pause a running round. Returning to the window does not silently resume it: the player chooses Resume. The adapter clears its accumulator and previous timestamp at these transitions so a resumed round does not consume time spent away.
The score, lives, remaining time, and round status are regular text. A polite live region announces status, score, and life changes without announcing every countdown second. Reduced-motion preference disables the basket's decorative bobbing. Essential falling objects still move, and the game can be paused at any time.
These choices improve access, but they do not make a visual reaction game fully playable through a screen reader. We have not performed a manual assistive-technology audit. A future nonvisual mode would need a different interaction design, not just more labels.
UML for states, BPMN for the player journey
The UML state model gives the implementation a compact vocabulary: ready, running, paused, won, and lost. Start only activates a ready round. Resume only activates a paused round. Neither command revives a lost round. Restart constructs fresh state; the result screen's Play again or Try again action constructs fresh state and starts it.
This diagram is useful because the difficult bugs tend to occur at transitions. Can an old object damage a new round? Does a hidden tab keep spending its timer? Can a repeated key press toggle pause several times? Those questions are easier to discuss when the states have names.
BPMN serves a different purpose here. It describes one round as a user journey: start the round, play until a terminal condition, inspect the remaining lives, and show the appropriate result. The collapsed Play round subprocess includes movement, scoring, pausing, and explicit resume.
We do not use BPMN to execute the animation loop. The download includes a non-executable BPMN 2.0 model with diagram interchange coordinates, plus editable PlantUML sources for the component and state models. XML parsing and reference checks passed. No workflow-engine execution or full BPMN schema-validation claim is made. The PNG illustrations are readable editorial renderings; they are not output from a BPMN execution engine.
What we tested
The rule suite runs with Node's built-in test runner and needs no installed npm dependencies:
node --test engine.test.mjs
node build.mjs
Run those commands inside the download's examples directory. On this workstation, the sandbox prevented the test runner from creating its usual child process, so we executed the same suite in process with:
node --test --test-isolation=none examples/engine.test.mjs
That command ran from the package root on Node.js 24.17.0. All 24 rule tests passed, with no failures or skipped tests. They cover scoring once per object, ghosts and life limits, lane boundaries, invalid inputs, pause and resume, reset, the final-life/time-expiry tie, exact round completion, seeded repeatability, and object bounds across 100 seeded simulations.
A separate browser suite passed 24 checks in Chromium, split across a 1280 × 1100 desktop viewport and a 390 × 844 touch viewport. It exercised starting, keyboard movement, pointer-coordinate conversion, lane buttons, pause/resume, a simulated blur event, P to pause, a complete round, and restart. It also checked for horizontal overflow, JavaScript exceptions, and remote requests while loading and playing the standalone file. None were observed in those checks.
The browser suite uses an accelerated test clock. It validates behavior, not frame-rate performance or reaction difficulty on a physical phone. It is not a Safari, Firefox, or physical-device certification. The project includes logs and browser evidence so those boundaries remain visible alongside the passing results.
Run it yourself or add it to WordPress
For the fastest route, extract the download and open examples/pumpkin-patrol.html. It contains the styles, source, and drawings it needs. No web server is required for that standalone file.
For an ordinary static website, place the HTML file at an HTTPS URL and link to it. To embed a hosted copy in a WordPress article, use an iframe in a Custom HTML block if your site permits it. The game URL should point to the hosted HTML file, not the downloadable ZIP. Give the frame a descriptive title and enough height for its controls.
The game embedded in this article uses a self-contained srcdoc iframe. It embeds the generated game document directly and uses a script-enabled sandbox without same-origin permission. On this site, the saved WordPress draft retained the iframe and ran the game in its frontend preview. The package contains both inline and hosted integration options; recheck the rendered result when you change hosting, security policies, or editor settings.
Do not paste a whole HTML document into an ordinary paragraph block and assume it will run. WordPress.com's code guidance explains that JavaScript and iframe support depend on hosting features and permissions. That guidance is not a universal statement about every self-hosted installation. WordPress filtering, security policy, and the final rendered page must be checked on the actual site. Saving article text is not proof that an embedded game works.
The game itself contains no analytics, cookies, account system, or persistent score storage. A hosting service or the surrounding WordPress page may still process requests or use its own analytics. Likewise, a local score is for fun: if a future version awards prizes or maintains an online ranking, client-side JavaScript cannot be the authority for trustworthy results.
Make the next round your own
Try a different palette, replace candy with autumn leaves, or change the round duration. A new rule should start with an explicit behavior and a test. If a golden candy is worth fifty points, define whether it can appear alongside a ghost and what should happen at the catch line before adding its glow.
For a larger game, a framework, an asset pipeline, audio, or more advanced rendering may become useful. For tonight, the complete experience fits in one file and a short round.
Happy Halloween from Homann Software. May your basket be full and your bugs be the decorative kind.