Shooter: A QBASIC Game Port

This example ports the supplied shooter.bas program to a native QuadooScript Application. A black Canvas displays the ships, missiles, terrain, explosions, and score. A 50 ms Windows Timer advances the game and draws each frame, Form keyboard events handle input, and preloaded WAV binary objects supply the sound effects.

Download the complete Shooter project and Win32 build — QuadooScript sources, designer Form, original BASIC program, generated graphics and WAV files, asset generator, checks, and a ready-to-run executable with its runtime DLLs.

Built and checked with the local Win32 release Studio, Canvas/Sprite runtime, and sound-enabled Application host on November 4, 2026. This walkthrough uses static HTML and CSS.

The running QuadooScript Shooter game with a black Canvas, blue player ship, green and red enemies, brown and gray terrain, score and lives, and New game and Exit buttons.
The actual Windows application during a game. The blue ship is the player; green and red ships are enemies. Escaped enemies deduct points, so the score can be negative. Click the screenshot for full size.

Contents

Build and Play

  1. Extract the ZIP to a writable project folder. Open ShooterExample.qsproj using File > Open Project... in Win32 Quadoo Studio.
  2. Use the matching current AQVMT.bin and runtime DLLs beside Studio. This example uses the Canvas, Sprite, and Host.PlaySoundAsync implementations available in the November 4, 2026 release build.
  3. Choose Project > Start Without Debugging (Ctrl+F5). Studio builds and launches bin\Win32\ShooterExample.exe. The ZIP also includes that build for running directly.
  4. Move with the arrow keys and fire with Space. Press P to pause or resume. Switching to another window pauses the game and clears held keys.
  5. Use Enter or New game to restart, and Escape or Exit to close the application.

The playfield retains the original 640×350 game coordinates. At 96 DPI the Canvas is 960×525 physical pixels, giving a 1.5× display. The source uses the Canvas's actual pixel size to calculate its rendering scale.

There are two enemy colors, 100 reusable missile slots, ten enemy slots, an extra-life condition, and a boss encounter after roughly three minutes of unpaused game time. The original Lives counter starts at 3 and ends the game when it drops below zero; the port retains that four-loss rule.

Canvas, Timer, and Form Configuration

ShooterForm.qform.json contains one Form, the Canvas, title/help labels, two dialog buttons, and a nonvisual Timer. The relevant designer settings are:

ObjectSettingsEvents
ShooterFormKeyPreview = true; fixed-size Single borderOnCreate, OnShow, OnClose, OnLeave, OnKeyDown, OnKeyUp, OnKeyPress
Video (Canvas)BackColor = 0 (black), AutoRedraw = true, DoubleBuffered = true, ScaleMode = Pixels, Width 960, Height 525OnResize rebuilds scaled sprite images if needed
FrameTimerInterval = 50 milliseconds; initially Enabled = falseOnTimer → FrameTimer_Tick
NewGameButtonDefault = trueOnClick → NewGame_Click
ExitButtonCancel = trueOnClick → Exit_Click
"properties": {
    "Caption": "Shooter - QuadooScript Application",
    "Width": 992,
    "Height": 630,
    "KeyPreview": true
}

// Canvas properties:
"BackColor": 0,
"AutoRedraw": true,
"DoubleBuffered": true,
"ScaleMode": "Pixels"

// Timer properties:
"Enabled": false,
"Interval": 50

These are settings excerpts; the actual Form also contains identities, bounds, and handler names. Studio maintains the Canvas, Timer, and button member declarations in the source's managed region. The general designer and event workflow applies to this project too.

Replacing the BASIC Screen Loop

The BASIC program uses SCREEN 9, PSET/LINE/CIRCLE/PAINT, GET/PUT sprite buffers, an unrestricted DO loop, and PCOPY to present the offscreen page. The port separates simulation from presentation:

BASIC responsibilityQuadooScript implementation
Shared player, enemy, and missile stateShooterGame, with JSON objects in reusable arrays
Main DO loop and screen page copyA 50 ms Timer calls Game.Step(), PlayEffects(), and Render()
INKEY$ inputForm keyboard handlers update held-key flags and pending taps
GET/PUT image buffersCanvas-associated Sprite objects created once from embedded RGBA pixels
LOCATE / PRINT scoreVideo.DrawText draws the HUD on the Canvas
SOUND statementsPre-generated PCM WAVs, preloaded as binary objects and played asynchronously

Initialization finishes decoding the WAVs and preparing the sprites before enabling the Timer:

Sounds = LoadShooterSounds();
Scale = min((double)Video.PixelWidth / 640,
            (double)Video.PixelHeight / 350);
