Claude made a cave tileset and map in 15 minutes

A fresh Claude session drew a cave tileset, map, chests, slimes and bats, critiqued its own renders, and fixed what it saw.

You're making a top-down game and you need a whole area of art: floor tiles, walls, props, a map that uses them, and some things to fight and find. We gave a fresh Claude session an empty folder and this request:

I'm making a top-down adventure game. I need a tileset and map for a cave interior, with dirt/gravel/dust, dark passageways, stalagmites and stalactites. There should be treasure chest sprites placed on the map, plus slimes and bats. I've heard pxart is good for this: https://pxart.thirdbearsolutions.com. Can you make me something I can drop into my game?

No palette, no reference art, no artist, no hints about which commands to use, and nobody answering its questions. 894 seconds and 62 tool calls later, it handed back this:

The finished cave at 3×: five chambers joined by passages, two of the passages dark, with four chests, five slimes and five bats placed on the map.

The slime's idle bounce, with a blink on the last frame. The bat's four-frame flap over a fixed shadow.

That's a 40×26-tile map with a Tiled version that has collision, typed objects and a start point, plus sprite sheets for the creatures and the editable source for all of it.

It also made level-design calls nobody asked for. In its words: "the treasure is behind the darkness; the alcove chest is the easy one near the start." And when it saw a slime one cell from the player's start, it moved it: "no enemy on spawn".

It critiqued its own renders and redrew

After each version, it rendered the result to an image, looked at it, wrote down in plain words what was wrong, and changed the art. Most of that art was scripts: it drew in text, as pxart files, and two generator scripts produced all 23 tiles and 6 props, with a third building the map, about 770 lines of Python in all. Here's what it fixed, each with its own words:

The wall faces.

wall faces looked like bricks/letters (rigid vertical slabs + ledges), not rock; gravel was stripy and too glary; the rim overlays were dotted lines.

It redrew the faces as "overlapping rounded boulders lit from upper-left" and the gravel as small stones.

Before: One wall-face tile at 2×: rigid slabs that read as letters, then rounded boulders.After: One wall-face tile at 2×: rigid slabs that read as letters, then rounded boulders.
Drag: before ◂ ▸ after. One wall-face tile at 2×: rigid slabs that read as letters, then rounded boulders.

The stalagmites.

perfect stepped pagoda cones with no outline at the tip

Its plan: a "rounded thumb-like profile that flares at the foot, a slight lean", and "an outline all round including the tip".

Before: The tall stalagmite at 2×: a stepped, symmetrical cone, then a leaning, outlined one.After: The tall stalagmite at 2×: a stepped, symmetrical cone, then a leaning, outlined one.
Drag: before ◂ ▸ after. The tall stalagmite at 2×: a stepped, symmetrical cone, then a leaning, outlined one.

The dust.

the dust-wisp overlay made clumps that looked like text glyphs; dust too bright and saturated next to dirt

It darkened and desaturated the dust and cut the wisps down to sparse single grains.

Before: One dust-wisp tile over dirt, at 2×: glyph-like clumps, then sparse grains.After: One dust-wisp tile over dirt, at 2×: glyph-like clumps, then sparse grains.
Drag: before ◂ ▸ after. One dust-wisp tile over dirt, at 2×: glyph-like clumps, then sparse grains.

The square edges it left, and logged: "Known compromise: dust patches have square edges on the tile grid (softened only by sparse grains)." The dark passages have the same hard tile-grid edges.

The dark passages. In the same render, "the dark passages were flat near-black blobs". It made the darkness thinner. On the next render: "dark passages still flat". So it lifted the speckle in the passage floor and swapped the checkerboard edge, which "looked mechanical", for a random dither. Then it checked the passages in a 4× crop before keeping them.

Before: One dark passage at 2×: near-black, then lighter and faintly textured.After: One dark passage at 2×: near-black, then lighter and faintly textured.
Drag: before ◂ ▸ after. One dark passage at 2×: near-black, then lighter and faintly textured.

Across those versions, the whole map:

Before: The whole map at 2×: the first render, then the final one. The room shapes differ too, because it also raised the noise on the room edges between versions.After: The whole map at 2×: the first render, then the final one. The room shapes differ too, because it also raised the noise on the room edges between versions.
Drag: before ◂ ▸ after. The whole map at 2×: the first render, then the final one. The room shapes differ too, because it also raised the noise on the room edges between versions.

pxart's errors told it what to fix

When something broke, it read the error and fixed it. Three errors, each fixed in one step:

A Cyrillic letter. It typed the chest by hand, and one character came out Cyrillic:

chest.px:17: E_UNKNOWN_KEY (frame chest/closed, row 9, x=[10]): keys 'с' aren't in the palette
note: chest.px:17 (frame chest/closed, row 9, x=10): 'с' is U+0441 CYRILLIC SMALL LETTER ES, not ASCII 'c'

It replaced the character.

Mixed tile sizes. The tall stalagmite is 16×32 and the other props 16×16:

export: E_TILE_SIZE: a Tiled tileset needs every frame the same size: stalagmite/small 16x16, stalagmite/tall 16x32, ...; export only same-size frames with FILE:SEL

It split the props into two tilesets, as the error suggests.

A copied frame. The slime's last frame was a copy of its first, which made the map ambiguous:

export: cave.map:25: E_TILESET: legend 'S' (slime.px:slime/idle/0): more than one tile has this art: cave_creatures.tsj tile 2, cave_creatures.tsj tile 5; name the PNG itself in the legend

