Hi,

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

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 :)

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' :)
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

#!/usr/bin/ruby
OPCODE_LENGTH = 4           # bytes
EOX = '0000'                # End Of eXecutable
VF = 15                     # Shorcut to the carry reg.

@CODE = '0' * 1024          # The program
@REG = Array.new(16, 0)     # The Registers
@CA = 0                     # Current Address
@CO = 0                     # Current Opcode

class << self
  def jmp(addr)   # Jump at addr
    @CODE[@CA, OPCODE_LENGTH] = '1' << ("%03X" % addr)
    @CA += OPCODE_LENGTH
  end
  def call(addr)  # Call code at addr
    @CODE[@CA, OPCODE_LENGTH] = '2' << ("%03X" % addr)
  end
  def sec(vx, kk) # Skip if Equal with Constant
    ivx = vx.to_s.delete('V')
    @CODE[@CA, OPCODE_LENGTH] = '3' << ivx  << ("%02X" % kk)
    @CA += OPCODE_LENGTH
  end
  def snc(vx, kk) # Skip if NOT equal with Constant
    ivx = vx.to_s.delete('V')
    @CODE[@CA, OPCODE_LENGTH] = '4' << ivx  << ("%02X" % kk)
    @CA += OPCODE_LENGTH
  end
  def seq(vx, vy) # Skip next open if VX = VY
    ivx, ivy = vx.to_s.delete('V'), vy.to_s.delete('V')
    @CODE[@CA, OPCODE_LENGTH] = '5' << ivx << ivy << '0'
    @CA += OPCODE_LENGTH
  end
  def sne(vx, vy) # Skip next opcode if VX != VY
    ivx, ivy = vx.to_s.delete('V'), vy.to_s.delete('V')
    @CODE[@CA, OPCODE_LENGTH] = '9' << ivx << ivy << '0'
    @CA += OPCODE_LENGTH
  end
  def mov(vx, kk) # VX = KK
    ivx = vx.to_s.delete('V')
    @CODE[@CA, OPCODE_LENGTH] = '6' << ivx  << ("%02X" % kk)
    @CA += OPCODE_LENGTH
  end
  def inc(vx, kk) # VX = VX + KK
    ivx = vx.to_s.delete('V')
    @CODE[@CA, OPCODE_LENGTH] = '7' << ivx  << ("%02X" % kk)
    @CA += OPCODE_LENGTH
  end
  def movr(vx, vy) # VX = VY
    ivx, ivy = vx.to_s.delete('V'), vy.to_s.delete('V')
    @CODE[@CA, OPCODE_LENGTH] = '8' << ivx << ivy << '0'
    @CA += OPCODE_LENGTH
  end
  def _or(vx, vy) # VX = VX OR VY
    ivx, ivy = vx.to_s.delete('V'), vy.to_s.delete('V')
    @CODE[@CA, OPCODE_LENGTH] = '8' << ivx << ivy << '1'
    @CA += OPCODE_LENGTH
  end
  def _and(vx, vy) # VX = VX AND VY
    ivx, ivy = vx.to_s.delete('V'), vy.to_s.delete('V')
    @CODE[@CA, OPCODE_LENGTH] = '8' << ivx << ivy << '2'
    @CA += OPCODE_LENGTH
  end
  def _xor(vx, vy) # VX = VX XOR VY
    ivx, ivy = vx.to_s.delete('V'), vy.to_s.delete('V')
    @CODE[@CA, OPCODE_LENGTH] = '8' << ivx << ivy << '3'
    @CA += OPCODE_LENGTH
  end
  def sum(vx, vy) # VX = VX + VY
    ivx, ivy = vx.to_s.delete('V'), vy.to_s.delete('V')
    @CODE[@CA, OPCODE_LENGTH] = '8' << ivx << ivy << '4'
    @CA += OPCODE_LENGTH
  end
  def sub(vx, vy) # VX = VX - VY
    ivx, ivy = vx.to_s.delete('V'), vy.to_s.delete('V')
    @CODE[@CA, OPCODE_LENGTH] = '8' << ivx << ivy << '5'
    @CA += OPCODE_LENGTH
  end
  def shr(vx, vy) # VX >> 1
    ivx, ivy = vx.to_s.delete('V'), vy.to_s.delete('V')
    @CODE[@CA, OPCODE_LENGTH] = '8' << ivx << ivy << '6'
    @CA += OPCODE_LENGTH
  end
  def subr(vx, vy) # VX = VY - VX
    ivx, ivy = vx.to_s.delete('V'), vy.to_s.delete('V')
    @CODE[@CA, OPCODE_LENGTH] = '8' << ivx << ivy << '7'
    @CA += OPCODE_LENGTH
  end
  def shl(vx, vy) # VX << 1
    ivx, ivy = vx.to_s.delete('V'), vy.to_s.delete('V')
    @CODE[@CA, OPCODE_LENGTH] = '8' << ivx << ivy << 'E'
    @CA += OPCODE_LENGTH
  end
  def jmp0(nnn) # Jump to nnn + V0
    @CODE[@CA, OPCODE_LENGTH] = 'B' << ("%03X" % nnn)
    @CA += OPCODE_LENGTH
  end
  def rndc(vx, kk) # Random AND Constant
    ivx = vx.to_s.delete('V')
    @CODE[@CA, OPCODE_LENGTH] = 'C' << ivx  << ("%02X" % kk)
    @CA += OPCODE_LENGTH
  end
  def ret
    @CODE = @CODE[0, @CA + OPCODE_LENGTH + (OPCODE_LENGTH / 2)]
  end
  def dump
    opcode, i = '', 1
    @CODE.each_byte do |b|
      if opcode.length == 4
        print "#{opcode} "; opcode = ''
        puts if i % 20 == 0
        i += 1
      end
      opcode << b.chr
    end
  end
  def store
    fname, fext = File.basename(ARGV[0]).split(/\./); filename = "#{fname}.bin"
    fh = File.open(filename, 'wb')
    byte = ''
    @CODE.each_byte do |b| 
      if byte.length == 2 # hex digits
        fh.write(byte.hex.chr); byte = ''
      end
      byte << b.chr
    end
    fh.close
  end
