view nes.py @ 101:822e2fb5197c

refactor: move the NES ROM manipulation code into its own module Also, rename the color module to colour 'cause I got tired of misspelling it when I didn't have to.
author Jordi Gutiérrez Hermoso <jordigh@octave.org>
date Sat, 14 Sep 2019 23:33:01 -0400
parents
children d5e69fe5c81a
line wrap: on
line source

from PyQt5 import QtGui as QG

from colours import TILE_PALETTES, palette_to_qt


class NES_ROM(object):
    def __init__(self, filename):
        self.filename = filename
        self.read_rom()

    def read_rom(self):
        with open(self.filename, 'rb') as f:
            self.ines = f.read()

        self.header = self.ines[0:16]
        body = self.ines[16:]
        PRG_size = self.header[4]*16384
        CHR_size = self.header[5]*8192

        self.PRG = body[0:PRG_size]
        if CHR_size == 0:
            # No chr, just read the whole ROM as tiles
            tile_data = body
            self.CHR = None
        else:
            self.CHR = body[PRG_size:PRG_size+CHR_size]
            tile_data = self.CHR

        self.tiles = [Tile(tile_data[i:i+16]) for i in range(0, len(tile_data), 16)]


class Tile(object):

    default_palette = TILE_PALETTES["Primary"]

    def __init__(self, raw_tile):
        super().__init__()

        self.raw_tile = raw_tile
        self.set_palette(self.default_palette.copy())

    def __repr__(self):
        num_to_ascii = {0: " ", 1: ".", 2: "o", 3: "#"}
        return "_"*10 + "\n" + "\n".join(
            "[" + "".join([num_to_ascii[val] for val in row]) + "]"
            for row in self.tile
        ) + "\n" + "-"*10

    def set_palette(self, new_palette):
        self.palette = new_palette.copy()
        self.update_pixmap()

    def update_pixmap(self):
        img_data = bytes(sum(self.tile, []))
        image = QG.QImage(img_data, 8, 8, QG.QImage.Format_Indexed8)
        image.setColorTable(palette_to_qt(self.palette))
        self.pixmap = QG.QPixmap(image)

    def get_tile(self):
        if getattr(self, "_tile", None) is None:
            self._tile = self.parse_tile(self.raw_tile)
        return self._tile

    def set_tile(self, tile):
        del self._tile
        self.raw_tile = self.unparse_tile(tile)

        self.update_pixmap()

    def del_tile(self):
        del self._tile
        del self.raw_tile

    tile = property(get_tile, set_tile, del_tile, "A tile parsed as an 8x8 array of ints")

    @classmethod
    def parse_tile(cls, tile):
        """Given a raw tile's bytes, convert it to an 8x8 array of integers."""
        lowplane, hiplane = tile[0:8], tile[8:16]
        rows = []
        for lowbyte, hibyte in zip(lowplane, hiplane):
            row = [
                ((lowbyte >> idx) & 1) + 2*((hibyte >> idx) & 1)
                for idx in range(7, -1, -1)
            ]
            rows.append(row)
        return rows

    @classmethod
    def unparse_tile(cls, rows):
        """Given an 8x8 array of integers, convert it to a raw tile's bytes."""
        lowplane = bytes([int(''.join(str(num & 1) for num in row), 2) for row in rows])
        hiplane = bytes([int(''.join(str((num & 2) >> 1) for num in row), 2) for row in rows])
        return lowplane + hiplane