gb: added presets

This commit is contained in:
Steven Hugg committed 2026-09-24 16:56:08 +02:00
1 parent 602e56db58
commit 20e9827210
9 files changed
+672 -11

No files matched your search

+135
View File
@@ -0,0 +1,135 @@
/*
Color Game Boy demo.
A CGB has 8 background and 8 sprite palettes of 4 colors each, chosen
from a 15-bit RGB palette, plus a double-speed CPU mode. This demo checks
_cpu to see which machine it is running on. On a CGB it:
- pulses one background palette entry smoothly with
set_bkg_palette_entry(),
- toggles between vivid color and the DMG-compatible grayscale palettes
with cgb_compatibility() (press A),
- switches the CPU between normal and double speed with cpu_slow() and
cpu_fast() (press START), and shows the difference with a busy-loop
counter that counts how many iterations fit between two VBlanks.
On a monochrome Game Boy it falls back to the four-shade DMG palettes.
*/
//#link "gb/sfr.sgb"
//#link "gb/crt0.sgb"
//#resource "gb/global.sgb"
#include <stdint.h>
#include "gb/types.h"
#include "gb/hardware.h"
#include "gb/gb.h"
#include "gb/cgb.h"
#include "gbtext.h"
// 2x2 checkerboard tile (colors 1 and 2)
static const uint8_t checker_tile[] = {
/*;;{w:8,h:8,bpp:1,count:1,brev:1,np:2,pofs:1,sl:2};;*/
0xCC,0xCC,0x33,0x33,0xCC,0xCC,0x33,0x33,
0x33,0x33,0xCC,0xCC,0x33,0x33,0xCC,0xCC
/*;;*/
};
// background palette. color 1 is replaced each frame by pulse().
palette_color_t colors[4] = {
RGB_BLACK, RGB_RED, RGB_DARKBLUE, RGB_WHITE
};
// smooth pastel rainbow: three triangle waves 120 degrees apart
static uint16_t pulse(uint8_t t) {
uint8_t r = (t + 0) & 63;
uint8_t g = (t + 21) & 63;
uint8_t b = (t + 42) & 63;
if (r >= 32) r = 63 - r;
if (g >= 32) g = 63 - g;
if (b >= 32) b = 63 - b;
return RGB(r, g, b);
}
volatile uint8_t vbl_flag;
void vbl_handler(void) NONBANKED { vbl_flag++; }
uint8_t cgb_mode;
uint8_t color_mode = 1;
uint8_t fast_mode = 0;
void main(void) {
uint8_t keys, last_keys = 0;
uint8_t frame = 0;
char buf[6];
cgb_mode = DEVICE_SUPPORTS_COLOR;
DISPLAY_OFF;
BGP_REG = OBP0_REG = OBP1_REG = 0xE4;
font_init();
set_bkg_data(0, 1, checker_tile);
fill_bkg_rect(0, 0, 32, 32, 0);
put_str(1, 0, cgb_mode ? "GAME BOY COLOR" : "GAME BOY");
put_str(1, 2, "A: PALETTE");
put_str(1, 3, "START: SPEED");
put_str(1, 5, "SPEED: NORMAL");
put_str(1, 7, "WORK: 00000");
if (cgb_mode)
set_bkg_palette(0, 1, colors);
add_VBL(vbl_handler);
set_interrupts(VBL_IFLAG);
SHOW_BKG;
DISPLAY_ON;
while (1) {
uint8_t start;
uint16_t work;
int8_t b;
wait_vbl_done();
keys = joypad();
// toggle color vs. DMG-compatible grayscale palettes
if ((keys & J_A) && !(last_keys & J_A)) {
color_mode = !color_mode;
if (cgb_mode) {
if (color_mode)
set_bkg_palette(0, 1, colors);
else
cgb_compatibility();
}
}
// toggle double speed
if ((keys & J_START) && !(last_keys & J_START)) {
fast_mode = !fast_mode;
if (cgb_mode) {
if (fast_mode) cpu_fast(); else cpu_slow();
}
put_str(8, 5, fast_mode ? "DOUBLE" : "NORMAL");
}
last_keys = keys;
// benchmark: count loop iterations between two VBlanks
start = vbl_flag;
work = 0;
while (vbl_flag == start) work++;
buf[5] = '\0';
for (b = 4; b >= 0; b--) {
buf[b] = '0' + (work % 10);
work /= 10;
}
put_str(7, 7, buf);
// pulse palette entry 1 (CGB only)
if (cgb_mode && color_mode)
set_bkg_palette_entry(0, 1, pulse(frame));
frame++;
}
}
+103
View File
@@ -0,0 +1,103 @@
/*
Interrupts on Game Boy.
Three interrupts matter in most games: the VBlank at ~60 Hz, the
programmable timer, and the LCD status line (see raster.c). GBDK installs
a VBlank handler by default; add_VBL()/add_TIM() append your own handler
to an interrupt's chain, and remove_*() takes it back out.
This demo counts VBlanks and timer overflows in two handlers. The timer
runs at 65536 Hz, so with TMA=0 it overflows about 256 times a second,
much faster than the VBlank. Press START to remove both handlers, then
press it again to add them back.
*/
//#link "gb/sfr.sgb"
//#link "gb/crt0.sgb"
//#resource "gb/global.sgb"
#include <stdint.h>
#include "gb/types.h"
#include "gb/hardware.h"
#include "gb/gb.h"
#include "gbtext.h"
// interrupt counters (volatile: written by the handlers)
volatile uint8_t vbl_count;
volatile uint16_t tim_count;
uint8_t handlers_installed = 1;
// VBlank handler: called once per frame
void vbl_handler(void) NONBANKED {
vbl_count++;
}
// timer handler: called when TIMA_REG overflows
void tim_handler(void) NONBANKED {
tim_count++;
}
// write a zero-padded 5-digit decimal number to the tile map
static void put_u16(uint8_t x, uint8_t y, uint16_t v) {
char buf[6];
int8_t i;
buf[5] = '\0';
for (i = 4; i >= 0; i--) {
buf[i] = '0' + (v % 10);
v /= 10;
}
put_str(x, y, buf);
}
void main(void) {
uint8_t keys, last_keys = 0;
DISPLAY_OFF;
BGP_REG = OBP0_REG = OBP1_REG = 0xE4;
font_init();
put_str(4, 0, "INTERRUPTS");
put_str(2, 3, "VBLANK");
put_str(2, 4, "00000");
put_str(2, 7, "TIMER");
put_str(2, 8, "00000");
put_str(2, 12, "START: PAUSE");
// timer overflows every 256 ticks at 65536 Hz
TMA_REG = 0x00;
TAC_REG = 0x06;
// install our handlers and enable their interrupts
add_VBL(vbl_handler);
add_TIM(tim_handler);
set_interrupts(VBL_IFLAG | TIM_IFLAG);
SHOW_BKG;
DISPLAY_ON;
while (1) {
uint16_t t;
wait_vbl_done();
// edge-detect START to add/remove the handlers
keys = joypad();
if ((keys & J_START) && !(last_keys & J_START)) {
handlers_installed = !handlers_installed;
if (handlers_installed) {
add_VBL(vbl_handler);
add_TIM(tim_handler);
put_str(2, 12, "START: PAUSE");
} else {
remove_VBL(vbl_handler);
remove_TIM(tim_handler);
put_str(2, 12, "START: GO");
}
}
last_keys = keys;
// read the 16-bit timer count without a torn read
CRITICAL { t = tim_count; }
put_u16(2, 4, vbl_count);
put_u16(2, 8, t);
}
}
+107
View File
@@ -0,0 +1,107 @@
/*
Joypad input on Game Boy.
waitpad() blocks until the chosen buttons are held, and waitpadup() waits
for them to be released again -- handy for title screens. For gameplay
you poll joypad() once per frame and compare against the previous frame
to find edges: a button that is down now but was up before has just been
pressed.
This demo waits for START, then moves a sprite with the d-pad while
counting A and B presses with edge detection.
*/
//#link "gb/sfr.sgb"
//#link "gb/crt0.sgb"
//#resource "gb/global.sgb"
#include <stdint.h>
#include "gb/types.h"
#include "gb/hardware.h"
#include "gb/gb.h"
#include "gbtext.h"
// an 8x8 ball sprite (2bpp)
static const uint8_t ball_tile[] = {
/*;;{w:8,h:8,bpp:1,count:1,brev:1,np:2,pofs:1,sl:2};;*/
0x3C,0x3C,0x42,0x7E,0x99,0xFF,0xA9,0xFF,
0x89,0xFF,0x89,0xFF,0x42,0x7E,0x3C,0x3C
/*;;*/
};
uint8_t a_count, b_count;
// write a zero-padded 3-digit decimal number
static void put_u8(uint8_t x, uint8_t y, uint8_t v) {
char buf[4];
buf[3] = '\0';
buf[2] = '0' + (v % 10);
buf[1] = '0' + (v / 10) % 10;
buf[0] = '0' + (v / 100) % 10;
put_str(x, y, buf);
}
void main(void) {
uint8_t keys, last_keys;
uint8_t x = 80, y = 72;
DISPLAY_OFF;
BGP_REG = OBP0_REG = OBP1_REG = 0xE4;
font_init();
set_sprite_data(0, 1, ball_tile);
set_sprite_tile(0, 0);
put_str(4, 6, "PRESS START");
SHOW_BKG;
SHOW_SPRITES;
DISPLAY_ON;
// block until START is pressed, then wait for it to be released
waitpad(J_START);
waitpadup();
// replace the title message with the HUD
fill_bkg_rect(4, 6, 11, 1, ' ');
put_str(1, 0, "D-PAD: MOVE");
put_str(1, 1, "A:000 B:000");
a_count = b_count = 0;
move_sprite(0, x, y);
last_keys = 0;
while (1) {
uint8_t pressed;
wait_vbl_done();
keys = joypad();
// a button "just pressed" is down now and was up last frame
pressed = keys & ~last_keys;
if (pressed & J_A) {
a_count++;
put_u8(3, 1, a_count);
}
if (pressed & J_B) {
b_count++;
put_u8(11, 1, b_count);
}
// dpad is read as held keys, so movement repeats while held
if (keys & J_LEFT) x--;
if (keys & J_RIGHT) x++;
if (keys & J_UP) y--;
if (keys & J_DOWN) y++;
// keep the sprite on screen
if (x < 8) x = 8;
if (x > 152) x = 152;
if (y < 16) y = 16;
if (y > 136) y = 136;
move_sprite(0, x, y);
last_keys = keys;
}
}
+19 -8
View File
@@ -1,7 +1,8 @@
/*
Metasprites combine several hardware sprites to make a larger
sprite. Our demo uses 4 hardware sprites in a 2x2 pattern,
forming 16x16 pixel sprites.
forming 16x16 pixel sprites. Actors alternate between the normal
draw, a horizontal flip and a vertical flip.
Ported from presets/nes/metasprites.c, using GBDK (gb/metasprites.h).
The Game Boy OAM holds 40 sprites, so 10 actors of 4 sprites each
@@ -41,6 +42,8 @@ uint8_t actor_y[NUM_ACTORS];
int8_t actor_dx[NUM_ACTORS];
int8_t actor_dy[NUM_ACTORS];
const int8_t deltas[4] = { -2, -1, 1, 2 };
// setup LCD and tile data
void setup_graphics(void) {
DISPLAY_OFF;
@@ -60,20 +63,28 @@ void main(void) {
setup_graphics();
// initialize actors with random values
srand(0);
for (i=0; i<NUM_ACTORS; i++) {
actor_x[i] = rand();
actor_y[i] = rand();
actor_dx[i] = (rand() & 3) - 1;
actor_dy[i] = (rand() & 3) - 1;
actor_x[i] = rand() + i * 8;
actor_y[i] = rand() + i * 8;
actor_dx[i] = deltas[rand() & 3];
actor_dy[i] = deltas[rand() & 3];
}
// loop forever
while (1) {
uint8_t base = 0; // first free sprite ID
// draw and move all actors
// draw and move all actors, flipping some of them
for (i=0; i<NUM_ACTORS; i++) {
base += move_metasprite(metasprite, 0x41, base,
actor_x[i], actor_y[i]);
if (i < 4)
base += move_metasprite(metasprite, 0x41, base,
actor_x[i], actor_y[i]);
else if (i < 7)
base += move_metasprite_hflip(metasprite, 0x41, base,
actor_x[i], actor_y[i]);
else
base += move_metasprite_vflip(metasprite, 0x41, base,
actor_x[i], actor_y[i]);
actor_x[i] += actor_dx[i];
actor_y[i] += actor_dy[i];
}
+82
View File
@@ -0,0 +1,82 @@
/*
STAT/LYC raster effects on Game Boy.
The LCD status (STAT) interrupt can fire when the raster reaches a
programmed scanline, that is when LY_REG == LYC_REG. From inside the
handler we can point LYC at a new line and write SCX_REG, changing the
scroll register in the middle of a frame -- an effect the tile engine
cannot do by itself.
This demo waves the background left and right. The handler runs once every
eight scanlines, sets SCX_REG from a sine table, then arms LYC for the
next interrupt. The table phase advances every two frames, so the wave travels
across the screen.
*/
//#link "gb/sfr.sgb"
//#link "gb/crt0.sgb"
//#resource "gb/global.sgb"
#include <stdint.h>
#include "gb/types.h"
#include "gb/hardware.h"
#include "gb/gb.h"
#include "gbtext.h"
// 32-step wave, +/- 20 pixels
static const int8_t wave[32] = {
0, 6, 12, 16, 19, 20, 19, 16,
12, 6, 0, -6, -12, -16, -19, -20,
-19, -16, -12, -6, 0, 6, 12, 16,
19, 20, 19, 16, 12, 6, 0, -6
};
// diagonal stripe tile so the per-line offset is easy to see
static const uint8_t stripe_tile[] = {
/*;;{w:8,h:8,bpp:1,count:1,brev:1,np:2,pofs:1,sl:2};;*/
0xF0,0xE1,0xC3,0x87,0x0F,0x1E,0x3C,0x78,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
/*;;*/
};
volatile uint8_t phase;
// LCD handler: called once per 8 visible scanlines (LY == LYC)
void lcd_handler(void) NONBANKED {
uint8_t ly = LY_REG;
if (ly < 143) {
// offset this line and arm the interrupt for the next one
SCX_REG = 248 + (int8_t)wave[(ly + phase) & 31];
LYC_REG = ly + 8;
} else {
// last line: reset so the next frame starts at the top
SCX_REG = 128;
LYC_REG = 0;
}
}
void main(void) {
DISPLAY_OFF;
BGP_REG = OBP0_REG = OBP1_REG = 0xE4;
font_init();
set_bkg_data(0, 1, stripe_tile);
fill_bkg_rect(0, 0, 32, 32, 0);
put_str(2, 6, "GAME BOY RASTER");
put_str(2, 8, "LYC WAVE");
// enable the LY==LYC interrupt and install the handler
LYC_REG = 0;
STAT_REG = STATF_LYC;
add_LCD(lcd_handler);
set_interrupts(VBL_IFLAG | LCD_IFLAG);
SHOW_BKG;
DISPLAY_ON;
while (1) {
wait_vbl_done();
wait_vbl_done();
phase++;
}
}
+96
View File
@@ -0,0 +1,96 @@
/*
VRAM and OAM access windows on Game Boy.
The CPU shares VRAM and OAM with the LCD controller. While the LCD is
off, both are freely accessible. While it is on, the CPU can only touch
them during VBlank -- writes at other times are ignored or corrupt the
display.
This demo exercises the low-level access helpers:
- vmemset() fills a VRAM range while the LCD is off, clearing the map.
- vmemcpy() and vmemset() copy a moving bar into the tile map, once per
VBlank.
- hide_sprite() hides the objects outside a moving four-sprite window.
*/
//#link "gb/sfr.sgb"
//#link "gb/crt0.sgb"
//#resource "gb/global.sgb"
#include <stdint.h>
#include "gb/types.h"
#include "gb/hardware.h"
#include "gb/gb.h"
#include "gbtext.h"
#define TILE_BAR 0x01U // custom tile (font lives at 0x20 and up)
// solid bar tile
static const uint8_t bar_tile[] = {
/*;;{w:8,h:8,bpp:1,count:1,brev:1,np:2,pofs:1,sl:2};;*/
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
/*;;*/
};
// one full row of bar tiles, copied with vmemcpy()
static const uint8_t bar_row[32] = {
TILE_BAR,TILE_BAR,TILE_BAR,TILE_BAR,TILE_BAR,TILE_BAR,TILE_BAR,TILE_BAR,
TILE_BAR,TILE_BAR,TILE_BAR,TILE_BAR,TILE_BAR,TILE_BAR,TILE_BAR,TILE_BAR,
TILE_BAR,TILE_BAR,TILE_BAR,TILE_BAR,TILE_BAR,TILE_BAR,TILE_BAR,TILE_BAR,
TILE_BAR,TILE_BAR,TILE_BAR,TILE_BAR,TILE_BAR,TILE_BAR,TILE_BAR,TILE_BAR
};
// an 8x8 ball sprite (2bpp)
static const uint8_t ball_tile[] = {
/*;;{w:8,h:8,bpp:1,count:1,brev:1,np:2,pofs:1,sl:2};;*/
0x3C,0x3C,0x42,0x7E,0x99,0xFF,0xA9,0xFF,
0x89,0xFF,0x89,0xFF,0x42,0x7E,0x3C,0x3C
/*;;*/
};
void main(void) {
uint8_t i;
uint8_t bar_y = 3, bar_dy = 1;
uint8_t spot = 0, spot_dx = 1;
DISPLAY_OFF;
BGP_REG = OBP0_REG = OBP1_REG = 0xE4;
font_init();
set_bkg_data(TILE_BAR, 1, bar_tile);
set_sprite_data(0, 1, ball_tile);
for (i = 0; i < 8; i++) {
set_sprite_tile(i, 0);
hide_sprite(i);
}
// LCD is off: fill the whole background map at once.
// (0x20 is the space tile loaded by font_init.)
vmemset(_SCRN0, 0x20, 32 * 32);
put_str(1, 0, "VRAM / OAM WINDOW");
SHOW_BKG;
SHOW_SPRITES;
DISPLAY_ON;
while (1) {
wait_vbl_done();
// erase the old bar and draw the new one -- only safe in VBlank
vmemset(_SCRN0 + bar_y * 32, 0x20, 32);
bar_y += bar_dy;
if (bar_y <= 2 || bar_y >= 16) bar_dy = -bar_dy;
vmemcpy(_SCRN0 + bar_y * 32, (uint8_t*)bar_row, 32);
// show four sprites and hide the rest
for (i = 0; i < 8; i++) {
if (i >= spot && i < spot + 4)
move_sprite(i, 16 + i * 16, 136);
else
hide_sprite(i);
}
spot += spot_dx;
if (spot == 0 || spot == 4) spot_dx = -spot_dx;
}
}
+105
View File
@@ -0,0 +1,105 @@
/*
The Game Boy window layer as a status bar.
The window is a second tile map that the PPU draws on top of the
background. Unlike the background it does not scroll; instead WX_REG and
WY_REG set where its top-left corner appears, so it makes a natural HUD.
This demo builds a status bar with init_win() and fill_win_rect(), stamps
a row of icons with set_win_submap(), then slides the whole bar in from
the left with scroll_win() while the background scrolls behind it.
*/
//#link "gb/sfr.sgb"
//#link "gb/crt0.sgb"
//#resource "gb/global.sgb"
#include <stdint.h>
#include <string.h>
#include "gb/types.h"
#include "gb/hardware.h"
#include "gb/gb.h"
#include "gbtext.h"
#define TILE_BLOCK 0
#define TILE_STRIPE 1
#define TILE_CHECK 2
// solid, diagonal-stripe and checkerboard 2bpp tiles
static const uint8_t block_tile[] = {
/*;;{w:8,h:8,bpp:1,count:1,brev:1,np:2,pofs:1,sl:2};;*/
0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
/*;;*/
};
static const uint8_t stripe_tile[] = {
/*;;{w:8,h:8,bpp:1,count:1,brev:1,np:2,pofs:1,sl:2};;*/
0xF0,0xE1,0xC3,0x87,0x0F,0x1E,0x3C,0x78,
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00
/*;;*/
};
static const uint8_t checker_tile[] = {
/*;;{w:8,h:8,bpp:1,count:1,brev:1,np:2,pofs:1,sl:2};;*/
0xCC,0xCC,0x33,0x33,0xCC,0xCC,0x33,0x33,
0x33,0x33,0xCC,0xCC,0x33,0x33,0xCC,0xCC
/*;;*/
};
// one row of alternating icons for set_win_submap()
static const uint8_t icon_map[] = { TILE_CHECK, TILE_BLOCK, TILE_CHECK, TILE_BLOCK,
TILE_CHECK, TILE_BLOCK, TILE_CHECK, TILE_BLOCK };
// write a string into the window tile map
static void put_win_str(uint8_t x, uint8_t y, const char* s) {
set_win_tiles(x, y, strlen(s), 1, (uint8_t*)s);
}
void main(void) {
int8_t win_x = 0;
uint8_t scroll = 0;
DISPLAY_OFF;
BGP_REG = OBP0_REG = OBP1_REG = 0xE4;
// put the window map at 0x9C00, separate from the background map.
// set LCDC before loading tiles: GBDK picks the tile-data region
// (0x8000 or 0x9000) from the LCDC bit in effect at the call.
LCDC_REG = LCDCF_OFF | LCDCF_WIN9C00 | LCDCF_BG8000 | LCDCF_BGON | LCDCF_WINON;
// load the font plus our three pattern tiles
font_init();
set_bkg_data(TILE_BLOCK, 1, block_tile);
set_bkg_data(TILE_STRIPE, 1, stripe_tile);
set_bkg_data(TILE_CHECK, 1, checker_tile);
// background: stripes, so the scrolling is easy to see
fill_bkg_rect(0, 0, 32, 32, TILE_STRIPE);
// build the status bar (2 rows tall)
init_win(TILE_BLOCK); // clear the whole window map
fill_win_rect(0, 0, 20, 2, TILE_BLOCK); // solid 20x2 bar
set_win_submap(12, 0, 8, 1, icon_map, 8);// icon row from a submap
put_win_str(1, 0, "STATUS BAR");
put_win_str(1, 1, "SCORE 000000");
// anchor the bar to the bottom two rows, hidden off the left edge
// (the window always extends from WY down to the bottom of the screen)
move_win(0, 128);
DISPLAY_ON;
while (1) {
wait_vbl_done();
// WX=7 puts the window's left edge at screen x=0
if (win_x < 7) {
scroll_win(1, 0);
win_x++;
}
// scroll the background
scroll++;
SCX_REG = scroll;
SCY_REG = scroll >> 1;
}
}
+16
View File
@@ -1171,6 +1171,22 @@ export class GameBoyMachine extends BasicScanlineMachine {
}
// CGB speed switch (executed during STOP instruction)
//
// TODO: double speed is only partially implemented. performSpeedSwitch()
// toggles the flag, but nothing calls it and advanceCPU() does not account
// for the faster CPU. Plan for fixing:
// 1. Hook STOP: in advanceCPU(), snapshot the PC before advanceInsn() and
// call performSpeedSwitch() when the executed opcode is 0x10.
// 2. Decouple CPU from hardware cycles: at double speed one CPU T-cycle is
// half a hardware T-cycle. Accumulate the fractional remainder (the CPU
// clock is 8 MHz vs the 4 MHz PPU/timer/APU) and pass only the hardware
// T-cycles to updatePPUMode(), the timer and the APU, while returning
// those hardware cycles so advanceFrame() fits ~2x instructions per
// scanline. Keep the full tCycles for probe.logClocks().
// 3. The timer/APU paths currently take M-cycles; convert them to a
// T-cycle entry point (or pass the halved value) so they stay at 4 MHz.
// 4. Once done, presets/gb/gbc.c's WORK counter should roughly double when
// SPEED is DOUBLE, and DIV/timer rates should be unchanged.
performSpeedSwitch(): void {
if (this.cgbMode && this.speedSwitchArmed) {
this.doubleSpeed = !this.doubleSpeed;
+9 -3
View File
@@ -7,22 +7,28 @@ const GB_PRESETS: Preset[] = [
{ id: 'hello.c', name: 'Hello World', category: 'C' },
{ id: 'typewriter.c', name: 'Typewriter Text' },
{ id: 'testdrawing.c', name: 'Drawing Routines' },
{ id: 'testphys.c', name: 'Joypad Sprite' },
{ id: 'joypad.c', name: 'Joypad Input' },
{ id: 'testphys.c', name: 'Sprite Movement' },
{ id: 'sprites.c', name: 'Multiple Sprites' },
{ id: 'metasprites.c', name: 'Metasprites' },
{ id: 'paint.c', name: 'Paint Program' },
{ id: 'scroll.c', name: 'Scrolling' },
{ id: 'large_map.c', name: 'Large Scrolling Map' },
{ id: 'window.c', name: 'Window Status Bar' },
{ id: 'galaxy.c', name: 'Window + Sprites' },
{ id: 'raster.c', name: 'STAT/LYC Raster Effects' },
{ id: 'interrupts.c', name: 'VBlank + Timer Interrupts' },
{ id: 'vram.c', name: 'VRAM/OAM Access' },
{ id: 'gbc.c', name: 'Game Boy Color' },
{ id: 'gbdecompress.c', name: 'Decompression' },
{ id: 'music.c', name: 'Music Player' },
{ id: 'clock.c', name: 'MBC3 Real-Time Clock' },
{ id: 'banking.c', name: 'ROM Bank Switching' },
{ id: 'siegegame.c', name: 'Siege Game' },
{ id: 'siegegame.c', name: 'Siege Game', category: 'Games' },
{ id: 'climber.c', name: 'Climber Game' },
{ id: 'chase.c', name: "Shiru's Chase Game" },
{ id: 'pakupaku.c', name: 'Paku Paku' },
{ id: 'hello.sgb', name: 'Hello World (ASM)', category: 'assembler' },
{ id: 'hello.sgb', name: 'Hello World (ASM)', category: 'Assembler' },
{ id: 'main.wiz', name: 'Snake Game (Wiz)' },
];