end

eval(File.open(ARGV[0]).read)
#!/usr/bin/ruby

class Processor
  OPCODE_LENGTH = 4         # bytes
  EOX = '0000'              # End Of eXecutable
  VF = 15                   # Shorcut to the carry reg.

  def initialize(code)
    @CODE = code            # The program
    @REG = Array.new(16, 0) # The Registers
    @IP = 0                 # Instruction Pointer
    @CO = 0                 # Current Opcode
  end

  def dump
    puts "Opcode: #@CO"
    0.upto(VF) {|i| puts "V%X:" % i << "%08b %02X" % [@REG[i], @REG[i]]}; puts 
  end

  def run
    @CO = @CODE[@IP, OPCODE_LENGTH]
    vx, vy  = @CO[1,1].hex.to_i, @CO[2,1].hex.to_i
    kk, nnn = @CO[2,2].hex.to_i, @CO[1,3].hex.to_i
    case @CO[0,1]
    when '1'; @IP = nnn - OPCODE_LENGTH
    when '2'; ip = @IP; @IP = nnn; run; @IP = ip
    when '3'; @IP += OPCODE_LENGTH if @REG[vx] == kk
    when '4'; @IP += OPCODE_LENGTH if @REG[vx] != kk
    when '5'; @IP += OPCODE_LENGTH if @REG[vx] == @REG[vy]
    when '6'; @REG[vx] = kk
    when '7'; @REG[vx] += kk
    when '8' 
      case @CO[3,1]
      when '0'; @REG[vx]  = @REG[vy]
      when '1'; @REG[vx] |= @REG[vy]
      when '2'; @REG[vx] &= @REG[vy]
      when '3'; @REG[vx] ^= @REG[vy]
      when '4'
        sum = @REG[vx] + @REG[vy]
        if sum > 255
          @REG[vx] = sum % 256
          @REG[VF] = 1
        else
          @REG[vx] = sum
          @REG[VF] = 0
        end
      when '5'
        diff = @REG[vx] - @REG[vy]
        if diff < 0
          @REG[vx] = 256 - @REG[vy]
          @REG[VF] = 0
        else
          @REG[vx] = diff
          @REG[VF] = 1
        end
      when '6'
        bin = "%b" % @REG[vx]
        @REG[VF] = bin[-1, 1].to_i
        @REG[vx] = @REG[vx] >> 1
      when '7'
        diff = @REG[vy] - @REG[vx]
        if diff < 0
          @REG[vx] = 256 - @REG[vx]
          @REG[VF] = 0
        else
          @REG[vx] = diff
          @REG[VF] = 1
        end
      when 'E'
        bin = "%08b" % @REG[vx]
        @REG[VF] = bin[0, 1].to_i
        @REG[vx] = (@REG[vx] << 1) % 256
      end
    when '9'; @IP += OPCODE_LENGTH if @REG[vx] != @REG[vy]
    when 'A'; @IP = nnn - OPCODE_LENGTH
    when 'B'; @IP = nnn + @REG[0] - OPCODE_LENGTH
    when 'C'; @REG[vx] = rand(256) & kk
    when '0'; return if @CO == EOX
    end
    @IP += OPCODE_LENGTH
    run # again
  end
