Initial implementation

This commit is contained in:
Fabian Mersch 2021-08-09 21:34:14 +02:00
parent 86ebf67e89
commit 0bb9c55ed9
13 changed files with 1185 additions and 0 deletions

2
.gitignore vendored Normal file
View file

@ -0,0 +1,2 @@
.bundle
.DS_Store

7
Gemfile Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 KiB

595
test/cpu_test.rb Normal file
View 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
View file

@ -0,0 +1,3 @@
require "minitest/autorun"
require "chip8"