KITAQGB APPLICATION NOTES · GB / CGB

HARAPEKO SHIROHEBI

Programmer’s Guide · Flow, movement and library integration

KITAQ manualsEnglish日本語한국어简体中文繁體中文FrançaisEspañolDeutschREADMEDAISUKE OBA · MIT

01Start here

This guide explains how HARAPEKO SHIROHEBI works, from controller input to the pixels on the screen. The game is by DAISUKE OBA. Eat pomegranates, grow the white snake and avoid mines and your own body. The supplied program, original assets and this guide are released under the MIT License.

The screen is 160 × 144 pixels. One ROM supports DMG and CGB; color is selected at runtime. Storage uses MBC5, 64 KiB ROM and 8 KiB battery-backed RAM. Obtain the playable release from the game's itch.io page.

Reading route: start with the flowchart, then read the head-motion and chain sections together. The library table connects each technique to the source you can reuse. “Apple” and “Block” in identifiers mean the on-screen pomegranate and mine.

↑ Back to contents

02Build and file layout

Build the compiler and all helper tools on Windows, Linux, macOS ARM or macOS Intel with Rust 1.85 or later. The native executables run without .NET; production asset tools also run without Python or Pillow.

PowerShell build scripts use the root Windows executable. For Linux/macOS use the native compiler command with the same C inputs and options, or PowerShell 7. Graphics CHR and C source are separate inputs; font.chr keeps the original sample font.

powershell -NoProfile -ExecutionPolicy Bypass -File .\apps\harapeko_shirohebi\build.ps1

The game is assembled from 17 split C files in an explicit order. Music, exported assets and the bank-selection wrappers are separate compiler inputs. The build links the repository's lib/; it does not need a private art project or an asset exporter. Do not add the split files to a generic C build individually: they share declarations through the assembled translation unit.

Option / outputMeaning
-KitaqgbRootSelect the directory containing kitaqgb.exe and lib/. Omit it in this repository layout.
-OutputDirectoryChoose the output directory; the default is the game's out/.
-KeepBuildFilesKeep the combined source, temporary audio adaptations and compiler diagnostics after success.
shirohebi.gbThe newly compiled ROM. Never use an older output after a failed build.
shirohebi.mapSymbol addresses for debugging this exact build.
build_manifest.jsonCompiler/ROM hashes, output size and interrupt-vector address.

Why the build has a post-processing step

The VBlank music interrupt must remain callable while any switchable ROM bank is selected. The script verifies four interrupt-related symbols are in fixed bank 0, installs a JP at $0040 and recalculates both cartridge checksums. It refuses an unexpected vector. A temporary audio copy disables the ordinary music service so music is not advanced by both the main loop and the interrupt. Another temporary copy places audio helpers and uses an in-range absolute branch. Repository libraries are not edited.

See build.ps1 for the complete ordered inputs and compiler flags. Main flags are --profile=release --stack-bank=fixed --rst-disable --cgb=cgb --cart=mbc5_ram_battery --romsize=64k --ramsize=8k.

↑ Back to contents

03Program flow and states

INITIALIZEHardware, display, shared state
ARM INTERRUPTAudio hook, load scores, title
MAIN LOOPInput → state update → present
One loop: audio bookkeeping → read held/new keys → shortcut/release gate → one state handler → build shadow OAM → wait for VBlank → transfer OAM → service SFX → apply dirty overlays. Repeat.
Figure 1. Main program flow. Hardware VBlank interrupts can occur while the main loop is executing.
State transitionTrigger and effect
TITLE → READY → PLAYSTART begins a run. The START introduction finishes before movement begins.
PLAY ⇄ PAUSESTART pauses/resumes. Physics and chain updates stop while paused.
PAUSE → RETURN_CONFIRMSELECT opens the NO-default confirmation. Cancel returns to the same paused run; YES returns to TITLE.
PLAY → MISSA mine or self hit triggers the corresponding animation.
MISS → NAME_ENTRY / GAMEOVERA qualifying score opens name entry. Other scores go directly to retry. Confirming a name saves it and opens retry.
GAMEOVER → READY / TITLERetry YES starts a fresh run; NO returns to the title.
TITLE ⇄ ERASESELECT+START opens score erasure. A confirms; B/START cancels. Initial choice is NO.

The release gate consumes input until every button is released, including the release iteration. This prevents a button that opened a screen from immediately confirming its next action. Demo mode substitutes steering input and uses the same motion/collision code. Title inactivity is counted in 900 main-loop passes; the demo duration uses 1,800 VBlank ticks. Neither counter should be described as a guaranteed wall-clock duration.

↑ Back to contents

04Head motion: fixed point and steering

