This is a bit of a love letter to Space Invaders, and video games in general.
I started working on this as a simple emulation project, to rekindle my own passion in low level video game hacking, but then realized with a bit of care I could take it further.
So, this is not a simple clone of the game, but rather a painstaking recreation of the source code in clean, readable C code.
I wanted to make something nice that would last a while, as a tribute to the original, and hopefully function a bit like a rosetta stone for future audiences.
Looking through your code, the text character set seems to be coming from the original ROM image. Is that correct? What I thought was interesting was that the glyphs appear 100% identical to the original Apple II character set, down to the pixel. Was there some kind of unexplored connection between Space Invaders and Apple, back in the day?
I have to ask an honest question (as someone who is interested in emulation and old-game restoration as a whole): why C? Did you consider a newer hacker-news-friendly language like D or Rust?
(my usual disclaimer: I'm not asking this passive aggressively, I'm genuinely interested in the answer to this).
I'm obviously not the author, but I can think of a good reason not to use rust.
The project works exactly as the original and recreates the memory state byte for byte, so like the original it has different tasks running at once that are reading and writing to shared memory. Rust's borrow check exists to prevent this sort of thing, because it is so hard to do it correctly or prove it is correct once you have done it. So to use rust, the author would have needed to either totally re-architect Space Invaders, or write the whole thing in ugly, non-idiomatic rust.
Rust simply doesn't let you do the things assembly and C programmers did all the time in 1978 (and with the complexity of our software now and the extra computing power, that's usually for the better). C, on the other hand, has at times been described as "portable assembly," which makes it a good choice for someone wanting to stay true to the original program flow.
This is a good answer, and does reflect a good chunk of the reason for picking C. Dealing with such low level tasks is what the language was made for.
The other big reason I used C, is that it is more of a lingua franca than something more modern, and will make the code more accessible to a wider audience.
> Rust simply doesn't let you do the things assembly and C programmers did all the time in 1978 (and with the complexity of our software now and the extra computing power, that's usually for the better).
Rust does let you do those things. You could run this repo through c2rust and get a working Rust program.
I'm a hacker-news reader and I use C on a daily basis. Seems pretty friendly to me.
Every language has a problem that it is good at solving, and some languages express the underlying idea more clearly than others. In this case, the author has done a bang-up job picking a common, everyday language to solve the problem.
Story time: I wrote an 8080 emulator to play the Space Invaders. I implemented just enough instructions so I could play the game. Problem was, the game's title screen would show, but after a few seconds, it would immediately jump back to the beginning.
First I thought "oh I must have implemented an instruction wrong". So I re-checked all the instructions, read through the Intel 8080 programming manual multiple times. I did find a few errors related to the carry flag, but fixing them didn't change anything.
I then started to actually debug the game code. This was significantly harder than I'd expected. Since the game was in assembly, it was not obvious what each instruction did. What I had to do was pretty much annotate each and every instruction, along with all the memory locations. E.g. address 0xABCD is for player score, 0xCDEF is for player position, instruction X is for drawing the bunker, etc.
It was truly a pain, but it paid off. So the game had an interrupt handler registered to the display refresh signal. And I finally realized the game was constantly interrupted when it was not supposed to. Turns out I had forgotten to disable the signal when entering the handler.
> In the early eighties you would have found the Space Invaders cabinet in an arcade right next to the pinball machines. So a "tilt" switch, like you would find in a pinball machine, would not have seemed as strange as it does today. If you shake, slap, or otherwise physically abuse an SI cabinet you will get a TILT message and your game will end.
Interesting! With pinball machines, that was to prevent cheating, since you could change the course of the ball by smacking the cabinet. (Some players consider doing this without triggering the tilt sensor a legitimate part of the game.) Hilariously, Video Pinball on the Atari 2600 supported it specifically: holding the button while you move the joystick nudges the ball around, but if you do it too much you get a TILT message and can't score any more points until you lose the ball.
It seems like a weird sort of reflex action to put a tilt sensor in a video game, though. Maybe they were just worried about frustrated players damaging the equipment?
Space Cadet on the old versions of Windows supported bumping, too. You could avoid tilt by alternating which side you bumped. Two in a row from the same side would tilt, but alternating sides never did (for me).
I'm firmly in the no-tilt camp, and it's very frustrating that all the pinball games out on Switch right now have no option to disable the tilt controls.
Sure, but if somebody made > terminate URIs properly in one of the forks, it might osmose into HN. One can dream. You can tell that they're still using the same broken code that is in the old ARC distribution.
HN actually has many improvements from the last Arc distribution, speaking as a person who runs my own fork of Anarki and used to run my own fork of the Arc distribution.
Unfortunately, they can't distribute their improvements, due to not having the time or YCombinator not wanting to give up some of their "special sauce" for dealing with spammers and vote rigging, etc.
And for what it's worth (which is likely very little) as a contributor to Anarki, support for > is on my list of things to add to sometime in the (mumble) future. Not that it will do HN much good.
Interesting metrics, you'd expect C to do a lot better in the line-count department than assembly.
I tried building it on Ubuntu, if you follow the instructions the SDL library include files will end up in a directory called SDL so you have to include SDL/SDL.h and even then the build fails with lots of SDL related definitions missing (SDL_Window for instance).
After downloading you'll have to rename the files because the names will all be uppercase:
cd inv1
mv INVADERS.E invaders.e
mv INVADERS.F invaders.f
mv INVADERS.G invaders.g
mv INVADERS.H invaders.h
cd ..
Now the game should work:
./bin/si78c
Some minor nitpicks about the code:
- bracket your ifs and place the starting { on the same line as if/while, or one day you'll sit there staring at the screen for 8 hours trying to figure out why your code no longer works due to an accidentally deleted line.
So:
while (num < 64)
{
int has = SDL_PollEvent(&event_buffer[num]);
if (!has) break;
num++;
}
becomes:
while (num < 64) {
if (SDL_PollEvent(&event_buffer[num])) {
break;
}
num++;
}
> bracket your ifs and place the starting { on the same line as if/while, or one day you'll sit there staring at the screen for 8 hours trying to figure out why your code no longer works due to an accidentally deleted line.
This is some rather strange advice. That and the rewrite dropped a '!'.
That's advice borne from a lifetime of programming C. Good catch about the !, but that was just as an example of the form, not meant as a cut-and-paste replacement.
I'm at 30 years of programming in C, and don't understand the comment. I've never had a problem with either curly brace placement. If a line is deleted, and that line is a curly brace, there will be a syntax error. "git diff" (or whatever you're using) will show you what lines you have deleted.
Both ways have some merit. Cuddled curly is good for ismple one-liners. Curly on next line is when the statement header is stuffed with multi-line expressions.
After all the years, I have settled on:
while (simple) {
}
while ((big || complex)
(condition && multiple && lines))
{
}
for (i = 0; i < MAX; i++) {
}
for (big_initialization;
big && step;
complex, increment++)
{
}
Why are you still going on about this bizarre single line deletion phenomenon?
The only way a single line deletion that contains curly braces will still compile is if it contains balanced curly braces:
if (cond)
{ foo }
stmt;
All styles which avoid this do not have an issue with the deletion of random single line that contains a curly brace.
Regardless of bracing style, we can easily find lines in a program which can be deleted without causing a compile error, just not ones which open a syntax without closing it and vice versa.
Try not to randomly delete lines from your program, and inspect your "git diff" or what have you should such a thing happen.
Well, if you want to go all K&R you can just put the whole thing in a single while line, without the extra conditional inside. Single for line, if you're going for a more 'asshole C expert' style. If you're on your 200th hour trying to capture the flavour of the original 8085 assembly, you might end up writing something like what's in the code.
It may be clever, but short-sighted. It creates complexity for distro builds. sdl2-config has to be compiled for the host machine, and get installed in a build-time toolchain sysroot, and when invoked produce parameters for compiling for the target machine/sysroot. The correct sdl2-config built by the distro build has to be invoked; it must not be one that is locally installed in the build machine's /usr/bin.
Imagine if every package with headers and libs had its' own "<package>-config" program! That would be a huge problem, which is why there is one program called package-config, to which packages or build systems just contribute blurbs of info in .pc files.
Agreed, but still, it is something I didn't know about and I'm always impressed by the encyclopedic knowledge of HN about such arcane subjects. For similar reasons I listed a bunch of 'mv' commands because they will always work rather than the optional 'rename' package which would make it a one liner but less universal.
you'd expect C to do a lot better in the line-count department
You would but it's worth keeping in mind this implementation is constrained by closely following the structure and logic of the original. It's like writing C but being told exactly what state you have to maintain (and where), down to the bit, in advance. It really is a 'hardware simulator' where the spec and input handed to you is the sequence of memory states (and a few other bits) of the original machine.
Good show dragging bracing style into the discussion. Nevermind that it is one of the oldest and least important battlegrounds in the history of programming, at least you got to be needlessly pedantic.
That's where I assumed the comment was going as well, but it's actually not really about brace style at all. Rather it's about "if style" which is a much more interesting discussion (at least as applied to C error checking).
That's not a charitable reading of my comment, it actually does have practical impact and is not just about style but all about function. I could not care less about the looks but found out the hard way that separating the { from if and while constructs can bite you really hard.
It's possible to get the information with any debugger, you just need to know the ABI. On amd64 with the SystemV ABI, its %rax for integer return values and %xmm0 for floating-point return values.
If you don't want to remember, you can write a macro for it.
GDB probably has something builtin for this, but I gave up trying to find features in GDB a long time ago.
[edit]
Stackoverflow seems to be of the opinion that GDB doesn't have a tool for automatically showing the return value of a just-called function. However if you step into a function and execute the command "finish" you will return from the function and print the value. Properly showing the return value with source-level (rather than instruction-level) debugging is a non-trivial feature to get right.
Of course, I can chase return registers by myself all day long, problem is I don't want to. Then again, what about pointers? PODs? strings? If you use VS debugger, you can see how distinctly it can display these, which is as different as night and day to me.
Thanks for pulling the code and trying it out, jacquesm!
That kind of feedback is gold.
I have updated the README with the correct Ubuntu package details.
I think the issue with the includes is likely to do with having both SDL1 and 2 installed, and the slightly dirty way I am pulling in the header (so it works on Mac too).
I will have a bit of a think about how best to resolve that issue, likely needs some ifdefing.
If you like this kind of thing, there's also a faithful re-implementation of the original Elite game, with (readable) source code.
Quoting Wikipedia:
"... around 1999 Christian Pinder developed Elite: The New Kind as a modern PC port of the original BBC Micro version. He achieved a faithful port by reverse-engineering the original assembly written BBC Micro version and recreating a platform neutral C code variant from it, but at David Braben's request this version was withdrawn from distribution in 2003. In September 2014, on Elite's 30th birthday, Ian Bell blessed Elite: The New Kind and re-released it for free on his website. Since then, Elite: The New Kind is also distributed again in version 1.1 by Christian Pinder; a source code mirror is hosted on GitHub."
There's a crash bug in the source: when the local star is to be rendered, the game crashes because it tried to explode the star. There are a couple of forks that solve that rather glaring issue, however I don't recall if they're in a playable state themselves.
The white invaders on a black background was iconic in my childhood, so I was kind of blown away when I first saw a real arcade machine with the painted backdrop and the screen reflected over it with a half silvered mirror (or whatever crazy tech they used in those days).
Thank you for posting this! Me too! I was at a local arcade and came across a real/original spaceinvader cabinet and I was floored at how much of the experience is not captured by an emulator on a laptop. The silvered screen layer is very eye catching and the sound - so much bass really adds to the experience of the dropping invaders.
This is an awesome project with awesome write up. I miss the days of pixelated console games. The constrained environment led to so much creativity. Are there any new games like this being written anymore?
BTW, in case anyone tries to build it on Ubuntu 14.04 x86 with gcc, you'd need `-std=gnu99 -D_GNU_SOURCE` flags, otherwise it barks about stack_t in ucontext.
In that article, Graham Toal discusses a hack where you can bootstrap a new emulator core from an existing one by making a custom emulator that drives both cores at the same time.
After each clock tick, you check the states of both processors for divergence and halt with the diff if there is one.
Co-simulation is widely used for verifying RTL implementations, see fx. dromajo.org but it's a general useful thing and I've used in many contexts, even for verifying a compiler against an interpreter.
It's not like the source for this one was released, this is a re-enactment rather than the original source. But back then the software was compact enough to allow reverse engineering to the point that you could re-implement it faithfully.
Somewhat off-topic rant, but why do websites display long videos as GIFs? Is there some size or compatibility benefit? Because it's really annoying to not be able to pause or rewind these GIFs that take the place of where a video would usually be.
Also, get an HTTPS certificate friend, it's 2019. Dreamhost provides LE certs literally for free.
It's not great compression (The gif itself is about 600k) but these frames at least compress fairly well and produce pixel-perfect output which fits the pixely-game theme.
H264 encoded video can be pretty good for screen recording too. What kills it is a lack of some browsers' support for YUV444. (Chrome works, Firefox doesn't, others I'm not sure) You really don't want YUV422 or YUV420 for screen capture of content mostly composed of small/fine colorful text.
D: The more internet data is encrypted, the less encrypted data stands out. I.e., if you only encrypt data when it's important nobody listen, everyone can tell when you're doing something you don't want anyone to hear. If you encrypt data all the time, nobody can tell "secret" from "normal".
This! ^^^ (I know I should have something substantial to add, so: Everybody hear this, this is the reason for HTTPS Everywhere, all communications encrypted by default in all possible channels 24/7/365.)
This is a bit of a love letter to Space Invaders, and video games in general.
I started working on this as a simple emulation project, to rekindle my own passion in low level video game hacking, but then realized with a bit of care I could take it further.
So, this is not a simple clone of the game, but rather a painstaking recreation of the source code in clean, readable C code.
I wanted to make something nice that would last a while, as a tribute to the original, and hopefully function a bit like a rosetta stone for future audiences.
Enjoy.