
Palette Enforcer
- 1 installs
- 1 repo stars
- Updated August 3, 2026
- ar9av/game-exa
palette-enforcer is a game-build pipeline skill that quantizes all generated PNG assets to a single named 8-bit palette so a game's sprites, tiles, and background share one coherent color set.
About
This skill quantizes every generated PNG asset (sprites, tiles, background) to a single named 8-bit color palette so a game's art looks coherent. A developer runs it after the art-generation stages and before code generation. It preserves transparent pixels and the magenta chroma-key background, auto-selects a palette by genre, and writes the chosen palette to game-state.json for downstream skills.
- Quantizes all generated PNGs to one named 8-bit palette for visual coherence
- Preserves transparency and the magenta chroma-key background
- Auto-selects a palette by genre and records state.style.palette
Palette Enforcer by the numbers
- 1 all-time installs (skills.sh)
- Ranked #218 of 247 Game Development skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
palette-enforcer capabilities & compatibility
Free; local image processing with no API calls.
- Capabilities
- tile artist · manifest auditor · palette quantization
- Use cases
- image generation · ui design
- Pricing
- Free
What palette-enforcer says it does
Quantizes all generated PNG assets (sprites, tiles, background) to a named 8-bit color palette to achieve visual coherence.
Transparent pixels (alpha < 128) are preserved; only opaque pixels are quantized.
npx skills add https://github.com/ar9av/game-exa --skill palette-enforcerAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 1 |
| Last updated | August 3, 2026 |
| Repository | ar9av/game-exa ↗ |
What it does
Unify all generated game art to one 8-bit palette for visual coherence before code generation.
Who is it for?
Making AI-generated game art cohesive by enforcing a shared palette.
Skip if: Non-game or non-pixel-art image work.
When should I use this skill?
After art generation and before codesmith, or when the user says 'make the art more cohesive' or 'unify the palette'.
What you get
All PNGs quantized to one palette in-place, with state.style.palette recorded for downstream skills.
- Palette-quantized PNGs in-place
- state.style.palette in game-state.json
By the numbers
- 5 named palettes (sweetie-16, pico8, endesga-32, gameboy, nes)
- gameboy palette has 4 colors
Files
Palette Enforcer — Visual Coherence
Applies color palette constraints to all generated PNG assets so every sprite, tile, and background shares the same enumerated color set. Without this, GPT Image 2 returns arbitrary colors and assets from the same game look like they came from different games.
When to use
After all art generation (sprite-artist, tile-artist, bg-artist) and before codesmith. Also run if the user says "make the art more cohesive" or "unify the palette."
Palettes available
| ID | Colors | Best for |
|---|---|---|
sweetie-16 | 16 | Default — friendly, any genre |
pico8 | 16 | Bold retro, shoot-em-ups, platformers |
endesga-32 | 32 | RPGs, dungeon crawlers, rich detail |
gameboy | 4 | Maximum constraint, mono-theme games |
nes | 16 | Classic NES aesthetic |
Output contract
- All PNGs in
public/assets/are overwritten in-place with palette-quantized versions. game-state.jsongainsstate.style.palettewith the chosen palette ID.- Transparent pixels (alpha < 128) are preserved; only opaque pixels are quantized.
- The magenta chroma-key background (#FF00FF) is kept as-is — it's stripped later at runtime.
// game-state.json after running
{
"style": {
"palette": "sweetie-16"
}
}Process
1. Choose palette: use --palette <id> arg, or auto-select based on genre:
platformer/action-platformer→pico8top-down-rpg/dungeon-crawler→endesga-32shoot-em-up→pico8- default →
sweetie-16
2. Run scripts/enforce_palette.mjs <project-dir> [--palette <id>] 3. Script processes all PNGs found in public/assets/ and writes them back. 4. Writes state.style.palette to game-state.json.
Scripts
scripts/enforce_palette.mjs <project-dir> [--palette <id>]— quantize all assets.
References
references/palettes.json— all palette definitions with hex color arrays.
{
"sweetie-16": {
"name": "Sweetie 16",
"description": "Modern 16-color palette, friendly and vibrant. Good default for any genre.",
"colors": [
"#1a1c2c", "#5d275d", "#b13e53", "#ef7d57",
"#ffcd75", "#a7f070", "#38b764", "#257179",
"#29366f", "#3b5dc9", "#41a6f6", "#73eff7",
"#f4f4f4", "#94b0c2", "#566c86", "#333c57"
]
},
"pico8": {
"name": "PICO-8",
"description": "16-color palette from the PICO-8 fantasy console. Bold, saturated, iconic.",
"colors": [
"#000000", "#1D2B53", "#7E2553", "#008751",
"#AB5236", "#5F574F", "#C2C3C7", "#FFF1E8",
"#FF004D", "#FFA300", "#FFEC27", "#00E436",
"#29ADFF", "#83769C", "#FF77A8", "#FFCCAA"
]
},
"endesga-32": {
"name": "Endesga 32",
"description": "32-color palette with rich shadows and saturated highlights. Great for RPGs and platformers.",
"colors": [
"#be4a2f", "#d77643", "#ead4aa", "#e4a672",
"#b86f50", "#733e39", "#3e2731", "#a22633",
"#e43b44", "#f77622", "#feae34", "#fee761",
"#63c74d", "#3e8948", "#265c42", "#193c3e",
"#124e89", "#0099db", "#2ce8f5", "#ffffff",
"#c0cbdc", "#8b9bb4", "#5a6988", "#3a4466",
"#262b44", "#181425", "#ff0044", "#68386c",
"#b55088", "#f6757a", "#e8b796", "#c28569"
]
},
"gameboy": {
"name": "Game Boy",
"description": "4-color Game Boy palette. Maximum constraint, instantly recognizable as retro.",
"colors": [
"#0f380f", "#306230", "#8bac0f", "#9bbc0f"
]
},
"nes": {
"name": "NES-like",
"description": "16-color NES-inspired palette. Authentic 8-bit feel with earth tones and bright primaries.",
"colors": [
"#000000", "#fcfcfc", "#f8f8f8", "#bcbcbc",
"#7c7c7c", "#a40000", "#fc0000", "#fc7460",
"#fc9838", "#f8b800", "#00a800", "#00fc00",
"#0058f8", "#3cbcfc", "#9878f8", "#f878f8"
]
}
}
#!/usr/bin/env node
// Quantize all PNGs in public/assets/ to a named 8-bit palette.
// Usage: node enforce_palette.mjs <project-dir> [--palette <id>]
import { readFile, writeFile, readdir } from 'node:fs/promises';
import { resolve, join, extname } from 'node:path';
import sharp from 'sharp';
const args = process.argv.slice(2);
const projectDir = resolve(args.find((a) => !a.startsWith('--')) ?? '.');
const paletteFlagIdx = args.indexOf('--palette');
let paletteId = paletteFlagIdx >= 0 ? args[paletteFlagIdx + 1] : null;
const state = JSON.parse(await readFile(join(projectDir, 'game-state.json'), 'utf8'));
const genre = state.gdd?.genre ?? '';
if (!paletteId) {
if (['platformer', 'action-platformer', 'shoot-em-up'].includes(genre)) paletteId = 'pico8';
else if (['top-down-rpg', 'dungeon-crawler'].includes(genre)) paletteId = 'endesga-32';
else paletteId = 'sweetie-16';
}
const palettesPath = new URL('../references/palettes.json', import.meta.url).pathname;
const palettes = JSON.parse(await readFile(palettesPath, 'utf8'));
if (!palettes[paletteId]) {
console.error(`Unknown palette "${paletteId}". Available: ${Object.keys(palettes).join(', ')}`);
process.exit(3);
}
const paletteHex = palettes[paletteId].colors;
// Parse hex → [r, g, b]
const palette = paletteHex.map((h) => {
const n = parseInt(h.replace('#', ''), 16);
return [(n >> 16) & 0xff, (n >> 8) & 0xff, n & 0xff];
});
// Euclidean distance squared in RGB space (no sqrt needed for comparison)
function nearest(r, g, b) {
let bestDist = Infinity;
let bestColor = palette[0];
for (const c of palette) {
const d = (r - c[0]) ** 2 + (g - c[1]) ** 2 + (b - c[2]) ** 2;
if (d < bestDist) { bestDist = d; bestColor = c; }
}
return bestColor;
}
const assetsDir = join(projectDir, 'public', 'assets');
const files = await readdir(assetsDir);
const pngs = files.filter((f) => extname(f).toLowerCase() === '.png');
let processed = 0;
for (const file of pngs) {
const fullPath = join(assetsDir, file);
const img = sharp(fullPath).ensureAlpha();
const { data, info } = await img.raw().toBuffer({ resolveWithObject: true });
const { width, height, channels } = info; // channels = 4 (RGBA)
for (let i = 0; i < data.length; i += channels) {
const a = data[i + 3];
if (a < 128) continue; // keep transparent pixels unchanged
const r = data[i], g = data[i + 1], b = data[i + 2];
// Preserve magenta chroma-key background — used by the runtime for alpha extraction.
if (r === 255 && g === 0 && b === 255) continue;
const [nr, ng, nb] = nearest(r, g, b);
data[i] = nr; data[i + 1] = ng; data[i + 2] = nb;
}
await sharp(data, { raw: { width, height, channels } })
.png()
.toFile(fullPath);
processed++;
console.error(`[palette-enforcer] ${file} → ${paletteId}`);
}
// Write state.style.palette
state.style = { ...(state.style ?? {}), palette: paletteId };
await writeFile(join(projectDir, 'game-state.json'), JSON.stringify(state, null, 2));
console.log(JSON.stringify({
ok: true,
palette: paletteId,
colors: paletteHex.length,
filesProcessed: processed,
}));
Related skills
FAQ
Which palettes are available?
sweetie-16, pico8, endesga-32, gameboy, and nes, with auto-selection by genre.
Does it preserve transparency?
Yes, transparent pixels (alpha < 128) are preserved and only opaque pixels are quantized.