Only the head is a KQBody2D. Its KQWorld2D contains one active body, zero gravity and zero contact-solver iterations. The game implements its own hit checks after motion. Q4 coordinates use 16 integer units per pixel: 160 represents 10 pixels. This retains fractional motion without floating-point arithmetic.

base = 8 + 2 * min(score / 5, 10)     // Q4 component limit
first UP update: limit = base + (base >> 2)
held UP:        limit += max(base >> 2, 1), capped at 2 * base
released UP:    limit -= max(base >> 2, 1), floored at base
pixel_center = (q4_position + 8) >> 4

The base component limit ranges from 0.5 to 1.75 pixels per update. Sixteen clockwise headings start at right (0), then down (4), left (8) and up (12). LEFT/RIGHT change the intended heading by one sector. Held turning uses an initial countdown of 8 and a repeat countdown of 4; simultaneous LEFT and RIGHT cancel turning.

The velocity direction uses full, three-quarter, quarter and zero components. Each actual component approaches its target by at most 3 Q4 units per update. These are inexpensive integer approximations, not normalized trigonometric vectors. The displayed heading can therefore change before the velocity finishes aligning.

After kq2d_step, the head snaps across the center limits: X below 4 goes to 156, X above 156 goes to 4; Y similarly uses 4 and 140. It then rounds once to pixel coordinates for both drawing and collision. This head-boundary rule differs from the chain's modulo-160/144 coordinate wrapping.

Read shirohebi_snake.c / shirohebi_flow.c

↑ Back to contents

05How the white snake follows

ChainBody points directly to SND_TraceX, SND_TraceY and SND_TraceDir. These arrays hold the current pose of each segment, not a recording of where the head travelled. Collision and rendering read the same arrays. The snake starts with a head and three joints and can grow to 31 segments in total.

A new heading travels from the head to successive jointsHEAD [0]heading = 0JOINT [1]old 15 → new 0JOINT [2]old 14 → new 15015Pass the saved OLD heading to the next joint.
Figure 2. One update. The old heading is saved before overwriting it, so a bend propagates one joint at a time. Position uses the predecessor after its update.
  1. Store the head's pixel position and heading at index 0; set pull to that heading.
  2. For joint i, save its old heading. Subtract a small heading-dependent offset from the already-updated predecessor to obtain the target position.
  3. Find the shortest wrapped X/Y displacement to the target. Move each axis by 0, 1 or 2 pixels according to the table below.
  4. Assign pull to the joint's heading, wrap its new position and pass the saved old heading to joint i + 1.
Signed displacement dPer-axis correction
d > 4+2
0 < d ≤ 4+1
d = 00
−4 ≤ d < 0−1
d < −4−2

The X offsets for headings 0…15 are [4,4,3,1,0,-1,-3,-4,-4,-4,-3,-1,0,1,3,4]; Y uses the same values shifted by 12 sectors. Coordinates wrap in a 160 × 144 field. For a field size W, subtract W from a displacement above W/2 and add W below −W/2; an exact half-field displacement keeps its sign.

Worked example at the screen edge

A right-facing predecessor at (2, 50) wants the following joint four pixels behind it: target X = (2 − 4) mod 160 = 158. If the joint is at X=156, the displacement is +2, so it advances by +1 to X=157. A joint at X=0 instead sees the shortest displacement −2 and moves to X=159. It follows across the edge rather than crossing the whole screen.

Growth duplicates the tail's position and heading, then increases the active count. Subsequent following separates the new joint. The former tail is highlighted for 20 update counts. The library wrapper keeps the follower and its helpers together in bank 2, avoiding per-joint bank switches.

/* Excerpt: the game owns these arrays for the entire run. */
chain_body_init(&SND_ChainBody, SND_TraceX, SND_TraceY, SND_TraceDir,
                SND_MAX_SEGMENTS, SND_SCREEN_W, SND_SCREEN_H);
chain_body_reset(&SND_ChainBody, SND_Length,
                 SND_HeadX, SND_HeadY, SND_Facing);

/* Once per PLAY update, after integrating and rounding the head. */
SND_ChainBody.count = SND_Length;
chain_body_step(&SND_ChainBody, SND_HeadX, SND_HeadY, SND_Facing);

/* On fruit collection, within capacity. */
chain_body_grow(&SND_ChainBody);
SND_Length = SND_ChainBody.count;

This excerpt illustrates storage ownership and call order; the full game also checks growth failure and sets the highlight. Sources: shirohebi_snake.c / shirohebi_chain.c / chain.c

↑ Back to contents

06Fruit, collision and miss effects

A PLAY update processes boost → steering → head physics → head wrap/rounding → body following → mines → self hit → fruit → highlight expiry. Fatal hits take precedence over collecting a fruit in the same update.

