├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md └── workflows │ └── python-app.yml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FONTS.chip8 ├── LICENSE ├── PULL_REQUEST_TEMPLATE.md ├── README.md ├── chip8 ├── __init__.py ├── config.py ├── cpu.py ├── emulator.py └── screen.py ├── codecov.yml ├── requirements.txt ├── test ├── __init__.py ├── romfile ├── test_chip8cpu.py └── test_chip8screen.py └── yac8e.py /.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 behaviour** 8 | A clear and concise description of what the bug is. 9 | 10 | **Steps for reproduction** 11 | Steps to reproduce the behavior: 12 | 1. Go to '...' 13 | 2. Click on '....' 14 | 3. Scroll down to '....' 15 | 4. See error 16 | 17 | **Describe expected behaviour** 18 | A clear and concise description of what you expected to happen. 19 | 20 | **Screenshots** 21 | If applicable, add screenshots to help explain your problem. 22 | 23 | **Desktop (please complete the following information):** 24 | - OS: [e.g. iOS] 25 | - Browser [e.g. chrome, safari] 26 | - Version [e.g. 22] 27 | 28 | **Additional context** 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | 5 | --- 6 | 7 | **Is your feature request related to a problem? Please describe.** 8 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 9 | 10 | **Describe the solution you'd like** 11 | A clear and concise description of what you want to happen. 12 | 13 | **Describe alternatives you've considered** 14 | A clear and concise description of any alternative solutions or features you've considered. 15 | 16 | **Additional context** 17 | Add any other context or screenshots about the feature request here. 18 | -------------------------------------------------------------------------------- /.github/workflows/python-app.yml: -------------------------------------------------------------------------------- 1 | name: Build Test Coverage 2 | on: [pull_request, workflow_dispatch] 3 | jobs: 4 | run: 5 | runs-on: ubuntu-latest 6 | env: 7 | DISPLAY: :0 8 | SDL_AUDIODRIVER: "disk" 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v3 12 | - name: Setup Python 3.8.12 13 | uses: actions/setup-python@v4 14 | with: 15 | python-version: '3.8.12' 16 | - name: Update PIP 17 | run: python -m pip install --upgrade pip 18 | - name: Install OS Dependencies 19 | run: sudo apt-get install xvfb 20 | - name: Install Requirements 21 | run: pip install -r requirements.txt 22 | - name: Generate Report 23 | run: xvfb-run --auto-servernum coverage run --source=chip8 -m unittest 24 | - name: Codecov 25 | uses: codecov/codecov-action@v4.2.0 26 | env: 27 | CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .idea 3 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | craig.thomas@gmail.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | First of all, thank you for considering making a contribution to the project! Below are some guidelines for contributing 4 | to the project. Please feel free to propose changes to the guidelines in a pull request if you feel something is missing 5 | or needs more clarity. 6 | 7 | # Table of Contents 8 | 9 | 1. [How can I contribute?](#how-can-i-contribute) 10 | 1. [Reporting bugs](#reporting-bugs) 11 | 2. [Suggesting features](#suggesting-features) 12 | 3. [Contributing code](#contributing-code) 13 | 4. [Pull requests](#pull-requests) 14 | 2. [Style guidelines](#style-guidelines) 15 | 1. [Git commit messages](#git-commit-messages) 16 | 2. [Code style](#code-style) 17 | 18 | # How can I contribute? 19 | 20 | There are several different way that you can contribute to the development of the project, each of which are most 21 | welcome. 22 | 23 | ## Reporting bugs 24 | 25 | Bugs are reported through the [GitHub Issue](https://github.com/craigthomas/Chip8Python/issues) interface. Please check 26 | first to see if the bug you are reporting has already been reported. When reporting a bug, please take the time to 27 | describe the following details: 28 | 29 | 1. **Descriptive Title** - please ensure that the title to your issue is clear and descriptive of the problem. 30 | 2. **Steps for Reproduction** - outline all the steps exactly that are required to reproduce the bug. For example, 31 | start by explaining how you started the program, which ROM you were running, and any other command line switches 32 | that were set, as well as what environment you are running in (e.g. Windows, Linux, MacOS, etc). 33 | 3. **Describe the Bug Behaviour** - describe what happens with the emulator, and why you think that this behaviour 34 | represents a bug. 35 | 4. **Describe Expected Behaviour** - describe what behaviour you believe should occur as a result of the steps 36 | that you took up to the point where the bug occurred. 37 | 38 | Please feel free to provide additional context to help a developer track down the bug: 39 | 40 | * **Can you reproduce the bug reliably or is it intermittent?** If it is intermittent, please try to explain what 41 | would normally provoke the bug, or under what conditions you saw it occur last time. 42 | * **Does it happen for any other ROMs?** It is helpful if you can try and pinpoint the buggy behaviour to a certain 43 | ROM or handful of ROMs. 44 | 45 | ## Suggesting features 46 | 47 | When suggesting features or enhancements for the emulator, it is best to check out the open issues first to see whether 48 | or not such a feature is already under development. It is also worthwhile checking any open branches to see if the 49 | feature you are requesting is already in development. Finally, before submitting your suggestion, please ensure that 50 | the feature does not already exist by updating your local copy with the latest version from our `master` branch. 51 | 52 | To submit a feature request, please open a new [GitHub Issue](https://github.com/craigthomas/Chip8Python/issues) and 53 | provide the following details: 54 | 55 | 1. **Descriptive Title** - please ensure that the title to your issue is clear and descriptive of the enhancement or 56 | functionality you wish to see. 57 | 2. **Step by Step Description** - describe how the functionality of the system should occur with a step-by-step breakdown 58 | of how you expect the emulator to run. For example, if you wish to have a new debugging key added, describe how the 59 | emulator execution flow will change when the key is pressed. 60 | 3. **Use Animated GIFs** - if you are feeling ambitious, or if you feel words do not adequately describe the proposed 61 | functionality, please submit an animated GIF, or a drawing of the new proposed functionality. 62 | 4. **Explain Usefulness** - please take a brief moment to describe why you feel the new functionality would be useful. 63 | 64 | ## Contributing code 65 | 66 | Code contributions should be made using the following process: 67 | 68 | 1. **Fork the Repository** - create a fork of the respository using the Fork button in GitHub. 69 | 2. **Make Code Changes** - using your forked repository, make changes to the code as you see fit. We recommend creating a 70 | branch for your code changes so that you can easily update your own local master without creating too many merge 71 | conflicts. 72 | 3. **Submit Pull Request** - when you are ready, submit a pull request back to the `master` branch of this repository. 73 | The pull request will be reviewed, and you may be asked questions about the changes you are proposing. In some cases, 74 | we may ask you to make adjustments to the code to fit in with the overall style and behavioiur of the rest of the 75 | project. 76 | 77 | There are also some additional guidelines you should follow when coding up enhancements or bugfixes: 78 | 79 | 1. Please reference any open issues that the Pull Request will close by writing `Closes #` with the issue number (e.g. `Closes #12`). 80 | 2. New functionality should have unit and/or integration tests that exercise at least 50% of the code that was added. 81 | 3. For bug fixes, please ensure that you have a test that covers buggy input conditions so that we reduce the likelihood of 82 | a regression in the future. 83 | 4. Please ensure all functions have appropriately descriptive docstrings, as well as descriptions for inputs and outputs. 84 | 85 | If you don't know where to start, then take a look for issues marked `beginner` or `help-wanted`. Any issues with the `beginner` tag 86 | will generally only require one or two lines of code to fix. Issues marked `help-wanted` may be more complex than beginner issues, 87 | but should be scoped in such a way to ease you in to the codebase. 88 | 89 | ## Pull requests 90 | 91 | Please follow all the instructions as mentioned in the Pull Request template. When you submit your pull request, please ensure that 92 | all of the required [status checks](https://help.github.com/articles/about-status-checks/) have succeeded. If the status checks 93 | are failing, and you believe that the failures are not related to your change, please leave a description within the pull request why 94 | you believe the failures are not related to your code changes. A maintainer will re-run the checks manually, and investigate further. 95 | 96 | A maintainer will review your pull request, and may ask you to perform some additional design work, tests, or other changes prior 97 | to approving and merging your code. 98 | 99 | # Style guidelines 100 | 101 | In general, there are two sets of style guidelines that we ask contributors to follow. 102 | 103 | ## Git commit messages 104 | 105 | * For large changesets, provide detailed descriptions in your commit logs regarding what was changed. The git commit message should 106 | look like this: 107 | 108 | ``` 109 | $ git commit -m "A brief title / description of the commit 110 | > 111 | > A more descriptive set of paragraphs about the changeset." 112 | ``` 113 | 114 | * Limit your first line to 70 characters. 115 | * If you are just changing documentation, please include `[ci skip]` in the commit title. 116 | * Please reference issue numbers and pull requests in the commit description where applicable using `#` and the issue number (e.g. 117 | `#24`). 118 | 119 | ## Code style 120 | 121 | * Please follow the [PEP 8 Style Guidelines](https://www.python.org/dev/peps/pep-0008/) for any code that you wish to contribute. 122 | * When submitting code, we strongly suggesting running [pylint](https://www.pylint.org/) on any file that you changed to see 123 | if there are any problems introduced to the codebase. 124 | * Please ensure any docstrings describe the functionality of the functions, including what the input and output parameters 125 | do. 126 | * Please do not use docstrings on unit test functions (these get displayed by the test runner instead of the name of the function). 127 | Instead, use descriptive names for the functions (e.g. `test_cpu_raises_nmi_when_overflow_occurs`). 128 | * When importing, we prefer the following layout: 129 | * Full package imports at the top (e.g. `import os`). 130 | * Selective function imports using `from` next (e.g. `from os import chdir`). 131 | * Local imports last. 132 | -------------------------------------------------------------------------------- /FONTS.chip8: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigthomas/Chip8Python/5e414e9d5f7d35d013a002129bd01f0c5c16338b/FONTS.chip8 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2012-2019 Craig Thomas 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 15 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 16 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 17 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 18 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 19 | -------------------------------------------------------------------------------- /PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Thank you for your contribution! Before submitting this PR, please make sure: 2 | 3 | - [ ] The unit test suite runs without any errors or warnings 4 | - If the unit test suite fails, and you believe the failure is due to the test suite, please let us know in your PR description 5 | - We recommend using `nose` to run the unit test suite with the command: 6 | ``` 7 | nosetests -v --with-coverage --cover-package=chip8 8 | ``` 9 | - [ ] You have followed [PEP 8 Style Guidelines](https://www.python.org/dev/peps/pep-0008/) 10 | - [ ] You have run [pylint](https://www.pylint.org/) on your code changes to check for errors or warnings 11 | - [ ] You have added unit tests to cover the functionality you have added 12 | - We ask that at least 50% of the changeset you are submitting has been covered by tests 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Yet Another (Super) Chip 8 Emulator 2 | 3 | [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/craigthomas/Chip8Python/python-app.yml?style=flat-square&branch=main)](https://github.com/craigthomas/Chip8Python/actions) 4 | [![Codecov](https://img.shields.io/codecov/c/gh/craigthomas/Chip8Python?style=flat-square)](https://codecov.io/gh/craigthomas/Chip8Python) 5 | [![Dependencies](https://img.shields.io/librariesio/github/craigthomas/Chip8Python?style=flat-square)](https://libraries.io/github/craigthomas/Chip8Python) 6 | [![Releases](https://img.shields.io/github/release/craigthomas/Chip8Python?style=flat-square)](https://github.com/craigthomas/Chip8Python/releases) 7 | [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square)](https://opensource.org/licenses/MIT) 8 | 9 | An Octo compatible XO Chip, Super Chip, and Chip 8 emulator. 10 | 11 | ## Table of Contents 12 | 13 | 1. [What is it?](#what-is-it) 14 | 2. [License](#license) 15 | 3. [Installing](#installing) 16 | 1. [Ubuntu Installation](#ubuntu-installation) 17 | 2. [Windows Installation](#windows-installation) 18 | 4. [Running](#running) 19 | 1. [Running a ROM](#running-a-rom) 20 | 2. [Screen Scale](#screen-scale) 21 | 3. [Instructions Per Second](#instructions-per-second) 22 | 4. [Quirks Modes](#quirks-modes) 23 | 1. [Shift Quirks](#shift-quirks) 24 | 2. [Index Quirks](#index-quirks) 25 | 3. [Jump Quirks](#jump-quirks) 26 | 4. [Clip Quirks](#clip-quirks) 27 | 5. [Logic Quirks](#logic-quirks) 28 | 5. [Memory Size](#memory-size) 29 | 6. [Colors](#colors) 30 | 5. [Customization](#customization) 31 | 1. [Keys](#keys) 32 | 2. [Debug Keys](#debug-keys) 33 | 6. [ROM Compatibility](#rom-compatibility) 34 | 7. [Further Documentation](#further-documentation) 35 | 36 | ## What is it? 37 | 38 | This project is a Chip 8 emulator written in Python 3. The original purpose 39 | of the project was to create a simple learning emulator that was well 40 | documented and coded in terms that were easy to understand. It was also an 41 | exercise to learn more about Python. The result is a simple command-line 42 | based Chip 8 emulator. 43 | 44 | In addition to supporting Chip 8 ROMs, the emulator also supports the 45 | [XO Chip](https://johnearnest.github.io/Octo/docs/XO-ChipSpecification.html) 46 | and [Super Chip](https://github.com/JohnEarnest/Octo/blob/gh-pages/docs/SuperChip.md) specifications. 47 | Note that while there are no special flags that are needed to run an XO Chip, 48 | Super Chip, or normal Chip 8 ROM, there are other compatibility flags that 49 | may need to be set for the ROM to run properly. See the [Quirks Modes](#quirks-modes) 50 | documentation below for more information. 51 | 52 | There are two other versions of the emulator written in different languages: 53 | 54 | * [Chip8Java](https://github.com/craigthomas/Chip8Java) 55 | * [Chip8C](https://github.com/craigthomas/Chip8C) 56 | 57 | 58 | ## License 59 | 60 | This project makes use of an MIT style license. Please see the file called LICENSE. 61 | 62 | 63 | ## Installing 64 | 65 | Copy the source files to a directory of your choice. In addition to 66 | the source, you will need the following required software packages: 67 | 68 | * [Python 3.6.8 or better](http://www.python.org) 69 | * [pygame](http://www.pygame.org) 70 | 71 | I strongly recommend creating a virtual environment using the 72 | [virtualenv](http://pypi.python.org/pypi/virtualenv) builder as well as the 73 | [virtualenvwrapper](https://bitbucket.org/dhellmann/virtualenvwrapper) tools. 74 | With these tools, you can easily create a virtual sandbox to install pygame 75 | and run the emulator in, without touching your master Python environment. 76 | 77 | 78 | ### Ubuntu Installation 79 | 80 | The installation under Ubuntu 20.04 requires several different steps: 81 | 82 | 1. Install SDL libraries. The SDL (Simple DirectMedia Layer) libraries are used by PyGame to draw 83 | images on the screen. Several other dependencies are needed by SDL in order to install PyGame. 84 | To install the required SDL libraries (plus dependencies) from the command-line: 85 | 86 | ``` 87 | sudo apt install python3 python3-dev libsdl-dev libfreetype6-dev \ 88 | libsdl-image1.2-dev libsdl-mixer1.2-dev libsdl-ttf2.0-dev libsdl-sound1.2-dev \ 89 | libportmidi-dev 90 | ``` 91 | 92 | 2. Install PIP. The `pip` package manager is used for managing Python packages. To install `pip` 93 | from the command-line: 94 | 95 | ``` 96 | sudo apt install python3-pip 97 | ``` 98 | 99 | 3. (*Optional*) Install virtual environment support for Python: 100 | 101 | 1. Install virtual environment support: 102 | 103 | ``` 104 | pip3 install virtualenv 105 | pip3 install virtualenvwrapper 106 | ``` 107 | 108 | 2. First you must update your `.bashrc` file in the home directory and add a few lines 109 | to the bottom of that file: 110 | 111 | ``` 112 | cat >> ~/.bashrc << EOF 113 | export VIRTUALENVWRAPPER_PYTHON=/usr/bin/python3 114 | export WORKON_HOME=$HOME/.virtualenvs 115 | export PATH=$PATH:$HOME/.local/bin 116 | source $HOME/.local/bin/virtualenvwrapper.sh 117 | EOF 118 | ``` 119 | 120 | 3. Next you must source the `.bashrc` file: 121 | 122 | ``` 123 | source ~/.bashrc 124 | ``` 125 | 126 | 4. Finally, you can create the environment: 127 | 128 | ``` 129 | mkvirtualenv chip8 130 | ``` 131 | 132 | 4. Clone (or download) the Chip 8 emulator project: 133 | 134 | ``` 135 | sudo apt install git 136 | git clone https://github.com/craigthomas/Chip8Python.git 137 | ``` 138 | 139 | 5. Install the requirements from the project: 140 | 141 | ``` 142 | pip install -r requirements.txt 143 | ``` 144 | 145 | 146 | ### Windows Installation 147 | 148 | 1. Download and install [Python 3.6.8 for Windows](https://www.python.org/downloads/release/python-368/). 149 | Make sure that `pip` and `Add python.exe to Path` options are checked when performing the installation. Later 150 | versions of Python 3 are also likely to work correctly with the emulator. 151 | 152 | 2. (*Optional*) Install virtual environment support for Python. Run the following commands from a command prompt: 153 | 154 | 1. Install the virtual environment wrapper: 155 | 156 | ``` 157 | pip install virtualenv 158 | pip install virtualenvwrapper-win 159 | ``` 160 | 161 | 2. Create a new environment for the Chip 8 emulator: 162 | 163 | ``` 164 | mkvirtualenv chip8 165 | ``` 166 | 167 | 3. Install [Git for Windows](https://git-scm.com/download/win). 168 | 169 | 4. Clone (or download) the source files from GitHub. Run the following commands in a command prompt window: 170 | 171 | ``` 172 | git clone https://github.com/craigthomas/Chip8Python.git 173 | ``` 174 | 175 | 5. Install the requirements for the project. Run the following commands in a command prompt window 176 | in the directory where you cloned or downloaded the source files: 177 | 178 | ``` 179 | pip install -r requirements.txt 180 | ``` 181 | 182 | 183 | ## Running 184 | 185 | ### Running a ROM 186 | 187 | Note that if you created a virtual environment as detailed above, 188 | you will need to `workon` that environment before starting the emulator: 189 | 190 | workon chip8 191 | 192 | The command-line interface requires a single argument, which is the full 193 | path to a Chip 8 ROM. Run the following command in the directory where you 194 | cloned or downloaded the source files: 195 | 196 | python yac8e.py /path/to/rom/filename 197 | 198 | This will start the emulator with the specified ROM. 199 | 200 | ### Screen Scale 201 | 202 | The `--scale` switch will scale the size of the window (the original size at 1x 203 | scale is 64 x 32): 204 | 205 | python yac8e.py /path/to/rom/filename --scale 10 206 | 207 | The command above will scale the window so that it is 10 times the normal 208 | size. 209 | 210 | ### Instructions Per Second 211 | 212 | The `--ticks` switch will limit the number of instructions per second that the 213 | emulator is allowed to run. By default, the value is set to 1,000. Minimum values 214 | are 200. Use this switch to adjust the running time of ROMs that execute too quickly. 215 | For simplicity, each instruction is assumed to take the same amount of time. 216 | 217 | python yac8e.py /path/to/rom/filename --ticks 2000 218 | 219 | ### Quirks Modes 220 | 221 | Over time, various extensions to the Chip8 mnemonics were developed, which 222 | resulted in an interesting fragmentation of the Chip8 language specification. 223 | As discussed in Octo's [Mastering SuperChip](https://github.com/JohnEarnest/Octo/blob/gh-pages/docs/SuperChip.md) 224 | documentation, one version of the SuperChip instruction set subtly changed 225 | the meaning of a few instructions from their original Chip8 definitions. 226 | This change went mostly unnoticed for many implementations of the Chip8 227 | langauge. Problems arose when people started writing programs using the 228 | updated language model - programs written for "pure" Chip8 ceased to 229 | function correctly on emulators making use of the altered specification. 230 | 231 | To address this issue, [Octo](https://github.com/JohnEarnest/Octo) implements 232 | a number of _quirks_ modes so that all Chip8 software can run correctly, 233 | regardless of which specification was used when developing the Chip8 program. 234 | This same approach is used here, such that there are several `quirks` flags 235 | that can be passed to the emulator at startup to force it to run with 236 | adjustments to the language specification. 237 | 238 | Additional quirks and their impacts on the running Chip8 interpreter are 239 | examined in great depth at Chromatophore's [HP48-Superchip](https://github.com/Chromatophore/HP48-Superchip) 240 | repository. Many thanks for this detailed explanation of various quirks 241 | found in the wild! 242 | 243 | #### Shift Quirks 244 | 245 | The `--shift_quirks` flag will change the way that register shift operations work. 246 | In the original language specification two registers were required: the 247 | destination register `x`, and the source register `y`. The source register `y` 248 | value was shifted one bit left or right, and stored in `x`. For example, 249 | shift left was defined as: 250 | 251 | Vx = Vy << 1 252 | 253 | However, with the updated language specification, the source and destination 254 | register are assumed to always be the same, thus the `y` register is ignored and 255 | instead the value is sourced from `x` as such: 256 | 257 | Vx = Vx << 1 258 | 259 | 260 | #### Index Quirks 261 | 262 | The `--index_quirks` flag controls whether post-increments are made to the index register 263 | following various register based operaitons. For load (`Fn65`) and store (`Fn55`) register 264 | operations, the original specification for the Chip8 language results in the index 265 | register being post-incremented by the number of registers stored. With the Super 266 | Chip8 specification, this behavior is not always adhered to. Setting `--index_quirks` 267 | will prevent the post-increment of the index register from occurring after either of these 268 | instructions. 269 | 270 | 271 | #### Jump Quirks 272 | 273 | The `--jump_quirks` controls how jumps to various addresses are made with the jump (`Bnnn`) 274 | instruction. In the original Chip8 language specification, the jump is made by taking the 275 | contents of register 0, and adding it to the encoded numeric value, such as: 276 | 277 | PC = V0 + nnn 278 | 279 | With the Super Chip8 specification, the highest 4 bits of the instruction encode the 280 | register to use (`Bxnn`) such. The behavior of `--jump_quirks` becomes: 281 | 282 | PC = Vx + nn 283 | 284 | 285 | #### Clip Quirks 286 | 287 | The `--clip_quirks` controls whether sprites are allowed to wrap around the display. 288 | By default, sprits will wrap around the borders of the screen. If turned on, then 289 | sprites will not be allowed to wrap. 290 | 291 | 292 | #### Logic Quirks 293 | 294 | The `--logic_quirks` controls whether the F register is cleared after logic operations 295 | such as AND, OR, and XOR. By default, F is left undefined following these operations. 296 | With the flag turned on, F will always be cleared. 297 | 298 | 299 | ### Memory Size 300 | 301 | The original specification of the Chip8 language defined a 4K memory size for the 302 | interpreter. The addition of the XO Chip extensions require a 64K memory size 303 | for the interpreter. By default, the interpreter will start with a 64K memory size, 304 | but this behavior can be controlled with the `--mem_size` flag. Valid options are 305 | `64K` or `4K` for historical purposes. 306 | 307 | 308 | ### Colors 309 | 310 | The original Chip8 language specification called for pixels to be turned on or 311 | off. It did not specify what color the pixel states had to be. The emulator 312 | lets the user specify what colors they want to use when the emulator is running. 313 | Color values are specified by using HTML hex values such as `AABBCC` without the 314 | leading `#`. There are currently 4 color values that can be set: 315 | 316 | * `--color_0` specifies the background color. This defaults to `000000`. 317 | * `--color_1` specifies bitplane 1 color. This defaults to `FF33CC`. 318 | * `--color_2` specifies bitplane 2 color. This defaults to `33CCFF`. 319 | * `--color_3` specifies bitplane 1 and 2 overlap color. This defaults to `FFFFFF`. 320 | 321 | For Chip8 and SuperChip 8 programs, only the background color `color_0` (for pixels 322 | turned off) and the bitplane 1 color `color_1` (for pixels turned on) are used. 323 | Only XO Chip programs will use `color_2` and `color_3` when the additional bitplanes 324 | are potentially used. 325 | 326 | 327 | ## Customization 328 | 329 | The file `chip8/config.py` contains several variables that can be changed to 330 | customize the operation of the emulator. The Chip 8 has 16 keys: 331 | 332 | ### Keys 333 | 334 | The original Chip 8 had a keypad with the numbered keys 0 - 9 and A - F (16 335 | keys in total). The original key configuration was as follows: 336 | 337 | 338 | | `1` | `2` | `3` | `C` | 339 | |-----|-----|-----|-----| 340 | | `4` | `5` | `6` | `D` | 341 | | `7` | `8` | `9` | `E` | 342 | | `A` | `0` | `B` | `F` | 343 | 344 | The Chip8 emulator maps them to the following keyboard keys by default: 345 | 346 | | `1` | `2` | `3` | `4` | 347 | |-----|-----|-----|-----| 348 | | `Q` | `W` | `E` | `R` | 349 | | `A` | `S` | `D` | `F` | 350 | | `Z` | `X` | `C` | `V` | 351 | 352 | If you wish to configure a different key-mapping, simply change the `KEY_MAPPINGS` variable 353 | in the configuration file to reflect the mapping that you want. The 354 | [pygame.key](https://www.pygame.org/docs/ref/key.html) documentation contains a 355 | list of all the valid constants for keyboard key values. 356 | 357 | ### Debug Keys 358 | 359 | In addition to the key mappings specified in the configuration file, there are additional 360 | keys that impact the execution of the emulator. 361 | 362 | | Keyboard Key | Effect | 363 | | :----------: |--------------------| 364 | | `ESC` | Quits the emulator | 365 | 366 | 367 | ## ROM Compatibility 368 | 369 | Here are the list of public domain ROMs and their current status with the emulator, along 370 | with links to public domain repositories where applicable. 371 | 372 | ### Chip 8 ROMs 373 | 374 | | ROM Name | Working | Flags | 375 | |:--------------------------------------------------------------------------------------------------|:------------------:|:-------------:| 376 | | [1D Cellular Automata](https://johnearnest.github.io/chip8Archive/play.html?p=1dcell) | :heavy_check_mark: | | 377 | | [8CE Attourny - Disc 1](https://johnearnest.github.io/chip8Archive/play.html?p=8ceattourny_d1) | :heavy_check_mark: | | 378 | | [8CE Attourny - Disc 2](https://johnearnest.github.io/chip8Archive/play.html?p=8ceattourny_d2) | :heavy_check_mark: | | 379 | | [8CE Attourny - Disc 3](https://johnearnest.github.io/chip8Archive/play.html?p=8ceattourny_d3) | :heavy_check_mark: | | 380 | | [Bad Kaiju Ju](https://johnearnest.github.io/chip8Archive/play.html?p=BadKaiJuJu) | :heavy_check_mark: | | 381 | | [Br8kout](https://johnearnest.github.io/chip8Archive/play.html?p=br8kout) | :heavy_check_mark: | | 382 | | [Carbon8](https://johnearnest.github.io/chip8Archive/play.html?p=carbon8) | :heavy_check_mark: | | 383 | | [Cave Explorer](https://johnearnest.github.io/chip8Archive/play.html?p=caveexplorer) | :heavy_check_mark: | | 384 | | [Chipquarium](https://johnearnest.github.io/chip8Archive/play.html?p=chipquarium) | :heavy_check_mark: | | 385 | | [Danm8ku](https://johnearnest.github.io/chip8Archive/play.html?p=danm8ku) | :heavy_check_mark: | | 386 | | [down8](https://johnearnest.github.io/chip8Archive/play.html?p=down8) | :heavy_check_mark: | | 387 | | [Falling Ghosts](https://veganjay.itch.io/falling-ghosts) | :heavy_check_mark: | | 388 | | [Flight Runner](https://johnearnest.github.io/chip8Archive/play.html?p=flightrunner) | :heavy_check_mark: | | 389 | | [Fuse](https://johnearnest.github.io/chip8Archive/play.html?p=fuse) | :heavy_check_mark: | | 390 | | [Ghost Escape](https://johnearnest.github.io/chip8Archive/play.html?p=ghostEscape) | :heavy_check_mark: | | 391 | | [Glitch Ghost](https://johnearnest.github.io/chip8Archive/play.html?p=glitchGhost) | :heavy_check_mark: | | 392 | | [Horse World Online](https://johnearnest.github.io/chip8Archive/play.html?p=horseWorldOnline) | :heavy_check_mark: | | 393 | | [Invisible Man](https://mremerson.itch.io/invisible-man) | :heavy_check_mark: | `clip_quirks` | 394 | | [Knumber Knower](https://internet-janitor.itch.io/knumber-knower) | :heavy_check_mark: | | 395 | | [Masquer8](https://johnearnest.github.io/chip8Archive/play.html?p=masquer8) | :heavy_check_mark: | | 396 | | [Mastermind](https://johnearnest.github.io/chip8Archive/play.html?p=mastermind) | :heavy_check_mark: | | 397 | | [Mini Lights Out](https://johnearnest.github.io/chip8Archive/play.html?p=mini-lights-out) | :heavy_check_mark: | | 398 | | [Octo: a Chip 8 Story](https://johnearnest.github.io/chip8Archive/play.html?p=octoachip8story) | :heavy_check_mark: | | 399 | | [Octogon Trail](https://tarsi.itch.io/octogon-trail) | :heavy_check_mark: | | 400 | | [Octojam 1 Title](https://johnearnest.github.io/chip8Archive/play.html?p=octojam1title) | :heavy_check_mark: | | 401 | | [Octojam 2 Title](https://johnearnest.github.io/chip8Archive/play.html?p=octojam2title) | :heavy_check_mark: | | 402 | | [Octojam 3 Title](https://johnearnest.github.io/chip8Archive/play.html?p=octojam3title) | :heavy_check_mark: | | 403 | | [Octojam 4 Title](https://johnearnest.github.io/chip8Archive/play.html?p=octojam4title) | :heavy_check_mark: | | 404 | | [Octojam 5 Title](https://johnearnest.github.io/chip8Archive/play.html?p=octojam5title) | :heavy_check_mark: | | 405 | | [Octojam 6 Title](https://johnearnest.github.io/chip8Archive/play.html?p=octojam6title) | :heavy_check_mark: | | 406 | | [Octojam 7 Title](https://johnearnest.github.io/chip8Archive/play.html?p=octojam7title) | :heavy_check_mark: | | 407 | | [Octojam 8 Title](https://johnearnest.github.io/chip8Archive/play.html?p=octojam8title) | :heavy_check_mark: | | 408 | | [Octojam 9 Title](https://johnearnest.github.io/chip8Archive/play.html?p=octojam9title) | :heavy_check_mark: | | 409 | | [Octojam 10 Title](https://johnearnest.github.io/chip8Archive/play.html?p=octojam10title) | :heavy_check_mark: | | 410 | | [Octo Rancher](https://johnearnest.github.io/chip8Archive/play.html?p=octorancher) | :heavy_check_mark: | | 411 | | [Outlaw](https://johnearnest.github.io/chip8Archive/play.html?p=outlaw) | :heavy_check_mark: | | 412 | | [Pet Dog](https://johnearnest.github.io/chip8Archive/play.html?p=petdog) | :heavy_check_mark: | | 413 | | [Piper](https://johnearnest.github.io/chip8Archive/play.html?p=piper) | :heavy_check_mark: | | 414 | | [Pumpkin "Dress" Up](https://johnearnest.github.io/chip8Archive/play.html?p=pumpkindressup) | :heavy_check_mark: | | 415 | | [RPS](https://johnearnest.github.io/chip8Archive/play.html?p=RPS) | :heavy_check_mark: | | 416 | | [Slippery Slope](https://johnearnest.github.io/chip8Archive/play.html?p=slipperyslope) | :heavy_check_mark: | | 417 | | [Snek](https://johnearnest.github.io/chip8Archive/play.html?p=snek) | :heavy_check_mark: | | 418 | | [Space Jam](https://johnearnest.github.io/chip8Archive/play.html?p=spacejam) | :heavy_check_mark: | | 419 | | [Spock Paper Scissors](https://johnearnest.github.io/chip8Archive/play.html?p=spockpaperscissors) | :heavy_check_mark: | | 420 | | [Super Pong](https://johnearnest.github.io/chip8Archive/play.html?p=superpong) | :heavy_check_mark: | | 421 | | [Tank!](https://johnearnest.github.io/chip8Archive/play.html?p=tank) | :heavy_check_mark: | | 422 | | [TOMB STON TIPP](https://johnearnest.github.io/chip8Archive/play.html?p=tombstontipp) | :heavy_check_mark: | | 423 | | [WDL](https://johnearnest.github.io/chip8Archive/play.html?p=wdl) | :heavy_check_mark: | | 424 | 425 | ### Super Chip ROMs 426 | 427 | | ROM Name | Working | Flags | 428 | |:---------------------------------------------------------------------------------------------|:------------------:|:-----:| 429 | | [Applejak](https://johnearnest.github.io/chip8Archive/play.html?p=applejak) | :heavy_check_mark: | | 430 | | [Bulb](https://johnearnest.github.io/chip8Archive/play.html?p=bulb) | :heavy_check_mark: | | 431 | | [Black Rainbow](https://johnearnest.github.io/chip8Archive/play.html?p=blackrainbow) | :heavy_check_mark: | | 432 | | [Chipcross](https://tobiasvl.itch.io/chipcross) | :heavy_check_mark: | | 433 | | [Chipolarium](https://tobiasvl.itch.io/chipolarium) | :heavy_check_mark: | | 434 | | [Collision Course](https://ninjaweedle.itch.io/collision-course) | :heavy_check_mark: | | 435 | | [Dodge](https://johnearnest.github.io/chip8Archive/play.html?p=dodge) | :heavy_check_mark: | | 436 | | [DVN8](https://johnearnest.github.io/chip8Archive/play.html?p=DVN8) | :heavy_check_mark: | | 437 | | [Eaty the Alien](https://johnearnest.github.io/chip8Archive/play.html?p=eaty) | :heavy_check_mark: | | 438 | | [Grad School Simulator 2014](https://johnearnest.github.io/chip8Archive/play.html?p=gradsim) | :heavy_check_mark: | | 439 | | [Horsey Jump](https://johnearnest.github.io/chip8Archive/play.html?p=horseyJump) | :heavy_check_mark: | | 440 | | [Knight](https://johnearnest.github.io/chip8Archive/play.html?p=knight) | :x: | | 441 | | [Mondri8](https://johnearnest.github.io/chip8Archive/play.html?p=mondri8) | :heavy_check_mark: | | 442 | | [Octopeg](https://johnearnest.github.io/chip8Archive/play.html?p=octopeg) | :heavy_check_mark: | | 443 | | [Octovore](https://johnearnest.github.io/chip8Archive/play.html?p=octovore) | :heavy_check_mark: | | 444 | | [Rocto](https://johnearnest.github.io/chip8Archive/play.html?p=rockto) | :heavy_check_mark: | | 445 | | [Sens8tion](https://johnearnest.github.io/chip8Archive/play.html?p=sens8tion) | :heavy_check_mark: | | 446 | | [Snake](https://johnearnest.github.io/chip8Archive/play.html?p=snake) | :heavy_check_mark: | | 447 | | [Squad](https://johnearnest.github.io/chip8Archive/play.html?p=squad) | :heavy_check_mark: | | 448 | | [Sub-Terr8nia](https://johnearnest.github.io/chip8Archive/play.html?p=sub8) | :heavy_check_mark: | | 449 | | [Super Octogon](https://johnearnest.github.io/chip8Archive/play.html?p=octogon) | :heavy_check_mark: | | 450 | | [Super Square](https://johnearnest.github.io/chip8Archive/play.html?p=supersquare) | :heavy_check_mark: | | 451 | | [The Binding of COSMAC](https://johnearnest.github.io/chip8Archive/play.html?p=binding) | :heavy_check_mark: | | 452 | | [Turnover '77](https://johnearnest.github.io/chip8Archive/play.html?p=turnover77) | :heavy_check_mark: | | 453 | 454 | ### XO Chip ROMs 455 | 456 | | ROM Name | Working | Flags | 457 | |:------------------------------------------------------------------------------------------------------|:------------------:|:-----:| 458 | | [An Evening to Die For](https://johnearnest.github.io/chip8Archive/play.html?p=anEveningToDieFor) | :heavy_check_mark: | | 459 | | [Business Is Contagious](https://johnearnest.github.io/chip8Archive/play.html?p=businessiscontagious) | :heavy_check_mark: | | 460 | | [Chicken Scratch](https://johnearnest.github.io/chip8Archive/play.html?p=chickenScratch) | :heavy_check_mark: | | 461 | | [Civiliz8n](https://johnearnest.github.io/chip8Archive/play.html?p=civiliz8n) | :heavy_check_mark: | | 462 | | [Flutter By](https://johnearnest.github.io/chip8Archive/play.html?p=flutterby) | :heavy_check_mark: | | 463 | | [Into The Garlicscape](https://johnearnest.github.io/chip8Archive/play.html?p=garlicscape) | :heavy_check_mark: | | 464 | | [jub8 Song 1](https://johnearnest.github.io/chip8Archive/play.html?p=jub8-1) | :heavy_check_mark: | | 465 | | [jub8 Song 2](https://johnearnest.github.io/chip8Archive/play.html?p=jub8-2) | :heavy_check_mark: | | 466 | | [Kesha Was Biird](https://johnearnest.github.io/chip8Archive/play.html?p=keshaWasBiird) | :heavy_check_mark: | | 467 | | [Kesha Was Niinja](https://johnearnest.github.io/chip8Archive/play.html?p=keshaWasNiinja) | :heavy_check_mark: | | 468 | | [Octo paint](https://johnearnest.github.io/chip8Archive/play.html?p=octopaint) | :heavy_check_mark: | | 469 | | [Octo Party Mix!](https://johnearnest.github.io/chip8Archive/play.html?p=OctoPartyMix) | :heavy_check_mark: | | 470 | | [Octoma](https://johnearnest.github.io/chip8Archive/play.html?p=octoma) | :heavy_check_mark: | | 471 | | [Red October V](https://johnearnest.github.io/chip8Archive/play.html?p=redOctober) | :heavy_check_mark: | | 472 | | [Skyward](https://johnearnest.github.io/chip8Archive/play.html?p=skyward) | :heavy_check_mark: | | 473 | | [Spock Paper Scissors](https://johnearnest.github.io/chip8Archive/play.html?p=spockpaperscissors) | :heavy_check_mark: | | 474 | | [T8NKS](https://johnearnest.github.io/chip8Archive/play.html?p=t8nks) | :heavy_check_mark: | | 475 | | [Tapeworm](https://tarsi.itch.io/tapeworm) | :heavy_check_mark: | | 476 | | [Truck Simul8or](https://johnearnest.github.io/chip8Archive/play.html?p=trucksimul8or) | :heavy_check_mark: | | 477 | | [SK8 H8 1988](https://johnearnest.github.io/chip8Archive/play.html?p=sk8) | :heavy_check_mark: | | 478 | | [Super NeatBoy](https://johnearnest.github.io/chip8Archive/play.html?p=superneatboy) | :heavy_check_mark: | | 479 | | [Wonky Pong](https://johnearnest.github.io/chip8Archive/play.html?p=wonkypong) | :heavy_check_mark: | | 480 | 481 | 482 | ## Further Documentation 483 | 484 | The best documentation is in the code itself. Please feel free to examine the 485 | code and experiment with it. 486 | -------------------------------------------------------------------------------- /chip8/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigthomas/Chip8Python/5e414e9d5f7d35d013a002129bd01f0c5c16338b/chip8/__init__.py -------------------------------------------------------------------------------- /chip8/config.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2024 Craig Thomas 3 | This project uses an MIT style license - see LICENSE for details. 4 | 5 | A simple Chip 8 emulator - see the README file for more information. 6 | """ 7 | # I M P O R T S ############################################################### 8 | 9 | import pygame 10 | 11 | # C U S T O M I Z A T I O N V A R I A B L E S############################### 12 | 13 | # Where the stack pointer should originally point 14 | STACK_POINTER_START = 0x52 15 | 16 | # Where the program counter should originally point 17 | PROGRAM_COUNTER_START = 0x200 18 | 19 | # Sets which keys on the keyboard map to the Chip 8 keys 20 | KEY_MAPPINGS = { 21 | 0x0: pygame.K_x, 22 | 0x1: pygame.K_1, 23 | 0x2: pygame.K_2, 24 | 0x3: pygame.K_3, 25 | 0x4: pygame.K_q, 26 | 0x5: pygame.K_w, 27 | 0x6: pygame.K_e, 28 | 0x7: pygame.K_a, 29 | 0x8: pygame.K_s, 30 | 0x9: pygame.K_d, 31 | 0xA: pygame.K_z, 32 | 0xB: pygame.K_c, 33 | 0xC: pygame.K_4, 34 | 0xD: pygame.K_r, 35 | 0xE: pygame.K_f, 36 | 0xF: pygame.K_v, 37 | } 38 | 39 | # The font file to use 40 | FONT_FILE = "FONTS.chip8" 41 | 42 | # Delay timer decrement interval (in ms) 43 | DELAY_INTERVAL = 17 44 | 45 | # E N D O F F I L E ####################################################### 46 | -------------------------------------------------------------------------------- /chip8/cpu.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2024 Craig Thomas 3 | This project uses an MIT style license - see LICENSE for details. 4 | 5 | A Chip 8 CPU - see the README file for more information. 6 | """ 7 | # I M P O R T S ############################################################### 8 | 9 | import numpy as np 10 | 11 | from pygame import key 12 | from pygame import mixer 13 | from pygame.mixer import Sound 14 | from random import randint 15 | 16 | from chip8.config import STACK_POINTER_START, KEY_MAPPINGS, PROGRAM_COUNTER_START 17 | 18 | # C O N S T A N T S ########################################################### 19 | 20 | # The total number of registers in the Chip 8 CPU 21 | NUM_REGISTERS = 0x10 22 | 23 | # The various modes of operation 24 | MODE_NORMAL = 'normal' 25 | MODE_EXTENDED = 'extended' 26 | 27 | # Memory sizes available 28 | MEM_SIZE = { 29 | "4K": 4096, 30 | "64K": 65536, 31 | } 32 | 33 | # The minimum number of audio samples we want to generate. The minimum amount 34 | # of time an audio clip can be played is 1/60th of a second (the frequency 35 | # that the sound timer is decremented). Since we initialize the pygame 36 | # audio mixer to require 48000 samples per second, this means each 1/60th 37 | # of a second requires 800 samples. The audio pattern buffer is only 38 | # 128 bits long, so we will need to repeat it to fill at least 1/60th of a 39 | # second with audio (resampled at the correct frequency). To be safe, 40 | # we'll construct a buffer of at least 4/60ths of a second of 41 | # audio. We can be bigger than the minimum number of samples below, but 42 | # we don't want less than that. 43 | MIN_AUDIO_SAMPLES = 3200 44 | 45 | # The audio playback rate to use for Pygame mixer initialization 46 | PYGAME_AUDIO_PLAYBACK_RATE = 48000 47 | 48 | # C L A S S E S ############################################################### 49 | 50 | 51 | class UnknownOpCodeException(Exception): 52 | """ 53 | A class to raise unknown op code exceptions. 54 | """ 55 | def __init__(self, op_code): 56 | Exception.__init__(self, f"Unknown op-code: {op_code:04X}") 57 | 58 | 59 | class Chip8CPU: 60 | """ 61 | A class to emulate a Chip 8 CPU. There are several good resources out on 62 | the web that describe the internals of the Chip 8 CPU. For example: 63 | 64 | http://devernay.free.fr/hacks/chip8/C8TECH10.HTM 65 | http://michael.toren.net/mirrors/chip8/chip8def.htm 66 | 67 | As usual, a simple Google search will find you other excellent examples. 68 | To summarize these sources, the Chip 8 has: 69 | 70 | * 16 x 8-bit general purpose registers (V0 - VF**) 71 | * 1 x 16-bit index register (I) 72 | * 1 x 16-bit stack pointer (SP) 73 | * 1 x 16-bit program counter (PC) 74 | * 1 x 8-bit delay timer (DT) 75 | * 1 x 8-bit sound timer (ST) 76 | 77 | ** VF is a special register - it is used to store the overflow bit 78 | """ 79 | def __init__( 80 | self, 81 | screen, 82 | shift_quirks=False, 83 | index_quirks=False, 84 | jump_quirks=False, 85 | clip_quirks=False, 86 | logic_quirks=False, 87 | mem_size="64K", 88 | max_ticks=1000, 89 | ): 90 | """ 91 | Initialize the Chip8 CPU. The only required parameter is a screen 92 | object that supports the draw_pixel function. For testing purposes, 93 | this can be set to None. 94 | 95 | :param screen: the screen object to draw pixels on 96 | :param shift_quirks: enables bit-shift quirks mode 97 | :param index_quirks: enables index quirks on load/store commands 98 | :param jump_quirks: enables jump quirks on Bxxx commands 99 | :param clip_quirks: enables screen clipping quirks 100 | :param logic_quirks: enables logic quirks 101 | :param mem_size: sets the maximum memory available "4K" or "64K" 102 | :param max_ticks: sets the maximum allowable operations per second 103 | """ 104 | self.last_pc = 0x0000 105 | self.last_op = "None" 106 | self.sound = 0 107 | self.delay = 0 108 | self.v = [0] * NUM_REGISTERS 109 | self.pc = PROGRAM_COUNTER_START 110 | self.sp = STACK_POINTER_START 111 | self.index = 0 112 | self.rpl = [0] * NUM_REGISTERS 113 | 114 | self.tick_counter = 0 115 | self.max_ticks = max_ticks 116 | if self.max_ticks < 200: 117 | self.max_ticks = 200 118 | 119 | self.pitch = 64 120 | self.playback_rate = 4000 121 | self.audio_pattern_buffer = [0] * 16 122 | self.sound_playing = False 123 | self.sound_waveform = None 124 | 125 | self.bitplane = 1 126 | 127 | self.shift_quirks = shift_quirks 128 | self.index_quirks = index_quirks 129 | self.jump_quirks = jump_quirks 130 | self.clip_quirks = clip_quirks 131 | self.logic_quirks = logic_quirks 132 | 133 | self.awaiting_keypress = False 134 | self.keypress_register = None 135 | 136 | # The operation_lookup table is executed according to the most 137 | # significant byte of the operand (e.g. operand 8nnn would call 138 | # self.execute_logical_instruction) 139 | self.operation_lookup = { 140 | 0x0: self.clear_return, # 0nnn - SYS nnn 141 | 0x1: self.jump_to_address, # 1nnn - JUMP nnn 142 | 0x2: self.jump_to_subroutine, # 2nnn - CALL nnn 143 | 0x3: self.skip_if_reg_equal_val, # 3snn - SKE Vs, nn 144 | 0x4: self.skip_if_reg_not_equal_val, # 4snn - SKNE Vs, nn 145 | 0x5: self.save_skip_routines, # see subfunctions below 146 | 0x6: self.move_value_to_reg, # 6snn - LOAD Vs, nn 147 | 0x7: self.add_value_to_reg, # 7snn - ADD Vs, nn 148 | 0x8: self.execute_logical_instruction, # see subfunctions below 149 | 0x9: self.skip_if_reg_not_equal_reg, # 9st0 - SKNE Vs, Vt 150 | 0xA: self.load_index_reg_with_value, # Annn - LOAD I, nnn 151 | 0xB: self.jump_to_register_plus_value, # Bnnn - JUMP [V0 + nnn] | [Vx + nnn] 152 | 0xC: self.generate_random_number, # Ctnn - RAND Vt, nn 153 | 0xD: self.draw_sprite, # Dstn - DRAW Vs, Vy, n 154 | 0xE: self.keyboard_routines, # see subfunctions below 155 | 0xF: self.misc_routines, # see subfunctions below 156 | } 157 | 158 | self.save_skip_lookup = { 159 | 0x0: self.skip_if_reg_equal_reg, # 5xy0 - SKE Vx, Vy 160 | 0x2: self.store_subset_regs_in_memory, # 5xy2 - STORSUB x, y 161 | 0x3: self.read_subset_regs_in_memory, # 5xy3 - LOADSUB x, y 162 | } 163 | 164 | self.clear_routines = { 165 | 0xE0: self.clear_screen, # 00E0 - CLS 166 | 0xEE: self.return_from_subroutine, # 00EE - RTS 167 | 0xFB: self.scroll_right, # 00FB - SCRR 168 | 0xFC: self.scroll_left, # 00FC - SCRL 169 | 0xFD: self.exit, # 00FD - EXIT 170 | 0xFE: self.disable_extended_mode, # 00FE - SET NORMAL 171 | 0xFF: self.enable_extended_mode, # 00FF - SET EXTENDED 172 | } 173 | 174 | # This set of operations is invoked when the operand loaded into the 175 | # CPU starts with 8 (e.g. operand 8nn0 would call 176 | # self.move_reg_into_reg) 177 | self.logical_operation_lookup = { 178 | 0x0: self.move_reg_into_reg, # 8st0 - LOAD Vs, Vt 179 | 0x1: self.logical_or, # 8st1 - OR Vs, Vt 180 | 0x2: self.logical_and, # 8st2 - AND Vs, Vt 181 | 0x3: self.exclusive_or, # 8st3 - XOR Vs, Vt 182 | 0x4: self.add_reg_to_reg, # 8st4 - ADD Vs, Vt 183 | 0x5: self.subtract_reg_from_reg, # 8st5 - SUB Vs, Vt 184 | 0x6: self.right_shift_reg, # 8st6 - SHR Vs 185 | 0x7: self.subtract_reg_from_reg1, # 8st7 - SUBN Vs, Vt 186 | 0xE: self.left_shift_reg, # 8stE - SHL Vs 187 | } 188 | 189 | # This set of operations is invoked when the operand loaded into the 190 | # CPU starts with F (e.g. operand Fn07 would call 191 | # self.move_delay_timer_into_reg) 192 | self.misc_routine_lookup = { 193 | 0x00: self.index_load_long, # F000 - LOADLONG 194 | 0x01: self.set_bitplane, # Fn01 - BITPLANE n 195 | 0x02: self.load_audio_pattern_buffer, # F002 - AUDIO 196 | 0x07: self.move_delay_timer_into_reg, # Ft07 - LOAD Vt, DELAY 197 | 0x0A: self.wait_for_keypress, # Ft0A - KEYD Vt 198 | 0x15: self.move_reg_into_delay_timer, # Fs15 - LOAD DELAY, Vs 199 | 0x18: self.move_reg_into_sound_timer, # Fs18 - LOAD SOUND, Vs 200 | 0x1E: self.add_reg_into_index, # Fs1E - ADD I, Vs 201 | 0x29: self.load_index_with_reg_sprite, # Fs29 - LOAD I, Vs 202 | 0x30: self.load_index_with_extended_reg_sprite, # Fs30 - LOAD I, Vs 203 | 0x33: self.store_bcd_in_memory, # Fs33 - BCD 204 | 0x3A: self.load_pitch, # Fx3A - PITCH Vx 205 | 0x55: self.store_regs_in_memory, # Fs55 - STOR [I], Vs 206 | 0x65: self.read_regs_from_memory, # Fs65 - LOAD Vs, [I] 207 | 0x75: self.store_regs_in_rpl, # Fs75 - SRPL Vs 208 | 0x85: self.read_regs_from_rpl, # Fs85 - LRPL Vs 209 | } 210 | self.operand = 0 211 | self.mode = MODE_NORMAL 212 | self.screen = screen 213 | self.memory = bytearray(MEM_SIZE[mem_size]) 214 | self.reset() 215 | self.running = True 216 | mixer.init(frequency=PYGAME_AUDIO_PLAYBACK_RATE, size=8, channels=1) 217 | 218 | def __str__(self): 219 | val = f"PC:{self.last_pc:04X} OP:{self.operand:04X} " 220 | for index in range(0x10): 221 | val += f"V{index:X}:{self.v[index]:02X} " 222 | val += f"I:{self.index:04X} DELAY:{self.delay} SOUND:{self.sound} " 223 | val += f"{self.last_op}" 224 | return val 225 | 226 | def execute_instruction(self, operand=None): 227 | """ 228 | Execute the next instruction pointed to by the program counter. 229 | For testing purposes, pass the operand directly to the 230 | function. When the operand is not passed directly to the 231 | function, the program counter is increased by 2. 232 | 233 | :param operand: the operand to execute 234 | :return: returns the operand executed 235 | """ 236 | if self.tick_counter > self.max_ticks: 237 | return None 238 | 239 | self.last_pc = self.pc 240 | if operand: 241 | self.operand = operand 242 | else: 243 | self.operand = int(self.memory[self.pc]) 244 | self.operand = self.operand << 8 245 | self.operand += int(self.memory[self.pc + 1]) 246 | self.pc += 2 247 | operation = (self.operand & 0xF000) >> 12 248 | self.operation_lookup[operation]() 249 | self.tick_counter += 1 250 | return self.operand 251 | 252 | def execute_logical_instruction(self): 253 | """ 254 | Execute the logical instruction based upon the current operand. 255 | For testing purposes, pass the operand directly to the function. 256 | """ 257 | operation = self.operand & 0x000F 258 | try: 259 | self.logical_operation_lookup[operation]() 260 | except KeyError: 261 | raise UnknownOpCodeException(self.operand) 262 | 263 | def keyboard_routines(self): 264 | """ 265 | Run the specified keyboard routine based upon the operand. These 266 | operations are: 267 | 268 | Ex9E - SKPR Vx 269 | ExA1 - SKUP Vx 270 | 271 | 0x9E will check to see if the key specified in the x register is 272 | pressed, and if it is, skips the next instruction. Operation 0xA1 will 273 | again check for the specified keypress in the x register, and 274 | if it is NOT pressed, will skip the next instruction. The register 275 | calculations are as follows: 276 | 277 | Bits: 15-12 11-8 7-4 3-0 278 | E x 9 or A E or 1 279 | """ 280 | operation = self.operand & 0x00FF 281 | x = (self.operand & 0x0F00) >> 8 282 | 283 | key_to_check = self.v[x] 284 | keys_pressed = key.get_pressed() 285 | 286 | # Skip if the key specified in the source register is pressed 287 | if operation == 0x9E: 288 | if key_to_check <= 0xF and keys_pressed[KEY_MAPPINGS[key_to_check]]: 289 | self.pc += 2 290 | if self.memory[self.pc - 2] == 0xF0 and self.memory[self.pc - 1] == 0x00: 291 | self.pc += 2 292 | 293 | # Skip if the key specified in the source register is not pressed 294 | if operation == 0xA1: 295 | if key_to_check <= 0xF and not keys_pressed[KEY_MAPPINGS[key_to_check]]: 296 | self.pc += 2 297 | if self.memory[self.pc - 2] == 0xF0 and self.memory[self.pc - 1] == 0x00: 298 | self.pc += 2 299 | 300 | def save_skip_routines(self): 301 | """ 302 | Will execute either a register save or skip routine. 303 | """ 304 | operation = self.operand & 0x000F 305 | try: 306 | self.save_skip_lookup[operation]() 307 | except KeyError: 308 | raise UnknownOpCodeException(self.operand) 309 | 310 | def misc_routines(self): 311 | """ 312 | Will execute one of the routines specified in misc_routines. 313 | """ 314 | operation = self.operand & 0x00FF 315 | try: 316 | self.misc_routine_lookup[operation]() 317 | except KeyError: 318 | raise UnknownOpCodeException(self.operand) 319 | 320 | def clear_return(self): 321 | """ 322 | Opcodes starting with a 0 usually correspond to screen clearing or scrolling 323 | routines, or emulator exit routines. 324 | """ 325 | operation = self.operand & 0x00FF 326 | sub_operation = operation & 0x00F0 327 | if sub_operation == 0x00C0: 328 | num_lines = self.operand & 0x000F 329 | self.screen.scroll_down(num_lines, self.bitplane) 330 | self.last_op = f"Scroll Down {num_lines:01X}" 331 | elif sub_operation == 0x00D0: 332 | num_lines = self.operand & 0x000F 333 | self.screen.scroll_up(num_lines, self.bitplane) 334 | self.last_op = f"Scroll Up {num_lines:01X}" 335 | else: 336 | try: 337 | self.clear_routines[operation]() 338 | except KeyError: 339 | raise UnknownOpCodeException(self.operand) 340 | 341 | def clear_screen(self): 342 | """ 343 | 00E0 - CLS 344 | 345 | Clears the screen 346 | """ 347 | self.screen.clear_screen(self.bitplane) 348 | self.last_op = "CLS" 349 | 350 | def return_from_subroutine(self): 351 | """ 352 | 00EE - RTS 353 | 354 | Return from subroutine. Pop the current value in the stack pointer 355 | off of the stack, and set the program counter to the value popped. 356 | """ 357 | self.sp -= 1 358 | self.pc = self.memory[self.sp] << 8 359 | self.sp -= 1 360 | self.pc += self.memory[self.sp] 361 | self.last_op = "RTS" 362 | 363 | def scroll_right(self): 364 | """ 365 | 00FB - SCRR 366 | 367 | Scrolls the screen right by 4 pixels. 368 | """ 369 | self.screen.scroll_right(self.bitplane) 370 | self.last_op = "Scroll Right" 371 | 372 | def scroll_left(self): 373 | """ 374 | 00FC - SCRL 375 | 376 | Scrolls the screen left by 4 pixels. 377 | """ 378 | self.screen.scroll_left(self.bitplane) 379 | self.last_op = "Scroll Left" 380 | 381 | def exit(self): 382 | """ 383 | 00FD - EXIT 384 | 385 | Exits the emulator. 386 | """ 387 | self.running = False 388 | self.last_op = "EXIT" 389 | 390 | def disable_extended_mode(self): 391 | """ 392 | 00FE - SET NORMAL 393 | 394 | Disables extended mode. 395 | """ 396 | self.screen.set_normal() 397 | self.mode = MODE_NORMAL 398 | self.last_op = "Set Normal Mode" 399 | 400 | def enable_extended_mode(self): 401 | """ 402 | 00FF - SET EXTENDED 403 | 404 | Set extended mode. 405 | """ 406 | self.screen.set_extended() 407 | self.mode = MODE_EXTENDED 408 | self.last_op = "Set Extended Mode" 409 | 410 | def jump_to_address(self): 411 | """ 412 | 1nnn - JUMP nnn 413 | 414 | Jump to address. The address to jump to is calculated using the bits 415 | taken from the operand as follows: 416 | 417 | Bits: 15-12 11-8 7-4 3-0 418 | 1 n n n 419 | """ 420 | self.pc = self.operand & 0x0FFF 421 | self.last_op = f"JUMP {self.pc:04X}" 422 | 423 | def jump_to_subroutine(self): 424 | """ 425 | 2nnn - CALL nnn 426 | 427 | Jump to subroutine. Save the current program counter on the stack. The 428 | subroutine to jump to is taken from the operand as follows: 429 | 430 | Bits: 15-12 11-8 7-4 3-0 431 | 2 n n n 432 | """ 433 | self.memory[self.sp] = self.pc & 0x00FF 434 | self.sp += 1 435 | self.memory[self.sp] = (self.pc & 0xFF00) >> 8 436 | self.sp += 1 437 | self.pc = self.operand & 0x0FFF 438 | self.last_op = f"CALL {self.pc:04X}" 439 | 440 | def skip_if_reg_equal_val(self): 441 | """ 442 | 3xnn - SKE Vx, nn 443 | 444 | Skip if register contents equal to constant value. The calculation for 445 | the register and constant is performed on the operand: 446 | 447 | Bits: 15-12 11-8 7-4 3-0 448 | 3 x n n 449 | 450 | The program counter is updated to skip the next instruction by 451 | advancing it by 2 bytes. 452 | """ 453 | x = (self.operand & 0x0F00) >> 8 454 | if self.v[x] == (self.operand & 0x00FF): 455 | self.pc += 2 456 | if self.memory[self.pc - 2] == 0xF0 and self.memory[self.pc - 1] == 0x00: 457 | self.pc += 2 458 | self.last_op = f"SKE V{x:01X}, {self.operand & 0x00FF:02X}" 459 | 460 | def skip_if_reg_not_equal_val(self): 461 | """ 462 | 4xnn - SKNE Vx, nn 463 | 464 | Skip if register contents not equal to constant value. The calculation 465 | for the register and constant is performed on the operand: 466 | 467 | Bits: 15-12 11-8 7-4 3-0 468 | 4 x n n 469 | 470 | The program counter is updated to skip the next instruction by 471 | advancing it by 2 bytes. 472 | """ 473 | x = (self.operand & 0x0F00) >> 8 474 | self.last_op = f"SKNE V{x:X}, {self.operand & 0x00FF:02X} (comparing {self.v[x]:02X} to {self.operand & 0xFF:02X})" 475 | if self.v[x] != (self.operand & 0x00FF): 476 | self.pc += 2 477 | if self.memory[self.pc - 2] == 0xF0 and self.memory[self.pc - 1] == 0x00: 478 | self.pc += 2 479 | 480 | def skip_if_reg_equal_reg(self): 481 | """ 482 | 5xy0 - SKE Vx, Vy 483 | 484 | Skip if x register is equal to y register. The calculation 485 | for the registers to use is performed on the operand: 486 | 487 | Bits: 15-12 11-8 7-4 3-0 488 | 5 x y 0 489 | 490 | The program counter is updated to skip the next instruction by 491 | advancing it by 2 bytes. 492 | """ 493 | x = (self.operand & 0x0F00) >> 8 494 | y = (self.operand & 0x00F0) >> 4 495 | if self.v[x] == self.v[y]: 496 | self.pc += 2 497 | if self.memory[self.pc - 2] == 0xF0 and self.memory[self.pc - 1] == 0x00: 498 | self.pc += 2 499 | self.last_op = f"SKE V{x:01X}, V{y:01X}" 500 | 501 | def store_subset_regs_in_memory(self): 502 | """ 503 | 5xy2 - STORSUB [I], Vx, Vy 504 | 505 | Store a subset of registers from x to y in memory starting at index. 506 | The x and y calculation is as follows: 507 | 508 | Bits: 15-12 11-8 7-4 3-0 509 | F x y 2 510 | 511 | If x is larger than y, then they will be stored in reverse order. 512 | """ 513 | x = (self.operand & 0x0F00) >> 8 514 | y = (self.operand & 0x00F0) >> 4 515 | pointer = 0 516 | if y >= x: 517 | for z in range(x, y+1): 518 | self.memory[self.index + pointer] = self.v[z] 519 | pointer += 1 520 | else: 521 | for z in range(x, y-1, -1): 522 | self.memory[self.index + pointer] = self.v[z] 523 | pointer += 1 524 | 525 | self.last_op = f"STORSUB [I], {x:01X}, {y:01X}" 526 | 527 | def read_subset_regs_in_memory(self): 528 | """ 529 | 5xy3 - LOADSUB [I], Vx, Vy 530 | 531 | Load a subset of registers from x to y in memory starting at index. 532 | The x and y calculation is as follows: 533 | 534 | Bits: 15-12 11-8 7-4 3-0 535 | F x y 2 536 | 537 | If x is larger than y, then they will be loaded in reverse order. 538 | """ 539 | x = (self.operand & 0x0F00) >> 8 540 | y = (self.operand & 0x00F0) >> 4 541 | pointer = 0 542 | if y >= x: 543 | for z in range(x, y+1): 544 | self.v[z] = self.memory[self.index + pointer] 545 | pointer += 1 546 | else: 547 | for z in range(x, y-1, -1): 548 | self.v[z] = self.memory[self.index + pointer] 549 | pointer += 1 550 | 551 | self.last_op = f"LOADSUB [I], {x:01X}, {y:01X}" 552 | 553 | def move_value_to_reg(self): 554 | """ 555 | 6xnn - LOAD Vx, nn 556 | 557 | Move the constant value into the specified register. The calculation 558 | for the registers is performed on the operand: 559 | 560 | Bits: 15-12 11-8 7-4 3-0 561 | 6 x n n 562 | """ 563 | x = (self.operand & 0x0F00) >> 8 564 | self.v[x] = self.operand & 0x00FF 565 | self.last_op = f"LOAD V{x:X}, {self.operand & 0x00FF:02X}" 566 | 567 | def add_value_to_reg(self): 568 | """ 569 | 7xnn - ADD Vx, nn 570 | 571 | Add the constant value to the specified register. The calculation 572 | for the registers is performed on the operand: 573 | 574 | Bits: 15-12 11-8 7-4 3-0 575 | 7 x n n 576 | """ 577 | x = (self.operand & 0x0F00) >> 8 578 | self.v[x] = (self.v[x] + (self.operand & 0x00FF)) % 256 579 | self.last_op = f"ADD V{x:01X}, {self.operand & 0x00FF:02X}" 580 | 581 | def move_reg_into_reg(self): 582 | """ 583 | 8xy0 - LOAD Vx, Vy 584 | 585 | Move the value of the x register into the value of the y 586 | register. The calculation for the registers is performed on the 587 | operand: 588 | 589 | Bits: 15-12 11-8 7-4 3-0 590 | 8 x y 0 591 | """ 592 | x = (self.operand & 0x0F00) >> 8 593 | y = (self.operand & 0x00F0) >> 4 594 | self.v[x] = self.v[y] 595 | self.last_op = f"LOAD V{x:01X}, V{y:01X}" 596 | 597 | def logical_or(self): 598 | """ 599 | 8xy1 - OR Vx, Vy 600 | 601 | Perform a logical OR operation between the x and the y 602 | register, and store the result in the x register. The register 603 | calculations are as follows: 604 | 605 | Bits: 15-12 11-8 7-4 3-0 606 | 8 x y 1 607 | """ 608 | x = (self.operand & 0x0F00) >> 8 609 | y = (self.operand & 0x00F0) >> 4 610 | self.v[x] |= self.v[y] 611 | if self.logic_quirks: 612 | self.v[0xF] = 0 613 | self.last_op = f"OR V{x:01X}, V{y:01X}" 614 | 615 | def logical_and(self): 616 | """ 617 | 8xy2 - AND Vx, Vy 618 | 619 | Perform a logical AND operation between the x and the y 620 | register, and store the result in the x register. The register 621 | calculations are as follows: 622 | 623 | Bits: 15-12 11-8 7-4 3-0 624 | 8 x y 2 625 | """ 626 | x = (self.operand & 0x0F00) >> 8 627 | y = (self.operand & 0x00F0) >> 4 628 | self.v[x] &= self.v[y] 629 | if self.logic_quirks: 630 | self.v[0xF] = 0 631 | self.last_op = f"AND V{x:01X}, V{y:01X}" 632 | 633 | def exclusive_or(self): 634 | """ 635 | 8xy3 - XOR Vx, Vy 636 | 637 | Perform a logical XOR operation between the x and the y 638 | register, and store the result in the x register. The register 639 | calculations are as follows: 640 | 641 | Bits: 15-12 11-8 7-4 3-0 642 | 8 x y 3 643 | """ 644 | x = (self.operand & 0x0F00) >> 8 645 | y = (self.operand & 0x00F0) >> 4 646 | self.v[x] ^= self.v[y] 647 | if self.logic_quirks: 648 | self.v[0xF] = 0 649 | self.last_op = f"XOR V{x:01X}, V{y:01X}" 650 | 651 | def add_reg_to_reg(self): 652 | """ 653 | 8xy4 - ADD Vx, Vy 654 | 655 | Add the value in the x register to the value in the y 656 | register, and store the result in the x register. The register 657 | calculations are as follows: 658 | 659 | Bits: 15-12 11-8 7-4 3-0 660 | 8 x y 4 661 | 662 | If a carry is generated, set a carry flag in register VF. 663 | """ 664 | x = (self.operand & 0x0F00) >> 8 665 | y = (self.operand & 0x00F0) >> 4 666 | carry = 1 if self.v[x] + self.v[y] > 255 else 0 667 | self.v[x] = (self.v[x] + self.v[y]) % 256 668 | self.v[0xF] = carry 669 | self.last_op = f"ADD V{x:01X}, V{y:01X}" 670 | 671 | def subtract_reg_from_reg(self): 672 | """ 673 | 8xy5 - SUB Vx, Vy 674 | 675 | Subtract the value in the target register from the value in the source 676 | register, and store the result in the target register. The register 677 | calculations are as follows: 678 | 679 | Bits: 15-12 11-8 7-4 3-0 680 | 8 x y 5 681 | 682 | If a borrow is generated, set a carry flag in register VF. 683 | """ 684 | x = (self.operand & 0x0F00) >> 8 685 | y = (self.operand & 0x00F0) >> 4 686 | borrow = 1 if self.v[x] >= self.v[y] else 0 687 | self.v[x] = self.v[x] - self.v[y] if self.v[x] >= self.v[y] else 256 + self.v[x] - self.v[y] 688 | self.v[0xF] = borrow 689 | self.last_op = f"SUB V{x:01X}, V{y:01X}" 690 | 691 | def right_shift_reg(self): 692 | """ 693 | 8xy6 - SHR Vx, (Vy) 694 | 695 | Shift the bits in the y register 1 bit to the right and stores the result 696 | in the x register. Bit 0 will be shifted into register 697 | vf. The register calculation is as follows: 698 | 699 | Bits: 15-12 11-8 7-4 3-0 700 | 8 x y 6 701 | 702 | If shift_quirks mode is enabled, then register x will be bit shifted and 703 | stored in x. 704 | """ 705 | x = (self.operand & 0x0F00) >> 8 706 | y = (self.operand & 0x00F0) >> 4 707 | if self.shift_quirks: 708 | bit_one = self.v[x] & 0x1 709 | self.v[x] = self.v[x] >> 1 710 | self.v[0xF] = bit_one 711 | self.last_op = f"SHR V{x:01X}" 712 | else: 713 | bit_one = self.v[y] & 0x1 714 | self.v[x] = self.v[y] >> 1 715 | self.v[0xF] = bit_one 716 | self.last_op = f"SHR V{x:01X}, V{y:01X}" 717 | 718 | def subtract_reg_from_reg1(self): 719 | """ 720 | 8xy7 - SUBN Vx, Vy 721 | 722 | Subtract the value in the x register from the value in the y 723 | register, and store the result in the x register. The register 724 | calculations are as follows: 725 | 726 | Bits: 15-12 11-8 7-4 3-0 727 | 8 x y 7 728 | 729 | If a borrow is NOT generated, set a carry flag in register VF. 730 | """ 731 | x = (self.operand & 0x0F00) >> 8 732 | y = (self.operand & 0x00F0) >> 4 733 | not_borrow = 1 if self.v[y] >= self.v[x] else 0 734 | self.last_op = f"SUBN V{x:01X} ({self.v[x]:02X}), V{y:01X} ({self.v[y]:02X})" 735 | self.v[x] = self.v[y] - self.v[x] if self.v[y] >= self.v[x] else 256 + self.v[y] - self.v[x] 736 | self.v[0xF] = not_borrow 737 | 738 | def left_shift_reg(self): 739 | """ 740 | 8xyE - SHL Vx, (Vy) 741 | 742 | Shift the bits in the source register 1 bit to the left and 743 | stores the result in the source register. Bit 7 will be shifted into 744 | register vf. The register calculation is as follows: 745 | 746 | Bits: 15-12 11-8 7-4 3-0 747 | 8 x y E 748 | 749 | if shift_quirks is set, then the source and destination register 750 | will always be x. 751 | """ 752 | x = (self.operand & 0x0F00) >> 8 753 | y = (self.operand & 0x00F0) >> 4 754 | if self.shift_quirks: 755 | bit_seven = (self.v[x] & 0x80) >> 7 756 | self.v[x] = (self.v[x] << 1) & 0xFF 757 | self.v[0xF] = bit_seven 758 | self.last_op = f"SHL V{x:01X}" 759 | else: 760 | bit_seven = (self.v[y] & 0x80) >> 7 761 | self.v[x] = (self.v[y] << 1) & 0xFF 762 | self.v[0xF] = bit_seven 763 | self.last_op = f"SHL V{x:01X}, V{y:01X}" 764 | 765 | def skip_if_reg_not_equal_reg(self): 766 | """ 767 | 9xy0 - SKNE Vx, Vy 768 | 769 | Skip if x register is equal to y register. The calculation 770 | for the registers to use is performed on the operand: 771 | 772 | Bits: 15-12 11-8 7-4 3-0 773 | 9 x y 0 774 | 775 | The program counter is updated to skip the next instruction by 776 | advancing it by 2 bytes. 777 | """ 778 | x = (self.operand & 0x0F00) >> 8 779 | y = (self.operand & 0x00F0) >> 4 780 | if self.v[x] != self.v[y]: 781 | self.pc += 2 782 | if self.memory[self.pc - 2] == 0xF0 and self.memory[self.pc - 1] == 0x00: 783 | self.pc += 2 784 | self.last_op = f"SKNE V{x:01X}, V{y:01X} (comparing {self.v[x]:02X} to {self.v[y]:02X})" 785 | 786 | def load_index_reg_with_value(self): 787 | """ 788 | Annn - LOAD I, nnn 789 | 790 | Load index register with constant value. The calculation for the 791 | constant value is performed on the operand: 792 | 793 | Bits: 15-12 11-8 7-4 3-0 794 | A n n n 795 | """ 796 | self.index = self.operand & 0x0FFF 797 | self.last_op = f"LOAD I, {self.index:03X}" 798 | 799 | def jump_to_register_plus_value(self): 800 | """ 801 | Bnnn - JUMP [V0 + nnn] | [Vx + nn] 802 | 803 | Load the program counter with the memory value located in the specified 804 | operand plus the value of the V0 register. The address calculation 805 | is based on the operand, masked as follows: 806 | 807 | Bits: 15-12 11-8 7-4 3-0 808 | B n n n 809 | 810 | Note that a quirk exists with several emulators. With some Super Chip 8 811 | emulators, bits 11-8 are interpreted to be a register to use as the 812 | base offset, so the address calculation and register source would be: 813 | 814 | Bits: 15-12 11-8 7-4 3-0 815 | B x n n 816 | """ 817 | if self.jump_quirks: 818 | x = (self.operand & 0x0F00) >> 8 819 | self.pc = self.v[x] + (self.operand & 0x00FF) 820 | self.last_op = f"JUMP V{x:01X} + {self.operand & 0x0FF:03X}" 821 | else: 822 | self.pc = self.v[0] + (self.operand & 0x0FFF) 823 | self.last_op = f"JUMP V0 + {self.operand & 0x0FFF:03X}" 824 | 825 | def generate_random_number(self): 826 | """ 827 | Cxnn - RAND Vx, nn 828 | 829 | A random number between 0 and 255 is generated. The contents of it are 830 | then ANDed with the constant value passed in the operand. The result is 831 | stored in the x register. The register and constant values are 832 | calculated as follows: 833 | 834 | Bits: 15-12 11-8 7-4 3-0 835 | C x n n 836 | """ 837 | value = self.operand & 0x00FF 838 | x = (self.operand & 0x0F00) >> 8 839 | self.v[x] = value & randint(0, 255) 840 | self.last_op = f"RAND V{x:01X}, {value:02X}" 841 | 842 | def draw_sprite(self): 843 | """ 844 | Dxyn - DRAW x, y, num_bytes 845 | 846 | Draws the sprite pointed to in the index register at the specified 847 | x and y coordinates. Drawing is done via an XOR routine, meaning that 848 | if the target pixel is already turned on, and a pixel is set to be 849 | turned on at that same location via the draw, then the pixel is turned 850 | off. The routine will wrap the pixels if they are drawn off the edge 851 | of the screen. Each sprite is 8 bits (1 byte) wide. The num_bytes 852 | parameter sets how tall the sprite is. Consecutive bytes in the memory 853 | pointed to by the index register make up the bytes of the sprite. Each 854 | bit in the sprite byte determines whether a pixel is turned on (1) or 855 | turned off (0). For example, assume that the index register pointed 856 | to the following 7 bytes: 857 | 858 | bit 0 1 2 3 4 5 6 7 859 | 860 | byte 0 0 1 1 1 1 1 0 0 861 | byte 1 0 1 0 0 0 0 0 0 862 | byte 2 0 1 0 0 0 0 0 0 863 | byte 3 0 1 1 1 1 1 0 0 864 | byte 4 0 1 0 0 0 0 0 0 865 | byte 5 0 1 0 0 0 0 0 0 866 | byte 6 0 1 1 1 1 1 0 0 867 | 868 | This would draw a character on the screen that looks like an 'E'. The 869 | x_source and y_source tell which registers contain the x and y 870 | coordinates for the sprite. If writing a pixel to a location causes 871 | that pixel to be turned off, then VF will be set to 1. 872 | 873 | Bits: 15-12 11-8 7-4 3-0 874 | unused x_source y_source num_bytes 875 | """ 876 | x_source = (self.operand & 0x0F00) >> 8 877 | y_source = (self.operand & 0x00F0) >> 4 878 | x_pos = self.v[x_source] 879 | y_pos = self.v[y_source] 880 | num_bytes = self.operand & 0x000F 881 | self.v[0xF] = 0 882 | 883 | if num_bytes == 0: 884 | if self.bitplane == 3: 885 | self.draw_extended(x_pos, y_pos, 1, index=self.index) 886 | self.draw_extended(x_pos, y_pos, 2, index=self.index + 32) 887 | else: 888 | self.draw_extended(x_pos, y_pos, self.bitplane) 889 | self.last_op = f"DRAWEX V{x_source:01X}, V{y_source:01X}" 890 | else: 891 | if self.bitplane == 3: 892 | self.draw_normal(x_pos, y_pos, num_bytes, 1, index=self.index) 893 | self.draw_normal(x_pos, y_pos, num_bytes, 2, index=self.index + num_bytes) 894 | else: 895 | self.draw_normal(x_pos, y_pos, num_bytes, self.bitplane) 896 | self.last_op = f"DRAW V{x_source:01X}, V{y_source:01X}" 897 | 898 | def draw_normal(self, x_pos, y_pos, num_bytes, bitplane, index=None): 899 | """ 900 | Draws a sprite on the screen while in NORMAL mode. 901 | 902 | :param x_pos: the X position of the sprite 903 | :param y_pos: the Y position of the sprite 904 | :param num_bytes: the number of bytes to draw 905 | :param bitplane: the bitplane to draw to 906 | :param index: the memory index in memory where byte the pattern is stored 907 | """ 908 | if not index: 909 | index = self.index 910 | 911 | for y_index in range(num_bytes): 912 | color_byte = self.memory[index + y_index] 913 | y_coord = y_pos + y_index 914 | if not self.clip_quirks or (self.clip_quirks and y_coord < self.screen.get_height()): 915 | y_coord = y_coord % self.screen.get_height() 916 | mask = 0x80 917 | for x_index in range(8): 918 | x_coord = x_pos + x_index 919 | if not self.clip_quirks or (self.clip_quirks and x_coord < self.screen.get_width()): 920 | x_coord = x_coord % self.screen.get_width() 921 | turned_on = (color_byte & mask) > 0 922 | current_on = self.screen.get_pixel(x_coord, y_coord, bitplane) 923 | self.v[0xF] |= 1 if turned_on and current_on else 0 924 | self.screen.draw_pixel(x_coord, y_coord, turned_on ^ current_on, bitplane) 925 | mask = mask >> 1 926 | self.screen.update() 927 | 928 | def draw_extended(self, x_pos, y_pos, bitplane, index=None): 929 | """ 930 | Draws a sprite on the screen while in EXTENDED mode. Sprites in this 931 | mode are assumed to be 16x16 pixels. This means that two bytes will 932 | be read from the memory location, and 16 two-byte sequences in total 933 | will be read. 934 | 935 | :param x_pos: the X position of the sprite 936 | :param y_pos: the Y position of the sprite 937 | :param bitplane: the bitplane to draw to 938 | :param index: the memory index in memory where byte the pattern is stored 939 | """ 940 | if not index: 941 | index = self.index 942 | 943 | for y_index in range(16): 944 | for x_byte in range(2): 945 | color_byte = self.memory[index + (y_index * 2) + x_byte] 946 | y_coord = y_pos + y_index 947 | if y_coord < self.screen.get_height(): 948 | y_coord = y_coord % self.screen.get_height() 949 | mask = 0x80 950 | for x_index in range(8): 951 | x_coord = x_pos + x_index + (x_byte * 8) 952 | if not self.clip_quirks or (self.clip_quirks and x_coord < self.screen.get_width()): 953 | x_coord = x_coord % self.screen.get_width() 954 | turned_on = (color_byte & mask) > 0 955 | current_on = self.screen.get_pixel(x_coord, y_coord, bitplane) 956 | self.v[0xF] += 1 if turned_on and current_on else 0 957 | self.screen.draw_pixel(x_coord, y_coord, turned_on ^ current_on, bitplane) 958 | mask = mask >> 1 959 | else: 960 | self.v[0xF] += 1 961 | self.screen.update() 962 | 963 | def index_load_long(self): 964 | """ 965 | F000 - LOADLONG 966 | 967 | Loads the index register with a 16-bit long value. Consumes the next two 968 | bytes from memory and increments the PC by two bytes. 969 | """ 970 | self.index = (self.memory[self.pc] << 8) + self.memory[self.pc+1] 971 | self.pc += 2 972 | self.last_op = f"LOADLONG {self.index:04X}" 973 | 974 | def set_bitplane(self): 975 | """ 976 | Fn01 - BITPLANE n 977 | 978 | Selects the active bitplane for screen drawing operations. Bitplane 979 | selection is as follows: 980 | 981 | 0 - no bitplane selected 982 | 1 - first bitplane selected 983 | 2 - second bitplane selected 984 | 3 - first and second bitplane selected 985 | 986 | The bitplane selection values is as follows: 987 | 988 | Bits: 15-12 11-8 7-4 3-0 989 | F n 0 1 990 | """ 991 | self.bitplane = (self.operand & 0x0F00) >> 8 992 | self.last_op = f"BITPLANE {self.bitplane:01X}" 993 | 994 | def load_audio_pattern_buffer(self): 995 | """ 996 | F002 - AUDIO 997 | 998 | Loads the 16-byte audio pattern buffer with 16 bytes from memory 999 | pointed to by the index register. 1000 | """ 1001 | for x in range(16): 1002 | self.audio_pattern_buffer[x] = self.memory[self.index + x] 1003 | self.calculate_audio_waveform() 1004 | self.last_op = f"AUDIO {self.index:04X}" 1005 | 1006 | def move_delay_timer_into_reg(self): 1007 | """ 1008 | Fx07 - LOAD Vx, DELAY 1009 | 1010 | Move the value of the delay timer into the x register. The 1011 | register calculation is as follows: 1012 | 1013 | Bits: 15-12 11-8 7-4 3-0 1014 | F x 0 7 1015 | """ 1016 | x = (self.operand & 0x0F00) >> 8 1017 | self.v[x] = self.delay 1018 | self.last_op = f"LOAD V{x:01X}, DELAY" 1019 | 1020 | def wait_for_keypress(self): 1021 | """ 1022 | Fx0A - KEYD Vx 1023 | 1024 | Stop execution until a key is pressed. The main emulator loop is responsible 1025 | for checking for keypress events and passing them into the function 1026 | decode_keypress_and_continue to continue the exectuion loop. The register calculation is 1027 | as follows: 1028 | 1029 | Bits: 15-12 11-8 7-4 3-0 1030 | F x 0 A 1031 | """ 1032 | x = (self.operand & 0x0F00) >> 8 1033 | self.awaiting_keypress = True 1034 | self.keypress_register = x 1035 | self.last_op = f"KEYD V{x:01X}" 1036 | 1037 | def decode_keypress_and_continue(self, keys_pressed): 1038 | """ 1039 | Given a set of keys pressed, checks to see if any of them are chip8 1040 | keys, and if they are, store them in the register specified by the 1041 | wait_for_keypress function. Will flag the CPU to continue executing. 1042 | 1043 | :param keys_pressed: the list of keys pressed 1044 | """ 1045 | for keyval, lookup_key in KEY_MAPPINGS.items(): 1046 | if keys_pressed[lookup_key]: 1047 | self.v[self.keypress_register] = keyval 1048 | self.awaiting_keypress = False 1049 | 1050 | def move_reg_into_delay_timer(self): 1051 | """ 1052 | Fx15 - LOAD DELAY, Vx 1053 | 1054 | Move the value stored in the specified x register into the delay 1055 | timer. The register calculation is as follows: 1056 | 1057 | Bits: 15-12 11-8 7-4 3-0 1058 | F x 1 5 1059 | """ 1060 | x = (self.operand & 0x0F00) >> 8 1061 | self.delay = self.v[x] 1062 | self.last_op = f"LOAD DELAY, V{x:01X}" 1063 | 1064 | def move_reg_into_sound_timer(self): 1065 | """ 1066 | Fx18 - LOAD SOUND, Vx 1067 | 1068 | Move the value stored in the specified x register into the sound 1069 | timer. The register calculation is as follows: 1070 | 1071 | Bits: 15-12 11-8 7-4 3-0 1072 | F x 1 8 1073 | """ 1074 | x = (self.operand & 0x0F00) >> 8 1075 | self.sound = self.v[x] 1076 | self.last_op = f"LOAD SOUND, V{x:01X}" 1077 | 1078 | def load_index_with_reg_sprite(self): 1079 | """ 1080 | Fx29 - LOAD I, Vx 1081 | 1082 | Load the index with the sprite indicated in the x register. All 1083 | sprites are 5 bytes long, so the location of the specified sprite 1084 | is its index multiplied by 5. The register calculation is as 1085 | follows: 1086 | 1087 | Bits: 15-12 11-8 7-4 3-0 1088 | F x 2 9 1089 | """ 1090 | x = (self.operand & 0x0F00) >> 8 1091 | self.index = self.v[x] * 5 1092 | self.last_op = f"LOAD I, V{x:01X}" 1093 | 1094 | def load_index_with_extended_reg_sprite(self): 1095 | """ 1096 | Fx30 - LOAD I, Vx 1097 | 1098 | Load the index with the sprite indicated in the x register. All 1099 | sprites are 10 bytes long, so the location of the specified sprite 1100 | is its index multiplied by 10. The register calculation is as 1101 | follows: 1102 | 1103 | Bits: 15-12 11-8 7-4 3-0 1104 | F x 2 9 1105 | """ 1106 | x = (self.operand & 0x0F00) >> 8 1107 | self.index = self.v[x] * 10 1108 | self.last_op = f"LOADEXT I, V{x:01X}" 1109 | 1110 | def add_reg_into_index(self): 1111 | """ 1112 | Fx1E - ADD I, Vx 1113 | 1114 | Add the value of the x register into the index register value. The 1115 | register calculation is as follows: 1116 | 1117 | Bits: 15-12 11-8 7-4 3-0 1118 | F x 1 E 1119 | """ 1120 | x = (self.operand & 0x0F00) >> 8 1121 | self.index += self.v[x] 1122 | self.last_op = f"ADD I, V{x:01X}" 1123 | 1124 | def store_bcd_in_memory(self): 1125 | """ 1126 | Fx33 - BCD 1127 | 1128 | Take the value stored in source and place the digits in the following 1129 | locations: 1130 | 1131 | hundreds -> self.memory[index] 1132 | tens -> self.memory[index + 1] 1133 | ones -> self.memory[index + 2] 1134 | 1135 | For example, if the value is 123, then the following values will be 1136 | placed at the specified locations: 1137 | 1138 | 1 -> self.memory[index] 1139 | 2 -> self.memory[index + 1] 1140 | 3 -> self.memory[index + 2] 1141 | 1142 | The register calculation is as follows: 1143 | 1144 | Bits: 15-12 11-8 7-4 3-0 1145 | F x 3 3 1146 | """ 1147 | x = (self.operand & 0x0F00) >> 8 1148 | bcd_value = f"{self.v[x]:03d}" 1149 | self.memory[self.index] = int(bcd_value[0]) 1150 | self.memory[self.index + 1] = int(bcd_value[1]) 1151 | self.memory[self.index + 2] = int(bcd_value[2]) 1152 | self.last_op = f"BCD V{x:01X} ({bcd_value})" 1153 | 1154 | def load_pitch(self): 1155 | """ 1156 | Fx3A - PITCH Vx 1157 | 1158 | Loads the value from register x into the pitch register. The 1159 | register calculation is as follows: 1160 | 1161 | Bits: 15-12 11-8 7-4 3-0 1162 | F x 3 A 1163 | """ 1164 | x = (self.operand & 0x0F00) >> 8 1165 | self.pitch = self.v[x] 1166 | self.playback_rate = 4000 * 2 ** ((self.pitch - 64) / 48) 1167 | self.last_op = f"PITCH V{x:01X}" 1168 | 1169 | def store_regs_in_memory(self): 1170 | """ 1171 | Fn55 - STOR [I] 1172 | 1173 | Store all V registers in the memory pointed to by the index 1174 | register. The register calculation is as follows: 1175 | 1176 | Bits: 15-12 11-8 7-4 3-0 1177 | F n 5 5 1178 | 1179 | For example, to store all V registers, num_regs would contain 1180 | the value 'F'. 1181 | """ 1182 | num_regs = (self.operand & 0x0F00) >> 8 1183 | for counter in range(num_regs + 1): 1184 | self.memory[self.index + counter] = self.v[counter] 1185 | if not self.index_quirks: 1186 | self.index += num_regs + 1 1187 | self.last_op = f"STOR {num_regs:01X}" 1188 | 1189 | def read_regs_from_memory(self): 1190 | """ 1191 | Fn65 - LOAD V, I 1192 | 1193 | Read the V registers from the memory pointed to by the index 1194 | register. The number of registers to read back is encoded 1195 | as the numeric value in the instruction 1196 | 1197 | Bits: 15-12 11-8 7-4 3-0 1198 | F n 6 5 1199 | 1200 | For example, to load all the V registers, num_regs would 1201 | contain the value 'F'. 1202 | """ 1203 | num_regs = (self.operand & 0x0F00) >> 8 1204 | for counter in range(num_regs + 1): 1205 | self.v[counter] = self.memory[self.index + counter] 1206 | if not self.index_quirks: 1207 | self.index += num_regs + 1 1208 | self.last_op = f"READ {num_regs}" 1209 | 1210 | def store_regs_in_rpl(self): 1211 | """ 1212 | Fs75 - SRPL Vs 1213 | 1214 | Stores all or fewer of the V registers in the RPL store. 1215 | 1216 | Bits: 15-12 11-8 7-4 3-0 1217 | unused num_regs 7 5 1218 | 1219 | For example, to store all the V registers, num_regs would contain 1220 | the value 'F'. 1221 | """ 1222 | num_regs = (self.operand & 0x0F00) >> 8 1223 | for counter in range(num_regs + 1): 1224 | self.rpl[counter] = self.v[counter] 1225 | self.last_op = f"STORRPL {num_regs:01X}" 1226 | 1227 | def read_regs_from_rpl(self): 1228 | """ 1229 | Fs85 - LRPL Vs 1230 | 1231 | Read all or fewer of the V registers from the RPL store. 1232 | 1233 | Bits: 15-12 11-8 7-4 3-0 1234 | unused num_regs 6 5 1235 | 1236 | For example, to load all the V registers, num_regs would 1237 | contain the value 'F'. 1238 | """ 1239 | num_regs = (self.operand & 0x0F00) >> 8 1240 | for counter in range(num_regs + 1): 1241 | self.v[counter] = self.rpl[counter] 1242 | self.last_op = f"READRPL {num_regs:01X}" 1243 | 1244 | def reset(self): 1245 | """ 1246 | Reset the CPU by blanking out all registers, and reseting the stack 1247 | pointer and program counter to their starting values. 1248 | """ 1249 | self.last_op = "None" 1250 | self.sound = 0 1251 | self.delay = 0 1252 | self.v = [0] * NUM_REGISTERS 1253 | self.pc = PROGRAM_COUNTER_START 1254 | self.sp = STACK_POINTER_START 1255 | self.index = 0 1256 | self.rpl = [0] * NUM_REGISTERS 1257 | self.pitch = 64 1258 | self.playback_rate = 4000 1259 | self.audio_pattern_buffer = [0] * 16 1260 | self.sound_playing = False 1261 | self.sound_waveform = None 1262 | self.bitplane = 1 1263 | self.tick_counter = 0 1264 | 1265 | def load_rom(self, filename, offset=PROGRAM_COUNTER_START): 1266 | """ 1267 | Load the ROM indicated by the filename into memory. 1268 | 1269 | :param filename: the name of the file to load 1270 | :type filename: string 1271 | 1272 | :param offset: the location in memory at which to load the ROM 1273 | :type offset: integer 1274 | """ 1275 | with open(filename, 'rb') as rom_file: 1276 | rom_data = rom_file.read() 1277 | for index, val in enumerate(rom_data): 1278 | self.memory[offset + index] = val 1279 | 1280 | def decrement_timers(self): 1281 | """ 1282 | Decrement both the sound and delay timer. 1283 | """ 1284 | self.tick_counter = 0 1285 | 1286 | self.delay -= 1 if self.delay > 0 else 0 1287 | self.sound -= 1 if self.delay > 0 else 0 1288 | if self.sound > 0 and not self.sound_playing: 1289 | if self.sound_waveform: 1290 | self.sound_waveform.play(loops=-1) 1291 | self.sound_playing = True 1292 | 1293 | if self.sound == 0 and self.sound_playing: 1294 | if self.sound_waveform: 1295 | self.sound_waveform.stop() 1296 | self.sound_playing = False 1297 | 1298 | def calculate_audio_waveform(self): 1299 | """ 1300 | Based on a playback rate specified by the XO Chip pitch, generate 1301 | an audio waveform from the 16-byte audio_pattern_buffer. It converts 1302 | the 16-bytes pattern into 128 separate bits. The bits are then used to fill 1303 | a sample buffer. The sample buffer is filled by resampling the 128-bit 1304 | pattern at the specified frequency. The sample buffer is then repeated 1305 | until it is at least MIN_AUDIO_SAMPLES long. Playback (if currently 1306 | happening) is stopped, the new waveform is loaded, and then playback 1307 | is starts again (if the emulator had previously been playing a sound). 1308 | """ 1309 | # Convert the 16-byte value into an array of 128-bit samples 1310 | data = [int(bit) * 255 for bit in ''.join(f"{audio_byte:08b}" for audio_byte in self.audio_pattern_buffer)] 1311 | step = self.playback_rate / PYGAME_AUDIO_PLAYBACK_RATE 1312 | buffer = [] 1313 | 1314 | # Generate the initial re-sampled buffer 1315 | position = 0.0 1316 | while position < 128: 1317 | buffer.append(data[int(position)]) 1318 | position += step 1319 | 1320 | # Lengthen the buffer until it is at least MIN_AUDIO_SAMPLES long 1321 | while len(buffer) < MIN_AUDIO_SAMPLES: 1322 | buffer += buffer 1323 | 1324 | # Stop playing any waveform if it is currently playing 1325 | if self.sound_playing and self.sound_waveform: 1326 | self.sound_waveform.stop() 1327 | 1328 | # Generate a new waveform from the sample buffer 1329 | self.sound_waveform = Sound(np.array(buffer).astype(np.uint8)) 1330 | 1331 | # Start playing the sound again if we should be playing one 1332 | if self.sound_playing: 1333 | self.sound_waveform.play(loops=-1) 1334 | 1335 | # E N D O F F I L E ######################################################## 1336 | -------------------------------------------------------------------------------- /chip8/emulator.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2024 Craig Thomas 3 | This project uses an MIT style license - see LICENSE for details. 4 | 5 | A simple Chip 8 emulator - see the README file for more information. 6 | """ 7 | # I M P O R T S ############################################################### 8 | 9 | import pygame 10 | 11 | from chip8.config import FONT_FILE 12 | from chip8.cpu import Chip8CPU 13 | from chip8.screen import Chip8Screen 14 | 15 | # F U N C T I O N S ########################################################## 16 | 17 | 18 | def main_loop(args): 19 | """ 20 | Runs the main emulator loop with the specified arguments. 21 | 22 | :param args: the parsed command-line arguments 23 | """ 24 | delay_timer_event = 24 25 | max_ticks = int(args.ticks / 60) 26 | 27 | screen = Chip8Screen( 28 | scale_factor=args.scale, 29 | color_0=args.color_0, 30 | color_1=args.color_1, 31 | color_2=args.color_2, 32 | color_3=args.color_3, 33 | ) 34 | screen.init_display() 35 | cpu = Chip8CPU( 36 | screen, 37 | shift_quirks=args.shift_quirks, 38 | index_quirks=args.index_quirks, 39 | jump_quirks=args.jump_quirks, 40 | clip_quirks=args.clip_quirks, 41 | logic_quirks=args.logic_quirks, 42 | mem_size=args.mem_size, 43 | max_ticks=max_ticks 44 | ) 45 | cpu.load_rom(FONT_FILE, 0) 46 | cpu.load_rom(args.rom) 47 | 48 | pygame.init() 49 | pygame.time.set_timer(delay_timer_event, 17) 50 | 51 | while cpu.running: 52 | if not cpu.awaiting_keypress: 53 | cpu.execute_instruction() 54 | 55 | if args.trace: 56 | print(cpu) 57 | 58 | # # Check for events of specific types 59 | for event in pygame.event.get(): 60 | if event.type == delay_timer_event: 61 | cpu.decrement_timers() 62 | if event.type == pygame.QUIT: 63 | cpu.running = False 64 | if event.type == pygame.KEYDOWN: 65 | keys_pressed = pygame.key.get_pressed() 66 | if keys_pressed[pygame.K_ESCAPE]: 67 | cpu.running = False 68 | if cpu.awaiting_keypress: 69 | cpu.decode_keypress_and_continue(keys_pressed) 70 | 71 | # E N D O F F I L E ####################################################### 72 | -------------------------------------------------------------------------------- /chip8/screen.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2012 Craig Thomas 3 | This project uses an MIT style license - see LICENSE for details. 4 | 5 | A Chip 8 Screen - see the README file for more information. 6 | """ 7 | # I M P O R T S ############################################################### 8 | 9 | from pygame import display, HWSURFACE, DOUBLEBUF, Color, draw 10 | 11 | # C O N S T A N T S ########################################################### 12 | 13 | # Various screen modes 14 | SCREEN_MODE_NORMAL = 0 15 | SCREEN_MODE_EXTENDED = 1 16 | 17 | # The depth of the screen is the number of bits used to represent the color 18 | # of a pixel. 19 | SCREEN_DEPTH = 8 20 | 21 | # C L A S S E S ############################################################### 22 | 23 | 24 | class Chip8Screen: 25 | """ 26 | A class to emulate a Chip 8 Screen. The original Chip 8 screen was 64 x 32 27 | with 2 colors. In this emulator, this translates to color 0 (off) and color 28 | 1 (on). 29 | """ 30 | def __init__( 31 | self, 32 | scale_factor, 33 | color_0="000000", 34 | color_1="ff33cc", 35 | color_2="33ccff", 36 | color_3="FFFFFF" 37 | ): 38 | """ 39 | Initializes the main screen. The scale factor is used to modify 40 | the size of the main screen, since the original resolution of the 41 | Chip 8 was 64 x 32, which is quite small. 42 | 43 | :param scale_factor: the scaling factor to apply to the screen 44 | """ 45 | self.height = 64 46 | self.width = 128 47 | self.scale_factor = scale_factor 48 | self.surface = None 49 | self.mode = SCREEN_MODE_NORMAL 50 | self.pixel_colors = { 51 | 0: Color(f"#{color_0}"), 52 | 1: Color(f"#{color_1}"), 53 | 2: Color(f"#{color_2}"), 54 | 3: Color(f"#{color_3}"), 55 | } 56 | 57 | def init_display(self): 58 | """ 59 | Attempts to initialize a screen with the specified height and width. 60 | The screen will by default be of depth SCREEN_DEPTH, and will be 61 | double-buffered in hardware (if possible). 62 | """ 63 | display.init() 64 | self.surface = display.set_mode( 65 | ((self.width * self.scale_factor), 66 | (self.height * self.scale_factor)), 67 | HWSURFACE | DOUBLEBUF, 68 | SCREEN_DEPTH) 69 | display.set_caption('CHIP8 Emulator') 70 | self.clear_screen(3) 71 | self.update() 72 | 73 | def draw_pixel(self, x_pos, y_pos, turn_on, bitplane): 74 | """ 75 | Turn a pixel on or off at the specified location on the screen. Note 76 | that the pixel will not automatically be drawn on the screen, you 77 | must call the update() function to flip the drawing buffer to the 78 | display. The coordinate system starts with (0, 0) being in the top 79 | left of the screen. 80 | 81 | :param x_pos: the x coordinate to place the pixel 82 | :param y_pos: the y coordinate to place the pixel 83 | :param turn_on: whether to turn the pixel on or off 84 | :param bitplane: the bitplane where the pixel is located 85 | """ 86 | if bitplane == 0: 87 | return 88 | 89 | other_bitplane = 2 if bitplane == 1 else 1 90 | other_pixel_on = self.get_pixel(x_pos, y_pos, other_bitplane) 91 | 92 | mode_scale = 1 if self.mode == SCREEN_MODE_EXTENDED else 2 93 | x_base = (x_pos * mode_scale) * self.scale_factor 94 | y_base = (y_pos * mode_scale) * self.scale_factor 95 | 96 | if turn_on and other_pixel_on: 97 | pixel_color = 3 98 | elif turn_on and not other_pixel_on: 99 | pixel_color = bitplane 100 | elif not turn_on and other_pixel_on: 101 | pixel_color = other_bitplane 102 | else: 103 | pixel_color = 0 104 | 105 | draw.rect(self.surface, 106 | self.pixel_colors[pixel_color], 107 | (x_base, y_base, mode_scale * self.scale_factor, mode_scale * self.scale_factor)) 108 | 109 | def get_pixel(self, x_pos, y_pos, bitplane): 110 | """ 111 | Returns whether the pixel is on (1) or off (0) at the specified 112 | location for the specified bitplane. 113 | 114 | :param x_pos: the x coordinate to check 115 | :param y_pos: the y coordinate to check 116 | :param bitplane: the bitplane where the pixel is located 117 | :return: True if the pixel is currently turned on, False otherwise 118 | """ 119 | if bitplane == 0: 120 | return False 121 | 122 | mode_scale = 1 if self.mode == SCREEN_MODE_EXTENDED else 2 123 | x_scale = (x_pos * mode_scale) * self.scale_factor 124 | y_scale = (y_pos * mode_scale) * self.scale_factor 125 | pixel_color = self.surface.get_at((x_scale, y_scale)) 126 | return pixel_color == self.pixel_colors[bitplane] or pixel_color == self.pixel_colors[3] 127 | 128 | def get_width(self): 129 | """ 130 | Returns the width of the screen in pixels. 131 | 132 | :return: the width of the screen 133 | """ 134 | return 128 if self.mode == SCREEN_MODE_EXTENDED else 64 135 | 136 | def get_height(self): 137 | """ 138 | Returns the height of the screen in pixels. 139 | 140 | :return: the height of the screen 141 | """ 142 | return 64 if self.mode == SCREEN_MODE_EXTENDED else 32 143 | 144 | def clear_screen(self, bitplane): 145 | """ 146 | Turns off all the pixels on the specified bitplane. 147 | 148 | :param bitplane: the bitplane to clear 149 | """ 150 | if bitplane == 0: 151 | return 152 | 153 | if bitplane == 3: 154 | self.surface.fill(self.pixel_colors[0]) 155 | return 156 | 157 | max_x = self.get_width() 158 | max_y = self.get_height() 159 | for x in range(max_x): 160 | for y in range(max_y): 161 | self.draw_pixel(x, y, False, bitplane) 162 | 163 | @staticmethod 164 | def update(): 165 | """ 166 | Updates the display by swapping the back buffer and screen buffer. 167 | According to the pygame documentation, the flip should wait for a 168 | vertical retrace when both HWSURFACE and DOUBLEBUF are set on the 169 | surface. 170 | """ 171 | display.flip() 172 | 173 | def set_extended(self): 174 | """ 175 | Sets the screen mode to extended. 176 | """ 177 | self.mode = SCREEN_MODE_EXTENDED 178 | 179 | def set_normal(self): 180 | """ 181 | Sets the screen mode to normal. 182 | """ 183 | self.mode = SCREEN_MODE_NORMAL 184 | 185 | def scroll_down(self, num_lines, bitplane): 186 | """ 187 | Scroll the screen down by num_lines. 188 | 189 | :param num_lines: the number of lines to scroll down 190 | :param bitplane: the bitplane to scroll 191 | """ 192 | if bitplane == 0: 193 | return 194 | 195 | mode_scale = 1 if self.mode == SCREEN_MODE_EXTENDED else 2 196 | actual_lines = num_lines * mode_scale * self.scale_factor 197 | if bitplane == 3: 198 | self.surface.scroll(0, actual_lines) 199 | self.surface.fill(self.pixel_colors[0], (0, 0, self.width * mode_scale * self.scale_factor, actual_lines)) 200 | self.update() 201 | return 202 | 203 | max_x = self.get_width() 204 | max_y = self.get_height() 205 | 206 | # Blank out any pixels in the bottom n lines that we will copy to 207 | for x in range(max_x): 208 | for y in range(max_y - num_lines, max_y): 209 | self.draw_pixel(x, y, False, bitplane) 210 | 211 | # Start copying pixels from the top to the bottom and shift by n pixels 212 | for x in range(max_x): 213 | for y in range(max_y - num_lines - 1, -1, -1): 214 | current_pixel = self.get_pixel(x, y, bitplane) 215 | self.draw_pixel(x, y, False, bitplane) 216 | self.draw_pixel(x, y + num_lines, current_pixel, bitplane) 217 | 218 | # Blank out any pixels in the first num_lines horizontal lines 219 | for x in range(max_x): 220 | for y in range(num_lines): 221 | self.draw_pixel(x, y, False, bitplane) 222 | 223 | def scroll_up(self, num_lines, bitplane): 224 | """ 225 | Scroll the screen up by num_lines. 226 | 227 | :param num_lines: the number of lines to scroll up 228 | :param bitplane: the bitplane to scroll 229 | """ 230 | if bitplane == 0: 231 | return 232 | 233 | mode_scale = 1 if self.mode == SCREEN_MODE_EXTENDED else 2 234 | actual_lines = num_lines * mode_scale * self.scale_factor 235 | if bitplane == 3: 236 | self.surface.scroll(0, -actual_lines) 237 | self.surface.fill( 238 | self.pixel_colors[0], 239 | ( 240 | 0, 241 | self.height * mode_scale * self.scale_factor - actual_lines, 242 | self.width * mode_scale * self.scale_factor, 243 | self.height * mode_scale * self.scale_factor 244 | ) 245 | ) 246 | self.update() 247 | return 248 | 249 | max_x = self.get_width() 250 | max_y = self.get_height() 251 | 252 | # Blank out any pixels in the top n lines that we will copy to 253 | for x in range(max_x): 254 | for y in range(num_lines): 255 | self.draw_pixel(x, y, False, bitplane) 256 | 257 | # Start copying pixels from the top to the bottom and shift up by n pixels 258 | for x in range(max_x): 259 | for y in range(num_lines, max_y): 260 | current_pixel = self.get_pixel(x, y, bitplane) 261 | self.draw_pixel(x, y, False, bitplane) 262 | self.draw_pixel(x, y - num_lines, current_pixel, bitplane) 263 | 264 | # Blank out any pixels in the bottom num_lines horizontal lines 265 | for x in range(max_x): 266 | for y in range(max_y - num_lines, max_y): 267 | self.draw_pixel(x, y, False, bitplane) 268 | 269 | def scroll_left(self, bitplane): 270 | """ 271 | Scroll the screen left 4 pixels. 272 | 273 | :param bitplane: the bitplane to scroll 274 | """ 275 | if bitplane == 0: 276 | return 277 | 278 | other_bitplane = 1 if bitplane == 2 else 2 279 | max_x = self.get_width() 280 | max_y = self.get_height() 281 | 282 | if bitplane == 3: 283 | # Blank out any pixels in the left 4 vertical lines that we will copy to 284 | for x in range(4): 285 | for y in range(max_y): 286 | self.draw_pixel(x, y, False, 1) 287 | self.draw_pixel(x, y, False, 2) 288 | 289 | # Start copying pixels from the right to the left and shift by 4 pixels 290 | for x in range(4, max_x): 291 | for y in range(max_y): 292 | current_pixel = self.get_pixel(x, y, 1) 293 | self.draw_pixel(x, y, False, 1) 294 | self.draw_pixel(x - 4, y, current_pixel, 1) 295 | current_pixel = self.get_pixel(x, y, 2) 296 | self.draw_pixel(x, y, False, 2) 297 | self.draw_pixel(x - 4, y, current_pixel, 2) 298 | 299 | # Blank out any pixels in the right 4 vertical lines 300 | for x in range(max_x - 4, max_x): 301 | for y in range(max_y): 302 | self.draw_pixel(x, y, False, 1) 303 | self.draw_pixel(x, y, False, 2) 304 | else: 305 | for x in range(4): 306 | for y in range(max_y): 307 | self.draw_pixel(x, y, False, bitplane) 308 | 309 | # Start copying pixels from the right to the left and shift by 4 pixels 310 | for x in range(4, max_x): 311 | for y in range(max_y): 312 | current_pixel = self.get_pixel(x, y, bitplane) 313 | self.draw_pixel(x, y, False, bitplane) 314 | self.draw_pixel(x - 4, y, current_pixel, bitplane) 315 | 316 | # Blank out any pixels in the right 4 vertical lines 317 | for x in range(max_x - 4, max_x): 318 | for y in range(max_y): 319 | self.draw_pixel(x, y, False, bitplane) 320 | self.update() 321 | 322 | def scroll_right(self, bitplane): 323 | """ 324 | Scroll the screen right 4 pixels. 325 | 326 | :param bitplane: the bitplane to scroll 327 | """ 328 | if bitplane == 0: 329 | return 330 | 331 | mode_scale = 1 if self.mode == SCREEN_MODE_EXTENDED else 2 332 | actual_lines = 4 * mode_scale * self.scale_factor 333 | 334 | if bitplane == 3: 335 | self.surface.scroll(actual_lines, 0) 336 | self.surface.fill(self.pixel_colors[0], (0, 0, actual_lines, self.height * mode_scale * self.scale_factor)) 337 | self.update() 338 | return 339 | 340 | max_x = self.get_width() 341 | max_y = self.get_height() 342 | 343 | # Blank out any pixels in the right vertical lines that we will copy to 344 | for x in range(max_x - 4, max_x): 345 | for y in range(max_y): 346 | self.draw_pixel(x, y, False, bitplane) 347 | 348 | # Start copying pixels from the left to the right and shift by 4 pixels 349 | for x in range(max_x - 4 - 1, -1, -1): 350 | for y in range(max_y): 351 | current_pixel = self.get_pixel(x, y, bitplane) 352 | self.draw_pixel(x, y, False, bitplane) 353 | self.draw_pixel(x + 4, y, current_pixel, bitplane) 354 | 355 | # Blank out any pixels in the left 4 vertical lines 356 | for x in range(4): 357 | for y in range(max_y): 358 | self.draw_pixel(x, y, False, bitplane) 359 | 360 | # E N D O F F I L E ######################################################## 361 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | coverage: 2 | precision: 2 3 | round: down 4 | range: "70...100" 5 | 6 | status: 7 | project: 8 | default: on 9 | patch: 10 | default: on 11 | changes: 12 | default: off 13 | 14 | comment: 15 | layout: "header, reach, diff, flags, files, footer" 16 | behavior: default 17 | require_changes: no 18 | require_base: no 19 | require_head: yes -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pygame 2 | mock 3 | nose 4 | coverage 5 | numpy 6 | -------------------------------------------------------------------------------- /test/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/craigthomas/Chip8Python/5e414e9d5f7d35d013a002129bd01f0c5c16338b/test/__init__.py -------------------------------------------------------------------------------- /test/romfile: -------------------------------------------------------------------------------- 1 | abcdefg 2 | -------------------------------------------------------------------------------- /test/test_chip8screen.py: -------------------------------------------------------------------------------- 1 | """ 2 | Copyright (C) 2024 Craig Thomas 3 | This project uses an MIT style license - see LICENSE for details. 4 | 5 | A simple Chip 8 emulator - see the README file for more information. 6 | """ 7 | # I M P O R T S ############################################################### 8 | 9 | import unittest 10 | 11 | from chip8.screen import Chip8Screen 12 | 13 | # C L A S S E S ############################################################### 14 | 15 | 16 | class TestChip8Screen(unittest.TestCase): 17 | """ 18 | A test class for the Chip 8 Screen. 19 | """ 20 | def setUp(self): 21 | """ 22 | Common setup routines needed for all unit tests. 23 | """ 24 | self.screen = Chip8Screen(2) 25 | 26 | def test_get_width_normal(self): 27 | self.assertEqual(64, self.screen.get_width()) 28 | 29 | def test_get_width_extended(self): 30 | self.screen.set_extended() 31 | self.assertEqual(128, self.screen.get_width()) 32 | 33 | def test_get_height_normal(self): 34 | self.assertEqual(32, self.screen.get_height()) 35 | 36 | def test_get_height_extended(self): 37 | self.screen.set_extended() 38 | self.assertEqual(64, self.screen.get_height()) 39 | 40 | def test_all_pixels_off_on_screen_init(self): 41 | self.screen.init_display() 42 | for x_pos in range(64): 43 | for y_pos in range(32): 44 | self.assertEqual(0, self.screen.get_pixel(x_pos, y_pos, 1)) 45 | self.assertEqual(0, self.screen.get_pixel(x_pos, y_pos, 2)) 46 | 47 | def test_get_pixel_on_bitplane_0_returns_false(self): 48 | self.screen.init_display() 49 | for xpos in range(64): 50 | for ypos in range(32): 51 | self.screen.draw_pixel(xpos, ypos, 1, 1) 52 | self.screen.draw_pixel(xpos, ypos, 1, 2) 53 | self.assertFalse(self.screen.get_pixel(xpos, ypos, 0)) 54 | 55 | def test_write_pixel_turns_on_pixel_on_bitplane(self): 56 | self.screen.init_display() 57 | for xpos in range(64): 58 | for ypos in range(32): 59 | self.screen.draw_pixel(xpos, ypos, 1, 1) 60 | self.assertTrue(self.screen.get_pixel(xpos, ypos, 1)) 61 | self.assertFalse(self.screen.get_pixel(xpos, ypos, 2)) 62 | 63 | def test_write_pixel_turns_on_pixel_on_both_bitplanes(self): 64 | self.screen.init_display() 65 | for xpos in range(64): 66 | for ypos in range(32): 67 | self.screen.draw_pixel(xpos, ypos, 1, 3) 68 | self.assertTrue(self.screen.get_pixel(xpos, ypos, 1)) 69 | self.assertTrue(self.screen.get_pixel(xpos, ypos, 2)) 70 | 71 | def test_write_pixel_does_nothing_on_bitplane_0(self): 72 | self.screen.init_display() 73 | for xpos in range(64): 74 | for ypos in range(32): 75 | self.screen.draw_pixel(xpos, ypos, 1, 0) 76 | self.assertFalse(self.screen.get_pixel(xpos, ypos, 1)) 77 | self.assertFalse(self.screen.get_pixel(xpos, ypos, 2)) 78 | 79 | def test_clear_screen_clears_pixels_on_bitplane_1(self): 80 | self.screen.init_display() 81 | for x_pos in range(64): 82 | for y_pos in range(32): 83 | self.screen.draw_pixel(x_pos, y_pos, 1, 1) 84 | self.screen.clear_screen(1) 85 | for x_pos in range(64): 86 | for y_pos in range(32): 87 | self.assertFalse(self.screen.get_pixel(x_pos, y_pos, 1)) 88 | self.assertFalse(self.screen.get_pixel(x_pos, y_pos, 2)) 89 | 90 | def test_clear_screen_clears_pixels_on_bitplane_1_only_when_both_set(self): 91 | self.screen.init_display() 92 | for x_pos in range(64): 93 | for y_pos in range(32): 94 | self.screen.draw_pixel(x_pos, y_pos, 1, 1) 95 | self.screen.draw_pixel(x_pos, y_pos, 1, 2) 96 | self.screen.clear_screen(1) 97 | for x_pos in range(64): 98 | for y_pos in range(32): 99 | self.assertFalse(self.screen.get_pixel(x_pos, y_pos, 1)) 100 | self.assertTrue(self.screen.get_pixel(x_pos, y_pos, 2)) 101 | 102 | def test_clear_screen_on_bitplane_0_does_nothing(self): 103 | self.screen.init_display() 104 | for xpos in range(64): 105 | for ypos in range(32): 106 | self.screen.draw_pixel(xpos, ypos, 1, 1) 107 | self.screen.draw_pixel(xpos, ypos, 1, 2) 108 | self.screen.clear_screen(0) 109 | for xpos in range(64): 110 | for ypos in range(32): 111 | self.assertTrue(self.screen.get_pixel(xpos, ypos, 1)) 112 | self.assertTrue(self.screen.get_pixel(xpos, ypos, 2)) 113 | 114 | def test_scroll_down_bitplane_0_does_nothing(self): 115 | self.screen.init_display() 116 | self.screen.draw_pixel(0, 0, 1, 1) 117 | self.screen.draw_pixel(0, 0, 1, 2) 118 | self.assertTrue(self.screen.get_pixel(0, 0, 1)) 119 | self.assertTrue(self.screen.get_pixel(0, 0, 2)) 120 | self.screen.scroll_down(1, 0) 121 | self.assertTrue(self.screen.get_pixel(0, 0, 1)) 122 | self.assertTrue(self.screen.get_pixel(0, 0, 2)) 123 | self.assertFalse(self.screen.get_pixel(0, 1, 1)) 124 | self.assertFalse(self.screen.get_pixel(0, 1, 2)) 125 | 126 | def test_scroll_up_bitplane_0_does_nothing(self): 127 | self.screen.init_display() 128 | self.screen.draw_pixel(0, 1, 1, 1) 129 | self.screen.draw_pixel(0, 1, 1, 2) 130 | self.assertTrue(self.screen.get_pixel(0, 1, 1)) 131 | self.assertTrue(self.screen.get_pixel(0, 1, 2)) 132 | self.screen.scroll_up(1, 0) 133 | self.assertFalse(self.screen.get_pixel(0, 0, 1)) 134 | self.assertFalse(self.screen.get_pixel(0, 0, 2)) 135 | self.assertTrue(self.screen.get_pixel(0, 1, 1)) 136 | self.assertTrue(self.screen.get_pixel(0, 1, 2)) 137 | 138 | def test_scroll_down_bitplane_1(self): 139 | self.screen.init_display() 140 | self.screen.draw_pixel(0, 0, 1, 1) 141 | self.assertTrue(self.screen.get_pixel(0, 0, 1)) 142 | self.assertFalse(self.screen.get_pixel(0, 0, 2)) 143 | self.screen.scroll_down(1, 1) 144 | self.assertFalse(self.screen.get_pixel(0, 0, 1)) 145 | self.assertFalse(self.screen.get_pixel(0, 0, 2)) 146 | self.assertTrue(self.screen.get_pixel(0, 1, 1)) 147 | self.assertFalse(self.screen.get_pixel(0, 1, 2)) 148 | 149 | def test_scroll_up_bitplane_1(self): 150 | self.screen.init_display() 151 | self.screen.draw_pixel(0, 1, 1, 1) 152 | self.assertTrue(self.screen.get_pixel(0, 1, 1)) 153 | self.assertFalse(self.screen.get_pixel(0, 1, 2)) 154 | self.screen.scroll_up(1, 1) 155 | self.assertTrue(self.screen.get_pixel(0, 0, 1)) 156 | self.assertFalse(self.screen.get_pixel(0, 0, 2)) 157 | self.assertFalse(self.screen.get_pixel(0, 1, 1)) 158 | self.assertFalse(self.screen.get_pixel(0, 1, 2)) 159 | 160 | def test_scroll_down_bitplane_1_both_pixels_active(self): 161 | self.screen.init_display() 162 | self.screen.draw_pixel(0, 0, 1, 1) 163 | self.screen.draw_pixel(0, 0, 1, 2) 164 | self.assertTrue(self.screen.get_pixel(0, 0, 1)) 165 | self.assertTrue(self.screen.get_pixel(0, 0, 2)) 166 | self.screen.scroll_down(1, 1) 167 | self.assertFalse(self.screen.get_pixel(0, 0, 1)) 168 | self.assertTrue(self.screen.get_pixel(0, 0, 2)) 169 | self.assertTrue(self.screen.get_pixel(0, 1, 1)) 170 | self.assertFalse(self.screen.get_pixel(0, 1, 2)) 171 | 172 | def test_scroll_up_bitplane_1_both_pixels_active(self): 173 | self.screen.init_display() 174 | self.screen.draw_pixel(0, 1, 1, 1) 175 | self.screen.draw_pixel(0, 1, 1, 2) 176 | self.assertTrue(self.screen.get_pixel(0, 1, 1)) 177 | self.assertTrue(self.screen.get_pixel(0, 1, 2)) 178 | self.screen.scroll_up(1, 1) 179 | self.assertTrue(self.screen.get_pixel(0, 0, 1)) 180 | self.assertFalse(self.screen.get_pixel(0, 0, 2)) 181 | self.assertFalse(self.screen.get_pixel(0, 1, 1)) 182 | self.assertTrue(self.screen.get_pixel(0, 1, 2)) 183 | 184 | def test_scroll_down_bitplane_3_both_pixels_active(self): 185 | self.screen.init_display() 186 | self.screen.draw_pixel(0, 0, 1, 1) 187 | self.screen.draw_pixel(0, 0, 1, 2) 188 | self.assertTrue(self.screen.get_pixel(0, 0, 1)) 189 | self.assertTrue(self.screen.get_pixel(0, 0, 2)) 190 | self.screen.scroll_down(1, 3) 191 | self.assertFalse(self.screen.get_pixel(0, 0, 1)) 192 | self.assertFalse(self.screen.get_pixel(0, 0, 2)) 193 | self.assertTrue(self.screen.get_pixel(0, 1, 1)) 194 | self.assertTrue(self.screen.get_pixel(0, 1, 2)) 195 | 196 | def test_scroll_up_bitplane_3_both_pixels_active(self): 197 | self.screen.init_display() 198 | self.screen.draw_pixel(0, 1, 1, 1) 199 | self.screen.draw_pixel(0, 1, 1, 2) 200 | self.assertTrue(self.screen.get_pixel(0, 1, 1)) 201 | self.assertTrue(self.screen.get_pixel(0, 1, 2)) 202 | self.screen.scroll_up(1, 3) 203 | self.assertTrue(self.screen.get_pixel(0, 0, 1)) 204 | self.assertTrue(self.screen.get_pixel(0, 0, 2)) 205 | self.assertFalse(self.screen.get_pixel(0, 1, 1)) 206 | self.assertFalse(self.screen.get_pixel(0, 1, 2)) 207 | 208 | def test_scroll_right_bitplane_0_does_nothing(self): 209 | self.screen.init_display() 210 | self.screen.draw_pixel(0, 0, 1, 1) 211 | self.screen.draw_pixel(0, 0, 1, 2) 212 | self.assertTrue(self.screen.get_pixel(0, 0, 1)) 213 | self.assertTrue(self.screen.get_pixel(0, 0, 2)) 214 | self.screen.scroll_right(0) 215 | self.assertTrue(self.screen.get_pixel(0, 0, 1)) 216 | self.assertFalse(self.screen.get_pixel(1, 0, 1)) 217 | self.assertFalse(self.screen.get_pixel(2, 0, 1)) 218 | self.assertFalse(self.screen.get_pixel(3, 0, 1)) 219 | self.assertFalse(self.screen.get_pixel(4, 0, 1)) 220 | self.assertTrue(self.screen.get_pixel(0, 0, 2)) 221 | self.assertFalse(self.screen.get_pixel(1, 0, 2)) 222 | self.assertFalse(self.screen.get_pixel(2, 0, 2)) 223 | self.assertFalse(self.screen.get_pixel(3, 0, 2)) 224 | self.assertFalse(self.screen.get_pixel(4, 0, 2)) 225 | 226 | def test_scroll_right_bitplane_1(self): 227 | self.screen.init_display() 228 | self.screen.draw_pixel(0, 0, 1, 1) 229 | self.assertTrue(self.screen.get_pixel(0, 0, 1)) 230 | self.assertFalse(self.screen.get_pixel(0, 0, 2)) 231 | self.screen.scroll_right(1) 232 | self.assertFalse(self.screen.get_pixel(0, 0, 1)) 233 | self.assertFalse(self.screen.get_pixel(1, 0, 1)) 234 | self.assertFalse(self.screen.get_pixel(2, 0, 1)) 235 | self.assertFalse(self.screen.get_pixel(3, 0, 1)) 236 | self.assertTrue(self.screen.get_pixel(4, 0, 1)) 237 | self.assertFalse(self.screen.get_pixel(0, 0, 2)) 238 | self.assertFalse(self.screen.get_pixel(1, 0, 2)) 239 | self.assertFalse(self.screen.get_pixel(2, 0, 2)) 240 | self.assertFalse(self.screen.get_pixel(3, 0, 2)) 241 | self.assertFalse(self.screen.get_pixel(4, 0, 2)) 242 | 243 | def test_scroll_right_bitplane_1_both_pixels_active(self): 244 | self.screen.init_display() 245 | self.screen.draw_pixel(0, 0, 1, 1) 246 | self.screen.draw_pixel(0, 0, 1, 2) 247 | self.assertTrue(self.screen.get_pixel(0, 0, 1)) 248 | self.assertTrue(self.screen.get_pixel(0, 0, 2)) 249 | self.screen.scroll_right(1) 250 | self.assertFalse(self.screen.get_pixel(0, 0, 1)) 251 | self.assertFalse(self.screen.get_pixel(1, 0, 1)) 252 | self.assertFalse(self.screen.get_pixel(2, 0, 1)) 253 | self.assertFalse(self.screen.get_pixel(3, 0, 1)) 254 | self.assertTrue(self.screen.get_pixel(4, 0, 1)) 255 | self.assertTrue(self.screen.get_pixel(0, 0, 2)) 256 | self.assertFalse(self.screen.get_pixel(1, 0, 2)) 257 | self.assertFalse(self.screen.get_pixel(2, 0, 2)) 258 | self.assertFalse(self.screen.get_pixel(3, 0, 2)) 259 | self.assertFalse(self.screen.get_pixel(4, 0, 2)) 260 | 261 | def test_scroll_right_bitplane_3_both_pixels_active(self): 262 | self.screen.init_display() 263 | self.screen.draw_pixel(0, 0, 1, 1) 264 | self.screen.draw_pixel(0, 0, 1, 2) 265 | self.assertTrue(self.screen.get_pixel(0, 0, 1)) 266 | self.assertTrue(self.screen.get_pixel(0, 0, 2)) 267 | self.screen.scroll_right(3) 268 | self.assertFalse(self.screen.get_pixel(0, 0, 1)) 269 | self.assertFalse(self.screen.get_pixel(1, 0, 1)) 270 | self.assertFalse(self.screen.get_pixel(2, 0, 1)) 271 | self.assertFalse(self.screen.get_pixel(3, 0, 1)) 272 | self.assertTrue(self.screen.get_pixel(4, 0, 1)) 273 | self.assertFalse(self.screen.get_pixel(0, 0, 2)) 274 | self.assertFalse(self.screen.get_pixel(1, 0, 2)) 275 | self.assertFalse(self.screen.get_pixel(2, 0, 2)) 276 | self.assertFalse(self.screen.get_pixel(3, 0, 2)) 277 | self.assertTrue(self.screen.get_pixel(4, 0, 2)) 278 | 279 | def test_scroll_left_bitplane_0_does_nothing(self): 280 | self.screen.init_display() 281 | self.screen.draw_pixel(63, 0, 1, 1) 282 | self.screen.draw_pixel(63, 0, 1, 2) 283 | self.assertTrue(self.screen.get_pixel(63, 0, 1)) 284 | self.assertTrue(self.screen.get_pixel(63, 0, 2)) 285 | self.screen.scroll_left(0) 286 | self.assertTrue(self.screen.get_pixel(63, 0, 1)) 287 | self.assertFalse(self.screen.get_pixel(62, 0, 1)) 288 | self.assertFalse(self.screen.get_pixel(61, 0, 1)) 289 | self.assertFalse(self.screen.get_pixel(60, 0, 1)) 290 | self.assertFalse(self.screen.get_pixel(59, 0, 1)) 291 | self.assertTrue(self.screen.get_pixel(63, 0, 2)) 292 | self.assertFalse(self.screen.get_pixel(62, 0, 2)) 293 | self.assertFalse(self.screen.get_pixel(61, 0, 2)) 294 | self.assertFalse(self.screen.get_pixel(60, 0, 2)) 295 | self.assertFalse(self.screen.get_pixel(59, 0, 2)) 296 | 297 | def test_scroll_left_bitplane_1(self): 298 | self.screen.init_display() 299 | self.screen.draw_pixel(63, 0, 1, 1) 300 | self.assertTrue(self.screen.get_pixel(63, 0, 1)) 301 | self.assertFalse(self.screen.get_pixel(63, 0, 2)) 302 | self.screen.scroll_left(1) 303 | self.assertFalse(self.screen.get_pixel(63, 0, 1)) 304 | self.assertFalse(self.screen.get_pixel(62, 0, 1)) 305 | self.assertFalse(self.screen.get_pixel(61, 0, 1)) 306 | self.assertFalse(self.screen.get_pixel(60, 0, 1)) 307 | self.assertTrue(self.screen.get_pixel(59, 0, 1)) 308 | self.assertFalse(self.screen.get_pixel(63, 0, 2)) 309 | self.assertFalse(self.screen.get_pixel(62, 0, 2)) 310 | self.assertFalse(self.screen.get_pixel(61, 0, 2)) 311 | self.assertFalse(self.screen.get_pixel(60, 0, 2)) 312 | self.assertFalse(self.screen.get_pixel(59, 0, 2)) 313 | 314 | def test_scroll_left_bitplane_1_both_pixels_active(self): 315 | self.screen.init_display() 316 | self.screen.draw_pixel(63, 0, 1, 1) 317 | self.screen.draw_pixel(63, 0, 1, 2) 318 | self.assertTrue(self.screen.get_pixel(63, 0, 1)) 319 | self.assertTrue(self.screen.get_pixel(63, 0, 2)) 320 | self.screen.scroll_left(1) 321 | self.assertFalse(self.screen.get_pixel(63, 0, 1)) 322 | self.assertFalse(self.screen.get_pixel(62, 0, 1)) 323 | self.assertFalse(self.screen.get_pixel(61, 0, 1)) 324 | self.assertFalse(self.screen.get_pixel(60, 0, 1)) 325 | self.assertTrue(self.screen.get_pixel(59, 0, 1)) 326 | self.assertTrue(self.screen.get_pixel(63, 0, 2)) 327 | self.assertFalse(self.screen.get_pixel(62, 0, 2)) 328 | self.assertFalse(self.screen.get_pixel(61, 0, 2)) 329 | self.assertFalse(self.screen.get_pixel(60, 0, 2)) 330 | self.assertFalse(self.screen.get_pixel(59, 0, 2)) 331 | 332 | def test_scroll_left_bitplane_3_both_pixels_active(self): 333 | self.screen.init_display() 334 | self.screen.draw_pixel(63, 0, 1, 1) 335 | self.screen.draw_pixel(63, 0, 1, 2) 336 | self.assertTrue(self.screen.get_pixel(63, 0, 1)) 337 | self.assertTrue(self.screen.get_pixel(63, 0, 2)) 338 | self.screen.scroll_left(3) 339 | self.assertFalse(self.screen.get_pixel(63, 0, 1)) 340 | self.assertFalse(self.screen.get_pixel(62, 0, 1)) 341 | self.assertFalse(self.screen.get_pixel(61, 0, 1)) 342 | self.assertFalse(self.screen.get_pixel(60, 0, 1)) 343 | self.assertTrue(self.screen.get_pixel(59, 0, 1)) 344 | self.assertFalse(self.screen.get_pixel(63, 0, 2)) 345 | self.assertFalse(self.screen.get_pixel(62, 0, 2)) 346 | self.assertFalse(self.screen.get_pixel(61, 0, 2)) 347 | self.assertFalse(self.screen.get_pixel(60, 0, 2)) 348 | self.assertTrue(self.screen.get_pixel(59, 0, 2)) 349 | 350 | def test_set_normal(self): 351 | self.screen.init_display() 352 | self.screen.set_extended() 353 | self.screen.set_normal() 354 | self.assertEqual(64, self.screen.get_width()) 355 | self.assertEqual(32, self.screen.get_height()) 356 | 357 | 358 | # M A I N ##################################################################### 359 | 360 | if __name__ == '__main__': 361 | unittest.main() 362 | 363 | # E N D O F F I L E ####################################################### 364 | -------------------------------------------------------------------------------- /yac8e.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | """ 3 | Copyright (C) 2024 Craig Thomas 4 | This project uses an MIT style license - see LICENSE for details. 5 | 6 | A simple Chip 8 emulator - see the README file for more information. 7 | """ 8 | # I M P O R T S ############################################################### 9 | 10 | import argparse 11 | import os 12 | 13 | # G L O B A L S ############################################################### 14 | 15 | os.environ['PYGAME_HIDE_SUPPORT_PROMPT'] = "hide" 16 | 17 | # F U N C T I O N S ########################################################## 18 | 19 | 20 | def parse_arguments(): 21 | """ 22 | Parses the command-line arguments passed to the emulator. 23 | 24 | :return: the parsed command-line arguments 25 | """ 26 | parser = argparse.ArgumentParser( 27 | description="Starts a simple Chip 8 " 28 | "emulator. See README.md for more information, and LICENSE for " 29 | "terms of use.") 30 | parser.add_argument( 31 | "rom", help="the ROM file to load on startup") 32 | parser.add_argument( 33 | "--scale", help="the scale factor to apply to the display " 34 | "(default is 5)", type=int, default=5, dest="scale") 35 | parser.add_argument( 36 | "--ticks", help="how many instructions per second are allowed", 37 | type=int, default=1000, dest="ticks") 38 | parser.add_argument( 39 | "--shift_quirks", help="Enable shift quirks", 40 | action="store_true", dest="shift_quirks" 41 | ) 42 | parser.add_argument( 43 | "--index_quirks", help="Enable index quirks", 44 | action="store_true", dest="index_quirks" 45 | ) 46 | parser.add_argument( 47 | "--jump_quirks", help="Enable jump quirks", 48 | action="store_true", dest="jump_quirks" 49 | ) 50 | parser.add_argument( 51 | "--clip_quirks", help="Enable screen clipping quirks", 52 | action="store_true", dest="clip_quirks" 53 | ) 54 | parser.add_argument( 55 | "--logic_quirks", help="Enable logic quirks", 56 | action="store_true", dest="logic_quirks" 57 | ) 58 | parser.add_argument( 59 | "--mem_size", help="Maximum memory size (64K default)", 60 | dest="mem_size", choices=["4K", "64K"], default="64K" 61 | ) 62 | parser.add_argument( 63 | "--trace", help="print registers and instructions to STDOUT", 64 | action="store_true", dest="trace" 65 | ) 66 | parser.add_argument( 67 | "--color_0", help="the hex color to use for the background (default=000000)", 68 | dest="color_0", default="000000" 69 | ) 70 | parser.add_argument( 71 | "--color_1", help="the hex color to use for bitplane 1 (default=ff33cc)", 72 | dest="color_1", default="ff33cc" 73 | ) 74 | parser.add_argument( 75 | "--color_2", help="the hex color to use for bitplane 2 (default=33ccff)", 76 | dest="color_2", default="33ccff" 77 | ) 78 | parser.add_argument( 79 | "--color_3", help="the hex color to use for bitplane overlaps (default=FFFFFF)", 80 | dest="color_3", default="FFFFFF" 81 | ) 82 | return parser.parse_args() 83 | 84 | 85 | # M A I N ##################################################################### 86 | 87 | if __name__ == "__main__": 88 | from chip8.emulator import main_loop 89 | main_loop(parse_arguments()) 90 | 91 | # E N D O F F I L E ####################################################### 92 | --------------------------------------------------------------------------------