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:
155
examples/galaga/galaga.h
Normal file
155
examples/galaga/galaga.h
Normal file
@@ -0,0 +1,155 @@
|
||||
/**
|
||||
* @file galaga.h
|
||||
* @brief Shared declarations for the GALAGA embedding example.
|
||||
*
|
||||
* The engine is C on libakgl; the enemies think in BASIC. Everything the two
|
||||
* sides share crosses in exactly one place: the three structures below, which
|
||||
* script.c registers as host types so a script reads and writes them directly.
|
||||
* docs/20-tutorial-galaga.md and docs/21-tutorial-galaga-enemies.md build this
|
||||
* program from an empty file; the split between files follows the split
|
||||
* between chapters.
|
||||
*/
|
||||
|
||||
#ifndef _GALAGA_H_
|
||||
#define _GALAGA_H_
|
||||
|
||||
#include <stdbool.h>
|
||||
#include <stdint.h>
|
||||
|
||||
#include <akerror.h>
|
||||
|
||||
#include <akgl/actor.h>
|
||||
|
||||
/* ------------------------------------------------------------- geometry --- */
|
||||
|
||||
/*
|
||||
* The view is sized to the artwork rather than the other way round: the Kenney
|
||||
* sprites are ~100 pixels wide, libakgl has no way to draw a sprite smaller
|
||||
* than it is (akgl_Actor.scale is overwritten every frame -- libakgl
|
||||
* docs/12-actors.md), and a ten-column formation of them needs 1120 pixels.
|
||||
*/
|
||||
#define GALAGA_VIEW_WIDTH 1280
|
||||
#define GALAGA_VIEW_HEIGHT 960
|
||||
|
||||
#define GALAGA_FORM_COLUMNS 10 /* formation width, in slots */
|
||||
#define GALAGA_FORM_LEFT 136 /* x of column 0, map pixels */
|
||||
#define GALAGA_FORM_TOP 120 /* y of row 0, map pixels */
|
||||
#define GALAGA_COL_PITCH 112
|
||||
#define GALAGA_ROW_PITCH 100
|
||||
|
||||
#define GALAGA_PLAYER_Y 860.0f
|
||||
#define GALAGA_PLAYER_SPEED 420.0f /* map pixels per second */
|
||||
#define GALAGA_PLAYER_MARGIN 60.0f /* how close to the edge it may go */
|
||||
|
||||
/* ------------------------------------------------------------- entities --- */
|
||||
|
||||
#define GALAGA_ENEMY_BEE 0
|
||||
#define GALAGA_ENEMY_BUTTERFLY 1
|
||||
#define GALAGA_ENEMY_BOSS 2
|
||||
#define GALAGA_ENEMY_KINDS 3
|
||||
|
||||
#define GALAGA_MAX_ENEMIES 40
|
||||
#define GALAGA_MAX_PLAYER_SHOTS 2 /* the classic two-on-screen rule */
|
||||
#define GALAGA_MAX_ENEMY_SHOTS 8
|
||||
|
||||
/*
|
||||
* galaga_Enemy.state bits. The script owns these transitions; the engine only
|
||||
* writes the word at spawn and when a script error forces an enemy dumb.
|
||||
* galaga.bas spells the same three values as literals, with a REM naming them.
|
||||
*
|
||||
* 8 0
|
||||
* 0 0 0 0 0 1 1 1
|
||||
* | | `-- ENTERING: flying its entry path toward the formation slot
|
||||
* | `---- FORMATION: holding (and breathing around) homex/homey
|
||||
* `------ DIVING: attacking, off the grid until it leaves the screen
|
||||
*/
|
||||
#define GALAGA_ES_ENTERING (1 << 0)
|
||||
#define GALAGA_ES_FORMATION (1 << 1)
|
||||
#define GALAGA_ES_DIVING (1 << 2)
|
||||
|
||||
/** @brief One enemy, as both sides see it. Hangs off akgl_Actor.actorData. */
|
||||
typedef struct galaga_Enemy
|
||||
{
|
||||
int32_t kind; /* GALAGA_ENEMY_BEE / BUTTERFLY / BOSS */
|
||||
int32_t state; /* GALAGA_ES_* bit flags */
|
||||
float homex; /* formation slot, in map pixels */
|
||||
float homey;
|
||||
float t; /* parametric clock for the current maneuver */
|
||||
int32_t hp;
|
||||
int32_t fire; /* outbox: script sets 1, engine consumes */
|
||||
float rnd; /* inbox: engine writes fresh 0..1 each call */
|
||||
} galaga_Enemy;
|
||||
|
||||
/** @brief Frame state every enemy may read. Bound once as GAME@. */
|
||||
typedef struct galaga_Shared
|
||||
{
|
||||
float playerx; /* the player actor's position, this frame */
|
||||
float playery;
|
||||
int32_t wave;
|
||||
float rnd; /* fresh 0..1 each frame; the issue #16 route */
|
||||
} galaga_Shared;
|
||||
|
||||
/* --------------------------------------------------------------- screens --- */
|
||||
|
||||
typedef enum
|
||||
{
|
||||
GALAGA_SCREEN_TITLE = 0,
|
||||
GALAGA_SCREEN_PLAY,
|
||||
GALAGA_SCREEN_GAMEOVER,
|
||||
GALAGA_SCREEN_VICTORY
|
||||
} galaga_Screen;
|
||||
|
||||
/* ------------------------------------------------------------ game state --- */
|
||||
|
||||
typedef struct galaga_Game
|
||||
{
|
||||
galaga_Screen screen;
|
||||
int frame;
|
||||
float dt; /* seconds, clamped; see main.c */
|
||||
bool autoplay;
|
||||
|
||||
int score;
|
||||
int lives;
|
||||
int kills[GALAGA_ENEMY_KINDS];
|
||||
int shots[GALAGA_ENEMY_KINDS]; /* shots each kind fired */
|
||||
int script_errors;
|
||||
|
||||
akgl_Actor *player;
|
||||
float fire_cooldown;
|
||||
float respawn_timer; /* > 0 while the player is invulnerable */
|
||||
bool firing;
|
||||
bool moveleft;
|
||||
bool moveright;
|
||||
|
||||
int player_shots_live;
|
||||
int enemy_shots_live;
|
||||
} galaga_Game;
|
||||
|
||||
extern galaga_Game galaga_game;
|
||||
extern galaga_Shared galaga_shared;
|
||||
|
||||
extern galaga_Enemy galaga_enemies[GALAGA_MAX_ENEMIES];
|
||||
extern akgl_Actor *galaga_enemy_actors[GALAGA_MAX_ENEMIES];
|
||||
|
||||
/* ---------------------------------------------------------------- script --- */
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *galaga_script_boot(char *path);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *galaga_script_update_enemy(galaga_Enemy *enemy, akgl_Actor *actor, float dt);
|
||||
|
||||
/* --------------------------------------------------------------- enemies --- */
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *galaga_wave_spawn(void);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *galaga_wave_release(void);
|
||||
int galaga_enemies_alive(void);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *galaga_boom_spawn(float x, float y);
|
||||
|
||||
/* ---------------------------------------------------------------- player --- */
|
||||
|
||||
akerr_ErrorContext AKERR_NOIGNORE *galaga_player_spawn(void);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *galaga_player_controls(void);
|
||||
akerr_ErrorContext AKERR_NOIGNORE *galaga_player_autoplay(int frame);
|
||||
|
||||
/** @brief A 0..1 random draw from the engine's own PRNG (see enemies.c). */
|
||||
float galaga_random(void);
|
||||
|
||||
#endif // _GALAGA_H_
|
||||
Reference in New Issue
Block a user