Dev Log #12: Building an Isometric ARPG Engine in Pure Python
When I told people I was building an isometric action RPG in Python, the reactions ranged from polite concern to open laughter.
“Python is too slow for real-time combat.” “Just use Unity.” “Why would you do this to yourself?”
Fair questions. Here are the answers.
The Stack
Bound to Rot runs on four pillars:
- Raylib through its pyray C bindings: lightweight windowing, input, and 2D rendering. No scene graph telling me how my game should work.
- Numpy: entities, particles, and inventory grids live in contiguous typed arrays, not Python objects scattered across the heap.
- Numba: the genuinely hot code paths get
@njitcompiled down to machine code. - Pillow: texture preprocessing before anything touches GPU memory.
The rule that keeps it all fast is data-oriented design. Keep core logic functions simple, primitive types, basic arrays, basic loops, so they can be JIT compiled without modification. Reuse memory blocks for projectiles and dropped items instead of feeding the garbage collector. Prefer vectorized Numpy operations over Python loops wherever movement or physics allows it.
Python where it’s convenient. C-speed where it counts.
Diamond Math Everywhere
Isometric projection means every coordinate lives twice: once in map space, once on screen. The forward transform is:
sx = (col - row) * step + screen_offset_x
sy = (col + row) * step + screen_offset_y
Where step is half the scaled tile width. Clicking requires the inverse: take the mouse position, apply a half-tile diamond offset, solve the two-equation system, and you land back on col and row. Get one sign wrong and your player walks toward wherever the camera wants, which is a very confusing bug to describe out loud at 2am.
Rendering order is solved by the painter’s algorithm over diagonals: iterate layers by diag = col + row, draw terrain first, then walls, items, and entities per layer. The diagonal sort is what stops overlapping sprites from clipping through each other.
Movement maps across a 16-direction compass grid, with sprite frames selected by modular index arrays keyed on the input vector. Every sprite follows a strict naming convention of name, direction, angle, and frame index. Boring, mechanical, essential.
The Entropy System: No Streaky RNG
Here’s the part I’m proudest of. Traditional accuracy rolls feel awful because pure RNG produces streaks: five misses in a row when you needed one hit. Players don’t experience probability, they experience injustice.
Bound to Rot resolves hits through an entropy system instead. Each entity spawns with an incoming entropy value seeded between 1 and 100. Hit evaluation compares accuracy against evasion thresholds while stepping that entropy value deterministically. The outcome converges toward the correct odds without letting variance stack into kill-streaks of misses. Combat feels fair even when it kills you.
Layered on top: six damage types with distinct mechanics. Fire burns as damage over time and denies area. Lightning swings wild between 1 and 100 and chains between targets. Cold slows, then freezes. Rot corrodes armor and spreads as a debuff. Magic pierces standard defenses. Physical just hits hard and lets armor argue back.
Was It Worth It?
Every frame on screen traces back to math I understand, running on data layouts I chose. When something breaks, I know exactly where to look. That ownership is worth more to me than any engine’s asset store.
If you want to see how it plays, the Steam page is live. Wishlists decide how loud this thing launches, and every single one helps.
Follow along:
See you in the next one,
- Stamatios