P4 AAU project - Penguin Lang for the Nintendo Game Boy

2026-02-23

https://https://github.com/cs-25-sw-4-15/penguinlang

Penguin Lang (Spring 2025)

The fourth semester of the bachelor is all about creating a working programming language. We decided to make the programming language "Penguin" — because all programming should relate to animals. This is a domain-specific programming language that tries to abstract the hardware of the Game Boy. It compiles down to SM83 assembly (it is a freak child of Z80 and Intel 8080 — at least the processor is), and from there the RGBDS toolchain creates a ROM for it, which can be run on an emulator or on the real hardware.

Technologies

  • ANTLR4 (Python runtime)
  • SM83 assembly
  • RGBDS
  • Compiler frontend and backend

Abstract (From Project Report)

This project set out to address the nostalgia many people have for retro gaming devices, with a specific focus on the Nintendo Game Boy. The console and its emulators are still used today, but developing for them requires programmers to write in low-level SM83 assembly language and to understand the Game Boy’s hardware in detail. As of 2025, assembly language is neither widely taught nor commonly used, which has resulted in a steep learning curve for programmers who want to develop for the Game Boy. The goal of this project is to design and implement PENGUIN, a statically typed imperative programming language that compiles to SM83 assembly. PENGUIN abstracts away hardware complexity by introducing more accessible higher-level constructs to make Game Boy development more approachable, while it retains low-level control over the hardware. While some features are still unimplemented, PENGUIN represents a solid first step toward making Game Boy development more accessible through higher-level abstractions.

Code Examples

Penguinoid

A remake of the "Unbricked" game written in GM ASM tutorial in 100% Penguin code. Unbriked itself is a Breakout/Arkanoid clone.

tileset tileset_block_0 = "penguinoid_tileset.2bpp";
tileset tileset_block_2 = "penguinoid_tileset.2bpp";
tilemap tilemap0 = "penguinoid_tilemap.bin";

control_waitVBlankStart();
control_LCDoff();

display_tileset_block_0 = tileset_block_0;
display_tileset_block_2 = tileset_block_2;
display_tilemap0 = tilemap0;

int BRICK_LEFT = 5;
int BRICK_RIGHT = 6;
int BLANK_TILE = 8;
int WALL_RIGHT = 7;
int WALL_LEFT = 4;
int WALL_BOTTOM = 9;
int WALL_TOP = 1;
int TOP_RIGHT_CORNER = 0;
int TOP_LEFT_CORNER = 2;

int paddleX = 16;
int paddleY = 145;
int ballX = 25;
int ballY = 100;

int i = 0;
loop (i < 40) {
    display_oam_y[i] = 0;
    i = i + 1;
}

display_oam_tile[0] = 10;
display_oam_x[0] = paddleX;
display_oam_y[0]= paddleY;
display_oam_tile[1] = 11;
display_oam_x[1] = ballX;
display_oam_y[1] = ballY;

int Xtile = 0;
int Ytile = 0;
int Xmomentum = 1; // 1 = right, 2 = left
int Ymomentum = 2; // 1 = down, 2 = up
int tile = 0;
int left = 0;
int right = 0;

procedure handleWallCollision() {
    if (tile == WALL_LEFT or tile == WALL_RIGHT or tile == TOP_LEFT_CORNER or tile == TOP_RIGHT_CORNER) {
        if (Xmomentum == 1) {
            Xmomentum = 2;
        } else {
            Xmomentum = 1;
        }
    }

    if (tile == WALL_TOP or tile == WALL_BOTTOM) {
        if (Ymomentum == 1) {
            Ymomentum = 2;
        } else {
            Ymomentum = 1;
        }
    }
}

procedure handleBrickCollision() {
    if (tile == BRICK_LEFT or tile == BRICK_RIGHT) {
        display_tilemap0[Xtile][Ytile] = BLANK_TILE;
        if (Ymomentum == 1) {
            Ymomentum = 2;
        } else {
            Ymomentum = 1;
        }
        control_waitVBlankOver();
        control_waitVBlankStart();
        if (tile == BRICK_LEFT) {
            display_tilemap0[Xtile+1][Ytile] = BLANK_TILE;
        } else {
            display_tilemap0[Xtile-1][Ytile] = BLANK_TILE;
        }
    }
}

procedure handlePaddleCollision() {
    if (ballX <= (paddleX+8)) {
        if (ballX >= (paddleX - 8)) {
            if (((ballY+5) >= 145)) {
                if (((ballY+3) <= 145)) {
                    if (Ymomentum == 1) {
                        Ymomentum = 2;
                    }
                }
            }
            if (((ballY) >= 145)) {
                if (((ballY-3) <= 145)) {
                    if (Ymomentum == 2) {
                        Ymomentum = 1;
                    }
                }
            }
        }
    }
}

procedure updateBallPosition() {
    if (Xmomentum == 1) {
        ballX = ballX + 2;
    } else {
        ballX = ballX - 2;
    }
    if (Ymomentum == 1) {
        ballY = ballY + 2;
    } else {
        ballY = ballY - 2;
    }
}

procedure setBallAndPaddlePosition() {
    if (left) {
        if (paddleX > 16) {
            paddleX = paddleX - 2;
        }
    }
    if (right) {
        if (paddleX < 104) {
            paddleX = paddleX + 2;
        }
    }

    display_oam_x[1] = ballX;
    display_oam_y[1] = ballY;
    display_oam_x[0] = paddleX;
    display_oam_y[0] = 145;
}

procedure main() {
    int frame = 0;
    control_LCDon();
    control_initDisplayRegs();
    control_initPalette();

    loop (1) {
        control_waitVBlankOver();
        control_waitVBlankStart();

        if (frame == 0) {
            control_updateInput();
            left = control_checkLeft();
            right = control_checkRight();

            updateBallPosition();

            Xtile = (ballX-9) >> 3;
            Ytile = (ballY-16) >> 3;
            tile = display_tilemap0[Xtile][Ytile];

            handleBrickCollision();
            handleWallCollision();
            handlePaddleCollision();
            frame = 1;
        } else {
            if (frame == 1) {
                setBallAndPaddlePosition();
                frame = 0;
            }
        }
    }
}

main();

Images

Penguinoid / Unbricked

Screenshot of Penguinoid running on an emulator
Screenshot of Penguinoid running on an emulator