mirror of
https://github.com/f-mer/chip8
synced 2024-12-25 21:58:45 +01:00
Initial implementation
This commit is contained in:
parent
86ebf67e89
commit
0bb9c55ed9
13 changed files with 1185 additions and 0 deletions
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
.bundle
|
||||
.DS_Store
|
7
Gemfile
Normal file
7
Gemfile
Normal file
|
@ -0,0 +1,7 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
source "https://rubygems.org"
|
||||
|
||||
gem "minitest"
|
||||
gem "rake", "~> 13.0"
|
||||
gem "ruby-sdl2", "~> 0.3.5"
|
17
Gemfile.lock
Normal file
17
Gemfile.lock
Normal file
|
@ -0,0 +1,17 @@
|
|||
GEM
|
||||
remote: https://rubygems.org/
|
||||
specs:
|
||||
minitest (5.14.4)
|
||||
rake (13.0.4)
|
||||
ruby-sdl2 (0.3.5)
|
||||
|
||||
PLATFORMS
|
||||
ruby
|
||||
|
||||
DEPENDENCIES
|
||||
minitest
|
||||
rake (~> 13.0)
|
||||
ruby-sdl2 (~> 0.3.5)
|
||||
|
||||
BUNDLED WITH
|
||||
2.2.15
|
32
README.md
Normal file
32
README.md
Normal file
|
@ -0,0 +1,32 @@
|
|||
# Chip-8
|
||||
|
||||
A [Chip-8](https://en.wikipedia.org/wiki/CHIP-8) [bytecode interpreter](https://en.wikipedia.org/wiki/Interpreter_(computing)#Bytecode_interpreters) written in the [ruby](https://www.ruby-lang.org/) programming language.
|
||||
|
||||
![Screenshot of executing bin/chip8 pong.ch8](static/screenshot.png)
|
||||
|
||||
## Installation
|
||||
|
||||
> :warning: Heads up! This has only been tested on OSX.
|
||||
|
||||
- Install [SDL](https://en.wikipedia.org/wiki/Simple_DirectMedia_Layer) by executing `brew install sdl`. It is used for handling keyboard events and drawing onto the screen.
|
||||
- Clone the repository with the `git clone` command
|
||||
- Navigate into the cloned folder
|
||||
- Execute `bundle` to install required gems
|
||||
|
||||
## Usage
|
||||
|
||||
```
|
||||
$ bin/chip8
|
||||
Usage: chip8 [rom]
|
||||
|
||||
Options:
|
||||
--debug Prints every instruction before executing
|
||||
--dump-memory Loads the rom and dumps the memory content
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
[Minitest](https://github.com/seattlerb/minitest) is used fot testing. Tests can be run by executing `bundle exec rake`.
|
||||
|
||||
## References
|
||||
- [Cowgod's Chip-8 Technical Reference](http://devernay.free.fr/hacks/chip8/C8TECH10.HTM)
|
8
Rakefile
Normal file
8
Rakefile
Normal file
|
@ -0,0 +1,8 @@
|
|||
require "rake/testtask"
|
||||
|
||||
Rake::TestTask.new do |t|
|
||||
t.libs << "test"
|
||||
t.test_files = FileList["test/*_test.rb"]
|
||||
end
|
||||
|
||||
task default: :test
|
89
bin/chip8
Executable file
89
bin/chip8
Executable file
|
@ -0,0 +1,89 @@
|
|||
#!/usr/bin/env ruby
|
||||
|
||||
require "bundler/setup"
|
||||
$LOAD_PATH.unshift("./lib")
|
||||
|
||||
# Adds IO#beep
|
||||
require 'io/console'
|
||||
|
||||
require "chip8"
|
||||
require "chip8/peripheral"
|
||||
|
||||
CYCLES_PER_SECOND = 500
|
||||
SECONDS_PER_CYCLE = 1.0 / CYCLES_PER_SECOND
|
||||
DT_NTH_CYCLE = CYCLES_PER_SECOND / 60
|
||||
DRAW_NTH_CYCLE = CYCLES_PER_SECOND / 30
|
||||
|
||||
HELP = <<~TXT
|
||||
Usage: chip8 [rom]
|
||||
|
||||
Options:
|
||||
--debug Prints every instruction before executing
|
||||
--dump-memory Loads the rom and dumps the memory content
|
||||
TXT
|
||||
|
||||
rom_path, *flags = ARGV
|
||||
|
||||
unless rom_path || flags.include?("--help")
|
||||
abort HELP
|
||||
end
|
||||
|
||||
unless File.exist?(rom_path)
|
||||
abort "ERROR: ROM file does not exist"
|
||||
end
|
||||
|
||||
frame_buffer = Array.new(Chip8::SCREEN_WIDTH * Chip8::SCREEN_HEIGHT, 0)
|
||||
cpu = Chip8::CPU.new(
|
||||
frame_buffer: frame_buffer,
|
||||
beep: -> { $stdout.beep },
|
||||
)
|
||||
peripheral = Chip8::Peripheral.new(
|
||||
frame_buffer: frame_buffer,
|
||||
keydown: cpu.method(:key_pressed),
|
||||
keyup: cpu.method(:key_released),
|
||||
)
|
||||
rom = File.read(rom_path, mode: "rb")
|
||||
cpu.load(rom)
|
||||
|
||||
if flags.include?("--debug")
|
||||
cpu.extend(Module.new do
|
||||
def execute_instruction
|
||||
opcode = fetch_opcode
|
||||
instruction, *operands = decode_opcode(opcode)
|
||||
puts "#{opcode.to_s(16).rjust(4, "0")}: #{instruction} #{operands.join(", ")}"
|
||||
|
||||
super
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
if flags.include?("--dump-memory")
|
||||
abort cpu.memory.each_byte.each_slice(32).map { |slice|
|
||||
slice.map { |byte| byte.to_s(16).rjust(2, "0") }.join(" ")
|
||||
}.join("\n")
|
||||
end
|
||||
|
||||
cycles = 0
|
||||
loop do
|
||||
cycles += 1
|
||||
started = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
||||
cpu.execute_instruction
|
||||
if cycles % DT_NTH_CYCLE == 0
|
||||
cpu.timer_interrupt
|
||||
end
|
||||
peripheral.update
|
||||
if cycles % DRAW_NTH_CYCLE == 0
|
||||
peripheral.draw
|
||||
end
|
||||
ended = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
||||
delta = ended - started
|
||||
|
||||
if delta < SECONDS_PER_CYCLE
|
||||
sleep SECONDS_PER_CYCLE - delta
|
||||
end
|
||||
rescue Chip8::CPU::Error => error
|
||||
puts cpu.memory.each_byte.each_slice(32).map { |slice|
|
||||
slice.map { |byte| byte.to_s(16).rjust(2, "0") }.join(" ")
|
||||
}.join("\n")
|
||||
raise error
|
||||
end
|
6
lib/chip8.rb
Normal file
6
lib/chip8.rb
Normal file
|
@ -0,0 +1,6 @@
|
|||
module Chip8
|
||||
SCREEN_WIDTH = 64
|
||||
SCREEN_HEIGHT = 32
|
||||
end
|
||||
|
||||
require "chip8/cpu"
|
10
lib/chip8/byte_array.rb
Normal file
10
lib/chip8/byte_array.rb
Normal file
|
@ -0,0 +1,10 @@
|
|||
module Chip8
|
||||
class ByteArray < String
|
||||
def initialize(size, default_vaule = 0)
|
||||
super [default_vaule].pack("C*") * size
|
||||
end
|
||||
|
||||
alias :[] :getbyte
|
||||
alias :[]= :setbyte
|
||||
end
|
||||
end
|
370
lib/chip8/cpu.rb
Normal file
370
lib/chip8/cpu.rb
Normal file
|
@ -0,0 +1,370 @@
|
|||
require "set"
|
||||
require "chip8/byte_array"
|
||||
|
||||
module Chip8
|
||||
class CPU
|
||||
FONT = "\xF0\x90\x90\x90\xF0" \
|
||||
"\x20\x60\x20\x20\x70" \
|
||||
"\xF0\x10\xF0\x80\xF0" \
|
||||
"\xF0\x10\xF0\x10\xF0" \
|
||||
"\x90\x90\xF0\x10\x10" \
|
||||
"\xF0\x80\xF0\x10\xF0" \
|
||||
"\xF0\x80\xF0\x90\xF0" \
|
||||
"\xF0\x10\x20\x40\x40" \
|
||||
"\xF0\x90\xF0\x90\xF0" \
|
||||
"\xF0\x90\xF0\x10\xF0" \
|
||||
"\xF0\x90\xF0\x90\x90" \
|
||||
"\xE0\x90\xE0\x90\xE0" \
|
||||
"\xF0\x80\x80\x80\xF0" \
|
||||
"\xE0\x90\x90\x90\xE0" \
|
||||
"\xF0\x80\xF0\x80\xF0" \
|
||||
"\xF0\x80\xF0\x80\x80"
|
||||
MEMORY_START = 0x200
|
||||
|
||||
Error = Class.new(StandardError)
|
||||
|
||||
attr_reader :program_counter, :stack_pointer, :i_register, :delay_timer, :sound_timer
|
||||
|
||||
def memory; @memory.dup.freeze; end
|
||||
def registers; @registers.dup.freeze; end
|
||||
def stack; @stack.dup.freeze; end
|
||||
def frame_buffer; @frame_buffer.dup.freeze; end
|
||||
def pressed_keys; @pressed_keys.dup.freeze; end
|
||||
|
||||
def initialize(**kwargs)
|
||||
reset(**kwargs)
|
||||
end
|
||||
|
||||
def reset(
|
||||
memory: ByteArray.new(4096),
|
||||
program_counter: MEMORY_START,
|
||||
frame_buffer: Array.new(SCREEN_WIDTH * SCREEN_HEIGHT, 0),
|
||||
stack: Array.new,
|
||||
registers: Array.new(16, 0),
|
||||
i_register: 0x000,
|
||||
delay_timer: 0x00,
|
||||
sound_timer: 0x00,
|
||||
pressed_keys: Set.new,
|
||||
beep: -> { }
|
||||
)
|
||||
@memory = memory
|
||||
@program_counter = program_counter
|
||||
@frame_buffer = frame_buffer
|
||||
@stack = stack
|
||||
@registers = registers
|
||||
@i_register = i_register
|
||||
@delay_timer = delay_timer
|
||||
@sound_timer = sound_timer
|
||||
@pressed_keys = pressed_keys
|
||||
@beep = beep
|
||||
|
||||
load(FONT, start_addr: 0x000)
|
||||
end
|
||||
|
||||
def load(bytes, start_addr: MEMORY_START)
|
||||
bytes.each_byte.with_index do |byte, index|
|
||||
@memory[start_addr + index] = byte
|
||||
end
|
||||
end
|
||||
|
||||
def execute_instruction
|
||||
instruction, *operands = decode_opcode(fetch_opcode)
|
||||
send(instruction, *operands)
|
||||
end
|
||||
|
||||
def timer_interrupt
|
||||
@delay_timer -= 1 unless @delay_timer.zero?
|
||||
@sound_timer -= 1 unless @sound_timer.zero?
|
||||
end
|
||||
|
||||
def key_pressed(key)
|
||||
@pressed_keys.add(key)
|
||||
end
|
||||
|
||||
def key_released(key)
|
||||
@pressed_keys.delete(key)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def fetch_opcode
|
||||
@memory[@program_counter] << 8 | @memory[@program_counter + 1]
|
||||
end
|
||||
|
||||
def decode_opcode(opcode)
|
||||
case opcode & 0xF000
|
||||
when 0x0000
|
||||
case opcode & 0x00FF
|
||||
when 0x00E0 then [:cls]
|
||||
when 0x00EE then [:ret]
|
||||
else raise Error, "Unrecognized opcode: 0x#{opcode.to_s(16).rjust(4, "0")}"
|
||||
end
|
||||
when 0x1000 then [:jp_addr, opcode & 0x0FFF]
|
||||
when 0x2000 then [:call_addr, opcode & 0x0FFF]
|
||||
when 0x3000 then [:se_vx_byte, (opcode & 0x0F00) >> 8, opcode & 0x00FF]
|
||||
when 0x4000 then [:sne_vx_byte, (opcode & 0x0F00) >> 8, opcode & 0x00FF]
|
||||
when 0x5000 then [:se_vx_vy, (opcode & 0x0F00) >> 8, (opcode & 0x00F0) >> 4]
|
||||
when 0x6000 then [:ld_vx_byte, (opcode & 0x0F00) >> 8, opcode & 0x00FF]
|
||||
when 0x7000 then [:add_vx_byte, (opcode & 0x0F00) >> 8, opcode & 0x00FF]
|
||||
when 0x8000
|
||||
case opcode & 0x000F
|
||||
when 0x0000 then [:ld_vx_vy, (opcode & 0x0F00) >> 8, (opcode & 0x00F0) >> 4]
|
||||
when 0x0001 then [:or_vx_vy, (opcode & 0x0F00) >> 8, (opcode & 0x00F0) >> 4]
|
||||
when 0x0002 then [:and_vx_vy, (opcode & 0x0F00) >> 8, (opcode & 0x00F0) >> 4]
|
||||
when 0x0003 then [:xor_vx_vy, (opcode & 0x0F00) >> 8, (opcode & 0x00F0) >> 4]
|
||||
when 0x0004 then [:add_vx_vy, (opcode & 0x0F00) >> 8, (opcode & 0x00F0) >> 4]
|
||||
when 0x0005 then [:sub_vx_vy, (opcode & 0x0F00) >> 8, (opcode & 0x00F0) >> 4]
|
||||
when 0x0006 then [:shr_vx_vy, (opcode & 0x0F00) >> 8]
|
||||
when 0x0007 then [:subn_vx_vy, (opcode & 0x0F00) >> 8, (opcode & 0x00F0) >> 4]
|
||||
when 0x000e then [:shl_vx_vy, (opcode & 0x0F00) >> 8]
|
||||
else raise Error, "Unrecognized opcode: #{opcode.to_s(16)}"
|
||||
end
|
||||
when 0x9000 then [:sne_vx_vy, (opcode & 0x0F00) >> 8, (opcode & 0x00F0) >> 4]
|
||||
when 0xA000 then [:ld_i_addr, opcode & 0x0FFF]
|
||||
when 0xB000 then [:jp_v0_addr, opcode & 0x0FFF]
|
||||
when 0xC000 then [:rnd_vx_byte, (opcode & 0x0F00) >> 8, opcode & 0x00FF]
|
||||
when 0xD000 then [:drw_vx_vy_nibble, (opcode & 0x0F00) >> 8, (opcode & 0x00F0) >> 4, opcode & 0x000F]
|
||||
when 0xE000
|
||||
case opcode & 0x00FF
|
||||
when 0x009E then [:skp_vx, (opcode & 0x0F00) >> 8]
|
||||
when 0x00A1 then [:sknp_vx, (opcode & 0x0F00) >> 8]
|
||||
else raise Error, "Unrecognized opcode: #{opcode.to_s(16)}"
|
||||
end
|
||||
when 0xF000
|
||||
case opcode & 0x00FF
|
||||
when 0x0007 then [:ld_vx_dt, (opcode & 0x0F00) >> 8]
|
||||
when 0x000A then [:ld_vx_k, (opcode & 0x0F00) >> 8]
|
||||
when 0x0015 then [:ld_dt_vx, (opcode & 0x0F00) >> 8]
|
||||
when 0x0018 then [:ld_st_vx, (opcode & 0x0F00) >> 8]
|
||||
when 0x001E then [:add_i_vx, (opcode & 0x0F00) >> 8]
|
||||
when 0x0029 then [:ld_f_vx, (opcode & 0x0F00) >> 8]
|
||||
when 0x0033 then [:ld_b_vx, (opcode & 0x0F00) >> 8]
|
||||
when 0x0055 then [:ld_i_vx, (opcode & 0x0F00) >> 8]
|
||||
when 0x0065 then [:ld_vx_i, (opcode & 0x0F00) >> 8]
|
||||
else raise Error, "Unrecognized opcode: #{opcode.to_s(16)}"
|
||||
end
|
||||
else raise Error, "Unrecognized opcode: #{opcode.to_s(16)}"
|
||||
end
|
||||
end
|
||||
|
||||
def next_instruction
|
||||
@program_counter += 2
|
||||
end
|
||||
|
||||
def skip_instruction
|
||||
2.times { next_instruction }
|
||||
end
|
||||
|
||||
def cls
|
||||
@frame_buffer.map! { 0 }
|
||||
next_instruction
|
||||
end
|
||||
|
||||
def ret
|
||||
@program_counter = @stack.pop
|
||||
end
|
||||
|
||||
def jp_addr(addr)
|
||||
@program_counter = addr
|
||||
end
|
||||
|
||||
def call_addr(addr)
|
||||
@stack.push(next_instruction)
|
||||
@program_counter = addr
|
||||
end
|
||||
|
||||
def se_vx_byte(vx, byte)
|
||||
if @registers[vx] == byte
|
||||
skip_instruction
|
||||
else
|
||||
next_instruction
|
||||
end
|
||||
end
|
||||
|
||||
def sne_vx_byte(vx, byte)
|
||||
if @registers[vx] != byte
|
||||
skip_instruction
|
||||
else
|
||||
next_instruction
|
||||
end
|
||||
end
|
||||
|
||||
def se_vx_vy(vx, vy)
|
||||
if @registers[vx] == @registers[vy]
|
||||
skip_instruction
|
||||
else
|
||||
next_instruction
|
||||
end
|
||||
end
|
||||
|
||||
def ld_vx_byte(vx, byte)
|
||||
@registers[vx] = byte
|
||||
next_instruction
|
||||
end
|
||||
|
||||
def add_vx_byte(vx, byte)
|
||||
@registers[vx] = (@registers[vx] + byte) & 0xFF
|
||||
next_instruction
|
||||
end
|
||||
|
||||
def ld_vx_vy(vx, vy)
|
||||
@registers[vx] = @registers[vy]
|
||||
next_instruction
|
||||
end
|
||||
|
||||
def or_vx_vy(vx, vy)
|
||||
@registers[vx] |= @registers[vy]
|
||||
next_instruction
|
||||
end
|
||||
|
||||
def and_vx_vy(vx, vy)
|
||||
@registers[vx] &= @registers[vy]
|
||||
next_instruction
|
||||
end
|
||||
|
||||
def xor_vx_vy(vx, vy)
|
||||
@registers[vx] ^= @registers[vy]
|
||||
next_instruction
|
||||
end
|
||||
|
||||
def add_vx_vy(vx, vy)
|
||||
sum = @registers[vx] + @registers[vy]
|
||||
@registers[0xF] = sum > 0xFF ? 1 : 0
|
||||
@registers[vx] = sum & 0xFF
|
||||
next_instruction
|
||||
end
|
||||
|
||||
def sub_vx_vy(vx, vy)
|
||||
difference = @registers[vx] - @registers[vy]
|
||||
@registers[0xF] = @registers[vx] > @registers[vy] ? 1 : 0
|
||||
@registers[vx] = difference & 0xFF
|
||||
next_instruction
|
||||
end
|
||||
|
||||
def shr_vx_vy(vx)
|
||||
@registers[0xF] = @registers[vx] & 1
|
||||
@registers[vx] >>= 1
|
||||
next_instruction
|
||||
end
|
||||
|
||||
def subn_vx_vy(vx, vy)
|
||||
difference = @registers[vy] - @registers[vx]
|
||||
@registers[0xF] = @registers[vy] > @registers[vx] ? 1 : 0
|
||||
@registers[vx] = difference & 0xFF
|
||||
next_instruction
|
||||
end
|
||||
|
||||
def shl_vx_vy(vx)
|
||||
@registers[0xF] = (@registers[vx] >> 7) & 1
|
||||
@registers[vx] = (@registers[vx] << 1) & 0xFF
|
||||
next_instruction
|
||||
end
|
||||
|
||||
def sne_vx_vy(vx, vy)
|
||||
if @registers[vx] != @registers[vy]
|
||||
skip_instruction
|
||||
else
|
||||
next_instruction
|
||||
end
|
||||
end
|
||||
|
||||
def ld_i_addr(addr)
|
||||
@i_register = addr
|
||||
next_instruction
|
||||
end
|
||||
|
||||
def jp_v0_addr(addr)
|
||||
@program_counter = @registers[0] + addr
|
||||
end
|
||||
|
||||
def rnd_vx_byte(vx, byte)
|
||||
@registers[vx] = rand(0xFF) & byte
|
||||
next_instruction
|
||||
end
|
||||
|
||||
def drw_vx_vy_nibble(vx, vy, nibble)
|
||||
@registers[0xF] = 0
|
||||
nibble.times do |row_index|
|
||||
byte = @memory[@i_register + row_index]
|
||||
y = (@registers[vy] + row_index) % SCREEN_HEIGHT
|
||||
8.times do |column_index|
|
||||
x = (@registers[vx] + column_index) % SCREEN_WIDTH
|
||||
bit = (byte >> (8 - 1 - column_index)) & 1
|
||||
addr = y * SCREEN_WIDTH + x
|
||||
@registers[0xF] |= @frame_buffer[addr] & bit
|
||||
@frame_buffer[addr] ^= bit
|
||||
end
|
||||
end
|
||||
next_instruction
|
||||
end
|
||||
|
||||
def skp_vx(vx)
|
||||
if @pressed_keys.include?(@registers[vx])
|
||||
skip_instruction
|
||||
else
|
||||
next_instruction
|
||||
end
|
||||
end
|
||||
|
||||
def sknp_vx(vx)
|
||||
if @pressed_keys.include?(@registers[vx])
|
||||
next_instruction
|
||||
else
|
||||
skip_instruction
|
||||
end
|
||||
end
|
||||
|
||||
def ld_vx_dt(vx)
|
||||
@registers[vx] = @delay_timer
|
||||
next_instruction
|
||||
end
|
||||
|
||||
def ld_vx_k(vx)
|
||||
return if @pressed_keys.empty?
|
||||
@registers[vx] = @pressed_keys.first
|
||||
next_instruction
|
||||
end
|
||||
|
||||
def ld_dt_vx(vx)
|
||||
@delay_timer = @registers[vx]
|
||||
next_instruction
|
||||
end
|
||||
|
||||
def ld_st_vx(vx)
|
||||
@beep.call
|
||||
next_instruction
|
||||
end
|
||||
|
||||
def add_i_vx(vx)
|
||||
@i_register += @registers[vx]
|
||||
next_instruction
|
||||
end
|
||||
|
||||
def ld_f_vx(vx)
|
||||
@i_register = @registers[vx] * 5
|
||||
next_instruction
|
||||
end
|
||||
|
||||
def ld_b_vx(vx)
|
||||
ones, tens, hundreds = @registers[vx].digits
|
||||
@memory[@i_register] = hundreds || 0
|
||||
@memory[@i_register + 1] = tens || 0
|
||||
@memory[@i_register + 2] = ones
|
||||
next_instruction
|
||||
end
|
||||
|
||||
def ld_i_vx(vx)
|
||||
(0..vx).each do |v|
|
||||
@memory[@i_register + v] = @registers[v]
|
||||
end
|
||||
next_instruction
|
||||
end
|
||||
|
||||
def ld_vx_i(vx)
|
||||
(0..vx).each do |v|
|
||||
@registers[v] = @memory[@i_register + v]
|
||||
end
|
||||
next_instruction
|
||||
end
|
||||
end
|
||||
end
|
46
lib/chip8/peripheral.rb
Normal file
46
lib/chip8/peripheral.rb
Normal file
|
@ -0,0 +1,46 @@
|
|||
require "sdl2"
|
||||
|
||||
module Chip8
|
||||
class Peripheral
|
||||
COLOR_WHITE = [0xff, 0xff, 0xff].freeze
|
||||
COLOR_BLACK = [0x00, 0x00, 0x00].freeze
|
||||
|
||||
def initialize(frame_buffer:, scale: 16, keydown:, keyup:)
|
||||
SDL2.init(SDL2::INIT_VIDEO | SDL2::INIT_EVENTS)
|
||||
@frame_buffer = frame_buffer
|
||||
@keydown = keydown
|
||||
@keyup = keyup
|
||||
@window = SDL2::Window.create("chip8", SDL2::Window::POS_CENTERED, SDL2::Window::POS_CENTERED, 64 * scale, 32 * scale, 0)
|
||||
@renderer = @window.create_renderer(-1, 0)
|
||||
@renderer.scale = [scale, scale]
|
||||
end
|
||||
|
||||
def [](x, y)
|
||||
@frame_buffer[y * 64 + x]
|
||||
end
|
||||
|
||||
def draw
|
||||
@renderer.draw_color = COLOR_WHITE
|
||||
@renderer.clear
|
||||
Chip8::SCREEN_HEIGHT.times do |y|
|
||||
Chip8::SCREEN_WIDTH.times do |x|
|
||||
@renderer.draw_color = self[x, y] == 0 ? COLOR_WHITE : COLOR_BLACK
|
||||
@renderer.draw_point(x, y)
|
||||
end
|
||||
end
|
||||
@renderer.present
|
||||
end
|
||||
|
||||
def update
|
||||
while event = SDL2::Event.poll
|
||||
case event
|
||||
when SDL2::Event::KeyDown
|
||||
@keydown.call(SDL2::Key::Scan.name_of(event.scancode).hex)
|
||||
exit if event.scancode == SDL2::Key::Scan::ESCAPE
|
||||
when SDL2::Event::KeyUp
|
||||
@keyup.call(SDL2::Key::Scan.name_of(event.scancode).hex)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
BIN
static/screenshot.png
Normal file
BIN
static/screenshot.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 339 KiB |
595
test/cpu_test.rb
Normal file
595
test/cpu_test.rb
Normal file
|
@ -0,0 +1,595 @@
|
|||
require "test_helper"
|
||||
|
||||
class CPUTest < Minitest::Test
|
||||
def test_cls
|
||||
frame_buffer = Array.new(Chip8::SCREEN_WIDTH * Chip8::SCREEN_HEIGHT, 1)
|
||||
cpu = Chip8::CPU.new(frame_buffer: frame_buffer)
|
||||
cpu.load("\x00\xE0")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert cpu.frame_buffer.all?(&:zero?)
|
||||
assert_equal 0x202, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_ret
|
||||
stack = [0xFFF]
|
||||
cpu = Chip8::CPU.new(stack: stack)
|
||||
cpu.load("\x00\xEE")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 0xFFF, cpu.program_counter
|
||||
assert_equal 0, cpu.stack.size
|
||||
end
|
||||
|
||||
def test_jp_addr
|
||||
cpu = Chip8::CPU.new
|
||||
cpu.load("\x12\x22")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 0x222, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_call_addr
|
||||
cpu = Chip8::CPU.new
|
||||
cpu.load("\x20\x95")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 0x95, cpu.program_counter
|
||||
assert_equal [0x202], cpu.stack
|
||||
end
|
||||
|
||||
def test_se_vx_byte_equal
|
||||
registers = Array.new(16, 0)
|
||||
registers[0xA] = 7
|
||||
cpu = Chip8::CPU.new(registers: registers)
|
||||
cpu.load("\x3A\x07")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 0x204, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_se_vx_byte_not_equal
|
||||
registers = Array.new(16, 0)
|
||||
registers[0xA] = 7
|
||||
cpu = Chip8::CPU.new(registers: registers)
|
||||
cpu.load("\x3A\x06")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 0x202, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_sne_vx_byte_not_equal
|
||||
registers = Array.new(16, 0)
|
||||
registers[0xA] = 7
|
||||
cpu = Chip8::CPU.new(registers: registers)
|
||||
cpu.load("\x4A\x06")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 0x204, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_sne_vx_byte_equal
|
||||
registers = Array.new(16, 0)
|
||||
registers[0xA] = 7
|
||||
cpu = Chip8::CPU.new(registers: registers)
|
||||
cpu.load("\x4A\x07")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 0x202, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_se_vx_vy_equal
|
||||
registers = Array.new(16, 0)
|
||||
registers[0xA] = 7
|
||||
registers[0xB] = 7
|
||||
cpu = Chip8::CPU.new(registers: registers)
|
||||
cpu.load("\x5A\xB0")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 0x204, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_se_vx_vy_not_equal
|
||||
registers = Array.new(16, 0)
|
||||
registers[0xA] = 7
|
||||
registers[0xB] = 6
|
||||
cpu = Chip8::CPU.new(registers: registers)
|
||||
cpu.load("\x5A\xb0")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 0x202, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_ld_vx_byte
|
||||
cpu = Chip8::CPU.new
|
||||
cpu.load("\x6A\xFF")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 0xFF, cpu.registers[0xA]
|
||||
assert_equal 0x202, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_add_vx_byte_without_carry
|
||||
registers = Array.new(16, 0)
|
||||
registers[0xA] = 3
|
||||
cpu = Chip8::CPU.new(registers: registers)
|
||||
cpu.load("\x7A\x04")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 7, cpu.registers[0xA]
|
||||
assert_equal 0x202, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_add_vx_byte_with_carry
|
||||
registers = Array.new(16, 0)
|
||||
registers[0xA] = 3
|
||||
cpu = Chip8::CPU.new(registers: registers)
|
||||
cpu.load("\x7A\xFF")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 2, cpu.registers[0xA]
|
||||
assert_equal 0x202, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_ld_vx_vy
|
||||
registers = Array.new(16, 0)
|
||||
registers[0xA] = 7
|
||||
cpu = Chip8::CPU.new(registers: registers)
|
||||
cpu.load("\x8B\xA0")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 0x07, cpu.registers[0xB]
|
||||
assert_equal 0x202, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_or_vx_vy
|
||||
registers = Array.new(16, 0)
|
||||
registers[0xA] = 0b0000_1010
|
||||
registers[0xB] = 0b0000_1100
|
||||
cpu = Chip8::CPU.new(registers: registers)
|
||||
cpu.load("\x8A\xB1")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 0b0000_1110, cpu.registers[0xA]
|
||||
assert_equal 0x202, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_and_vx_vy
|
||||
registers = Array.new(16, 0)
|
||||
registers[0xA] = 0b0000_1010
|
||||
registers[0xB] = 0b0000_1100
|
||||
cpu = Chip8::CPU.new(registers: registers)
|
||||
cpu.load("\x8A\xB2")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 0b0000_1000, cpu.registers[0xA]
|
||||
assert_equal 0x202, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_xor_vx_vy
|
||||
registers = Array.new(16, 0)
|
||||
registers[0xA] = 0b0000_1010
|
||||
registers[0xB] = 0b0000_1100
|
||||
cpu = Chip8::CPU.new(registers: registers)
|
||||
cpu.load("\x8A\xB3")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 0b0000_0110, cpu.registers[0xA]
|
||||
assert_equal 0x202, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_add_vx_vy_without_carry
|
||||
registers = Array.new(16, 0)
|
||||
registers[0xA] = 3
|
||||
registers[0xB] = 4
|
||||
cpu = Chip8::CPU.new(registers: registers)
|
||||
cpu.load("\x8A\xB4")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 7, cpu.registers[0xA]
|
||||
assert_equal 0, cpu.registers[0xF]
|
||||
assert_equal 0x202, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_add_vx_vy_with_carry
|
||||
registers = Array.new(16, 0)
|
||||
registers[0xA] = 0xFF
|
||||
registers[0xB] = 1
|
||||
cpu = Chip8::CPU.new(registers: registers)
|
||||
cpu.load("\x8A\xB4")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 0, cpu.registers[0xA]
|
||||
assert_equal 1, cpu.registers[0xF]
|
||||
assert_equal 0x202, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_sub_vx_vy_without_carry
|
||||
registers = Array.new(16, 0)
|
||||
registers[0xA] = 3
|
||||
registers[0xB] = 4
|
||||
cpu = Chip8::CPU.new(registers: registers)
|
||||
cpu.load("\x8A\xB5")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 255, cpu.registers[0xA]
|
||||
assert_equal 0, cpu.registers[0xF]
|
||||
assert_equal 0x202, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_sub_vx_vy_with_carry
|
||||
registers = Array.new(16, 0)
|
||||
registers[0xA] = 4
|
||||
registers[0xB] = 3
|
||||
cpu = Chip8::CPU.new(registers: registers)
|
||||
cpu.load("\x8A\xB5")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 1, cpu.registers[0xA]
|
||||
assert_equal 1, cpu.registers[0xF]
|
||||
assert_equal 0x202, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_shr_vx_vy_without_carry
|
||||
registers = Array.new(16, 0)
|
||||
registers[0xA] = 0b10
|
||||
cpu = Chip8::CPU.new(registers: registers)
|
||||
cpu.load("\x8A\xB6")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 1, cpu.registers[0xA]
|
||||
assert_equal 0, cpu.registers[0xF]
|
||||
assert_equal 0x202, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_shr_vx_vy_with_carry
|
||||
registers = Array.new(16, 0)
|
||||
registers[0xA] = 0b11
|
||||
cpu = Chip8::CPU.new(registers: registers)
|
||||
cpu.load("\x8A\xB6")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 1, cpu.registers[0xA]
|
||||
assert_equal 1, cpu.registers[0xF]
|
||||
assert_equal 0x202, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_subn_vx_vy_without_carry
|
||||
registers = Array.new(16, 0)
|
||||
registers[0xA] = 4
|
||||
registers[0xB] = 3
|
||||
cpu = Chip8::CPU.new(registers: registers)
|
||||
cpu.load("\x8A\xB7")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 255, cpu.registers[0xA]
|
||||
assert_equal 0, cpu.registers[0xF]
|
||||
assert_equal 0x202, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_subn_vx_vy_with_carry
|
||||
registers = Array.new(16, 0)
|
||||
registers[0xA] = 3
|
||||
registers[0xB] = 4
|
||||
cpu = Chip8::CPU.new(registers: registers)
|
||||
cpu.load("\x8A\xB7")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 1, cpu.registers[0xA]
|
||||
assert_equal 1, cpu.registers[0xF]
|
||||
assert_equal 0x202, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_shl_vx_vy_without_carry
|
||||
registers = Array.new(16, 0)
|
||||
registers[0xA] = 0b0100_0000
|
||||
cpu = Chip8::CPU.new(registers: registers)
|
||||
cpu.load("\x8A\xBE")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 0b1000_0000, cpu.registers[0xA]
|
||||
assert_equal 0, cpu.registers[0xF]
|
||||
assert_equal 0x202, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_shl_vx_vy_with_carry
|
||||
registers = Array.new(16, 0)
|
||||
registers[0xA] = 0b1000_0000
|
||||
cpu = Chip8::CPU.new(registers: registers)
|
||||
cpu.load("\x8A\xBE")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 0, cpu.registers[0xA]
|
||||
assert_equal 1, cpu.registers[0xF]
|
||||
assert_equal 0x202, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_sne_vx_vy_not_equal
|
||||
registers = Array.new(16, 0)
|
||||
registers[0xA] = 7
|
||||
registers[0xB] = 3
|
||||
cpu = Chip8::CPU.new(registers: registers)
|
||||
cpu.load("\x9A\xB0")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 0x204, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_sne_vx_vy_equal
|
||||
registers = Array.new(16, 0)
|
||||
registers[0xA] = 3
|
||||
registers[0xB] = 3
|
||||
cpu = Chip8::CPU.new(registers: registers)
|
||||
cpu.load("\x9A\xB0")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 0x202, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_ld_i_addr
|
||||
cpu = Chip8::CPU.new
|
||||
cpu.load("\xAF\xAB")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 0xFAB, cpu.i_register
|
||||
assert_equal 0x202, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_jp_v0_addr
|
||||
registers = Array.new(16, 0)
|
||||
registers[0] = 40
|
||||
cpu = Chip8::CPU.new(registers: registers)
|
||||
cpu.load("\xB0\x02")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 42, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_rnd_vx_byte
|
||||
cpu = Chip8::CPU.new
|
||||
cpu.load("\xCA\x0F")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 0, cpu.registers[0xA] & 0xF0
|
||||
assert_equal 0x202, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_drw_vx_vy_nibble
|
||||
registers = Array.new(16, 0)
|
||||
registers[0xA] = 1
|
||||
registers[0xB] = 1
|
||||
cpu = Chip8::CPU.new(registers: registers, i_register: 75)
|
||||
cpu.load("\xDA\xB5")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
[
|
||||
[0, 0, 0, 0, 0, 0],
|
||||
[0, 1, 1, 1, 1, 0],
|
||||
[0, 1, 0, 0, 0, 0],
|
||||
[0, 1, 1, 1, 1, 0],
|
||||
[0, 1, 0, 0, 0, 0],
|
||||
[0, 1, 0, 0, 0, 0],
|
||||
[0, 0, 0, 0, 0, 0],
|
||||
].each_with_index do |expected, row|
|
||||
start_addr = row * Chip8::SCREEN_WIDTH
|
||||
assert_equal expected, cpu.frame_buffer[start_addr...start_addr + expected.size]
|
||||
end
|
||||
assert_equal 0, cpu.registers[0xF]
|
||||
assert_equal 0x202, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_skp_vx_key_pressed
|
||||
cpu = Chip8::CPU.new(registers: [0xA])
|
||||
cpu.load("\xE0\x9E")
|
||||
|
||||
cpu.key_pressed(0xA)
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 0x204, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_skp_vx_kye_not_pressed
|
||||
cpu = Chip8::CPU.new(registers: [0xA])
|
||||
cpu.load("\xE0\x9E")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 0x202, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_sknp_vx_key_pressed
|
||||
cpu = Chip8::CPU.new(registers: [0xA])
|
||||
cpu.load("\xE0\xA1")
|
||||
|
||||
cpu.key_pressed(0xA)
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 0x202, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_sknp_vx_kye_not_pressed
|
||||
cpu = Chip8::CPU.new(registers: [0xA])
|
||||
cpu.load("\xE0\xA1")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 0x204, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_drw_vx_vy_nibble_vf
|
||||
cpu = Chip8::CPU.new
|
||||
cpu.load("\xDA\xB5\xDA\xB5")
|
||||
|
||||
cpu.execute_instruction
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 1, cpu.registers[0xF]
|
||||
end
|
||||
|
||||
def test_ld_vx_dt
|
||||
cpu = Chip8::CPU.new(delay_timer: 3)
|
||||
cpu.load("\xFA\x07")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 3, cpu.registers[0xA]
|
||||
assert_equal 0x202, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_ld_vx_k_key_pressed
|
||||
cpu = Chip8::CPU.new
|
||||
cpu.load("\xFA\x0A")
|
||||
|
||||
cpu.key_pressed(0xB)
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 0xB, cpu.registers[0xA]
|
||||
assert_equal 0x202, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_ld_vx_k_no_key_pressed
|
||||
cpu = Chip8::CPU.new
|
||||
cpu.load("\xFA\x0A")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 0, cpu.registers[0xA]
|
||||
assert_equal 0x200, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_ld_dt_vx
|
||||
cpu = Chip8::CPU.new(registers: [500])
|
||||
cpu.load("\xF0\x15")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 500, cpu.delay_timer
|
||||
assert_equal 0x202, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_add_i_vx
|
||||
cpu = Chip8::CPU.new(i_register: 1000, registers: [500])
|
||||
cpu.load("\xF0\x1E")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 1500, cpu.i_register
|
||||
assert_equal 0x202, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_ld_f_vx
|
||||
cpu = Chip8::CPU.new(registers: [0xF])
|
||||
cpu.load("\xF0\x29")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 75, cpu.i_register
|
||||
assert_equal 0x202, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_ld_b_vx
|
||||
cpu = Chip8::CPU.new(registers: [213], i_register: 0x300)
|
||||
cpu.load("\xF0\x33")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 2, cpu.memory[0x300]
|
||||
assert_equal 1, cpu.memory[0x301]
|
||||
assert_equal 3, cpu.memory[0x302]
|
||||
assert_equal 0x202, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_ld_i_vx
|
||||
cpu = Chip8::CPU.new(registers: [1, 2, 3, 4, 5, 4], i_register: 0x300)
|
||||
cpu.load("\xF5\x55")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 1, cpu.memory[0x300]
|
||||
assert_equal 2, cpu.memory[0x301]
|
||||
assert_equal 3, cpu.memory[0x302]
|
||||
assert_equal 4, cpu.memory[0x303]
|
||||
assert_equal 5, cpu.memory[0x304]
|
||||
assert_equal 0x202, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_ld_vx_i
|
||||
cpu = Chip8::CPU.new(registers: [0, 0, 0, 0, 0, 4], i_register: 0x300)
|
||||
cpu.load("\x01\x02\x03\x04\x05", start_addr: 0x300)
|
||||
cpu.load("\xF5\x65")
|
||||
|
||||
cpu.execute_instruction
|
||||
|
||||
assert_equal 1, cpu.registers[0x0]
|
||||
assert_equal 2, cpu.registers[0x1]
|
||||
assert_equal 3, cpu.registers[0x2]
|
||||
assert_equal 4, cpu.registers[0x3]
|
||||
assert_equal 5, cpu.registers[0x4]
|
||||
assert_equal 0x202, cpu.program_counter
|
||||
end
|
||||
|
||||
def test_key_pressed
|
||||
cpu = Chip8::CPU.new
|
||||
|
||||
cpu.key_pressed 0xA
|
||||
|
||||
assert cpu.pressed_keys.include?(0xA)
|
||||
end
|
||||
|
||||
def test_key_released
|
||||
cpu = Chip8::CPU.new(pressed_keys: Set[0xA])
|
||||
|
||||
cpu.key_released 0xA
|
||||
|
||||
assert cpu.pressed_keys.empty?
|
||||
end
|
||||
|
||||
def test_timer_interrupt_delay_timer_is_0
|
||||
cpu = Chip8::CPU.new(delay_timer: 0)
|
||||
|
||||
cpu.timer_interrupt
|
||||
|
||||
assert_equal 0, cpu.delay_timer
|
||||
end
|
||||
|
||||
def test_timer_interrupt_delay_timer_greater_0
|
||||
cpu = Chip8::CPU.new(delay_timer: 3)
|
||||
|
||||
cpu.timer_interrupt
|
||||
|
||||
assert_equal 2, cpu.delay_timer
|
||||
end
|
||||
end
|
3
test/test_helper.rb
Normal file
3
test/test_helper.rb
Normal file
|
@ -0,0 +1,3 @@
|
|||
require "minitest/autorun"
|
||||
|
||||
require "chip8"
|
Loading…
Reference in a new issue