[QUIZ] Chip-8 (#88)

The three rules of Ruby Quiz:

1. Please do not post any solutions or spoiler discussion for this quiz until
48 hours have passed from the time on this message.

2. Support Ruby Quiz by submitting ideas as often as you can:

http://www.rubyquiz.com/

3. Enjoy!

Suggestion: A [QUIZ] in the subject of emails about the problem helps everyone
on Ruby Talk follow the discussion. Please reply to the original quiz message,
if you can.

···

-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=

by Ethan Price

CHIP-8 was an interpreted language used in the 1970's for basic games like Pong.
While it is technically an interpreted language, it defines many components a
real computer has, such as registers and a processor. Today, like many other
gaming consoles, it is kept alive by emulators allowing it to be played on most
any modern computer. Emulation is where raw operation codes (the most low-level
way of feeding instructions to a computer) are translated by another computer,
giving a totally different system the ability to run programs not designed for
it.

Your job is to make an emulator. It will only cover some basic parts of it, but
all emulators have to start somewhere. I have written a simple program, and will
give you a list of all the opcodes I used and what they are supposed to do. We
won't worry about graphics right now, just simple operations. Four things need
to be done:

  1. Make the emulator read the file. All opcodes are four digit hex numbers,
     but those will need to be split.
  2. Set up the registers and other assorted things so they can be modified.
  3. Interpret the function of the opcode in Ruby.
  4. Dump all the registers so you can check and make sure everything went well.

Like I said, all opcodes are four digit long hex numbers. So you read four hex
digits worth of data, process it, then move on to the next four digits. There
are 16 registers (areas where data can be stored). They are named V0 through VF,
and are all eight bits wide. VF is usually used as a carry for math operations.

Here is the list of opcodes you will need to interpret:

  NNN is an address
  KK is an 8 bit constant
  X and Y are two 4 bits constants
  
  0000 This is nothing. If you see this, it should mean that you are at the end
       of the file and have nothing left to read, so you therefore need to exit
       gracefully.
  
  1NNN Jump to the address NNN of the file
  
  3XKK Skip next instruction if VX == KK
  
  6XKK VX = KK
  
  7XKK VX = VX + KK (Note: The documentation does not mention this, but I would
                           assume that VF would act as a carry if needed. The
                           included program will not need a carry for this
                           function, so providing for one is purely optional.)
  
  8XY0 VX = VY
  
  8XY1 VX = VX OR VY
  
  8XY2 VX = VX AND VY
  
  8XY3 VX = VX XOR VY
  
  8XY4 VX = VX + VY, Sets VF to 1 if there is a carry, 0 if there isn't
  
  8XY5 VX = VX - VY, VF is set to 0 if there is a borrow, 1 if there isn't.
  
  8X06 VX = VX SHIFT RIGHT 1 (VX=VX/2), VF is set to the value of the least
                                        significant bit of VX before the shift.
  
  8XY7 VX = VY - VX, VF is set to 0 if there is a borrow, 1 if there isn't.
  
  8X0E VX = VX SHIFT LEFT 1 (VX=VX*2), VF is set to the value of the most
                                       significant bit of VX before the shift.
  
  CXKK VX = Random number AND KK

Lets explain how to read these opcodes. Take opcode 1NNN for example. Since the
1NNN starts with 1, we know that no matter what comes after it we will be doing
a jump instruction. Same with the other codes. For the opcode 8XY2, we know if
an opcode starts with 8 and ends in 2 it will be the operation "VX = VX AND VY".

The variables NNN, KK, X and Y are a little different. They can represent any
hex number (0-F), and also are used to note how big of chunks to read them in.
For NNN, you will want to read those as a three digit hex number. Lets do an
example opcode (in hex): 1234. Since it starts with 1, we know it is a "Jump to
address NNN" command. The three numbers after that (the NNN part), are 234, so
we know to jump to location 234 in the file (this is a hex number, not decimal).
KK and X/Y are similar, just for different sized chunks.

While I'm on the subject, let's talk about addresses in detail. Your current
address in the file is how many bytes you are from the beginning of the file. So
if I am at address 008 in the file, I am eight bytes away from the beginning of
the file, and therefore the next byte I read will be byte nine. Say we had the
opcode 100F, that means we would jump to after byte 16 (it is a hex remember).

If your wondering how the carries work, here are some examples. First off,
addition. Say we add these two numbers (opcode 8xy4):

  11111111 <--This is VX
  +
  00000001 <--This is VY

Obviously this would equal 100000000, but that is too big for one register. So
what we do is make VF equal 00000001, and then roll over to 00000000. Sort of
like an old car odometer. When it hits 99,999 miles, it goes back to 00,000
miles, except we are keeping track of the fact that it rolled over. Subtraction
is similar (opcode 8xy5):

  00000000 <-- This is VX
  -
  00000001 <-- This is VY

Since the registers can only deal with positive numbers, we have to keep it
positive. So what we do is "borrow", making VX equal to 100000000. All of a
sudden our subtraction works:

  100000000 <--VX
  -
  00000001 <--VY
  =
  11111111

If we do this though, we have to make sure VF is zero to indicate that a borrow
was needed. If a borrow was not needed, VF gets set to 00000001, since you
didn't need the extra.

Now for shifts. To sum it up, if you shift left, the far left number gets taken
away, and a zero added to the far right. So 10101111 would become 01011110,
because we got rid of the far left number and added a zero to the far right.
Shift right is the same except going the other way (get rid of far right number,
add 0 to the left). As you can see, a number disappears when you shift. VF is
set to the number that disappears (obviously it can only be a 1 or 0).

A full list of opcodes can be had at David Winter's page, under section 1.5:

  http://www.pdc.kth.se/~lfo/chip8/CHIP8.htm

He also has a full explanation of everything, so if check there for more
details. If you want to write code for an opcode that I didn't list, by all
means go for it.

Be sure to start reading the attached program from the beginning. To my
understanding in a real Chip-8 game up to address 200 was reserved for machine
code which we wont worry about.

The register values you should get are:

  V1:01000101
  V2:10111011
  V3:11101100
  V4:this number should be random, so do multiple runs to make sure it changes
  VF:00000000

A note: If everything is working as it is supposed to, you should run straight
through the file once. If you are looping, I would start with double checking
all your math operations.

Good Luck!

I know this quiz is a bit longer (to read) than we normally prefer, it is not hard however and I encourage people to try it. I solved it myself in just over 100 lines of code.

The length mainly comes from me pestering Ethan to clarify details, which he did a great job of.

James Edward Gray II

···

On Jul 28, 2006, at 12:50 PM, Ruby Quiz wrote:

by Ethan Price

CHIP-8 was an interpreted language used in the 1970's for basic games like Pong.

It was just pointed out to me that I didn't attach the program. You can find it at:

http://www.rubyquiz.com/Chip8Test

James Edward Gray II

···

On Jul 28, 2006, at 12:50 PM, Ruby Quiz wrote:

I have written a simple program

Ruby Quiz <james@grayproductions.net> writes:

1. Make the emulator read the file. All opcodes are four digit hex numbers,
  but those will need to be split.

What's not specified here is that the opcodes are written to the file
as big-endian 16-bit numbers. That is, most significant digit first.
The file represents a memory dump of a simple chip-8 processor that
begins at memory address 0000.

2. Set up the registers and other assorted things so they can be modified.

I'll note that the spec. nowhere tells us what the initial, power-on
values of the registers are. My personal recommendation is that
people intialize the registers with random values, so that bad chip-8
programs would be caught. As an addition, it might be nice to accept
command line arguments to initialize the registers.

3. Interpret the function of the opcode in Ruby.
4. Dump all the registers so you can check and make sure everything went well.

I'd really like some more I/O than this. I'm tempted to add an
extension that does memory mapped IO, or, looking at the chip-8 page,
implement the graphics through ASCII-graphics.

<snip>

While I'm on the subject, let's talk about addresses in detail. Your
current address in the file is how many bytes you are from the
beginning of the file. So if I am at address 008 in the file, I am
eight bytes away from the beginning of the file, and therefore the
next byte I read will be byte nine. Say we had the opcode 100F, that
means we would jump to after byte 16 (it is a hex remember).

I'm not sure you mean this. I think you mean either "jump to before
the 16th byte" or meant to use opcode 1010.

Looking at your program, I see an easy way to make this explanation
easier:

  Opcode 0100 will send you back to the beginning, so that the next
  instruction read and executed is the first instruction of the file.
  Opcode 0102 will send you back to the second instruction in the file
  (remember that instructions are four hex digits - or two bytes -
  long). Other addresses follow.

Be sure to start reading the attached program from the beginning. To
my understanding in a real Chip-8 game up to address 200 was
reserved for machine code which we wont worry about.

In case people are looking at this in an environment where they can't
get online and grab the program, here's a short ruby program that
recreates the file:

File.open("Chip8Test","wb") { |f|
  x = "x"
  %w{ 61 77 62 45 71 01 83 20 81 21 81 22 82 33 81 34
      82 35 81 06 83 27 83 0e 64 ff c4 11 32 bb 10 00
      00 00 }.each {|b| x[0] = b.hex; f.write(x)}
}

Hi,

here is my solution:

I initialize the registers with zeroes, because it simplifies unit-testing.

A binary program is unpacked into an array of opcodes (16bit). The
program counter (named "current") counts opcodes, not bytes, so
for a jump instruction the destination address has to be divided by two.

I also wrote a small "disassembler" which dumped the test program as:

000: [6177] V1 = 0x77
002: [6245] V2 = 0x45
004: [7101] V1 = V1 + 0x01
006: [8320] V3 = V2
008: [8121] V1 = V1 | V2
00A: [8122] V1 = V1 & V2
00C: [8233] V2 = V2 ^ V3
00E: [8134] V1 = V1 + V3
010: [8235] V2 = V2 - V3
012: [8106] V1 = V1 >> 1
014: [8327] V3 = V2 - V3
016: [830E] V3 = V3 << 1
018: [64FF] V4 = 0xFF
01A: [C411] V4 = rand() & 0x11
01C: [32BB] skip next if V2 == 0xBB
01E: [1000] goto 000
020: [0000] exit

(Almost looks like ruby code. We really need a "goto" in ruby :wink:

Regards,
Boris

### chip8_emu.rb
class Chip8Emulator
   attr_accessor :register

   VF = 15 # index of the carry/borrow register

   def initialize
     @register = [0] * 16 # V0..VF
   end

   def exec(program)
     opcodes = program.unpack('n*')
     current = 0 # current opcode index
     loop do
       opcode = opcodes[current]

       # these are needed often:
       x = opcode >> 8 & 0x0F
       y = opcode >> 4 & 0x0F
       kk = opcode & 0xFF
       nnn = opcode & 0x0FFF

       case opcode >> 12 # first nibble
       when 0 then return
       when 1 then current = (nnn) / 2 and next
       when 3 then current += 1 if @register[x] == kk
       when 6 then @register[x] = kk
       when 7 then add(x, kk)
       when 8
         case opcode & 0x0F # last nibble
         when 0 then @register[x] = @register[y]
         when 1 then @register[x] |= @register[y]
         when 2 then @register[x] &= @register[y]
         when 3 then @register[x] ^= @register[y]
         when 4 then add(x, @register[y])
         when 5 then subtract(x, x, y)
         when 6 then shift_right(x)
         when 7 then subtract(x, y, x)
         when 0xE then shift_left(x)
         else raise "Unknown opcode: " + opcode.to_s(16)
         end
       when 0xC then random(x, kk)
       else raise "Unknown opcode: " + opcode.to_s(16)
       end
       current += 1 # next opcode
     end
   end

   def add(reg, value)
     result = @register[reg] + value
     @register[reg] = result & 0xFF
     @register[VF] = result >> 8 # carry
   end

   def subtract(reg, a, b)
     result = @register[a] - @register[b]
     @register[reg] = result & 0xFF
     @register[VF] = - (result >> 8) # borrow
   end

   def shift_right(reg)
     @register[VF] = @register[reg] & 0x01
     @register[reg] >>= 1
   end

   def shift_left(reg)
     @register[VF] = @register[reg] >> 7
     @register[reg] = (@register[reg] << 1) & 0xFF
   end

   def random(reg, kk)
     @register[reg] = rand(256) & kk
   end

   # show all registers
   def dump
     0.upto(VF) do |reg|
       printf("V%1X:%08b\n", reg, @register[reg])
     end
   end
end

if $0 == __FILE__
   ARGV.each do |program|
     emu = Chip8Emulator.new
     emu.exec(File.read(program))
     emu.dump
   end
end

### test_chip8_emu.rb
require 'test/unit'
require 'chip8_emu'

class Chip8EmulatorTest < Test::Unit::TestCase
   def setup
     @emu = Chip8Emulator.new
   end

   def test_init
     assert_equal [0] * 16, @emu.register
   end

   def test_set_register
     @emu.exec("\x60\x42" + "\x63\xFF" + "\x6F\x66" + "\0\0")
     assert_equal [66, 0, 0, 255] + [0]*11 + [102], @emu.register
   end

   def test_jump
     @emu.exec("\x10\x04" + "\x00\x00" + "\x60\x42" + "\0\0")
     assert_equal [66] + [0]*15, @emu.register
   end

   def test_skip_next
     @emu.exec("\x60\x42" + "\x30\x42" + "\x60\x43" + "\0\0")
     assert_equal [66] + [0]*15, @emu.register
   end

   def test_add_const
     @emu.exec("\x60\xFF" + "\x70\x01" + "\0\0")
     assert_equal [0]*15 + [1], @emu.register
   end

   def test_copy
     @emu.exec("\x60\x42" + "\x81\x00" + "\0\0")
     assert_equal [66]*2 + [0]*14, @emu.register
   end

   def test_or
     @emu.exec("\x60\x03" + "\x61\x05" + "\x80\x11" + "\0\0")
     assert_equal [7, 5] + [0]*14, @emu.register
   end

   def test_and
     @emu.exec("\x60\x03" + "\x61\x05" + "\x80\x12" + "\0\0")
     assert_equal [1, 5] + [0]*14, @emu.register
   end

   def test_xor
     @emu.exec("\x60\x03" + "\x61\x05" + "\x80\x13" + "\0\0")
     assert_equal [6, 5] + [0]*14, @emu.register
   end

   def test_add
     @emu.exec("\x60\x01" + "\x61\x01" + "\x80\x14" + "\0\0")
     assert_equal [2, 1] + [0]*14, @emu.register
   end

   def test_subtract
     @emu.exec("\x60\x00" + "\x61\x01" + "\x80\x15" + "\0\0")
     assert_equal [255, 1] + [0]*13 + [1], @emu.register
   end

   def test_subtract2
     @emu.exec("\x60\x01" + "\x61\x02" + "\x80\x17" + "\0\0")
     assert_equal [1, 2] + [0]*14, @emu.register
   end

   def test_shift_right
     @emu.exec("\x60\xFF" + "\x80\x06" + "\0\0")
     assert_equal [0x7F] + [0]*14 + [1], @emu.register
   end

   def test_shift_left
     @emu.exec("\x60\xFF" + "\x80\x0E" + "\0\0")
     assert_equal [0xFE] + [0]*14 + [1], @emu.register
   end

   def test_rand
     srand 0
     first_rand = rand(256)
     srand 0
     @emu.exec("\xC0\x0F" + "\0\0")
     assert_equal [first_rand & 0x0F] + [0]*15, @emu.register
   end
end

### chip8_asm.rb
class Chip8Disassembler
   CODES = {
     /0000/ => 'exit',
     /1(...)/ => 'goto \1',
     /3(.)(..)/ => 'skip next if V\1 == 0x\2',
     /6(.)(..)/ => 'V\1 = 0x\2',
     /7(.)(..)/ => 'V\1 = V\1 + 0x\2',
     /8(.)(.)0/ => 'V\1 = V\2',
     /8(.)(.)1/ => 'V\1 = V\1 | V\2',
     /8(.)(.)2/ => 'V\1 = V\1 & V\2',
     /8(.)(.)3/ => 'V\1 = V\1 ^ V\2',
     /8(.)(.)4/ => 'V\1 = V\1 + V\2',
     /8(.)(.)5/ => 'V\1 = V\1 - V\2',
     /8(.)06/ => 'V\1 = V\1 >> 1',
     /8(.)(.)7/ => 'V\1 = V\2 - V\1',
     /8(.)0E/ => 'V\1 = V\1 << 1',
     /C(.)(..)/ => 'V\1 = rand() & 0x\2',
   }

   def self.code2text hexcode
     CODES.each do |re, subs|
       if hexcode =~ re
         return hexcode.sub(re, subs)
       end
     end
     '???'
   end

   def self.dump binary
     opcodes = binary.unpack "n*"
     opcodes.each_with_index do |code, waddr|
       code_hex = "%04X" % code
       printf("%03X: [%s] %s\n", waddr*2, code_hex, code2text(code_hex));
     end
   end
end

binary = File.read(ARGV[0])
Chip8Disassembler.dump(binary)

Hi,

This one was a lot of fun :slight_smile:
Not that the others weren't but I must admit I forgot
how much I liked assembler... Thanks for reminding me :slight_smile:

Now, once the emulator was done, wasn't too much fun to
watch the same program over and over, so I wrote a small
'assembler' to be able to create programs as well :slight_smile:

I rewrote the Chip8Test file as well in this pseudo
assembler, then 'compiled' it back, and the run it.
I also added a few more opcodes, almost all except those
used for the actual graphical part.

The files are:
chip8.rb - the emulator
cc8.rb - the 'assembler'
Chip8Test.c8 - the original test program, in 'source' :slight_smile:
AnotherTest.c8 - another test program
cc8-quickref.txt - a quick reference to the 'assembly' lang
                   used in the .c8 tests files.

The way it works is:
ruby cc8.rb Chip8Test.c8 will create a Chip8Test.bin file
which can then be run with chip8.rb

Great quiz!!

Have a nice day everyone,
Alex

cc8.rb (4.01 KB)

chip8.rb (2.44 KB)

cc8-quickref.txt (1.11 KB)

AnotherTest.c8 (645 Bytes)

Chip8Test.c8 (277 Bytes)

I implemented pretty much all the opcodes and used Tk to display the
graphics as curses didn't work on my machine.
It can run the games from the site James posted
(http://www.pdc.kth.se/~lfo/chip8/CHIP8/GAMES/\).
I used one Win32 function (GetKeyState) as I didn't know a compatible
way to check if a key is pressed.

Controls: q for quit, normal controls mapped just like on the site
with the explanation of the opcodes.

I also put the code on Pastie, as Gmail will probably mess up my code
(again): http://pastie.caboo.se/6634

require 'enumerator'
require 'tk'

require 'Win32API'
keystate = Win32API.new("user32", "GetKeyState", 'I', 'I')
# virtual keycodes, mapping keys like
http://www.pdc.kth.se/~lfo/chip8/CHIP8.htm does
VK = {'q' => 81, 0 => 110, 1 => 103, 2 => 104, 3 => 105, 4 => 100, 5
=> 101, 6 => 102, 7 => 97, 8 => 98, 9 => 99, 0xA => 96, 0xB => 13, 0xC
=> 111, 0xD => 106, 0xE => 109, 0xF => 107}

$key_pressed = proc{|key|
  keystate.call(VK[key]) & ~1 != 0 # higher bit set = key currently pressed
}

class Integer
  def lo; self & 0xF end # low nibble of byte
  def hi; (self >> 4).lo end # high nibble of byte
  def bcd_byte
    [self / 100, (self/10) % 10, self % 10].map{|n| n.chr}.join
  end
end

class Chip8Emulator
  COLORS = ['#000000','#ffffff']
  FONT_TO_BIN = {' ' => 0,'#' => 1}
  FONT = ["#### # #### #### # # #### #### #### #### #### ## ###
### ### #### #### ",
          "# # ## # # # # # # # # # # # # # # #
# # # # # ",
          "# # # #### ### #### #### #### # #### #### #### ###
# # # ### ### ",
          "# # # # # # # # # # # # # # # # #
# # # # # ",
          "#### ### #### #### # #### #### # #### #### # # ###
### ### #### # "
         ].map{|l| l.split('').enum_slice(5).to_a }.transpose.map{|lines|
             lines.map{|line| (line.map{|c| FONT_TO_BIN[c] }.join +
'000').to_i(2).chr }.join
          }.join # FONT is now the encoded font: 5 bytes for 0, 5
bytes for 1, etc total 80 bytes

  def initialize(code)
    @ip = 0x200
    @mem = FONT + "\0" * (@ip - 80) + code + "\0" * 4096 # ensure
4kb mem + program at start
    @regs = "\0" * 160
    @I = 0
    @stack =
    init_screen
  end

  def +(a,b)
    a += b
    @regs[0xF] = a > 0xFF ? 1 : 0
    a & 0xFF
  end

  def -(a,b)
    a -= b
    @regs[0xF] = a < 0 ? 0 : 1
    (a + 0x100) & 0xFF
  end

  def get_keypress
    sleep 0.01 until k = (0..0xF).find{|k| $key_pressed.call(k) }
    k
  end

  def init_screen
    @screen = Array.new(32) {Array.new(64,0) }
    @img_screen = TkPhotoImage.new('width'=>64,'height'=>32)
    @img_screen.put( Array.new(32) {Array.new(64,COLORS[0]) } )
    update_screen
  end

  def draw_sprite(x,y,size)
    x %= 64
    y %= 32
    @regs[0xF] = 0
    img_update = Array.new(size){ Array.new(8) }
    @mem[@I,size].split('').each_with_index{|b,dy|
      ypos = (y+dy) % 32
      (0..7).each{|dx|
        chr = b[0][7-dx]
        xpos = (x+dx) % 64
        @regs[0xF] = 1 if(chr==1 && @screen[ypos][xpos]==1) # collision
        col = @screen[ypos][xpos] ^= chr
        img_update[dy][dx] = COLORS[col]
      }
    }
    @img_screen.put(img_update, :to => [x,y] )
    update_screen
  end

  def update_screen
    $tkscreen.copy(@img_screen,:zoom=>10)
  end

  def fetch
    @instr = @mem[@ip,2]
    @ip += 2
  end

  def execute
    x = @instr[0].lo
    y = @instr[1].hi
    opt = @instr[1].lo
    kk = @instr[1]
    nnn = @instr[0].lo << 8 | @instr[1]
    case @instr[0].hi
      when 0
        case kk
          when 0xE0 then init_screen
     # 00E0 Erase the screen
          when 0xEE then @ip = @stack.pop
     # 00EE Return from a CHIP-8 sub-routine
          else return nil
        end
      when 1 then @ip = nnn
     # 1NNN Jump to the address NNN of the file
      when 2
     # 2NNN Call CHIP-8 sub-routine at NNN
        @stack.push @ip
        @ip = nnn
      when 3 then @ip += 2 if @regs == kk
     # 3XKK Skip next instruction if VX == KK
      when 4 then @ip += 2 if @regs != kk
     # 4XKK Skip next instruction if VX != KK
      when 5 then @ip += 2 if @regs == @regs[y]
     # 5XY0 Skip next instruction if VX == VY
      when 6 then @regs = kk
     # 6XKK VX = KK
      when 7 then @regs = self.+(@regs,kk)
     # 7XKK VX = VX + KK
      when 8
        case opt
          when 0 then @regs = @regs[y]
      # 8XY0 VX = VY
          when 1 then @regs |= @regs[y]
      # 8XY1 VX = VX OR VY
          when 2 then @regs &= @regs[y]
      # 8XY2 VX = VX AND VY
          when 3 then @regs ^= @regs[y]
      # 8XY3 VX = VX XOR VY
          when 4 then @regs = self.+(@regs,@regs[y])
      # 8XY4 VX = VX + VY
          when 5 then @regs = self.-(@regs,@regs[y])
      # 8XY5 VX = VX - VY
          when 6 then @regs[0xF], @regs = @regs[0], @regs

1 # 8X06 VX = VX SHIFT RIGHT 1 VF = least significant bit

          when 7 then @regs = self.-(@regs[y],@regs)
      # 8XY7 VX = VY - VX
          when 0xE then @regs[0xF], @regs = @regs[7], @regs
<< 1 # 8X0E VX = VX SHIFT LEFT 1, VF = most significant bit
          else return nil
        end
      when 9 then @ip += 2 if @regs != @regs[y]
     # 9XY0 Skip next instruction if VX != VY
      when 0xA then @I = nnn
     # ANNN I = NNN
      when 0xB then @ip = nnn + @regs[0]
     # BNNN Jump to NNN + V0
      when 0xC then @regs = kk & rand(0xFF)
     # CXKK VX = Random number AND KK
      when 0xD then draw_sprite(@regs,@regs[y],opt)
     # DXYN Draws a sprite at (VX,VY) starting at M(I). VF =
collision.
      when 0xE
        case kk
          when 0x9E then @ip +=2 if $key_pressed.call @regs
     # EX9E Skip next instruction if key VX pressed
          when 0xA1 then @ip +=2 unless $key_pressed.call @regs
     # EXA1 Skip next instruction if key VX not pressed
          else return nil
        end
      when 0xF
        case kk
          when 0x07 then @regs = @delay_timer
     # FX07 VX = Delay timer
          when 0x0A then @regs = get_keypress
     # FX0A Waits a keypress and stores it in VX
          when 0x15 then @delay_timer = @regs
     # FX15 Delay timer = VX
          when 0x18 then
     # FX18 Sound timer = VX, not implemented as it doesn't do
anything except beep
          when 0x1E then @I += @regs
     # FX1E I = I + VX
          when 0x29 then @I = 5 * @regs
     # FX29 I points to the 4 x 5 font sprite of hex char in VX (
font at start of mem, 5 bytes per char)
          when 0x33 then @mem[@I,2] = @regs.bcd_byte
     # FX33 Store BCD representation of VX in M(I)...M(I+2)
          when 0x55 then @mem[@I,x+1] = @regs[0..x]
     # FX55 Save V0...VX in memory starting at M(I)
          when 0x65 then @regs[0..x] = @mem[@I,x+1]
     # FX65 Load V0...VX from memory starting at M(I)
          else return nil
        end
      else return nil
    end
    return true
  end

  def run
    Thread.new {
      @key_timer = @delay_timer = 0
      loop{
        sleep 1.0 / 60
        @delay_timer -= 1 if @delay_timer > 0
        @key_timer -= 1 if @key_timer > 0
        @key_pressed = nil if @key_timer == 0
        exit! if $key_pressed.call('q')
      }
    }

    loop {
      fetch
      break unless execute
    }
    puts "Halted at instruction %02X%02X " % [@instr[0],@instr[1]]
  end

end

$tkscreen = TkPhotoImage.new('width'=>640,'height'=>320)
TkLabel.new(nil, 'image' => $tkscreen ).pack
Thread.new{ Tk.mainloop }

Chip8Emulator.new( File.open(ARGV[0],'rb').read).run if ARGV[0]

require 'logger'

class OpcodeGenerator
  include Enumerable

  OPCODE_SIZE = 2

  def initialize(source_file)
    @source_file = source_file
  end

  def each
    while true
      text = @source_file.read(OPCODE_SIZE)
      return if text.nil?
      yield text.unpack('S')[0]
    end
  end

  def seek(address)
    @source_file.seek address
  end

  def skip_next
    @source_file.seek(OPCODE_SIZE, IO::SEEK_CUR)
  end

end

class Chip_8_Emulator

  REGISTER_COUNT = 0x16,
  REGISTER_PATTERN = /V([0-9A-F])/
  REGISTER_MASK = 0xFF
  CARRY_MASK = 0x100
  LEFT_SHIFT_MASK = 0x80
  RIGHT_SHIFT_MASK = 0x1
  CARRY_REGISTER = 0xF

  def initialize
    @register = Array.new(REGISTER_COUNT)
  end

  def run(opcode_generator)
    @register.fill 0
    opcode_generator.each do |opcode|
      case opcode & 0xF000
      when 0x0000 # exit gracefully
        $LOG.debug sprintf("0x%04X Exit", opcode)
        break
      when 0x1000 # Jump to NNNN
        address = address_from_opcode opcode
        $LOG.debug sprintf("0x%04X Jump to 0x%04X", opcode, address)
        opcode_generator.seek address
      when 0x3000 # Skip next instruction if VX == KK
        x, k = register_and_constant_from_opcode opcode
        $LOG.debug sprintf("0x%04X Skip if V%X == 0x%04X", opcode, x,
k)
        if @register[x] == k then
          opcode_generator.skip_next
        end
      when 0x6000 # VX = KK
        x, k = register_and_constant_from_opcode opcode
        $LOG.debug sprintf("0x%04X V%X = 0x%04X", opcode, x, k)
        @register[x] = k
      when 0x7000 # VX = VX + KK
        x, k = register_and_constant_from_opcode opcode
        $LOG.debug sprintf("0x%04X V%X = V%X + 0x%04X", opcode, x, x,
k)
        @register[x] = short_int_add(@register[x], k)
      when 0x8000 # register operations
        case opcode & 0x000F
        when 0x0 # VX = VY
          x, y = register_pair_from_opcode opcode
          $LOG.debug sprintf("0x%04X V%X = V%X", opcode, x, y)
          @register[x] = @register[y]
        when 0x1 # VX = VX OR VY
          x, y = register_pair_from_opcode opcode
          $LOG.debug sprintf("0x%04X V%X = V%X OR V%X", opcode, x, x,
y)
          @register[x] |= @register[y]
        when 0x2 # VX = VX AND VY
          x, y = register_pair_from_opcode opcode
          $LOG.debug sprintf("0x%04X V%X = V%X AND V%X", opcode, x, x,
y)
          @register[x] &= @register[y]
        when 0x3 # VX = VX AND VY
          x, y = register_pair_from_opcode opcode
          $LOG.debug sprintf("0x%04X V%X = V%X XOR V%X", opcode, x, x,
y)
          @register[x] ^= @register[y]
        when 0x4 # VX = VX + VY
          x, y = register_pair_from_opcode opcode
          @register[x] = short_int_add(@register[x], @register[y])
        when 0x5 # VX = VX - VY
          x, y = register_pair_from_opcode opcode
          $LOG.debug sprintf("0x%04X V%X = V%X - V%X", opcode, x, x, y)
          @register[x] = short_int_subtract(@register[x], @register[y])
        when 0x6 # VX = VX shift right 1
          x = register_from_opcode opcode
          $LOG.debug sprintf("0x%04X V%X = V%X shift right 1", opcode,
x, x)
          @register[x] = short_int_shift_right(@register[x])
        when 0x7 # VX = VY - VX
          x, y = register_pair_from_opcode opcode
          $LOG.debug sprintf("0x%04X V%X = V%X - V%X", opcode, x, y, x)
          @register[x] = short_int_subtract(@register[y], @register[x])
        when 0xE # VX = VX shift left 1
          x = register_from_opcode opcode
          $LOG.debug sprintf("0x%04X V%X = V%X shift left 1", opcode,
x, x)
          @register[x] = short_int_shift_left(@register[x])
        else
          raise RuntimeError, "Unknown register opcode
0x#{opcode.to_s(16)}"
        end
        register_index = (opcode & 0x0F00) >> 8
        value = opcode & 0x00FF
        $LOG.debug sprintf("0x%04X V%X = V%X + 0x%04X", opcode,
register_index, register_index, value)
      when 0xC000 # VX = Random number AND KK
        x, k = register_and_constant_from_opcode opcode
        r = rand(0xFF)
        $LOG.debug sprintf("0x%04X V%X = random number 0x%04X AND
0x%04X", opcode, x, r, k)
        @register[x] = r & k
      else
        raise RuntimeError, "Unknown opcode 0x#{opcode.to_s(16)}"
      end
    end
  end

  def address_from_opcode(opcode)
    opcode & 0x0FFF
  end

  def register_from_opcode(opcode)
    (opcode & 0x0F00) >> 8
  end

  def register_and_constant_from_opcode(opcode)
    x = (opcode & 0x0F00) >> 8
    k = opcode & 0x00FF

    [x, k]
  end

  def register_pair_from_opcode(opcode)
    x = (opcode & 0x0F00) >> 8
    y = (opcode & 0x00F0) >> 4

    [x, y]
  end

  def short_int_add(a, b)
    sum = a + b
    @register[CARRY_REGISTER] = (sum & CARRY_MASK) >> 8
    sum & REGISTER_MASK
  end

  def short_int_subtract(a, b)
    difference = (a | CARRY_MASK) - b
    @register[CARRY_REGISTER] = (difference & CARRY_MASK) >> 8
    difference & REGISTER_MASK
  end

  def short_int_shift_left(a)
    @register[CARRY_REGISTER] = a & LEFT_SHIFT_MASK
    (a << 1) & REGISTER_MASK
  end

  def short_int_shift_right(a)
    @register[CARRY_REGISTER] = a & RIGHT_SHIFT_MASK
    a >> 1
  end

  def method_missing(method_id)
    match_object = REGISTER_PATTERN.match(method_id.id2name)
    if match_object.nil? : raise NoMethodError, method_id.inspect end

    @register[match_object[1].hex]
  end

end

if __FILE__ == $0

  $LOG = Logger.new STDOUT
  $LOG.info "program starts"

  require 'test/unit'
  require 'stringio'

  class TestOpcodeGenerator < Test::Unit::TestCase

    TEST_DATA = [0x0001, 0x0002, 0x0003]

    def test_generator
      opcodes = OpcodeGenerator.new StringIO.new(TEST_DATA.pack("S*"))
      opcodes.zip(TEST_DATA) {|opcode, test_element| assert_equal
test_element, opcode}
    end

    def test_seek
      opcodes = OpcodeGenerator.new StringIO.new(TEST_DATA.pack("S*"))
      opcodes.seek 2
      opcodes.zip(TEST_DATA[1,TEST_DATA.length-1]) {|opcode,
test_element| assert_equal test_element, opcode}
    end

    def test_skip_next
      opcodes = OpcodeGenerator.new StringIO.new(TEST_DATA.pack("S*"))
      opcodes.seek 2
      opcodes.skip_next
      opcodes.zip(TEST_DATA[2,TEST_DATA.length-2]) {|opcode,
test_element| assert_equal test_element, opcode}
    end

  end

  class TestChip_8 < Test::Unit::TestCase

    # dump of file Chip8Test.html
    TEST_DATA =
[0x6177,0x6245,0x7101,0x8320,0x8121,0x8122,0x8233,0x8134,0x8235,0x8106,0x8327,0x830E,0x64FF,0xC411,0x32BB,0x1000,0x0000,].pack('S*')

    # V1:01000101
    # V2:10111011
    # V3:11101100
    # V4:this number should be random, so do multiple runs to make sure
it changes
    # VF:00000000
    def test_emulator

      emulator = Chip_8_Emulator.new

      emulator.run OpcodeGenerator.new(StringIO.new(TEST_DATA))
      assert_equal 0b01000101, emulator.V1
      assert_equal 0b10111011, emulator.V2
      assert_equal 0b11101100, emulator.V3
      first_v4 = emulator.V4
      assert_equal 0b00000000, emulator.VF

      emulator.run OpcodeGenerator.new(StringIO.new(TEST_DATA))
      assert_equal 0b01000101, emulator.V1
      assert_equal 0b10111011, emulator.V2
      assert_equal 0b11101100, emulator.V3

# note that this test fails sometimes because the domain isn't very big
# assert_not_equal first_v4, emulator.V4

      assert_equal 0b00000000, emulator.VF
      
    end
  
  end
  
end

My solution for managing binary numbers was to make a new class
BitArray.

It seems like I'm repeating myself and being awfully wordy in the main
loop.

Mitchell Koch

chip8.rb (4.02 KB)

My solution: It's like a clone of my ICFP solution. Where's the codex.chip8?
-Adam

···

On 7/28/06, Ruby Quiz <james@grayproductions.net> wrote:

Your job is to make an emulator...

-------------------------------------------------

class Chip8
  class Instruction
    attr_reader :op,:nn,:x,:y,:kk
    def parse val
      @op = (val&0xF000)>>12
      @nn = (val&0x0FFF)
      @kk = (val&0x00FF)
      @x = (val&0x0F00)>>8
      @y = (val&0x00F0)>>4
    end
  end
  def initialize
    @program =
    @cp = 0
    @v = Array.new(16){0}
  end
  def load filename
    File.open(filename, "rb"){|f|
      while !f.eof
        @program << f.read(2).unpack('n*')[0]
      end
    }
  end
  def run
    halt = false
    i = Instruction.new
    while !halt
      i.parse(@program[@cp])
      @cp+=1
      case i.op
        when 0 then halt=true
        when 1 then @cp = i.nn/2
        when 3 then @cp+=1 if @v[i.x]==i.kk
        when 6 then @v[i.x] = i.kk
        when 7 then @v[0xF],@v[i.x] = (@v[i.x]+ i.kk).divmod 0x100
        when 8
          case i.nn&0xF
            when 0 then @v[i.x] = @v[i.y]
            when 1 then @v[i.x] |= @v[i.y]
            when 2 then @v[i.x] &= @v[i.y]
            when 3 then @v[i.x] ^= @v[i.y]
            when 4 then @v[0xF],@v[i.x] = (@v[i.x]+@v[i.y]).divmod 0x100
            when 5 then @v[0xF],@v[i.x] = (@v[i.x]-@v[i.y]).divmod 0x100
            when 6 then @v[0xF]=@v[i.x][0]
                        @v[i.x]>>=1
            when 7 then c,@v[i.x] = (@v[i.y]-@v[i.x]).divmod 0x100
                        @v[0xF]=1+c
            when 0xE then @v[0xF]=@v[i.x][7]
                          @v[i.x]<<=1
          end
        when 0xC then @v[i.x] = rand(256)&i.kk
      end
    end
  end
  def dump base = 10
    puts @v.map{|e|e.to_s(base)}.inspect
  end
end

if __FILE__ == $0
  c = Chip8.new
  c.load ARGV[0]||"chip8.test"
  c.run
  c.dump(2)
end

I think you mean 1000 and 1002 in the description above. :slight_smile:

So, should an absolute jump to N make it so that the Nth (0-based
index) instruction is the next executed, or the (N+1)th? You're
example above seems to put it both ways. 1000 sends you back to the
beginning, so the "0th" instruction (first instruction in the file) is
the next executed. But then 1002 should make it so that the "2nd"
instruction (third instruction in the file) is the next executed,
right? Maybe this is what you meant by "second" anyways...

Jacob Fugal

···

On 7/28/06, Daniel Martin <martin@snowplow.org> wrote:

> While I'm on the subject, let's talk about addresses in detail. Your
> current address in the file is how many bytes you are from the
> beginning of the file. So if I am at address 008 in the file, I am
> eight bytes away from the beginning of the file, and therefore the
> next byte I read will be byte nine. Say we had the opcode 100F, that
> means we would jump to after byte 16 (it is a hex remember).

I'm not sure you mean this. I think you mean either "jump to before
the 16th byte" or meant to use opcode 1010.

Looking at your program, I see an easy way to make this explanation
easier:

  Opcode 0100 will send you back to the beginning, so that the next
  instruction read and executed is the first instruction of the file.
  Opcode 0102 will send you back to the second instruction in the file
  (remember that instructions are four hex digits - or two bytes -
  long). Other addresses follow.

I would think nils would be more likely to blow up with a problem in the later math operations.

James Edward Gray II

···

On Jul 28, 2006, at 3:15 PM, Daniel Martin wrote:

2. Set up the registers and other assorted things so they can be modified.

I'll note that the spec. nowhere tells us what the initial, power-on
values of the registers are. My personal recommendation is that
people intialize the registers with random values, so that bad chip-8
programs would be caught. As an addition, it might be nice to accept
command line arguments to initialize the registers.

Heres my solution. I was more shooting for brevity of code than cool
stuff... Like previous posts, it uses instruction-level addressing,
so jumps are divided by two.

http://pastie.caboo.se/6639

<snip>

I discuss this a little at the end of this summary:

http://www.rubyquiz.com/quiz5.html

You can also use HighLine for this.

Amazing solution. Thanks for sharing.

James Edward Gray II

···

On Jul 30, 2006, at 1:55 PM, Sander Land wrote:

I used one Win32 function (GetKeyState) as I didn't know a compatible
way to check if a key is pressed.

1234ABCD5678
Opcode 1000 would send us to before 1234, so after jumping the first
instruction we would read is 1234. Opcode 1002 would send us to after 1234
and before ABCD (were moving forward 2 bytes, which equals 4 hex digits
worth of data). 1004 would send us to after ABCD and before 5678, and so on.

From Daniel's message
"I'll note that the spec. nowhere tells us what the initial, power-on
values of the registers are. My personal recommendation is that
people intialize the registers with random values, so that bad chip-8
programs would be caught."
Initial power on values for all registers should be zero. Initializing with
random numbers sounds like a good idea, but I imagine it might cause the
Chip-8 program to output different numbers, so I would cautious in doing so.

Hope that helps,
Ethan Price

···

On 7/28/06, Jacob Fugal <lukfugl@gmail.com> wrote:

On 7/28/06, Daniel Martin <martin@snowplow.org> wrote:
> I'm not sure you mean this. I think you mean either "jump to before
> the 16th byte" or meant to use opcode 1010.
>
> Looking at your program, I see an easy way to make this explanation
> easier:
>
> Opcode 0100 will send you back to the beginning, so that the next
> instruction read and executed is the first instruction of the file.
> Opcode 0102 will send you back to the second instruction in the file
> (remember that instructions are four hex digits - or two bytes -
> long). Other addresses follow.

I think you mean 1000 and 1002 in the description above. :slight_smile:

So, should an absolute jump to N make it so that the Nth (0-based
index) instruction is the next executed, or the (N+1)th? You're
example above seems to put it both ways. 1000 sends you back to the
beginning, so the "0th" instruction (first instruction in the file) is
the next executed. But then 1002 should make it so that the "2nd"
instruction (third instruction in the file) is the next executed,
right? Maybe this is what you meant by "second" anyways...

Jacob Fugal

Here is how it is supposed to work. Say we have a file that reads in hex:

I installed highline and checked the docs as well as the source and
couldn't find anything.
GetKeyState checks if a key is currently held down and not if it was
pressed, so get_character does not do what i want.

An earlier implementation used kbhit/getch, but that didn't work at
all because games like breakout check key input like:
V0 = 4
skip unless key V0 pressed
<some call here for processing input>
V0 = 6
skip unless key V0 pressed
<some call here for processing input>

And the repeat rate of keys seems to be too slow for the "6" to
trigger even if you hold the key down.
One other thing I tried is saving a key as held down for 0.25s if it
was pressed, but that was far too unreliable. The two "@key_timer"
lines of my code are a leftover from this which I forgot to remove.

···

On 7/31/06, James Edward Gray II <james@grayproductions.net> wrote:

On Jul 30, 2006, at 1:55 PM, Sander Land wrote:

> I used one Win32 function (GetKeyState) as I didn't know a compatible
> way to check if a key is pressed.
I discuss this a little at the end of this summary:
Ruby Quiz - Sokoban (#5)
You can also use HighLine for this.

Ah, gotcha. I was thinking in instruction addressing, rather than
memory addressing. That probably reflects the fact that I read and
decompile all the instructions before beginning execution. This makes
sense now. So, regarding the initial quiz description, 100F would send
us F (15) bytes forward from the start of the file, or only 7.5
instructions, and you'd have your instruction pointer in the middle of
an instruction. Is that valid? Or was it indeed a typo as Daniel
suggested?

Jacob Fugal

···

On 7/28/06, Ethan Price <nahteecirp@gmail.com> wrote:

On 7/28/06, Jacob Fugal <lukfugl@gmail.com> wrote:
> On 7/28/06, Daniel Martin <martin@snowplow.org> wrote:
> > Opcode 0100 will send you back to the beginning, so that the next
> > instruction read and executed is the first instruction of the file.
> > Opcode 0102 will send you back to the second instruction in the file
> > (remember that instructions are four hex digits - or two bytes -
> > long). Other addresses follow.
>
> So, should an absolute jump to N make it so that the Nth (0-based
> index) instruction is the next executed, or the (N+1)th? You're
> example above seems to put it both ways. 1000 sends you back to the
> beginning, so the "0th" instruction (first instruction in the file) is
> the next executed. But then 1002 should make it so that the "2nd"
> instruction (third instruction in the file) is the next executed,
> right? Maybe this is what you meant by "second" anyways...

Here is how it is supposed to work. Say we have a file that reads in hex:
1234ABCD5678
Opcode 1000 would send us to before 1234, so after jumping the first
instruction we would read is 1234. Opcode 1002 would send us to after 1234
and before ABCD (were moving forward 2 bytes, which equals 4 hex digits
worth of data). 1004 would send us to after ABCD and before 5678, and so on.

My apologies. I misunderstood.

James Edward Gray II

···

On Jul 31, 2006, at 2:43 PM, Sander Land wrote:

On 7/31/06, James Edward Gray II <james@grayproductions.net> wrote:

On Jul 30, 2006, at 1:55 PM, Sander Land wrote:

> I used one Win32 function (GetKeyState) as I didn't know a compatible
> way to check if a key is pressed.
I discuss this a little at the end of this summary:
Ruby Quiz - Sokoban (#5)
You can also use HighLine for this.

I installed highline and checked the docs as well as the source and
couldn't find anything.
GetKeyState checks if a key is currently held down and not if it was
pressed, so get_character does not do what i want.

Well it would be vaild, but indeed you would end up right in the middle of
an opcode (probably not where one wants to be), so it was a typo on my part.
100F would send you 15 bytes forward, not 16. So it probably should have
read something to the effect of "Say we had the opcode 100F, that means we
would jump to after byte 15, and the next instruction read would be bytes 16
and 17". My apologies for that.

Ethan Price

···

On 7/28/06, Jacob Fugal <lukfugl@gmail.com> wrote:

On 7/28/06, Ethan Price <nahteecirp@gmail.com> wrote:
> On 7/28/06, Jacob Fugal <lukfugl@gmail.com> wrote:
> > On 7/28/06, Daniel Martin <martin@snowplow.org> wrote:
> > > Opcode 0100 will send you back to the beginning, so that the next
> > > instruction read and executed is the first instruction of the
file.
> > > Opcode 0102 will send you back to the second instruction in the
file
> > > (remember that instructions are four hex digits - or two bytes -
> > > long). Other addresses follow.
> >
> > So, should an absolute jump to N make it so that the Nth (0-based
> > index) instruction is the next executed, or the (N+1)th? You're
> > example above seems to put it both ways. 1000 sends you back to the
> > beginning, so the "0th" instruction (first instruction in the file) is
> > the next executed. But then 1002 should make it so that the "2nd"
> > instruction (third instruction in the file) is the next executed,
> > right? Maybe this is what you meant by "second" anyways...
>
> Here is how it is supposed to work. Say we have a file that reads in
hex:
> 1234ABCD5678
> Opcode 1000 would send us to before 1234, so after jumping the first
> instruction we would read is 1234. Opcode 1002 would send us to after
1234
> and before ABCD (were moving forward 2 bytes, which equals 4 hex digits
> worth of data). 1004 would send us to after ABCD and before 5678, and so
on.

Ah, gotcha. I was thinking in instruction addressing, rather than
memory addressing. That probably reflects the fact that I read and
decompile all the instructions before beginning execution. This makes
sense now. So, regarding the initial quiz description, 100F would send
us F (15) bytes forward from the start of the file, or only 7.5
instructions, and you'd have your instruction pointer in the middle of
an instruction. Is that valid? Or was it indeed a typo as Daniel
suggested?

Jacob Fugal