end

if $PROGRAM_NAME == __FILE__
  code = ''
  filetorun = (ARGV[0].nil?) ? 'Chip8Test' : ARGV[0]
  File.open(filetorun).each_byte {|b| code << "%02X"%b}
  P = Processor.new(code)
  P.run
  P.dump
end
CHIP-8 Pseudo Assembler 
   - Quick Reference

  jmp  addr     # Jump at addr
  call addr     # Call code at addr
  sec  vx, kk   # Skip if equal with constant
  snc  vx, kk   # Skip if NOT equal with constant
  seq  vx, vy   # Skip if equal
  sne  vx, vy   # Skip if NOT equal with constant
  mov  vx, kk   # Set register
  inc  vx, kk   # Increment register
  movr vx, vy   # Copy register
  _or  vx, vy   # Copy register /w AND
  _and vx, vy   # Copy register w/ OR
  _xor vx, vy   # Copy register w/ XOR
  sum  vx, vy   # Sum up registers
  sub  vx, vy   # Substract registers
  shr  vx, vy   # Substract registers
  subr vx, vy   # Substract registers, reversed
  shl  vx, vy   # Substract registers
  jmp0 nnn      # Jump to nnn + V0
  rndc vx, kk   # Random AND constant

At the end of the program you can tell the compiler to either dump the
opcodes to the screen, or 'compile' it into a binary file. Or both.
Before that however you must use:

  ret           # output '0000' the exit opcode

and only after that you can use:
  dump          # Dump the opcodes on screen
  store         # Dump the opcodes into a binary file

mov  :V1, 0x10
mov  :V2, 0x13
mov  :V3, 0x31
mov  :V4, 0x21
mov  :V5, 0x22
mov  :V6, 0x25
sec  :V2, 7
snc  :V8, 0x25
seq  :V2, :V7
sne  :VA, :VD
_and :V4, :V2
inc  :V4, 0x11
movr :V2, :V4
mov  :V4, 0x33
_or  :V1, :V4
rndc :V9, 0x71
sec  :V2, 7
seq  :V2, :V7
shl  :V8, :V0
shr  :V9, :V0
snc  :V8, 0x25
sne  :VA, :VD
subr :V2, :V1
shr  :V9, :V0
shl  :V8, :V0
subr :V2, :V1
inc  :V4, 0x11
sub  :V2, :V3
sum  :V2, :V1
_xor :V5, :V5
mov  :V4, 0x33
inc  :V4, 0x11
rndc :V9, 0x71
movr :V2, :V4
_or  :V1, :V4
_and :V4, :V2
_xor :V5, :V5
sum  :V2, :V1
sub  :V2, :V3
shr  :V9, :V0
shl  :V8, :V0
subr :V2, :V1
inc  :V4, 0x11
rndc :V9, 0x71
ret

dump
store
# .CODE
mov  :V1, 0x77
mov  :V2, 0x45
inc  :V1, 0x01
movr :V3, :V2
_or  :V1, :V2
_and :V1, :V2
_xor :V2, :V3
sum  :V1, :V3
sub  :V2, :V3
shr  :V1, :V0
subr :V3, :V2
shl  :V3, :V0
mov  :V4, 0xFF
rndc :V4, 0x11
sec  :V2, 0xBB
jmp 0         
ret           
# .COMPILER
dump
store