|
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:
Internet Janitor fucked around with this message at 05:44 on Aug 26, 2015 |
# ? May 16, 2014 17:49 |
|
|
# ? Apr 25, 2024 05:12 |
|
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 |
# ? May 16, 2014 21:45 |
|
jusion posted:Nowhere nearly as impressive as F8Z, but I threw together a little sample platformer today: You can jump infinitely in midair, I notice.
|
# ? May 16, 2014 21:52 |
|
HottiePippen posted:You can jump infinitely in midair, I notice. Oops! I accidentally shared an old build somehow. I updated it.
|
# ? May 16, 2014 22:00 |
|
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.
|
# ? May 17, 2014 03:13 |
|
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 |
# ? May 17, 2014 15:52 |
|
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:
|
# ? May 17, 2014 20:09 |
|
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!
|
# ? May 17, 2014 20:20 |
|
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:
taqueso fucked around with this message at 20:33 on May 17, 2014 |
# ? May 17, 2014 20:23 |
|
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?
|
# ? May 18, 2014 02:26 |
|
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 |
# ? May 18, 2014 02:29 |
|
HottiePippen posted:but this seems broken: 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:
|
# ? May 18, 2014 02:34 |
|
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: while loops don't work like C. code:
code:
|
# ? May 18, 2014 02:35 |
|
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 |
# ? May 18, 2014 02:41 |
|
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.
|
# ? May 18, 2014 10:29 |
|
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 |
# ? May 19, 2014 21:21 |
|
As a minor heads-up I've made a number of improvements to Octo that should make it nicer to use:
I hope this makes playing with it less frustrating and error prone.
|
# ? May 21, 2014 03:43 |
|
Very nice, especially tabs in the editor! Any chance of getting the ability to define named numeric constants?
|
# ? May 21, 2014 03:54 |
|
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:
|
# ? May 21, 2014 15:29 |
|
Thank you, that is a big improvement in DRY. I was wishing for register aliasing last night, too!
|
# ? May 21, 2014 16:45 |
|
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:
Lime fucked around with this message at 02:20 on May 24, 2014 |
# ? May 24, 2014 01:49 |
|
Lime posted:
code:
|
# ? May 24, 2014 05:11 |
|
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:
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?
|
# ? May 25, 2014 03:49 |
|
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:
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:
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:
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:
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?
|
# ? Jun 26, 2014 06:33 |
|
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:
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.
|
# ? Jun 27, 2014 06:26 |
|
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 |
# ? Jun 27, 2014 06:51 |
|
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.
|
# ? Jun 28, 2014 16:19 |
|
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:
code:
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 |
# ? Jul 6, 2014 21:32 |
|
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:
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 |
# ? Jul 12, 2014 19:06 |
|
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.
|
# ? Jul 17, 2014 03:24 |
|
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.
|
# ? Jul 17, 2014 23:06 |
|
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
|
# ? Jul 21, 2014 04:42 |
|
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.
|
# ? Jul 22, 2014 23:33 |
|
There is interest.
|
# ? Jul 23, 2014 00:25 |
|
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:
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:
code:
code:
code:
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:
Hopefully that provides some interesting food for thought. I'd love to hear any questions or comments.
|
# ? Jul 23, 2014 01:37 |
|
Enjoyed the ending. Wrapped up the story nicely.
|
# ? Jul 23, 2014 03:40 |
|
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/
|
# ? Jul 23, 2014 03:47 |
|
Oh wow I'd read that one before but completely forgot about it.
|
# ? Jul 23, 2014 04:03 |
|
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: In the future I'll see if I can come up with demonstrations of other fun tricks this feature makes more convenient.
|
# ? Jul 24, 2014 05:05 |
|
|
# ? Apr 25, 2024 05:12 |
|
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.
|
# ? Jul 26, 2014 03:37 |