joeyagi/tests/tools/compareFrames.py
2026-10-06 16:59:41 -05:00

200 lines
6.8 KiB
Python
Executable file

#!/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)