Inspiration
How to Build an Interactive, Self-Generating ASCII Art Museum in Your Terminal Using Claude 3.5 Sonnet
LLMs are notoriously bad at spatial reasoning. Here is how to bypass their tokenisation blind spots to build a gorgeous, dynamically generated ASCII art gallery directly in your command line.
Updated 10/7/2026
Large Language Models are brilliant at poetry, exceptional at debugging, and absolutely, spectacularly terrible at drawing. Ask almost any LLM to draw a simple bicycle in ASCII art, and you will receive a chaotic, multi-car collision of backslashes and semicolons.
This isn't because the model is stupid; it is a fundamental limitation of tokenisation. LLMs do not see characters in a spatial grid; they process tokens sequentially. To an LLM, a line break is just another token, not a physical drop down to the next row of a canvas.
But we can fix that. By forcing spatial awareness onto the model through structured prompting and pairing it with a simple Python terminal wrapper, you can build a self-generating, interactive ASCII art museum. This local terminal application lets you type any exhibition theme (e.g., "1980s retro-futurism" or "deep-sea bioluminescence") and watch as a custom, framed ASCII masterpiece renders live on your screen.
Here is how to make it work, using the spatial reasoning strengths of /platforms/claude.
Why LLMs Fail at ASCII (And the Spatial Grid Fix)
To get decent spatial art out of Claude, we have to stop asking it to "draw" and start asking it to "plot."
When you ask for raw ASCII art, the model attempts to generate text line-by-line from left to right, top to bottom. It cannot "look back" to check if the wheel on line 5 aligns with the frame on line 2.
To bypass this tokenisation blind spot, we instruct the model to think in a strict coordinate system. We force it to map its output to a defined grid (for example, 80 characters wide by 24 characters high) and use a structured system prompt that rewards character alignment.
What makes this particular terminal project tick is the way we bypass the standard text-generation loop. Instead of letting Claude output free-form markdown blocks, we force it to wrap its visual output in structured XML tags.
Setting Up the Python Terminal Museum
To build our museum, we need a lightweight Python application that handles user inputs, makes calls to the Claude API, and renders the output within a beautifully framed terminal window using the standard curses or blessed library.
First, make sure you have the correct dependencies installed:
`bash
pip install anthropic blessed
`
Now, let us write the terminal loop. This script initialises a full-screen terminal canvas, handles the user prompt, queries Claude, and streams the ASCII art back to the terminal inside a retro museum-style frame.
`python
import os
from anthropic import Anthropic
from blessed import Terminal
Initialize the Anthropic client # Make sure your ANTHROPIC_API_KEY environment variable is set client = Anthropic() term = Terminal()
def get_ascii_prompt(user_theme): return f"""You are a master terminal ASCII artist. Your job is to generate a single piece of ASCII art representing the theme: "{user_theme}".
Constraints: 1. Your canvas is exactly 60 characters wide and 18 characters high. 2. Use a mix of high-density and low-density characters for shading (e.g., @, #, 8, &, o, :, *, ., ). 3. Do not include any explanations, introduction, or conversational text. 4. Wrap your final ASCII art inside opening <canvas> and closing </canvas> tags. 5. Ensure every line is exactly the same length, padded with spaces if necessary, to preserve the vertical alignment. """
def fetch_art(theme): prompt = get_ascii_prompt(theme) response = client.messages.create( model="claude-3-5-sonnet-20241022", max_tokens=1500, temperature=0.5, system="You are an expert system that output ONLY raw, perfectly aligned ASCII art within XML tags.", messages=[{"role": "user", "content": prompt}] ) raw_text = response.content[0].text # Extract text between <canvas> tags if "<canvas>" in raw_text and "</canvas>" in raw_text: return raw_text.split("<canvas>")[1].split("</canvas>")[0].strip("\n") return raw_text
def draw_museum_frame(term, art_lines, theme): print(term.clear()) # Draw the museum label print(term.move_xy(5, 2) + term.bold_gold(f"EXHIBIT: {theme.upper()}")) print(term.move_xy(5, 3) + term.gray("─" * 62)) # Draw top frame border print(term.move_xy(4, 5) + term.white("┌" + "─" * 60 + "┐")) # Draw art lines with side borders for i, line in enumerate(art_lines): print(term.move_xy(4, 6 + i) + term.white("│") + term.normal(line.ljust(60)[:60]) + term.white("│")) # Draw bottom frame border print(term.move_xy(4, 6 + len(art_lines)) + term.white("└" + "─" * 60 + "┘")) print(term.move_xy(5, 8 + len(art_lines)) + term.italic_gray("Press any key to request a new exhibit, or 'q' to exit."))
def main(): with term.cbreak(), term.hidden_cursor(): print(term.clear()) print(term.move_xy(5, 5) + "Welcome to the Generative ASCII Museum.") while True: print(term.move_xy(5, 7) + "Enter a theme for the next exhibit: ") theme = input("> ") if theme.lower() == 'q': break print(term.move_xy(5, 9) + term.blue("Generating canvas... (Connecting to Claude)")) try: art_output = fetch_art(theme) art_lines = art_output.split("\n")[:18] # Bound to 18 lines max draw_museum_frame(term, art_lines, theme) except Exception as e: print(term.clear()) print(term.red(f"Error fetching exhibit: {e}")) print("Ensure your API key is configured correctly.") # Wait for user input to loop inp = term.inkey() if inp.lower() == 'q': break
if __name__ == "__main__":
main()
`
Optimising the Prompt for Spatial Coherence
If you find Claude's outputs are still slightly skewed or missing the mark, it is usually a sign of coordinate drift. When troubleshooting output layout, you can consult our dedicated platform guide on /platforms/claude/articles, but for ASCII art specifically, the solution lies in character-density hinting.
LLMs are surprisingly good at understanding shading if you treat characters as pixel brightness values. In your prompts (which you can refine with our /prompts), try supplying the model with a clear lookup table:
(Space) = Empty air / Background.,:,-= Light mist / Ambient reflections*,=,+= Mid-tone textures / Rough surfaces#,%,@= Solid objects / Heavy shadows
By framing the generation as a texture-filling task rather than an outline-drawing task, you prompt the model to think in blocks. This leads to incredibly clean, visually readable terminal art that actually looks like the subject matter, rather than a random collection of punctuation marks. Now, fire up your terminal, type in a prompt like "an ancient cassette tape" or "a cyberpunk rainstorm", and watch the command line transform into your personal low-fi gallery.
Keep going
Build something with the prompt generator, decode the jargon in the glossary, or compare the tools on our platform deep-dives.