diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..97dbb82 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.bundle +.DS_Store diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..9c0f924 --- /dev/null +++ b/Gemfile @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +source "https://rubygems.org" + +gem "minitest" +gem "rake", "~> 13.0" +gem "ruby-sdl2", "~> 0.3.5" diff --git a/Gemfile.lock b/Gemfile.lock new file mode 100644 index 0000000..deaf861 --- /dev/null +++ b/Gemfile.lock @@ -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 diff --git a/README.md b/README.md new file mode 100644 index 0000000..cce7e3f --- /dev/null +++ b/README.md @@ -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) diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..f905b73 --- /dev/null +++ b/Rakefile @@ -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 diff --git a/bin/chip8 b/bin/chip8 new file mode 100755 index 0000000..034da81 --- /dev/null +++ b/bin/chip8 @@ -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 diff --git a/lib/chip8.rb b/lib/chip8.rb new file mode 100644 index 0000000..17e5629 --- /dev/null +++ b/lib/chip8.rb @@ -0,0 +1,6 @@ +module Chip8 + SCREEN_WIDTH = 64 + SCREEN_HEIGHT = 32 +end + +require "chip8/cpu" diff --git a/lib/chip8/byte_array.rb b/lib/chip8/byte_array.rb new file mode 100644 index 0000000..f1fd572 --- /dev/null +++ b/lib/chip8/byte_array.rb @@ -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 diff --git a/lib/chip8/cpu.rb b/lib/chip8/cpu.rb new file mode 100644 index 0000000..0e6a4fa --- /dev/null +++ b/lib/chip8/cpu.rb @@ -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 diff --git a/lib/chip8/peripheral.rb b/lib/chip8/peripheral.rb new file mode 100644 index 0000000..a0122a8 --- /dev/null +++ b/lib/chip8/peripheral.rb @@ -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 diff --git a/static/screenshot.png b/static/screenshot.png new file mode 100644 index 0000000..c2178d7 Binary files /dev/null and b/static/screenshot.png differ diff --git a/test/cpu_test.rb b/test/cpu_test.rb new file mode 100644 index 0000000..9dd6976 --- /dev/null +++ b/test/cpu_test.rb @@ -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 diff --git a/test/test_helper.rb b/test/test_helper.rb new file mode 100644 index 0000000..043f81b --- /dev/null +++ b/test/test_helper.rb @@ -0,0 +1,3 @@ +require "minitest/autorun" + +require "chip8"