Bloom

Grow your colour, starve theirs. One tap a turn.

▶ Run in browser

Tags: game strategy hex 2d ai touch

Bloom

Grow your colour, starve theirs. One tap a turn.

Bloom is an original abstract hex-territory game built on SimVX. The whole game is one rule and one gesture: tap an empty hex to drop a seed of your colour; every adjacent enemy hex your placement now outnumbers flips to you, and the flips cascade outward in a visible wave. When the 61-cell board fills, the majority wins. Matches last a couple of minutes and play identically with a mouse or a single finger on a phone.

The one rule

  • The board is a hexagon of 61 cells. It opens with one amber and one azure seed on opposite rims; the centre starts empty.

  • On your turn, press an empty cell to aim (a preview rings the cells that would flip) and release to place a seed of your colour. Sliding off the board before releasing cancels.

  • Capture is local, not about totals. A hex is judged only by the cells touching it: an enemy hex flips when your seeds touching it strictly outnumber its own colour touching it. Captures cascade: a freshly flipped cell can tip its neighbour over, rippling outward ring by ring.

  • The rule is symmetric, so your hexes flip the same way on the opponent’s turn. A big connected group is safe in its interior (each cell is touched by many friends), but a thin, exposed cell can be flipped by two adjacent enemies even if they own very few cells overall. Keep your shapes connected.

  • Ties never capture and empty cells count for neither side, so rim and corner cells (fewer neighbours) are naturally defensible while the open centre is the most volatile.

  • The board has an odd number of cells, so a draw is impossible. Strict majority wins.

Modes

  • vs Computer with three difficulties: Calm (greedy), Sharp (looks one move ahead), Ruthless (depth-2 search). The AI is fair: it plays the same legal moves through the same capture rule you do, with no hidden state.

  • 2 Players hotseat on one device.

Controls

Action

Desktop

Touch

Place a seed

Press an empty hex (preview), release to place

Hold an empty hex (preview), lift to place

Menu

Esc

Menu button

Undo

Z / Backspace

Undo button

Help

H

Help button

Restart

R

Rematch on the game-over card

Everything is reachable by touch: large hit targets, a bottom control strip, and no hover-only information. The layout is anchored and re-fits the board to the viewport, so it works on desktop, phone landscape and phone portrait.

Run it

The commands below are for your own copy of this directory. From the root of a repository checkout, prefix each with uv run and spell the path from the root: uv run python examples/demos/bloom/main.py.

# Desktop
python main.py

# Self-contained browser build (Pyodide + WebGPU): this is the version playable
# on the SimVX site, and it runs entirely client-side.
simvx export web main.py -o /tmp/bloom.html

# Remote play from a machine that has a GPU: the server renders with Vulkan and
# streams frames over a WebSocket, so the browser only needs a video decoder.
python web.py      # then open http://localhost:8080

Architecture

Pure game logic is kept completely separate from rendering, input and UI, so the rules and AI are deterministic and unit-testable with plain pytest.

File

Responsibility

hex_grid.py

Pointy-top axial hex math, parametrized by viewport size.

board.py

Owner, Board, and resolve_placement (the capture cascade). Pure.

ai.py

Negamax + alpha-beta over board copies. Pure.

render.py

HexBoardView: fit, hit-test and draw the board via Draw2D.

game.py

BloomGame node: turns, animation, AI pacing, undo, HUD, signals.

menu.py

MainMenu and the live HowToCard.

app.py

BloomApp root: screen state machine, global input, bottom bar.

main.py

Desktop entry point, and the root the web export builds from.

web.py

Optional streaming server for remote play.

The capture resolver is the single source of truth: the player turn, the cascade animation, and the AI search all run the exact same function.

