Skip to content

Commit

Permalink
Merge pull request #48 from Kautenja/simplify
Browse files Browse the repository at this point in the history
Simplify
  • Loading branch information
Kautenja authored Jan 6, 2019
2 parents 94f8d24 + e2a8548 commit 73daa27
Show file tree
Hide file tree
Showing 29 changed files with 807 additions and 358 deletions.
57 changes: 0 additions & 57 deletions nes_py/_app/play.py

This file was deleted.

1 change: 0 additions & 1 deletion nes_py/_app/visualize/__init__.py

This file was deleted.

63 changes: 0 additions & 63 deletions nes_py/_app/visualize/realtime_plot.py

This file was deleted.

228 changes: 228 additions & 0 deletions nes_py/_rom.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
"""An abstraction of the NES Read-Only Memory (ROM).
Notes:
- http://wiki.nesdev.com/w/index.php/INES
"""
import os
import numpy as np


class ROM(object):
"""An abstraction of the NES Read-Only Memory (ROM)."""

# the magic bytes expected at the first four bytes of the header.
# It spells "NES<END>"
_MAGIC = np.array([0x4E, 0x45, 0x53, 0x1A])

def __init__(self, rom_path):
"""
Initialize a new ROM.
Args:
rom_path (str): the path to the ROM file
Returns:
None
"""
# make sure the rom path is a string
if not isinstance(rom_path, str):
raise TypeError('rom_path must be of type: str.')
# make sure the rom path exists
if not os.path.exists(rom_path):
msg = 'rom_path points to non-existent file: {}.'.format(rom_path)
raise ValueError(msg)
# read the binary data in the .nes ROM file
self.raw_data = np.fromfile(rom_path, dtype='uint8')
# ensure the first 4 bytes are 0x4E45531A (NES<EOF>)
if not np.array_equal(self._magic, self._MAGIC):
raise ValueError('ROM missing magic number in header.')
if self._zero_fill != 0:
raise ValueError("ROM header zero fill bytes are not zero.")

#
# MARK: Header
#

@property
def header(self):
"""Return the header of the ROM file as bytes."""
return self.raw_data[:16]

@property
def _magic(self):
"""Return the magic bytes in the first 4 bytes."""
return self.header[:4]

@property
def prg_rom_size(self):
"""Return the size of the PRG ROM in KB."""
return 16 * self.header[4]

@property
def chr_rom_size(self):
"""Return the size of the CHR ROM in KB."""
return 8 * self.header[5]

@property
def flags_6(self):
"""Return the flags at the 6th byte of the header."""
return '{:08b}'.format(self.header[6])

@property
def flags_7(self):
"""Return the flags at the 7th byte of the header."""
return '{:08b}'.format(self.header[7])

@property
def prg_ram_size(self):
"""Return the size of the PRG RAM in KB."""
size = self.header[8]
# size becomes 8 when it's zero for compatibility
if size == 0:
size = 1

return 8 * size

@property
def flags_9(self):
"""Return the flags at the 9th byte of the header."""
return '{:08b}'.format(self.header[9])

@property
def flags_10(self):
"""
Return the flags at the 10th byte of the header.
Notes:
- these flags are not part of official specification.
- ignored in this emulator
"""
return '{:08b}'.format(self.header[10])

@property
def _zero_fill(self):
"""Return the zero fill bytes at the end of the header."""
return self.header[11:].sum()

#
# MARK: Header Flags
#

@property
def mapper(self):
"""Return the mapper number this ROM uses."""
# the high nibble is in flags 7, the low nibble is in flags 6
return int(self.flags_7[:4] + self.flags_6[:4], 2)

@property
def is_ignore_mirroring(self):
"""Return a boolean determining if the ROM ignores mirroring."""
return bool(int(self.flags_6[4]))

@property
def has_trainer(self):
"""Return a boolean determining if the ROM has a trainer block."""
return bool(int(self.flags_6[5]))

@property
def has_battery_backed_ram(self):
"""Return a boolean determining if the ROM has a battery-backed RAM."""
return bool(int(self.flags_6[6]))

@property
def is_vertical_mirroring(self):
"""Return the mirroring mode this ROM uses."""
return bool(int(self.flags_6[7]))

@property
def has_play_choice_10(self):
"""
Return whether this cartridge uses PlayChoice-10.
Note:
- Play-Choice 10 uses different color palettes for a different PPU
- ignored in this emulator
"""
return bool(int(self.flags_7[6]))

@property
def has_vs_unisystem(self):
"""
Return whether this cartridge has VS Uni-system.
Note:
VS Uni-system is for ROMs that have a coin slot (Arcades).
- ignored in this emulator
"""
return bool(int(self.flags_7[7]))

@property
def is_pal(self):
"""Return the TV system this ROM supports."""
return bool(int(self.flags_9[7]))

#
# MARK: ROM
#

@property
def trainer_rom_start(self):
"""The inclusive starting index of the trainer ROM."""
return 16

@property
def trainer_rom_stop(self):
"""The exclusive stopping index of the trainer ROM."""
if self.has_trainer:
return 16 + 512
else:
return 16

@property
def trainer_rom(self):
"""Return the trainer ROM of the ROM file."""
return self.raw_data[self.trainer_rom_start:self.trainer_rom_stop]

@property
def prg_rom_start(self):
"""The inclusive starting index of the PRG ROM."""
return self.trainer_rom_stop

@property
def prg_rom_stop(self):
"""The exclusive stopping index of the PRG ROM."""
return self.prg_rom_start + self.prg_rom_size * 2**10

@property
def prg_rom(self):
"""Return the PRG ROM of the ROM file."""
try:
return self.raw_data[self.prg_rom_start:self.prg_rom_stop]
except IndexError:
raise ValueError('failed to read PRG-ROM on ROM.')

@property
def chr_rom_start(self):
"""The inclusive starting index of the CHR ROM."""
return self.prg_rom_stop

@property
def chr_rom_stop(self):
"""The exclusive stopping index of the CHR ROM."""
return self.chr_rom_start + self.chr_rom_size * 2**10

@property
def chr_rom(self):
"""Return the CHR ROM of the ROM file."""
try:
return self.raw_data[self.chr_rom_start:self.chr_rom_stop]
except IndexError:
raise ValueError('failed to read CHR-ROM on ROM.')


# explicitly define the outward facing API of this module
__all__ = [ROM.__name__]
File renamed without changes.
22 changes: 12 additions & 10 deletions nes_py/_app/cli.py → nes_py/app/cli.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,10 @@
"""Command line interface to nes-py NES emulator."""
import argparse
from .play import play_human, play_random
from .play_human import play_human
from .play_random import play_random
from ..nes_env import NESEnv


# The play modes for the system
_PLAY_MODES = {
'human': play_human,
'random': play_random
}


def _get_args():
"""Parse arguments from the command line and return them."""
parser = argparse.ArgumentParser(description=__doc__)
Expand All @@ -27,7 +21,12 @@ def _get_args():
choices=['human', 'random'],
help='The execution mode for the emulation.',
)

# add the argument for the number of steps to take in random mode
parser.add_argument('--steps', '-s',
type=int,
default=500,
help='The number of random steps to take.',
)
return parser.parse_args()


Expand All @@ -38,7 +37,10 @@ def main():
# create the environment
env = NESEnv(args.rom)
# play the environment with the given mode
_PLAY_MODES[args.mode](env)
if args.mode == 'human':
play_human(env)
else:
play_random(env, args.steps)


# explicitly define the outward facing API of this module
Expand Down
Loading

0 comments on commit 73daa27

Please sign in to comment.