CheckActual rule
MinesEvery segment center is tested against inclusive rectangles X=46…58 or 102…114, Y=62…74.
Self hitHead versus segments 2 onward; shortest wrapped |dx| < 3 and |dy| < 3. The immediately following joint is excluded.
FruitHead versus fruit center; ordinary, nonwrapped |dx| < 7 and |dy| < 7. Add a point, attempt growth and choose the next candidate.

These are axis-aligned thresholds, not circle-distance tests. Sixteen fixed fruit positions are visited deterministically. Minimum tile distance from a mine decreases with score (4, 3, 2, 1 at scores 0, 10, 20, 30); an identical tile is always rejected. A search tries at most 16 candidates. The score is an 8-bit value, so its storage is not an unlimited counter.

A self hit freezes the pose and fades it. A mine hit scatters segments with fixed-point velocities, delayed gravity and staggered bursts, then shows a crater. The miss timer routes a qualifying score to name entry and other scores to the retry prompt. CGB uses colors for the effects; DMG uses its shade palettes.

shirohebi_collision.c / shirohebi_apples.c

↑ Back to contents

07Drawing: tiles and sprite order

Initialization waits for a safe display phase, turns the LCD off, clears VRAM, loads tile/font data and palettes, and enables the display. The CGB path supplies background attributes and color palettes; the DMG path uses shade registers. The exported background is 20 tiles wide, while the hardware tilemap uses a stride of 32, so their indices are not interchangeable.

During a loop, the game builds a shadow OAM buffer in RAM. Each sprite is written with the hardware's X+8 and Y+16 coordinate biases. After system_wait_vblank(), it commits the OAM data. Snake slots occupy 0…30; text uses the remaining slots. Unused entries are cleared.

The first six segments keep stable OAM priority. Later segments rotate their ordering by a phase of four per render. This shares the hardware's ten-sprites-per-scanline limit among the rear joints; it does not remove that limit or delete body segments. When changing the maximum snake length, recheck both the storage capacity and all 40 OAM slots.

START and PAUSE overlays track desired, shown and dirty state. Removing text restores each cell using live fruit state before falling back to the background map. Clearing the area to a single tile would erase a fruit underneath it. Font masks are expanded from eight one-bit rows into sixteen bytes of two-bit tile data, with glyph pixels taking precedence over the background pattern.

shirohebi_render.c / shirohebi_display.c / shirohebi_pause.c

↑ Back to contents

08Music and VBlank responsibilities

The fixed-bank music stream is read by audio_vblank.c. Each normal record is ordered delay, CH1, CH2, CH3, CH4: pulse with sweep, pulse, wave, then noise. Channel identity therefore follows the physical hardware order. Music control uses AudioVBlank_PlayMusic, AudioVBlank_Stop and AudioVBlank_SetEnabled.

The ordinary audio.c driver handles game effects. Its music service is excluded by the build's temporary define. Game effects select banked data with __bankof and use priorities; the mine effect also uses direct CH4 noise. Pause coordinates both music and effects. The main loop's SND_AudioServiceWork calls are deliberately empty: processing extra joints must not advance the music clock.

AudioVBlank_FrameHook calls the game's fixed-bank SND_DemoVBlankHook. It increments an 8-bit demo counter and samples the system-button row, preserving the P1 selection. When armed, it latches title SELECT+START and pause SELECT so short presses survive a busy main loop. It does not draw or call switchable-bank routines.

/* Excerpt from initialization, after shared state has been set up. */
SND_AudioInit();
AudioVBlank_FrameHook = SND_DemoVBlankHook;
AudioVBlank_EnableIrq();

shirohebi_audio.c / shirohebi_lib_bank0.c / music.c

↑ Back to contents

09Scores and name entry

Slot 0 stores a 42-byte application payload: magic 0x53, version 2, ten one-byte scores and thirty name characters. The save library supplies the enclosing length/checksum validation. The loader also recognizes 5-byte and 15-byte score payloads, while writes use the 42-byte format.

A nonzero qualifying score is inserted into descending order. The comparison is strictly greater, so equal scores follow existing entries. Names move with their scores. A new entry begins as AAA in RAM; the game writes SRAM when the player confirms the three-character name. Abandoning a pending result reloads the saved ranking instead of committing it.

UP/DOWN select A–Z or a period. RIGHT/SELECT move to the next character with wraparound; LEFT moves back with wraparound. B moves back only when the cursor is not on the first character. A advances, or confirms at the final character; START confirms from any position. The release gate prevents that confirmation from also selecting retry.

shirohebi_save.c / shirohebi_flow.c

↑ Back to contents

10KITAQGB library usage

Use the repository library and header together. The game-specific wrappers and settings are part of the example, especially its bank placement and audio coordination.

