[QUIZ SOLUTION] #88 Chip-8

T

Tom Rauchenwald

--=-=-=

Hi,

Here is my solution to the current quiz. It has a few flaws, and it
uses a method call for each statement (which is probably quite slow).
The nice thing is that you can dump a program and view it in an
assembler-like way.
The implementation is quite simple, I don't think i need to explain it
in detail. I hope i haven't screwed up somewhere :)

Tom


--=-=-=
Content-Disposition: inline; filename=emu.rb

#!/usr/bin/env ruby
class Emulator
def initialize
@pc=0 #program counter
@prog = Array.new #where the read program is stored
@register=Array.new(16) #the machine has 16 registers
end
#read program into memory
def scan(filename)
opcode = Array.new
f = File.new(filename)
loop do
opcode.clear
begin
#read 2 byte and split it into 4 bit chunks
2.times {
ch = f.readchar
opcode << ((ch&0xF0) >> 4)
opcode << (ch&0xF)
}
rescue
f.close
break
end
#determine which opcode we are dealing with, and add it to
#the program
case opcode[0]
when 1
#Jump!
@prog << [:jmp, (opcode[1]<<8)+(opcode[2]<<4)+opcode[3]]
when 3
#skip when equal
@prog << [:skip_eq, opcode[1], (opcode[2]<<4)+opcode[3]]
when 6
#load constant into register
@prog << [:load_c, opcode[1], (opcode[2]<<4)+opcode[3]]
when 7
#add constant to register
@prog << [:add_c, opcode[1], (opcode[2]<<4)+opcode[3]]
when 8
case opcode[3]
when 0
#load value of register in register
@prog << [:load_r, opcode[1], opcode[2]]
#some bitwise operations
when 1
@prog << [:eek:r, opcode[1], opcode[2]]
when 2
@prog << [:and, opcode[1], opcode[2]]
when 3
@prog << [:xor, opcode[1], opcode[2]]
#adding and substracting 2 registers
when 4
@prog << [:add_r, opcode[1], opcode[2]]
when 5
@prog << [:sub_r, opcode[1], opcode[2]]
when 6
@prog << [:shift_r, opcode[1]]
when 7
#same as sub, but vx=vy-vx
@prog << [:sub2_r, opcode[1], opcode[2]]
when 0xE
@prog << [:shift_l, opcode[1]]
end
when 0xC
#set register to random value AND constant
@prog << [:load_rnd_and_c, opcode[1], (opcode[2]<<4)+opcode[3]]
else
#exit when we don't understand an opcode
@prog << [:end]
end
end
end

def run pc=nil
@pc=0 if pc==nil #program counter
while @prog[@pc]!=[:end]
#execute instruction
self.send(*@prog[@pc])
#next instruction
@pc+=1
end
end
#simple debug-method, waits after each instruction for a newline
#and dumps the registers
def step
@pc=0
while @prog[@pc]!=[:end]
self.send(*@prog[@pc])
@pc+=1
dump
STDIN.readline
end
end
#dump registers
def dump
print "PC: ", @pc, "\n"
1.upto(16) { |i|
print "V%02d: %08b (%03d)"%[i,@register, @register], "\n" if @register!=nil
}
end
#show which program was read
def dump_prog
@prog.each do |instr|
print instr[0].to_s, " "
print instr[1].to_s if instr[1]
print ", ", instr[2].to_s if instr[2]
print "\n"
end
end
#one method for each operation
private
def jmp addr
@pc=addr/4-1 #each instruction has 4 byte. -1, because @pc gets incremented in main loop
end
def skip_eq rx, c
@pc+=1 if @register[rx]==c
end
def load_c rx, c
@register[rx]=c
end
def load_r rx, ry
@register[rx]=@register[ry]
end
def add_c rx, c
tmp=@register[rx]+c
@register[rx]=tmp&0xFF
@register[0xF]=((tmp&0x100)>>8)
end
def and rx, ry
@register[rx]=(@register[rx]&@register[ry])
end
def or rx, ry
@register[rx]=(@register[rx]|@register[ry])
end
def xor rx, ry
@register[rx]=(@register[rx]^@register[ry])
end
def add_r rx, ry
tmp=@register[rx]+@register[ry]
@register[rx]=tmp&0xFF
@register[0xF]=((tmp&0x100)>>8)
end
def sub_r rx, ry
tmp=0x100+@register[rx]-@register[ry]
@register[rx]=tmp&0xFF
@register[0xF]=((tmp&0x100)>>8)
end
def sub2_r rx, ry
tmp=0x100+@register[ry]-@register[rx]
@register[rx]=tmp&0xFF
@register[0xF]=((tmp&0x100)>>8)
end
def shift_l rx
@register[0xF]=@register[rx]&0x80 #is this correct?
@register[rx]=((@register[rx]<<1)&0xFF)
end
def shift_r rx
@register[0xF]=@register[rx]&0x1
@register[rx]=(@register[rx]>>1)
end
def load_rnd_and_c rx, c
tmp=rand(0x100)
@register[rx]=tmp&c
end
end

