One of the oldest traditions in roguelike engines is using event replay to reconstruct game state. Instead of storing full snapshots of the game, the engine records a sequence of player inputs and random seed values, and “replays” them from the beginning to rebuild the current state.
This approach has an elegance to it. It keeps save files small and ensures reproducibility—until it doesn’t.
In the Swift reimplementation of my roguelike engine, I decided to abandon event replay entirely. Instead, the engine saves the entire game state snapshot directly. This shift eliminates one of the most frustrating problems in traditional roguelike development: save desynchronization.
At a glance, event replay seems ideal:
But in practice, replay-based engines suffer from extremely brittle behavior, especially over time.
Any of these issues can cause the replay to diverge from the original, even though the inputs are technically the same.
When this happens, saved games become unloadable, replays become inaccurate, and the player’s progress is lost.
Because replay-based engines don’t store full game state, debugging requires re-simulating every frame from the start of the game. There’s no way to directly inspect the exact moment of failure.
This makes bug reproduction slow and error-prone—especially when the desync only occurs after 2,000 turns of play.
In the Swift reimplementation, I now save the entire serialized game state at each save point. No input history. No simulation replay.
This is a major architectural change with several benefits:
The save file contains exactly what the player was experiencing—no simulation, no divergence. What you save is what you load.
Because the snapshot is complete, you can safely save mid-turn, mid-action, or even mid-animation. This makes quicksave support on mobile much more robust.
Bug reports now include a complete snapshot of the game state. Developers (or testers) can load that snapshot and immediately inspect what went wrong.
No need to replay from the beginning and pray the bug reproduces.
You can safely change how the game engine handles logic without worrying about invalidating every old replay file.
As long as you maintain save format compatibility, the old games still load cleanly.
Historically, one reason developers avoided full game saves was file size. In a world of floppy disks and memory-constrained systems, storing 50KB per save was expensive.
But modern mobile devices, even low-end ones, can easily store full snapshots in the range of 50–500 KB without breaking a sweat.
In return, you get:
In an earlier test build, a simple change to monster rendering caused an extra RNG call for a cosmetic particle effect. This call didn't affect gameplay—but it altered the global RNG state.
Because the replay system relied on perfect RNG determinism, this change caused entire save files to break. Monster AI moved differently. Damage rolls changed. The game became unplayable—all from a visual effect.
That was the tipping point. No more input logs. No more seed juggling.
The engine uses structured, strongly typed game state:
struct GameState: Codable {
var dungeonLevels: [DungeonLevel]
var player: Creature
var turnCounter: Int
var rngState: RNGSnapshot
...
}When saving, the state is serialized to disk (or iCloud). When loading, the game resumes instantly from exactly that moment.
No rewinding. No replaying. Just play.
Replay-based engines are elegant in theory—but fragile in practice. In modern development environments, especially on mobile, it's more important to have reliable saves, quick debugging, and resilient architecture.
By switching to full state snapshots, the Swift reimplementation avoids one of the most persistent and frustrating issues in classic roguelike engines: save desync. It’s one less thing for players—and developers—to worry about.
And in a game where death is permanent, the save system shouldn’t be the thing that fails.