DEV Community

Cover image for The image agents — prompt to PNG
Sunitha Eswaraiah
Sunitha Eswaraiah

Posted on

The image agents — prompt to PNG

Post 4 of 8 in the game-factory series.

Icons are what people look at

You can theme fonts, colors, win messages, and sound effects. Change all of it and the game still reads like the original with a skin on it. Swap the icons — the actual symbols spinning in the reels — and it reads like a different game. The casino template I built by hand uses cloud service logos. Replace those with golden scarabs and ankhs and it becomes an Egyptian game. Keep the logos and give it an Egyptian color scheme and it doesn't.

A full theme has around thirty symbols. Each needs to be small enough to read at reel size, distinctive enough to tell apart mid-spin, and consistent enough that they look like they came from the same place. Getting that by hand for every theme is exactly what I wanted to avoid.

So two agents handle the visual layer: Image-Gen and Background-Gen. They're the shortest story in the pipeline — almost identical code, real results, and one failure mode I still haven't fully solved.

Two agents, one loop

The Designer's spec carries everything the image agents need. Each symbol entry has an icon_prompt — a short text description the Designer wrote to describe that symbol's appearance. The spec also carries a single background_prompt for the full-page background.

Image-Gen reads the spec, loops through every symbol, and for each one calls Stable Image Core on Bedrock with the symbol's prompt. It gets a PNG back, resizes it to 256×256 (the size the reels expect), and writes it into the app's public/images folder. After the icons are approved, it seeds DynamoDB — putting each symbol into a table the game queries at runtime to know which icons to load.

Background-Gen does the same process exactly once, for the background image.

That's the scope. I grouped them in one post because splitting them into two would mean writing the same agent story twice. They share the same architecture, the same failure modes, and the same lessons. The only thing different is the count of outputs.

What runs

The happy path is simple. Stable Image Core returns the image as base64; I decode it, resize it, write it to disk. A thirty-symbol run takes a few minutes.

The resize step looks like a footnote but it isn't. Stable Image Core returns full-resolution PNGs — much larger than the 256×256 the reel layout expects. At native size the images overflow their containers and the game renders incorrectly. No model parameter controls the output dimensions. It's a plain post-download resize call in Python, on every image.

The fallback is deliberate. Image generation calls fail: timeouts, model errors, prompts that get rejected. A single failed icon shouldn't crash a thirty-symbol run. A single broken icon means a known gap you can regenerate later. So the agent catches per-call failures, logs them, substitutes a placeholder image, and keeps going. The build finishes with a gap rather than a crash.

Where it broke

Consistency across a set is the real problem. Each call to the model is independent. It has no memory of the previous call. Generate thirty icons in a loop and you get thirty independent draws from the same distribution — which means thirty different art styles.

The scarab might come back painterly and textured. The ankh lands flat and geometric. The pyramid looks like concept art from a different project. All technically correct against their prompts. None of them obviously from the same game.

You can reduce this with prompt engineering. I added a style string to every call — "pixel art, dark background, warm palette, consistent line weight" — and it narrows the range. Keeping symbol counts lower helps. But I didn't solve it. Icon visual quality stayed the main limitation of the factory. One good icon is a solved problem. A coherent set of thirty isn't, not with independent calls to a generative model.

The games ship looking like themed games. They don't look like art-directed games. That's the honest description of the ceiling.

The path failure was invisible. Generated images land in public/images. The image agents write them correctly. But when deployed under a non-root path, referencing the background in CSS with a plain url('/images/background.png') silently fails — the browser doesn't find the file. No error, no 404 in the console; the CSS rule parses fine, the image just doesn't appear.

The correct way is a JavaScript inline style using process.env.PUBLIC_URL:

style={{ backgroundImage: `url(${process.env.PUBLIC_URL}/images/background.png)` }}
Enter fullscreen mode Exit fullscreen mode

That resolves correctly across deployment paths. The fix is a one-liner. Finding it cost more time than that, because nothing in the output told me where to look. Technically a Builder problem — but discovered while debugging why generated images weren't showing up.

The size mismatch was boring but necessary. Images downloaded fine, the folder looked right, and the game was broken. I had to open the game in a browser to find out something was wrong, then trace back to the image dimensions. The resize step was ten minutes of work; discovering it needed to exist took longer.

What to take from this

Text-to-image models are strong at generating individual images. They're weak at consistency across a batch. That isn't a limitation of any specific model — it's what you get when each call samples independently. If you're generating a set of related images, plan for this before you start.

The options: put a strong, specific style string in every prompt and repeat it verbatim; use a fixed seed if the model supports it; generate one reference image first and use img2img for the rest; or set an explicit acceptance that the set will have variance and decide early what "good enough" looks like. Don't let the consistency problem surface when you're looking at thirty finished icons that don't cohere.

Own the boring post-processing in plain code. Resize, path wiring, fallback handling — none of that belongs in a prompt or a model setting. The model's job is to produce a good image. Your code's job is to put it in the right place at the right size and handle the call that fails. Keep those two responsibilities separate and both stay simple.

The fallback pattern generalizes beyond image generation: in any pipeline loop over independent, replaceable items, a per-item failure is recoverable. Catch it at the item level, substitute something visible (not silent), and let the build finish. A gap you can fill later is better than a crash you have to start over from — as long as QA can see the gaps.


Previous: the Designer agent. Next: the Builder agent.

Top comments (0)