LibraryEntry points / purposeExample source
system.csystem_init, system_wait_vblank
Initialize system state and synchronize presentation. The audio IRQ hook is installed separately.
shirohebi_main.c
chain.cchain_body_init, chain_body_reset, chain_body_step, chain_body_grow
Bind persistent arrays, construct a pose, follow the head and append the tail. The ring-history Chain API is not used.
shirohebi_snake.c
physics2d.ckq2d_world_init, kq2d_step
Integrate one Q4 head body. Set zero gravity and no contact-solver iterations.
shirohebi_snake.c
sprite.csprite_init; kq_sprite_oam and companion state
Initialize and use shadow OAM. The game packs entries directly, then commits them; respect slot and coordinate rules.
shirohebi_render.c
audio_vblank.cAudioVBlank_Init, PlayMusic, Stop, SetEnabled, EnableIrq, FrameHook
Resident music stream and an interrupt-time game callback. Keep IRQ code/data in fixed ROM. Names after the first share the AudioVBlank_ prefix.
shirohebi_audio.c
audio.cAudio_Init, Audio_Update, Audio_PlaySFXBanked, Audio_StopSfx, Audio_SetPaused
Effect playback, priority and pause, coordinated with the VBlank music driver.
shirohebi_audio.c
save.csave_exists, save_read, save_write
Read/write the versioned ranking payload in slot 0. save_read returns success/failure after checking the exact requested length and checksum; the game checks its own signature too.
shirohebi_save.c
cgb_palette.ccgb_bg_palette, cgb_obj_palette
Configure the game's CGB colors alongside explicit DMG palette registers.
shirohebi_display.c
map.cmap_load and the exported map_t descriptor
Support the asset file's map helper; gameplay restoration writes individual cells itself.
shirohebi_assets.c
fixed.cfixed-point support
Linked support code; head motion is expressed through physics2d and integer Q4 arithmetic.
shirohebi_common.c
audio_hwregs_gb.cshared audio register bindings
Provide the GB hardware register definitions required by both audio paths.
shirohebi_audio.c

Compiler built-ins used alongside libraries

__readpadex(previous) returns held keys in the low byte and newly pressed keys in the high byte. __cgb_is_cgb() selects color handling. Tile/attribute built-ins such as __settile and __settileattr update background cells; __bankof supplies the ROM bank of effect data. These are compiler facilities, not additional C implementations to copy into the game.

↑ Back to contents

11Source map and safe changes

FileResponsibility
shirohebi_common.cTypes, constants, shared state and forward declarations. Read this first when following a symbol.
shirohebi_main.cBoot sequence and the input → update → render → VBlank loop.
shirohebi_flow.cState dispatch, title options, READY, PLAY, pause, miss, name entry and retry.
shirohebi_input.cRelease gating, title shortcut and return-to-title confirmation.
shirohebi_snake.cHead physics, boost, relative steering, boundary wrap and chain setup/update/growth.
shirohebi_chain.cBank-2 wrapper that includes the complete shared chain implementation.
shirohebi_collision.cMine/self hits, impact classification and scattered-segment animation.
shirohebi_apples.cPomegranate candidate selection, collection, scoring and growth. Apple is the internal identifier.
shirohebi_demo.cAttract-mode steering, long-snake pose and elapsed-tick accounting using the VBlank counter.
shirohebi_render.cShadow OAM packing, head/joint/tail tiles, priority rotation and miss effects.
shirohebi_display.cLCD/VRAM initialization, palette and font uploads, title and ranking layout.
shirohebi_background.cGameplay background cells, mines and restoration of overwritten cells.
shirohebi_pause.cSTART/PAUSE text overlays with requested/shown/dirty state.
shirohebi_text.cCharacter-to-tile conversion, strings, numeric scores and menu cursors.
shirohebi_save.cRanking validation, insertion, three-character names and SRAM storage.
shirohebi_audio.cMusic/SFX setup, start/stop, options, pause and channel coordination.
music.cResident fixed-length VBlank music records for the start jingle, main song and game-over song.
sfx.cEffect byte streams selected by the game audio functions.
text_tiles.cFont masks and conversion into two-bit Game Boy tile rows.
shirohebi_assets.cExported tiles, tilemap, attributes, collision/event tables; map descriptor and helpers.
shirohebi_input_shared.hDeclarations for the WRAM input flags and fixed-bank callback shared with the interrupt.
shirohebi_lib_bank0.cFixed-bank VBlank input/clock callback, shared WRAM flags and bank-0 selection for the following system/audio inputs.
shirohebi_lib_bank1.cSelect bank 1 for the following support-library compilation inputs.

When changing the game

After rebuilding, use the generated map from that build. Check the title, START introduction, movement, turning, fruit, self/mine hits, pause/resume, return confirmation and ranking in both hardware modes. A successful compilation alone does not prove those paths or physical-hardware behavior.

↑ Back to contents