if ARGV[0]==nil
STDERR.puts "Usage: #{$0} <program file>\n"
exit 1
end
if !File.exist? ARGV[0]
STDERR.puts "Error: File not found.\n"
exit 1
end
emu = Emulator.new
emu.scan(ARGV[0])
print "Program read: \n"
print "============= \n"
emu.dump_prog
print "\nRunning... \n\n"
emu.run
print "Done! Registers: \n"
print "================ \n"
emu.dump

--=-=-=--
 
T

Tom Rauchenwald

Tom Rauchenwald said:
Hi,

Here is my solution to the current quiz. It has a few flaws, and it
uses a method call for each statement (which is probably quite slow).
The nice thing is that you can dump a program and view it in an
assembler-like way.
The implementation is quite simple, I don't think i need to explain it
in detail. I hope i haven't screwed up somewhere :)

Well, I just read my code again, and noticed that I did screw up. An instruction is
2 byte, so
def jmp addr
@pc=addr/4-1 #each instruction has 4 byte. -1, because @pc gets incremented in main loop
end
should be
def jmp addr
@pc=addr/2-1 #each instruction has 2 byte. -1, because @pc gets incremented in main loop
end

I hope next time I'll notice such things earlier :)

Tom
 
J

James Edward Gray II

Hi,

Here is my solution to the current quiz. It has a few flaws, and it
uses a method call for each statement (which is probably quite slow).
The nice thing is that you can dump a program and view it in an
assembler-like way.
The implementation is quite simple, I don't think i need to explain it
in detail. I hope i haven't screwed up somewhere :)

Sure looks good to me. Here's one trivial suggestion though:
if ARGV[0]==nil
STDERR.puts "Usage: #{$0} <program file>\n"
exit 1
end
if !File.exist? ARGV[0]
STDERR.puts "Error: File not found.\n"
exit 1
end
emu = Emulator.new
emu.scan(ARGV[0])
print "Program read: \n"
print "============= \n"
emu.dump_prog
print "\nRunning... \n\n"
emu.run
print "Done! Registers: \n"
print "================ \n"
emu.dump

puts() will add a newline character, if it's not already at the end
of the String you pass to it, so you could lose a lot of \n's above
if you like.

James Edward Gray II
 
T

Tom Rauchenwald

James Edward Gray II said:
Sure looks good to me. Here's one trivial suggestion though:

What bugs me a little bit is that the jmp-code isn't quite right. If i
read the spec right, it would be possible to jump to any byte in the
source-file, even if it's in the middle of an opcode. With my solution
this is not possible. But I realized that after I finished it, and I
didn't want to rewrite it.
if ARGV[0]==nil
STDERR.puts "Usage: #{$0} <program file>\n"
exit 1
end
if !File.exist? ARGV[0]
STDERR.puts "Error: File not found.\n"
exit 1
end
emu = Emulator.new
emu.scan(ARGV[0])
print "Program read: \n"
print "============= \n"
emu.dump_prog
print "\nRunning... \n\n"
emu.run
print "Done! Registers: \n"
print "================ \n"
emu.dump

