chip8-py 0.1.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chip8_py-0.1.1.dist-info/METADATA +71 -0
- chip8_py-0.1.1.dist-info/RECORD +14 -0
- chip8_py-0.1.1.dist-info/WHEEL +5 -0
- chip8_py-0.1.1.dist-info/entry_points.txt +2 -0
- chip8_py-0.1.1.dist-info/licenses/LICENSE +21 -0
- chip8_py-0.1.1.dist-info/top_level.txt +1 -0
- pychip8/__init__.py +0 -0
- pychip8/__main__.py +87 -0
- pychip8/config.toml +15 -0
- pychip8/cpu.py +247 -0
- pychip8/display.py +41 -0
- pychip8/keypad.py +20 -0
- pychip8/memory.py +40 -0
- pychip8/timers.py +55 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chip8-py
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A CHIP-8 emulator ecosystem.
|
|
5
|
+
Author: las-r
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.11
|
|
8
|
+
Description-Content-Type: text/markdown
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Requires-Dist: numpy
|
|
11
|
+
Requires-Dist: platformdirs
|
|
12
|
+
Requires-Dist: raylib
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
# pychip8
|
|
16
|
+
A modular, extensible CHIP-8 emulator ecosystem written in modern Python.
|
|
17
|
+
|
|
18
|
+
> **Note:** This is a remake of an old project of mine, [chip-8-python](https://github.com/las-r/chip-8-python), which aimed to do the same thing but was honestly pretty poorly made. It was monolithic and clunky, and this one is built to be an actual Python package.
|
|
19
|
+
|
|
20
|
+
## Installation
|
|
21
|
+
Requires Python 3.11+.
|
|
22
|
+
|
|
23
|
+
```sh
|
|
24
|
+
git clone https://github.com/las-r/pychip8
|
|
25
|
+
cd pychip8
|
|
26
|
+
pip install -e .
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage
|
|
30
|
+
```sh
|
|
31
|
+
pychip8 path/to/rom.ch8
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Config
|
|
35
|
+
### Flags
|
|
36
|
+
Flags override the config file.
|
|
37
|
+
|
|
38
|
+
| Flag | Description |
|
|
39
|
+
|---|---|
|
|
40
|
+
| `--cpf <int>` | Cycles per frame (default: 10) |
|
|
41
|
+
| `--scale <int>` | Display scale (default: 12) |
|
|
42
|
+
| `--volume <float>` | Audio volume 0.0–1.0 (default: 0.2) |
|
|
43
|
+
| `--cosmac-shift / --no-cosmac-shift` | COSMAC VIP shift quirk |
|
|
44
|
+
| `--cosmac-jump / --no-cosmac-jump` | COSMAC VIP jump quirk |
|
|
45
|
+
| `--cosmac-i-add / --no-cosmac-i-add` | COSMAC VIP index add quirk |
|
|
46
|
+
| `--cosmac-font / --no-cosmac-font` | COSMAC VIP font quirk |
|
|
47
|
+
| `--cosmac-ls / --no-cosmac-ls` | COSMAC VIP load/store quirk |
|
|
48
|
+
| `--vf-reset / --no-vf-reset` | Reset VF after logic instructions |
|
|
49
|
+
|
|
50
|
+
### File
|
|
51
|
+
On first run, a default config is created at:
|
|
52
|
+
- **Windows:** `C:/Users/YOUR_USER_PROFILE/AppData/Local/pychip8/`
|
|
53
|
+
- **macOS:** `~/Library/Application Support/pychip8/`
|
|
54
|
+
- **Linux:** `~/.config/pychip8/`
|
|
55
|
+
|
|
56
|
+
Edit this file to set your preferred defaults.
|
|
57
|
+
|
|
58
|
+
## Keypad
|
|
59
|
+
The CHIP-8 hex keypad maps to the left side of a QWERTY keyboard:
|
|
60
|
+
```
|
|
61
|
+
CHIP-8 Keyboard
|
|
62
|
+
1 2 3 C 1 2 3 4
|
|
63
|
+
4 5 6 D Q W E R
|
|
64
|
+
7 8 9 E A S D F
|
|
65
|
+
A 0 B F Z X C V
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Credits
|
|
69
|
+
- [raylib](https://www.raylib.com/) ([Python](https://github.com/electronstudio/raylib-python-cffi/))
|
|
70
|
+
- [Guide to making a CHIP-8 emulator](https://tobiasvl.github.io/blog/write-a-chip-8-emulator/)
|
|
71
|
+
- [Timendus' CHIP-8 Test Suite](https://github.com/Timendus/chip8-test-suite)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
chip8_py-0.1.1.dist-info/licenses/LICENSE,sha256=0sUKddG_MAEmXC_bOrbwo0aYFSfKiAdDgwirseE3cGo,1066
|
|
2
|
+
pychip8/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
3
|
+
pychip8/__main__.py,sha256=FcRWP3TQ4SnRmJRCYvNJK-kcZ5GJdqWBP-4qnwZ0ta8,3078
|
|
4
|
+
pychip8/config.toml,sha256=3EgBXb_6xBvtTVdkaR3pi9DlCqKwmIYhh-tX7mkqXxo,192
|
|
5
|
+
pychip8/cpu.py,sha256=UkbLwiPZ9hAhY8UQXl_M9uZMDP8Ly914cwAetoZhyf8,8041
|
|
6
|
+
pychip8/display.py,sha256=yx-yxG_5XUOQY0vu_8xBOqJkIXAQvvMfVV-pwsPL4dk,1130
|
|
7
|
+
pychip8/keypad.py,sha256=a1inot44EOiy_taiSFYYYVGr4wIoC843LURun2laai0,792
|
|
8
|
+
pychip8/memory.py,sha256=kYmv88I8aS6X9SPl0XOHqIlL8_-sb5hfVTenxyvo29w,1222
|
|
9
|
+
pychip8/timers.py,sha256=2bpiabZeGI2Ceee4po0y4Rez90M57MDKu0h_Gf24Wv8,1646
|
|
10
|
+
chip8_py-0.1.1.dist-info/METADATA,sha256=MZaickaCSOmzmSQQhfCAqH8yFPDlycXeyJjwaGi2ewI,2185
|
|
11
|
+
chip8_py-0.1.1.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
|
|
12
|
+
chip8_py-0.1.1.dist-info/entry_points.txt,sha256=RB3j_uxeFd9hXZaxNpXAqOKFilV8TpV8pTVuuNgc9gg,50
|
|
13
|
+
chip8_py-0.1.1.dist-info/top_level.txt,sha256=uGwyF_4peqm6uOkGF5EGfiGWwppVMwvmQMVb7Lkt4ks,8
|
|
14
|
+
chip8_py-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Nayif Ehan
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pychip8
|
pychip8/__init__.py
ADDED
|
File without changes
|
pychip8/__main__.py
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
from .cpu import Processor
|
|
2
|
+
from .display import Display
|
|
3
|
+
from .keypad import Keypad
|
|
4
|
+
from .memory import Memory
|
|
5
|
+
from .timers import Timers
|
|
6
|
+
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from platformdirs import user_config_dir
|
|
9
|
+
import argparse
|
|
10
|
+
import shutil
|
|
11
|
+
import tomllib
|
|
12
|
+
import pyray as rl
|
|
13
|
+
|
|
14
|
+
# setup and load config
|
|
15
|
+
CONFDIR = Path(user_config_dir("pychip8"))
|
|
16
|
+
CONFPATH = CONFDIR / "config.toml"
|
|
17
|
+
if not CONFPATH.exists():
|
|
18
|
+
CONFDIR.mkdir(parents=True, exist_ok=True)
|
|
19
|
+
shutil.copy(Path(__file__).parent / "config.toml", CONFPATH)
|
|
20
|
+
with open(CONFPATH, "rb") as f:
|
|
21
|
+
cfg = tomllib.load(f)
|
|
22
|
+
|
|
23
|
+
# main
|
|
24
|
+
def main():
|
|
25
|
+
# args
|
|
26
|
+
parser = argparse.ArgumentParser(prog="pychip8", description="A CHIP-8 emulator.")
|
|
27
|
+
parser.add_argument("rom", help="path to rom file")
|
|
28
|
+
parser.add_argument("--cpf", type=int, help="cycles per frame")
|
|
29
|
+
parser.add_argument("--scale", type=int, help="display scale")
|
|
30
|
+
parser.add_argument("--volume", type=float, help="audio volume (0.0-1.0)")
|
|
31
|
+
parser.add_argument("--cosmac-shift", action=argparse.BooleanOptionalAction)
|
|
32
|
+
parser.add_argument("--cosmac-jump", action=argparse.BooleanOptionalAction)
|
|
33
|
+
parser.add_argument("--cosmac-i-add", action=argparse.BooleanOptionalAction)
|
|
34
|
+
parser.add_argument("--cosmac-font", action=argparse.BooleanOptionalAction)
|
|
35
|
+
parser.add_argument("--cosmac-ls", action=argparse.BooleanOptionalAction)
|
|
36
|
+
parser.add_argument("--vf-reset", action=argparse.BooleanOptionalAction)
|
|
37
|
+
parser.add_argument("--fg", type=lambda x: int(x, 16), help="foreground color (hex, e.g. FFFFFFFF)")
|
|
38
|
+
parser.add_argument("--bg", type=lambda x: int(x, 16), help="background color (hex, e.g. 000000FF)")
|
|
39
|
+
args = parser.parse_args()
|
|
40
|
+
|
|
41
|
+
# config helpers
|
|
42
|
+
def gcpu(key, flag):
|
|
43
|
+
return flag if flag is not None else cfg["cpu"][key]
|
|
44
|
+
def gdisp(key, flag):
|
|
45
|
+
return flag if flag is not None else cfg["display"][key]
|
|
46
|
+
def gaud(key, flag):
|
|
47
|
+
return flag if flag is not None else cfg["audio"][key]
|
|
48
|
+
|
|
49
|
+
# load rom
|
|
50
|
+
with open(args.rom, "rb") as f:
|
|
51
|
+
rom = f.read()
|
|
52
|
+
|
|
53
|
+
# init
|
|
54
|
+
display = Display(
|
|
55
|
+
scale=gdisp("scale", args.scale),
|
|
56
|
+
fg=gdisp("fg", args.fg),
|
|
57
|
+
bg=gdisp("bg", args.bg),
|
|
58
|
+
)
|
|
59
|
+
keypad = Keypad()
|
|
60
|
+
timers = Timers(vol=gaud("volume", args.volume))
|
|
61
|
+
memory = Memory()
|
|
62
|
+
memory.load_rom(rom)
|
|
63
|
+
cpu = Processor(memory, display, keypad, timers, cpf=gcpu("cpf", args.cpf))
|
|
64
|
+
|
|
65
|
+
# apply quirks
|
|
66
|
+
cpu.cosmac_shift = gcpu("cosmac_shift", args.cosmac_shift)
|
|
67
|
+
cpu.cosmac_jump = gcpu("cosmac_jump", args.cosmac_jump)
|
|
68
|
+
cpu.cosmac_i_add = gcpu("cosmac_i_add", args.cosmac_i_add)
|
|
69
|
+
cpu.cosmac_font = gcpu("cosmac_font", args.cosmac_font)
|
|
70
|
+
cpu.cosmac_ls = gcpu("cosmac_ls", args.cosmac_ls)
|
|
71
|
+
cpu.vf_reset = gcpu("vf_reset", args.vf_reset)
|
|
72
|
+
|
|
73
|
+
# main loop
|
|
74
|
+
while not rl.window_should_close():
|
|
75
|
+
cpu.pkeys = cpu.kp.keys.copy()
|
|
76
|
+
cpu.kp.update()
|
|
77
|
+
for _ in range(cpu.cpf):
|
|
78
|
+
cpu.cycle()
|
|
79
|
+
cpu.tm.update()
|
|
80
|
+
cpu.disp.render()
|
|
81
|
+
|
|
82
|
+
# deinit
|
|
83
|
+
cpu.disp.deinit()
|
|
84
|
+
cpu.tm.deinit()
|
|
85
|
+
|
|
86
|
+
if __name__ == "__main__":
|
|
87
|
+
main()
|
pychip8/config.toml
ADDED
pychip8/cpu.py
ADDED
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
from .display import Display
|
|
2
|
+
from .keypad import Keypad
|
|
3
|
+
from .memory import Memory
|
|
4
|
+
from .timers import Timers
|
|
5
|
+
import numpy as np
|
|
6
|
+
import random
|
|
7
|
+
|
|
8
|
+
# pychip8 cpu
|
|
9
|
+
# by las-r
|
|
10
|
+
|
|
11
|
+
# processor class
|
|
12
|
+
class Processor:
|
|
13
|
+
def __init__(self, ram: Memory, disp: Display, kp: Keypad, tm: Timers, cpf: int = 10):
|
|
14
|
+
self.ram = ram
|
|
15
|
+
self.disp = disp
|
|
16
|
+
self.kp = kp
|
|
17
|
+
self.tm = tm
|
|
18
|
+
self.cpf = cpf
|
|
19
|
+
|
|
20
|
+
# opcode options
|
|
21
|
+
self.cosmac_shift = False # def: False
|
|
22
|
+
self.cosmac_jump = False # def: False
|
|
23
|
+
self.cosmac_i_add = False # def: False
|
|
24
|
+
self.cosmac_font = True # def: True
|
|
25
|
+
self.cosmac_ls = False # def: False
|
|
26
|
+
self.vf_reset = True # def: True
|
|
27
|
+
|
|
28
|
+
# data storage
|
|
29
|
+
self.v = np.zeros(16, np.uint8)
|
|
30
|
+
self.pc = np.uint16(0x200)
|
|
31
|
+
self.i = np.uint16(0)
|
|
32
|
+
self.stack = []
|
|
33
|
+
|
|
34
|
+
# other
|
|
35
|
+
self.pkeys = np.zeros(16, dtype=np.uint8)
|
|
36
|
+
|
|
37
|
+
def fetch(self) -> np.uint16:
|
|
38
|
+
high = self.ram[self.pc]
|
|
39
|
+
low = self.ram[self.pc + 1]
|
|
40
|
+
opc = (np.uint16(high) << 8) | np.uint16(low)
|
|
41
|
+
self.pc += 2
|
|
42
|
+
return opc
|
|
43
|
+
|
|
44
|
+
def execute(self, inst):
|
|
45
|
+
l = inst >> 12
|
|
46
|
+
x = (inst >> 8) & 0xf
|
|
47
|
+
y = (inst >> 4) & 0xf
|
|
48
|
+
n = inst & 0xf
|
|
49
|
+
nn = inst & 0xff
|
|
50
|
+
nnn = inst & 0xfff
|
|
51
|
+
|
|
52
|
+
match (l, x, y, n):
|
|
53
|
+
# clear screen
|
|
54
|
+
case (0, 0, 14, 0):
|
|
55
|
+
self.disp.clear()
|
|
56
|
+
|
|
57
|
+
# return from subroutine
|
|
58
|
+
case (0, 0, 14, 14):
|
|
59
|
+
self.pc = self.stack.pop()
|
|
60
|
+
|
|
61
|
+
# jump to nnn
|
|
62
|
+
case (1, _, _, _):
|
|
63
|
+
self.pc = nnn
|
|
64
|
+
|
|
65
|
+
# call subroutine
|
|
66
|
+
case (2, _, _, _):
|
|
67
|
+
self.stack.append(self.pc)
|
|
68
|
+
self.pc = nnn
|
|
69
|
+
|
|
70
|
+
# skip if vx == nn
|
|
71
|
+
case (3, _, _, _):
|
|
72
|
+
if self.v[x] == nn:
|
|
73
|
+
self.pc += 2
|
|
74
|
+
|
|
75
|
+
# skip if vx != nn
|
|
76
|
+
case (4, _, _, _):
|
|
77
|
+
if self.v[x] != nn:
|
|
78
|
+
self.pc += 2
|
|
79
|
+
|
|
80
|
+
# skip if vx == vy
|
|
81
|
+
case (5, _, _, 0):
|
|
82
|
+
if self.v[x] == self.v[y]:
|
|
83
|
+
self.pc += 2
|
|
84
|
+
|
|
85
|
+
# set vx
|
|
86
|
+
case (6, _, _, _):
|
|
87
|
+
self.v[x] = nn
|
|
88
|
+
|
|
89
|
+
# add to vx
|
|
90
|
+
case (7, _, _, _):
|
|
91
|
+
self.v[x] += nn
|
|
92
|
+
|
|
93
|
+
# set vx to vy
|
|
94
|
+
case (8, _, _, 0):
|
|
95
|
+
self.v[x] = self.v[y]
|
|
96
|
+
|
|
97
|
+
# set vx to vx OR vy
|
|
98
|
+
case (8, _, _, 1):
|
|
99
|
+
self.v[x] |= self.v[y]
|
|
100
|
+
if self.vf_reset:
|
|
101
|
+
self.v[0xf] = 0
|
|
102
|
+
|
|
103
|
+
# set vx to vx AND vy
|
|
104
|
+
case (8, _, _, 2):
|
|
105
|
+
self.v[x] &= self.v[y]
|
|
106
|
+
if self.vf_reset:
|
|
107
|
+
self.v[0xf] = 0
|
|
108
|
+
|
|
109
|
+
# set vx to vx XOR vy
|
|
110
|
+
case (8, _, _, 3):
|
|
111
|
+
self.v[x] ^= self.v[y]
|
|
112
|
+
if self.vf_reset:
|
|
113
|
+
self.v[0xf] = 0
|
|
114
|
+
|
|
115
|
+
# set vx to vx + vy
|
|
116
|
+
case (8, _, _, 4):
|
|
117
|
+
o = self.v[x]
|
|
118
|
+
self.v[x] += self.v[y]
|
|
119
|
+
self.v[0xf] = 1 if o > self.v[x] else 0
|
|
120
|
+
|
|
121
|
+
# set vx to vx - vy
|
|
122
|
+
case (8, _, _, 5):
|
|
123
|
+
self.v[x] -= self.v[y]
|
|
124
|
+
self.v[0xf] = 1 if self.v[x] >= self.v[y] else 0
|
|
125
|
+
|
|
126
|
+
# right shift
|
|
127
|
+
case (8, _, _, 6):
|
|
128
|
+
if self.cosmac_shift:
|
|
129
|
+
self.v[x] = self.v[y]
|
|
130
|
+
self.v[0xf] = self.v[x] & 0x1
|
|
131
|
+
self.v[x] >>= 1
|
|
132
|
+
|
|
133
|
+
# set vx to vy - vx
|
|
134
|
+
case (8, _, _, 7):
|
|
135
|
+
self.v[x] = self.v[y] - self.v[x]
|
|
136
|
+
self.v[0xf] = 1 if self.v[y] >= self.v[x] else 0
|
|
137
|
+
|
|
138
|
+
# left shift
|
|
139
|
+
case (8, _, _, 14):
|
|
140
|
+
if self.cosmac_shift:
|
|
141
|
+
self.v[x] = self.v[y]
|
|
142
|
+
self.v[0xf] = self.v[x] >> 7
|
|
143
|
+
self.v[x] <<= 1
|
|
144
|
+
|
|
145
|
+
# skip if vx != vy
|
|
146
|
+
case (9, _, _, 0):
|
|
147
|
+
if self.v[x] != self.v[y]:
|
|
148
|
+
self.pc += 2
|
|
149
|
+
|
|
150
|
+
# set index register
|
|
151
|
+
case (10, _, _, _):
|
|
152
|
+
self.i = nnn
|
|
153
|
+
|
|
154
|
+
# jump w/ offset
|
|
155
|
+
case (11, _, _, _):
|
|
156
|
+
self.pc = nnn + self.v[0 if self.cosmac_jump else x]
|
|
157
|
+
|
|
158
|
+
# set vx to random AND nn
|
|
159
|
+
case (12, _, _, _):
|
|
160
|
+
self.v[x] = np.uint8(random.randint(0, 255) & nn)
|
|
161
|
+
|
|
162
|
+
# draw
|
|
163
|
+
case (13, _, _, _):
|
|
164
|
+
sx = self.v[x] & 63
|
|
165
|
+
sy = self.v[y] & 31
|
|
166
|
+
self.v[0xF] = 0
|
|
167
|
+
sbytes = self.ram.mem[self.i : self.i + n]
|
|
168
|
+
sbits = np.unpackbits(sbytes).reshape(n, 8)
|
|
169
|
+
for ri in range(n):
|
|
170
|
+
ty = (sy + ri) % 32
|
|
171
|
+
for ci in range(8):
|
|
172
|
+
tx = (sx + ci) % 64
|
|
173
|
+
pixel = sbits[ri, ci]
|
|
174
|
+
if pixel:
|
|
175
|
+
if self.disp.grid[ty, tx] == 1:
|
|
176
|
+
self.v[0xF] = 1
|
|
177
|
+
self.disp.grid[ty, tx] ^= 1
|
|
178
|
+
|
|
179
|
+
# skip if key vx pressed
|
|
180
|
+
case (14, _, 9, 14):
|
|
181
|
+
if self.kp.is_pressed(self.v[x]):
|
|
182
|
+
self.pc += 2
|
|
183
|
+
|
|
184
|
+
# skip if key vx not pressed
|
|
185
|
+
case (14, _, 10, 1):
|
|
186
|
+
if not self.kp.is_pressed(self.v[x]):
|
|
187
|
+
self.pc += 2
|
|
188
|
+
|
|
189
|
+
# set vx to delay timer
|
|
190
|
+
case (15, _, 0, 7):
|
|
191
|
+
self.v[x] = self.tm.delay
|
|
192
|
+
|
|
193
|
+
# get key
|
|
194
|
+
case (15, _, 0, 10):
|
|
195
|
+
print(f"pkeys: {self.pkeys}, keys: {self.kp.keys}")
|
|
196
|
+
released = None
|
|
197
|
+
for k in range(16):
|
|
198
|
+
if self.pkeys[k] == 1 and self.kp.keys[k] == 0:
|
|
199
|
+
released = k
|
|
200
|
+
break
|
|
201
|
+
if released is not None:
|
|
202
|
+
self.v[x] = released
|
|
203
|
+
else:
|
|
204
|
+
self.pc -= 2
|
|
205
|
+
|
|
206
|
+
# set delay timer to vx
|
|
207
|
+
case (15, _, 1, 5):
|
|
208
|
+
self.tm.set_delay(self.v[x])
|
|
209
|
+
|
|
210
|
+
# set sound timer to vx
|
|
211
|
+
case (15, _, 1, 8):
|
|
212
|
+
self.tm.set_sound(self.v[x])
|
|
213
|
+
|
|
214
|
+
# add to index
|
|
215
|
+
case (15, _, 1, 14):
|
|
216
|
+
self.i += self.v[x]
|
|
217
|
+
if not self.cosmac_i_add:
|
|
218
|
+
if self.i > 0xfff:
|
|
219
|
+
self.v[0xf] = 1
|
|
220
|
+
|
|
221
|
+
# set i to font character
|
|
222
|
+
case (15, _, 2, 9):
|
|
223
|
+
ch = (self.v[x] & 0xf) if self.cosmac_font else self.v[x]
|
|
224
|
+
self.i = 0x50 + (ch * 5)
|
|
225
|
+
|
|
226
|
+
# bcd
|
|
227
|
+
case (15, _, 3, 3):
|
|
228
|
+
self.ram[int(self.i)] = np.uint8(self.v[x] // 100)
|
|
229
|
+
self.ram[int(self.i) + 1] = np.uint8((self.v[x] // 10) % 10)
|
|
230
|
+
self.ram[int(self.i) + 2] = np.uint8(self.v[x] % 10)
|
|
231
|
+
|
|
232
|
+
# store in memory
|
|
233
|
+
case (15, _, 5, 5):
|
|
234
|
+
for j in range(0, x + 1):
|
|
235
|
+
self.ram[int(self.i) + j] = self.v[j]
|
|
236
|
+
if self.cosmac_ls:
|
|
237
|
+
self.i += x + 1
|
|
238
|
+
|
|
239
|
+
# load from memory
|
|
240
|
+
case (15, _, 6, 5):
|
|
241
|
+
for j in range(0, x + 1):
|
|
242
|
+
self.v[j] = self.ram[int(self.i) + j]
|
|
243
|
+
if self.cosmac_ls:
|
|
244
|
+
self.i += x + 1
|
|
245
|
+
|
|
246
|
+
def cycle(self):
|
|
247
|
+
self.execute(self.fetch())
|
pychip8/display.py
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pyray as rl
|
|
3
|
+
|
|
4
|
+
# pychip8 display
|
|
5
|
+
# by las-r
|
|
6
|
+
|
|
7
|
+
# display class
|
|
8
|
+
class Display:
|
|
9
|
+
def __init__(self, scale: int = 12, title: str = "pychip8", fg: int = 0xFFFFFFFF, bg: int = 0x000000FF):
|
|
10
|
+
self.cols = 64
|
|
11
|
+
self.rows = 32
|
|
12
|
+
self.scale = scale
|
|
13
|
+
self.grid = np.zeros((self.rows, self.cols), dtype=np.uint8)
|
|
14
|
+
self.on = rl.get_color(fg)
|
|
15
|
+
self.off = rl.get_color(bg)
|
|
16
|
+
|
|
17
|
+
rl.init_window(self.cols * self.scale, self.rows * self.scale, title)
|
|
18
|
+
rl.set_target_fps(60)
|
|
19
|
+
|
|
20
|
+
def clear(self):
|
|
21
|
+
self.grid.fill(0)
|
|
22
|
+
|
|
23
|
+
def render(self):
|
|
24
|
+
rl.begin_drawing()
|
|
25
|
+
rl.clear_background(self.off)
|
|
26
|
+
|
|
27
|
+
for y in range(self.rows):
|
|
28
|
+
for x in range(self.cols):
|
|
29
|
+
if self.grid[y, x] == 1:
|
|
30
|
+
rl.draw_rectangle(
|
|
31
|
+
x * self.scale,
|
|
32
|
+
y * self.scale,
|
|
33
|
+
self.scale,
|
|
34
|
+
self.scale,
|
|
35
|
+
self.on
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
rl.end_drawing()
|
|
39
|
+
|
|
40
|
+
def deinit(self):
|
|
41
|
+
rl.close_window()
|
pychip8/keypad.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import pyray as rl
|
|
2
|
+
import numpy as np
|
|
3
|
+
|
|
4
|
+
KEYMAP = {
|
|
5
|
+
rl.KeyboardKey.KEY_ONE: 1, rl.KeyboardKey.KEY_TWO: 2, rl.KeyboardKey.KEY_THREE: 3, rl.KeyboardKey.KEY_FOUR: 12,
|
|
6
|
+
rl.KeyboardKey.KEY_Q: 4, rl.KeyboardKey.KEY_W: 5, rl.KeyboardKey.KEY_E: 6, rl.KeyboardKey.KEY_R: 13,
|
|
7
|
+
rl.KeyboardKey.KEY_A: 7, rl.KeyboardKey.KEY_S: 8, rl.KeyboardKey.KEY_D: 9, rl.KeyboardKey.KEY_F: 14,
|
|
8
|
+
rl.KeyboardKey.KEY_Z: 10, rl.KeyboardKey.KEY_X: 0, rl.KeyboardKey.KEY_C: 11, rl.KeyboardKey.KEY_V: 15,
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
class Keypad:
|
|
12
|
+
def __init__(self):
|
|
13
|
+
self.keys = np.zeros(16, dtype=np.uint8)
|
|
14
|
+
|
|
15
|
+
def update(self):
|
|
16
|
+
for raykey, chip8key in KEYMAP.items():
|
|
17
|
+
self.keys[chip8key] = 1 if rl.is_key_down(raykey) else 0
|
|
18
|
+
|
|
19
|
+
def is_pressed(self, key) -> bool:
|
|
20
|
+
return self.keys[int(key)] == 1
|
pychip8/memory.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
|
|
3
|
+
# pychip8 memory
|
|
4
|
+
# by las-r
|
|
5
|
+
|
|
6
|
+
# font
|
|
7
|
+
FONTSET = np.array([
|
|
8
|
+
0xF0, 0x90, 0x90, 0x90, 0xF0, # 0
|
|
9
|
+
0x20, 0x60, 0x20, 0x20, 0x70, # 1
|
|
10
|
+
0xF0, 0x10, 0xF0, 0x80, 0xF0, # 2
|
|
11
|
+
0xF0, 0x10, 0xF0, 0x10, 0xF0, # 3
|
|
12
|
+
0x90, 0x90, 0xF0, 0x10, 0x10, # 4
|
|
13
|
+
0xF0, 0x80, 0xF0, 0x10, 0xF0, # 5
|
|
14
|
+
0xF0, 0x80, 0xF0, 0x90, 0xF0, # 6
|
|
15
|
+
0xF0, 0x10, 0x20, 0x40, 0x40, # 7
|
|
16
|
+
0xF0, 0x90, 0xF0, 0x90, 0xF0, # 8
|
|
17
|
+
0xF0, 0x90, 0xF0, 0x10, 0xF0, # 9
|
|
18
|
+
0xF0, 0x90, 0xF0, 0x90, 0x90, # A
|
|
19
|
+
0xE0, 0x90, 0xE0, 0x90, 0xE0, # B
|
|
20
|
+
0xF0, 0x80, 0x80, 0x80, 0xF0, # C
|
|
21
|
+
0xE0, 0x90, 0x90, 0x90, 0xE0, # D
|
|
22
|
+
0xF0, 0x80, 0xF0, 0x80, 0xF0, # E
|
|
23
|
+
0xF0, 0x80, 0xF0, 0x80, 0x80, # F
|
|
24
|
+
], dtype=np.uint8)
|
|
25
|
+
|
|
26
|
+
# ram class
|
|
27
|
+
class Memory:
|
|
28
|
+
def __init__(self, size: int = 4096):
|
|
29
|
+
self.mem = np.zeros(size, dtype=np.uint8)
|
|
30
|
+
self.mem[0x50:0xa0] = FONTSET
|
|
31
|
+
|
|
32
|
+
def __setitem__(self, loc: int | np.uint16, val: int | np.uint8):
|
|
33
|
+
self.mem[loc] = val
|
|
34
|
+
|
|
35
|
+
def __getitem__(self, loc: int | np.uint16) -> np.uint8:
|
|
36
|
+
return self.mem[loc]
|
|
37
|
+
|
|
38
|
+
def load_rom(self, romb: bytes, start: int = 0x200):
|
|
39
|
+
end = start + len(romb)
|
|
40
|
+
self.mem[start:end] = np.frombuffer(romb, dtype=np.uint8)
|
pychip8/timers.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pyray as rl
|
|
3
|
+
import math
|
|
4
|
+
|
|
5
|
+
class Timers:
|
|
6
|
+
def __init__(self, vol: float = 0.05):
|
|
7
|
+
self.delay = np.uint8(0)
|
|
8
|
+
self.sound = np.uint8(0)
|
|
9
|
+
self.playing = False
|
|
10
|
+
|
|
11
|
+
rl.init_audio_device()
|
|
12
|
+
smprate = 44100
|
|
13
|
+
smps = int(smprate * 0.5)
|
|
14
|
+
freq = 600
|
|
15
|
+
raw = []
|
|
16
|
+
for i in range(smps):
|
|
17
|
+
t = i / smprate
|
|
18
|
+
sample = math.sin(2 * math.pi * freq * t)
|
|
19
|
+
raw.append(sample)
|
|
20
|
+
self.data_buffer = rl.ffi.new(f"float[{smps}]", raw)
|
|
21
|
+
rl.set_audio_stream_buffer_size_default(smps)
|
|
22
|
+
self.stream = rl.load_audio_stream(smprate, 32, 1)
|
|
23
|
+
rl.update_audio_stream(self.stream, self.data_buffer, smps)
|
|
24
|
+
rl.set_audio_stream_volume(self.stream, vol)
|
|
25
|
+
|
|
26
|
+
def set_delay(self, val):
|
|
27
|
+
self.delay = np.uint8(int(val))
|
|
28
|
+
|
|
29
|
+
def set_sound(self, val):
|
|
30
|
+
self.sound = np.uint8(int(val))
|
|
31
|
+
|
|
32
|
+
def update(self):
|
|
33
|
+
if self.delay > 0:
|
|
34
|
+
self.delay -= 1
|
|
35
|
+
if self.sound > 0:
|
|
36
|
+
self.sound -= 1
|
|
37
|
+
self.play_beep()
|
|
38
|
+
else:
|
|
39
|
+
self.stop_beep()
|
|
40
|
+
|
|
41
|
+
def play_beep(self):
|
|
42
|
+
if not self.playing:
|
|
43
|
+
rl.play_audio_stream(self.stream)
|
|
44
|
+
self.playing = True
|
|
45
|
+
if rl.is_audio_stream_processed(self.stream):
|
|
46
|
+
rl.update_audio_stream(self.stream, self.data_buffer, len(self.data_buffer))
|
|
47
|
+
|
|
48
|
+
def stop_beep(self):
|
|
49
|
+
if self.playing:
|
|
50
|
+
rl.stop_audio_stream(self.stream)
|
|
51
|
+
self.playing = False
|
|
52
|
+
|
|
53
|
+
def deinit(self):
|
|
54
|
+
rl.unload_audio_stream(self.stream)
|
|
55
|
+
rl.close_audio_device()
|