├── nes_py ├── tests │ ├── games │ │ ├── blank │ │ ├── empty.nes │ │ ├── excitebike.nes │ │ ├── super-mario-bros-1.nes │ │ ├── super-mario-bros-2.nes │ │ ├── super-mario-bros-3.nes │ │ ├── the-legend-of-zelda.nes │ │ └── super-mario-bros-lost-levels.nes │ ├── __init__.py │ ├── rom_file_abs_path.py │ ├── test_multiple_makes.py │ ├── test_nes_env.py │ └── test_rom.py ├── app │ ├── __init__.py │ ├── play_random.py │ ├── cli.py │ └── play_human.py ├── __init__.py ├── wrappers │ ├── __init__.py │ └── joypad_space.py ├── nes │ ├── include │ │ ├── log.hpp │ │ ├── common.hpp │ │ ├── palette.hpp │ │ ├── mapper_factory.hpp │ │ ├── controller.hpp │ │ ├── cartridge.hpp │ │ ├── mappers │ │ │ ├── mapper_UxROM.hpp │ │ │ ├── mapper_CNROM.hpp │ │ │ ├── mapper_NROM.hpp │ │ │ └── mapper_SxROM.hpp │ │ ├── picture_bus.hpp │ │ ├── mapper.hpp │ │ ├── emulator.hpp │ │ ├── main_bus.hpp │ │ ├── cpu_opcodes.hpp │ │ ├── cpu.hpp │ │ └── ppu.hpp │ ├── src │ │ ├── controller.cpp │ │ ├── mappers │ │ │ ├── mapper_CNROM.cpp │ │ │ ├── mapper_NROM.cpp │ │ │ ├── mapper_UxROM.cpp │ │ │ └── mapper_SxROM.cpp │ │ ├── cartridge.cpp │ │ ├── lib_nes_env.cpp │ │ ├── emulator.cpp │ │ ├── picture_bus.cpp │ │ ├── main_bus.cpp │ │ ├── ppu.cpp │ │ └── cpu.cpp │ └── SConstruct ├── _image_viewer.py ├── _rom.py └── nes_env.py ├── MANIFEST.in ├── .clang_complete ├── requirements.txt ├── __main__.py ├── scripts └── run.py ├── speedtest.py ├── backup_restore.py ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── PULL_REQUEST_TEMPLATE.md ├── makefile ├── LICENSE ├── setup.py ├── .gitignore ├── .travis.yml └── README.md /nes_py/tests/games/blank: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /nes_py/tests/games/empty.nes: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | graft nes_py/nes 2 | -------------------------------------------------------------------------------- /.clang_complete: -------------------------------------------------------------------------------- 1 | -Ines_py/nes/include 2 | -------------------------------------------------------------------------------- /nes_py/tests/__init__.py: -------------------------------------------------------------------------------- 1 | """Test cases for the parent package.""" 2 | -------------------------------------------------------------------------------- /nes_py/app/__init__.py: -------------------------------------------------------------------------------- 1 | """The application code for running this package from the command line.""" 2 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | gym>=0.17.2 2 | numpy>=1.18.5 3 | pyglet<=1.5.21,>=1.4.0 4 | tqdm>=4.48.2 5 | twine>=1.11.0 6 | -------------------------------------------------------------------------------- /nes_py/tests/games/excitebike.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kautenja/nes-py/HEAD/nes_py/tests/games/excitebike.nes -------------------------------------------------------------------------------- /nes_py/tests/games/super-mario-bros-1.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kautenja/nes-py/HEAD/nes_py/tests/games/super-mario-bros-1.nes -------------------------------------------------------------------------------- /nes_py/tests/games/super-mario-bros-2.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kautenja/nes-py/HEAD/nes_py/tests/games/super-mario-bros-2.nes -------------------------------------------------------------------------------- /nes_py/tests/games/super-mario-bros-3.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kautenja/nes-py/HEAD/nes_py/tests/games/super-mario-bros-3.nes -------------------------------------------------------------------------------- /nes_py/tests/games/the-legend-of-zelda.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kautenja/nes-py/HEAD/nes_py/tests/games/the-legend-of-zelda.nes -------------------------------------------------------------------------------- /nes_py/tests/games/super-mario-bros-lost-levels.nes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Kautenja/nes-py/HEAD/nes_py/tests/games/super-mario-bros-lost-levels.nes -------------------------------------------------------------------------------- /__main__.py: -------------------------------------------------------------------------------- 1 | """The main script for development testing.""" 2 | from nes_py.app.cli import main 3 | 4 | 5 | # execute the main entry point of the CLI 6 | main() 7 | -------------------------------------------------------------------------------- /nes_py/__init__.py: -------------------------------------------------------------------------------- 1 | """The nes-py NES emulator for Python 2 & 3.""" 2 | from .nes_env import NESEnv 3 | 4 | 5 | # explicitly define the outward facing API of this package 6 | __all__ = [NESEnv.__name__] 7 | -------------------------------------------------------------------------------- /nes_py/wrappers/__init__.py: -------------------------------------------------------------------------------- 1 | """Wrappers for altering the functionality of the game.""" 2 | from .joypad_space import JoypadSpace 3 | 4 | 5 | # explicitly define the outward facing API of this package 6 | __all__ = [JoypadSpace.__name__] 7 | -------------------------------------------------------------------------------- /scripts/run.py: -------------------------------------------------------------------------------- 1 | from nes_py import NESEnv 2 | import tqdm 3 | env = NESEnv('./nes_py/tests/games/super-mario-bros-1.nes') 4 | 5 | done = True 6 | 7 | try: 8 | for _ in tqdm.tqdm(range(5000)): 9 | if done: 10 | state = env.reset() 11 | done = False 12 | else: 13 | state, reward, done, info = env.step(env.action_space.sample()) 14 | except KeyboardInterrupt: 15 | pass 16 | -------------------------------------------------------------------------------- /speedtest.py: -------------------------------------------------------------------------------- 1 | from nes_py import NESEnv 2 | import tqdm 3 | env = NESEnv('./nes_py/tests/games/super-mario-bros-1.nes') 4 | 5 | done = True 6 | 7 | try: 8 | for _ in tqdm.tqdm(range(5000)): 9 | if done: 10 | state = env.reset() 11 | done = False 12 | else: 13 | state, reward, done, info = env.step(env.action_space.sample()) 14 | except KeyboardInterrupt: 15 | pass 16 | -------------------------------------------------------------------------------- /backup_restore.py: -------------------------------------------------------------------------------- 1 | from nes_py import NESEnv 2 | import tqdm 3 | env = NESEnv('./nes_py/tests/games/super-mario-bros-1.nes') 4 | 5 | done = True 6 | 7 | try: 8 | for i in tqdm.tqdm(range(5000)): 9 | if done: 10 | state = env.reset() 11 | done = False 12 | else: 13 | state, reward, done, info = env.step(env.action_space.sample()) 14 | if (i + 1) % 12: 15 | env._backup() 16 | if (i + 1) % 27: 17 | env._restore() 18 | except KeyboardInterrupt: 19 | pass 20 | -------------------------------------------------------------------------------- /nes_py/nes/include/log.hpp: -------------------------------------------------------------------------------- 1 | // Program: nes-py 2 | // File: log.hpp 3 | // Description: Logging utilities for the project 4 | // 5 | // Copyright (c) 2019 Christian Kauten. All rights reserved. 6 | // 7 | 8 | #ifndef LOG_HPP 9 | #define LOG_HPP 10 | 11 | #include 12 | 13 | #define debug_disabled true 14 | 15 | #define LOG(level) \ 16 | if (debug_disabled) {} \ 17 | else std::cerr 18 | 19 | enum Level { 20 | None, 21 | Error, 22 | Info, 23 | InfoVerbose, 24 | CpuTrace 25 | }; 26 | 27 | #endif // LOG_HPP 28 | -------------------------------------------------------------------------------- /nes_py/nes/src/controller.cpp: -------------------------------------------------------------------------------- 1 | // Program: nes-py 2 | // File: controller.cpp 3 | // Description: This class houses the logic and data for an NES controller 4 | // 5 | // Copyright (c) 2019 Christian Kauten. All rights reserved. 6 | // 7 | 8 | #include "controller.hpp" 9 | 10 | namespace NES { 11 | 12 | NES_Byte Controller::read() { 13 | NES_Byte ret; 14 | if (is_strobe) { 15 | ret = (joypad_buttons & 1); 16 | } else { 17 | ret = (joypad_bits & 1); 18 | joypad_bits >>= 1; 19 | } 20 | return ret | 0x40; 21 | } 22 | 23 | } // namespace NES 24 | -------------------------------------------------------------------------------- /nes_py/nes/src/mappers/mapper_CNROM.cpp: -------------------------------------------------------------------------------- 1 | // Program: nes-py 2 | // File: mapper_CNROM.cpp 3 | // Description: An implementation of the CNROM mapper 4 | // 5 | // Copyright (c) 2019 Christian Kauten. All rights reserved. 6 | // 7 | 8 | #include "mappers/mapper_CNROM.hpp" 9 | #include "log.hpp" 10 | 11 | namespace NES { 12 | 13 | void MapperCNROM::writeCHR(NES_Address address, NES_Byte value) { 14 | LOG(Info) << 15 | "Read-only CHR memory write attempt at " << 16 | std::hex << 17 | address << 18 | std::endl; 19 | } 20 | 21 | } // namespace NES 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this package 4 | 5 | --- 6 | 7 | ### Is your feature request related to a problem? Please describe. 8 | 9 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 10 | 11 | ### Describe the solution you'd like 12 | 13 | A clear and concise description of what you want to happen. 14 | 15 | ### Describe alternatives you've considered 16 | 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | ### Additional context 20 | 21 | Add any other context (e.g. notes, code snippets, figures) about the feature request here. 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | 5 | --- 6 | 7 | ### Describe the bug 8 | 9 | A clear and concise description of what the bug is. 10 | 11 | ### To Reproduce 12 | 13 | Steps to reproduce the behavior: 14 | 15 | 1. foo 16 | 2. bar 17 | 18 | ### Expected behavior 19 | 20 | A clear and concise description of what you expected to happen. 21 | 22 | ### Screenshots 23 | 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | ### Environment 27 | 28 | - Operating System: 29 | - Python version: 30 | - C++ compiler and version: 31 | 32 | ### Additional context 33 | 34 | Add any other context about the problem here. 35 | -------------------------------------------------------------------------------- /nes_py/nes/include/common.hpp: -------------------------------------------------------------------------------- 1 | // Program: nes-py 2 | // File: common.hpp 3 | // Description: This file defines common types used in the project 4 | // 5 | // Copyright (c) 2019 Christian Kauten. All rights reserved. 6 | // 7 | 8 | #ifndef COMMON_HPP 9 | #define COMMON_HPP 10 | 11 | // resolve an issue with MSVC overflow during compilation (Windows) 12 | #define _CRT_DECLARE_NONSTDC_NAMES 0 13 | #include 14 | 15 | namespace NES { 16 | 17 | /// A shortcut for a byte 18 | typedef uint8_t NES_Byte; 19 | /// A shortcut for a memory address (16-bit) 20 | typedef uint16_t NES_Address; 21 | /// A shortcut for a single pixel in memory 22 | typedef uint32_t NES_Pixel; 23 | 24 | } // namespace NES 25 | 26 | #endif // COMMON_HPP 27 | -------------------------------------------------------------------------------- /makefile: -------------------------------------------------------------------------------- 1 | # build everything 2 | all: test deployment 3 | 4 | # build the LaiNES CPP code 5 | lib_nes_env: 6 | scons -C nes_py/nes 7 | mv nes_py/nes/lib_nes_env*.so nes_py 8 | 9 | # run the Python test suite 10 | test: lib_nes_env 11 | python3 -m unittest discover . 12 | 13 | # clean the build directory 14 | clean: 15 | rm -rf build/ dist/ .eggs/ *.egg-info/ || true 16 | find . -name "*.pyc" -delete 17 | find . -name "__pycache__" -delete 18 | find . -name ".sconsign.dblite" -delete 19 | find . -name "build" | rm -rf 20 | find . -name "lib_nes_env.so" -delete 21 | 22 | # build the deployment package 23 | deployment: clean 24 | python3 setup.py sdist bdist_wheel 25 | 26 | # ship the deployment package to PyPi 27 | ship: test deployment 28 | twine upload dist/* 29 | -------------------------------------------------------------------------------- /nes_py/nes/SConstruct: -------------------------------------------------------------------------------- 1 | """The compilation script for this project using SCons.""" 2 | from os import environ 3 | 4 | 5 | # create a separate build directory 6 | VariantDir('build', 'src', duplicate=0) 7 | 8 | 9 | # the compiler and linker flags for the C++ environment 10 | FLAGS = [ 11 | '-std=c++1y', 12 | '-O3', 13 | '-pipe', 14 | ] 15 | 16 | 17 | # Create the C++ environment 18 | ENV = Environment( 19 | ENV=environ, 20 | CXX='g++', 21 | CPPFLAGS=['-Wno-unused-value'], 22 | CXXFLAGS=FLAGS, 23 | LINKFLAGS=FLAGS, 24 | CPPPATH=['#include'], 25 | ) 26 | 27 | 28 | # Locate all the C++ source files 29 | SRC = Glob('build/*.cpp') + Glob('build/*/*.cpp') 30 | # Create a shared library (it will add "lib" to the front automatically) 31 | ENV.SharedLibrary('_nes_env.so', SRC) 32 | -------------------------------------------------------------------------------- /nes_py/tests/rom_file_abs_path.py: -------------------------------------------------------------------------------- 1 | """A method to get absolute paths of game ROMs.""" 2 | import os 3 | 4 | 5 | def rom_file_abs_path(file_name): 6 | """ 7 | Return the absolute path to a ROM in the games directory. 8 | 9 | Args: 10 | file_name (str): the name of the ROM in the games directory to fetch 11 | 12 | Returns (str): 13 | the absolute path to the given ROM filename in the games directory 14 | 15 | """ 16 | # the directory of this file 17 | dir_path = os.path.dirname(os.path.realpath(__file__)) 18 | # the absolute path to the given ROM file 19 | game_path = '{}/games/{}'.format(dir_path, file_name) 20 | 21 | return game_path 22 | 23 | 24 | # explicitly define the outward facing API of this module 25 | __all__ = [rom_file_abs_path.__name__] 26 | -------------------------------------------------------------------------------- /nes_py/app/play_random.py: -------------------------------------------------------------------------------- 1 | """Methods for playing the game randomly, or as a human.""" 2 | from tqdm import tqdm 3 | 4 | 5 | def play_random(env, steps): 6 | """ 7 | Play the environment making uniformly random decisions. 8 | 9 | Args: 10 | env (gym.Env): the initialized gym environment to play 11 | steps (int): the number of random steps to take 12 | 13 | Returns: 14 | None 15 | 16 | """ 17 | try: 18 | done = True 19 | progress = tqdm(range(steps)) 20 | for _ in progress: 21 | if done: 22 | _ = env.reset() 23 | action = env.action_space.sample() 24 | _, reward, done, info = env.step(action) 25 | progress.set_postfix(reward=reward, info=info) 26 | env.render() 27 | except KeyboardInterrupt: 28 | pass 29 | # close the environment 30 | env.close() 31 | 32 | 33 | # explicitly define the outward facing API of this module 34 | __all__ = [play_random.__name__] 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Christian Kauten 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /nes_py/nes/include/palette.hpp: -------------------------------------------------------------------------------- 1 | // Program: nes-py 2 | // File: palette.hpp 3 | // Description: This file describes the color palette for RGB conversion 4 | // 5 | // Copyright (c) 2019 Christian Kauten. All rights reserved. 6 | // 7 | 8 | #ifndef PALETTE_HPP 9 | #define PALETTE_HPP 10 | 11 | #include "common.hpp" 12 | 13 | namespace NES { 14 | 15 | // Colors in xRGB 16 | // http://www.thealmightyguru.com/Games/Hacking/Wiki/index.php/NES_Palette 17 | const NES_Pixel PALETTE[] = { 18 | 0x7C7C7C, 0x0000FC, 0x0000BC, 0x4428BC, 0x940084, 0xA80020, 0xA81000, 0x881400, 19 | 0x503000, 0x007800, 0x006800, 0x005800, 0x004058, 0x000000, 0x000000, 0x000000, 20 | 0xBCBCBC, 0x0078F8, 0x0058F8, 0x6844FC, 0xD800CC, 0xE40058, 0xF83800, 0xE45C10, 21 | 0xAC7C00, 0x00B800, 0x00A800, 0x00A844, 0x008888, 0x000000, 0x000000, 0x000000, 22 | 0xF8F8F8, 0x3CBCFC, 0x6888FC, 0x9878F8, 0xF878F8, 0xF85898, 0xF87858, 0xFCA044, 23 | 0xF8B800, 0xB8F818, 0x58D854, 0x58F898, 0x00E8D8, 0x787878, 0x000000, 0x000000, 24 | 0xFCFCFC, 0xA4E4FC, 0xB8B8F8, 0xD8B8F8, 0xF8B8F8, 0xF8A4C0, 0xF0D0B0, 0xFCE0A8, 25 | 0xF8D878, 0xD8F878, 0xB8F8B8, 0xB8F8D8, 0x00FCFC, 0xF8D8F8, 0x000000, 0x000000 26 | }; 27 | 28 | } // namespace NES 29 | 30 | #endif // PALETTE_HPP 31 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change. 4 | 5 | - Fixes # 6 | 7 | ### Type of change 8 | 9 | Please select all relevant options: 10 | 11 | - [ ] Bug fix (non-breaking change which fixes an issue) 12 | - [ ] New feature (non-breaking change which adds functionality) 13 | 14 | ### How Has This Been Tested? 15 | 16 | Please describe the tests that you ran to verify your changes. Provide instructions so we can reproduce. Please also list any relevant details for your test configuration 17 | 18 | - [ ] Test A 19 | - [ ] Test B 20 | 21 | ### Test Configuration 22 | 23 | - Operating System: 24 | - Python version: 25 | - C++ compiler version: 26 | 27 | ### Checklist 28 | 29 | - [ ] My code follows the [style guidelines of this project](https://github.com/google/styleguide/blob/gh-pages/pyguide.md) 30 | - [ ] I have performed a self-review of my own code 31 | - [ ] I have commented my code, particularly in hard-to-understand areas 32 | - [ ] I have made corresponding changes to the documentation 33 | - [ ] I have added tests that prove my fix is effective or that my feature works 34 | -------------------------------------------------------------------------------- /nes_py/nes/src/mappers/mapper_NROM.cpp: -------------------------------------------------------------------------------- 1 | // Program: nes-py 2 | // File: mapper_NROM.cpp 3 | // Description: An implementation of the NROM mapper 4 | // 5 | // Copyright (c) 2019 Christian Kauten. All rights reserved. 6 | // 7 | 8 | #include "mappers/mapper_NROM.hpp" 9 | #include "log.hpp" 10 | 11 | namespace NES { 12 | 13 | MapperNROM::MapperNROM(Cartridge* cart) : 14 | Mapper(cart), 15 | is_one_bank(cart->getROM().size() == 0x4000), 16 | has_character_ram(cart->getVROM().size() == 0) { 17 | if (has_character_ram) { 18 | character_ram.resize(0x2000); 19 | LOG(Info) << "Uses character RAM" << std::endl; 20 | } 21 | } 22 | 23 | void MapperNROM::writePRG(NES_Address address, NES_Byte value) { 24 | LOG(InfoVerbose) << 25 | "ROM memory write attempt at " << 26 | +address << 27 | " to set " << 28 | +value << 29 | std::endl; 30 | } 31 | 32 | void MapperNROM::writeCHR(NES_Address address, NES_Byte value) { 33 | if (has_character_ram) 34 | character_ram[address] = value; 35 | else 36 | LOG(Info) << 37 | "Read-only CHR memory write attempt at " << 38 | std::hex << 39 | address << 40 | std::endl; 41 | } 42 | 43 | } // namespace NES 44 | -------------------------------------------------------------------------------- /nes_py/nes/src/cartridge.cpp: -------------------------------------------------------------------------------- 1 | // Program: nes-py 2 | // File: cartridge.cpp 3 | // Description: This class houses the logic and data for an NES cartridge 4 | // 5 | // Copyright (c) 2019 Christian Kauten. All rights reserved. 6 | // 7 | 8 | #include 9 | #include "cartridge.hpp" 10 | #include "log.hpp" 11 | 12 | namespace NES { 13 | 14 | void Cartridge::loadFromFile(std::string path) { 15 | // create a stream to load the ROM file 16 | std::ifstream romFile(path, std::ios_base::binary | std::ios_base::in); 17 | // create a byte vector for the iNES header 18 | std::vector header; 19 | header.resize(0x10); 20 | romFile.read(reinterpret_cast(&header[0]), 0x10); 21 | // read internal data 22 | name_table_mirroring = header[6] & 0xB; 23 | mapper_number = ((header[6] >> 4) & 0xf) | (header[7] & 0xf0); 24 | has_extended_ram = header[6] & 0x2; 25 | // read PRG-ROM 16KB banks 26 | NES_Byte banks = header[4]; 27 | prg_rom.resize(0x4000 * banks); 28 | romFile.read(reinterpret_cast(&prg_rom[0]), 0x4000 * banks); 29 | // read CHR-ROM 8KB banks 30 | NES_Byte vbanks = header[5]; 31 | if (!vbanks) 32 | return; 33 | chr_rom.resize(0x2000 * vbanks); 34 | romFile.read(reinterpret_cast(&chr_rom[0]), 0x2000 * vbanks); 35 | } 36 | 37 | } // namespace NES 38 | -------------------------------------------------------------------------------- /nes_py/nes/include/mapper_factory.hpp: -------------------------------------------------------------------------------- 1 | // Program: nes-py 2 | // File: mapper.hpp 3 | // Description: An abstract factory for mappers 4 | // 5 | // Copyright (c) 2019 Christian Kauten. All rights reserved. 6 | // 7 | 8 | #ifndef MAPPER_FACTORY_HPP 9 | #define MAPPER_FACTORY_HPP 10 | 11 | #include "mapper.hpp" 12 | #include "mappers/mapper_NROM.hpp" 13 | #include "mappers/mapper_SxROM.hpp" 14 | #include "mappers/mapper_UxROM.hpp" 15 | #include "mappers/mapper_CNROM.hpp" 16 | 17 | namespace NES { 18 | 19 | /// an enumeration of mapper IDs 20 | enum class MapperID : NES_Byte { 21 | NROM = 0, 22 | SxROM = 1, 23 | UxROM = 2, 24 | CNROM = 3, 25 | }; 26 | 27 | /// Create a mapper for the given cartridge with optional callback function 28 | /// 29 | /// @param game the cartridge to initialize a mapper for 30 | /// @param callback the callback function for the mapper (if necessary) 31 | /// 32 | Mapper* MapperFactory(Cartridge* game, std::function callback) { 33 | switch (static_cast(game->getMapper())) { 34 | case MapperID::NROM: 35 | return new MapperNROM(game); 36 | case MapperID::SxROM: 37 | return new MapperSxROM(game, callback); 38 | case MapperID::UxROM: 39 | return new MapperUxROM(game); 40 | case MapperID::CNROM: 41 | return new MapperCNROM(game); 42 | default: 43 | return nullptr; 44 | } 45 | } 46 | 47 | } // namespace NES 48 | 49 | #endif // MAPPER_FACTORY_HPP 50 | -------------------------------------------------------------------------------- /nes_py/nes/src/mappers/mapper_UxROM.cpp: -------------------------------------------------------------------------------- 1 | // Program: nes-py 2 | // File: mapper_UxROM.cpp 3 | // Description: An implementation of the UxROM mapper 4 | // 5 | // Copyright (c) 2019 Christian Kauten. All rights reserved. 6 | // 7 | 8 | #include "mappers/mapper_UxROM.hpp" 9 | #include "log.hpp" 10 | 11 | namespace NES { 12 | 13 | MapperUxROM::MapperUxROM(Cartridge* cart) : 14 | Mapper(cart), 15 | has_character_ram(cart->getVROM().size() == 0), 16 | last_bank_pointer(cart->getROM().size() - 0x4000), 17 | select_prg(0) { 18 | if (has_character_ram) { 19 | character_ram.resize(0x2000); 20 | LOG(Info) << "Uses character RAM" << std::endl; 21 | } 22 | } 23 | 24 | NES_Byte MapperUxROM::readPRG(NES_Address address) { 25 | if (address < 0xc000) 26 | return cartridge->getROM()[((address - 0x8000) & 0x3fff) | (select_prg << 14)]; 27 | else 28 | return cartridge->getROM()[last_bank_pointer + (address & 0x3fff)]; 29 | } 30 | 31 | NES_Byte MapperUxROM::readCHR(NES_Address address) { 32 | if (has_character_ram) 33 | return character_ram[address]; 34 | else 35 | return cartridge->getVROM()[address]; 36 | } 37 | 38 | void MapperUxROM::writeCHR(NES_Address address, NES_Byte value) { 39 | if (has_character_ram) 40 | character_ram[address] = value; 41 | else 42 | LOG(Info) << 43 | "Read-only CHR memory write attempt at " << 44 | std::hex << 45 | address << 46 | std::endl; 47 | } 48 | 49 | } // namespace NES 50 | -------------------------------------------------------------------------------- /nes_py/nes/include/controller.hpp: -------------------------------------------------------------------------------- 1 | // Program: nes-py 2 | // File: controller.hpp 3 | // Description: This class houses the logic and data for an NES controller 4 | // 5 | // Copyright (c) 2019 Christian Kauten. All rights reserved. 6 | // 7 | 8 | #ifndef CONTROLLER_HPP 9 | #define CONTROLLER_HPP 10 | 11 | #include "common.hpp" 12 | 13 | namespace NES { 14 | 15 | /// A standard NES controller 16 | class Controller { 17 | private: 18 | /// whether strobe is on 19 | bool is_strobe; 20 | /// the emulation of the buttons on the controller 21 | NES_Byte joypad_buttons; 22 | /// the state of the buttons 23 | NES_Byte joypad_bits; 24 | 25 | public: 26 | /// Initialize a new controller. 27 | Controller() : is_strobe(true), joypad_buttons(0), joypad_bits(0) { } 28 | 29 | /// Return a pointer to the joypad buffer. 30 | inline NES_Byte* get_joypad_buffer() { return &joypad_buttons; } 31 | 32 | /// Write buttons to the virtual controller. 33 | /// 34 | /// @param buttons the button bitmap to write to the controller 35 | /// 36 | inline void write_buttons(NES_Byte buttons) { joypad_buttons = buttons; } 37 | 38 | /// Strobe the controller. 39 | inline void strobe(NES_Byte b) { 40 | is_strobe = (b & 1); 41 | if (!is_strobe) joypad_bits = joypad_buttons; 42 | } 43 | 44 | /// Read the controller state. 45 | /// 46 | /// @return a state from the controller 47 | /// 48 | NES_Byte read(); 49 | }; 50 | 51 | } // namespace NES 52 | 53 | #endif // CONTROLLER_HPP 54 | -------------------------------------------------------------------------------- /nes_py/app/cli.py: -------------------------------------------------------------------------------- 1 | """Command line interface to nes-py NES emulator.""" 2 | import argparse 3 | from .play_human import play_human 4 | from .play_random import play_random 5 | from ..nes_env import NESEnv 6 | 7 | 8 | def _get_args(): 9 | """Parse arguments from the command line and return them.""" 10 | parser = argparse.ArgumentParser(description=__doc__) 11 | # add the argument for the Super Mario Bros environment to run 12 | parser.add_argument('--rom', '-r', 13 | type=str, 14 | help='The path to the ROM to play.', 15 | required=True, 16 | ) 17 | # add the argument for the mode of execution as either human or random 18 | parser.add_argument('--mode', '-m', 19 | type=str, 20 | default='human', 21 | choices=['human', 'random'], 22 | help='The execution mode for the emulation.', 23 | ) 24 | # add the argument for the number of steps to take in random mode 25 | parser.add_argument('--steps', '-s', 26 | type=int, 27 | default=500, 28 | help='The number of random steps to take.', 29 | ) 30 | return parser.parse_args() 31 | 32 | 33 | def main(): 34 | """The main entry point for the command line interface.""" 35 | # get arguments from the command line 36 | args = _get_args() 37 | # create the environment 38 | env = NESEnv(args.rom) 39 | # play the environment with the given mode 40 | if args.mode == 'human': 41 | play_human(env) 42 | else: 43 | play_random(env, args.steps) 44 | 45 | 46 | # explicitly define the outward facing API of this module 47 | __all__ = [main.__name__] 48 | -------------------------------------------------------------------------------- /nes_py/nes/include/cartridge.hpp: -------------------------------------------------------------------------------- 1 | // Program: nes-py 2 | // File: cartridge.hpp 3 | // Description: This class houses the logic and data for an NES cartridge 4 | // 5 | // Copyright (c) 2019 Christian Kauten. All rights reserved. 6 | // 7 | 8 | #ifndef CARTRIDGE_HPP 9 | #define CARTRIDGE_HPP 10 | 11 | #include 12 | #include 13 | #include "common.hpp" 14 | 15 | namespace NES { 16 | 17 | /// A cartridge holding game ROM and a special hardware mapper emulation 18 | class Cartridge { 19 | private: 20 | /// the PRG ROM 21 | std::vector prg_rom; 22 | /// the CHR ROM 23 | std::vector chr_rom; 24 | /// the name table mirroring mode 25 | NES_Byte name_table_mirroring; 26 | /// the mapper ID number 27 | NES_Byte mapper_number; 28 | /// whether this cartridge uses extended RAM 29 | bool has_extended_ram; 30 | 31 | public: 32 | /// Initialize a new cartridge 33 | Cartridge() : 34 | name_table_mirroring(0), 35 | mapper_number(0), 36 | has_extended_ram(false) { } 37 | 38 | /// Return the ROM data. 39 | const inline std::vector& getROM() { return prg_rom; } 40 | 41 | /// Return the VROM data. 42 | const inline std::vector& getVROM() { return chr_rom; } 43 | 44 | /// Return the mapper ID number. 45 | inline NES_Byte getMapper() { return mapper_number; } 46 | 47 | /// Return the name table mirroring mode. 48 | inline NES_Byte getNameTableMirroring() { return name_table_mirroring; } 49 | 50 | /// Return a boolean determining whether this cartridge uses extended RAM. 51 | inline bool hasExtendedRAM() { return has_extended_ram; } 52 | 53 | /// Load a ROM file into the cartridge and build the corresponding mapper. 54 | void loadFromFile(std::string path); 55 | }; 56 | 57 | } // namespace NES 58 | 59 | #endif // CARTRIDGE_HPP 60 | -------------------------------------------------------------------------------- /nes_py/nes/include/mappers/mapper_UxROM.hpp: -------------------------------------------------------------------------------- 1 | // Program: nes-py 2 | // File: mapper_UxROM.hpp 3 | // Description: An implementation of the UxROM mapper 4 | // 5 | // Copyright (c) 2019 Christian Kauten. All rights reserved. 6 | // 7 | 8 | #ifndef MAPPERUXROM_HPP 9 | #define MAPPERUXROM_HPP 10 | 11 | #include 12 | #include "common.hpp" 13 | #include "mapper.hpp" 14 | 15 | namespace NES { 16 | 17 | class MapperUxROM : public Mapper { 18 | private: 19 | /// whether the cartridge use character RAM 20 | bool has_character_ram; 21 | /// the pointer to the last bank 22 | std::size_t last_bank_pointer; 23 | /// TODO: what is this? 24 | NES_Address select_prg; 25 | /// The character RAM on the mapper 26 | std::vector character_ram; 27 | 28 | public: 29 | /// Create a new mapper with a cartridge. 30 | /// 31 | /// @param cart a reference to a cartridge for the mapper to access 32 | /// 33 | explicit MapperUxROM(Cartridge* cart); 34 | 35 | /// Read a byte from the PRG RAM. 36 | /// 37 | /// @param address the 16-bit address of the byte to read 38 | /// @return the byte located at the given address in PRG RAM 39 | /// 40 | NES_Byte readPRG(NES_Address address); 41 | 42 | /// Write a byte to an address in the PRG RAM. 43 | /// 44 | /// @param address the 16-bit address to write to 45 | /// @param value the byte to write to the given address 46 | /// 47 | inline void writePRG(NES_Address address, NES_Byte value) { 48 | select_prg = value; 49 | } 50 | 51 | /// Read a byte from the CHR RAM. 52 | /// 53 | /// @param address the 16-bit address of the byte to read 54 | /// @return the byte located at the given address in CHR RAM 55 | /// 56 | NES_Byte readCHR(NES_Address address); 57 | 58 | /// Write a byte to an address in the CHR RAM. 59 | /// 60 | /// @param address the 16-bit address to write to 61 | /// @param value the byte to write to the given address 62 | /// 63 | void writeCHR(NES_Address address, NES_Byte value); 64 | }; 65 | 66 | } // namespace NES 67 | 68 | #endif // MAPPERUXROM_HPP 69 | -------------------------------------------------------------------------------- /nes_py/nes/include/picture_bus.hpp: -------------------------------------------------------------------------------- 1 | // Program: nes-py 2 | // File: picture_bus.hpp 3 | // Description: This class houses picture bus data from the PPU 4 | // 5 | // Copyright (c) 2019 Christian Kauten. All rights reserved. 6 | // 7 | 8 | #ifndef PICTURE_BUS_HPP 9 | #define PICTURE_BUS_HPP 10 | 11 | #include 12 | #include 13 | #include "common.hpp" 14 | #include "mapper.hpp" 15 | 16 | namespace NES { 17 | 18 | /// The bus for graphical data to travel along 19 | class PictureBus { 20 | private: 21 | /// the VRAM on the picture bus 22 | std::vector ram; 23 | /// indexes where they start in RAM vector 24 | std::size_t name_tables[4] = {0, 0, 0, 0}; 25 | /// the palette for decoding RGB tuples 26 | std::vector palette; 27 | /// a pointer to the mapper on the cartridge 28 | Mapper* mapper; 29 | 30 | public: 31 | /// Initialize a new picture bus. 32 | PictureBus() : ram(0x800), palette(0x20), mapper(nullptr) { } 33 | 34 | /// Read a byte from an address on the VRAM. 35 | /// 36 | /// @param address the 16-bit address of the byte to read in the VRAM 37 | /// 38 | /// @return the byte located at the given address 39 | /// 40 | NES_Byte read(NES_Address address); 41 | 42 | /// Write a byte to an address in the VRAM. 43 | /// 44 | /// @param address the 16-bit address to write the byte to in VRAM 45 | /// @param value the byte to write to the given address 46 | /// 47 | void write(NES_Address address, NES_Byte value); 48 | 49 | /// Set the mapper pointer to a new value. 50 | /// 51 | /// @param mapper the new mapper pointer for the bus to use 52 | /// 53 | inline void set_mapper(Mapper *mapper) { 54 | this->mapper = mapper; update_mirroring(); 55 | } 56 | 57 | /// Read a color index from the palette. 58 | /// 59 | /// @param address the address of the palette color 60 | /// 61 | /// @return the index of the RGB tuple in the color array 62 | /// 63 | inline NES_Byte read_palette(NES_Byte address) { 64 | return palette[address]; 65 | } 66 | 67 | /// Update the mirroring and name table from the mapper. 68 | void update_mirroring(); 69 | }; 70 | 71 | } // namespace NES 72 | 73 | #endif // PICTURE_BUS_HPP 74 | -------------------------------------------------------------------------------- /nes_py/nes/include/mappers/mapper_CNROM.hpp: -------------------------------------------------------------------------------- 1 | // Program: nes-py 2 | // File: mapper_CNROM.hpp 3 | // Description: An implementation of the CNROM mapper 4 | // 5 | // Copyright (c) 2019 Christian Kauten. All rights reserved. 6 | // 7 | 8 | #ifndef MAPPERCNROM_HPP 9 | #define MAPPERCNROM_HPP 10 | 11 | #include "common.hpp" 12 | #include "mapper.hpp" 13 | 14 | namespace NES { 15 | 16 | class MapperCNROM : public Mapper { 17 | private: 18 | /// whether there are 1 or 2 banks 19 | bool is_one_bank; 20 | /// TODO: what is this value 21 | NES_Address select_chr; 22 | 23 | public: 24 | /// Create a new mapper with a cartridge. 25 | /// 26 | /// @param cart a reference to a cartridge for the mapper to access 27 | /// 28 | explicit MapperCNROM(Cartridge* cart) : 29 | Mapper(cart), 30 | is_one_bank(cart->getROM().size() == 0x4000), 31 | select_chr(0) { } 32 | 33 | /// Read a byte from the PRG RAM. 34 | /// 35 | /// @param address the 16-bit address of the byte to read 36 | /// @return the byte located at the given address in PRG RAM 37 | /// 38 | inline NES_Byte readPRG(NES_Address address) { 39 | if (!is_one_bank) 40 | return cartridge->getROM()[address - 0x8000]; 41 | else // mirrored 42 | return cartridge->getROM()[(address - 0x8000) & 0x3fff]; 43 | } 44 | 45 | /// Write a byte to an address in the PRG RAM. 46 | /// 47 | /// @param address the 16-bit address to write to 48 | /// @param value the byte to write to the given address 49 | /// 50 | inline void writePRG(NES_Address address, NES_Byte value) { 51 | select_chr = value & 0x3; 52 | } 53 | 54 | /// Read a byte from the CHR RAM. 55 | /// 56 | /// @param address the 16-bit address of the byte to read 57 | /// @return the byte located at the given address in CHR RAM 58 | /// 59 | inline NES_Byte readCHR(NES_Address address) { 60 | return cartridge->getVROM()[address | (select_chr << 13)]; 61 | } 62 | 63 | /// Write a byte to an address in the CHR RAM. 64 | /// 65 | /// @param address the 16-bit address to write to 66 | /// @param value the byte to write to the given address 67 | /// 68 | void writeCHR(NES_Address address, NES_Byte value); 69 | }; 70 | 71 | } // namespace NES 72 | 73 | #endif // MAPPERCNROM_HPP 74 | -------------------------------------------------------------------------------- /nes_py/nes/include/mappers/mapper_NROM.hpp: -------------------------------------------------------------------------------- 1 | // Program: nes-py 2 | // File: mapper_NROM.hpp 3 | // Description: An implementation of the NROM mapper 4 | // 5 | // Copyright (c) 2019 Christian Kauten. All rights reserved. 6 | // 7 | 8 | #ifndef MAPPERNROM_HPP 9 | #define MAPPERNROM_HPP 10 | 11 | #include 12 | #include "common.hpp" 13 | #include "mapper.hpp" 14 | 15 | namespace NES { 16 | 17 | class MapperNROM : public Mapper { 18 | private: 19 | /// whether there are 1 or 2 banks 20 | bool is_one_bank; 21 | /// whether this mapper uses character RAM 22 | bool has_character_ram; 23 | /// the character RAM on the mapper 24 | std::vector character_ram; 25 | 26 | public: 27 | /// Create a new mapper with a cartridge. 28 | /// 29 | /// @param cart a reference to a cartridge for the mapper to access 30 | /// 31 | explicit MapperNROM(Cartridge* cart); 32 | 33 | /// Read a byte from the PRG RAM. 34 | /// 35 | /// @param address the 16-bit address of the byte to read 36 | /// @return the byte located at the given address in PRG RAM 37 | /// 38 | inline NES_Byte readPRG(NES_Address address) { 39 | if (!is_one_bank) 40 | return cartridge->getROM()[address - 0x8000]; 41 | else // mirrored 42 | return cartridge->getROM()[(address - 0x8000) & 0x3fff]; 43 | } 44 | 45 | /// Write a byte to an address in the PRG RAM. 46 | /// 47 | /// @param address the 16-bit address to write to 48 | /// @param value the byte to write to the given address 49 | /// 50 | void writePRG(NES_Address address, NES_Byte value); 51 | 52 | /// Read a byte from the CHR RAM. 53 | /// 54 | /// @param address the 16-bit address of the byte to read 55 | /// @return the byte located at the given address in CHR RAM 56 | /// 57 | inline NES_Byte readCHR(NES_Address address) { 58 | if (has_character_ram) 59 | return character_ram[address]; 60 | else 61 | return cartridge->getVROM()[address]; 62 | } 63 | 64 | /// Write a byte to an address in the CHR RAM. 65 | /// 66 | /// @param address the 16-bit address to write to 67 | /// @param value the byte to write to the given address 68 | /// 69 | void writeCHR(NES_Address address, NES_Byte value); 70 | }; 71 | 72 | } // namespace NES 73 | 74 | #endif // MAPPERNROM_HPP 75 | -------------------------------------------------------------------------------- /nes_py/nes/include/mapper.hpp: -------------------------------------------------------------------------------- 1 | // Program: nes-py 2 | // File: mapper.hpp 3 | // Description: This class provides an abstraction of an NES cartridge mapper 4 | // 5 | // Copyright (c) 2019 Christian Kauten. All rights reserved. 6 | // 7 | 8 | #ifndef MAPPER_HPP 9 | #define MAPPER_HPP 10 | 11 | #include 12 | #include "common.hpp" 13 | #include "cartridge.hpp" 14 | 15 | namespace NES { 16 | 17 | /// Mirroring modes supported by the NES 18 | enum NameTableMirroring { 19 | HORIZONTAL = 0, 20 | VERTICAL = 1, 21 | FOUR_SCREEN = 8, 22 | ONE_SCREEN_LOWER, 23 | ONE_SCREEN_HIGHER, 24 | }; 25 | 26 | /// An abstraction of a general hardware mapper for different NES cartridges 27 | class Mapper { 28 | protected: 29 | /// The cartridge this mapper associates with 30 | Cartridge* cartridge; 31 | 32 | public: 33 | /// Create a new mapper with a cartridge and given type. 34 | /// 35 | /// @param game a reference to a cartridge for the mapper to access 36 | /// 37 | explicit Mapper(Cartridge* game) : cartridge(game) { } 38 | 39 | /// Return the name table mirroring mode of this mapper. 40 | inline virtual NameTableMirroring getNameTableMirroring() { 41 | return static_cast(cartridge->getNameTableMirroring()); 42 | } 43 | 44 | /// Return true if this mapper has extended RAM, false otherwise. 45 | inline bool hasExtendedRAM() { return cartridge->hasExtendedRAM(); } 46 | 47 | /// Read a byte from the PRG RAM. 48 | /// 49 | /// @param address the 16-bit address of the byte to read 50 | /// @return the byte located at the given address in PRG RAM 51 | /// 52 | virtual NES_Byte readPRG(NES_Address address) = 0; 53 | 54 | /// Write a byte to an address in the PRG RAM. 55 | /// 56 | /// @param address the 16-bit address to write to 57 | /// @param value the byte to write to the given address 58 | /// 59 | virtual void writePRG(NES_Address address, NES_Byte value) = 0; 60 | 61 | /// Read a byte from the CHR RAM. 62 | /// 63 | /// @param address the 16-bit address of the byte to read 64 | /// @return the byte located at the given address in CHR RAM 65 | /// 66 | virtual NES_Byte readCHR(NES_Address address) = 0; 67 | 68 | /// Write a byte to an address in the CHR RAM. 69 | /// 70 | /// @param address the 16-bit address to write to 71 | /// @param value the byte to write to the given address 72 | /// 73 | virtual void writeCHR(NES_Address address, NES_Byte value) = 0; 74 | }; 75 | 76 | } // namespace NES 77 | 78 | #endif // MAPPER_HPP 79 | -------------------------------------------------------------------------------- /nes_py/nes/src/lib_nes_env.cpp: -------------------------------------------------------------------------------- 1 | // Program: nes-py 2 | // File: lib_nes_env.cpp 3 | // Description: file describes the outward facing ctypes API for Python 4 | // 5 | // Copyright (c) 2019 Christian Kauten. All rights reserved. 6 | // 7 | 8 | #include 9 | #include "common.hpp" 10 | #include "emulator.hpp" 11 | 12 | // Windows-base systems 13 | #if defined(_WIN32) || defined(WIN32) || defined(__CYGWIN__) || defined(__MINGW32__) || defined(__BORLANDC__) 14 | // setup the module initializer. required to link visual studio C++ ctypes 15 | void PyInit_lib_nes_env() { } 16 | // setup the function modifier to export in the DLL 17 | #define EXP __declspec(dllexport) 18 | // Unix-like systems 19 | #else 20 | // setup the modifier as a dummy 21 | #define EXP 22 | #endif 23 | 24 | // definitions of functions for the Python interface to access 25 | extern "C" { 26 | /// Return the width of the NES. 27 | EXP int Width() { 28 | return NES::Emulator::WIDTH; 29 | } 30 | 31 | /// Return the height of the NES. 32 | EXP int Height() { 33 | return NES::Emulator::HEIGHT; 34 | } 35 | 36 | /// Initialize a new emulator and return a pointer to it 37 | EXP NES::Emulator* Initialize(wchar_t* path) { 38 | // convert the c string to a c++ std string data structure 39 | std::wstring ws_rom_path(path); 40 | std::string rom_path(ws_rom_path.begin(), ws_rom_path.end()); 41 | // create a new emulator with the given ROM path 42 | return new NES::Emulator(rom_path); 43 | } 44 | 45 | /// Return a pointer to a controller on the machine 46 | EXP NES::NES_Byte* Controller(NES::Emulator* emu, int port) { 47 | return emu->get_controller(port); 48 | } 49 | 50 | /// Return the pointer to the screen buffer 51 | EXP NES::NES_Pixel* Screen(NES::Emulator* emu) { 52 | return emu->get_screen_buffer(); 53 | } 54 | 55 | /// Return the pointer to the memory buffer 56 | EXP NES::NES_Byte* Memory(NES::Emulator* emu) { 57 | return emu->get_memory_buffer(); 58 | } 59 | 60 | /// Reset the emulator 61 | EXP void Reset(NES::Emulator* emu) { 62 | emu->reset(); 63 | } 64 | 65 | /// Perform a discrete step in the emulator (i.e., 1 frame) 66 | EXP void Step(NES::Emulator* emu) { 67 | emu->step(); 68 | } 69 | 70 | /// Create a deep copy (i.e., a clone) of the given emulator 71 | EXP void Backup(NES::Emulator* emu) { 72 | emu->backup(); 73 | } 74 | 75 | /// Create a deep copy (i.e., a clone) of the given emulator 76 | EXP void Restore(NES::Emulator* emu) { 77 | emu->restore(); 78 | } 79 | 80 | /// Close the emulator, i.e., purge it from memory 81 | EXP void Close(NES::Emulator* emu) { 82 | delete emu; 83 | } 84 | } 85 | 86 | // un-define the macro 87 | #undef EXP 88 | -------------------------------------------------------------------------------- /nes_py/nes/src/emulator.cpp: -------------------------------------------------------------------------------- 1 | // Program: nes-py 2 | // File: emulator.cpp 3 | // Description: This class houses the logic and data for an NES emulator 4 | // 5 | // Copyright (c) 2019 Christian Kauten. All rights reserved. 6 | // 7 | 8 | #include "emulator.hpp" 9 | #include "mapper_factory.hpp" 10 | #include "log.hpp" 11 | 12 | namespace NES { 13 | 14 | Emulator::Emulator(std::string rom_path) { 15 | // set the read callbacks 16 | bus.set_read_callback(PPUSTATUS, [&](void) { return ppu.get_status(); }); 17 | bus.set_read_callback(PPUDATA, [&](void) { return ppu.get_data(picture_bus); }); 18 | bus.set_read_callback(JOY1, [&](void) { return controllers[0].read(); }); 19 | bus.set_read_callback(JOY2, [&](void) { return controllers[1].read(); }); 20 | bus.set_read_callback(OAMDATA, [&](void) { return ppu.get_OAM_data(); }); 21 | // set the write callbacks 22 | bus.set_write_callback(PPUCTRL, [&](NES_Byte b) { ppu.control(b); }); 23 | bus.set_write_callback(PPUMASK, [&](NES_Byte b) { ppu.set_mask(b); }); 24 | bus.set_write_callback(OAMADDR, [&](NES_Byte b) { ppu.set_OAM_address(b); }); 25 | bus.set_write_callback(PPUADDR, [&](NES_Byte b) { ppu.set_data_address(b); }); 26 | bus.set_write_callback(PPUSCROL, [&](NES_Byte b) { ppu.set_scroll(b); }); 27 | bus.set_write_callback(PPUDATA, [&](NES_Byte b) { ppu.set_data(picture_bus, b); }); 28 | bus.set_write_callback(OAMDMA, [&](NES_Byte b) { cpu.skip_DMA_cycles(); ppu.do_DMA(bus.get_page_pointer(b)); }); 29 | bus.set_write_callback(JOY1, [&](NES_Byte b) { controllers[0].strobe(b); controllers[1].strobe(b); }); 30 | bus.set_write_callback(OAMDATA, [&](NES_Byte b) { ppu.set_OAM_data(b); }); 31 | // set the interrupt callback for the PPU 32 | ppu.set_interrupt_callback([&]() { cpu.interrupt(bus, CPU::NMI_INTERRUPT); }); 33 | // load the ROM from disk, expect that the Python code has validated it 34 | cartridge.loadFromFile(rom_path); 35 | // create the mapper based on the mapper ID in the iNES header of the ROM 36 | auto mapper = MapperFactory(&cartridge, [&](){ picture_bus.update_mirroring(); }); 37 | // give the IO buses a pointer to the mapper 38 | bus.set_mapper(mapper); 39 | picture_bus.set_mapper(mapper); 40 | } 41 | 42 | void Emulator::step() { 43 | // render a single frame on the emulator 44 | for (int i = 0; i < CYCLES_PER_FRAME; i++) { 45 | // 3 PPU steps per CPU step 46 | ppu.cycle(picture_bus); 47 | ppu.cycle(picture_bus); 48 | ppu.cycle(picture_bus); 49 | cpu.cycle(bus); 50 | } 51 | } 52 | 53 | } // namespace NES 54 | -------------------------------------------------------------------------------- /nes_py/tests/test_multiple_makes.py: -------------------------------------------------------------------------------- 1 | """Test that the multiprocessing package works with the env.""" 2 | from multiprocessing import Process 3 | from threading import Thread 4 | from unittest import TestCase 5 | from .rom_file_abs_path import rom_file_abs_path 6 | from nes_py.nes_env import NESEnv 7 | 8 | 9 | def play(steps): 10 | """ 11 | Play the environment making uniformly random decisions. 12 | 13 | Args: 14 | steps (int): the number of steps to take 15 | 16 | Returns: 17 | None 18 | 19 | """ 20 | # create an NES environment with Super Mario Bros. 21 | path = rom_file_abs_path('super-mario-bros-1.nes') 22 | env = NESEnv(path) 23 | # step the environment for some arbitrary number of steps 24 | done = True 25 | for _ in range(steps): 26 | if done: 27 | _ = env.reset() 28 | action = env.action_space.sample() 29 | _, _, done, _ = env.step(action) 30 | # close the environment 31 | env.close() 32 | 33 | 34 | class ShouldMakeMultipleEnvironmentsParallel(object): 35 | """An abstract test case to make environments in parallel.""" 36 | 37 | # the class to the parallel initializer (Thread, Process, etc.) 38 | parallel_initializer = lambda target, args: None 39 | 40 | # the number of parallel executions 41 | num_execs = 4 42 | 43 | # the number of steps to take per environment 44 | steps = 10 45 | 46 | def test(self): 47 | procs = [None] * self.num_execs 48 | args = (self.steps, ) 49 | # spawn the parallel instances 50 | for idx in range(self.num_execs): 51 | procs[idx] = self.parallel_initializer(target=play, args=args) 52 | procs[idx].start() 53 | # join the parallel instances 54 | for proc in procs: 55 | proc.join() 56 | 57 | 58 | class ProcessTest(ShouldMakeMultipleEnvironmentsParallel, TestCase): 59 | """Test that processes (true multi-threading) work.""" 60 | parallel_initializer = Process 61 | 62 | 63 | class ThreadTest(ShouldMakeMultipleEnvironmentsParallel, TestCase): 64 | """Test that threads (internal parallelism) work""" 65 | parallel_initializer = Thread 66 | 67 | 68 | class ShouldMakeMultipleEnvironmentsSingleThread(TestCase): 69 | """Test making 4 environments in a single code stream.""" 70 | 71 | # the number of environments to spawn 72 | num_envs = 4 73 | 74 | # the number of steps to take per environment 75 | steps = 10 76 | 77 | def test(self): 78 | path = rom_file_abs_path('super-mario-bros-1.nes') 79 | envs = [NESEnv(path) for _ in range(self.num_envs)] 80 | dones = [True] * self.num_envs 81 | 82 | for _ in range(self.steps): 83 | for idx in range(self.num_envs): 84 | if dones[idx]: 85 | _ = envs[idx].reset() 86 | action = envs[idx].action_space.sample() 87 | _, _, dones[idx], _ = envs[idx].step(action) 88 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """The setup script for installing and distributing the nes-py package.""" 2 | import os 3 | from glob import glob 4 | from setuptools import setup, find_packages, Extension 5 | 6 | 7 | # set the compiler for the C++ framework 8 | os.environ['CC'] = 'g++' 9 | os.environ['CCX'] = 'g++' 10 | 11 | 12 | # read the contents from the README file 13 | with open('README.md') as README_file: 14 | README = README_file.read() 15 | 16 | 17 | # The prefix name for the .so library to build. It will follow the format 18 | # lib_nes_env.*.so where the * changes depending on the build system 19 | LIB_NAME = 'nes_py.lib_nes_env' 20 | # The source files for building the extension. Globs locate all the cpp files 21 | # used by the LaiNES subproject. MANIFEST.in has to include the blanket 22 | # "cpp" directory to ensure that the .inc file gets included too 23 | SOURCES = glob('nes_py/nes/src/*.cpp') + glob('nes_py/nes/src/mappers/*.cpp') 24 | # The directory pointing to header files used by the LaiNES cpp files. 25 | # This directory has to be included using MANIFEST.in too to include the 26 | # headers with sdist 27 | INCLUDE_DIRS = ['nes_py/nes/include'] 28 | # Build arguments to pass to the compiler 29 | EXTRA_COMPILE_ARGS = ['-std=c++1y', '-pipe', '-O3'] 30 | # The official extension using the name, source, headers, and build args 31 | LIB_NES_ENV = Extension(LIB_NAME, 32 | sources=SOURCES, 33 | include_dirs=INCLUDE_DIRS, 34 | extra_compile_args=EXTRA_COMPILE_ARGS, 35 | ) 36 | 37 | 38 | setup( 39 | name='nes_py', 40 | version='8.2.1', 41 | description='An NES Emulator and OpenAI Gym interface', 42 | long_description=README, 43 | long_description_content_type='text/markdown', 44 | keywords='NES Emulator OpenAI-Gym', 45 | classifiers=[ 46 | 'Development Status :: 5 - Production/Stable', 47 | 'Intended Audience :: Developers', 48 | 'Intended Audience :: Science/Research', 49 | 'License :: OSI Approved :: MIT License', 50 | 'Operating System :: MacOS :: MacOS X', 51 | 'Operating System :: POSIX :: Linux', 52 | 'Operating System :: Microsoft :: Windows', 53 | 'Programming Language :: C++', 54 | 'Programming Language :: Python :: 3 :: Only', 55 | 'Programming Language :: Python :: 3.5', 56 | 'Programming Language :: Python :: 3.6', 57 | 'Programming Language :: Python :: 3.7', 58 | 'Programming Language :: Python :: 3.8', 59 | 'Programming Language :: Python :: 3.9', 60 | 'Topic :: Games/Entertainment', 61 | 'Topic :: Software Development :: Libraries :: Python Modules', 62 | 'Topic :: System :: Emulators', 63 | ], 64 | url='https://github.com/Kautenja/nes-py', 65 | author='Christian Kauten', 66 | author_email='kautencreations@gmail.com', 67 | license='MIT', 68 | packages=find_packages(exclude=['tests', '*.tests', '*.tests.*']), 69 | ext_modules=[LIB_NES_ENV], 70 | zip_safe=False, 71 | install_requires=[ 72 | 'gym>=0.17.2', 73 | 'numpy>=1.18.5', 74 | 'pyglet<=1.5.21,>=1.4.0', 75 | 'tqdm>=4.48.2', 76 | ], 77 | entry_points={ 78 | 'console_scripts': [ 79 | 'nes_py = nes_py.app.cli:main', 80 | ], 81 | }, 82 | ) 83 | -------------------------------------------------------------------------------- /nes_py/app/play_human.py: -------------------------------------------------------------------------------- 1 | """A method to play gym environments using human IO inputs.""" 2 | import gym 3 | import time 4 | from pyglet import clock 5 | from .._image_viewer import ImageViewer 6 | 7 | 8 | # the sentinel value for "No Operation" 9 | _NOP = 0 10 | 11 | 12 | def play_human(env: gym.Env, callback=None): 13 | """ 14 | Play the environment using keyboard as a human. 15 | 16 | Args: 17 | env: the initialized gym environment to play 18 | callback: a callback to receive output from the environment 19 | 20 | Returns: 21 | None 22 | 23 | """ 24 | # ensure the observation space is a box of pixels 25 | assert isinstance(env.observation_space, gym.spaces.box.Box) 26 | # ensure the observation space is either B&W pixels or RGB Pixels 27 | obs_s = env.observation_space 28 | is_bw = len(obs_s.shape) == 2 29 | is_rgb = len(obs_s.shape) == 3 and obs_s.shape[2] in [1, 3] 30 | assert is_bw or is_rgb 31 | # get the mapping of keyboard keys to actions in the environment 32 | if hasattr(env, 'get_keys_to_action'): 33 | keys_to_action = env.get_keys_to_action() 34 | elif hasattr(env.unwrapped, 'get_keys_to_action'): 35 | keys_to_action = env.unwrapped.get_keys_to_action() 36 | else: 37 | raise ValueError('env has no get_keys_to_action method') 38 | # create the image viewer 39 | viewer = ImageViewer( 40 | env.spec.id if env.spec is not None else env.__class__.__name__, 41 | env.observation_space.shape[0], # height 42 | env.observation_space.shape[1], # width 43 | monitor_keyboard=True, 44 | relevant_keys=set(sum(map(list, keys_to_action.keys()), [])) 45 | ) 46 | # create a done flag for the environment 47 | done = True 48 | # prepare frame rate limiting 49 | target_frame_duration = 1 / env.metadata['video.frames_per_second'] 50 | last_frame_time = 0 51 | # start the main game loop 52 | try: 53 | while True: 54 | current_frame_time = time.time() 55 | # limit frame rate 56 | if last_frame_time + target_frame_duration > current_frame_time: 57 | continue 58 | # save frame beginning time for next refresh 59 | last_frame_time = current_frame_time 60 | # clock tick 61 | clock.tick() 62 | # reset if the environment is done 63 | if done: 64 | done = False 65 | state = env.reset() 66 | viewer.show(env.unwrapped.screen) 67 | # unwrap the action based on pressed relevant keys 68 | action = keys_to_action.get(viewer.pressed_keys, _NOP) 69 | next_state, reward, done, _ = env.step(action) 70 | viewer.show(env.unwrapped.screen) 71 | # pass the observation data through the callback 72 | if callback is not None: 73 | callback(state, action, reward, done, next_state) 74 | state = next_state 75 | # shutdown if the escape key is pressed 76 | if viewer.is_escape_pressed: 77 | break 78 | except KeyboardInterrupt: 79 | pass 80 | 81 | viewer.close() 82 | env.close() 83 | 84 | 85 | # explicitly define the outward facing API of the module 86 | __all__ = [play_human.__name__] 87 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # MARK: Project Files 2 | build/ 3 | 4 | 5 | 6 | # MARK: Python gitignore 7 | # https://github.com/github/gitignore/blob/master/Python.gitignore 8 | 9 | # Byte-compiled / optimized / DLL files 10 | __pycache__/ 11 | *.py[cod] 12 | *$py.class 13 | 14 | # C extensions 15 | # TODO: this might not need ignored for this package 16 | *.so 17 | 18 | # Distribution / packaging 19 | MANIFEST 20 | .Python 21 | build/ 22 | develop-eggs/ 23 | dist/ 24 | downloads/ 25 | eggs/ 26 | .eggs/ 27 | lib/ 28 | lib64/ 29 | parts/ 30 | sdist/ 31 | var/ 32 | wheels/ 33 | *.egg-info/ 34 | .installed.cfg 35 | *.egg 36 | 37 | # PyInstaller 38 | # Usually these files are written by a python script from a template 39 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 40 | *.manifest 41 | *.spec 42 | 43 | # Installer logs 44 | pip-log.txt 45 | pip-delete-this-directory.txt 46 | 47 | # Unit test / coverage reports 48 | htmlcov/ 49 | .tox/ 50 | .coverage 51 | .coverage.* 52 | .cache 53 | nosetests.xml 54 | coverage.xml 55 | *.cover 56 | .hypothesis/ 57 | 58 | # Translations 59 | *.mo 60 | *.pot 61 | 62 | # Django stuff: 63 | *.log 64 | local_settings.py 65 | 66 | # Flask stuff: 67 | instance/ 68 | .webassets-cache 69 | 70 | # Scrapy stuff: 71 | .scrapy 72 | 73 | # Sphinx documentation 74 | docs/_build/ 75 | 76 | # PyBuilder 77 | target/ 78 | 79 | # Jupyter Notebook 80 | .ipynb_checkpoints 81 | 82 | # pyenv 83 | .python-version 84 | 85 | # celery beat schedule file 86 | celerybeat-schedule 87 | 88 | # SageMath parsed files 89 | *.sage.py 90 | 91 | # Environments 92 | .env 93 | .venv 94 | env/ 95 | venv/ 96 | ENV/ 97 | 98 | # Spyder project settings 99 | .spyderproject 100 | .spyproject 101 | 102 | # Rope project settings 103 | .ropeproject 104 | 105 | # mkdocs documentation 106 | /site 107 | 108 | # mypy 109 | .mypy_cache/ 110 | 111 | 112 | 113 | 114 | # MARK: CPP Project Files 115 | # https://github.com/github/gitignore/blob/master/C%2B%2B.gitignore 116 | 117 | ## Prerequisites 118 | *.d 119 | 120 | ## Compiled Object files 121 | *.slo 122 | *.lo 123 | *.o 124 | *.obj 125 | 126 | ## Precompiled Headers 127 | *.gch 128 | *.pch 129 | 130 | ## Compiled Dynamic libraries 131 | *.so 132 | *.dylib 133 | *.dll 134 | 135 | ## Fortran module files 136 | *.mod 137 | *.smod 138 | 139 | ## Compiled Static libraries 140 | *.lai 141 | *.la 142 | *.a 143 | *.lib 144 | 145 | ## Executables 146 | *.exe 147 | *.out 148 | *.app 149 | 150 | 151 | 152 | # MARK: scons Files 153 | # https://github.com/github/gitignore/blob/master/SCons.gitignore 154 | 155 | .sconsign.dblite 156 | .scon* 157 | config.log 158 | 159 | 160 | 161 | # MacOS gitignore 162 | # https://raw.githubusercontent.com/github/gitignore/master/Global/macOS.gitignore 163 | 164 | ## General 165 | .DS_Store 166 | .AppleDouble 167 | .LSOverride 168 | 169 | ## Icon must end with two \r 170 | Icon 171 | 172 | ## Thumbnails 173 | ._* 174 | 175 | ## Files that might appear in the root of a volume 176 | .DocumentRevisions-V100 177 | .fseventsd 178 | .Spotlight-V100 179 | .TemporaryItems 180 | .Trashes 181 | .VolumeIcon.icns 182 | .com.apple.timemachine.donotpresent 183 | 184 | ## Directories potentially created on remote AFP share 185 | .AppleDB 186 | .AppleDesktop 187 | Network Trash Folder 188 | Temporary Items 189 | .apdisk -------------------------------------------------------------------------------- /nes_py/nes/src/picture_bus.cpp: -------------------------------------------------------------------------------- 1 | // Program: nes-py 2 | // File: picture_bus.cpp 3 | // Description: This class houses picture bus data from the PPU 4 | // 5 | // Copyright (c) 2019 Christian Kauten. All rights reserved. 6 | // 7 | 8 | #include "picture_bus.hpp" 9 | #include "log.hpp" 10 | 11 | namespace NES { 12 | 13 | NES_Byte PictureBus::read(NES_Address address) { 14 | if (address < 0x2000) { 15 | return mapper->readCHR(address); 16 | } else if (address < 0x3eff) { // Name tables up to 0x3000, then mirrored up to 0x3ff 17 | if (address < 0x2400) // NT0 18 | return ram[name_tables[0] + (address & 0x3ff)]; 19 | else if (address < 0x2800) // NT1 20 | return ram[name_tables[1] + (address & 0x3ff)]; 21 | else if (address < 0x2c00) // NT2 22 | return ram[name_tables[2] + (address & 0x3ff)]; 23 | else // NT3 24 | return ram[name_tables[3] + (address & 0x3ff)]; 25 | } else if (address < 0x3fff) { 26 | return palette[address & 0x1f]; 27 | } 28 | return 0; 29 | } 30 | 31 | void PictureBus::write(NES_Address address, NES_Byte value) { 32 | if (address < 0x2000) { 33 | mapper->writeCHR(address, value); 34 | } else if (address < 0x3eff) { // Name tables up to 0x3000, then mirrored up to 0x3ff 35 | if (address < 0x2400) // NT0 36 | ram[name_tables[0] + (address & 0x3ff)] = value; 37 | else if (address < 0x2800) // NT1 38 | ram[name_tables[1] + (address & 0x3ff)] = value; 39 | else if (address < 0x2c00) // NT2 40 | ram[name_tables[2] + (address & 0x3ff)] = value; 41 | else // NT3 42 | ram[name_tables[3] + (address & 0x3ff)] = value; 43 | } else if (address < 0x3fff) { 44 | if (address == 0x3f10) 45 | palette[0] = value; 46 | else 47 | palette[address & 0x1f] = value; 48 | } 49 | } 50 | 51 | void PictureBus::update_mirroring() { 52 | switch (mapper->getNameTableMirroring()) { 53 | case HORIZONTAL: 54 | name_tables[0] = name_tables[1] = 0; 55 | name_tables[2] = name_tables[3] = 0x400; 56 | LOG(InfoVerbose) << 57 | "Horizontal Name Table mirroring set. (Vertical Scrolling)" << 58 | std::endl; 59 | break; 60 | case VERTICAL: 61 | name_tables[0] = name_tables[2] = 0; 62 | name_tables[1] = name_tables[3] = 0x400; 63 | LOG(InfoVerbose) << 64 | "Vertical Name Table mirroring set. (Horizontal Scrolling)" << 65 | std::endl; 66 | break; 67 | case ONE_SCREEN_LOWER: 68 | name_tables[0] = name_tables[1] = name_tables[2] = name_tables[3] = 0; 69 | LOG(InfoVerbose) << 70 | "Single Screen mirroring set with lower bank." << 71 | std::endl; 72 | break; 73 | case ONE_SCREEN_HIGHER: 74 | name_tables[0] = name_tables[1] = name_tables[2] = name_tables[3] = 0x400; 75 | LOG(InfoVerbose) << 76 | "Single Screen mirroring set with higher bank." << 77 | std::endl; 78 | break; 79 | default: 80 | name_tables[0] = name_tables[1] = name_tables[2] = name_tables[3] = 0; 81 | LOG(Error) << 82 | "Unsupported Name Table mirroring : " << 83 | mapper->getNameTableMirroring() << 84 | std::endl; 85 | } 86 | } 87 | 88 | } // namespace NES 89 | -------------------------------------------------------------------------------- /nes_py/nes/include/emulator.hpp: -------------------------------------------------------------------------------- 1 | // Program: nes-py 2 | // File: emulator.hpp 3 | // Description: This class houses the logic and data for an NES emulator 4 | // 5 | // Copyright (c) 2019 Christian Kauten. All rights reserved. 6 | // 7 | 8 | #ifndef EMULATOR_HPP 9 | #define EMULATOR_HPP 10 | 11 | #include 12 | #include "common.hpp" 13 | #include "cartridge.hpp" 14 | #include "controller.hpp" 15 | #include "cpu.hpp" 16 | #include "ppu.hpp" 17 | #include "main_bus.hpp" 18 | #include "picture_bus.hpp" 19 | 20 | namespace NES { 21 | 22 | /// An NES Emulator and OpenAI Gym interface 23 | class Emulator { 24 | private: 25 | /// The number of cycles in 1 frame 26 | static const int CYCLES_PER_FRAME = 29781; 27 | /// the virtual cartridge with ROM and mapper data 28 | Cartridge cartridge; 29 | /// the 2 controllers on the emulator 30 | Controller controllers[2]; 31 | 32 | /// the main data bus of the emulator 33 | MainBus bus; 34 | /// the picture bus from the PPU of the emulator 35 | PictureBus picture_bus; 36 | /// The emulator's CPU 37 | CPU cpu; 38 | /// the emulators' PPU 39 | PPU ppu; 40 | 41 | /// the main data bus of the emulator 42 | MainBus backup_bus; 43 | /// the picture bus from the PPU of the emulator 44 | PictureBus backup_picture_bus; 45 | /// The emulator's CPU 46 | CPU backup_cpu; 47 | /// the emulators' PPU 48 | PPU backup_ppu; 49 | 50 | public: 51 | /// The width of the NES screen in pixels 52 | static const int WIDTH = SCANLINE_VISIBLE_DOTS; 53 | /// The height of the NES screen in pixels 54 | static const int HEIGHT = VISIBLE_SCANLINES; 55 | 56 | /// Initialize a new emulator with a path to a ROM file. 57 | /// 58 | /// @param rom_path the path to the ROM for the emulator to run 59 | /// 60 | explicit Emulator(std::string rom_path); 61 | 62 | /// Return a 32-bit pointer to the screen buffer's first address. 63 | /// 64 | /// @return a 32-bit pointer to the screen buffer's first address 65 | /// 66 | inline NES_Pixel* get_screen_buffer() { return ppu.get_screen_buffer(); } 67 | 68 | /// Return a 8-bit pointer to the RAM buffer's first address. 69 | /// 70 | /// @return a 8-bit pointer to the RAM buffer's first address 71 | /// 72 | inline NES_Byte* get_memory_buffer() { return bus.get_memory_buffer(); } 73 | 74 | /// Return a pointer to a controller port 75 | /// 76 | /// @param port the port of the controller to return the pointer to 77 | /// @return a pointer to the byte buffer for the controller state 78 | /// 79 | inline NES_Byte* get_controller(int port) { 80 | return controllers[port].get_joypad_buffer(); 81 | } 82 | 83 | /// Load the ROM into the NES. 84 | inline void reset() { cpu.reset(bus); ppu.reset(); } 85 | 86 | /// Perform a step on the emulator, i.e., a single frame. 87 | void step(); 88 | 89 | /// Create a backup state on the emulator. 90 | inline void backup() { 91 | backup_bus = bus; 92 | backup_picture_bus = picture_bus; 93 | backup_cpu = cpu; 94 | backup_ppu = ppu; 95 | } 96 | 97 | /// Restore the backup state on the emulator. 98 | inline void restore() { 99 | bus = backup_bus; 100 | picture_bus = backup_picture_bus; 101 | cpu = backup_cpu; 102 | ppu = backup_ppu; 103 | } 104 | }; 105 | 106 | } // namespace NES 107 | 108 | #endif // EMULATOR_HPP 109 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: python # this works for Linux but is an error on macOS or Windows 3 | matrix: 4 | include: 5 | # - name: "Python 3.5 on Ubuntu 14.04" 6 | # python: 3.5 7 | # dist: trusty 8 | # before_install: pip3 install scons 9 | # - name: "Python 3.6 on Ubuntu 14.04" 10 | # python: 3.6 11 | # dist: trusty 12 | # before_install: pip3 install scons 13 | - name: "Python 3.5 on Ubuntu 16.04" 14 | python: 3.5 15 | dist: xenial 16 | before_install: pip3 install scons 17 | - name: "Python 3.6 on Ubuntu 16.04" 18 | python: 3.6 19 | dist: xenial 20 | before_install: pip3 install scons 21 | - name: "Python 3.7 on Ubuntu 16.04" 22 | python: 3.7 23 | dist: xenial 24 | before_install: pip3 install scons 25 | - name: "Python 3.8 on Ubuntu 16.04" 26 | python: 3.8 27 | dist: xenial 28 | before_install: 29 | - sudo apt-get install libsdl-image1.2-dev libsdl-mixer1.2-dev libsdl-ttf2.0-dev libsmpeg-dev libsdl1.2-dev libportmidi-dev libswscale-dev libavformat-dev libavcodec-dev libtiff5-dev libx11-6 libx11-dev fluid-soundfont-gm timgm6mb-soundfont xfonts-base xfonts-100dpi xfonts-75dpi xfonts-cyrillic fontconfig fonts-freefont-ttf libfreetype6-dev 30 | - pip3 install scons 31 | - name: "Python 3.9 on Ubuntu 16.04" 32 | python: 3.9 33 | dist: xenial 34 | before_install: 35 | - sudo apt-get install libsdl-image1.2-dev libsdl-mixer1.2-dev libsdl-ttf2.0-dev libsmpeg-dev libsdl1.2-dev libportmidi-dev libswscale-dev libavformat-dev libavcodec-dev libtiff5-dev libx11-6 libx11-dev fluid-soundfont-gm timgm6mb-soundfont xfonts-base xfonts-100dpi xfonts-75dpi xfonts-cyrillic fontconfig fonts-freefont-ttf libfreetype6-dev 36 | - pip3 install scons 37 | - name: "Python 3.5 on Ubuntu 18.04" 38 | python: 3.5 39 | dist: bionic 40 | before_install: pip3 install scons 41 | - name: "Python 3.6 on Ubuntu 18.04" 42 | python: 3.6 43 | dist: bionic 44 | before_install: pip3 install scons 45 | - name: "Python 3.7 on Ubuntu 18.04" 46 | python: 3.7 47 | dist: bionic 48 | before_install: pip3 install scons 49 | - name: "Python 3.8 on Ubuntu 18.04" 50 | python: 3.8 51 | dist: bionic 52 | before_install: 53 | - sudo apt-get install libsdl-image1.2-dev libsdl-mixer1.2-dev libsdl-ttf2.0-dev libsmpeg-dev libsdl1.2-dev libportmidi-dev libswscale-dev libavformat-dev libavcodec-dev libtiff5-dev libx11-6 libx11-dev fluid-soundfont-gm timgm6mb-soundfont xfonts-base xfonts-100dpi xfonts-75dpi xfonts-cyrillic fontconfig fonts-freefont-ttf libfreetype6-dev 54 | - pip3 install scons 55 | - name: "Python 3.9 on Ubuntu 18.04" 56 | python: 3.9 57 | dist: bionic 58 | before_install: 59 | - sudo apt-get install libsdl-image1.2-dev libsdl-mixer1.2-dev libsdl-ttf2.0-dev libsmpeg-dev libsdl1.2-dev libportmidi-dev libswscale-dev libavformat-dev libavcodec-dev libtiff5-dev libx11-6 libx11-dev fluid-soundfont-gm timgm6mb-soundfont xfonts-base xfonts-100dpi xfonts-75dpi xfonts-cyrillic fontconfig fonts-freefont-ttf libfreetype6-dev 60 | - pip3 install scons 61 | # - name: "Python 3.7 on macOS 10.14 (Xcode 10.2)" 62 | # os: osx 63 | # osx_image: xcode10.2 64 | # language: shell 65 | # before_install: brew install scons 66 | install: 67 | - pip3 install -r requirements.txt 68 | script: 69 | - scons -C nes_py/nes 70 | - mv nes_py/nes/lib_nes_env*.so nes_py 71 | - python3 -m unittest discover . 72 | - python3 setup.py sdist bdist_wheel 73 | notifications: 74 | email: false 75 | -------------------------------------------------------------------------------- /nes_py/nes/include/mappers/mapper_SxROM.hpp: -------------------------------------------------------------------------------- 1 | // Program: nes-py 2 | // File: mapper_SxROM.hpp 3 | // Description: An implementation of the SxROM mapper 4 | // 5 | // Copyright (c) 2019 Christian Kauten. All rights reserved. 6 | // 7 | 8 | #ifndef MAPPERSXROM_HPP 9 | #define MAPPERSXROM_HPP 10 | 11 | #include 12 | #include "common.hpp" 13 | #include "mapper.hpp" 14 | 15 | namespace NES { 16 | 17 | class MapperSxROM : public Mapper { 18 | private: 19 | /// The mirroring callback on the PPU 20 | std::function mirroring_callback; 21 | /// the mirroring mode on the device 22 | NameTableMirroring mirroring; 23 | /// whether the cartridge uses character RAM 24 | bool has_character_ram; 25 | /// the mode for CHR ROM 26 | int mode_chr; 27 | /// the mode for PRG ROM 28 | int mode_prg; 29 | /// a temporary register 30 | NES_Byte temp_register; 31 | /// a write counter 32 | int write_counter; 33 | /// the PRG register 34 | NES_Byte register_prg; 35 | /// The first CHR register 36 | NES_Byte register_chr0; 37 | /// The second CHR register 38 | NES_Byte register_chr1; 39 | /// The first PRG bank 40 | std::size_t first_bank_prg; 41 | /// The second PRG bank 42 | std::size_t second_bank_prg; 43 | /// The first CHR bank 44 | std::size_t first_bank_chr; 45 | /// The second CHR bank 46 | std::size_t second_bank_chr; 47 | /// The character RAM on the cartridge 48 | std::vector character_ram; 49 | 50 | /// TODO: what does this do 51 | void calculatePRGPointers(); 52 | 53 | public: 54 | /// Create a new mapper with a cartridge. 55 | /// 56 | /// @param cart a reference to a cartridge for the mapper to access 57 | /// @param mirroring_cb the callback to change mirroring modes on the PPU 58 | /// 59 | MapperSxROM(Cartridge* cart, std::function mirroring_cb); 60 | 61 | /// Read a byte from the PRG RAM. 62 | /// 63 | /// @param address the 16-bit address of the byte to read 64 | /// @return the byte located at the given address in PRG RAM 65 | /// 66 | inline NES_Byte readPRG(NES_Address address) { 67 | if (address < 0xc000) 68 | return cartridge->getROM()[first_bank_prg + (address & 0x3fff)]; 69 | else 70 | return cartridge->getROM()[second_bank_prg + (address & 0x3fff)]; 71 | } 72 | 73 | /// Write a byte to an address in the PRG RAM. 74 | /// 75 | /// @param address the 16-bit address to write to 76 | /// @param value the byte to write to the given address 77 | /// 78 | void writePRG(NES_Address address, NES_Byte value); 79 | 80 | /// Read a byte from the CHR RAM. 81 | /// 82 | /// @param address the 16-bit address of the byte to read 83 | /// @return the byte located at the given address in CHR RAM 84 | /// 85 | inline NES_Byte readCHR(NES_Address address) { 86 | if (has_character_ram) 87 | return character_ram[address]; 88 | else if (address < 0x1000) 89 | return cartridge->getVROM()[first_bank_chr + address]; 90 | else 91 | return cartridge->getVROM()[second_bank_chr + (address & 0xfff)]; 92 | } 93 | 94 | /// Write a byte to an address in the CHR RAM. 95 | /// 96 | /// @param address the 16-bit address to write to 97 | /// @param value the byte to write to the given address 98 | /// 99 | void writeCHR(NES_Address address, NES_Byte value); 100 | 101 | /// Return the name table mirroring mode of this mapper. 102 | inline NameTableMirroring getNameTableMirroring() { return mirroring; } 103 | }; 104 | 105 | } // namespace NES 106 | 107 | #endif // MAPPERSXROM_HPP 108 | -------------------------------------------------------------------------------- /nes_py/nes/include/main_bus.hpp: -------------------------------------------------------------------------------- 1 | // Program: nes-py 2 | // File: main_bus.hpp 3 | // Description: This class houses the main bus data for the NES 4 | // 5 | // Copyright (c) 2019 Christian Kauten. All rights reserved. 6 | // 7 | 8 | #ifndef MAIN_BUS_HPP 9 | #define MAIN_BUS_HPP 10 | 11 | #include 12 | #include 13 | #include "common.hpp" 14 | #include "mapper.hpp" 15 | 16 | namespace NES { 17 | 18 | /// The IO registers on the main bus 19 | enum IORegisters { 20 | PPUCTRL = 0x2000, 21 | PPUMASK, 22 | PPUSTATUS, 23 | OAMADDR, 24 | OAMDATA, 25 | PPUSCROL, 26 | PPUADDR, 27 | PPUDATA, 28 | OAMDMA = 0x4014, 29 | JOY1 = 0x4016, 30 | JOY2 = 0x4017, 31 | }; 32 | 33 | /// An enum functor object for calculating the hash of an enum class 34 | /// https://stackoverflow.com/questions/18837857/cant-use-enum-class-as-unordered-map-key 35 | struct EnumClassHash { 36 | template 37 | std::size_t operator()(T t) const { return static_cast(t); } 38 | }; 39 | 40 | /// a type for write callback functions 41 | typedef std::function WriteCallback; 42 | /// a map type from IORegsiters to WriteCallbacks 43 | typedef std::unordered_map IORegisterToWriteCallbackMap; 44 | /// a type for read callback functions 45 | typedef std::function ReadCallback; 46 | /// a map type from IORegsiters to ReadCallbacks 47 | typedef std::unordered_map IORegisterToReadCallbackMap; 48 | 49 | /// The main bus for data to travel along the NES hardware 50 | class MainBus { 51 | private: 52 | /// The RAM on the main bus 53 | std::vector ram; 54 | /// The extended RAM (if the mapper has extended RAM) 55 | std::vector extended_ram; 56 | /// a pointer to the mapper on the cartridge 57 | Mapper* mapper; 58 | /// a map of IO registers to callback methods for writes 59 | IORegisterToWriteCallbackMap write_callbacks; 60 | /// a map of IO registers to callback methods for reads 61 | IORegisterToReadCallbackMap read_callbacks; 62 | 63 | public: 64 | /// Initialize a new main bus. 65 | MainBus() : ram(0x800, 0), mapper(nullptr) { } 66 | 67 | /// Return a 8-bit pointer to the RAM buffer's first address. 68 | /// 69 | /// @return a 8-bit pointer to the RAM buffer's first address 70 | /// 71 | inline NES_Byte* get_memory_buffer() { return &ram.front(); } 72 | 73 | /// Read a byte from an address on the RAM. 74 | /// 75 | /// @param address the 16-bit address of the byte to read in the RAM 76 | /// 77 | /// @return the byte located at the given address 78 | /// 79 | NES_Byte read(NES_Address address); 80 | 81 | /// Write a byte to an address in the RAM. 82 | /// 83 | /// @param address the 16-bit address to write the byte to in RAM 84 | /// @param value the byte to write to the given address 85 | /// 86 | void write(NES_Address address, NES_Byte value); 87 | 88 | /// Set the mapper pointer to a new value. 89 | /// 90 | /// @param mapper the new mapper pointer for the bus to use 91 | /// 92 | void set_mapper(Mapper* mapper); 93 | 94 | /// Set a callback for when writes occur. 95 | inline void set_write_callback(IORegisters reg, WriteCallback callback) { 96 | write_callbacks.insert({reg, callback}); 97 | } 98 | 99 | /// Set a callback for when reads occur. 100 | inline void set_read_callback(IORegisters reg, ReadCallback callback) { 101 | read_callbacks.insert({reg, callback}); 102 | } 103 | 104 | /// Return a pointer to the page in memory. 105 | const NES_Byte* get_page_pointer(NES_Byte page); 106 | }; 107 | 108 | } // namespace NES 109 | 110 | #endif // MAIN_BUS_HPP 111 | -------------------------------------------------------------------------------- /nes_py/nes/src/main_bus.cpp: -------------------------------------------------------------------------------- 1 | // Program: nes-py 2 | // File: main_bus.cpp 3 | // Description: This class houses the main bus data for the NES 4 | // 5 | // Copyright (c) 2019 Christian Kauten. All rights reserved. 6 | // 7 | 8 | #include "main_bus.hpp" 9 | #include "log.hpp" 10 | 11 | namespace NES { 12 | 13 | NES_Byte MainBus::read(NES_Address address) { 14 | if (address < 0x2000) { 15 | return ram[address & 0x7ff]; 16 | } else if (address < 0x4020) { 17 | if (address < 0x4000) { // PPU registers, mirrored 18 | auto reg = static_cast(address & 0x2007); 19 | if (read_callbacks.count(reg)) 20 | return read_callbacks.at(reg)(); 21 | else 22 | LOG(InfoVerbose) << "No read callback registered for I/O register at: " << std::hex << +address << std::endl; 23 | } else if (address < 0x4018 && address >= 0x4014) { // only *some* IO registers 24 | auto reg = static_cast(address); 25 | if (read_callbacks.count(reg)) 26 | return read_callbacks.at(reg)(); 27 | else 28 | LOG(InfoVerbose) << "No read callback registered for I/O register at: " << std::hex << +address << std::endl; 29 | } 30 | else { 31 | LOG(InfoVerbose) << "Read access attempt at: " << std::hex << +address << std::endl; 32 | } 33 | } else if (address < 0x6000) { 34 | LOG(InfoVerbose) << "Expansion ROM read attempted. This is currently unsupported" << std::endl; 35 | } else if (address < 0x8000) { 36 | if (mapper->hasExtendedRAM()) 37 | return extended_ram[address - 0x6000]; 38 | } else { 39 | return mapper->readPRG(address); 40 | } 41 | return 0; 42 | } 43 | 44 | void MainBus::write(NES_Address address, NES_Byte value) { 45 | if (address < 0x2000) { 46 | ram[address & 0x7ff] = value; 47 | } else if (address < 0x4020) { 48 | if (address < 0x4000) { // PPU registers, mirrored 49 | auto reg = static_cast(address & 0x2007); 50 | if (write_callbacks.count(reg)) 51 | return write_callbacks.at(reg)(value); 52 | else 53 | LOG(InfoVerbose) << "No write callback registered for I/O register at: " << std::hex << +address << std::endl; 54 | } else if (address < 0x4017 && address >= 0x4014) { // only some registers 55 | auto reg = static_cast(address); 56 | if (write_callbacks.count(reg)) 57 | return write_callbacks.at(reg)(value); 58 | else 59 | LOG(InfoVerbose) << "No write callback registered for I/O register at: " << std::hex << +address << std::endl; 60 | } else { 61 | LOG(InfoVerbose) << "Write access attmept at: " << std::hex << +address << std::endl; 62 | } 63 | } else if (address < 0x6000) { 64 | LOG(InfoVerbose) << "Expansion ROM access attempted. This is currently unsupported" << std::endl; 65 | } else if (address < 0x8000) { 66 | if (mapper->hasExtendedRAM()) 67 | extended_ram[address - 0x6000] = value; 68 | } else { 69 | mapper->writePRG(address, value); 70 | } 71 | } 72 | 73 | const NES_Byte* MainBus::get_page_pointer(NES_Byte page) { 74 | NES_Address address = page << 8; 75 | if (address < 0x2000) 76 | return &ram[address & 0x7ff]; 77 | else if (address < 0x4020) 78 | LOG(Error) << "Register address memory pointer access attempt" << std::endl; 79 | else if (address < 0x6000) 80 | LOG(Error) << "Expansion ROM access attempted, which is unsupported" << std::endl; 81 | else if (address < 0x8000) 82 | if (mapper->hasExtendedRAM()) 83 | return &extended_ram[address - 0x6000]; 84 | 85 | return nullptr; 86 | } 87 | 88 | void MainBus::set_mapper(Mapper* mapper) { 89 | this->mapper = mapper; 90 | if (mapper->hasExtendedRAM()) 91 | extended_ram.resize(0x2000); 92 | } 93 | 94 | } // namespace NES 95 | -------------------------------------------------------------------------------- /nes_py/nes/include/cpu_opcodes.hpp: -------------------------------------------------------------------------------- 1 | // Program: nes-py 2 | // File: cpu_opcodes.hpp 3 | // Description: This file defines relevant CPU opcodes 4 | // 5 | // Copyright (c) 2019 Christian Kauten. All rights reserved. 6 | // 7 | 8 | #ifndef CPU_OPCODES_HPP 9 | #define CPU_OPCODES_HPP 10 | 11 | #include "common.hpp" 12 | 13 | namespace NES { 14 | 15 | const auto INSTRUCTION_MODE_MASK = 0x3; 16 | 17 | const auto OPERATION_MASK = 0xe0; 18 | const auto OPERATION_SHIFT = 5; 19 | 20 | const auto ADRESS_MODE_MASK = 0x1c; 21 | const auto ADDRESS_MODE_SHIFT = 2; 22 | 23 | const auto BRANCH_INSTRUCTION_MASK = 0x1f; 24 | const auto BRANCH_INSTRUCTION_MASK_RESULT = 0x10; 25 | const auto BRANCH_CONDITION_MASK = 0x20; 26 | const auto BRANCH_ON_FLAG_SHIFT = 6; 27 | 28 | const auto NMI_VECTOR = 0xfffa; 29 | const auto RESET_VECTOR = 0xfffc; 30 | const auto IRQ_VECTOR = 0xfffe; 31 | 32 | enum BranchOnFlag { 33 | NEGATIVE_, 34 | OVERFLOW_, 35 | CARRY_, 36 | ZERO_, 37 | }; 38 | 39 | enum Operation1 { 40 | ORA, 41 | AND, 42 | EOR, 43 | ADC, 44 | STA, 45 | LDA, 46 | CMP, 47 | SBC, 48 | }; 49 | 50 | enum AddrMode1 { 51 | M1_INDEXED_INDIRECT_X, 52 | M1_ZERO_PAGE, 53 | M1_IMMEDIATE, 54 | M1_ABSOLUTE, 55 | M1_INDIRECT_Y, 56 | M1_INDEXED_X, 57 | M1_ABSOLUTE_Y, 58 | M1_ABSOLUTE_X, 59 | }; 60 | 61 | enum Operation2 { 62 | ASL, 63 | ROL, 64 | LSR, 65 | ROR, 66 | STX, 67 | LDX, 68 | DEC, 69 | INC, 70 | }; 71 | 72 | enum AddrMode2 { 73 | M2_IMMEDIATE, 74 | M2_ZERO_PAGE, 75 | M2_ACCUMULATOR, 76 | M2_ABSOLUTE, 77 | M2_INDEXED = 5, 78 | M2_ABSOLUTE_INDEXED = 7, 79 | }; 80 | 81 | enum Operation0 { 82 | BIT = 1, 83 | STY = 4, 84 | LDY, 85 | CPY, 86 | CPX, 87 | }; 88 | 89 | /// Implied mode opcodes 90 | enum OperationImplied { 91 | BRK = 0x00, 92 | PHP = 0x08, 93 | CLC = 0x18, 94 | JSR = 0x20, 95 | PLP = 0x28, 96 | SEC = 0x38, 97 | RTI = 0x40, 98 | PHA = 0x48, 99 | JMP = 0x4C, 100 | CLI = 0x58, 101 | RTS = 0x60, 102 | PLA = 0x68, 103 | JMPI = 0x6C, // JMP indirect 104 | SEI = 0x78, 105 | DEY = 0x88, 106 | TXA = 0x8a, 107 | TYA = 0x98, 108 | TXS = 0x9a, 109 | TAY = 0xa8, 110 | TAX = 0xaa, 111 | CLV = 0xb8, 112 | TSX = 0xba, 113 | INY = 0xc8, 114 | DEX = 0xca, 115 | CLD = 0xd8, 116 | INX = 0xe8, 117 | NOP = 0xea, 118 | SED = 0xf8, 119 | }; 120 | 121 | /// A structure for working with the flags register 122 | typedef union { 123 | struct { 124 | bool N : 1, 125 | V : 1, 126 | ONE : 1, 127 | B : 1, 128 | D : 1, 129 | I : 1, 130 | Z : 1, 131 | C : 1; 132 | } bits; 133 | NES_Byte byte; 134 | } CPU_Flags; 135 | 136 | /// a mapping of opcodes to the number of cycles used by the opcode. 0 implies 137 | /// an unused opcode. 138 | const NES_Byte OPERATION_CYCLES[0x100] = { 139 | 7, 6, 0, 0, 0, 3, 5, 0, 3, 2, 2, 0, 0, 4, 6, 0, 140 | 2, 5, 0, 0, 0, 4, 6, 0, 2, 4, 0, 0, 0, 4, 7, 0, 141 | 6, 6, 0, 0, 3, 3, 5, 0, 4, 2, 2, 0, 4, 4, 6, 0, 142 | 2, 5, 0, 0, 0, 4, 6, 0, 2, 4, 0, 0, 0, 4, 7, 0, 143 | 6, 6, 0, 0, 0, 3, 5, 0, 3, 2, 2, 0, 3, 4, 6, 0, 144 | 2, 5, 0, 0, 0, 4, 6, 0, 2, 4, 0, 0, 0, 4, 7, 0, 145 | 6, 6, 0, 0, 0, 3, 5, 0, 4, 2, 2, 0, 5, 4, 6, 0, 146 | 2, 5, 0, 0, 0, 4, 6, 0, 2, 4, 0, 0, 0, 4, 7, 0, 147 | 0, 6, 0, 0, 3, 3, 3, 0, 2, 0, 2, 0, 4, 4, 4, 0, 148 | 2, 6, 0, 0, 4, 4, 4, 0, 2, 5, 2, 0, 0, 5, 0, 0, 149 | 2, 6, 2, 0, 3, 3, 3, 0, 2, 2, 2, 0, 4, 4, 4, 0, 150 | 2, 5, 0, 0, 4, 4, 4, 0, 2, 4, 2, 0, 4, 4, 4, 0, 151 | 2, 6, 0, 0, 3, 3, 5, 0, 2, 2, 2, 0, 4, 4, 6, 0, 152 | 2, 5, 0, 0, 0, 4, 6, 0, 2, 4, 0, 0, 0, 4, 7, 0, 153 | 2, 6, 0, 0, 3, 3, 5, 0, 2, 2, 2, 2, 4, 4, 6, 0, 154 | 2, 5, 0, 0, 0, 4, 6, 0, 2, 4, 0, 0, 0, 4, 7, 0, 155 | }; 156 | 157 | } // namespace NES 158 | 159 | #endif // CPU_OPCODES_HPP 160 | -------------------------------------------------------------------------------- /nes_py/wrappers/joypad_space.py: -------------------------------------------------------------------------------- 1 | """An environment wrapper to convert binary to discrete action space.""" 2 | import gym 3 | from gym import Env 4 | from gym import Wrapper 5 | 6 | 7 | class JoypadSpace(Wrapper): 8 | """An environment wrapper to convert binary to discrete action space.""" 9 | 10 | # a mapping of buttons to binary values 11 | _button_map = { 12 | 'right': 0b10000000, 13 | 'left': 0b01000000, 14 | 'down': 0b00100000, 15 | 'up': 0b00010000, 16 | 'start': 0b00001000, 17 | 'select': 0b00000100, 18 | 'B': 0b00000010, 19 | 'A': 0b00000001, 20 | 'NOOP': 0b00000000, 21 | } 22 | 23 | @classmethod 24 | def buttons(cls) -> list: 25 | """Return the buttons that can be used as actions.""" 26 | return list(cls._button_map.keys()) 27 | 28 | def __init__(self, env: Env, actions: list): 29 | """ 30 | Initialize a new binary to discrete action space wrapper. 31 | 32 | Args: 33 | env: the environment to wrap 34 | actions: an ordered list of actions (as lists of buttons). 35 | The index of each button list is its discrete coded value 36 | 37 | Returns: 38 | None 39 | 40 | """ 41 | super().__init__(env) 42 | # create the new action space 43 | self.action_space = gym.spaces.Discrete(len(actions)) 44 | # create the action map from the list of discrete actions 45 | self._action_map = {} 46 | self._action_meanings = {} 47 | # iterate over all the actions (as button lists) 48 | for action, button_list in enumerate(actions): 49 | # the value of this action's bitmap 50 | byte_action = 0 51 | # iterate over the buttons in this button list 52 | for button in button_list: 53 | byte_action |= self._button_map[button] 54 | # set this action maps value to the byte action value 55 | self._action_map[action] = byte_action 56 | self._action_meanings[action] = ' '.join(button_list) 57 | 58 | def step(self, action): 59 | """ 60 | Take a step using the given action. 61 | 62 | Args: 63 | action (int): the discrete action to perform 64 | 65 | Returns: 66 | a tuple of: 67 | - (numpy.ndarray) the state as a result of the action 68 | - (float) the reward achieved by taking the action 69 | - (bool) a flag denoting whether the episode has ended 70 | - (dict) a dictionary of extra information 71 | 72 | """ 73 | # take the step and record the output 74 | return self.env.step(self._action_map[action]) 75 | 76 | def reset(self): 77 | """Reset the environment and return the initial observation.""" 78 | return self.env.reset() 79 | 80 | def get_keys_to_action(self): 81 | """Return the dictionary of keyboard keys to actions.""" 82 | # get the old mapping of keys to actions 83 | old_keys_to_action = self.env.unwrapped.get_keys_to_action() 84 | # invert the keys to action mapping to lookup key combos by action 85 | action_to_keys = {v: k for k, v in old_keys_to_action.items()} 86 | # create a new mapping of keys to actions 87 | keys_to_action = {} 88 | # iterate over the actions and their byte values in this mapper 89 | for action, byte in self._action_map.items(): 90 | # get the keys to press for the action 91 | keys = action_to_keys[byte] 92 | # set the keys value in the dictionary to the current discrete act 93 | keys_to_action[keys] = action 94 | 95 | return keys_to_action 96 | 97 | def get_action_meanings(self): 98 | """Return a list of actions meanings.""" 99 | actions = sorted(self._action_meanings.keys()) 100 | return [self._action_meanings[action] for action in actions] 101 | 102 | 103 | # explicitly define the outward facing API of this module 104 | __all__ = [JoypadSpace.__name__] 105 | -------------------------------------------------------------------------------- /nes_py/tests/test_nes_env.py: -------------------------------------------------------------------------------- 1 | """Test cases for the NESEnv class.""" 2 | from unittest import TestCase 3 | import gym 4 | import numpy as np 5 | from .rom_file_abs_path import rom_file_abs_path 6 | from nes_py.nes_env import NESEnv 7 | 8 | 9 | class ShouldRaiseTypeErrorOnInvalidROMPathType(TestCase): 10 | def test(self): 11 | self.assertRaises(TypeError, NESEnv, 0) 12 | 13 | 14 | class ShouldRaiseValueErrorOnMissingNonexistentROMFile(TestCase): 15 | def test(self): 16 | path = rom_file_abs_path('missing.nes') 17 | self.assertRaises(ValueError, NESEnv, path) 18 | 19 | 20 | class ShouldRaiseValueErrorOnNonexistentFile(TestCase): 21 | def test(self): 22 | self.assertRaises(ValueError, NESEnv, 'not_a_file.nes') 23 | 24 | 25 | class ShouldRaiseValueErrorOnNoniNES_ROMPath(TestCase): 26 | def test(self): 27 | self.assertRaises(ValueError, NESEnv, rom_file_abs_path('blank')) 28 | 29 | 30 | class ShouldRaiseValueErrorOnInvalidiNES_ROMPath(TestCase): 31 | def test(self): 32 | self.assertRaises(ValueError, NESEnv, rom_file_abs_path('empty.nes')) 33 | 34 | 35 | class ShouldRaiseErrorOnStepBeforeReset(TestCase): 36 | def test(self): 37 | env = NESEnv(rom_file_abs_path('super-mario-bros-1.nes')) 38 | self.assertRaises(ValueError, env.step, 0) 39 | 40 | 41 | class ShouldCreateInstanceOfNESEnv(TestCase): 42 | def test(self): 43 | env = NESEnv(rom_file_abs_path('super-mario-bros-1.nes')) 44 | self.assertIsInstance(env, gym.Env) 45 | env.close() 46 | 47 | 48 | def create_smb1_instance(): 49 | """Return a new SMB1 instance.""" 50 | return NESEnv(rom_file_abs_path('super-mario-bros-1.nes')) 51 | 52 | 53 | class ShouldReadAndWriteMemory(TestCase): 54 | def test(self): 55 | env = create_smb1_instance() 56 | env.reset() 57 | for _ in range(90): 58 | env.step(8) 59 | env.step(0) 60 | self.assertEqual(129, env.ram[0x0776]) 61 | env.ram[0x0776] = 0 62 | self.assertEqual(0, env.ram[0x0776]) 63 | env.close() 64 | 65 | 66 | class ShouldResetAndCloseEnv(TestCase): 67 | def test(self): 68 | env = create_smb1_instance() 69 | env.reset() 70 | env.close() 71 | # trying to close again should raise an error 72 | self.assertRaises(ValueError, env.close) 73 | 74 | 75 | class ShouldStepEnv(TestCase): 76 | def test(self): 77 | env = create_smb1_instance() 78 | done = True 79 | for _ in range(500): 80 | if done: 81 | # reset the environment and check the output value 82 | state = env.reset() 83 | self.assertIsInstance(state, np.ndarray) 84 | # sample a random action and check it 85 | action = env.action_space.sample() 86 | self.assertIsInstance(action, int) 87 | # take a step and check the outputs 88 | output = env.step(action) 89 | self.assertIsInstance(output, tuple) 90 | self.assertEqual(4, len(output)) 91 | # check each output 92 | state, reward, done, info = output 93 | self.assertIsInstance(state, np.ndarray) 94 | self.assertIsInstance(reward, float) 95 | self.assertIsInstance(done, bool) 96 | self.assertIsInstance(info, dict) 97 | # check the render output 98 | render = env.render('rgb_array') 99 | self.assertIsInstance(render, np.ndarray) 100 | env.reset() 101 | env.close() 102 | 103 | 104 | class ShouldStepEnvBackupRestore(TestCase): 105 | def test(self): 106 | done = True 107 | env = create_smb1_instance() 108 | 109 | for _ in range(250): 110 | if done: 111 | state = env.reset() 112 | done = False 113 | state, _, done, _ = env.step(0) 114 | 115 | backup = state.copy() 116 | 117 | env._backup() 118 | 119 | for _ in range(250): 120 | if done: 121 | state = env.reset() 122 | done = False 123 | state, _, done, _ = env.step(0) 124 | 125 | self.assertFalse(np.array_equal(backup, state)) 126 | env._restore() 127 | self.assertTrue(np.array_equal(backup, env.screen)) 128 | env.close() 129 | -------------------------------------------------------------------------------- /nes_py/nes/src/mappers/mapper_SxROM.cpp: -------------------------------------------------------------------------------- 1 | // Program: nes-py 2 | // File: mapper_SxROM.cpp 3 | // Description: An implementation of the SxROM mapper 4 | // 5 | // Copyright (c) 2019 Christian Kauten. All rights reserved. 6 | // 7 | 8 | #include "mappers/mapper_SxROM.hpp" 9 | #include "log.hpp" 10 | 11 | namespace NES { 12 | 13 | MapperSxROM::MapperSxROM(Cartridge* cart, std::function mirroring_cb) : 14 | Mapper(cart), 15 | mirroring_callback(mirroring_cb), 16 | mirroring(HORIZONTAL), 17 | mode_chr(0), 18 | mode_prg(3), 19 | temp_register(0), 20 | write_counter(0), 21 | register_prg(0), 22 | register_chr0(0), 23 | register_chr1(0), 24 | first_bank_prg(0), 25 | second_bank_prg(cart->getROM().size() - 0x4000), 26 | first_bank_chr(0), 27 | second_bank_chr(0) { 28 | if (cart->getVROM().size() == 0) { 29 | has_character_ram = true; 30 | character_ram.resize(0x2000); 31 | LOG(Info) << "Uses character RAM" << std::endl; 32 | } else { 33 | LOG(Info) << "Using CHR-ROM" << std::endl; 34 | has_character_ram = false; 35 | first_bank_chr = 0; 36 | second_bank_chr = 0x1000 * register_chr1; 37 | } 38 | } 39 | 40 | void MapperSxROM::writePRG(NES_Address address, NES_Byte value) { 41 | if (!(value & 0x80)) { // reset bit is NOT set 42 | temp_register = (temp_register >> 1) | ((value & 1) << 4); 43 | ++write_counter; 44 | 45 | if (write_counter == 5) { 46 | if (address <= 0x9fff) { 47 | switch (temp_register & 0x3) { 48 | case 0: { mirroring = ONE_SCREEN_LOWER; break; } 49 | case 1: { mirroring = ONE_SCREEN_HIGHER; break; } 50 | case 2: { mirroring = VERTICAL; break; } 51 | case 3: { mirroring = HORIZONTAL; break; } 52 | } 53 | mirroring_callback(); 54 | 55 | mode_chr = (temp_register & 0x10) >> 4; 56 | mode_prg = (temp_register & 0xc) >> 2; 57 | calculatePRGPointers(); 58 | 59 | // Recalculate CHR pointers 60 | if (mode_chr == 0) { // one 8KB bank 61 | // ignore last bit 62 | first_bank_chr = 0x1000 * (register_chr0 | 1); 63 | second_bank_chr = first_bank_chr + 0x1000; 64 | } else { // two 4KB banks 65 | first_bank_chr = 0x1000 * register_chr0; 66 | second_bank_chr = 0x1000 * register_chr1; 67 | } 68 | } else if (address <= 0xbfff) { // CHR Reg 0 69 | register_chr0 = temp_register; 70 | // OR 1 if 8KB mode 71 | first_bank_chr = 0x1000 * (temp_register | (1 - mode_chr)); 72 | if (mode_chr == 0) 73 | second_bank_chr = first_bank_chr + 0x1000; 74 | } else if (address <= 0xdfff) { 75 | register_chr1 = temp_register; 76 | if(mode_chr == 1) 77 | second_bank_chr = 0x1000 * temp_register; 78 | } else { 79 | // TODO: PRG-RAM 80 | if ((temp_register & 0x10) == 0x10) { 81 | LOG(Info) << "PRG-RAM activated" << std::endl; 82 | } 83 | temp_register &= 0xf; 84 | register_prg = temp_register; 85 | calculatePRGPointers(); 86 | } 87 | 88 | temp_register = 0; 89 | write_counter = 0; 90 | } 91 | } else { // reset 92 | temp_register = 0; 93 | write_counter = 0; 94 | mode_prg = 3; 95 | calculatePRGPointers(); 96 | } 97 | } 98 | 99 | void MapperSxROM::calculatePRGPointers() { 100 | if (mode_prg <= 1) { // 32KB changeable 101 | // equivalent to multiplying 0x8000 * (register_prg >> 1) 102 | first_bank_prg = 0x4000 * (register_prg & ~1); 103 | // add 16KB 104 | second_bank_prg = first_bank_prg + 0x4000; 105 | } else if (mode_prg == 2) { // fix first switch second 106 | first_bank_prg = 0; 107 | second_bank_prg = first_bank_prg + 0x4000 * register_prg; 108 | } else { // switch first fix second 109 | first_bank_prg = 0x4000 * register_prg; 110 | second_bank_prg = cartridge->getROM().size() - 0x4000; 111 | } 112 | } 113 | 114 | void MapperSxROM::writeCHR(NES_Address address, NES_Byte value) { 115 | if (has_character_ram) 116 | character_ram[address] = value; 117 | else 118 | LOG(Info) << "Read-only CHR memory write attempt at " << std::hex << address << std::endl; 119 | } 120 | 121 | } // namespace NES 122 | -------------------------------------------------------------------------------- /nes_py/_image_viewer.py: -------------------------------------------------------------------------------- 1 | """A simple class for viewing images using pyglet.""" 2 | 3 | 4 | class ImageViewer(object): 5 | """A simple class for viewing images using pyglet.""" 6 | 7 | def __init__(self, caption, height, width, 8 | monitor_keyboard=False, 9 | relevant_keys=None 10 | ): 11 | """ 12 | Initialize a new image viewer. 13 | 14 | Args: 15 | caption (str): the caption/title for the window 16 | height (int): the height of the window 17 | width (int): the width of the window 18 | monitor_keyboard: whether to monitor events from the keyboard 19 | relevant_keys: the relevant keys to monitor events from 20 | 21 | Returns: 22 | None 23 | """ 24 | # detect if rendering from python threads and fail 25 | import threading 26 | if threading.current_thread() is not threading.main_thread(): 27 | msg = 'rendering from python threads is not supported' 28 | raise RuntimeError(msg) 29 | # import pyglet within class scope to resolve issues with how pyglet 30 | # interacts with OpenGL while using multiprocessing 31 | import pyglet 32 | self.pyglet = pyglet 33 | # a mapping from pyglet key identifiers to native identifiers 34 | self.KEY_MAP = { 35 | self.pyglet.window.key.ENTER: ord('\r'), 36 | self.pyglet.window.key.SPACE: ord(' '), 37 | } 38 | self.caption = caption 39 | self.height = height 40 | self.width = width 41 | self.monitor_keyboard = monitor_keyboard 42 | self.relevant_keys = relevant_keys 43 | self._window = None 44 | self._pressed_keys = [] 45 | self._is_escape_pressed = False 46 | 47 | @property 48 | def is_open(self): 49 | """Return a boolean determining if this window is open.""" 50 | return self._window is not None 51 | 52 | @property 53 | def is_escape_pressed(self): 54 | """Return True if the escape key is pressed.""" 55 | return self._is_escape_pressed 56 | 57 | @property 58 | def pressed_keys(self): 59 | """Return a sorted list of the pressed keys.""" 60 | return tuple(sorted(self._pressed_keys)) 61 | 62 | def _handle_key_event(self, symbol, is_press): 63 | """ 64 | Handle a key event. 65 | 66 | Args: 67 | symbol: the symbol in the event 68 | is_press: whether the event is a press or release 69 | 70 | Returns: 71 | None 72 | 73 | """ 74 | # remap the key to the expected domain 75 | symbol = self.KEY_MAP.get(symbol, symbol) 76 | # check if the symbol is the escape key 77 | if symbol == self.pyglet.window.key.ESCAPE: 78 | self._is_escape_pressed = is_press 79 | return 80 | # make sure the symbol is relevant 81 | if self.relevant_keys is not None and symbol not in self.relevant_keys: 82 | return 83 | # handle the press / release by appending / removing the key to pressed 84 | if is_press: 85 | self._pressed_keys.append(symbol) 86 | else: 87 | self._pressed_keys.remove(symbol) 88 | 89 | def on_key_press(self, symbol, modifiers): 90 | """Respond to a key press on the keyboard.""" 91 | self._handle_key_event(symbol, True) 92 | 93 | def on_key_release(self, symbol, modifiers): 94 | """Respond to a key release on the keyboard.""" 95 | self._handle_key_event(symbol, False) 96 | 97 | def open(self): 98 | """Open the window.""" 99 | # create a window for this image viewer instance 100 | self._window = self.pyglet.window.Window( 101 | caption=self.caption, 102 | height=self.height, 103 | width=self.width, 104 | vsync=False, 105 | resizable=True, 106 | ) 107 | 108 | # add keyboard event monitors if enabled 109 | if self.monitor_keyboard: 110 | self._window.event(self.on_key_press) 111 | self._window.event(self.on_key_release) 112 | 113 | def close(self): 114 | """Close the window.""" 115 | if self.is_open: 116 | self._window.close() 117 | self._window = None 118 | 119 | def show(self, frame): 120 | """ 121 | Show an array of pixels on the window. 122 | 123 | Args: 124 | frame (numpy.ndarray): the frame to show on the window 125 | 126 | Returns: 127 | None 128 | """ 129 | # check that the frame has the correct dimensions 130 | if len(frame.shape) != 3: 131 | raise ValueError('frame should have shape with only 3 dimensions') 132 | # open the window if it isn't open already 133 | if not self.is_open: 134 | self.open() 135 | # prepare the window for the next frame 136 | self._window.clear() 137 | self._window.switch_to() 138 | self._window.dispatch_events() 139 | # create an image data object 140 | image = self.pyglet.image.ImageData( 141 | frame.shape[1], 142 | frame.shape[0], 143 | 'RGB', 144 | frame.tobytes(), 145 | pitch=frame.shape[1]*-3 146 | ) 147 | # send the image to the window 148 | image.blit(0, 0, width=self._window.width, height=self._window.height) 149 | self._window.flip() 150 | 151 | 152 | # explicitly define the outward facing API of this module 153 | __all__ = [ImageViewer.__name__] 154 | -------------------------------------------------------------------------------- /nes_py/nes/include/cpu.hpp: -------------------------------------------------------------------------------- 1 | // Program: nes-py 2 | // File: cpu.hpp 3 | // Description: This class houses the logic and data for the NES CPU 4 | // 5 | // Copyright (c) 2019 Christian Kauten. All rights reserved. 6 | // 7 | 8 | #ifndef CPU_HPP 9 | #define CPU_HPP 10 | 11 | #include "common.hpp" 12 | #include "cpu_opcodes.hpp" 13 | #include "main_bus.hpp" 14 | 15 | namespace NES { 16 | 17 | /// The MOS6502 CPU for the Nintendo Entertainment System (NES) 18 | class CPU { 19 | private: 20 | /// The program counter register 21 | NES_Address register_PC; 22 | /// The stack pointer register 23 | NES_Byte register_SP; 24 | /// The A register 25 | NES_Byte register_A; 26 | /// The X register 27 | NES_Byte register_X; 28 | /// The Y register 29 | NES_Byte register_Y; 30 | /// The flags register 31 | CPU_Flags flags; 32 | /// The number of cycles to skip 33 | int skip_cycles; 34 | /// The number of cycles the CPU has run 35 | int cycles; 36 | 37 | /// Set the zero and negative flags based on the given value. 38 | /// 39 | /// @param value the value to set the zero and negative flags using 40 | /// 41 | inline void set_ZN(NES_Byte value) { 42 | flags.bits.Z = !value; flags.bits.N = value & 0x80; 43 | } 44 | 45 | /// Read a 16-bit address from the bus given an address. 46 | /// 47 | /// @param bus the bus to read data from 48 | /// @param address the address in memory to read an address from 49 | /// @return the 16-bit address located at the given memory address 50 | /// 51 | inline NES_Address read_address(MainBus &bus, NES_Address address) { 52 | return bus.read(address) | bus.read(address + 1) << 8; 53 | } 54 | 55 | /// Push a value onto the stack. 56 | /// 57 | /// @param bus the bus to read data from 58 | /// @param value the value to push onto the stack 59 | /// 60 | inline void push_stack(MainBus &bus, NES_Byte value) { 61 | bus.write(0x100 | register_SP--, value); 62 | } 63 | 64 | /// Pop a value off the stack. 65 | /// 66 | /// @param bus the bus to read data from 67 | /// @return the value on the top of the stack 68 | /// 69 | inline NES_Byte pop_stack(MainBus &bus) { 70 | return bus.read(0x100 | ++register_SP); 71 | } 72 | 73 | /// Increment the skip cycles if two addresses refer to different pages. 74 | /// 75 | /// @param a an address 76 | /// @param b another address 77 | /// @param inc the number of skip cycles to add 78 | /// 79 | inline void set_page_crossed(NES_Address a, NES_Address b, int inc = 1) { 80 | if ((a & 0xff00) != (b & 0xff00)) skip_cycles += inc; 81 | } 82 | 83 | /// Execute an implied mode instruction. 84 | /// 85 | /// @param bus the bus to read and write data from and to 86 | /// @param opcode the opcode of the operation to perform 87 | /// @return true if the instruction succeeds 88 | /// 89 | bool implied(MainBus &bus, NES_Byte opcode); 90 | 91 | /// Execute a branch instruction. 92 | /// 93 | /// @param bus the bus to read and write data from and to 94 | /// @param opcode the opcode of the operation to perform 95 | /// @return true if the instruction succeeds 96 | /// 97 | bool branch(MainBus &bus, NES_Byte opcode); 98 | 99 | /// Execute a type 0 instruction. 100 | /// 101 | /// @param bus the bus to read and write data from and to 102 | /// @param opcode the opcode of the operation to perform 103 | /// @return true if the instruction succeeds 104 | /// 105 | bool type0(MainBus &bus, NES_Byte opcode); 106 | 107 | /// Execute a type 1 instruction. 108 | /// 109 | /// @param bus the bus to read and write data from and to 110 | /// @param opcode the opcode of the operation to perform 111 | /// @return true if the instruction succeeds 112 | /// 113 | bool type1(MainBus &bus, NES_Byte opcode); 114 | 115 | /// Execute a type 2 instruction. 116 | /// 117 | /// @param bus the bus to read and write data from and to 118 | /// @param opcode the opcode of the operation to perform 119 | /// @return true if the instruction succeeds 120 | /// 121 | bool type2(MainBus &bus, NES_Byte opcode); 122 | 123 | /// Reset the emulator using the given starting address. 124 | /// 125 | /// @param start_address the starting address for the program counter 126 | /// 127 | void reset(NES_Address start_address); 128 | 129 | public: 130 | /// The interrupt types available to this CPU 131 | enum InterruptType { 132 | IRQ_INTERRUPT, 133 | NMI_INTERRUPT, 134 | BRK_INTERRUPT, 135 | }; 136 | 137 | /// Initialize a new CPU. 138 | CPU() { }; 139 | 140 | /// Reset using the given main bus to lookup a starting address. 141 | /// 142 | /// @param bus the main bus of the NES emulator 143 | /// 144 | inline void reset(MainBus &bus) { reset(read_address(bus, RESET_VECTOR)); } 145 | 146 | /// Interrupt the CPU. 147 | /// 148 | /// @param bus the main bus of the machine 149 | /// @param type the type of interrupt to issue 150 | /// 151 | /// TODO: Assuming sequential execution, for asynchronously calling this 152 | /// with Execute, further work needed 153 | /// 154 | void interrupt(MainBus &bus, InterruptType type); 155 | 156 | /// Perform a full CPU cycle using and storing data in the given bus. 157 | /// 158 | /// @param bus the bus to read and write data from / to 159 | /// 160 | void cycle(MainBus &bus); 161 | 162 | /// Skip DMA cycles. 163 | /// 164 | /// 513 = 256 read + 256 write + 1 dummy read 165 | /// &1 -> +1 if on odd cycle 166 | /// 167 | inline void skip_DMA_cycles() { skip_cycles += 513 + (cycles & 1); } 168 | }; 169 | 170 | } // namespace NES 171 | 172 | #endif // CPU_HPP 173 | -------------------------------------------------------------------------------- /nes_py/nes/include/ppu.hpp: -------------------------------------------------------------------------------- 1 | // Program: nes-py 2 | // File: ppu.hpp 3 | // Description: This class houses the logic and data for the PPU of an NES 4 | // 5 | // Copyright (c) 2019 Christian Kauten. All rights reserved. 6 | // 7 | 8 | #ifndef PPU_HPP 9 | #define PPU_HPP 10 | 11 | #include "common.hpp" 12 | #include "picture_bus.hpp" 13 | 14 | namespace NES { 15 | 16 | /// The number of visible scan lines (i.e., the height of the screen) 17 | const int VISIBLE_SCANLINES = 240; 18 | /// The number of visible dots per scan line (i.e., the width of the screen) 19 | const int SCANLINE_VISIBLE_DOTS = 256; 20 | /// The number of cycles per scanline 21 | const int SCANLINE_CYCLE_LENGTH = 341; 22 | /// The last cycle of a scan line (changed from 340 to fix render glitch) 23 | const int SCANLINE_END_CYCLE = 341; 24 | /// The last scanline per frame 25 | const int FRAME_END_SCANLINE = 261; 26 | 27 | /// The Picture Processing Unit (PPU) for the NES 28 | class PPU { 29 | private: 30 | /// The callback to fire when entering vertical blanking mode 31 | std::function vblank_callback; 32 | /// The OAM memory (sprites) 33 | std::vector sprite_memory; 34 | /// OAM memory (sprites) for the next scanline 35 | std::vector scanline_sprites; 36 | 37 | /// The current pipeline state of the PPU 38 | enum State { 39 | PRE_RENDER, 40 | RENDER, 41 | POST_RENDER, 42 | VERTICAL_BLANK 43 | } pipeline_state; 44 | 45 | /// The number of cycles left in the frame 46 | int cycles; 47 | /// the current scanline of the frame 48 | int scanline; 49 | /// whether the PPU is on an even frame 50 | bool is_even_frame; 51 | 52 | // Status 53 | 54 | /// whether the PPU is in vertical blanking mode 55 | bool is_vblank; 56 | /// whether sprite 0 has been hit (i.e., collision detection) 57 | bool is_sprite_zero_hit; 58 | 59 | // Registers 60 | 61 | /// the current data address to (read / write) (from / to) 62 | NES_Address data_address; 63 | /// a temporary address register 64 | NES_Address temp_address; 65 | /// the fine scrolling position 66 | NES_Byte fine_x_scroll; 67 | /// TODO: doc 68 | bool is_first_write; 69 | /// The address of the data buffer 70 | NES_Byte data_buffer; 71 | /// the read / write address for the OAM memory (sprites) 72 | NES_Byte sprite_data_address; 73 | 74 | // Mask 75 | 76 | /// whether the PPU is showing sprites 77 | bool is_showing_sprites; 78 | /// whether the PPU is showing background pixels 79 | bool is_showing_background; 80 | /// whether the PPU is hiding sprites along the edges 81 | bool is_hiding_edge_sprites; 82 | /// whether the PPU is hiding the background along the edges 83 | bool is_hiding_edge_background; 84 | 85 | // Setup flags and variables 86 | 87 | /// TODO: doc 88 | bool is_long_sprites; 89 | /// whether the PPU is in the interrupt handler 90 | bool is_interrupting; 91 | 92 | /// TODO: doc 93 | enum CharacterPage { 94 | LOW, 95 | HIGH, 96 | } background_page, sprite_page; 97 | 98 | /// The value to increment the data address by 99 | NES_Address data_address_increment; 100 | 101 | /// The internal screen data structure as a vector representation of a 102 | /// matrix of height matching the visible scans lines and width matching 103 | /// the number of visible scan line dots 104 | NES_Pixel screen[VISIBLE_SCANLINES][SCANLINE_VISIBLE_DOTS]; 105 | 106 | public: 107 | /// Initialize a new PPU. 108 | PPU() : sprite_memory(64 * 4) { } 109 | 110 | /// Perform a single cycle on the PPU. 111 | void cycle(PictureBus& bus); 112 | 113 | /// Reset the PPU. 114 | void reset(); 115 | 116 | /// Set the interrupt callback for the CPU. 117 | inline void set_interrupt_callback(std::function cb) { 118 | vblank_callback = cb; 119 | } 120 | 121 | /// TODO: doc 122 | void do_DMA(const NES_Byte* page_ptr); 123 | 124 | // MARK: Callbacks mapped to CPU address space 125 | 126 | /// Set the control register to a new value. 127 | /// 128 | /// @param ctrl the new control register byte 129 | /// 130 | void control(NES_Byte ctrl); 131 | 132 | /// Set the mask register to a new value. 133 | /// 134 | /// @param mask the new mask value 135 | /// 136 | void set_mask(NES_Byte mask); 137 | 138 | /// Set the scroll register to a new value. 139 | /// 140 | /// @param scroll the new scroll register value 141 | /// 142 | void set_scroll(NES_Byte scroll); 143 | 144 | /// Return the value in the PPU status register. 145 | NES_Byte get_status(); 146 | 147 | /// TODO: doc 148 | void set_data_address(NES_Byte address); 149 | 150 | /// Read data off the picture bus. 151 | /// 152 | /// @param bus the bus to read data off of 153 | /// 154 | NES_Byte get_data(PictureBus& bus); 155 | 156 | /// TODO: doc 157 | void set_data(PictureBus& bus, NES_Byte data); 158 | 159 | /// Set the sprite data address to a new value. 160 | /// 161 | /// @param address the new OAM data address 162 | /// 163 | inline void set_OAM_address(NES_Byte address) { 164 | sprite_data_address = address; 165 | } 166 | 167 | /// Read a byte from OAM memory at the sprite data address. 168 | /// 169 | /// @return the byte at the given address in OAM memory 170 | /// 171 | inline NES_Byte get_OAM_data() { 172 | return sprite_memory[sprite_data_address]; 173 | } 174 | 175 | /// Write a byte to OAM memory at the sprite data address. 176 | /// 177 | /// @param value the byte to write to the given address 178 | /// 179 | inline void set_OAM_data(NES_Byte value) { 180 | sprite_memory[sprite_data_address++] = value; 181 | } 182 | 183 | /// Return a pointer to the screen buffer. 184 | inline NES_Pixel* get_screen_buffer() { return *screen; } 185 | }; 186 | 187 | } // namespace NES 188 | 189 | #endif // PPU_HPP 190 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 6 |

