Please Forward: Ruby Quiz Submission

From: "Glen F. Pankow" <Glen.F.Pankow@noaa.gov>
Date: July 31, 2006 12:15:35 PM CDT
To: submission@rubyquiz.com
Subject: Please Forward: Ruby Quiz Submission

Dear sir:

I thought that since this week's Ruby Quiz was very simple,
I'd give it a go. If reasonable, please forward this as a
solution.

I don't know much about the Quiz culture, so please let me
know if I'm doing something verboten. For one thing, it's
over 500 lines long, much of it due to my verbose commenting
style (actually, not as verbose as I would normally write it).
The comments are plain -- I could try to rdoc them if that's
preferred. Please let me know if there's anything I can do
to make it more appropriate for the Ruby community. Thanks.

Glen Pankow
#! /usr/bin/ruby
#
# chip8 -- framework for running a [limited] CHIP-8 program.
#
# Usage: chip8 [ -d ] [ <infile> ]
#
# Run the program for the CHIP-8 raw-nibbles file <infile>. If not
# specified, 'Chip8Text.txt' is used for it. If the file doesn't exist, the
# Ruby Quiz 88 test raw-nibbles are fudged for it.
#
# 'limited' here means the requirements for Ruby Quiz 88 are supported.
#
# Glen Pankow 07/29/06 Original version.
#

#
# Register -- class to embody the state of a [limited] CHIP-8 data register.
#
# This class method is supported:
#
# register = Register.new(hexchar) -- create a new Register object for a
# hex character '0'..'9' or 'A'..'F'.
#
# These instance methods are supported:
#
# name -- [String] the name of the register (e.g., 'V3').
#
# hexchar -- [String] the hex character portion of the name (e.g., '3').
#
# value/value= -- [Fixnum in range 0..255] the value held by the register.
#
# dump -- print a representation of the register to the standard output
# device.
#
class Register

    attr_reader :name, :hexchar
    attr_accessor :value

    def initialize(hexchar)
        @name, @hexchar, @value = 'V' + hexchar, hexchar, 0
# @value = rand(256)
    end

    def dump
        printf "register %s: %08b (%02x %3d)\n", @name, @value, @value, @value
    end
end

#
# Instruction -- classes to embody a disassembled [limited] CHIP-8
# instruction.
#
# This class method is supported:
#
# instruction = Instruction.new(trace, proc, *args) -- create a new
# Instruction object. trace is a human-readable form of the original
# instruction nibbles, proc is a process that modifies its master's
# state during execution, and args are any extra parameters to be passed
# to proc.
#
# These instance methods are supported:
#
# here/here= -- [TrueClass/FalseClass] whether this is the next
# instruction to be executed (in debug mode).
#
# execute -- call the instruction's process on the configured arguments.
#
# dump -- print a representation of the instruction to the standard output
# device.
#
class Instruction

    attr_accessor :here

    def initialize(trace, proc, *args)
        @trace, @proc, @args, @here = trace, proc, args, false
    end

    def execute
        @proc.call(*@args)
    end

    def dump
        print((@here ? '--> ' : ' '), @trace, "\n")
    end
end

