Files
akbasic/examples/galaga/galaga.h

156 lines
5.2 KiB
C
Raw Permalink Normal View History

/**
* @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 */
Unbreak the three example programs the RND merge left behind Adding native RND and ASC (ae2c702) made RND a function name, and a suffixed identifier that collides with one is refused -- "SYNTAX ERROR Reserved word in variable name". Three example programs held their PRNG output in a variable called RND#, or a host field called RND%, and none of them had run since: - examples/galaga/ bound RND% as a host field on both ENEMY and GAME. The BASIC-visible name is ROLL% now; the C member stays `rnd`. This one was caught by example_galaga and example_galaga_interop, which have been failing. - examples/breakout/characters/breakout.bas and examples/megademo/ megademo.bas both use RND# for their LCG output, renamed to ROLL#. Neither is in any test, so neither failure was visible. examples/breakout/sprites/breakout.bas was broken a second way: seven REM lines the reader refuses. Worth recording that the ceiling is not the one the message names -- src/sink_stdio.c fails when the read filled the buffer without seeing a terminator, so with AKBASIC_MAX_LINE_LENGTH at 80 the message says "79 character limit" and the real maximum is 78, because a 79-character line leaves no room for the newline. The sweeps that fixed the corpus and the megademo for this did not reach this file. The seven comments are reflowed. The prose went stale with the code. Chapter 21 said "there is no RND verb in this dialect; issue #16 tracks adding one", chapter 17's historical aside offered an LCG that no longer parses, and four REM blocks across the two games said the same thing. All of them now say RND exists, and say why these programs keep their own generator anyway: the sequence has to be reproducible for a headless run to be the same game on every machine, which is what lets interop_test.c assert exact counts. Chapter 21 also gains the rule that bit them, since a reader writing a host type will hit it: a host field name is a bare word and shares a namespace with every verb and function. None of this came from the submodule bump -- all three were already broken on main. It was found by running the tutorial games, which nothing else does; that gap is akbasic issue #58. Verified: all three run clean under the dummy drivers, and 114/114 default, 116/116 with akgl. Co-Authored-By: Andrew Kesterson <andrew@aklabs.net> Co-Authored-By: Claude Code (Claude Opus 5, claude-opus-5[1m]) <noreply@anthropic.com>
2026-08-05 23:26:33 -04:00
float rnd; /* inbox: fresh 0..1 each call; ROLL% in BASIC */
} 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;
Unbreak the three example programs the RND merge left behind Adding native RND and ASC (ae2c702) made RND a function name, and a suffixed identifier that collides with one is refused -- "SYNTAX ERROR Reserved word in variable name". Three example programs held their PRNG output in a variable called RND#, or a host field called RND%, and none of them had run since: - examples/galaga/ bound RND% as a host field on both ENEMY and GAME. The BASIC-visible name is ROLL% now; the C member stays `rnd`. This one was caught by example_galaga and example_galaga_interop, which have been failing. - examples/breakout/characters/breakout.bas and examples/megademo/ megademo.bas both use RND# for their LCG output, renamed to ROLL#. Neither is in any test, so neither failure was visible. examples/breakout/sprites/breakout.bas was broken a second way: seven REM lines the reader refuses. Worth recording that the ceiling is not the one the message names -- src/sink_stdio.c fails when the read filled the buffer without seeing a terminator, so with AKBASIC_MAX_LINE_LENGTH at 80 the message says "79 character limit" and the real maximum is 78, because a 79-character line leaves no room for the newline. The sweeps that fixed the corpus and the megademo for this did not reach this file. The seven comments are reflowed. The prose went stale with the code. Chapter 21 said "there is no RND verb in this dialect; issue #16 tracks adding one", chapter 17's historical aside offered an LCG that no longer parses, and four REM blocks across the two games said the same thing. All of them now say RND exists, and say why these programs keep their own generator anyway: the sequence has to be reproducible for a headless run to be the same game on every machine, which is what lets interop_test.c assert exact counts. Chapter 21 also gains the rule that bit them, since a reader writing a host type will hit it: a host field name is a bare word and shares a namespace with every verb and function. None of this came from the submodule bump -- all three were already broken on main. It was found by running the tutorial games, which nothing else does; that gap is akbasic issue #58. Verified: all three run clean under the dummy drivers, and 114/114 default, 116/116 with akgl. Co-Authored-By: Andrew Kesterson <andrew@aklabs.net> Co-Authored-By: Claude Code (Claude Opus 5, claude-opus-5[1m]) <noreply@anthropic.com>
2026-08-05 23:26:33 -04:00
float rnd; /* fresh 0..1 each frame; ROLL% to the script */
} 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_