Here comes the hard part. For the sake of comprehension, I won't go into the detailed code. The source code will be uploaded to Git if anyone's interested—you can view it there. But here's the organization; I know it looks crazy.
The basics of Flutter application development—I won't go into those details. app.dart, main.dart, routes.dart are basic files that are required for basic application development. main.dart starts the app (this is also where you run the application, initialize SDKs, and run required async startup operations), app.dart wraps around the build, and routes.dart is where you set up app navigation.
On top of the executive files, I also used many different helper functions which are then organized into app_logger.dart, json_helpers.dart, animation_helpers.dart. app_logger.dart contains lots of debug functions that print what is happening as the game renders to the console. For the other two, if you understand programming you must have figured out—they are helper functions for animation calling and JSON reader calling (they are just defined in separate Dart files for organization purposes).
Project Organization Overview
lib/
├── main.dart # App entry point
└── src/
├── app/ # App-level setup (MaterialApp, routes)
├── audio/ # Sound effects & background music
├── game/ # Core game logic (match-3 engine)
│ ├── board/ # Visual rendering (Flame components)
│ ├── model/ # Data structures (Grid, Cells, Coords)
│ ├── stages/ # Level definitions & loading
│ ├── inventory/ # Player's boosters/power-ups
│ ├── vfx/ # Visual effects (particles, animations)
│ └── utils/ # Helpers (logging, random picking)
├── screens/ # UI screens (menu, level select, gameplay)
├── widgets/ # Reusable UI components
└── utils/ # General utilities
The reason the game architecture is so complicated is because (
), I wanted to create a fully functional MVP. Flame and Flutter programming follow a strict cause-and-effect model, requiring numerous functions and managers (
). We'll dive into the most important parts: board_game.dart and board_controller.dart. Think of them as: 1) the main board (the graphics), and 2) the printed circuit (0s and 1s sending signals to control the mainboard). In this case, they're executing fiber optic functions and calling managers to control the game on board_game.dart.
JSON-Driven UI Design
For convenience, I implemented separate JSON designs. Screens such as the menu, gameplay, and level select define their visual structure in JSON files (gameplay_1.json, world_screen_1.json, etc.). Each JSON specifies background images, element positions, sizes, and layout metadata. Take a look at the snippet from gameplay_1.json below:
{
"type": "container",
"id": "board_frame",
"file": "assets/boards/frames/board.png",
"position": { "x": 540, "y": 960 },
"size": { "w": 1080, "h": 1500 },
"innerOffset": { "x": 0, "y": 80 },
"anchor": "center"
},
{
"type": "image",
"id": "bottom_hud",
"file": "assets/boards/frames/bottom_hud.png",
"position": { "x": 540, "y": 1825 },
"size": { "w": 1080, "h": 241 },
"anchor": "center"
}
As you can see, the size and position of the board and bottom_hud are specified using x and y axes, based on the edges of the screen. Using this method makes scaling and level design much easier. I personally attempted to "vibe code" layouts using media queries and different mathematical functions, but this approach kept breaking across devices. The most reliable solution I found was to use JSON-driven design.
One of the good thing when you are studying data science is that, you know the basic of JSON 🤣. I may not be good at coding, Copilot is way better than me, but i can comprehend JSON structure enough to turn all JSON design into the kind of metadata needed for the app to understand specific coordinate of where each data should be. The application parses this JSON data using stage_data.dart, allowing screens to be designed as if targeting a fixed 1080×1920 resolution. As long as the original design is kept at 1080p, layout consistency can be maintained while scaling responsively at runtime. Copilot can then generate the appropriate coordinate-scaling logic, making responsiveness easier to implement without constantly reworking layouts.
Game Architecture Overview
In terms of programming architecture, Flutter handles the game UI and HUDs, while the Flame engine is responsible for board rendering and animation. The BoardController handles core game rules such as swap validation, match detection, gravity, refills, and cascades, and calls necessary systems like SpecialTileSpawner and SpecialActivationResolver. Conceptually, this acts as the central processing unit of the game logic (not literally).
The BoardController orchestrates the full match-3 gameplay loop. It validates whether tiles can be swapped, scans the grid for valid matches, analyzes match patterns to determine when special tiles should be spawned, clears matched tiles and blockers, applies gravity to fill empty cells, refills the board with new tiles, and repeatedly resolves cascades until the board reaches a stable state. Once the turn is complete, it decrements the remaining moves and checks win or lose conditions.
All animations, including tile movement and particle effects, are handled using Flame's MoveEffect system and custom VFX components, which will be discussed next.