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.
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 / output | Meaning |
|---|---|
-KitaqgbRoot | Select the directory containing kitaqgb.exe and lib/. Omit it in this repository layout. |
-OutputDirectory | Choose the output directory; the default is the game's out/. |
-KeepBuildFiles | Keep the combined source, temporary audio adaptations and compiler diagnostics after success. |
shirohebi.gb | The newly compiled ROM. Never use an older output after a failed build. |
shirohebi.map | Symbol addresses for debugging this exact build. |
build_manifest.json | Compiler/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.
03Program flow and states
| State transition | Trigger and effect |
|---|---|
| TITLE → READY → PLAY | START begins a run. The START introduction finishes before movement begins. |
| PLAY ⇄ PAUSE | START pauses/resumes. Physics and chain updates stop while paused. |
| PAUSE → RETURN_CONFIRM | SELECT opens the NO-default confirmation. Cancel returns to the same paused run; YES returns to TITLE. |
| PLAY → MISS | A mine or self hit triggers the corresponding animation. |
| MISS → NAME_ENTRY / GAMEOVER | A qualifying score opens name entry. Other scores go directly to retry. Confirming a name saves it and opens retry. |
| GAMEOVER → READY / TITLE | Retry YES starts a fresh run; NO returns to the title. |
| TITLE ⇄ ERASE | SELECT+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.
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.
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.
- Store the head's pixel position and heading at index 0; set
pullto that heading. - For joint
i, save its old heading. Subtract a small heading-dependent offset from the already-updated predecessor to obtain the target position. - Find the shortest wrapped X/Y displacement to the target. Move each axis by 0, 1 or 2 pixels according to the table below.
- Assign
pullto the joint's heading, wrap its new position and pass the saved old heading to jointi + 1.
| Signed displacement d | Per-axis correction |
|---|---|
| d > 4 | +2 |
| 0 < d ≤ 4 | +1 |
| d = 0 | 0 |
| −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
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.
| Check | Actual rule |
|---|---|
| Mines | Every segment center is tested against inclusive rectangles X=46…58 or 102…114, Y=62…74. |
| Self hit | Head versus segments 2 onward; shortest wrapped |dx| < 3 and |dy| < 3. The immediately following joint is excluded. |
| Fruit | Head 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.
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
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();
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.
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.
| Library | Entry points / purpose | Example source |
|---|---|---|
system.c | system_init, system_wait_vblankInitialize system state and synchronize presentation. The audio IRQ hook is installed separately. | shirohebi_main.c |
chain.c | chain_body_init, chain_body_reset, chain_body_step, chain_body_growBind persistent arrays, construct a pose, follow the head and append the tail. The ring-history Chain API is not used. | shirohebi_snake.c |
physics2d.c | kq2d_world_init, kq2d_stepIntegrate one Q4 head body. Set zero gravity and no contact-solver iterations. | shirohebi_snake.c |
sprite.c | sprite_init; kq_sprite_oam and companion stateInitialize and use shadow OAM. The game packs entries directly, then commits them; respect slot and coordinate rules. | shirohebi_render.c |
audio_vblank.c | AudioVBlank_Init, PlayMusic, Stop, SetEnabled, EnableIrq, FrameHookResident 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.c | Audio_Init, Audio_Update, Audio_PlaySFXBanked, Audio_StopSfx, Audio_SetPausedEffect playback, priority and pause, coordinated with the VBlank music driver. | shirohebi_audio.c |
save.c | save_exists, save_read, save_writeRead/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.c | cgb_bg_palette, cgb_obj_paletteConfigure the game's CGB colors alongside explicit DMG palette registers. | shirohebi_display.c |
map.c | map_load and the exported map_t descriptorSupport the asset file's map helper; gameplay restoration writes individual cells itself. | shirohebi_assets.c |
fixed.c | fixed-point supportLinked support code; head motion is expressed through physics2d and integer Q4 arithmetic. | shirohebi_common.c |
audio_hwregs_gb.c | shared audio register bindingsProvide 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.
11Source map and safe changes
| File | Responsibility |
|---|---|
shirohebi_common.c | Types, constants, shared state and forward declarations. Read this first when following a symbol. |
shirohebi_main.c | Boot sequence and the input → update → render → VBlank loop. |
shirohebi_flow.c | State dispatch, title options, READY, PLAY, pause, miss, name entry and retry. |
shirohebi_input.c | Release gating, title shortcut and return-to-title confirmation. |
shirohebi_snake.c | Head physics, boost, relative steering, boundary wrap and chain setup/update/growth. |
shirohebi_chain.c | Bank-2 wrapper that includes the complete shared chain implementation. |
shirohebi_collision.c | Mine/self hits, impact classification and scattered-segment animation. |
shirohebi_apples.c | Pomegranate candidate selection, collection, scoring and growth. Apple is the internal identifier. |
shirohebi_demo.c | Attract-mode steering, long-snake pose and elapsed-tick accounting using the VBlank counter. |
shirohebi_render.c | Shadow OAM packing, head/joint/tail tiles, priority rotation and miss effects. |
shirohebi_display.c | LCD/VRAM initialization, palette and font uploads, title and ranking layout. |
shirohebi_background.c | Gameplay background cells, mines and restoration of overwritten cells. |
shirohebi_pause.c | START/PAUSE text overlays with requested/shown/dirty state. |
shirohebi_text.c | Character-to-tile conversion, strings, numeric scores and menu cursors. |
shirohebi_save.c | Ranking validation, insertion, three-character names and SRAM storage. |
shirohebi_audio.c | Music/SFX setup, start/stop, options, pause and channel coordination. |
music.c | Resident fixed-length VBlank music records for the start jingle, main song and game-over song. |
sfx.c | Effect byte streams selected by the game audio functions. |
text_tiles.c | Font masks and conversion into two-bit Game Boy tile rows. |
shirohebi_assets.c | Exported tiles, tilemap, attributes, collision/event tables; map descriptor and helpers. |
shirohebi_input_shared.h | Declarations for the WRAM input flags and fixed-bank callback shared with the interrupt. |
shirohebi_lib_bank0.c | Fixed-bank VBlank input/clock callback, shared WRAM flags and bank-0 selection for the following system/audio inputs. |
shirohebi_lib_bank1.c | Select bank 1 for the following support-library compilation inputs. |
When changing the game
- Speed or steering: change the Q4 constants and inspect both slow turns and UP acceleration. Do not treat the head's facing sector as its exact velocity direction.
- Length: update array capacity, chain capacity and OAM reservation together. Check mine/self collision at the tail and the scanline sprite limit.
- Graphics: preserve tile numbers, map stride and CGB attributes. When removing overlays, restore the live cell underneath.
- Audio: preserve fixed-bank interrupt residency and exactly one music-clock owner. Test pause/resume as well as normal playback.
- Saving: change the payload version when changing the layout; validate length before indexing and test fresh/invalid SRAM.
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.