7 | 8 | [![build-status][]][ci-server] 9 | [![PackageVersion][pypi-version]][pypi-home] 10 | [![PythonVersion][python-version]][python-home] 11 | [![Stable][pypi-status]][pypi-home] 12 | [![Format][pypi-format]][pypi-home] 13 | [![License][pypi-license]](LICENSE) 14 | 15 | [build-status]: https://app.travis-ci.com/Kautenja/nes-py.svg?branch=master 16 | [ci-server]: https://app.travis-ci.com/Kautenja/nes-py 17 | [pypi-version]: https://badge.fury.io/py/nes-py.svg 18 | [pypi-license]: https://img.shields.io/pypi/l/nes-py.svg 19 | [pypi-status]: https://img.shields.io/pypi/status/nes-py.svg 20 | [pypi-format]: https://img.shields.io/pypi/format/nes-py.svg 21 | [pypi-home]: https://badge.fury.io/py/nes-py 22 | [python-version]: https://img.shields.io/pypi/pyversions/nes-py.svg 23 | [python-home]: https://python.org 24 | 25 | nes-py is an NES emulator and OpenAI Gym interface for MacOS, Linux, and 26 | Windows based on the [SimpleNES](https://github.com/amhndu/SimpleNES) emulator. 27 | 28 | 29 | 30 | 37 | 44 | 51 | 52 | 53 | 60 | 67 | 74 | 75 | 76 | 83 | 90 | 97 | 98 |
31 | Bomberman II 36 | 38 | Castelvania II 43 | 45 | Excitebike 50 |
54 | Super Mario Bros. 59 | 61 | The Legend of Zelda 66 | 68 | Tetris 73 |
77 | Contra 82 | 84 | Mega Man II 89 | 91 | Bubble Bobble 96 |
99 | 100 | # Installation 101 | 102 | The preferred installation of `nes-py` is from `pip`: 103 | 104 | ```shell 105 | pip install nes-py 106 | ``` 107 | 108 | ## Debian 109 | 110 | Make sure you have the `clang++` compiler installed: 111 | 112 | ```shell 113 | sudo apt-get install clang 114 | ``` 115 | 116 | ## Windows 117 | 118 | You'll need to install the Visual-Studio 17.0 tools for Windows installation. 119 | The [Visual Studio Community](https://visualstudio.microsoft.com/downloads/) 120 | package provides these tools for free. 121 | 122 | # Usage 123 | 124 | To access the NES emulator from the command line use the following command. 125 | 126 | ```shell 127 | nes_py -r 128 | ``` 129 | 130 | To print out documentation for the command line interface execute: 131 | 132 | ```shell 133 | nes_py -h 134 | ``` 135 | 136 | ## Controls 137 | 138 | | Keyboard Key | NES Joypad | 139 | |:-------------|:--------------| 140 | | W | Up | 141 | | A | Left | 142 | | S | Down | 143 | | D | Right | 144 | | O | A | 145 | | P | B | 146 | | Enter | Start | 147 | | Space | Select | 148 | 149 | ## Parallelism Caveats 150 | 151 | both the `threading` and `multiprocessing` packages are supported by 152 | `nes-py` with some caveats related to rendering: 153 | 154 | 1. rendering **is not** supported from instances of `threading.Thread` 155 | 2. rendering **is** supported from instances of `multiprocessing.Process`, 156 | but `nes-py` must be imported within the process that executes the render 157 | call 158 | 159 | # Development 160 | 161 | To design a custom environment using `nes-py`, introduce new features, or fix 162 | a bug, please refer to the [Wiki](https://github.com/Kautenja/nes-py/wiki). 163 | There you will find instructions for: 164 | 165 | - setting up the development environment 166 | - designing environments based on the `NESEnv` class 167 | - reference material for the `NESEnv` API 168 | - documentation for the `nes_py.wrappers` module 169 | 170 | # Cartridge Mapper Compatibility 171 | 172 | 0. NROM 173 | 1. MMC1 / SxROM 174 | 2. UxROM 175 | 3. CNROM 176 | 177 | You can check the compatibility for each ROM in the following 178 | [list](https://github.com/Kautenja/nes-py/blob/master/nesmapper.txt) 179 | 180 | # Disclaimer 181 | 182 | **This project is provided for educational purposes only. It is not 183 | affiliated with and has not been approved by Nintendo.** 184 | -------------------------------------------------------------------------------- /nes_py/_rom.py: -------------------------------------------------------------------------------- 1 | """An abstraction of the NES Read-Only Memory (ROM). 2 | 3 | Notes: 4 | - http://wiki.nesdev.com/w/index.php/INES 5 | """ 6 | import os 7 | import numpy as np 8 | 9 | 10 | class ROM(object): 11 | """An abstraction of the NES Read-Only Memory (ROM).""" 12 | 13 | # the magic bytes expected at the first four bytes of the header. 14 | # It spells "NES" 15 | _MAGIC = np.array([0x4E, 0x45, 0x53, 0x1A]) 16 | 17 | def __init__(self, rom_path): 18 | """ 19 | Initialize a new ROM. 20 | 21 | Args: 22 | rom_path (str): the path to the ROM file 23 | 24 | Returns: 25 | None 26 | 27 | """ 28 | # make sure the rom path is a string 29 | if not isinstance(rom_path, str): 30 | raise TypeError('rom_path must be of type: str.') 31 | # make sure the rom path exists 32 | if not os.path.exists(rom_path): 33 | msg = 'rom_path points to non-existent file: {}.'.format(rom_path) 34 | raise ValueError(msg) 35 | # read the binary data in the .nes ROM file 36 | self.raw_data = np.fromfile(rom_path, dtype='uint8') 37 | # ensure the first 4 bytes are 0x4E45531A (NES) 38 | if not np.array_equal(self._magic, self._MAGIC): 39 | raise ValueError('ROM missing magic number in header.') 40 | if self._zero_fill != 0: 41 | raise ValueError("ROM header zero fill bytes are not zero.") 42 | 43 | # 44 | # MARK: Header 45 | # 46 | 47 | @property 48 | def header(self): 49 | """Return the header of the ROM file as bytes.""" 50 | return self.raw_data[:16] 51 | 52 | @property 53 | def _magic(self): 54 | """Return the magic bytes in the first 4 bytes.""" 55 | return self.header[:4] 56 | 57 | @property 58 | def prg_rom_size(self): 59 | """Return the size of the PRG ROM in KB.""" 60 | return 16 * self.header[4] 61 | 62 | @property 63 | def chr_rom_size(self): 64 | """Return the size of the CHR ROM in KB.""" 65 | return 8 * self.header[5] 66 | 67 | @property 68 | def flags_6(self): 69 | """Return the flags at the 6th byte of the header.""" 70 | return '{:08b}'.format(self.header[6]) 71 | 72 | @property 73 | def flags_7(self): 74 | """Return the flags at the 7th byte of the header.""" 75 | return '{:08b}'.format(self.header[7]) 76 | 77 | @property 78 | def prg_ram_size(self): 79 | """Return the size of the PRG RAM in KB.""" 80 | size = self.header[8] 81 | # size becomes 8 when it's zero for compatibility 82 | if size == 0: 83 | size = 1 84 | 85 | return 8 * size 86 | 87 | @property 88 | def flags_9(self): 89 | """Return the flags at the 9th byte of the header.""" 90 | return '{:08b}'.format(self.header[9]) 91 | 92 | @property 93 | def flags_10(self): 94 | """ 95 | Return the flags at the 10th byte of the header. 96 | 97 | Notes: 98 | - these flags are not part of official specification. 99 | - ignored in this emulator 100 | 101 | """ 102 | return '{:08b}'.format(self.header[10]) 103 | 104 | @property 105 | def _zero_fill(self): 106 | """Return the zero fill bytes at the end of the header.""" 107 | return self.header[11:].sum() 108 | 109 | # 110 | # MARK: Header Flags 111 | # 112 | 113 | @property 114 | def mapper(self): 115 | """Return the mapper number this ROM uses.""" 116 | # the high nibble is in flags 7, the low nibble is in flags 6 117 | return int(self.flags_7[:4] + self.flags_6[:4], 2) 118 | 119 | @property 120 | def is_ignore_mirroring(self): 121 | """Return a boolean determining if the ROM ignores mirroring.""" 122 | return bool(int(self.flags_6[4])) 123 | 124 | @property 125 | def has_trainer(self): 126 | """Return a boolean determining if the ROM has a trainer block.""" 127 | return bool(int(self.flags_6[5])) 128 | 129 | @property 130 | def has_battery_backed_ram(self): 131 | """Return a boolean determining if the ROM has a battery-backed RAM.""" 132 | return bool(int(self.flags_6[6])) 133 | 134 | @property 135 | def is_vertical_mirroring(self): 136 | """Return the mirroring mode this ROM uses.""" 137 | return bool(int(self.flags_6[7])) 138 | 139 | @property 140 | def has_play_choice_10(self): 141 | """ 142 | Return whether this cartridge uses PlayChoice-10. 143 | 144 | Note: 145 | - Play-Choice 10 uses different color palettes for a different PPU 146 | - ignored in this emulator 147 | 148 | """ 149 | return bool(int(self.flags_7[6])) 150 | 151 | @property 152 | def has_vs_unisystem(self): 153 | """ 154 | Return whether this cartridge has VS Uni-system. 155 | 156 | Note: 157 | VS Uni-system is for ROMs that have a coin slot (Arcades). 158 | - ignored in this emulator 159 | 160 | """ 161 | return bool(int(self.flags_7[7])) 162 | 163 | @property 164 | def is_pal(self): 165 | """Return the TV system this ROM supports.""" 166 | return bool(int(self.flags_9[7])) 167 | 168 | # 169 | # MARK: ROM 170 | # 171 | 172 | @property 173 | def trainer_rom_start(self): 174 | """The inclusive starting index of the trainer ROM.""" 175 | return 16 176 | 177 | @property 178 | def trainer_rom_stop(self): 179 | """The exclusive stopping index of the trainer ROM.""" 180 | if self.has_trainer: 181 | return 16 + 512 182 | else: 183 | return 16 184 | 185 | @property 186 | def trainer_rom(self): 187 | """Return the trainer ROM of the ROM file.""" 188 | return self.raw_data[self.trainer_rom_start:self.trainer_rom_stop] 189 | 190 | @property 191 | def prg_rom_start(self): 192 | """The inclusive starting index of the PRG ROM.""" 193 | return self.trainer_rom_stop 194 | 195 | @property 196 | def prg_rom_stop(self): 197 | """The exclusive stopping index of the PRG ROM.""" 198 | return self.prg_rom_start + self.prg_rom_size * 2**10 199 | 200 | @property 201 | def prg_rom(self): 202 | """Return the PRG ROM of the ROM file.""" 203 | try: 204 | return self.raw_data[self.prg_rom_start:self.prg_rom_stop] 205 | except IndexError: 206 | raise ValueError('failed to read PRG-ROM on ROM.') 207 | 208 | @property 209 | def chr_rom_start(self): 210 | """The inclusive starting index of the CHR ROM.""" 211 | return self.prg_rom_stop 212 | 213 | @property 214 | def chr_rom_stop(self): 215 | """The exclusive stopping index of the CHR ROM.""" 216 | return self.chr_rom_start + self.chr_rom_size * 2**10 217 | 218 | @property 219 | def chr_rom(self): 220 | """Return the CHR ROM of the ROM file.""" 221 | try: 222 | return self.raw_data[self.chr_rom_start:self.chr_rom_stop] 223 | except IndexError: 224 | raise ValueError('failed to read CHR-ROM on ROM.') 225 | 226 | 227 | # explicitly define the outward facing API of this module 228 | __all__ = [ROM.__name__] 229 | -------------------------------------------------------------------------------- /nes_py/tests/test_rom.py: -------------------------------------------------------------------------------- 1 | """Test cases for the ROM. 2 | 3 | Information about ROMs if found here: 4 | http://tuxnes.sourceforge.net/nesmapper.txt 5 | 6 | """ 7 | from unittest import TestCase 8 | from .rom_file_abs_path import rom_file_abs_path 9 | from nes_py._rom import ROM 10 | 11 | 12 | class ShouldNotCreateInstanceOfROMWithoutPath(TestCase): 13 | def test(self): 14 | self.assertRaises(TypeError, ROM) 15 | 16 | 17 | class ShouldNotCreateInstanceOfROMWithInvaldPath(TestCase): 18 | def test(self): 19 | self.assertRaises(TypeError, lambda: ROM(5)) 20 | self.assertRaises(ValueError, lambda: ROM('not a path')) 21 | 22 | 23 | class ShouldNotCreateInstanceOfROMWithInvaldROMFile(TestCase): 24 | def test(self): 25 | empty = rom_file_abs_path('empty.nes') 26 | self.assertRaises(ValueError, lambda: ROM(empty)) 27 | 28 | 29 | # 30 | # MARK: ROM Headers 31 | # 32 | 33 | 34 | class ShouldReadROMHeaderTestCase(object): 35 | """The general form of a test case to check the header of a ROM.""" 36 | 37 | # the name of the ROM to test the header of 38 | rom_name = None 39 | 40 | # the amount of program memory in the ROM (KB) 41 | prg_rom_size = None 42 | 43 | # the amount of character map memory in the ROM (KB) 44 | chr_rom_size = None 45 | 46 | # the amount of program RAM in the ROM (KB) 47 | prg_ram_size = None 48 | 49 | # the number of the mapper this ROM uses 50 | mapper = None 51 | 52 | # whether to ignore the mirroring setting bit 53 | is_ignore_mirroring = None 54 | 55 | # whether the ROM contains a trainer block 56 | has_trainer = None 57 | 58 | # whether the cartridge of the ROM has a battery-backed RAM module 59 | has_battery_backed_ram = None 60 | 61 | # the mirroring mode used by the ROM 62 | is_vertical_mirroring = None 63 | 64 | # whether the ROM uses PlayChoice-10 (8KB of Hint Screen after CHR data) 65 | has_play_choice_10 = None 66 | 67 | # whether the ROM uses VS Unisystem 68 | has_vs_unisystem = None 69 | 70 | # the TV system the ROM is designed for 71 | is_pal = None 72 | 73 | # the address the trainer ROM starts at 74 | trainer_rom_start = None 75 | 76 | # the address the trainer ROM stops at 77 | trainer_rom_stop = None 78 | 79 | # the address the PRG ROM starts at 80 | prg_rom_start = None 81 | 82 | # the address the PRG ROM stops at 83 | prg_rom_stop = None 84 | 85 | # the address the CHR ROM starts at 86 | chr_rom_start = None 87 | 88 | # the address the CHR ROM stops at 89 | chr_rom_stop = None 90 | 91 | def setUp(self): 92 | """Perform setup before each test.""" 93 | rom_path = rom_file_abs_path(self.rom_name) 94 | self.rom = ROM(rom_path) 95 | 96 | def test_header_length(self): 97 | """Check the length of the header.""" 98 | self.assertEqual(16, len(self.rom.header)) 99 | 100 | def test_prg_rom_size(self): 101 | """Check the PRG ROM size.""" 102 | self.assertEqual(self.prg_rom_size, self.rom.prg_rom_size) 103 | 104 | def test_chr_rom_size(self): 105 | """Check the CHR ROM size.""" 106 | self.assertEqual(self.chr_rom_size, self.rom.chr_rom_size) 107 | 108 | def test_prg_ram_size(self): 109 | """Check the PRG RAM size.""" 110 | self.assertEqual(self.prg_ram_size, self.rom.prg_ram_size) 111 | 112 | def test_mapper(self): 113 | """Check the mapper number.""" 114 | self.assertEqual(self.mapper, self.rom.mapper) 115 | 116 | def test_is_ignore_mirroring(self): 117 | """Check whether the ROM is ignoring the mirroring mode.""" 118 | expected = self.is_ignore_mirroring 119 | actual = self.rom.is_ignore_mirroring 120 | self.assertEqual(expected, actual) 121 | 122 | def test_has_trainer(self): 123 | """Check whether the ROM has a trainer block or not.""" 124 | self.assertEqual(self.has_trainer, self.rom.has_trainer) 125 | 126 | def test_has_battery_backed_ram(self): 127 | """Check whether the ROM has battery-backed RAM.""" 128 | expected = self.has_battery_backed_ram 129 | actual = self.rom.has_battery_backed_ram 130 | self.assertEqual(expected, actual) 131 | 132 | def test_is_vertical_mirroring(self): 133 | """Check the mirroring mode of the ROM.""" 134 | self.assertEqual(self.is_vertical_mirroring, self.rom.is_vertical_mirroring) 135 | 136 | def test_has_play_choice_10(self): 137 | """Check whether the ROM uses PlayChoice-10.""" 138 | self.assertEqual(self.has_play_choice_10, self.rom.has_play_choice_10) 139 | 140 | def test_has_vs_unisystem(self): 141 | """Check whether the ROM uses a VS Unisystem.""" 142 | self.assertEqual(self.has_vs_unisystem, self.rom.has_vs_unisystem) 143 | 144 | def test_is_pal(self): 145 | """Check which TV mode the ROM is designed for.""" 146 | self.assertEqual(self.is_pal, self.rom.is_pal) 147 | 148 | def test_trainer_rom_start(self): 149 | """Check the starting address of trainer ROM.""" 150 | self.assertEqual(self.trainer_rom_start, self.rom.trainer_rom_start) 151 | 152 | def test_trainer_rom_stop(self): 153 | """Check the stopping address of trainer ROM.""" 154 | self.assertEqual(self.trainer_rom_stop, self.rom.trainer_rom_stop) 155 | 156 | def test_trainer_rom(self): 157 | """Check the trainer ROM.""" 158 | size = self.trainer_rom_stop - self.trainer_rom_start 159 | self.assertEqual(size, len(self.rom.trainer_rom)) 160 | 161 | def test_prg_rom_start(self): 162 | """Check the starting address of PRG ROM.""" 163 | self.assertEqual(self.prg_rom_start, self.rom.prg_rom_start) 164 | 165 | def test_prg_rom_stop(self): 166 | """Check the stopping address of PRG ROM.""" 167 | self.assertEqual(self.prg_rom_stop, self.rom.prg_rom_stop) 168 | 169 | def test_prg_rom(self): 170 | """Check the PRG ROM.""" 171 | size = (self.prg_rom_stop - self.prg_rom_start) 172 | self.assertEqual(size, len(self.rom.prg_rom)) 173 | 174 | def test_chr_rom_start(self): 175 | """Check the starting address of CHR ROM.""" 176 | self.assertEqual(self.chr_rom_start, self.rom.chr_rom_start) 177 | 178 | def test_chr_rom_stop(self): 179 | """Check the stopping address of CHR ROM.""" 180 | self.assertEqual(self.chr_rom_stop, self.rom.chr_rom_stop) 181 | 182 | def test_chr_rom(self): 183 | """Check the CHR ROM.""" 184 | size = (self.chr_rom_stop - self.chr_rom_start) 185 | self.assertEqual(size, len(self.rom.chr_rom)) 186 | 187 | 188 | class ShouldReadSuperMarioBros(ShouldReadROMHeaderTestCase, TestCase): 189 | """Check the Super Mario Bros 1 ROM.""" 190 | 191 | rom_name = 'super-mario-bros-1.nes' 192 | prg_rom_size = 32 193 | chr_rom_size = 8 194 | prg_ram_size = 8 195 | mapper = 0 196 | is_ignore_mirroring = False 197 | has_trainer = False 198 | has_battery_backed_ram = False 199 | is_vertical_mirroring = True 200 | has_play_choice_10 = False 201 | has_vs_unisystem = False 202 | is_pal = False 203 | trainer_rom_start = 16 204 | trainer_rom_stop = 16 205 | prg_rom_start = 16 206 | prg_rom_stop = 16 + 32 * 2**10 207 | chr_rom_start = 16 + 32 * 2**10 208 | chr_rom_stop = (16 + 32 * 2**10) + (8 * 2**10) 209 | 210 | 211 | class ShouldReadSMBLostLevels(ShouldReadROMHeaderTestCase, TestCase): 212 | """Check the Super Mario Bros Lost Levels ROM.""" 213 | 214 | rom_name = 'super-mario-bros-lost-levels.nes' 215 | prg_rom_size = 32 216 | chr_rom_size = 8 217 | prg_ram_size = 8 218 | mapper = 0 219 | is_ignore_mirroring = False 220 | has_trainer = False 221 | has_battery_backed_ram = False 222 | is_vertical_mirroring = True 223 | has_play_choice_10 = False 224 | has_vs_unisystem = False 225 | is_pal = False 226 | trainer_rom_start = 16 227 | trainer_rom_stop = 16 228 | prg_rom_start = 16 229 | prg_rom_stop = 16 + 32 * 2**10 230 | chr_rom_start = 16 + 32 * 2**10 231 | chr_rom_stop = (16 + 32 * 2**10) + (8 * 2**10) 232 | 233 | 234 | class ShouldReadSuperMarioBros2(ShouldReadROMHeaderTestCase, TestCase): 235 | """Check the Super Mario Bros 2 ROM.""" 236 | 237 | rom_name = 'super-mario-bros-2.nes' 238 | prg_rom_size = 128 239 | chr_rom_size = 128 240 | prg_ram_size = 8 241 | mapper = 4 242 | is_ignore_mirroring = False 243 | has_trainer = False 244 | has_battery_backed_ram = False 245 | is_vertical_mirroring = False 246 | has_play_choice_10 = False 247 | has_vs_unisystem = False 248 | is_pal = False 249 | trainer_rom_start = 16 250 | trainer_rom_stop = 16 251 | prg_rom_start = 16 252 | prg_rom_stop = 16 + 128 * 2**10 253 | chr_rom_start = 16 + 128 * 2**10 254 | chr_rom_stop = (16 + 128 * 2**10) + (128 * 2**10) 255 | 256 | 257 | class ShouldReadSuperMarioBros3(ShouldReadROMHeaderTestCase, TestCase): 258 | """Check the Super Mario Bros 3 ROM.""" 259 | 260 | rom_name = 'super-mario-bros-3.nes' 261 | prg_rom_size = 256 262 | chr_rom_size = 128 263 | prg_ram_size = 8 264 | mapper = 4 265 | is_ignore_mirroring = False 266 | has_trainer = False 267 | has_battery_backed_ram = False 268 | is_vertical_mirroring = False 269 | has_play_choice_10 = False 270 | has_vs_unisystem = False 271 | is_pal = False 272 | trainer_rom_start = 16 273 | trainer_rom_stop = 16 274 | prg_rom_start = 16 275 | prg_rom_stop = 16 + 256 * 2**10 276 | chr_rom_start = 16 + 256 * 2**10 277 | chr_rom_stop = (16 + 256 * 2**10) + (128 * 2**10) 278 | 279 | 280 | class ShouldReadExcitebike(ShouldReadROMHeaderTestCase, TestCase): 281 | """Check the Excitebike ROM.""" 282 | 283 | rom_name = 'excitebike.nes' 284 | prg_rom_size = 16 285 | chr_rom_size = 8 286 | prg_ram_size = 8 287 | mapper = 0 288 | is_ignore_mirroring = False 289 | has_trainer = False 290 | has_battery_backed_ram = False 291 | is_vertical_mirroring = True 292 | has_play_choice_10 = False 293 | has_vs_unisystem = False 294 | is_pal = False 295 | trainer_rom_start = 16 296 | trainer_rom_stop = 16 297 | prg_rom_start = 16 298 | prg_rom_stop = 16 + 16 * 2**10 299 | chr_rom_start = 16 + 16 * 2**10 300 | chr_rom_stop = (16 + 16 * 2**10) + (8 * 2**10) 301 | 302 | 303 | class ShouldReadLegendOfZelda(ShouldReadROMHeaderTestCase, TestCase): 304 | """Check The Legend Of Zelda ROM.""" 305 | 306 | rom_name = 'the-legend-of-zelda.nes' 307 | prg_rom_size = 128 308 | chr_rom_size = 0 309 | prg_ram_size = 8 310 | mapper = 1 311 | is_ignore_mirroring = False 312 | has_trainer = False 313 | has_battery_backed_ram = True 314 | is_vertical_mirroring = False 315 | has_play_choice_10 = False 316 | has_vs_unisystem = False 317 | is_pal = False 318 | trainer_rom_start = 16 319 | trainer_rom_stop = 16 320 | prg_rom_start = 16 321 | prg_rom_stop = 16 + 128 * 2**10 322 | chr_rom_start = 16 + 128 * 2**10 323 | chr_rom_stop = (16 + 128 * 2**10) + 0 324 | -------------------------------------------------------------------------------- /nes_py/nes/src/ppu.cpp: -------------------------------------------------------------------------------- 1 | // Program: nes-py 2 | // File: ppu.cpp 3 | // Description: This class houses the logic and data for the PPU of an NES 4 | // 5 | // Copyright (c) 2019 Christian Kauten. All rights reserved. 6 | // 7 | 8 | #include 9 | #include "ppu.hpp" 10 | #include "palette.hpp" 11 | #include "log.hpp" 12 | 13 | namespace NES { 14 | 15 | void PPU::reset() { 16 | is_long_sprites = false; 17 | is_interrupting = false; 18 | is_vblank = false; 19 | is_showing_background = true; 20 | is_showing_sprites = true; 21 | is_even_frame = true; 22 | is_first_write = true; 23 | background_page = LOW; 24 | sprite_page = LOW; 25 | data_address = 0; 26 | cycles = 0; 27 | scanline = 0; 28 | sprite_data_address = 0; 29 | fine_x_scroll = 0; 30 | temp_address = 0; 31 | data_address_increment = 1; 32 | pipeline_state = PRE_RENDER; 33 | scanline_sprites.reserve(8); 34 | scanline_sprites.resize(0); 35 | } 36 | 37 | void PPU::cycle(PictureBus& bus) { 38 | switch (pipeline_state) { 39 | case PRE_RENDER: { 40 | if (cycles == 1) 41 | is_vblank = is_sprite_zero_hit = false; 42 | else if (cycles == SCANLINE_VISIBLE_DOTS + 2 && is_showing_background && is_showing_sprites) { 43 | // Set bits related to horizontal position 44 | data_address &= ~0x41f; //Unset horizontal bits 45 | data_address |= temp_address & 0x41f; //Copy 46 | } 47 | else if (cycles > 280 && cycles <= 304 && is_showing_background && is_showing_sprites) { 48 | // Set vertical bits 49 | data_address &= ~0x7be0; //Unset bits related to horizontal 50 | data_address |= temp_address & 0x7be0; //Copy 51 | } 52 | // if (cycles > 257 && cycles < 320) 53 | // sprite_data_address = 0; 54 | // if rendering is on, every other frame is one cycle shorter 55 | if (cycles >= SCANLINE_END_CYCLE - (!is_even_frame && is_showing_background && is_showing_sprites)) { 56 | pipeline_state = RENDER; 57 | cycles = scanline = 0; 58 | } 59 | break; 60 | } 61 | case RENDER: { 62 | if (cycles > 0 && cycles <= SCANLINE_VISIBLE_DOTS) { 63 | NES_Byte bgColor = 0, sprColor = 0; 64 | bool bgOpaque = false, sprOpaque = true; 65 | bool spriteForeground = false; 66 | 67 | int x = cycles - 1; 68 | int y = scanline; 69 | 70 | if (is_showing_background) { 71 | auto x_fine = (fine_x_scroll + x) % 8; 72 | if (!is_hiding_edge_background || x >= 8) { 73 | // fetch tile 74 | // mask off fine y 75 | auto address = 0x2000 | (data_address & 0x0FFF); 76 | //auto address = 0x2000 + x / 8 + (y / 8) * (SCANLINE_VISIBLE_DOTS / 8); 77 | NES_Byte tile = bus.read(address); 78 | 79 | //fetch pattern 80 | //Each pattern occupies 16 bytes, so multiply by 16 81 | //Add fine y 82 | address = (tile * 16) + ((data_address >> 12/*y % 8*/) & 0x7); 83 | //set whether the pattern is in the high or low page 84 | address |= background_page << 12; 85 | //Get the corresponding bit determined by (8 - x_fine) from the right 86 | //bit 0 of palette entry 87 | bgColor = (bus.read(address) >> (7 ^ x_fine)) & 1; 88 | //bit 1 89 | bgColor |= ((bus.read(address + 8) >> (7 ^ x_fine)) & 1) << 1; 90 | 91 | //flag used to calculate final pixel with the sprite pixel 92 | bgOpaque = bgColor; 93 | 94 | //fetch attribute and calculate higher two bits of palette 95 | address = 0x23C0 | (data_address & 0x0C00) | ((data_address >> 4) & 0x38) 96 | | ((data_address >> 2) & 0x07); 97 | auto attribute = bus.read(address); 98 | int shift = ((data_address >> 4) & 4) | (data_address & 2); 99 | //Extract and set the upper two bits for the color 100 | bgColor |= ((attribute >> shift) & 0x3) << 2; 101 | } 102 | //Increment/wrap coarse X 103 | if (x_fine == 7) { 104 | // if coarse X == 31 105 | if ((data_address & 0x001F) == 31) { 106 | // coarse X = 0 107 | data_address &= ~0x001F; 108 | // switch horizontal nametable 109 | data_address ^= 0x0400; 110 | } 111 | else 112 | // increment coarse X 113 | data_address += 1; 114 | } 115 | } 116 | 117 | if (is_showing_sprites && (!is_hiding_edge_sprites || x >= 8)) { 118 | for (auto i : scanline_sprites) { 119 | NES_Byte spr_x = sprite_memory[i * 4 + 3]; 120 | 121 | if (0 > x - spr_x || x - spr_x >= 8) 122 | continue; 123 | 124 | NES_Byte spr_y = sprite_memory[i * 4 + 0] + 1, 125 | tile = sprite_memory[i * 4 + 1], 126 | attribute = sprite_memory[i * 4 + 2]; 127 | 128 | int length = (is_long_sprites) ? 16 : 8; 129 | 130 | int x_shift = (x - spr_x) % 8, y_offset = (y - spr_y) % length; 131 | 132 | if ((attribute & 0x40) == 0) //If NOT flipping horizontally 133 | x_shift ^= 7; 134 | if ((attribute & 0x80) != 0) //IF flipping vertically 135 | y_offset ^= (length - 1); 136 | 137 | NES_Address address = 0; 138 | 139 | if (!is_long_sprites) { 140 | address = tile * 16 + y_offset; 141 | if (sprite_page == HIGH) address += 0x1000; 142 | } 143 | // 8 x 16 sprites 144 | else { 145 | //bit-3 is one if it is the bottom tile of the sprite, multiply by two to get the next pattern 146 | y_offset = (y_offset & 7) | ((y_offset & 8) << 1); 147 | address = (tile >> 1) * 32 + y_offset; 148 | address |= (tile & 1) << 12; //Bank 0x1000 if bit-0 is high 149 | } 150 | 151 | sprColor |= (bus.read(address) >> (x_shift)) & 1; //bit 0 of palette entry 152 | sprColor |= ((bus.read(address + 8) >> (x_shift)) & 1) << 1; //bit 1 153 | 154 | if (!(sprOpaque = sprColor)) { 155 | sprColor = 0; 156 | continue; 157 | } 158 | 159 | sprColor |= 0x10; //Select sprite palette 160 | sprColor |= (attribute & 0x3) << 2; //bits 2-3 161 | 162 | spriteForeground = !(attribute & 0x20); 163 | 164 | //Sprite-0 hit detection 165 | if (!is_sprite_zero_hit && is_showing_background && i == 0 && sprOpaque && bgOpaque) 166 | is_sprite_zero_hit = true; 167 | 168 | break; //Exit the loop now since we've found the highest priority sprite 169 | } 170 | } 171 | // get the address of the color in the palette 172 | NES_Byte paletteAddr = bgColor; 173 | if ( (!bgOpaque && sprOpaque) || (bgOpaque && sprOpaque && spriteForeground) ) 174 | paletteAddr = sprColor; 175 | else if (!bgOpaque && !sprOpaque) 176 | paletteAddr = 0; 177 | // lookup the pixel in the palette and write it to the screen 178 | screen[y][x] = PALETTE[bus.read_palette(paletteAddr)]; 179 | } 180 | else if (cycles == SCANLINE_VISIBLE_DOTS + 1 && is_showing_background) { 181 | //Shamelessly copied from nesdev wiki 182 | if ((data_address & 0x7000) != 0x7000) { // if fine Y < 7 183 | // increment fine Y 184 | data_address += 0x1000; 185 | } else { 186 | // fine Y = 0 187 | data_address &= ~0x7000; 188 | // let y = coarse Y 189 | int y = (data_address & 0x03E0) >> 5; 190 | if (y == 29) { 191 | // coarse Y = 0 192 | y = 0; 193 | // switch vertical nametable 194 | data_address ^= 0x0800; 195 | } else if (y == 31) { 196 | // coarse Y = 0, nametable not switched 197 | y = 0; 198 | } else { 199 | // increment coarse Y 200 | y += 1; 201 | } 202 | // put coarse Y back into data_address 203 | data_address = (data_address & ~0x03E0) | (y << 5); 204 | } 205 | } 206 | else if (cycles == SCANLINE_VISIBLE_DOTS + 2 && is_showing_background && is_showing_sprites) { 207 | // Copy bits related to horizontal position 208 | data_address &= ~0x41f; 209 | data_address |= temp_address & 0x41f; 210 | } 211 | 212 | // if (cycles > 257 && cycles < 320) 213 | // sprite_data_address = 0; 214 | 215 | if (cycles >= SCANLINE_END_CYCLE) { 216 | //Find and index sprites that are on the next Scanline 217 | //This isn't where/when this indexing, actually copying in 2C02 is done 218 | //but (I think) it shouldn't hurt any games if this is done here 219 | 220 | scanline_sprites.resize(0); 221 | 222 | int range = 8; 223 | if (is_long_sprites) 224 | range = 16; 225 | 226 | NES_Byte j = 0; 227 | for (NES_Byte i = sprite_data_address / 4; i < 64; ++i) { 228 | auto diff = (scanline - sprite_memory[i * 4]); 229 | if (0 <= diff && diff < range) { 230 | scanline_sprites.push_back(i); 231 | if (++j >= 8) 232 | break; 233 | } 234 | } 235 | 236 | ++scanline; 237 | cycles = 0; 238 | } 239 | 240 | if (scanline >= VISIBLE_SCANLINES) 241 | pipeline_state = POST_RENDER; 242 | 243 | break; 244 | } 245 | case POST_RENDER: { 246 | if (cycles >= SCANLINE_END_CYCLE) { 247 | ++scanline; 248 | cycles = 0; 249 | pipeline_state = VERTICAL_BLANK; 250 | } 251 | break; 252 | } 253 | case VERTICAL_BLANK: { 254 | if (cycles == 1 && scanline == VISIBLE_SCANLINES + 1) { 255 | is_vblank = true; 256 | if (is_interrupting) vblank_callback(); 257 | } 258 | 259 | if (cycles >= SCANLINE_END_CYCLE) { 260 | ++scanline; 261 | cycles = 0; 262 | } 263 | 264 | if (scanline >= FRAME_END_SCANLINE) { 265 | pipeline_state = PRE_RENDER; 266 | scanline = 0; 267 | is_even_frame = !is_even_frame; 268 | // is_vblank = false; 269 | } 270 | 271 | break; 272 | } 273 | default: 274 | LOG(Error) << "Well, this shouldn't have happened." << std::endl; 275 | } 276 | ++cycles; 277 | } 278 | 279 | void PPU::do_DMA(const NES_Byte* page_ptr) { 280 | std::memcpy( 281 | sprite_memory.data() + sprite_data_address, 282 | page_ptr, 283 | 256 - sprite_data_address 284 | ); 285 | if (sprite_data_address) 286 | std::memcpy( 287 | sprite_memory.data(), 288 | page_ptr + (256 - sprite_data_address), 289 | sprite_data_address 290 | ); 291 | } 292 | 293 | void PPU::control(NES_Byte ctrl) { 294 | is_interrupting = ctrl & 0x80; 295 | is_long_sprites = ctrl & 0x20; 296 | background_page = static_cast(!!(ctrl & 0x10)); 297 | sprite_page = static_cast(!!(ctrl & 0x8)); 298 | if (ctrl & 0x4) 299 | data_address_increment = 0x20; 300 | else 301 | data_address_increment = 1; 302 | // baseNameTable = (ctrl & 0x3) * 0x400 + 0x2000; 303 | // Set the nametable in the temp address, this will be reflected in the 304 | // data address during rendering 305 | // v-- Unset 306 | temp_address &= ~0xc00; 307 | // v-- Set according to ctrl bits 308 | temp_address |= (ctrl & 0x3) << 10; 309 | } 310 | 311 | void PPU::set_mask(NES_Byte mask) { 312 | is_hiding_edge_background = !(mask & 0x2); 313 | is_hiding_edge_sprites = !(mask & 0x4); 314 | is_showing_background = mask & 0x8; 315 | is_showing_sprites = mask & 0x10; 316 | } 317 | 318 | NES_Byte PPU::get_status() { 319 | NES_Byte status = is_sprite_zero_hit << 6 | is_vblank << 7; 320 | // data_address = 0; 321 | is_vblank = false; 322 | is_first_write = true; 323 | return status; 324 | } 325 | 326 | void PPU::set_data_address(NES_Byte address) { 327 | // data_address = ((data_address << 8) & 0xff00) | address; 328 | if (is_first_write) { 329 | // Unset the upper byte 330 | temp_address &= ~0xff00; 331 | temp_address |= (address & 0x3f) << 8; 332 | is_first_write = false; 333 | } else { 334 | // Unset the lower byte; 335 | temp_address &= ~0xff; 336 | temp_address |= address; 337 | data_address = temp_address; 338 | is_first_write = true; 339 | } 340 | } 341 | 342 | NES_Byte PPU::get_data(PictureBus& bus) { 343 | auto data = bus.read(data_address); 344 | data_address += data_address_increment; 345 | // Reads are delayed by one byte/read when address is in this range 346 | if (data_address < 0x3f00) 347 | // Return from the data buffer and store the current value in the buffer 348 | std::swap(data, data_buffer); 349 | return data; 350 | } 351 | 352 | void PPU::set_data(PictureBus& bus, NES_Byte data) { 353 | bus.write(data_address, data); 354 | data_address += data_address_increment; 355 | } 356 | 357 | void PPU::set_scroll(NES_Byte scroll) { 358 | if (is_first_write) { 359 | temp_address &= ~0x1f; 360 | temp_address |= (scroll >> 3) & 0x1f; 361 | fine_x_scroll = scroll & 0x7; 362 | is_first_write = false; 363 | } else { 364 | temp_address &= ~0x73e0; 365 | temp_address |= ((scroll & 0x7) << 12) | ((scroll & 0xf8) << 2); 366 | is_first_write = true; 367 | } 368 | } 369 | 370 | } // namespace NES 371 | -------------------------------------------------------------------------------- /nes_py/nes_env.py: -------------------------------------------------------------------------------- 1 | """A CTypes interface to the C++ NES environment.""" 2 | import ctypes 3 | import glob 4 | import itertools 5 | import os 6 | import sys 7 | import gym 8 | from gym.spaces import Box 9 | from gym.spaces import Discrete 10 | import numpy as np 11 | from ._rom import ROM 12 | from ._image_viewer import ImageViewer 13 | 14 | 15 | # the path to the directory this file is in 16 | _MODULE_PATH = os.path.dirname(__file__) 17 | # the pattern to find the C++ shared object library 18 | _SO_PATH = 'lib_nes_env*' 19 | # the absolute path to the C++ shared object library 20 | _LIB_PATH = os.path.join(_MODULE_PATH, _SO_PATH) 21 | # load the library from the shared object file 22 | try: 23 | _LIB = ctypes.cdll.LoadLibrary(glob.glob(_LIB_PATH)[0]) 24 | except IndexError: 25 | raise OSError('missing static lib_nes_env*.so library!') 26 | 27 | 28 | # setup the argument and return types for Width 29 | _LIB.Width.argtypes = None 30 | _LIB.Width.restype = ctypes.c_uint 31 | # setup the argument and return types for Height 32 | _LIB.Height.argtypes = None 33 | _LIB.Height.restype = ctypes.c_uint 34 | # setup the argument and return types for Initialize 35 | _LIB.Initialize.argtypes = [ctypes.c_wchar_p] 36 | _LIB.Initialize.restype = ctypes.c_void_p 37 | # setup the argument and return types for Controller 38 | _LIB.Controller.argtypes = [ctypes.c_void_p, ctypes.c_uint] 39 | _LIB.Controller.restype = ctypes.c_void_p 40 | # setup the argument and return types for Screen 41 | _LIB.Screen.argtypes = [ctypes.c_void_p] 42 | _LIB.Screen.restype = ctypes.c_void_p 43 | # setup the argument and return types for GetMemoryBuffer 44 | _LIB.Memory.argtypes = [ctypes.c_void_p] 45 | _LIB.Memory.restype = ctypes.c_void_p 46 | # setup the argument and return types for Reset 47 | _LIB.Reset.argtypes = [ctypes.c_void_p] 48 | _LIB.Reset.restype = None 49 | # setup the argument and return types for Step 50 | _LIB.Step.argtypes = [ctypes.c_void_p] 51 | _LIB.Step.restype = None 52 | # setup the argument and return types for Backup 53 | _LIB.Backup.argtypes = [ctypes.c_void_p] 54 | _LIB.Backup.restype = None 55 | # setup the argument and return types for Restore 56 | _LIB.Restore.argtypes = [ctypes.c_void_p] 57 | _LIB.Restore.restype = None 58 | # setup the argument and return types for Close 59 | _LIB.Close.argtypes = [ctypes.c_void_p] 60 | _LIB.Close.restype = None 61 | 62 | 63 | # height in pixels of the NES screen 64 | SCREEN_HEIGHT = _LIB.Height() 65 | # width in pixels of the NES screen 66 | SCREEN_WIDTH = _LIB.Width() 67 | # shape of the screen as 24-bit RGB (standard for NumPy) 68 | SCREEN_SHAPE_24_BIT = SCREEN_HEIGHT, SCREEN_WIDTH, 3 69 | # shape of the screen as 32-bit RGB (C++ memory arrangement) 70 | SCREEN_SHAPE_32_BIT = SCREEN_HEIGHT, SCREEN_WIDTH, 4 71 | # create a type for the screen tensor matrix from C++ 72 | SCREEN_TENSOR = ctypes.c_byte * int(np.prod(SCREEN_SHAPE_32_BIT)) 73 | 74 | 75 | # create a type for the RAM vector from C++ 76 | RAM_VECTOR = ctypes.c_byte * 0x800 77 | 78 | 79 | # create a type for the controller buffers from C++ 80 | CONTROLLER_VECTOR = ctypes.c_byte * 1 81 | 82 | 83 | class NESEnv(gym.Env): 84 | """An NES environment based on the LaiNES emulator.""" 85 | 86 | # relevant meta-data about the environment 87 | metadata = { 88 | 'render.modes': ['rgb_array', 'human'], 89 | 'video.frames_per_second': 60 90 | } 91 | 92 | # the legal range for rewards for this environment 93 | reward_range = (-float('inf'), float('inf')) 94 | 95 | # observation space for the environment is static across all instances 96 | observation_space = Box( 97 | low=0, 98 | high=255, 99 | shape=SCREEN_SHAPE_24_BIT, 100 | dtype=np.uint8 101 | ) 102 | 103 | # action space is a bitmap of button press values for the 8 NES buttons 104 | action_space = Discrete(256) 105 | 106 | def __init__(self, rom_path): 107 | """ 108 | Create a new NES environment. 109 | 110 | Args: 111 | rom_path (str): the path to the ROM for the environment 112 | 113 | Returns: 114 | None 115 | 116 | """ 117 | # create a ROM file from the ROM path 118 | rom = ROM(rom_path) 119 | # check that there is PRG ROM 120 | if rom.prg_rom_size == 0: 121 | raise ValueError('ROM has no PRG-ROM banks.') 122 | # ensure that there is no trainer 123 | if rom.has_trainer: 124 | raise ValueError('ROM has trainer. trainer is not supported.') 125 | # try to read the PRG ROM and raise a value error if it fails 126 | _ = rom.prg_rom 127 | # try to read the CHR ROM and raise a value error if it fails 128 | _ = rom.chr_rom 129 | # check the TV system 130 | if rom.is_pal: 131 | raise ValueError('ROM is PAL. PAL is not supported.') 132 | # check that the mapper is implemented 133 | elif rom.mapper not in {0, 1, 2, 3}: 134 | msg = 'ROM has an unsupported mapper number {}. please see https://github.com/Kautenja/nes-py/issues/28 for more information.' 135 | raise ValueError(msg.format(rom.mapper)) 136 | # create a dedicated random number generator for the environment 137 | self.np_random = np.random.RandomState() 138 | # store the ROM path 139 | self._rom_path = rom_path 140 | # initialize the C++ object for running the environment 141 | self._env = _LIB.Initialize(self._rom_path) 142 | # setup a placeholder for a 'human' render mode viewer 143 | self.viewer = None 144 | # setup a placeholder for a pointer to a backup state 145 | self._has_backup = False 146 | # setup a done flag 147 | self.done = True 148 | # setup the controllers, screen, and RAM buffers 149 | self.controllers = [self._controller_buffer(port) for port in range(2)] 150 | self.screen = self._screen_buffer() 151 | self.ram = self._ram_buffer() 152 | 153 | def _screen_buffer(self): 154 | """Setup the screen buffer from the C++ code.""" 155 | # get the address of the screen 156 | address = _LIB.Screen(self._env) 157 | # create a buffer from the contents of the address location 158 | buffer_ = ctypes.cast(address, ctypes.POINTER(SCREEN_TENSOR)).contents 159 | # create a NumPy array from the buffer 160 | screen = np.frombuffer(buffer_, dtype='uint8') 161 | # reshape the screen from a column vector to a tensor 162 | screen = screen.reshape(SCREEN_SHAPE_32_BIT) 163 | # flip the bytes if the machine is little-endian (which it likely is) 164 | if sys.byteorder == 'little': 165 | # invert the little-endian BGRx channels to big-endian xRGB 166 | screen = screen[:, :, ::-1] 167 | # remove the 0th axis (padding from storing colors in 32 bit) 168 | return screen[:, :, 1:] 169 | 170 | def _ram_buffer(self): 171 | """Setup the RAM buffer from the C++ code.""" 172 | # get the address of the RAM 173 | address = _LIB.Memory(self._env) 174 | # create a buffer from the contents of the address location 175 | buffer_ = ctypes.cast(address, ctypes.POINTER(RAM_VECTOR)).contents 176 | # create a NumPy array from the buffer 177 | return np.frombuffer(buffer_, dtype='uint8') 178 | 179 | def _controller_buffer(self, port): 180 | """ 181 | Find the pointer to a controller and setup a NumPy buffer. 182 | 183 | Args: 184 | port: the port of the controller to setup 185 | 186 | Returns: 187 | a NumPy buffer with the controller's binary data 188 | 189 | """ 190 | # get the address of the controller 191 | address = _LIB.Controller(self._env, port) 192 | # create a memory buffer using the ctypes pointer for this vector 193 | buffer_ = ctypes.cast(address, ctypes.POINTER(CONTROLLER_VECTOR)).contents 194 | # create a NumPy buffer from the binary data and return it 195 | return np.frombuffer(buffer_, dtype='uint8') 196 | 197 | def _frame_advance(self, action): 198 | """ 199 | Advance a frame in the emulator with an action. 200 | 201 | Args: 202 | action (byte): the action to press on the joy-pad 203 | 204 | Returns: 205 | None 206 | 207 | """ 208 | # set the action on the controller 209 | self.controllers[0][:] = action 210 | # perform a step on the emulator 211 | _LIB.Step(self._env) 212 | 213 | def _backup(self): 214 | """Backup the NES state in the emulator.""" 215 | _LIB.Backup(self._env) 216 | self._has_backup = True 217 | 218 | def _restore(self): 219 | """Restore the backup state into the NES emulator.""" 220 | _LIB.Restore(self._env) 221 | 222 | def _will_reset(self): 223 | """Handle any RAM hacking after a reset occurs.""" 224 | pass 225 | 226 | def seed(self, seed=None): 227 | """ 228 | Set the seed for this environment's random number generator. 229 | 230 | Returns: 231 | list: Returns the list of seeds used in this env's random 232 | number generators. The first value in the list should be the 233 | "main" seed, or the value which a reproducer should pass to 234 | 'seed'. Often, the main seed equals the provided 'seed', but 235 | this won't be true if seed=None, for example. 236 | 237 | """ 238 | # if there is no seed, return an empty list 239 | if seed is None: 240 | return [] 241 | # set the random number seed for the NumPy random number generator 242 | self.np_random.seed(seed) 243 | # return the list of seeds used by RNG(s) in the environment 244 | return [seed] 245 | 246 | def reset(self, seed=None, options=None, return_info=None): 247 | """ 248 | Reset the state of the environment and returns an initial observation. 249 | 250 | Args: 251 | seed (int): an optional random number seed for the next episode 252 | options (any): unused 253 | return_info (any): unused 254 | 255 | Returns: 256 | state (np.ndarray): next frame as a result of the given action 257 | 258 | """ 259 | # Set the seed. 260 | self.seed(seed) 261 | # call the before reset callback 262 | self._will_reset() 263 | # reset the emulator 264 | if self._has_backup: 265 | self._restore() 266 | else: 267 | _LIB.Reset(self._env) 268 | # call the after reset callback 269 | self._did_reset() 270 | # set the done flag to false 271 | self.done = False 272 | # return the screen from the emulator 273 | return self.screen 274 | 275 | def _did_reset(self): 276 | """Handle any RAM hacking after a reset occurs.""" 277 | pass 278 | 279 | def step(self, action): 280 | """ 281 | Run one frame of the NES and return the relevant observation data. 282 | 283 | Args: 284 | action (byte): the bitmap determining which buttons to press 285 | 286 | Returns: 287 | a tuple of: 288 | - state (np.ndarray): next frame as a result of the given action 289 | - reward (float) : amount of reward returned after given action 290 | - done (boolean): whether the episode has ended 291 | - info (dict): contains auxiliary diagnostic information 292 | 293 | """ 294 | # if the environment is done, raise an error 295 | if self.done: 296 | raise ValueError('cannot step in a done environment! call `reset`') 297 | # set the action on the controller 298 | self.controllers[0][:] = action 299 | # pass the action to the emulator as an unsigned byte 300 | _LIB.Step(self._env) 301 | # get the reward for this step 302 | reward = float(self._get_reward()) 303 | # get the done flag for this step 304 | self.done = bool(self._get_done()) 305 | # get the info for this step 306 | info = self._get_info() 307 | # call the after step callback 308 | self._did_step(self.done) 309 | # bound the reward in [min, max] 310 | if reward < self.reward_range[0]: 311 | reward = self.reward_range[0] 312 | elif reward > self.reward_range[1]: 313 | reward = self.reward_range[1] 314 | # return the screen from the emulator and other relevant data 315 | return self.screen, reward, self.done, info 316 | 317 | def _get_reward(self): 318 | """Return the reward after a step occurs.""" 319 | return 0 320 | 321 | def _get_done(self): 322 | """Return True if the episode is over, False otherwise.""" 323 | return False 324 | 325 | def _get_info(self): 326 | """Return the info after a step occurs.""" 327 | return {} 328 | 329 | def _did_step(self, done): 330 | """ 331 | Handle any RAM hacking after a step occurs. 332 | 333 | Args: 334 | done (bool): whether the done flag is set to true 335 | 336 | Returns: 337 | None 338 | 339 | """ 340 | pass 341 | 342 | def close(self): 343 | """Close the environment.""" 344 | # make sure the environment hasn't already been closed 345 | if self._env is None: 346 | raise ValueError('env has already been closed.') 347 | # purge the environment from C++ memory 348 | _LIB.Close(self._env) 349 | # deallocate the object locally 350 | self._env = None 351 | # if there is an image viewer open, delete it 352 | if self.viewer is not None: 353 | self.viewer.close() 354 | 355 | def render(self, mode='human'): 356 | """ 357 | Render the environment. 358 | 359 | Args: 360 | mode (str): the mode to render with: 361 | - human: render to the current display 362 | - rgb_array: Return an numpy.ndarray with shape (x, y, 3), 363 | representing RGB values for an x-by-y pixel image 364 | 365 | Returns: 366 | a numpy array if mode is 'rgb_array', None otherwise 367 | 368 | """ 369 | if mode == 'human': 370 | # if the viewer isn't setup, import it and create one 371 | if self.viewer is None: 372 | # get the caption for the ImageViewer 373 | if self.spec is None: 374 | # if there is no spec, just use the .nes filename 375 | caption = self._rom_path.split('/')[-1] 376 | else: 377 | # set the caption to the OpenAI Gym id 378 | caption = self.spec.id 379 | # create the ImageViewer to display frames 380 | self.viewer = ImageViewer( 381 | caption=caption, 382 | height=SCREEN_HEIGHT, 383 | width=SCREEN_WIDTH, 384 | ) 385 | # show the screen on the image viewer 386 | self.viewer.show(self.screen) 387 | elif mode == 'rgb_array': 388 | return self.screen 389 | else: 390 | # unpack the modes as comma delineated strings ('a', 'b', ...) 391 | render_modes = [repr(x) for x in self.metadata['render.modes']] 392 | msg = 'valid render modes are: {}'.format(', '.join(render_modes)) 393 | raise NotImplementedError(msg) 394 | 395 | def get_keys_to_action(self): 396 | """Return the dictionary of keyboard keys to actions.""" 397 | # keyboard keys in an array ordered by their byte order in the bitmap 398 | # i.e. right = 7, left = 6, ..., B = 1, A = 0 399 | buttons = np.array([ 400 | ord('d'), # right 401 | ord('a'), # left 402 | ord('s'), # down 403 | ord('w'), # up 404 | ord('\r'), # start 405 | ord(' '), # select 406 | ord('p'), # B 407 | ord('o'), # A 408 | ]) 409 | # the dictionary of key presses to controller codes 410 | keys_to_action = {} 411 | # the combination map of values for the controller 412 | values = 8 * [[0, 1]] 413 | # iterate over all the combinations 414 | for combination in itertools.product(*values): 415 | # unpack the tuple of bits into an integer 416 | byte = int(''.join(map(str, combination)), 2) 417 | # unwrap the pressed buttons based on the bitmap 418 | pressed = buttons[list(map(bool, combination))] 419 | # assign the pressed buttons to the output byte 420 | keys_to_action[tuple(sorted(pressed))] = byte 421 | 422 | return keys_to_action 423 | 424 | def get_action_meanings(self): 425 | """Return a list of actions meanings.""" 426 | return ['NOOP'] 427 | 428 | 429 | # explicitly define the outward facing API of this module 430 | __all__ = [NESEnv.__name__] 431 | -------------------------------------------------------------------------------- /nes_py/nes/src/cpu.cpp: -------------------------------------------------------------------------------- 1 | // Program: nes-py 2 | // File: cpu.cpp 3 | // Description: This class houses the logic and data for the NES CPU 4 | // 5 | // Copyright (c) 2019 Christian Kauten. All rights reserved. 6 | // 7 | 8 | #include "cpu.hpp" 9 | #include "log.hpp" 10 | 11 | namespace NES { 12 | 13 | bool CPU::implied(MainBus &bus, NES_Byte opcode) { 14 | switch (static_cast(opcode)) { 15 | case BRK: { 16 | interrupt(bus, BRK_INTERRUPT); 17 | break; 18 | } 19 | case PHP: { 20 | push_stack(bus, flags.byte); 21 | break; 22 | } 23 | case CLC: { 24 | flags.bits.C = false; 25 | break; 26 | } 27 | case JSR: { 28 | // Push address of next instruction - 1, thus register_PC + 1 29 | // instead of register_PC + 2 since register_PC and 30 | // register_PC + 1 are address of subroutine 31 | push_stack(bus, static_cast((register_PC + 1) >> 8)); 32 | push_stack(bus, static_cast(register_PC + 1)); 33 | register_PC = read_address(bus, register_PC); 34 | break; 35 | } 36 | case PLP: { 37 | flags.byte = pop_stack(bus); 38 | break; 39 | } 40 | case SEC: { 41 | flags.bits.C = true; 42 | break; 43 | } 44 | case RTI: { 45 | flags.byte = pop_stack(bus); 46 | register_PC = pop_stack(bus); 47 | register_PC |= pop_stack(bus) << 8; 48 | break; 49 | } 50 | case PHA: { 51 | push_stack(bus, register_A); 52 | break; 53 | } 54 | case JMP: { 55 | register_PC = read_address(bus, register_PC); 56 | break; 57 | } 58 | case CLI: { 59 | flags.bits.I = false; 60 | break; 61 | } 62 | case RTS: { 63 | register_PC = pop_stack(bus); 64 | register_PC |= pop_stack(bus) << 8; 65 | ++register_PC; 66 | break; 67 | } 68 | case PLA: { 69 | register_A = pop_stack(bus); 70 | set_ZN(register_A); 71 | break; 72 | } 73 | case JMPI: { 74 | NES_Address location = read_address(bus, register_PC); 75 | // 6502 has a bug such that the when the vector of an indirect 76 | // address begins at the last byte of a page, the second byte 77 | // is fetched from the beginning of that page rather than the 78 | // beginning of the next 79 | // Recreating here: 80 | NES_Address Page = location & 0xff00; 81 | register_PC = bus.read(location) | bus.read(Page | ((location + 1) & 0xff)) << 8; 82 | break; 83 | } 84 | case SEI: { 85 | flags.bits.I = true; 86 | break; 87 | } 88 | case DEY: { 89 | --register_Y; 90 | set_ZN(register_Y); 91 | break; 92 | } 93 | case TXA: { 94 | register_A = register_X; 95 | set_ZN(register_A); 96 | break; 97 | } 98 | case TYA: { 99 | register_A = register_Y; 100 | set_ZN(register_A); 101 | break; 102 | } 103 | case TXS: { 104 | register_SP = register_X; 105 | break; 106 | } 107 | case TAY: { 108 | register_Y = register_A; 109 | set_ZN(register_Y); 110 | break; 111 | } 112 | case TAX: { 113 | register_X = register_A; 114 | set_ZN(register_X); 115 | break; 116 | } 117 | case CLV: { 118 | flags.bits.V = false; 119 | break; 120 | } 121 | case TSX: { 122 | register_X = register_SP; 123 | set_ZN(register_X); 124 | break; 125 | } 126 | case INY: { 127 | ++register_Y; 128 | set_ZN(register_Y); 129 | break; 130 | } 131 | case DEX: { 132 | --register_X; 133 | set_ZN(register_X); 134 | break; 135 | } 136 | case CLD: { 137 | flags.bits.D = false; 138 | break; 139 | } 140 | case INX: { 141 | ++register_X; 142 | set_ZN(register_X); 143 | break; 144 | } 145 | case NOP: { 146 | break; 147 | } 148 | case SED: { 149 | flags.bits.D = true; 150 | break; 151 | } 152 | default: return false; 153 | } 154 | return true; 155 | } 156 | 157 | bool CPU::branch(MainBus &bus, NES_Byte opcode) { 158 | if ((opcode & BRANCH_INSTRUCTION_MASK) != BRANCH_INSTRUCTION_MASK_RESULT) 159 | return false; 160 | 161 | // branch is initialized to the condition required (for the flag 162 | // specified later) 163 | bool branch = opcode & BRANCH_CONDITION_MASK; 164 | 165 | // set branch to true if the given condition is met by the given flag 166 | // We use xnor here, it is true if either both operands are true or 167 | // false 168 | switch (opcode >> BRANCH_ON_FLAG_SHIFT) { 169 | case NEGATIVE_: { 170 | branch = !(branch ^ flags.bits.N); 171 | break; 172 | } 173 | case OVERFLOW_: { 174 | branch = !(branch ^ flags.bits.V); 175 | break; 176 | } 177 | case CARRY_: { 178 | branch = !(branch ^ flags.bits.C); 179 | break; 180 | } 181 | case ZERO_: { 182 | branch = !(branch ^ flags.bits.Z); 183 | break; 184 | } 185 | default: return false; 186 | } 187 | 188 | if (branch) { 189 | int8_t offset = bus.read(register_PC++); 190 | ++skip_cycles; 191 | auto newPC = static_cast(register_PC + offset); 192 | set_page_crossed(register_PC, newPC, 2); 193 | register_PC = newPC; 194 | } else { 195 | ++register_PC; 196 | } 197 | return true; 198 | } 199 | 200 | bool CPU::type0(MainBus &bus, NES_Byte opcode) { 201 | if ((opcode & INSTRUCTION_MODE_MASK) != 0x0) 202 | return false; 203 | 204 | NES_Address location = 0; 205 | switch (static_cast((opcode & ADRESS_MODE_MASK) >> ADDRESS_MODE_SHIFT)) { 206 | case M2_IMMEDIATE: { 207 | location = register_PC++; 208 | break; 209 | } 210 | case M2_ZERO_PAGE: { 211 | location = bus.read(register_PC++); 212 | break; 213 | } 214 | case M2_ABSOLUTE: { 215 | location = read_address(bus, register_PC); 216 | register_PC += 2; 217 | break; 218 | } 219 | case M2_INDEXED: { 220 | // Address wraps around in the zero page 221 | location = (bus.read(register_PC++) + register_X) & 0xff; 222 | break; 223 | } 224 | case M2_ABSOLUTE_INDEXED: { 225 | location = read_address(bus, register_PC); 226 | register_PC += 2; 227 | set_page_crossed(location, location + register_X); 228 | location += register_X; 229 | break; 230 | } 231 | default: return false; 232 | } 233 | switch (static_cast((opcode & OPERATION_MASK) >> OPERATION_SHIFT)) { 234 | case BIT: { 235 | NES_Address operand = bus.read(location); 236 | flags.bits.Z = !(register_A & operand); 237 | flags.bits.V = operand & 0x40; 238 | flags.bits.N = operand & 0x80; 239 | break; 240 | } 241 | case STY: { 242 | bus.write(location, register_Y); 243 | break; 244 | } 245 | case LDY: { 246 | register_Y = bus.read(location); 247 | set_ZN(register_Y); 248 | break; 249 | } 250 | case CPY: { 251 | NES_Address diff = register_Y - bus.read(location); 252 | flags.bits.C = !(diff & 0x100); 253 | set_ZN(diff); 254 | break; 255 | } 256 | case CPX: { 257 | NES_Address diff = register_X - bus.read(location); 258 | flags.bits.C = !(diff & 0x100); 259 | set_ZN(diff); 260 | break; 261 | } 262 | default: return false; 263 | } 264 | return true; 265 | } 266 | 267 | bool CPU::type1(MainBus &bus, NES_Byte opcode) { 268 | if ((opcode & INSTRUCTION_MODE_MASK) != 0x1) 269 | return false; 270 | // Location of the operand, could be in RAM 271 | NES_Address location = 0; 272 | auto op = static_cast((opcode & OPERATION_MASK) >> OPERATION_SHIFT); 273 | switch (static_cast((opcode & ADRESS_MODE_MASK) >> ADDRESS_MODE_SHIFT)) { 274 | case M1_INDEXED_INDIRECT_X: { 275 | NES_Byte zero_address = register_X + bus.read(register_PC++); 276 | // Addresses wrap in zero page mode, thus pass through a mask 277 | location = bus.read(zero_address & 0xff) | bus.read((zero_address + 1) & 0xff) << 8; 278 | break; 279 | } 280 | case M1_ZERO_PAGE: { 281 | location = bus.read(register_PC++); 282 | break; 283 | } 284 | case M1_IMMEDIATE: { 285 | location = register_PC++; 286 | break; 287 | } 288 | case M1_ABSOLUTE: { 289 | location = read_address(bus, register_PC); 290 | register_PC += 2; 291 | break; 292 | } 293 | case M1_INDIRECT_Y: { 294 | NES_Byte zero_address = bus.read(register_PC++); 295 | location = bus.read(zero_address & 0xff) | bus.read((zero_address + 1) & 0xff) << 8; 296 | if (op != STA) 297 | set_page_crossed(location, location + register_Y); 298 | location += register_Y; 299 | break; 300 | } 301 | case M1_INDEXED_X: { 302 | // Address wraps around in the zero page 303 | location = (bus.read(register_PC++) + register_X) & 0xff; 304 | break; 305 | } 306 | case M1_ABSOLUTE_Y: { 307 | location = read_address(bus, register_PC); 308 | register_PC += 2; 309 | if (op != STA) 310 | set_page_crossed(location, location + register_Y); 311 | location += register_Y; 312 | break; 313 | } 314 | case M1_ABSOLUTE_X: { 315 | location = read_address(bus, register_PC); 316 | register_PC += 2; 317 | if (op != STA) 318 | set_page_crossed(location, location + register_X); 319 | location += register_X; 320 | break; 321 | } 322 | default: return false; 323 | } 324 | 325 | switch (op) { 326 | case ORA: { 327 | register_A |= bus.read(location); 328 | set_ZN(register_A); 329 | break; 330 | } 331 | case AND: { 332 | register_A &= bus.read(location); 333 | set_ZN(register_A); 334 | break; 335 | } 336 | case EOR: { 337 | register_A ^= bus.read(location); 338 | set_ZN(register_A); 339 | break; 340 | } 341 | case ADC: { 342 | NES_Byte operand = bus.read(location); 343 | NES_Address sum = register_A + operand + flags.bits.C; 344 | //Carry forward or UNSIGNED overflow 345 | flags.bits.C = sum & 0x100; 346 | //SIGNED overflow, would only happen if the sign of sum is 347 | //different from BOTH the operands 348 | flags.bits.V = (register_A ^ sum) & (operand ^ sum) & 0x80; 349 | register_A = static_cast(sum); 350 | set_ZN(register_A); 351 | break; 352 | } 353 | case STA: { 354 | bus.write(location, register_A); 355 | break; 356 | } 357 | case LDA: { 358 | register_A = bus.read(location); 359 | set_ZN(register_A); 360 | break; 361 | } 362 | case CMP: { 363 | NES_Address diff = register_A - bus.read(location); 364 | flags.bits.C = !(diff & 0x100); 365 | set_ZN(diff); 366 | break; 367 | } 368 | case SBC: { 369 | //High carry means "no borrow", thus negate and subtract 370 | NES_Address subtrahend = bus.read(location), 371 | diff = register_A - subtrahend - !flags.bits.C; 372 | //if the ninth bit is 1, the resulting number is negative => borrow => low carry 373 | flags.bits.C = !(diff & 0x100); 374 | //Same as ADC, except instead of the subtrahend, 375 | //substitute with it's one complement 376 | flags.bits.V = (register_A ^ diff) & (~subtrahend ^ diff) & 0x80; 377 | register_A = diff; 378 | set_ZN(diff); 379 | break; 380 | } 381 | default: return false; 382 | } 383 | return true; 384 | } 385 | 386 | bool CPU::type2(MainBus &bus, NES_Byte opcode) { 387 | if ((opcode & INSTRUCTION_MODE_MASK) != 2) 388 | return false; 389 | 390 | NES_Address location = 0; 391 | auto op = static_cast((opcode & OPERATION_MASK) >> OPERATION_SHIFT); 392 | auto address_mode = static_cast((opcode & ADRESS_MODE_MASK) >> ADDRESS_MODE_SHIFT); 393 | switch (address_mode) { 394 | case M2_IMMEDIATE: { 395 | location = register_PC++; 396 | break; 397 | } 398 | case M2_ZERO_PAGE: { 399 | location = bus.read(register_PC++); 400 | break; 401 | } 402 | case M2_ACCUMULATOR: { 403 | break; 404 | } 405 | case M2_ABSOLUTE: { 406 | location = read_address(bus, register_PC); 407 | register_PC += 2; 408 | break; 409 | } 410 | case M2_INDEXED: { 411 | location = bus.read(register_PC++); 412 | NES_Byte index; 413 | if (op == LDX || op == STX) 414 | index = register_Y; 415 | else 416 | index = register_X; 417 | //The mask wraps address around zero page 418 | location = (location + index) & 0xff; 419 | break; 420 | } 421 | case M2_ABSOLUTE_INDEXED: { 422 | location = read_address(bus, register_PC); 423 | register_PC += 2; 424 | NES_Byte index; 425 | if (op == LDX || op == STX) 426 | index = register_Y; 427 | else 428 | index = register_X; 429 | set_page_crossed(location, location + index); 430 | location += index; 431 | break; 432 | } 433 | default: return false; 434 | } 435 | 436 | NES_Address operand = 0; 437 | switch (op) { 438 | case ASL: 439 | case ROL: 440 | if (address_mode == M2_ACCUMULATOR) { 441 | auto prev_C = flags.bits.C; 442 | flags.bits.C = register_A & 0x80; 443 | register_A <<= 1; 444 | //If Rotating, set the bit-0 to the the previous carry 445 | register_A = register_A | (prev_C && (op == ROL)); 446 | set_ZN(register_A); 447 | } else { 448 | auto prev_C = flags.bits.C; 449 | operand = bus.read(location); 450 | flags.bits.C = operand & 0x80; 451 | operand = operand << 1 | (prev_C && (op == ROL)); 452 | set_ZN(operand); 453 | bus.write(location, operand); 454 | } 455 | break; 456 | case LSR: 457 | case ROR: 458 | if (address_mode == M2_ACCUMULATOR) { 459 | auto prev_C = flags.bits.C; 460 | flags.bits.C = register_A & 1; 461 | register_A >>= 1; 462 | //If Rotating, set the bit-7 to the previous carry 463 | register_A = register_A | (prev_C && (op == ROR)) << 7; 464 | set_ZN(register_A); 465 | } else { 466 | auto prev_C = flags.bits.C; 467 | operand = bus.read(location); 468 | flags.bits.C = operand & 1; 469 | operand = operand >> 1 | (prev_C && (op == ROR)) << 7; 470 | set_ZN(operand); 471 | bus.write(location, operand); 472 | } 473 | break; 474 | case STX: { 475 | bus.write(location, register_X); 476 | break; 477 | } 478 | case LDX: { 479 | register_X = bus.read(location); 480 | set_ZN(register_X); 481 | break; 482 | } 483 | case DEC: { 484 | auto tmp = bus.read(location) - 1; 485 | set_ZN(tmp); 486 | bus.write(location, tmp); 487 | break; 488 | } 489 | case INC: { 490 | auto tmp = bus.read(location) + 1; 491 | set_ZN(tmp); 492 | bus.write(location, tmp); 493 | break; 494 | } 495 | default: return false; 496 | } 497 | return true; 498 | } 499 | 500 | void CPU::reset(NES_Address start_address) { 501 | skip_cycles = 0; 502 | cycles = 0; 503 | register_A = 0; 504 | register_X = 0; 505 | register_Y = 0; 506 | // flags.bits.I = true; 507 | // flags.bits.C = false; 508 | // flags.bits.D = false; 509 | // flags.bits.N = false; 510 | // flags.bits.V = false; 511 | // flags.bits.Z = false; 512 | flags.byte = 0b00110100; 513 | register_PC = start_address; 514 | // documented startup state 515 | register_SP = 0xfd; 516 | } 517 | 518 | void CPU::interrupt(MainBus &bus, InterruptType type) { 519 | if (flags.bits.I && type != NMI_INTERRUPT && type != BRK_INTERRUPT) 520 | return; 521 | // Add one if BRK, a quirk of 6502 522 | if (type == BRK_INTERRUPT) 523 | ++register_PC; 524 | // push values on to the stack 525 | push_stack(bus, register_PC >> 8); 526 | push_stack(bus, register_PC); 527 | push_stack(bus, flags.byte | 0b00100000 | (type == BRK_INTERRUPT) << 4); 528 | // set the interrupt flag 529 | flags.bits.I = true; 530 | // handle the kind of interrupt 531 | switch (type) { 532 | case IRQ_INTERRUPT: 533 | case BRK_INTERRUPT: 534 | register_PC = read_address(bus, IRQ_VECTOR); 535 | break; 536 | case NMI_INTERRUPT: 537 | register_PC = read_address(bus, NMI_VECTOR); 538 | break; 539 | } 540 | // add the number of cycles to handle the interrupt 541 | skip_cycles += 7; 542 | } 543 | 544 | void CPU::cycle(MainBus &bus) { 545 | // increment the number of cycles 546 | ++cycles; 547 | // if in a skip cycle, return 548 | if (skip_cycles-- > 1) 549 | return; 550 | // reset the number of skip cycles to 0 551 | skip_cycles = 0; 552 | // read the opcode from the bus and lookup the number of cycles 553 | NES_Byte op = bus.read(register_PC++); 554 | // Using short-circuit evaluation, call the other function only if the 555 | // first failed. ExecuteImplied must be called first and ExecuteBranch 556 | // must be before ExecuteType0 557 | if (implied(bus, op) || branch(bus, op) || type1(bus, op) || type2(bus, op) || type0(bus, op)) 558 | skip_cycles += OPERATION_CYCLES[op]; 559 | else 560 | std::cout << "failed to execute opcode: " << std::hex << +op << std::endl; 561 | } 562 | 563 | } // namespace NES 564 | --------------------------------------------------------------------------------