Engine features showcased

  • WorldEnvironment post-processing. Bloom with bloom_threshold = 1.0 is the whole look: only the owned cell cores and capture flashes are authored above 1.0, so they glow while every matte fill, grid line and HUD element stays out of the effect. Vignette, film grain and FXAA are on for the whole match; the win sweep tweens bloom intensity up and pulses chromatic aberration once, then turns it back off.

  • Coroutine animation. Every pop, flip flash, afterglow, score slide and the staggered win sweep is a generator driven by start_coroutine with tween and wait, using the built-in easing curves. No animation state machine.

  • Immediate-mode 2D drawing. The board, pollen, ripples, HUD and titles are drawn in on_draw, including additive (blend="add") polygons and width-fitted text.

  • Synthesized audio. The three sound effects are generated at load with AudioClip.tone and played through AudioPlayer on the SFX bus, with pitch_scale rising per cascade ring. No audio assets ship with the demo.

  • Input actions. InputMap actions are registered in the root’s on_ready, so the same bindings work on desktop and in the browser build.

  • Anchored, resize-aware UI. Menus, the bottom control strip and the game-over card are built from Button / Label inside anchored HBoxContainer / VBoxContainer. The control strip uses SizingMode.FILL, so a window resize re-shares its width across the buttons on its own.

  • Signals. BloomGame emits move_made, turn_changed and game_over, and the menus emit their own; screen flow is driven by those connections rather than by the root polling the game every frame.

Tests

# In your own copy of this directory
pytest tests

# From the root of a repository checkout
uv run --package simvx-core pytest examples/demos/bloom/tests

test_board.py pins the rules (capture, cascade, ties, edge defensibility, no-draw parity, determinism), test_ai.py checks legality, determinism and that deeper search plays stronger, and test_game_flow.py drives the game node with simulated input through SceneRunner and verifies signals, undo and game-over.

Why it is original

Bloom is its own game with its own theme, rules and balance. Game mechanics are not copyrightable, and none of this code is copied from any other game. The hex coordinate math follows the standard, public cube/axial formulation.

Source files

File

Summary

Lines

main.py

Bloom: Grow your colour, starve theirs. One tap a turn.

34

ai.py

Bloom AI: a fair opponent. Pure logic, no graphics.

104

app.py

Bloom root node: screen state machine, global input, gradient backdrop.

280

board.py

Bloom board: pure game logic. No graphics, no input, no randomness.

132

game.py

Bloom game node: drives turns, animation, AI and the in-game HUD.

499

hex_grid.py

Hex grid: pointy-top axial-coordinate math, viewport-parametrized.

105

menu.py

Bloom menus: main menu (mode + difficulty) and the how-to-play card.

335

render.py

Bloom rendering: a plain helper class (not a Node) that lays out and draws

205

web.py

Bloom: play in your browser via video streaming.

35

Source

 1"""Bloom: Grow your colour, starve theirs. One tap a turn.
 2
 3# /// simvx
 4# tags = ["game", "strategy", "hex", "2d", "ai", "touch"]
 5# screenshot_frame = 90
 6# web = { width = 900, height = 700, root = "BloomApp", responsive = true }
 7# ///
 8
 9An original abstract hex-territory game. Tap one empty hex to drop a seed of
10your colour; every adjacent enemy hex your placement now outnumbers flips to
11you, and the flips cascade outward in a visible wave. When the board fills, the
12majority wins.
13
14Capture is local: a hex flips when the seeds touching it are outnumbered, so the
15same rule lets you take enemy cells and lose your own exposed ones. Keep your
16shapes connected.
17
18Modes: vs Computer (Calm / Sharp / Ruthless) or two-player hotseat.
19Controls: press/hold an empty hex to preview which cells flip, release to place.
20Esc = menu, Z = undo, H = help, R = restart. Plays identically with a mouse or a
21single finger on a touchscreen.
22"""
23
24from app import BloomApp  # noqa: F401  (re-exported for the web exporter)
25
26from simvx.graphics import App
27
28
29def main() -> None:
30    App(title="Bloom", width=900, height=700).run(BloomApp())
31
32
33if __name__ == "__main__":
34    main()