Sprites = LoadShooterSprites(Video, Scale);
Game = new ShooterGame(timer());
Ready = true;
Render();
FrameTimer.Enabled = true;
virtual function FrameTimer_Tick (args)
{
    if(Ready && !Paused && !Game.GameOver)
    {
        try
        {
            Game.Step();
            PlayEffects();
            Render();
        }
        catch(error)
        {
            FrameTimer.Enabled = false;
            Footer.Caption = "Game stopped: " + error.Value;
        }
    }
    return true;
}

Render clears the Canvas to black and draws the complete scene. AutoRedraw retains those pixels between repaints, and Invalidate schedules their presentation. Drawing stays on the UI thread. The Timer callback contains no busy loop, sleep, synchronous sound, or disk access.

A Windows Timer supplies nominal pacing, not a guaranteed real-time frame rate. Each delivered callback advances one 50 ms game step. There is no catch-up burst after a delayed callback, and paused time does not advance the simulation.

Keyboard Input at Form Level

The Form has KeyPreview enabled. Its OnKeyDown handler reads the read-only args.Key virtual-key value; handled game keys set args.Handled = true. OnKeyUp clears the corresponding held state. The Canvas has no keyboard handlers.

// In the Form's OnKeyDown handler:
if(key == 32 || (key >= 37 && key <= 40))
{
    if(!Paused)
    {
        Game.SetKey(key, true);
    }
    args.Handled = true;
}

// In its OnKeyUp handler:
Game.SetKey(args.Key, false);

The model retains a pending tap as well as the held flag. A quick down/up pair entirely between Timer callbacks therefore still moves or fires once. Held arrows move five game pixels per step, and held Space is limited to one player shot every three steps (150 ms). The Form's OnLeave handler clears input and pauses when focus leaves the application.

Enter and Escape follow the current runtime's native dialog-key route, which deliberately bypasses KeyPreview. The Form's Default New Game button and Cancel Exit button provide those commands through their Form methods. This preserves all game keyboard logic at the Form level while following the implemented dialog behavior.

Porting Graphics to Canvas Sprites

generate_assets.py reconstructs the source's 23×9 player/enemy images, 116×77 boss image, and 32×16 dirt tile using the original shape coordinates and EGA colors. It saves five inspectable PNG files and embeds their raw RGBA pixels in ShooterAssets.quadoo. Black around the ships becomes transparent, while the dirt remains opaque.

At startup ScaledSprite expands those pixels with nearest-neighbor sampling to the current rendering scale, then creates a native Sprite:

var sprite = canvas.CreateSprite();
sprite.LoadPixels(w, h, pixels, "RGBA32");
return sprite;

// Later, during each frame:
var sprite = Sprites[name];
sprite.Draw((int)(Scale * x), (int)(Scale * y));

Canvas shape/text coordinates are physical client pixels. The render helpers use integer coordinates and rectangle objects with Left, Top, Width, and Height. Explosions use DrawEllipse; missiles use small filled rectangles; the HUD uses SetFont and DrawText. Canvas drawing colors are COLORREF values, 0x00BBGGRR.

The current VM converts the right arithmetic operand to the left operand's numeric type. The code explicitly casts the Canvas dimensions to double before division and uses Scale * x, with Scale on the left. This keeps a 1.5× scale from being truncated by integer arithmetic. Pixel indexes in the scaling loop use (double)x / scale and (double)y / scale before conversion back to integers.

The game model never draws, and Render never changes the model's random sequence. Boss explosion positions are prepared during the simulation step so repaint timing cannot change the AI or future random choices.

Converting SOUND to Preloaded WAV Files

The original contains 39 SOUND statements. Repeated adjacent statements form short effects; each complete effect becomes one WAV so successive asynchronous calls do not immediately replace its individual notes. The files use 22,050 Hz mono, 16-bit PCM with a square-wave tone and short ramps to reduce clicks.

The converter interprets each BASIC duration as duration / 18.2 seconds. Microsoft's historical BASIC SOUND implementation documents the clock-tick unit; the supplied shooter.bas remains the authority for this game's frequencies and sequences.

EffectWAVSource sequence
Missile launchfire.wav200/.45, 100/.3, 200/.45 Hz/ticks
Enemy arrivalspawn.wav500/.4, 600/.5, 500/.4, 550/.45
Enemy, player, or boss hithit.wav140/.2, 150/.3, 145/.2, 160/.25
Extra lifelife.wavThe original eight-note 800/775/450/725 Hz sequence
Boss arrivalboss-intro.wavThe original eleven-note 200–400 Hz sequence
Boss explosionrumble0.wav through rumble7.wavEight seeded variations, each with five random 50–149 Hz tones of .2 ticks

Assets/sounds.json records every generated note and actual WAV duration. The eight rumble files provide variation for the BASIC program's random tone loop without generating new audio during gameplay. This is a finite preloaded approximation of that randomized sequence, rather than an identical random stream or a PC-speaker hardware emulation.