#
# Program -- class to embody the construction and running of a [limited]
# CHIP-8 program.
#
# This class method is supported:
#
# program = new(infileName) -- create a new Program object from a raw-
# nibbles program file (or Ruby Quiz 88 data).
#
# These instance methods are supported:
#
# run -- step through the program from beginning to end.
#
# debug -- step through the program incrementally.
#
# reset -- explicitly set the state of the program to the beginning
# (sort of -- register contents are not reset). You only need call
# this if you're doing manualy stepping through the code.
#
# step(print_trace = true) -- execute the next instruction of the program.
# An automatic call to reset is made if an exit instruction was last
# executed.
#
# running -- [TrueClass/FalseClass] whether the program is currently being
# stepped through (see the documentation to step() below).
#
# dump(full = true) -- print a representation of the program to the
# standard output device. If full = false, only the registers are dumped.
#
class Program

    attr_accessor :running, :program_counter
    attr_reader :registers

    def initialize(infileName)

        #
        # Set up the program registers.
        #
        @registers = # the program registers
        # (0..15).each { |i| @registers << Register.new(sprintf('%X', i)) }
        ('0'..'9').each { |hexchar| @registers << Register.new(hexchar) }
        ('A'..'F').each { |hexchar| @registers << Register.new(hexchar) }

        #
        # Read in all the nibbles of the program.
        #
        # Fudge some input for Ruby Quiz 88 testing if no other input is found.
        #
        @nibbles = # the program stack (nibbles version)
        @hexchars = # the program stack (nibbles hexchar version)
        if (File.exists?(infileName))
            infile = File.open(infileName)
        else
            require 'stringio'
            infile = StringIO.new( \
              "\x61\x77\x62\x45\x71\x01\x83\x20\x81\x21\x81\x22" \
              "\x82\x33\x81\x34\x82\x35\x81\x06\x83\x27\x83\x0e" \
              "\x64\xff\xc4\x11\x32\xbb\x10\x00\x00\x00")
        end
        @hexchars = infile.readlines.join.unpack('H*')[0].split(//)
        @nibbles = @hexchars.collect { |hexchar| hexchar.hex }
        infile.close

        #
        # Disassemble the nibbles into instructions.
        #
        @instructions =
        raw_program_counter = 0
        while (raw_program_counter < @nibbles.size)
            @instructions << disassemble(raw_program_counter)
            3.times { @instructions << nil } # pad (see note below)
            raw_program_counter += 4
        end
        #
        # Note: for simplicity, we'll keep our instruction array aligned with
        # our raw bytes arrays, since the program counter is really just an
        # offsets in our arrays.
        #

        #
        # Leave the freshly-disassembled program in a ready-to-run state.
        #
        reset
    end

    #
    # instruction = disassemble(raw_program_counter)
    #
    # Parse the next four raw nibbles and their character equivalents from
    # @nibbles and @hexchars offset by raw_program_counter; convert them into
    # a new Instruction object. An exception is raised on any parsing error.
    #
    # We call it raw_program_counter because we're not emulating any run-time
    # activity here (we're just stepping through it assuming a strict
    # correlation between four raw nibbles and one instruction), and we don't
    # want to confuse this with the accessors to the run-time program counter
    # @program_counter.
    #
    def disassemble(raw_program_counter)

        case @hexchars[raw_program_counter]
        when nil
            trace = sprintf("%03x: 0000 Abnormal exit!", raw_program_counter)
            proc = lambda { @running = false }
            Instruction.new(trace, proc)
        when '0'
            trace = sprintf("%03x: 0000 Exit", raw_program_counter)
            proc = lambda { @running = false }
            Instruction.new(trace, proc)
        when '1'
            address = get__NNN(raw_program_counter)
            trace = sprintf("%03x: 1%03x Jump to the address %03x of the file",
              raw_program_counter, address, address)
            proc = lambda { |address| @program_counter = _address }
            Instruction.new(trace, proc, address)
        when '3'
            register, value = get__XKK(raw_program_counter)
            trace = sprintf( \
              "%03x: 3%s%02x Skip next instruction if V%s == %02x",
              raw_program_counter, register.hexchar, value,
              register.hexchar, value)
            proc = lambda do |_register, _value|
                @program_counter += 4
                @program_counter += 4 if (_register.value == _value)
            end
            Instruction.new(trace, proc, register, value)
        when '6'
            register, value = get__XKK(raw_program_counter)
            trace = sprintf("%03x: 6%s%02x V%s = %02x", raw_program_counter,
              register.hexchar, value, register.hexchar, value)
            proc = lambda do |_register, _value|
                _register.value = _value
                @program_counter += 4
            end
            Instruction.new(trace, proc, register, value)
        when '7'
            register, value = get__XKK(raw_program_counter)
            trace = sprintf("%03x: 7%s%02x V%s = V%s + %02x",
              raw_program_counter, register.hexchar, value, register.hexchar,
              register.hexchar, value)
            proc = lambda do |_register, _value|
                newValue = _register.value + _value
                if (newValue > 0x00ff)
                    _register.value = newValue & 0x00ff
                    @registers[15].value = 1
                else
                    _register.value = newValue
                    @registers[15].value = 0
                end
                @program_counter += 4
            end
            Instruction.new(trace, proc, register, value)
        when '8'
            register1, register2, type = get__XYn(raw_program_counter)
            case type
            when '0'
                trace = sprintf("%03x: 8%s%s0 V%s = V%s", raw_program_counter,
                  register1.hexchar, register2.hexchar,
                  register1.hexchar, register2.hexchar)
                proc = lambda do |_register1, _register2|
                    _register1.value = _register2.value
                    @program_counter += 4
                end
                Instruction.new(trace, proc, register1, register2)
            when '1'
                trace = sprintf("%03x: 8%s%s1 V%s = V%s OR V%s",
                  raw_program_counter, register1.hexchar, register2.hexchar,
                  register1.hexchar, register1.hexchar, register2.hexchar)
                proc = lambda do |_register1, _register2|
                    _register1.value |= _register2.value
                    @program_counter += 4
                end
                Instruction.new(trace, proc, register1, register2)
            when '2'
                trace = sprintf("%03x: 8%s%s2 V%s = V%s AND V%s",
                  raw_program_counter, register1.hexchar, register2.hexchar,
                  register1.hexchar, register1.hexchar, register2.hexchar)
                proc = lambda do |_register1, _register2|
                    _register1.value &= _register2.value
                    @program_counter += 4
                end
                Instruction.new(trace, proc, register1, register2)
            when '3'
                trace = sprintf("%03x: 8%s%s3 V%s = V%s XOR V%s",
                  raw_program_counter, register1.hexchar, register2.hexchar,
                  register1.hexchar, register1.hexchar, register2.hexchar)
                proc = lambda do |_register1, _register2|
                    _register1.value ^= _register2.value
                    @program_counter += 4
                end
                Instruction.new(trace, proc, register1, register2)
            when '4'
                trace = sprintf("%03x: 8%s%s4 V%s = V%s + V%s",
                  raw_program_counter, register1.hexchar, register2.hexchar,
                  register1.hexchar, register1.hexchar, register2.hexchar)
                proc = lambda do |_register1, _register2|
                    newValue = _register1.value + _register2.value
                    if (newValue > 0x00ff)
                        _register1.value = newValue & 0x00ff
                        @registers[15].value = 1
                    else
                        _register1.value = newValue
                        @registers[15].value = 0
                    end
                    @program_counter += 4
                end
                Instruction.new(trace, proc, register1, register2)
            when '5'
                trace = sprintf("%03x: 8%s%s5 V%s = V%s - V%s",
                  raw_program_counter, register1.hexchar, register2.hexchar,
                  register1.hexchar, register1.hexchar, register2.hexchar)
                proc = lambda do |_register1, _register2|
                    if (_register1.value >= _register2.value)
                        _register1.value -= _register2.value
                        @registers[15].value = 1
                    else
                        _register1.value += 0x0100 - _register2.value
                        @registers[15].value = 0
                    end
                    @program_counter += 4
                end
                Instruction.new(trace, proc, register1, register2)
            when '6'
                trace = sprintf("%03x: 8%s06 V%s = V%s SHIFT RIGHT 1",
                  raw_program_counter,
                  register1.hexchar, register1.hexchar, register1.hexchar)
                proc = lambda do |_register|
                    @registers[15].value = _register.value & 0x0001
                    _register.value >>= 1
                    @program_counter += 4
                end
                Instruction.new(trace, proc, register1)
            when '7'
                trace = sprintf("%03x: 8%s%s7 V%s = V%s - V%s",
                  raw_program_counter, register1.hexchar, register2.hexchar,
                  register1.hexchar, register2.hexchar, register1.hexchar)
                proc = lambda do |_register1, _register2|
                    if (_register2.value >= _register1.value)
                        _register1.value = _register2.value - _register1.value
                        @registers[15].value = 1
                    else
                        _register1.value \
                          = _register2.value + 0x0100 - _register1.value
                        @registers[15].value = 0
                    end
                    @program_counter += 4
                end
                Instruction.new(trace, proc, register1, register2)
            when 'e'
                trace = sprintf("%03x: 8%s0e V%s = V%s SHIFT LEFT 1",
                  raw_program_counter,
                  register1.hexchar, register1.hexchar, register1.hexchar)
                proc = lambda do |_register|
                    @registers[15].value = _register.value & 0x0080
                    _register.value <<= 1
                    _register.value &= 0x00ff
                    @program_counter += 4
                end
                Instruction.new(trace, proc, register1)
            else
                raise ArgumentError,
                  sprintf("Invalid instruction 8%s%s%s at %03x",
                    @hexchars[raw_program_counter + 1],
                    @hexchars[raw_program_counter + 2],
                    @hexchars[raw_program_counter + 3], raw_program_counter)
            end
        when 'c'
            register, value = get__XKK(raw_program_counter)
            trace = sprintf("%03x: C%s%02x V%s = Random number AND %02x",
              raw_program_counter,
              register.hexchar, value, register.hexchar, value)
            proc = lambda do |_register, _value|
                _register.value = rand(256) & value
                @program_counter += 4
            end
            Instruction.new(trace, proc, register, value)
        else
            raise ArgumentError,
              sprintf("Invalid instruction %s%s%s%s at %03x",
                @hexchars[raw_program_counter],
                @hexchars[raw_program_counter + 1],
                @hexchars[raw_program_counter + 2],
                @hexchars[raw_program_counter + 3], raw_program_counter)
        end
    end
    protected :disassemble

    #
    # address = get_NNN(program_counter) -- return the 3-nibble address
    # literal value starting at program_counter + 1
    #
    def get__NNN(program_counter)
        @nibbles[program_counter + 1] << 8 \
          > @nibbles[program_counter + 2] << 4 \
          > @nibbles[program_counter + 3]
    end

    #
    # register, value = get__XKK(program_counter) -- return the register and
    # 2-nibble literal value starting at program_counter + 1
    #
    def get__XKK(program_counter)
        return get__X__(program_counter), get___KK(program_counter)
    end

    #
    # register = get__X__(program_counter) -- return the register at
    # program_counter + 1
    #
    def get__X__(program_counter)
        @registers[@nibbles[program_counter + 1]]
    end

    #
    # value = get___KK(program_counter) -- return the two-nibble literal
    # value starting at program_counter + 2
    #
    def get___KK(program_counter)
        @nibbles[program_counter + 2] << 4 | @nibbles[program_counter + 3]
    end

    #
    # register1, register2, type = get__XYn(program_counter) -- return the
    # registers and selector value starting at program_counter + 1
    #
    def get__XYn(program_counter)
        return @registers[@nibbles[program_counter + 1]],
          @registers[@nibbles[program_counter + 2]],
          @hexchars[program_counter + 3]
    end
    protected :get__NNN, :get__XKK, :get__X__, :get___KK, :get__XYn

    #
    # Reset the program so that the next execution step occurs at the start of
    # the program. Note that the registers are not cleared.
    #
    def reset
        @program_counter = 0
        @running = true
    end

    #
    # run -- run the program from beginning to end.
    #
    def run
        reset
        while (@running)
            step
        end
    end

    #
    # debug -- step through the program incrementally (also from beginning to
    # end).
    #
    def debug
        reset
        while (@running)
            instruction = @instructions[@program_counter]
            instruction.here = true
            @registers.each do |register|
next unless (((register.hexchar >= '1') && (register.hexchar <= '4')) \
  >> (register.hexchar == 'F'))
# ignore V5..VE for the quiz
                register.dump
            end
            @instructions.each do |inst|
                next if (inst.nil?) # skip padding
                inst.dump
            end
            print "Hit <enter> to execute the next instruction: "
            $stdout.flush
            dummy = $stdin.gets
            instruction.here = false
            puts
            step
        end
    end

    #
    # step(print_trace = true)
    #
    # Execute the next instruction (the instruction at the current program
    # counter, which is moved to the next instruction on exit). If print_trace
    # is true, a human-readable form of the instruction is printed.
    #
    # Also, running past the end of the instructions is treated like an exit.
    #
    def step(print_trace = true)
        reset unless (@running)
        instruction = @instructions[@program_counter]
        if (instruction.nil?)
            printf("%03x: ---- Premature EOF -- exit!\n", @program_counter) \
              if (print_trace)
            @running = false
            return
        end
        instruction.dump if (print_trace)
        instruction.execute
    end

    def dump(full = true)
        @registers.each do |register|
next unless (((register.hexchar >= '1') && (register.hexchar <= '4')) \
  >> (register.hexchar == 'F'))
# ignore V5..VE for the quiz
            register.dump
        end
        return unless (full)
        printf "program_counter: %03x\n", @program_counter
        (0...@nibbles.size).each do |i|
            printf "%03x: nibble = 0x%02x, hexchar = '%s'\n",
              i, @nibbles[i], @hexchars[i]
        end
    end
end

#
# Go for it!
#
do_debug = false
infile = 'Chip8Text.txt'
ARGV.each do |arg|
    if (arg == '-d')
        do_debug = true
    else
        infile = arg
    end
end
program = Program.new(infile)
if (do_debug)
    program.debug
else
    program.run
    program.dump(false)
end

···

Begin forwarded message:

There's certainly nothing wrong with your solution and we thank you for sharing it with us.

I do encourage you to glance at the other solutions people have sent in. You might get an idea or two about shortening up the code a little, if you're interested.

Thanks again.

James Edward Gray II

···

On Jul 31, 2006, at 1:29 PM, James Edward Gray II wrote:

Begin forwarded message:

From: "Glen F. Pankow" <Glen.F.Pankow@noaa.gov>
Date: July 31, 2006 12:15:35 PM CDT
To: submission@rubyquiz.com
Subject: Please Forward: Ruby Quiz Submission

Dear sir:

I thought that since this week's Ruby Quiz was very simple,
I'd give it a go. If reasonable, please forward this as a
solution.

I don't know much about the Quiz culture, so please let me
know if I'm doing something verboten. For one thing, it's
over 500 lines long, much of it due to my verbose commenting
style (actually, not as verbose as I would normally write it).
The comments are plain -- I could try to rdoc them if that's
preferred. Please let me know if there's anything I can do
to make it more appropriate for the Ruby community. Thanks.