Register a SA Forums Account here!
JOINING THE SA FORUMS WILL REMOVE THIS BIG AD, THE ANNOYING UNDERLINED ADS, AND STUPID INTERSTITIAL ADS!!!

You can: log in, read the tech support FAQ, or request your lost password. This dumb message (and those ads) will appear on every screen until you register! Get rid of this crap by registering your own SA Forums Account and joining roughly 150,000 Goons, for the one-time price of $9.95! We charge money because it costs us money per month for bills, and since we don't believe in showing ads to our users, we try to make the money back through forum registrations.
 
  • Locked thread
Internet Janitor
May 17, 2008

"That isn't the appropriate trash receptacle."
Once upon a time there was an extremely low-cost 8-bit computer called the COSMAC VIP. It was capable of basic video display, came with 2k of ram (expandable to 4k) and was programmed using a hex keypad. Here is a completely realistic and in no way staged presentation of how the VIP was used:



To save memory and ease programming, the VIP came with an interpreter for a very simple bytecode-based programming language called CHIP-8 which included rudimentary collision detection, sprite drawing, numeric display and memory management. In short, the bare minimum you need to write video games!



Chip8 has a 64x32 pixel monochrome display, a little less than 4k of shared program/data space (some of the VIP's low memory is reserved for the interpreter), a 16-level return stack, 16 general-purpose 8-bit registers (The 16th is used as a status flag by some instructions), a 16-button hex keypad (with a really goofy layout) and a "buzzer" which can make some unspecified noise.

Chip8 was lost to obscurity for some time after the death of the COSMAC VIP, but was revived during the 90s through interpreters that ran on HP graphing calculators, and over time interpreters have cropped up for virtually every computing platform. It's a fun and easy way to learn about assembly language, both due to the simplicity of the instruction set and the availability of convenient and interesting IO. Implementing a Chip8 interpreter is also a popular way of learning about emulators.



I've created this thread to gather discussion about Chip8 and serve as a repository for useful information. I think it would be a lot of fun to hold a Chip8-themed game jam- the platform has severe limitations, but they can serve as an excellent seed of creativity. Feel free to discuss Chip8 extensions like the SCHIP, too! If you've written your own Chip8 interpreter, link it and I will add it to the OP.

For my own part, I have recently worked with our own Scaevolus to develop a tiny web-based IDE called Octo that allows you to run Chip8 programs, share them and develop new ones using an assembler I designed. It's surprisingly easy to write complex Chip8 demos right from your browser:


http://johnearnest.github.io/Octo/index.html?gist=4c9fecd91afdb6163964

LINKS AND INFORMATION

Chip8 Information Resources:
Goon Implementations:
Homebrew Games and Programs:
Development Tools:

Internet Janitor fucked around with this message at 05:44 on Aug 26, 2015

Adbot
ADBOT LOVES YOU

jusion
Jan 24, 2007


Nowhere nearly as impressive as F8Z, but I threw together a little sample platformer today:

https://johnearnest.github.io/Octo/index.html?gist=0487868111461d064a7c

I hope to flesh it out a little more, and add a little "win" portal. It's really a fun little exercise, and makes you think in ways you usually wouldn't have to.

jusion fucked around with this message at 01:21 on May 17, 2014

HottiePippen
Nov 5, 2013

jusion posted:

Nowhere nearly as impressive as F8Z, but I threw together a little sample platformer today:

https://johnearnest.github.io/Octo/index.html?gist=7db34b62e9f22dea4fff

I hope to flesh it out a little more, and add a little "win" portal. It's really a fun little exercise, and makes you think in ways you usually wouldn't have to.

You can jump infinitely in midair, I notice.

jusion
Jan 24, 2007


HottiePippen posted:

You can jump infinitely in midair, I notice.

Oops! I accidentally shared an old build somehow. I updated it.

Internet Janitor
May 17, 2008

"That isn't the appropriate trash receptacle."
It's Friday night. Do you know where your RCA 1802 is?


http://johnearnest.github.io/Octo/index.html?gist=c770a15b0b068aaf762e

I've done a bunch of tinkering with various ways to achieve scrolling effects with sprites, and added my findings to my Chip8 programming techniques guide.

jusion
Jan 24, 2007


Cleaned up Thom8s Was Alone a little bit:

https://johnearnest.github.io/Octo/index.html?gist=b09d60941d1a51e1b363

It plays more like an actual game now, and the code is marginally better.

jusion fucked around with this message at 16:30 on May 17, 2014

taqueso
Mar 8, 2004


:911:
:wookie: :thermidor: :wookie:
:dehumanize:

:pirate::hf::tinfoil:
This is fun!


I was looking at the sync function recommended in the Programming Techniques doc. It seems to me that you want to set the delay timer before running the main loop (aka after doing the busy wait), so that the speed will be consistent regardless of the time taken by the rest of the code.
code:
: sync
    loop
        v0 := delay
        if v0 != 0 then
    again
    v0 := 2
    delay := v0
;

Internet Janitor
May 17, 2008

"That isn't the appropriate trash receptacle."
taqueso: That's a good point. My example makes the (often erroneous) assumption that the actual game code takes a negligible or consistent amount of time to run. I'll rewrite that section.

I'm still figuring a lot of things out, so I hope that my notes there will improve in quality and thoroughness along with my tools. If anyone else notices a mistake or has a better approach than I describe feel free to point it out!

taqueso
Mar 8, 2004


:911:
:wookie: :thermidor: :wookie:
:dehumanize:

:pirate::hf::tinfoil:
Because this runs so slow, I think the time the main game code takes to execute will almost always be significant.

I also came up with this debug version that turns on the buzzer if the game code runs over-time:
code:
: sync
    v0 := delay
    v1 := 4
    if v0 == 0 then buzzer := v1
    loop
        v0 := delay
        if v0 != 0 then
    again
    delay := v1
;
The buzzer always goes off on the first call, unless you init the delay timer to be non-zero at startup.

taqueso fucked around with this message at 20:33 on May 17, 2014

HottiePippen
Nov 5, 2013

I think I'm misusing while loops, but I can't figure out what I'm doing wrong. Wondering if someone can spot it. I can do the following:

: subroutine
i := memorylocation
i += vA
load v0
;

: routine
vA := 0
subroutine
vA := 1
subroutine
vA := 2
subroutine
vA := 3
subroutine
;

but this seems broken:

: subroutine
i := memorylocation
i += vA
load v0
vA += 1
;

: routine
vA := 0
while vA != 4 subroutine
;

AFAIKT they should be the same, but they are behaving differently. Any idea what I'm doing wrong?

Internet Janitor
May 17, 2008

"That isn't the appropriate trash receptacle."
I've done some experimenting with using PWM sprite display to simulate additional colors, and things got a bit out of hand.

epilepsy warning:



I call it "Chipenstein 3D". It's still a little flaky but I think I can fine-tune the raycaster to behave better and add niceties like collision. It doesn't actually use very much memory but it should go without saying that this would be completely unusable at anything close to the speed of real hardware.

edit: http://johnearnest.github.io/Octo/index.html?gist=627c8632c41899595f41

Internet Janitor fucked around with this message at 04:04 on May 18, 2014

Internet Janitor
May 17, 2008

"That isn't the appropriate trash receptacle."

HottiePippen posted:

but this seems broken:

: subroutine
i := memorylocation
i += vA
load v0
vA += 1
;

: routine
vA := 0
while vA != 4 subroutine
;

AFAIKT they should be the same, but they are behaving differently. Any idea what I'm doing wrong?

while is actually a keyword for breaking out of a loop…again structure. I'll see about adding an error message to the assembler to warn you about that. Here's an example of a loop using while from the readme:

code:
v0 := 0

# …

loop
    v0 += 1
    while v0 != 5

    # …

again

Scaevolus
Apr 16, 2007

HottiePippen posted:

I think I'm misusing while loops, but I can't figure out what I'm doing wrong. Wondering if someone can spot it. I can do the following:

AFAIKT they should be the same, but they are behaving differently. Any idea what I'm doing wrong?

while loops don't work like C.

code:
loop
BLAH
while CONDITION
BAR
again
could be translated to
code:
while(1) {
  BLAH
  if (!CONDITION) break;
  BAR
}

HottiePippen
Nov 5, 2013

Thanks guys. I did see it could be used to break out of a loop in the example, but I thought that was just a common way to use it rather than the only way.

Also drat IJ, that FPS looks great. That is seriously pushing the (hypothetical) hardware.

HottiePippen fucked around with this message at 03:26 on May 18, 2014

Pollyanna
Mar 5, 2005

Milk's on them.


I'm making an emulator for this too! It's taught me quite a bit of CS fundamentals I wouldn't get otherwise, but I still have a ways to go.

HottiePippen
Nov 5, 2013

I was traveling around on trains much of this weekend, and used the time (productively?) porting Minesweeper to Chip8.



You can play it here: Minesweep8r on Octo. WASD to move, Q to click, E to flag, R to restart.
Or get the code here: Github

I'll probably write up my thoughts and a few of the techniques later, since it's a little unintuitive. I'll post that here once I write it.

HottiePippen fucked around with this message at 21:24 on May 19, 2014

Internet Janitor
May 17, 2008

"That isn't the appropriate trash receptacle."
As a minor heads-up I've made a number of improvements to Octo that should make it nicer to use:

  • You can now type tab characters in the editor.
  • If your program fails to compile, the offending token will be highlighted.
  • Mismatched control constructs (loop, while and again) should now be identified as errors rather than silently producing weird behavior.
  • If a program fails to compile, clicking "run" will no longer obscure the error message and then run the last working version.

I hope this makes playing with it less frustrating and error prone.

taqueso
Mar 8, 2004


:911:
:wookie: :thermidor: :wookie:
:dehumanize:

:pirate::hf::tinfoil:
Very nice, especially tabs in the editor! Any chance of getting the ability to define named numeric constants?

Internet Janitor
May 17, 2008

"That isn't the appropriate trash receptacle."
taqueso: Sure thing. I've added named numeric constants along with another requested feature, register aliases. To quote the updated manual:

quote:

Numeric constants can be defined with :const followed by a name and then a value, which may be a number, another constant or a (non forward-declared) label. Registers may be given named aliases with :alias followed by a name and then a register. The i register may not be given an alias, but v registers can be given as many aliases as desired. Here are some examples of constants and aliases:

code:
:alias x v0
:alias CARRY_FLAG vF
:const iterations 16
:const sprite-height 9

taqueso
Mar 8, 2004


:911:
:wookie: :thermidor: :wookie:
:dehumanize:

:pirate::hf::tinfoil:
Thank you, that is a big improvement in DRY. I was wishing for register aliasing last night, too!

Lime
Jul 20, 2004

Wanted to get in on this, so I made Chip2048


http://johnearnest.github.io/Octo/index.html?gist=b1fa3c3cd504195823ae

WASD tilts the board as usual, 1 starts a new game.

Octo syntax is great, but it feels far enough removed from a bare list of mnemonics that I've got one kind of dumb request. Can you make commas read as whitespace to the tokenizer? I found it easier to deal with the structure of things when I could group commands together on one line that formed one logical unit. Like indexing a table is a least three commands but I liked treating it as one indivisible thing like
code:
i := board i += va load v0
and obviously that would look a lot nicer with something like a comma delimiting each command.

Lime fucked around with this message at 02:20 on May 24, 2014

taqueso
Mar 8, 2004


:911:
:wookie: :thermidor: :wookie:
:dehumanize:

:pirate::hf::tinfoil:

Lime posted:

code:
i := board i += va load v0
I've been putting multiple statements on one line, too. I've been using double spaces to separate them, which seems to be just enough visual whitespace.
code:
i := board  i += va  load v0

Internet Janitor
May 17, 2008

"That isn't the appropriate trash receptacle."
Lime: Your game is really neat- I'm glad you're enjoying chip8 programming! I'll think about your syntax suggestion, but the approach that taqueso suggests is how programmers normally distinguish between "phrases" of instructions together in Forth, which was the inspiration for some of Octo's syntax.

I've made a few more small improvements to the tools:
  • The sprite editor can now parse all the number formats Octo uses, so you can paste binary or decimal representations in and modify them. Modified output is still always in hex for the sake of compactness.
  • The sprite editor has been integrated with Octo itself for more convenient use. The "Sprite Editor" button toggles it.
  • In case you're getting tired of my amber and brown scheme, colors used to display your game can now be customized, as well as the background colors used for silence or a buzzer. These color choices are reflected in the sprite editor and will be saved when you share your programs. Colors can use any format that works in HTML/CSS.

I've also started doing some tinkering with data structures in Chip8. As some of you may have noticed, memory access is very strange and doing anything more complex than saving and restoring a chunk of the register file or indexing into a simple table can get kinda awkward. It's very important to remember that doing a load or store operation adds N+1 to the i register where N is the register index you're loading or saving "up to". Most of the time this is not particularly helpful behavior, and even less helpful is the fact that you can't subtract from i. I took two stabs at writing a modular stack.

In this first implementation I took a fairly straightforward approach using a static storage array and an index. Since in theory these might be called in a complex program I trash only v0 and vF (and of course i) and avoid touching any other registers. vF is pretty much always a good candidate for temporary storage because it's so easily blown away and would not be used for storing persistent state. Push takes 11 cycles and pop takes 8 (not counting the inevitable overhead of a call and return):

http://johnearnest.github.io/Octo/index.html?gist=69b4c6b775f8046c1106

In my second implementation I decided to try using load and save to shift whole chunks of memory at once. This approach is limited, since we can only support a very small stack that fits in available registers, but if we could afford to trash low registers it is significantly faster- Push is 7 cycles and pop is 5. If we have to preserve register contents it's slower at 11/9 respectively. It also has the benefit of preserving v0, though, so it should be usable in nearly any reasonable context:

http://johnearnest.github.io/Octo/index.html?gist=d751c7096b2cfb935157

Scaevolus managed to come up with a slightly better version of my first program by using some really heinous self-modifying code, but I'll let him talk about that if he wants. Can you guys come up with any better approaches? Perhaps a different puzzle?

Internet Janitor
May 17, 2008

"That isn't the appropriate trash receptacle."
Today I decided to do some more tinkering with Chip8 and explore a problem which is at the heart of a number of types of puzzle and strategy games. Given N objects, how do we randomly distribute them over M locations without placing any two at the same location? For example, consider the placement of mines in a game of Minesweeper. There are two common approaches for solving this problem: Guess-and-check or Shuffling.

Consider the locations as an array of booleans initialized to false, which we set to true when we place an object. To Guess-and-check we'll simply choose a random index to place our object, and if that space is occupied, choose another random index. We're done when we've found unoccupied locations for every object.

Java code:
boolean[] locations = new boolean[M];
int objects = N;
while(objects > 0) {
	int index = random(M);
	if (locations[index]) { continue; }
	locations[index] = true;
	objects--;
}
This approach works well if M is significantly larger than N, because we'll always have a wide range of available locations to select. If this is not the case, we may spend quite a bit of time trying and retrying invalid indices. If the random number generator gave us bad choices repeatedly this algorithm might not ever finish!

Another approach is to initialize our destination with the objects we want to place in any positions and then shuffle the items. The most well-known algorithm for a fair in-place shuffle is Fisher–Yates shuffle:

Java code:
boolean[] locations = new boolean[M];
int objects = N;
// distribute the objects:
while(objects --> 0) {
	locations[objects] = true;
}

// perform a shuffle:
for(int i = M-1; i >= 1; i--) {
	int j = random(i);
	boolean t = objects[i];
	objects[i] = objects[j];
	objects[j] = t;
}
Now let's consider how we could do something like this on a Chip8. Since memory operations are a pain, it would be nice to work with bit vectors in registers. The problem now becomes how we can generate a random M-bit number with exactly N bits set. Guess-and-check could work here, but is somewhat undesirable for the reasons I stated earlier. The Fisher-Yates shuffle is also rather difficult to translate since it requires in a large number of bitshifts and the generation of random numbers which are not in a power-of-two range. Since we're working on a game, it may be acceptable to generate bit vectors with distributions which are not completely fair or to consider algorithms which cannot produce every possible bit vector.

The simplest non-fair, non-exhaustive approach is to pre-generate a lookup table with a selection of suitable bit vectors. Here's a table with 32 of the 60 possible 8-bit vectors containing 4 set bits and appropriate code for fetching one into v0:

code:
: bit-vectors
	0xCC 0x39 0x53 0x78 0xF0 0xB1 0x93 0xD8
	0xA9 0x1D 0x55 0xC3 0x3C 0x8D 0xE2 0x63
	0x87 0x56 0xAC 0x27 0x4E 0x36 0x72 0x5C
	0x4D 0x1B 0x0F 0xE4 0xA3 0x5A 0x95 0x2B

: get-vector
	v0 := random 0b11111
	i  := bit-vectors
	i  += v0
	load v0
;
After quite a bit of discussion and experimentation, Scaevolus and I managed to come up with a fairly simple algorithm which can generate any 16-bit permutation with N bits set. Guess-and-check can be modified to complete reliably by choosing a random bit to set, and if it's already set, setting the next 0 bit after it. This works but produces a very biased distribution in which numbers like 0b1111111100000000 are much more likely than 0b010101010101. We can partially compensate for this by picking a second index if our first try has a collision.

For each of the N bits we rotate our bit vector by a random number of bits if the least significant bit is 1. Then we bitwise OR the vector with itself plus one. This has the effect of setting the least significant 0 bit in the bit vector. Then we rotate the vector a second time.

code:
# rotate the output vector
# by a random number of places:
: rotate-random
	v3 := random 0b1111
	loop
		# rotate the 16 bits in v1 and v0
		# using v4 as a temporary:
		v0 <<= v0
		v4 :=  vf
		v1 <<= v1
		v0 |=  vf
		v1 |=  v4

		# repeat random number of times:
		v3 += -1
		if v3 != 0 then
	again
;

# generate a random permutation of
# 16 bits, with N of those bits set:
: permute-bits
	v0 := 0 # output (hi)
	v1 := 0 # output (lo)
	v2 := 8 # loop counter (N)
	        # trash v3, v4
	loop
		# rotate if the LSB of output is set,
		# to avoid creating runs of 1s:
		v3 >>= v1
		if vf != 0 then rotate-random

		# OR output vector with itself + 1
		# to set the least significant 0:
		v3 := v0
		v4 := 1
		v4 += v1
		v3 += vf
		v0 |= v3
		v1 |= v4
	
		# rotate again, to avoid always having
		# 1 as the least significant bit:
		rotate-random

		# repeat N times
		v2 += -1
		if v2 != 0 then
	again
;
Note the trick we use for testing the least significant bit of v1- shift into a temporary register and consult vf. You can use the same approach for testing the most significant bit if you use a left shift instead of a right shift.

Here is a gist containing the tests Scaevolus wrote to evaluate how well-distributed the results of the algorithm are:
https://gist.github.com/rmmh/6a56c0b3dd986605459a

Thoughts? Any other approaches to the problem?

ephphatha
Dec 18, 2009




Hopefully you already noticed but that (still) generates bitstreams which are heavily biased to contain runs of consecutive 1 bits. Your top 16 most frequent sequences are every possible permutation with all 1 bits in a single consecutive run:

(reordered for clarity)
code:
1111111100000000 2186
0111111110000000 2359
0011111111000000 2379
0001111111100000 2294
0000111111110000 2314
0000011111111000 2319
0000001111111100 2241
0000000111111110 2397
0000000011111111 2365
1000000001111111 2349
1100000000111111 2306
1110000000011111 2214
1111000000001111 2294
1111100000000111 2276
1111110000000011 2362
1111111000000001 2276
The next set of most frequent occurrences are sequences where you have 7 consecutive 1 bits with an isolated 1 at some point. You have 112 of those. After that you start to get into the more evenly distributed numbers, but it's still possible to identify the preference for long sequences of consecutive 1 bits throughout most of the first third of the sample.

Plotting the distribution shows the bias fairly clearly:


You can see there are four distinct regions, the most frequent results are the 16 listed above, then there's a sharp drop to the 112 sequences with 7 consecutive bits, then another sharp drop to the sequences with 6 consecutive bits, then a small drop to the rest of the sequences.

Since you're not shuffling you'll never break up any runs of consecutive bits. What might be useful is to reverse one byte when you rotate, it wont be perfect but should improve things somewhat.

Scaevolus
Apr 16, 2007

The previous version had min/max/mean/stddev statistics of 92/11261/777/757, so the extra rotation is a significant improvement.

The conjecture is that it's "random enough" for a Chip8 game, despite significant biases.

I tested it, but reversing a byte before doing the rotation has negligible impact on the distribution -- if you have a run of one bits in a byte, you'll still have that run after you reverse it.

Doing two extra rotation attempts improves the statistics to 588/1346/777/77.

Scaevolus fucked around with this message at 06:53 on Jun 27, 2014

HappyHippo
Nov 19, 2003
Do you have an Air Miles Card?
Seems to me guess-and-check really only gets bad as N approaches M. But you can solve that easily: If N > M / 2, then do guess-and-check for (M-N) and then invert the bits. Now your worst case scenario is N = M / 2, which really isn't all that bad. You're doing on average just under two checks for the last bit you set, and less for the bits before that.

Internet Janitor
May 17, 2008

"That isn't the appropriate trash receptacle."
Last week I visited some friends and we did a little Chip8 game jam. My game was a (loose) adaptation of the classic Dice Wars:


http://johnearnest.github.io/Octo/index.html?gist=1b254e9ee3e44c10604f

This game takes place on a toroidal field of 16 territories. Each has some number of troops stationed there (1-5). At the beginning of the game, territories are divided between you (white) and an AI opponent (black).

You can order your territories to attack adjacent enemy territories provided you have more than one troop available. When you attack, a number of 8-sided dice will be rolled and summed based on the number of attackers and defenders. If the attackers win, they capture the territory and transfer all but one of their troops over. If the defenders win, they destroy all but one of the attackers and lose one troop, or none if they have only a single troop.



After white has taken a turn, each territory has a 50% chance of producing one new troop. The game ends when black or white has conquered the entire map.

When selecting a territory, ASWD move your cursor, E selects the territory and Q ends your turn. With a territory selected, ASWD choose the direction in which to attack, E confirms the attack and Q cancels. When the game is over press any key to play again.

One of my goals was to write a game which could run playably at a speed that is plausible for real hardware. A strategy game seemed like a natural choice, since I can get away with only redrawing small portions of the display most of the time. ChipWar is fairly elaborate but I'm still using less than half of the available RAM!

Along the way I also added a new set of features to Octo- pseudo-ops for <, >, <= and >=. Ordinarily when doing these sort of comparisons you'd use subtraction and consult the borrow bit:

code:
# is v0 < v1 ?
ve := v1 # ve is a temporary copy so we don't destroy v1
ve =- v0
if vf != 0 then ...
I found it very easy to get these kinds of checks subtly wrong, so for the sake of clarity you can now write the above as:

code:
if v0 < v1 then ...
This emits precisely the same machine code as the above example. Doing these comparisons requires a temporary register aside from vf, so by convention I use ve. It is possible to instruct Octo to use a different temporary register by defining a register alias called compare-temp. A test program demonstrating the new operators can be found here:
https://raw.githubusercontent.com/JohnEarnest/Octo/gh-pages/examples/testcompare.8o

Internet Janitor fucked around with this message at 21:54 on Jul 6, 2014

Internet Janitor
May 17, 2008

"That isn't the appropriate trash receptacle."
I did a little tinkering with text display. Chip8 has a built-in hex character set, but if we want to display arbitrary text we'll need to supply a font from scratch. 4x5 characters separated by a pixel of padding work pretty well- they allow us to display 5 rows of 12 reasonably legible characters with a few margin pixels left over to play with.

The most straightforward way to store this data would be a linear array of 5-byte chunks, so the alphabet and three punctuation characters will take 145 bytes. We could then convert a character index into an i-address as follows, assuming font is a label with our font data and v0 contains the character index we're interested in:
code:
i  :=  font
i  +=  v0
v0 <<= v0
v0 <<= v0
i  +=  v0
Multiplying by 5 like this is a little clumsy, though. We could eliminate it if we stored our strings as a list of pre-multiplied offsets into the font data. This presents an interesting possibility: if our offsets aren't restricted to simple multiples of 5 we could save some memory by partially overlapping sequential characters in the font. I wrote a simple program which compresses a font in this manner and converts a string into the necessary list of offsets. For the font I drew, applying this technique saved 33 bytes. Observe the raw font in blue, and the overlapped font in red:



And here's a demonstration of using the data in a program:


http://johnearnest.github.io/Octo/index.html?gist=fc20634c9f53d6929c87

This technique of overlapping sprite data can be very useful if you need to squeeze out a few bytes, and in fact the original Chip8 interpreter did this with its hex font data.

Internet Janitor fucked around with this message at 19:12 on Jul 12, 2014

Internet Janitor
May 17, 2008

"That isn't the appropriate trash receptacle."
Minor note: I corrected an error in the subtraction opcodes of my interpreter. I've updated all the affected example programs. Jusion's "Thom8s Was Alone" was broken by this fix, so I repaired it. The version linked in the OP should work now.

Internet Janitor
May 17, 2008

"That isn't the appropriate trash receptacle."
I wrote a recreation of the Atari 2600 classic, Outlaw:


Not bad for 512 bytes, hunh? Play it here, or if you'd prefer you can learn about it in tutorial form.

TodPunk
Feb 9, 2013

What do you mean, "TRON isn't a documentary?"
I was writing a whack-a-mole. Hit some snags with my logic and it being incredibly slow (load/save are rather slow on chip8 if you rely on them a lot).

After postulating with Internet Janitor about it for a while, basically I'd have to up the cycles to something ridiculous and frame-limit it somehow with the delay timer, or limit it to two moles which I could keep in memory. I started experimenting with a new data structure layout for that and it ended up working out. Maybe 3 would work out, I'm not sure.

Anyway, I've learned a lot but I'm not having fun with this particular project anymore, so I'm going back to other projects. I'm popping the current code out and if someone wants to play with it, I'm not offended (but you might be, looking at the code).

Warning: my draw animation functions don't work right now, but the graphics are top-notch chip8 greatness, if I do say so myself. '1' resets the board/game, and '2' fails to draw a mole.

http://johnearnest.github.io/Octo/index.html?gist=6690bfb2e97fc288fc0f

Internet Janitor
May 17, 2008

"That isn't the appropriate trash receptacle."
This afternoon I put the finishing touches on a Chip8 adventure game.

http://johnearnest.github.io/Octo/index.html?gist=a1d72c7bbf65520fd20d
Traverse a sprawling 16-screen overworld, solve puzzles, move crates around and, with a little luck, watch an incredible ending sequence! Move with ASWD, and in platformer levels use E to pick up/drop crates and Q to reset the level.


Do you have what it takes to be a Cave Explorer?

If there's interest I could talk about some of the techniques I used to write this game.

Subjunctive
Sep 12, 2006

✨sparkle and shine✨

There is interest. :worship:

Internet Janitor
May 17, 2008

"That isn't the appropriate trash receptacle."
OK, then. Sometimes I'm not sure whether I'm just rambling to myself!

The title sequence is made using several full-screen bitmap images. In earlier games I've constructed images by carefully splitting them apart into 8x15 chunks, but this is clumsy and artistically limiting. Instead, I simply wrote a small program to process 1-bit images and convert them into a sequence of hex literals I can paste into Octo. The drawing routine that consumes them looks like this:

code:
: draw-bitmap
	clear
	v0 := 0 # x
	v1 := 0 # y
	v2 := 0 # byte
	v3 := 1 # constant
	loop
		sprite v0 v1 1
		i  += v3
		v2 += 1
		v0 += 8
		if v0 == 64 then v1 += 1
		if v0 == 64 then v0 := 0
		if v1 != 32 then
	again
;
Since I don't have to do any load or store operations in the loop, I can just steadily increment i. Each byte is drawn as an 8x1 sprite, with 8 such sprites per row. I liked the vertical wipe animation this approach produces, but if I wanted to draw as fast as possible I would probably rearrange the data so that I can draw columns as 2 8x15 slices and an 8x2 slice (if necessary), or for maximum uniformity draw columns of 8x8 tiles. It's possible to produce a variety of simple transition effects by drawing portions of the screen in different orders.

Note that bitmaps like this are expensive! A single 32x64 pixel image will take up more than 1/16th your total supply of RAM. Since I only display the title sequence once at the beginning of the game, I reused that memory for scratch buffers in the main game engine. The platformer sequences modify level data in-place, but I need to be able to reset levels if the player makes a mistake. Thus, I start by making a copy of the level data, overwriting part of the title sequence:

code:
: copy-level
	v8 := 0
	loop
		# set i to base of current level
		...
		i += v8
		load v7

		i := level-buffer # overlaid with 'title2'
		i += v8
		save v7

		v8 += 8
		if v8 != 32 then
	again
;
The first time I wrote this routine it was unpleasantly slow, but there was an easy way to speed it up- using load and store to do block copies through the 8 lowest registers. If we're going to put up with the oddities of Chip8 memory operations at least we can find places they actually work to our advantage from time to time! Incidentally if we harness this and the fact that load and store increment i automatically we can write a very tight loop for initializing memory:
code:
: zero-buffer
	i  := buffer
	v8 := 0
	loop
		save v7 # 'stamp' with v0-v7
		v8 += 8
		if v8 != BUFFER_SIZE then # assume the size is a multiple of 8
	again
;
Finally, I used a rather unsavory trick for my text-drawing routine. The string data indexes into the sprite data (for compactness), so I have to juggle i between two locations. i can't be backed up and won't fit in a V-register, so we're stuck. Or are we?

code:
: draw-text
	v1 := 2 # x
	v2 := 1 # y
	v3 := 0 # byte
	# v4 contains length
	loop
		: text-addr i := 0 # self-modify to alter
		i += v3
		load v0
		i := font
		i += v0
		sprite v1 v2 5
		v1 += 5
		if v1 == 62 then v2 += 6
		if v1 == 62 then v1 := 2
		v3 += 1
		if v3 != v4 then
	again
;
Code to call this routine looks like this:
code:
	v0 := 0xA3
	v1 := 0x78
	i  := text-addr
	save v1
	v4 := 19
	draw-text
I stuff the halves of an "i := XXX" instruction into v0 and v1 and clobber those two words of the routine's code before calling it. Remember, folks- the only difference between machine code and a data structure is your frame of mind. Calculating these constant payloads by hand is a bit tedious and I may look into providing Octo with some syntactic sugar.

Most of the rest of the game implementation is fairly mundane. Like with the bitmap images and string/font preparation I discussed earlier I made use of a number of small utility programs to prepare data for consumption by the game engine. Overworld boards were drawn as bitmaps like this (blown up 4x):



And then converted into a sequence of bytes representing columns of 8 4x4 passable or impassable tiles:
code:
: board0
	0x28 0xEB 0x0A 0x7A 0x02 0xEF 0x28 0x2A 
	0x6A 0x4A 0x5A 0x42 0x7A 0x0A 0x6E 0x28 
A series of 4 tables keep track of which board is adjacent in each cardinal direction from the current one. Here's what the whole game map might look like if it was mashed together onto a grid sequentially:



Hopefully that provides some interesting food for thought. I'd love to hear any questions or comments.

HappyHippo
Nov 19, 2003
Do you have an Air Miles Card?
Enjoyed the ending. Wrapped up the story nicely.

Internet Janitor
May 17, 2008

"That isn't the appropriate trash receptacle."
Just in case anyone isn't already familiar, the game is based on my favorite Perry Bible Fellowship strip, of the same name: http://pbfcomics.com/172/

HappyHippo
Nov 19, 2003
Do you have an Air Miles Card?
Oh wow I'd read that one before but completely forgot about it.

Internet Janitor
May 17, 2008

"That isn't the appropriate trash receptacle."
I mentioned the use of self-modifying code to perform address indirection in Cave Explorer. This seems to be a fairly common idiom and comes up in a number of Chip8 roms out in the wild.

An unpleasant side-effect of this was the need to hardcode fixed addresses into the machine code I stored in registers. Octo now has a feature which makes this more elegant, and all the docs have been updated to reflect it.

To quote the revised Octo manual:

quote:

Sometimes you may wish to have the 12-bit address represented by a label available in v registers. Octo provides a command called :unpack for this purpose which expands into a pair of register assignment opcodes. It takes a nybble (0-15 numeric literal or constant) followed by a label as arguments. The lower 8 bits of the address will be stored in v1 and the upper 4 bits of the address will be bitwise ORed with the specified nybble shifted left 4 bits and stored in v0. If the label cucumber represented the address 0x582, the following sets of statements would be identical in meaning:
code:
v0 := 0xA5
v1 := 0x82
code:
:unpack 0xA cucumber
This operation makes it possible to write self-modifying code without hardcoding addresses as numeric literals. If you wish to unpack addresses into registers other than v0 and v1 you can define aliases called unpack-hi or unpack-lo, respectively.

In the future I'll see if I can come up with demonstrations of other fun tricks this feature makes more convenient.

Adbot
ADBOT LOVES YOU

Internet Janitor
May 17, 2008

"That isn't the appropriate trash receptacle."
More features today. I've extended Octo with the ability to emit and emulate SuperChip instructions. These are a set of extensions to the basic Chip8 instruction set introduced when Chip8 emulators were ported to the HP-48 calculator. They are mainly composed of new graphics instructions, including a high-res (comparatively) 128x64 pixel display mode, the ability to draw 16x16 sprites and the ability to scroll the display horizontally or down (although oddly not up). Good documentation for SuperChip is difficult to come by, so don't be surprised if I'm finding mistakes for the next few weeks.



Another major consideration I've addressed is creating some "quirks-mode" compatibility settings. There are a number of chip8 instruction set guides available online containing errors, particulary in the discussion of how load and store alter i and how the shift instructions work. It's basically intractable now as there are countless emulators and programs which depend on one or the other of these behaviors. Octo uses the most authoritative interpretations to the best of my knowledge (backed up by the original documentation and disassemblies of the original interpreter), but you can now configure Octo to use the alternatives via the "Options" pane. These settings are preserved if you "share" your program, although I do not personally recommend writing new programs which depend on the incorrect behavior. Octo is now capable of successfully running a number of previously troublesome games. If you find a game that doesn't work, please try to track down the source of the problem so that I can make corrections or add more quirks-mode settings as necessary.

  • Locked thread