Add the galaga example: a C engine with akbasic as its enemy brain
A GALAGA-style fixed shooter whose engine is C on libakgl (null physics) with the interpreter embedded as the scripting engine that owns every enemy's behavior. One DEF-only script is called per enemy per frame through a custom akgl_Actor update hook; SELF@, ACTOR@ and GAME@ are host bindings, so the script reads and writes the engine's real memory -- the boss even swaps its own damage sprite by raising an actor state bit from BASIC. Bullets, collision, scoring and screens stay C. Structure arguments were measured and rejected for the per-frame path: each pointer parameter spends a value-pool slot the pool never reclaims, 1,015 calls to exhaustion against an unbounded rebind (issue #36). Built when AKBASIC_WITH_AKGL=ON. Two CTest entries: a 600-frame headless autoplay run under the dummy SDL drivers, and an interop round-trip test that links the real script.c and galaga.bas and pins the four boundary claims, 24,000 sustained calls among them. docs_galaga_figures regenerates the two checked-in figures. Art is Kenney CC0, byte for byte, with provenance. Co-authored-by: andrew <andrew@aklabs.net> Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XiGgpHuXUm2mR4Wzndw3dc
This commit is contained in:
132
examples/galaga/interop_test.c
Normal file
132
examples/galaga/interop_test.c
Normal file
@@ -0,0 +1,132 @@
|
||||
/**
|
||||
* @file interop_test.c
|
||||
* @brief Round-trip test for the galaga boundary, hoststruct.c-style.
|
||||
*
|
||||
* Links the real script.c and the real galaga.bas -- not copies -- so this
|
||||
* fails the moment the boundary and the script disagree. The four claims it
|
||||
* pins:
|
||||
*
|
||||
* 1. The script writes the engine's actor memory: a formation enemy's sway
|
||||
* lands in akgl_Actor.x with no marshalling step.
|
||||
* 2. The outbox works: a diving enemy above the player raises FIRE# and the
|
||||
* C side reads it.
|
||||
* 3. The boss flips actor state bit 13 at one hit point -- the boundary
|
||||
* crossed engine-ward.
|
||||
* 4. Sustained calling holds: 24000 calls through the per-call
|
||||
* akbasic_environment_zero() regime, the load a 40-enemy wave puts on
|
||||
* the runtime in ten seconds.
|
||||
*
|
||||
* Exit status equals the number of failed claims.
|
||||
*/
|
||||
|
||||
#include <math.h>
|
||||
#include <stdbool.h>
|
||||
#include <stddef.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/actor.h>
|
||||
|
||||
#include "galaga.h"
|
||||
|
||||
#ifndef GALAGA_SCRIPT_PATH
|
||||
#define GALAGA_SCRIPT_PATH "galaga.bas"
|
||||
#endif
|
||||
|
||||
/* script.c reads these; main.c usually defines them. This test is the host. */
|
||||
galaga_Game galaga_game;
|
||||
galaga_Shared galaga_shared;
|
||||
|
||||
static int FAILURES = 0;
|
||||
|
||||
#define CLAIM(__cond, __text) \
|
||||
if ( !(__cond) ) { \
|
||||
fprintf(stderr, "FAILED: %s\n", __text); \
|
||||
FAILURES += 1; \
|
||||
} else { \
|
||||
printf("ok: %s\n", __text); \
|
||||
}
|
||||
|
||||
static akerr_ErrorContext *run_claims(void)
|
||||
{
|
||||
galaga_Enemy enemy;
|
||||
akgl_Actor actor;
|
||||
int i = 0;
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
PASS(errctx, galaga_script_boot((char *)GALAGA_SCRIPT_PATH));
|
||||
|
||||
/* --- 1: formation sway lands in the actor ---------------------------- */
|
||||
memset(&enemy, 0, sizeof(enemy));
|
||||
memset(&actor, 0, sizeof(actor));
|
||||
enemy.kind = GALAGA_ENEMY_BEE;
|
||||
enemy.state = GALAGA_ES_FORMATION;
|
||||
enemy.homex = 400.0f;
|
||||
enemy.homey = 300.0f;
|
||||
enemy.hp = 1;
|
||||
actor.x = 0.0f;
|
||||
actor.y = 0.0f;
|
||||
PASS(errctx, galaga_script_update_enemy(&enemy, &actor, 0.016f));
|
||||
CLAIM((fabsf(actor.x - enemy.homex) <= 16.5f) && (actor.y == enemy.homey),
|
||||
"a formation bee's sway is written into akgl_Actor.x/y by the script");
|
||||
|
||||
/* --- 2: the fire outbox ---------------------------------------------- */
|
||||
memset(&enemy, 0, sizeof(enemy));
|
||||
memset(&actor, 0, sizeof(actor));
|
||||
enemy.kind = GALAGA_ENEMY_BEE;
|
||||
enemy.state = GALAGA_ES_DIVING;
|
||||
enemy.rnd = 0.0f; /* 0.0 < DT% * 1.5: always willing */
|
||||
actor.x = 600.0f;
|
||||
actor.y = 200.0f;
|
||||
galaga_shared.playerx = 610.0f; /* just off the shot's column */
|
||||
galaga_shared.playery = 860.0f; /* well below */
|
||||
PASS(errctx, galaga_script_update_enemy(&enemy, &actor, 0.016f));
|
||||
CLAIM(enemy.fire == 1,
|
||||
"a diving bee above the player raises FIRE# for the engine to consume");
|
||||
|
||||
/* --- 3: the boss's hurt bit ------------------------------------------ */
|
||||
memset(&enemy, 0, sizeof(enemy));
|
||||
memset(&actor, 0, sizeof(actor));
|
||||
enemy.kind = GALAGA_ENEMY_BOSS;
|
||||
enemy.state = GALAGA_ES_FORMATION;
|
||||
enemy.homex = 500.0f;
|
||||
enemy.homey = 120.0f;
|
||||
enemy.hp = 1;
|
||||
actor.state = AKGL_ACTOR_STATE_ALIVE;
|
||||
PASS(errctx, galaga_script_update_enemy(&enemy, &actor, 0.016f));
|
||||
CLAIM((actor.state & AKGL_ACTOR_STATE_UNDEFINED_13) != 0,
|
||||
"a boss at one hit point raises actor state bit 13 from BASIC");
|
||||
|
||||
/* --- 4: sustained calling --------------------------------------------- */
|
||||
memset(&enemy, 0, sizeof(enemy));
|
||||
memset(&actor, 0, sizeof(actor));
|
||||
enemy.kind = GALAGA_ENEMY_BEE;
|
||||
enemy.state = GALAGA_ES_FORMATION;
|
||||
enemy.homex = 400.0f;
|
||||
enemy.homey = 300.0f;
|
||||
for ( i = 0; i < 24000; i++ ) {
|
||||
enemy.rnd = 0.9f; /* never dive: keep the state put */
|
||||
PASS(errctx, galaga_script_update_enemy(&enemy, &actor, 0.016f));
|
||||
}
|
||||
CLAIM(galaga_game.script_errors == 0,
|
||||
"24000 calls survive the per-call akbasic_environment_zero() regime");
|
||||
SUCCEED_RETURN(errctx);
|
||||
}
|
||||
|
||||
int main(void)
|
||||
{
|
||||
PREPARE_ERROR(errctx);
|
||||
|
||||
ATTEMPT {
|
||||
CATCH(errctx, run_claims());
|
||||
} CLEANUP {
|
||||
} PROCESS(errctx) {
|
||||
} HANDLE_DEFAULT(errctx) {
|
||||
LOG_ERROR_WITH_MESSAGE(errctx, "the interop test could not run");
|
||||
FAILURES += 1;
|
||||
} FINISH_NORETURN(errctx);
|
||||
|
||||
return FAILURES;
|
||||
}
|
||||
Reference in New Issue
Block a user