#!/usr/bin/env python3 # compareFrames.py - compare a frame from Sierra's interpreter with one # from JoeyAGI. Both are mapped to the 16 EGA colours, then compared one # 8x8 text cell at a time. JoeyAGI draws text in its own font, so a cell # where both frames hold a character -- read with each interpreter's font, # captured from the "font" probe -- counts as equal when the characters # and colours match. Every other differing pixel is a real difference. # # Sierra shows its text screen (text.screen, the inventory) in the PC's # 40-column text mode, which DOSBox captures at 360x400 (9x16 cells). # JoeyAGI draws the same screen on the bitmap, so those frames are # compared cell by cell: blank or not, and the colours. # # compareFrames.py ORACLE JOEY [DIFF.png] [--fonts ORACLEFONT JOEYFONT] # prints "real=N text=M" and the cells that differ; exit 1 if any do. import sys from collections import Counter from PIL import Image EGA = [(0, 0, 0), (0, 0, 170), (0, 170, 0), (0, 170, 170), (170, 0, 0), (170, 0, 170), (170, 85, 0), (170, 170, 170), (85, 85, 85), (85, 85, 255), (85, 255, 85), (85, 255, 255), (255, 85, 85), (255, 85, 255), (255, 255, 85), (255, 255, 255)] WIDTH = 320 HEIGHT = 200 TEXT_MODE_SIZE = (360, 400) TEXT_CELL_W = 9 TEXT_CELL_H = 16 CELL = 8 COLS = WIDTH // CELL ROWS = HEIGHT // CELL WINDOW_FRAME = 4 WINDOW_BG = 15 WINDOW_MIN_RUN = 16 UNKNOWN = "?" # The font probe displays these rows from row 0, column 0. FONT_ROWS = ['!"#$&\'()*+,-./0123456789:;<=>?', '@ABCDEFGHIJKLMNOPQRSTUVWXYZ[]^', '_`abcdefghijklmnopqrstuvwxyz{|', '}~'] _nearest = {} def isTextMode(path): return Image.open(path).size == TEXT_MODE_SIZE def egaGrid(im): px = im.load() grid = [] for y in range(im.size[1]): row = [] for x in range(im.size[0]): c = px[x, y] if c not in _nearest: _nearest[c] = min(range(16), key=lambda i: sum((a - b) ** 2 for a, b in zip(EGA[i], c))) row.append(_nearest[c]) grid.append(row) return grid def load(path): # A frame as rows of EGA colour numbers, scaled to 320x200 if need be. im = Image.open(path).convert("RGB") if im.size != (WIDTH, HEIGHT): im = im.resize((WIDTH, HEIGHT), Image.NEAREST) return egaGrid(im) def textCells(grid, cellW, cellH): # Each cell as (blank?, foreground, background): the background is # the cell's commonest colour. cells = [] for cy in range(ROWS): row = [] for cx in range(COLS): colours = Counter(grid[cy * cellH + y][cx * cellW + x] for y in range(cellH) for x in range(cellW)) common = colours.most_common() if len(common) == 1: row.append((True, None, common[0][0])) else: row.append((False, common[1][0], common[0][0])) cells.append(row) return cells def compareTextMode(oraclePath, joeyPath): a = textCells(egaGrid(Image.open(oraclePath).convert("RGB")), TEXT_CELL_W, TEXT_CELL_H) b = textCells(load(joeyPath), CELL, CELL) cells = [(cx, cy) for cy in range(ROWS) for cx in range(COLS) if a[cy][cx] != b[cy][cx]] return 0, len(cells), cells def cellBits(g, cx, cy, bg): return tuple(tuple(1 if g[cy * CELL + y][cx * CELL + x] != bg else 0 for x in range(CELL)) for y in range(CELL)) def cellColours(g, cx, cy): return Counter(g[cy * CELL + y][cx * CELL + x] for y in range(CELL) for x in range(CELL)) def fontTemplates(path): g = load(path) t = {} for r, s in enumerate(FONT_ROWS): for i, ch in enumerate(s): t[cellBits(g, i, r, 0)] = ch return t def readCell(g, cx, cy, templates): # (character, foreground, background) or None when it is not text. colours = cellColours(g, cx, cy) if len(colours) > 2: return None if len(colours) == 1: return (" ", None, next(iter(colours))) (a, na), (b, nb) = colours.most_common() for bg, fg in ((a, b), (b, a)): ch = templates.get(cellBits(g, cx, cy, bg)) if ch is not None: return (ch, fg, bg) return None def compare(a, b, fontA=None, fontB=None): real = 0 text = 0 cells = [] for cy in range(ROWS): for cx in range(COLS): diff = sum(1 for y in range(cy * CELL, cy * CELL + CELL) for x in range(cx * CELL, cx * CELL + CELL) if a[y][x] != b[y][x]) if diff == 0: continue if fontA and fontB: ta = readCell(a, cx, cy, fontA) tb = readCell(b, cx, cy, fontB) if ta and tb: if ta[0] == tb[0] and (ta[0] == " " or ta[1:] == tb[1:]) and ta[2] == tb[2]: continue text += 1 cells.append((cx, cy, ta[0], tb[0])) continue real += diff cells.append((cx, cy)) return real, text, cells def messageWindows(g): # Message windows: a red frame run of at least 16 pixels below white. found = [] for y in range(1, HEIGHT - 1): x = 0 while x < WIDTH: if g[y][x] == WINDOW_FRAME and g[y - 1][x] == WINDOW_BG: x2 = x while x2 < WIDTH and g[y][x2] == WINDOW_FRAME: x2 += 1 if x2 - x >= WINDOW_MIN_RUN: found.append((x, y, x2 - 1)) x = x2 else: x += 1 return found def diffImage(a, b, cells, path): marked = set((c[0], c[1]) for c in cells) out = Image.new("RGB", (WIDTH * 3, HEIGHT)) for y in range(HEIGHT): for x in range(WIDTH): out.putpixel((x, y), EGA[a[y][x]]) out.putpixel((WIDTH + x, y), EGA[b[y][x]]) if a[y][x] != b[y][x] and (x // CELL, y // CELL) in marked: d = (255, 0, 0) else: d = tuple(v // 4 for v in EGA[a[y][x]]) out.putpixel((2 * WIDTH + x, y), d) out.resize((WIDTH * 6, HEIGHT * 2), Image.NEAREST).save(path) if __name__ == "__main__": args = sys.argv[1:] fonts = (None, None) if "--fonts" in args: i = args.index("--fonts") fonts = (fontTemplates(args[i + 1]), fontTemplates(args[i + 2])) del args[i:i + 3] if len(args) < 2: sys.exit("usage: compareFrames.py ORACLE JOEY [DIFF.png] [--fonts ORACLEFONT JOEYFONT]") if isTextMode(args[0]): real, text, cells = compareTextMode(args[0], args[1]) print("text mode: cells=%d %s" % (text, cells[:16])) else: a = load(args[0]) b = load(args[1]) real, text, cells = compare(a, b, fonts[0], fonts[1]) print("real=%d text=%d %s" % (real, text, cells[:16])) if len(args) > 2: diffImage(a, b, cells, args[2]) sys.exit(1 if real or text else 0)