Building Flutter Apps (5/5) — Hexagonal Minesweeper: Cube Coordinates and BLoC

콰이어트(텍스트 북리더) 개발기 — Riverpod 상태 관리와 Play Store 등록

Hexagonal grid coordinate design, event-driven state management, and custom hit-testing — what changes compared to a square grid.


What This Article Covers

  • Why to use Cube Coordinates instead of offset coordinates in hexagonal grids, and the conversion rules
  • Why BLoC fits better than Riverpod when a single input triggers cascading state transitions
  • Flood Fill, touch hit-testing (Cube Round), and test boundaries — the implementation points that change in a hexagonal grid
  • BLoC separation criteria to prevent state explosion

1. Coordinate System — Offset vs Cube

A square grid is handled sufficiently with a (row, col) 2D array. A hexagonal grid is not. Starting with offset coordinates means neighbor indices differ for odd and even rows, and direction calculations scatter into conditional branches.

Cube coordinates represent each cell with three axes (q, r, s) subject to the constraint q + r + s = 0. The core benefits of this representation are:

  • Uniform neighbor calculation: a single array of 6 direction offsets yields all neighbors. Row parity branching disappears.
  • Simplified distance calculation: distance between two cells is (|dq| + |dr| + |ds|) / 2.
  • Mathematical representation of rotation and reflection: achievable through axis permutation alone.

Offset coordinates are convenient at render time because they are close to screen coordinates, but keeping logic in cube and display in offset produces cleaner separation. Maintaining only two conversion functions (offsetToCube / cubeToOffset) keeps all remaining logic consistent.

Application Principle

When working with a hexagonal grid, start with cube coordinates from the beginning. Offset-coordinate neighbor logic accumulates bugs repeatedly, and the cost of a full rewrite late in development is high.


2. State Management — When BLoC Is the Right Choice

Choosing a Flutter state management solution depends on project characteristics. Even within the same app ecosystem, the appropriate tool differs.

Project Type Characteristics Suitable Pattern
Viewer / Reader Simple input, shallow state graph Riverpod (reactive)
Game / Workflow Single input triggers cascading transitions BLoC (event-driven)

Characteristics of Game State

A single "cell tap" in minesweeper triggers the following flow:

Input: CellTapped(q, r, s)
  → Mine check
  → Flood Fill if empty cell
  → Victory condition check
  → UI state update

This flow must be treated as a single transaction. BLoC's Event → State mapping corresponds directly to this structure. The log shows exactly which Event caused which State transition, making regression tracing and reproduction straightforward.

2. UX 설계 —

Where Riverpod Falls Short

Riverpod composes state via an inter-Provider dependency graph. In "one input → multiple cascading state changes" scenarios, the Provider update order tends to become implicit, and tracing the originating event during debugging is difficult.

Decision Rule

  • Input → state change is 1-to-many and ordering matters → BLoC
  • Input → state change is mostly 1-to-1 and declarative composition is central → Riverpod

3. Flood Fill — BFS-Based Implementation

Tapping an empty cell with no neighboring mines causes adjacent zero-mine cells to open in cascade. The principle is the same as a square grid, but the neighbor set is computed using cube coordinates.

directions = [
  (+1, -1,  0), (+1,  0, -1), ( 0, +1, -1),
  (-1, +1,  0), (-1,  0, +1), ( 0, -1, +1),
]
4. Claude Code 페어 프로그래밍

BFS Instead of Recursion

On large boards, a single Flood Fill can open hundreds of cells. Recursive DFS risks stack overflow. Implementing with queue-based BFS is safe, and a visited Set prevents duplicate expansion.

Performance Observation Points

  • Cost of a single Flood Fill pass: O(number of cells opened)
  • UI updates are batched and emitted after Flood Fill completes. Calling state emit per cell causes a rebuild explosion.

4. Custom Rendering and Touch Hit-Testing

Regular Hexagons with CustomPainter

  • x-offset differs for even and odd rows
  • y-spacing between rows is 3/4 of tile height (height * 0.75)
  • Tiles are unified as either pointy-top (30° rotated) or flat-top hexagons

Touch Hit-Testing — Cube Round

Hexagons have angled edges, making bounding box comparison error-prone. At tile boundaries that cross into adjacent cells, the wrong cell gets selected.

The solution is to descend through: screen coordinates → fractional cube coordinates → integer cube coordinate rounding (Cube Round).

1. Convert touch coordinates (px, py) to fractional cube coordinates (qf, rf, sf)
2. Round each axis → (qi, ri, si)
3. If the q + r + s = 0 constraint is violated, recompute the axis with the largest rounding error from the remaining two axes

This approach mathematically guarantees the nearest tile center. It is cheaper and more accurate than running a point-in-polygon test per tile.


5. Test Boundaries — The Separation Line BLoC Provides

A key advantage of BLoC architecture is UI-independent testing. Because game logic and rendering are separated, the following can be verified with pure Dart tests:

Test Target Input Expected Output
Game over CellTapped on mine cell GameState.over
Victory All non-mine cells opened GameState.victory
Flood Fill CellTapped on zero-mine cell Set of cascade-opened cells
Coordinate conversion offset → cube → offset Original coordinates match (round-trip)

If the coordinate conversion round-trip breaks, all features fail in cascade. It is the highest-priority test target to lock in first.


6. BLoC Separation Criteria — Preventing State Explosion

Consolidating all game state into a single BLoC causes event types and branching to grow rapidly. Separate by responsibility.

BLoC Responsibility
GameBloc Board state, cell open/flag, win-loss determination
TimerBloc Elapsed time, pause
LeaderboardBloc Record save/retrieve (Hive integration)

The separation criteria are state lifetime and update frequency. Timer emits per second; Game emits per input event. Combining them causes Timer's high-frequency updates to trigger Game listeners unnecessarily, producing unwanted rebuilds.


Open Questions and Limitations

  • Cube coordinate adoption cost: team members unfamiliar with the system face a learning curve. Documentation is mandatory.
  • Accessibility of hexagonal tile UI: tile identification in screen reader and high-contrast modes requires additional design work.
  • Difficulty generation algorithm: uniform mine distribution does not guarantee a "good game." First-click safety, guaranteed logical solution paths — these are separate topics.
  • Platform-specific gestures: long-press (flag) on mobile and right-click on desktop can both be absorbed as BLoC events, but UX guidelines should be separated per platform.

Applicability

  • The same coordinate system applies to hexagonal board games in general (turn-based strategy, puzzles)
  • Workflow-style apps with a "single input → cascading state transitions" structure (checkout flows, multi-step forms, turn-based UI) also benefit from BLoC
  • Cube Round generalizes beyond hexagons to coordinate correction for non-rectangular tile layouts

댓글

이 블로그의 인기 게시물

Agent Memory Engine (2/10) — Building an AI Agent Memory System with SQLite Alone

"ML Foundations (9/9) — PyTorch vs TensorFlow, and the Road to Local LLMs"

"RAG Core Study (14/26) — Evaluation Sets with RAGAS & DeepEval"

"ML Foundations (8/9) — Deep Learning Architectures: CNN, RNN, Attention"

"ML Foundations (7/9) — Deep Learning Training: Optimizers, Regularization, Initialization"

OpenClaw to Hermes Migration (2/13) — What to Preserve, Partially Port, or Discard

AI Agents I Built (5/7) — Building an Automated Blogger API Publishing System