[QUIZ] Chip-8 (#88)

R

Ruby Quiz

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!
 
J

James Edward Gray II

by Ethan Price

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

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
 
J

Jacob Fugal

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

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
 
J

James Edward Gray II

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.

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

James Edward Gray II
 
J

Jacob Fugal

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
 
B

Boris Prinz

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

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
@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)
 
A

Alexandru E. Ungur

--Q68bSM7Ycu6FN28Q
Content-Type: text/plain; charset=us-ascii
Content-Disposition: inline

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


--Q68bSM7Ycu6FN28Q
Content-Type: text/plain; charset=us-ascii
Content-Disposition: attachment; filename="cc8.rb"

#!/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)

--Q68bSM7Ycu6FN28Q
Content-Type: text/plain; charset=us-ascii
Content-Disposition: attachment; filename="chip8.rb"

#!/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, @reg]}; 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

--Q68bSM7Ycu6FN28Q
Content-Type: text/plain; charset=us-ascii
Content-Disposition: attachment; filename="cc8-quickref.txt"

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


--Q68bSM7Ycu6FN28Q
Content-Type: text/plain; charset=us-ascii
Content-Disposition: attachment; filename="AnotherTest.c8"

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

--Q68bSM7Ycu6FN28Q
Content-Type: text/plain; charset=us-ascii
Content-Disposition: attachment; filename="Chip8Test.c8"

# .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

--Q68bSM7Ycu6FN28Q--
 
S

