diff --git a/presets/gb/gbc.c b/presets/gb/gbc.c new file mode 100644 index 00000000..b8318b23 --- /dev/null +++ b/presets/gb/gbc.c @@ -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 +#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++; + } +} diff --git a/presets/gb/interrupts.c b/presets/gb/interrupts.c new file mode 100644 index 00000000..cd9110c0 --- /dev/null +++ b/presets/gb/interrupts.c @@ -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 +#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); + } +} diff --git a/presets/gb/joypad.c b/presets/gb/joypad.c new file mode 100644 index 00000000..51c6a907 --- /dev/null +++ b/presets/gb/joypad.c @@ -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 +#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; + } +} diff --git a/presets/gb/metasprites.c b/presets/gb/metasprites.c index e2fa04bd..72f4d4c3 100644 --- a/presets/gb/metasprites.c +++ b/presets/gb/metasprites.c @@ -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 +#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++; + } +} diff --git a/presets/gb/vram.c b/presets/gb/vram.c new file mode 100644 index 00000000..0e1e08a1 --- /dev/null +++ b/presets/gb/vram.c @@ -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 +#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; + } +} diff --git a/presets/gb/window.c b/presets/gb/window.c new file mode 100644 index 00000000..ecd2eb87 --- /dev/null +++ b/presets/gb/window.c @@ -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 +#include +#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; + } +} diff --git a/src/machine/gb.ts b/src/machine/gb.ts index 29c22eae..4fbb7c11 100644 --- a/src/machine/gb.ts +++ b/src/machine/gb.ts @@ -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; diff --git a/src/platform/gb.ts b/src/platform/gb.ts index 5a46a9bb..f164499f 100644 --- a/src/platform/gb.ts +++ b/src/platform/gb.ts @@ -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)' }, ];