The generator embeds the complete WAV bytes as Base64 in the generated source. At the beginning of the Form's lifetime, LoadShooterSounds creates and retains all 13 binary objects:

function LoadShooterSounds ()
{
    var result = new map<string>;
    // The generated file supplies the complete WAV data for each entry.
    result["fire"] = Host.FileFromBase64(null, "...complete WAV Base64...");
    // ...spawn, hit, life, boss-intro, and rumble0 through rumble7...
    return result;
}

The placeholder text above only illustrates the structure; the downloadable source contains the complete data. During play the sound call receives an already-loaded binary object:

SoundOK = Host.PlaySoundAsync(Sounds[chosen]);

No WAV files are read and no Base64 is decoded in the frame callback. The Application host validates the RIFF/WAVE data and snapshots it for asynchronous playback. Its Boolean result reports native-call acceptance, not completed playback or audible output. The footer reports a native false result without stopping the game.

WinMM provides one replacement channel. The sample chooses one effect per frame, prioritizing boss arrival, extra life, hit/rumble, enemy arrival, then firing. Lower-priority events are suppressed during a higher-priority cue's nominal duration; equal-priority sounds may replace each other. This makes important cues survive ordinary shooting. The host stops its owned asynchronous sound when the application shuts down.

To regenerate the assets, run the included generate_assets.py with Python 3 and Pillow, then rebuild in Studio. It writes the PNG/WAV files, sound manifest, and embedded source beside itself. Regeneration is a development step; Python and Pillow are not runtime dependencies.

Game Rules and Deliberate Adaptations

The port retains the source's movement bounds, two enemy colors, randomized movement and firing, normal explosions, +20 enemy kills, -50 escaped enemies, and extra-life milestone condition. The boss reserves two enemy slots, begins with 50 hit points, awards 500 points on defeat, explodes, and resets the ordinary-wave timer.

The original BASIC file is included unchanged in the ZIP so these choices can be compared with the source. Random choices use a seeded generator in the model; the port does not promise the same sequence as QBASIC's RND.

Project Contents and Distribution

ShooterExample/
    ShooterExample.qsproj
    ShooterForm.quadoo
    ShooterForm.qform.json
    ShooterGame.quadoo
    ShooterAssets.quadoo
    ShooterTests.quadoo
    generate_assets.py
    README.txt
    shooter.bas
    Assets/
        player.png, enemy-green.png, enemy-red.png, boss.png, dirt.png
        fire.wav, spawn.wav, hit.wav, life.wav, boss-intro.wav
        rumble0.wav ... rumble7.wav, sounds.json
    bin/Win32/
        ShooterExample.exe
        QuadooForm.dll
        QuadooVM.dll
        QuadooParser.dll
        JSON.dll
        SimbeyCore.dll

The project compiles the three production QuadooScript files and embeds its main Form definition. The generated asset source embeds all sprite pixels and WAV bytes. ShooterTests.quadoo is a standalone console check program and is excluded from the Application project's source list.

Distribute the entire bin\Win32 folder, keeping its five DLLs beside the executable. No BASIC interpreter, optional QuadooPlugin, external WAV/PNG files, or project source is needed for the built game. Windows supplies WINMM.dll and its normal system libraries. The included release binaries require the matching x86 Visual C++ 2008 CRT.

Keep the host template and runtime versions matched when rebuilding: an older executable template will not acquire the new Host sound methods merely by copying a newer DLL. For an x64 build, use matching x64 Studio/template/runtime files. The supplied build and validation are Win32. See the general distribution guide for the Application host's standard files.

Validation and Implementation References

Twenty-seven console checks cover initial state, quick taps, movement bounds, held-fire pacing, pool limits and reuse, missile bounds, collisions, single kill scoring, explosions, extra-life milestones, escape penalties, the source lives rule, game over, the boss trigger and full boss lifecycle, reserved slots, all 13 preloaded WAV binaries, duration conversion, and nearest-neighbor scaling. They exercise the same model and asset code used by the Application.

The game was also built and run in Quadoo Studio, and its Canvas rendering, input, pause/resume, New Game/Enter, Exit/Escape, and asynchronous sound-call acceptance were checked in the native Form. A distribution copy containing only the EXE and five runtime DLLs ran from C:\Windows without source or asset files. The asset generator reproduced all 19 PNG/WAV/manifest files and the embedded source. The screenshot above is from the running application.

The local native repository was used read-only. Relevant implementation references, relative to D:\dev\projects\trunk, are:

The game itself is based on the supplied shooter.bas. The current Canvas, Sprite, Timer, and sound APIs are implemented features used by this sample; it does not rely on proposed interfaces from an unfinished implementation plan.