Sander Land

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[x] == kk
# 3XKK Skip next instruction if VX == KK
when 4 then @ip += 2 if @regs[x] != kk
# 4XKK Skip next instruction if VX != KK
when 5 then @ip += 2 if @regs[x] == @regs[y]
# 5XY0 Skip next instruction if VX == VY
when 6 then @regs[x] = kk
# 6XKK VX = KK
when 7 then @regs[x] = self.+(@regs[x],kk)
# 7XKK VX = VX + KK
when 8
case opt
when 0 then @regs[x] = @regs[y]
# 8XY0 VX = VY
when 1 then @regs[x] |= @regs[y]
# 8XY1 VX = VX OR VY
when 2 then @regs[x] &= @regs[y]
# 8XY2 VX = VX AND VY
when 3 then @regs[x] ^= @regs[y]
# 8XY3 VX = VX XOR VY
when 4 then @regs[x] = self.+(@regs[x],@regs[y])
# 8XY4 VX = VX + VY
when 5 then @regs[x] = self.-(@regs[x],@regs[y])
# 8XY5 VX = VX - VY
when 6 then @regs[0xF], @regs[x] = @regs[x][0], @regs[x]when 7 then @regs[x] = self.-(@regs[y],@regs[x])
# 8XY7 VX = VY - VX
when 0xE then @regs[0xF], @regs[x] = @regs[x][7], @regs[x]
<< 1 # 8X0E VX = VX SHIFT LEFT 1, VF = most significant bit
else return nil
end
when 9 then @ip += 2 if @regs[x] != @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[x] = kk & rand(0xFF)
# CXKK VX = Random number AND KK
when 0xD then draw_sprite(@regs[x],@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[x]
# EX9E Skip next instruction if key VX pressed
when 0xA1 then @ip +=2 unless $key_pressed.call @regs[x]
# EXA1 Skip next instruction if key VX not pressed
else return nil
end
when 0xF
case kk
when 0x07 then @regs[x] = @delay_timer
# FX07 VX = Delay timer
when 0x0A then @regs[x] = get_keypress
# FX0A Waits a keypress and stores it in VX
when 0x15 then @delay_timer = @regs[x]
# 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[x]
# FX1E I = I + VX
when 0x29 then @I = 5 * @regs[x]
# 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[x].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]
 
D

Doug.Fort

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
 
M

Mitchell Koch

--oyUTqETQ0mS9luUI
Content-Type: text/plain; charset=us-ascii
Content-Disposition: inline

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

--oyUTqETQ0mS9luUI
Content-Type: text/plain; charset=us-ascii
Content-Disposition: attachment; filename="chip8.rb"

#!/usr/bin/env ruby
# Chip-8 emulator

class Chip8
# Load program
def initialize(prog)
# split program into 16 bit opcodes (4 hex digits)
@prog = prog.unpack('S'*(prog.size/2)).inject([]) do |prog,i|
prog << BitArray.new(i,16)
end
# initialize registers V0..VF to random 8 bit values
@regs = (0x0..0xF).inject([]){|m,i| m<<BitArray.rand(8)}
end

# Execute program
def run
addr = 0
while opcode = @prog[addr]
case opcode.hex
when /0000/
break
when /1.../
addr = opcode.digits(1..3).val
next
when /3.../
if @regs[opcode.digits(1).val] == opcode.digits(2..3)
addr += 2
next
end
when /6.../
@regs[opcode.digits(1).val] = opcode.digits(2..3)
when /7.../
@regs[opcode.digits(1).val] += opcode.digits(2..3)
when /8..0/
@regs[opcode.digits(1).val] = @regs[opcode.digits(2).val]
when /8..1/
@regs[opcode.digits(1).val] |= @regs[opcode.digits(2).val]
when /8..2/
@regs[opcode.digits(1).val] &= @regs[opcode.digits(2).val]
when /8..3/
@regs[opcode.digits(1).val] ^= @regs[opcode.digits(2).val]
when /8..4/
@regs[opcode.digits(1).val], @regs[0xF] =
@regs[opcode.digits(1).val] + @regs[opcode.digits(2).val]
when /8..5/
@regs[opcode.digits(1).val], @regs[0xF] =
@regs[opcode.digits(1).val] - @regs[opcode.digits(2).val]
when /8..6/
@regs[opcode.digits(1).val], @regs[0xF] = @regs[opcode.digits(1).val].rshift
when /8..7/
@regs[opcode.digits(1).val], @regs[0xF] =
@regs[opcode.digits(2).val] - @regs[opcode.digits(1).val]
when /8..e/
@regs[opcode.digits(1).val], @regs[0xF] = @regs[opcode.digits(1).val].lshift
when /c.../
@regs[opcode.digits(1).val] = BitArray.rand(8) & opcode.digits(2..3)
end
addr += 1
end
self
end

# Dump registers
def to_s
s = ''
@regs.each_with_index do |reg, idx|
s << "V%X: %08b\n" % [idx, reg.val]
end
s
end
end

# Houses numbers represented by a fixed number of bits
class BitArray
attr_reader :val, :size

def initialize(val, size)
@Val = val
val_size = sprintf('%b', val).size
if val_size > size
raise "Cannot fit #{val} into #{size} bits."
else
@size = size
end
end

# Initialize a bit array with a random value <= size
def BitArray.rand(size)
val = Kernel.rand(eval('0b'+'1'*size))
BitArray.new(val, size)
end

def hex
"%x" % @Val
end

# Make a new BitArray based on the digits of another
def digits(digits)
digits = [digits] unless digits.is_a? Enumerable
hexnum = digits.inject(''){|m,i| m << hex.chr}
numval = eval('0x'+hexnum)
numsize = hexnum.size * 4
BitArray.new(numval, numsize)
end

def &(other)
BitArray.new((val & other.val), size)
end

def |(other)
BitArray.new((val | other.val), size)
end

def ^(other)
BitArray.new((val ^ other.val), size)
end

def +(other)
newval = val + other.val
binval = '%b' % newval
if binval.size > size
[ BitArray.new(eval('0b'+binval[binval.size-size..-1]), size),
BitArray.new(1, 8) ]
else
BitArray.new(newval, size)
end
end

def -(other)
if val >= other.val
[ BitArray.new(val-other.val, size), BitArray.new(1, 8) ]
else
binval = '%b' % val
borrowed = eval('0b1' + '0'*(size-binval.size) + binval)
[ BitArray.new(borrowed-other.val, size), BitArray.new(0, 8) ]
end
end

def rshift
if val % 2 == 0
[BitArray.new(val/2,size), BitArray.new(0,8)]
else
[BitArray.new(val/2,size), BitArray.new(1,8)]
end
end

def lshift
binval = '%b' % val
binval = '0'*(size-binval.size) + binval
[ BitArray.new(eval('0b' + binval[1..-1] + '0'), size),
BitArray.new(binval[0].chr.to_i, 8) ]
end

def ==(other)
(val == other.val) && (size == other.size)
end
end

if __FILE__ == $0
ARGV.each {|f| puts Chip8.new(File.read(f)).run}
end

--oyUTqETQ0mS9luUI--
 
A

Adam Shelly

Your job is to make an emulator...

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

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

class Chip8
class Instruction
attr_reader :eek:p,: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
 
S

Sander Land

I discuss this a little at the end of this summary:
http://www.rubyquiz.com/quiz5.html
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.

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.
 
J

James Edward Gray II

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.

My apologies. I misunderstood.

James Edward Gray II
 

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

No members online now.

Forum statistics

Threads
473,770
Messages
2,569,583
Members
45,073
Latest member
DarinCeden

Latest Threads

Top