chip8-py 0.1.1__tar.gz

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/LICENSE ADDED
@@ -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,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,57 @@
1
+ # pychip8
2
+ A modular, extensible CHIP-8 emulator ecosystem written in modern Python.
3
+
4
+ > **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.
5
+
6
+ ## Installation
7
+ Requires Python 3.11+.
8
+
9
+ ```sh
10
+ git clone https://github.com/las-r/pychip8
11
+ cd pychip8
12
+ pip install -e .
13
+ ```
14
+
15
+ ## Usage
16
+ ```sh
17
+ pychip8 path/to/rom.ch8
18
+ ```
19
+
20
+ ## Config
21
+ ### Flags
22
+ Flags override the config file.
23
+
24
+ | Flag | Description |
25
+ |---|---|
26
+ | `--cpf <int>` | Cycles per frame (default: 10) |
27
+ | `--scale <int>` | Display scale (default: 12) |
28
+ | `--volume <float>` | Audio volume 0.0–1.0 (default: 0.2) |
29
+ | `--cosmac-shift / --no-cosmac-shift` | COSMAC VIP shift quirk |
30
+ | `--cosmac-jump / --no-cosmac-jump` | COSMAC VIP jump quirk |
31
+ | `--cosmac-i-add / --no-cosmac-i-add` | COSMAC VIP index add quirk |
32
+ | `--cosmac-font / --no-cosmac-font` | COSMAC VIP font quirk |
33
+ | `--cosmac-ls / --no-cosmac-ls` | COSMAC VIP load/store quirk |
34
+ | `--vf-reset / --no-vf-reset` | Reset VF after logic instructions |
35
+
36
+ ### File
37
+ On first run, a default config is created at:
38
+ - **Windows:** `C:/Users/YOUR_USER_PROFILE/AppData/Local/pychip8/`
39
+ - **macOS:** `~/Library/Application Support/pychip8/`
40
+ - **Linux:** `~/.config/pychip8/`
41
+
42
+ Edit this file to set your preferred defaults.
43
+
44
+ ## Keypad
45
+ The CHIP-8 hex keypad maps to the left side of a QWERTY keyboard:
46
+ ```
47
+ CHIP-8 Keyboard
48
+ 1 2 3 C 1 2 3 4
49
+ 4 5 6 D Q W E R
50
+ 7 8 9 E A S D F
51
+ A 0 B F Z X C V
52
+ ```
53
+
54
+ ## Credits
55
+ - [raylib](https://www.raylib.com/) ([Python](https://github.com/electronstudio/raylib-python-cffi/))
56
+ - [Guide to making a CHIP-8 emulator](https://tobiasvl.github.io/blog/write-a-chip-8-emulator/)
57
+ - [Timendus' CHIP-8 Test Suite](https://github.com/Timendus/chip8-test-suite)
@@ -0,0 +1,27 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "chip8-py"
7
+ version = "0.1.1"
8
+ description = "A CHIP-8 emulator ecosystem."
9
+ authors = [{ name = "las-r" }]
10
+ readme = "README.md"
11
+ license = { text = "MIT" }
12
+ requires-python = ">=3.11"
13
+ dependencies = [
14
+ "numpy",
15
+ "platformdirs",
16
+ "raylib",
17
+ ]
18
+
19
+ [project.scripts]
20
+ pychip8 = "pychip8.__main__:main"
21
+
22
+ [tool.setuptools.packages.find]
23
+ where = ["src"]
24
+ include = ["pychip8*"]
25
+
26
+ [tool.setuptools.package-data]
27
+ pychip8 = ["config.toml"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,17 @@
1
+ LICENSE
2
+ README.md
3
+ pyproject.toml
4
+ src/chip8_py.egg-info/PKG-INFO
5
+ src/chip8_py.egg-info/SOURCES.txt
6
+ src/chip8_py.egg-info/dependency_links.txt
7
+ src/chip8_py.egg-info/entry_points.txt
8
+ src/chip8_py.egg-info/requires.txt
9
+ src/chip8_py.egg-info/top_level.txt
10
+ src/pychip8/__init__.py
11
+ src/pychip8/__main__.py
12
+ src/pychip8/config.toml
13
+ src/pychip8/cpu.py
14
+ src/pychip8/display.py
15
+ src/pychip8/keypad.py
16
+ src/pychip8/memory.py
17
+ src/pychip8/timers.py
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pychip8 = pychip8.__main__:main
@@ -0,0 +1,3 @@
1
+ numpy
2
+ platformdirs
3
+ raylib
@@ -0,0 +1 @@
1
+ pychip8
File without changes
@@ -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()
@@ -0,0 +1,15 @@
1
+ [cpu]
2
+ cpf = 10
3
+ cosmac_shift = false
4
+ cosmac_jump = false
5
+ cosmac_i_add = false
6
+ cosmac_font = true
7
+ cosmac_ls = false
8
+ vf_reset = true
9
+
10
+ [display]
11
+ scale = 12
12
+ title = "pychip8"
13
+
14
+ [audio]
15
+ volume = 0.05
@@ -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())
@@ -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()
@@ -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
@@ -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)
@@ -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()