Age of New Worlds is my attempt to build a hex-based 4X strategy game for iOS, MacOS, Android platforms with the kind of systems I love: exploration, expansion, city growth, production choices, research, turn pressure, and the slow emergence of a readable strategic map.
At the current stage, the game already has the core loop in place. I can start a match, explore a hex map, found cities, move units, manage production, choose research, improve tiles, handle turn flow, and save or load the game. There is also a map editor and a growing set of developer tools around balance and content iteration.
The interesting part, at least for this series, is not only what the game does. It is how the project is shaped so it can keep growing.
It is not a clone of one specific game. Of course, some inspirations were impossible to avoid. Games like Civilization III left a strong mark on how I think about hexes, turns, cities, and the feeling of slowly growing an empire. AoNW is more like a question I keep asking in code: what happens if I try to build a compact 4X game where every system has to be understandable, testable, and eventually multiplayer-safe?
Why I’m Writing This Series
I don’t want to write “how to make a 4X game in Flutter, step by step”. That would force the project into a clean teaching shape, and real games do not usually grow that way. Real games accumulate constraints. They have features that seemed small until they touched every layer. They have systems that start as UI problems and become domain problems. They have code that exists because yesterday’s version taught me something.
So instead, I want to write about the architecture and the problems behind the game.
The series will include real code, but the code will be used as evidence: examples of decisions, boundaries, trade-offs, mistakes, and refactors. I want to focus on questions like:
- How do I keep game rules separate from rendering?
- How do I make a turn-based game ready for multiplayer before multiplayer is fully finished?
- How do I render a rich hex map without letting the renderer own the game logic?
- How do I model commands, events, saves, snapshots, and migrations?
- How do I build AI and balance tools without turning the main game into a pile of special cases?
and more…
The Game Shape
Age of New Worlds is a turn-based 4X strategy game played on a hex map. The player explores the world, settles cities, produces units and buildings, researches technologies, improves terrain, and competes through territory, score, and domination pressure.
The game currently focuses on hotseat play, but a lot of the architecture is already aligned with a server-backed multiplayer model. That means player actions are treated as commands, game changes can become events, and the client/server boundary is not an afterthought.
A simplified version of that idea appears in the command layer:
sealed class GameCommand {
const GameCommand();
}
Every player action belongs to this command hierarchy: selecting a tile, moving a unit, founding a city, choosing research, ending a turn, attacking, assigning workers, and so on.
The UI does not directly mutate the game world. It asks the application layer to dispatch a command.
abstract interface class CommandTransport {
Future<CommandTransportResult> dispatch({
required String saveId,
required GameState currentState,
required GameCommand command,
GameCommandContext context = const GameCommandContext(),
});
}
That boundary is one of the most important architectural choices in the project. It means local play, saved games, event logs, and future networked play can all share the same mental model: a player sends a command, the game validates and reduces it, then the resulting state is persisted and rendered.
The Tech Stack
The client is built with Flutter and Flame.
Flutter handles the application shell, HUD, overlays, navigation, panels, localization, and all the normal UI work. Flame handles the interactive map renderer: the hex world, camera movement, unit markers, overlays, particles, fog of war, and visual layers.
State management is built around Riverpod. Routing uses go_router. Immutable models and serialization use Freezed and json_serializable where they make sense. The project also has a shared Dart package, aonw_core, which contains game rules, protocol models, map/domain logic, AI planning, and systems that need to be shared by the client and server.
The backend is also Dart. It uses Shelf, WebSockets, PostgreSQL, and a command/event-oriented runtime for multiplayer work. Docker is used for the development database and server workflow.
So the rough stack looks like this:
- Flutter for the client application
- Flame for the hex-map renderer
- Riverpod for state and dependency wiring
- Dart shared packages for reusable domain logic
- Freezed/json_serializable for structured data
- Shelf/WebSockets for the backend
- PostgreSQL for persistent server data
- A growing test suite for reducers, transport, rendering contracts, persistence, map logic, and architecture rules
The Main Architectural Split
The project is split into several layers.
The domain layer owns game concepts and rules. This is where reducers, movement, combat, city growth, research, turn flow, fog of war, and other mechanical systems live.
The application layer owns use cases and ports. It does not care whether a command is executed locally or sent to a server. It defines the shape of the work.
The infrastructure layer implements persistence, snapshots, event logs, clocks, IDs, storage, and transports.
The presentation layer owns Flutter UI, Riverpod providers, Flame rendering, view models, formatting, audio, and player-facing interaction.
This split is not there because architecture diagrams look nice. It is there because 4X games become complicated fast. A single player action can affect movement, fog of war, city state, resources, objectives, UI effects, autosaves, event logs, and future network synchronization.
Without boundaries, that complexity spreads everywhere.
Why Commands Matter
One of the earliest architectural pressures came from turn flow.
In a small prototype, clicking a tile can just move a unit. But in a strategy game, clicking a tile might mean many different things depending on context. It might select a tile, preview movement, confirm a worker action, choose a city expansion hex, inspect hidden information, target an attack, or do nothing because the active player cannot see that tile.
So the renderer should not be the authority. The renderer is excellent at input, camera behavior, animation, and visual feedback. It should not decide the truth of the world.
In AoNW, the renderer forwards intent as commands. The reducer decides what the command means for the current game state. The UI then reacts to the new state and any resulting effects.
That sounds simple, but it becomes powerful once save/load, hotseat, AI, and multiplayer enter the picture.
What This Series Will Cover
I want the next articles to follow the pressure points of the project rather than a strict chronological order.
Some likely topics:
- The command/reducer/event shape of the game
- How Flutter UI and Flame rendering coexist
- Hex maps, terrain data, and visual layers
- Fog of war and player-specific visibility
- Save files, snapshots, migrations, and event logs
- City production, research, and turn economy
- AI planning, telemetry, and balance tools
- The path from local hotseat to server-backed multiplayer
- Why developer tools like the map editor matter early
and more…
The goal is to show the real architecture of a growing strategy game: the useful parts, the awkward parts, and the parts that only became obvious after the code had to support one more system than expected.
Age of New Worlds is still in progress. That is exactly why I think it is worth writing about now. The architecture is not a polished museum piece. It is alive, and changing as the game becomes more real.