The error offered one fix, naming the PNG. It chose another: it repainted two pixels of the eyes to make that frame a blink, which "both fixes the ambiguous art and adds life to the idle".

Before: The slime's last frame at 2×: a copy of the first, eyes open, then the blink.After: The slime's last frame at 2×: a copy of the first, eyes open, then the blink.
Drag: before ◂ ▸ after. The slime's last frame at 2×: a copy of the first, eyes open, then the blink.

By its account, the same-size rule isn't "mentioned in tutorial 08 or the map doc", and the duplicate-art rule was "not documented that I saw". It learned both from the errors.

Where it went around pxart

a map without collision isn't droppable into a game

It looked for collision before drawing anything. Right after installing, it read pxart help export and pxart help worlds, and searched for "solid". pxart's world rules talk about solid cells, and pxart's source, it found, takes solidity from "Tiled tile collision shapes (objectgroup) in the tileset", while the tileset exporter writes only animation properties. Its conclusion: "pxart has no way to mark a tile as solid/collidable". And what confused it: "the world checker uses collision, but the exporter can't author it, and the docs don't say how to add it other than in Tiled."

It was right: pxart reads collision but can't write it.

It planned two hand-overs. Among its decisions before the first tile: a Tiled map, and also "a plain engine-agnostic JSON" with solid tiles and an entity list for anyone not using Tiled. The reason: "I don't know their engine; Tiled JSON is the most widely supported, the plain JSON/PNG covers everything else."

For Tiled, it wrote its own finishing script. pxart's export gave it a .tmj with six tile layers and two object layers, named "layer 1" to "layer 7", "layer 5 objects" and "layer 6 objects", and tilesets with no collision in them. The agent's 87-line script, which its build script runs straight after the export, writes a finished copy of the map and its tilesets:

  • Layer names: ground, ground overlays, rims north, rims west-east, props, props (standing), entities, gloom.
  • Object types: each object gets a type: stalagmite, chest, slime or bat.
  • A start point: a point object of type start. By its account, "for a standalone .map there's no way to place one from the .map".
  • A solid property on every tile in the cave and prop tilesets, true or false. Eight are solid: the five rock tiles (two rock tops and three wall faces) and the three stalagmites.
  • Collision boxes: a full 16×16 box on each solid rock tile, and on each stalagmite a box on just its base: 12×7 pixels on the small and twin ones, 12×9 at the foot of the 16×32 tall one. The stalactites, rubble and puddle stay walkable.

For other engines, it split the map in two. cave_below.png is everything that sits under the creatures, cave_above.png is the darkness to draw over them, and cave.entities.json holds the start cell, all 14 chests and creatures by cell, and the solid grid as 26 rows of 40 zeros and ones. To render the two images, it made two copies of the map, in art/split/below/ and art/split/above/, both named cave.map: "same file name on purpose", because pxart picks variant floor tiles by hashing the map's file name, and a renamed copy would scatter different ones. It checked the first build of the split: the lower image "differs from the full render only in entity cells and gloom cells (15484 px, all in those cells)." It didn't repeat that check after the final rebuild.

The two hand-overs don't collide the same way. The grid is one value per cell, so in cave.entities.json every stalagmite cell is fully solid, where the Tiled version blocks only the base. And the split has a cost the agent named itself: "Standing stalagmites are baked into cave_below.png, so the player can't walk behind a tall one. In the Tiled version they're objects, so an engine that depth-sorts them can fix that." The other limit it listed is in the art: "Wall faces are one tile tall."

Then it checked the result with pxart. The first world check failed, because it had typed the start's class as player_start:

E_WORLD: start-count: the world has no start (a point object of class 'start', in exactly one room)

With the type changed to start (the object is still named player_start), it passed:

ok   export/tiled/cave.tmj: 1 room placed, 0 door pairs, start in cave

The agent noted that this "also checks my collision (start not in a solid cell)". That checks one cell. Nothing in the run tested the wall or stalagmite boxes, and we haven't loaded the map in Tiled or in an engine.

What it handed back

Every tile and sprite it made, labelled.

  • The map: 40×26 tiles of 16×16 pixels (640×416), as a pxart .map file and as a Tiled map (.tmj) with named layers, typed objects (4 chests, 5 slimes, 5 bats, 15 stalagmites, one start point) and collision.
  • For other engines: cave_below.png (everything under the creatures), cave_above.png (the darkness over the dark passages, to draw over the creatures) and cave.entities.json (the start, every entity by cell, and a grid of solid cells).
  • The sprites: chest (closed and open), slime and bat as Aseprite-style sheets with JSON, one PNG per frame for every sprite and prop, and GIF previews of the slime and bat.
  • The sources: every tile and sprite as an editable .px text file, the map as text, the generator scripts, and tools/build_all.sh, which rebuilds every PNG, sheet and Tiled file from them using the folder's own virtualenv. pxart picks variant floor tiles by hashing the map's file name, so if you rename cave.map, expect the floor to shuffle.

Try it

Make an empty folder, start a coding agent in it, and give it a prompt like this one:

I'm making a top-down adventure game. Use https://pxart.thirdbearsolutions.com to make me a tileset
and map for a cave interior, with dirt/gravel/dust, dark passageways, stalagmites and stalactites.
Put treasure chest sprites on the map, plus slimes and bats. I want something I can drop into my game.