puts() will add a newline character, if it's not already at the end of
the String you pass to it, so you could lose a lot of \n's above if
you like.

Thanks, and thanks that you manage this whole quiz-thing and do the
summaries and everything.
James Edward Gray II

Tom
 
J

James Edward Gray II

There have been much better solutions, but for the sake of
completeness, here's what I came up with while considering the quiz.

James Edward Gray II

#!/usr/local/bin/ruby -w

require "enumerator"
require "forwardable"

module Kernel
module_function

def NNN(digits)
Integer("0x#{digits.map { |d| d.to_s(16) }.join}")
end
alias_method :KK, :NNN
end

class Chip8
extend Forwardable

MAX_REGISTER = 0b1111_1111
DEFAULT_HANDLER = [ Array.new,
lambda { |em, op| raise "Unknown Op: #
{op.inspect}."} ]

def self.handlers
@@handlers ||= Array.new
end

def self.handle(*pattern, &handler)
handlers << [pattern, handler]
end

handle(1) { |em, op| em.head = NNN(op[-3..-1]) }
handle(3) { |em, op| em.skip if em[op[1]] == KK(op[-2..-1]) }
handle(6) { |em, op| em[op[1]] = KK(op[-2..-1]) }
handle(7) { |em, op| em[op[1]] += KK(op[-2..-1]) }
handle(8, nil, nil, 0) { |em, op| em[op[1]] = em[op[2]] }
handle(8, nil, nil, 1) { |em, op| em[op[1]] |= em[op[2]] }
handle(8, nil, nil, 2) { |em, op| em[op[1]] &= em[op[2]] }
handle(8, nil, nil, 3) { |em, op| em[op[1]] ^= em[op[2]] }
handle(8, nil, nil, 4) do |em, op|
em[op[1]] += em[op[2]]
em[op[1]], em[15] = em[op[1]] - MAX_REGISTER, 1 if em[op[1]] >
MAX_REGISTER
end
handle(8, nil, nil, 5) do |em, op|
em[op[1]] -= em[op[2]]
em[op[1]], em[15] = MAX_REGISTER + 1 + em[op[1]], 1 if em[op[1]]
< 0
end
handle(8, nil, nil, 6) do |em, op|
em[15], em[op[1]] = em[op[1]][0], em[op[1]] >> 1
end
handle(8, nil, nil, 7) do |em, op|
em[op[1]] = em[op[2]] - em[op[1]]
em[op[1]], em[15] = MAX_REGISTER + 1 + em[op[1]], 1 if em[op[1]]
< 0
end
handle(8, nil, 0, 14) do |em, op|
em[15], em[op[1]] = em[op[1]][7], em[op[1]] << 1
end
handle(12) { |em, op| em[op[1]] = rand(MAX_REGISTER) & KK(op
[-2..-1]) }
handle(0, 0, 0, 0) { exit }

def initialize(file)
@addresses = File.read(file).scan(/../m)
@registers = Hash.new

@head = 0
end

attr_accessor :head

def_delegators :mad:registers, :[], :[]=

def run
while op_code = read_one_op_code
find_handler(op_code).call(self, op_code)
trim_registers
end
end

def read_one_op_code
if @head >= @addresses.size
nil
else
@head += 1
@addresses[@head - 1].unpack("HXhHXh").map { |nib| Integer("0x#
{nib}") }
end
end
alias_method :skip, :read_one_op_code

def to_s
@registers.inject(String.new) do |output, (key, value)|
output + "V#{key.to_s(16).upcase}:%08b\n" % value
end
end

private

def find_handler(op_code)
(self.class.handlers + [DEFAULT_HANDLER]).find do |pattern,
handler|
pattern.enum_for:)each_with_index).all? do |match, index|
match.nil? or match == op_code[index]
end
end.last
end

def trim_registers
@registers.each { |name, bits| @registers[name] = MAX_REGISTER &
bits }
end
end

if __FILE__ == $PROGRAM_NAME
emulator = Chip8.new("Chip8Test")
at_exit { puts emulator }
emulator.run
end
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,770
Messages
2,569,583
Members
45,074
Latest member
StanleyFra

Latest Threads

Top