#!/usr/bin/env python3 # Generate src/color.asm: CGB palette data + the lookup tables the terminal # uses to render per-glyph FOREGROUND *and* BACKGROUND color. # # The 7 colors are the points of a FANO PLANE (Steiner triple system S(2,3,7)): # 7 palettes = 7 lines, and every PAIR of colors lies on exactly one line, so a # tile that needs at most two distinct (non-black) colors always has a palette # holding both. slot0 of every palette is black, so a black background costs no # color id; a colored background is drawn by giving the cell's empty pixels the # background color's slot instead of slot0. # # Because a tile packs two independent cells (each with fg+bg) it can need up to # 3 non-black colors. Every color pair is covered exactly; a 3-color tile is # covered only if that triple happens to be a line - otherwise BestPal picks the # line covering the most of them and the odd one out is approximated. # # Color index: 0=black(bg/none) 1=red 2=green 3=yellow 4=blue 5=magenta 6=cyan # 7=white. BG palette 1 is reserved for the on-screen keyboard. def gbc15(r, g, b): return r | (g << 5) | (b << 10) COLOR_RGB = { 1: (31, 0, 0), # red 2: ( 0, 31, 0), # green 3: (31, 31, 0), # yellow 4: ( 0, 0, 31), # blue 5: (31, 0, 31), # magenta 6: ( 0, 31, 31), # cyan 7: (31, 31, 31), # white } # Hardware BG palette -> its 3 colors in slot1,slot2,slot3 order (slot0=black). # The 7 triples form a Fano plane over colors 1..7. Palette 0 keeps white at # slot1 (the OSK and plain white text draw with color-id 1). PALETTES = { 0: [7, 1, 6], 2: [1, 2, 3], 3: [1, 4, 5], 4: [2, 4, 6], 5: [2, 5, 7], 6: [3, 4, 7], 7: [3, 5, 6], } PAL_ORDER = [0, 2, 3, 4, 5, 6, 7] # --- verify Fano: every pair of 1..7 lies on exactly one line ------------------ seen = {} for pal, line in PALETTES.items(): assert len(set(line)) == 3 for i in range(3): for j in range(i + 1, 3): k = frozenset((line[i], line[j])) assert k not in seen, ("pair on two lines", k) seen[k] = pal assert set(seen) == {frozenset((a, b)) for a in range(1, 8) for b in range(1, 8) if a != b} def slot_of(pal, color): """id (0..3) of color in palette pal. black->0; absent->1 (visible fallback).""" if color == 0: return 0 line = PALETTES.get(pal, []) return (line.index(color) + 1) if color in line else 1 def best_pal(mask): """Palette covering the most colors present in the 7-bit set 'mask'.""" if mask == 0: return 0 best, bestn = 0, -1 for pal in PAL_ORDER: n = sum(1 for c in PALETTES[pal] if mask & (1 << (c - 1))) if n > bestn: best, bestn = pal, n return best # --- emit ---------------------------------------------------------------------- def pal_colors(hw): if hw == 1: # OSK: white bg, black ink (inverse) return [(31,31,31), (0,0,0), (0,0,0), (0,0,0)] return [(0,0,0)] + [COLOR_RGB[c] for c in PALETTES[hw]] L = [] L.append("; auto-generated by tools/gencolor.py - Fano-plane color tables") L.append("; (foreground + background). DO NOT EDIT.") L.append('SECTION "color", ROM0') L.append("") L.append("; 8 CGB BG palettes, 4 colors each (c0=black). Loaded at BCPS $80.") L.append("PalData::") for hw in range(8): words = [] for (r, g, b) in pal_colors(hw): v = gbc15(r, g, b) words.append("$%02X,$%02X" % (v & 0xFF, (v >> 8) & 0xFF)) L.append(" db " + ",".join(words) + " ; palette %d" % hw) L.append("") L.append("; BitTab[color] = bit for that color in the 'colors present' set (black=0)") L.append("BitTab::") L.append(" db " + ",".join("$%02X" % (0 if c == 0 else (1 << (c - 1))) for c in range(8))) L.append("") L.append("; BestPal[set] = palette (0..7) covering the most colors in a 7-bit set") L.append("BestPal::") for base in range(0, 128, 16): L.append(" db " + ",".join("$%02X" % best_pal(base + j) for j in range(16)) + " ; %d..%d" % (base, base + 15)) L.append("") L.append("; SlotOf[palette*8 + color] = that color's slot/id (0..3) in the palette") L.append("SlotOf::") for pal in range(8): L.append(" db " + ",".join("$%02X" % slot_of(pal, c) for c in range(8)) + " ; palette %d" % pal) open("/home/user/dev/gbos/src/color.asm", "w").write("\n".join(L) + "\n") print("wrote src/color.asm")