Skip to content
David Clamage edited this page Jun 24, 2016 · 3 revisions

Humble Beginnings

I started the project as most sane programmers would: as a console project. The result was less than 400 lines long, and that's with plenty of whitespace and comments.

It consisted of the following pieces:

Global storage:

static const size_t c_dwAddressSpace = 1 << 15;
static const size_t c_dwNumRegisters = 8;

std::vector<uint16_t> memory(c_dwAddressSpace);
std::vector<uint16_t> registers(c_dwNumRegisters);
std::vector<uint16_t> stack;

Some useful helpers:

uint16_t Translate(uint16_t value)
{
	if (value <= 32767)
	{
		return value;
	}
	if (value <= 32775)
	{
		return registers[value - 32768];
	}
	assert(0 && "Invalid value");
	return 0xFFFF;
}

void Write(uint16_t address, uint16_t value)
{
	if (address <= 32767)
	{
		memory[address] = value;
	}
	else if (address <= 32775)
	{
		registers[address - 32768] = value;
	}
	else
	{
		assert(0 && "Invalid address");
	}
}

Reading in the binary file from commandline:

std::ifstream in(argv[1], std::ifstream::in | std::ifstream::binary);
if (!in.good())
{
	std::cout << "Failed to open file: " << argv[1] << std::endl;
	return 1;
}

size_t dwCurOffset = 0;
while (in.good())
{
	in.read((char *)&memory[dwCurOffset++], sizeof(uint16_t));
}

And finally, a giant loop with a switch statement:

uint16_t inst = 0;
bool halted = false;
while (!halted)
{
	const uint16_t op = memory[inst++];

	switch (op)
	{
		// All the cases
	}
}

The source for this console VM is available on my own github, at this link: https://github.com/dclamage/SynacorVM

Some notable events:

  1. The documentation was not always clear whether a parameter should be treated as an immediate value, or whether it should be dereferenced as a memory address. This was particularly hairy for me with the RMEM and WMEM instructions. I went back and forth a few times on how they should work. In fact, at one point I had RMEM correct, but WMEM incorrect. Then I fixed WMEM and then applied that fix to RMEM, breaking it in the process.

  2. When running the challenge.bin, even with just a couple ops implemented, it will print out code #2.

  3. After printing the code, it runs a self test. This was the first time I started grasping how clever this challenge was. The self test essentially bootstraps the opcodes, in that they are tested in a specific order such that one is verified to work properly before being used as a dependency to verify other opcodes. Without the self test, there would have been much more headache involved in getting the opcodes exactly correct.

  4. After passing all self tests, you get code #3.

How is this only 3/8?

Oh, and after passing the tests, you end up playing a Zork-like adventure game! I figured this was the victory lap... boy was I wrong.

3. Zork-like

Clone this wiki locally