526 lines
21 KiB
C
526 lines
21 KiB
C
|
|
/**
|
||
|
|
* @file sprite_verbs.c
|
||
|
|
* @brief Tests the group H verbs against the recording mock backend.
|
||
|
|
*
|
||
|
|
* Same shape as tests/graphics_verbs.c and for the same reason: these verbs emit
|
||
|
|
* nothing a golden file can compare, so the assertions are on the call log --
|
||
|
|
* what reached the device, in what order, with what geometry. Running a real
|
||
|
|
* BASIC line rather than calling the handler directly is deliberate; it
|
||
|
|
* exercises the dispatch-table row and the parse handler too, which is where an
|
||
|
|
* added verb is most likely to be wrong. MOVSPR in particular has a parse
|
||
|
|
* handler all its own.
|
||
|
|
*/
|
||
|
|
|
||
|
|
#include <string.h>
|
||
|
|
|
||
|
|
#include <akbasic/error.h>
|
||
|
|
#include <akbasic/runtime.h>
|
||
|
|
#include <akbasic/sprite.h>
|
||
|
|
|
||
|
|
#include "harness.h"
|
||
|
|
#include "mockdevice.h"
|
||
|
|
#include "testutil.h"
|
||
|
|
|
||
|
|
/** @brief Bring up a runtime with the mock devices attached and a program loaded. */
|
||
|
|
static akerr_ErrorContext AKERR_NOIGNORE *run_program(const char *source)
|
||
|
|
{
|
||
|
|
PREPARE_ERROR(errctx);
|
||
|
|
|
||
|
|
PASS(errctx, harness_start(NULL));
|
||
|
|
mock_devices_init();
|
||
|
|
PASS(errctx, akbasic_runtime_set_devices(&HARNESS_RUNTIME, &MOCK_GRAPHICS,
|
||
|
|
&MOCK_AUDIO, &MOCK_INPUT, &MOCK_SPRITES));
|
||
|
|
PASS(errctx, akbasic_runtime_load(&HARNESS_RUNTIME, source));
|
||
|
|
PASS(errctx, akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
||
|
|
PASS(errctx, akbasic_runtime_run(&HARNESS_RUNTIME, 0));
|
||
|
|
SUCCEED_RETURN(errctx);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief SPRITE sets what it is given and leaves alone what it is not.
|
||
|
|
*
|
||
|
|
* The "leaves alone" half is the one worth asserting: BASIC 7.0 lets every
|
||
|
|
* argument after the number be omitted, so a handler that treated a missing
|
||
|
|
* argument as zero would silently turn a sprite off on its way to changing the
|
||
|
|
* colour.
|
||
|
|
*/
|
||
|
|
static void test_sprite(void)
|
||
|
|
{
|
||
|
|
TEST_REQUIRE_OK(run_program("10 SPRITE 1, 1, 3, 0, 1, 0\n"));
|
||
|
|
/* Palette index 3 is red: 0x88, 0x39, 0x32 in src/graphics_tables.c. */
|
||
|
|
TEST_REQUIRE_STR(MOCK.log,
|
||
|
|
"sprcfg 1 136,57,50 x1 y0 b0\n"
|
||
|
|
"sprshow 1 1\n");
|
||
|
|
TEST_REQUIRE(HARNESS_RUNTIME.sprite_state.sprites[0].enabled, "SPRITE 1,1 should enable it");
|
||
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.sprite_state.sprites[0].colorindex, 3);
|
||
|
|
harness_stop();
|
||
|
|
|
||
|
|
/* A second SPRITE naming only the number changes nothing at all. */
|
||
|
|
TEST_REQUIRE_OK(run_program("10 SPRITE 1, 1, 3, 0, 1, 0\n"
|
||
|
|
"20 SPRITE 1\n"));
|
||
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.sprite_state.sprites[0].colorindex, 3);
|
||
|
|
TEST_REQUIRE(HARNESS_RUNTIME.sprite_state.sprites[0].xexpand,
|
||
|
|
"an omitted argument should leave x-expand alone");
|
||
|
|
TEST_REQUIRE(HARNESS_RUNTIME.sprite_state.sprites[0].enabled,
|
||
|
|
"an omitted argument should leave the enable bit alone");
|
||
|
|
harness_stop();
|
||
|
|
|
||
|
|
/* Sprite 0 and sprite 9 do not exist. */
|
||
|
|
TEST_REQUIRE_OK(run_program("10 SPRITE 9, 1\n"));
|
||
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "outside 1..8") != NULL,
|
||
|
|
"sprite 9 should be refused, got \"%s\"", HARNESS_OUTPUT);
|
||
|
|
harness_stop();
|
||
|
|
|
||
|
|
TEST_REQUIRE_OK(run_program("10 SPRITE 1, 1, 17\n"));
|
||
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "outside 1..16") != NULL,
|
||
|
|
"colour 17 should be refused, got \"%s\"", HARNESS_OUTPUT);
|
||
|
|
harness_stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief All four MOVSPR forms, which is really a test of its parse handler.
|
||
|
|
*
|
||
|
|
* The separators are the whole difference between them and they exist nowhere
|
||
|
|
* else in the language, so this is the only thing that says `;` and `#` are
|
||
|
|
* scanned at all.
|
||
|
|
*/
|
||
|
|
static void test_movspr(void)
|
||
|
|
{
|
||
|
|
/* Absolute. */
|
||
|
|
TEST_REQUIRE_OK(run_program("10 MOVSPR 1, 100, 50\n"));
|
||
|
|
TEST_REQUIRE_STR(MOCK.log, "sprmove 1 100.00,50.00\n");
|
||
|
|
harness_stop();
|
||
|
|
|
||
|
|
/* Relative: a signed coordinate is an offset, not a position. */
|
||
|
|
TEST_REQUIRE_OK(run_program("10 MOVSPR 1, 100, 50\n"
|
||
|
|
"20 MOVSPR 1, +10, -20\n"));
|
||
|
|
TEST_REQUIRE_STR(MOCK.log,
|
||
|
|
"sprmove 1 100.00,50.00\n"
|
||
|
|
"sprmove 1 110.00,30.00\n");
|
||
|
|
harness_stop();
|
||
|
|
|
||
|
|
/*
|
||
|
|
* Polar: distance first, then a bearing clockwise from vertical. 90 degrees
|
||
|
|
* is due right, so the whole distance lands on x and none of it on y --
|
||
|
|
* which is the assertion that catches a sine and cosine swapped over.
|
||
|
|
*/
|
||
|
|
TEST_REQUIRE_OK(run_program("10 MOVSPR 1, 100, 50\n"
|
||
|
|
"20 MOVSPR 1, 10 ; 90\n"));
|
||
|
|
TEST_REQUIRE_STR(MOCK.log,
|
||
|
|
"sprmove 1 100.00,50.00\n"
|
||
|
|
"sprmove 1 110.00,50.00\n");
|
||
|
|
harness_stop();
|
||
|
|
|
||
|
|
/* And 0 degrees is straight up, which is *minus* y on a screen. */
|
||
|
|
TEST_REQUIRE_OK(run_program("10 MOVSPR 1, 100, 50\n"
|
||
|
|
"20 MOVSPR 1, 10 ; 0\n"));
|
||
|
|
TEST_REQUIRE_STR(MOCK.log,
|
||
|
|
"sprmove 1 100.00,50.00\n"
|
||
|
|
"sprmove 1 100.00,40.00\n");
|
||
|
|
harness_stop();
|
||
|
|
|
||
|
|
/*
|
||
|
|
* Continuous. Nothing moves on the statement itself -- the sprite starts
|
||
|
|
* moving on the next step, paced off the host's clock -- so what is asserted
|
||
|
|
* here is that the speed was recorded and the device was *not* told to move.
|
||
|
|
*/
|
||
|
|
TEST_REQUIRE_OK(run_program("10 MOVSPR 1, 45 # 8\n"));
|
||
|
|
TEST_REQUIRE_STR(MOCK.log, "");
|
||
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.sprite_state.sprites[0].speed, 8);
|
||
|
|
TEST_REQUIRE_FEQ(HARNESS_RUNTIME.sprite_state.sprites[0].angle, 45.0);
|
||
|
|
harness_stop();
|
||
|
|
|
||
|
|
/* Speed 16 does not exist. */
|
||
|
|
TEST_REQUIRE_OK(run_program("10 MOVSPR 1, 45 # 16\n"));
|
||
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "outside 0..15") != NULL,
|
||
|
|
"speed 16 should be refused, got \"%s\"", HARNESS_OUTPUT);
|
||
|
|
harness_stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Continuous motion is paced off the host's clock, not off step count.
|
||
|
|
*
|
||
|
|
* The service runs on every step, so a program that does nothing still moves --
|
||
|
|
* and a host that never sets a clock still gets a still picture rather than a
|
||
|
|
* sprite tearing across the screen at step rate.
|
||
|
|
*/
|
||
|
|
static void test_continuous_motion(void)
|
||
|
|
{
|
||
|
|
double stopped = 0.0;
|
||
|
|
|
||
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
||
|
|
mock_devices_init();
|
||
|
|
TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, NULL, NULL, NULL,
|
||
|
|
&MOCK_SPRITES));
|
||
|
|
TEST_REQUIRE_OK(akbasic_runtime_settime(&HARNESS_RUNTIME, 1000));
|
||
|
|
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
|
||
|
|
"1 MOVSPR 1, 90 # 10\n"
|
||
|
|
"2 GOTO 2\n"));
|
||
|
|
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
||
|
|
|
||
|
|
/* Line 0 and line 1, with the clock standing still. */
|
||
|
|
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 2));
|
||
|
|
TEST_REQUIRE_FEQ(HARNESS_RUNTIME.sprite_state.sprites[0].x, 0.0);
|
||
|
|
|
||
|
|
/*
|
||
|
|
* One second later: speed 10 at AKBASIC_SPRITE_SPEED_PIXELS_PER_SECOND
|
||
|
|
* apiece, due right, so 50 pixels of x and none of y.
|
||
|
|
*/
|
||
|
|
TEST_REQUIRE_OK(akbasic_runtime_settime(&HARNESS_RUNTIME, 2000));
|
||
|
|
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 1));
|
||
|
|
TEST_REQUIRE_FEQ(HARNESS_RUNTIME.sprite_state.sprites[0].x,
|
||
|
|
10.0 * AKBASIC_SPRITE_SPEED_PIXELS_PER_SECOND);
|
||
|
|
TEST_REQUIRE_FEQ(HARNESS_RUNTIME.sprite_state.sprites[0].y, 0.0);
|
||
|
|
|
||
|
|
/*
|
||
|
|
* Speed 0 stops it where it is. The step that runs the MOVSPR services the
|
||
|
|
* motion *before* it runs the line -- the sprite is still moving when that
|
||
|
|
* step begins -- so one more second's worth lands first; where it stopped is
|
||
|
|
* whatever it reads after that, and the point is that six more seconds do
|
||
|
|
* not move it again.
|
||
|
|
*/
|
||
|
|
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "1 MOVSPR 1, 90 # 0\n"));
|
||
|
|
HARNESS_RUNTIME.environment->nextline = 1;
|
||
|
|
TEST_REQUIRE_OK(akbasic_runtime_settime(&HARNESS_RUNTIME, 3000));
|
||
|
|
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 1));
|
||
|
|
stopped = HARNESS_RUNTIME.sprite_state.sprites[0].x;
|
||
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.sprite_state.sprites[0].speed, 0);
|
||
|
|
|
||
|
|
TEST_REQUIRE_OK(akbasic_runtime_settime(&HARNESS_RUNTIME, 9000));
|
||
|
|
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 1));
|
||
|
|
TEST_REQUIRE_FEQ(HARNESS_RUNTIME.sprite_state.sprites[0].x, stopped);
|
||
|
|
harness_stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief SPRCOLOR sets the two shared registers, and RSPCOLOR reads them back. */
|
||
|
|
static void test_sprcolor(void)
|
||
|
|
{
|
||
|
|
TEST_REQUIRE_OK(run_program("10 SPRCOLOR 5, 7\n"
|
||
|
|
"20 PRINT RSPCOLOR(1)\n"
|
||
|
|
"30 PRINT RSPCOLOR(2)\n"));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "5\n7\n");
|
||
|
|
harness_stop();
|
||
|
|
|
||
|
|
/* One argument leaves the other register alone. */
|
||
|
|
TEST_REQUIRE_OK(run_program("10 SPRCOLOR 5, 7\n"
|
||
|
|
"20 SPRCOLOR 9\n"
|
||
|
|
"30 PRINT RSPCOLOR(1)\n"
|
||
|
|
"40 PRINT RSPCOLOR(2)\n"));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "9\n7\n");
|
||
|
|
harness_stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief The pattern bit order, asserted once here rather than trusted per backend.
|
||
|
|
*
|
||
|
|
* Three bytes per row, most significant bit leftmost. Getting it backwards would
|
||
|
|
* mirror every published sprite horizontally, and nothing else in the suite can
|
||
|
|
* see it: a SPRSAV test fills the array with 0xff, where a reversed bit order
|
||
|
|
* produces the identical picture.
|
||
|
|
*/
|
||
|
|
static void test_pattern_bit_order(void)
|
||
|
|
{
|
||
|
|
uint8_t pattern[AKBASIC_SPRITE_PATTERN_BYTES];
|
||
|
|
uint8_t pixels[AKBASIC_SPRITE_WIDTH * AKBASIC_SPRITE_HEIGHT];
|
||
|
|
int i = 0;
|
||
|
|
|
||
|
|
memset(pattern, 0, sizeof(pattern));
|
||
|
|
/* Row 0: the leftmost pixel and nothing else. */
|
||
|
|
pattern[0] = 0x80;
|
||
|
|
/* Row 1: the rightmost pixel of all 24, which is the low bit of the third byte. */
|
||
|
|
pattern[5] = 0x01;
|
||
|
|
/* Row 2: the ninth pixel across, which is the high bit of the second byte. */
|
||
|
|
pattern[7] = 0x80;
|
||
|
|
|
||
|
|
TEST_REQUIRE_OK(akbasic_sprite_unpack(pattern, sizeof(pattern), pixels));
|
||
|
|
TEST_REQUIRE_INT(pixels[0], 1);
|
||
|
|
TEST_REQUIRE_INT(pixels[1], 0);
|
||
|
|
TEST_REQUIRE_INT(pixels[AKBASIC_SPRITE_WIDTH + (AKBASIC_SPRITE_WIDTH - 1)], 1);
|
||
|
|
TEST_REQUIRE_INT(pixels[AKBASIC_SPRITE_WIDTH + (AKBASIC_SPRITE_WIDTH - 2)], 0);
|
||
|
|
TEST_REQUIRE_INT(pixels[(2 * AKBASIC_SPRITE_WIDTH) + 8], 1);
|
||
|
|
TEST_REQUIRE_INT(pixels[(2 * AKBASIC_SPRITE_WIDTH) + 7], 0);
|
||
|
|
|
||
|
|
/* And every other pixel is clear. */
|
||
|
|
for ( i = 3 * AKBASIC_SPRITE_WIDTH; i < AKBASIC_SPRITE_WIDTH * AKBASIC_SPRITE_HEIGHT; i++ ) {
|
||
|
|
TEST_REQUIRE_INT(pixels[i], 0);
|
||
|
|
}
|
||
|
|
|
||
|
|
/* A pattern that is not 63 bytes is not a pattern. */
|
||
|
|
TEST_REQUIRE_STATUS(akbasic_sprite_unpack(pattern, 8, pixels), AKBASIC_ERR_BOUNDS);
|
||
|
|
TEST_REQUIRE_STATUS(akbasic_sprite_unpack(NULL, sizeof(pattern), pixels), AKERR_NULLPOINTER);
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief SPRSAV takes a pattern from a DIMmed integer array.
|
||
|
|
*
|
||
|
|
* The array form is ours rather than BASIC 7.0's, because a value in this
|
||
|
|
* interpreter carries a NUL-terminated string and a C128 puts 63 raw bytes in
|
||
|
|
* one -- a string that cannot hold a zero byte cannot hold a sprite. See
|
||
|
|
* TODO.md section 5.
|
||
|
|
*/
|
||
|
|
static void test_sprsav_from_array(void)
|
||
|
|
{
|
||
|
|
TEST_REQUIRE_OK(run_program("10 DIM P#(63)\n"
|
||
|
|
"20 FOR I# = 0 TO 62\n"
|
||
|
|
"30 P#(I#) = 255\n"
|
||
|
|
"40 NEXT I#\n"
|
||
|
|
"50 P#(62) = 129\n"
|
||
|
|
"60 SPRSAV P#, 1\n"));
|
||
|
|
TEST_REQUIRE(strstr(MOCK.log, "sprdef 1 63 bytes ff..81") != NULL,
|
||
|
|
"the whole pattern should have reached the device, got \"%s\"", MOCK.log);
|
||
|
|
TEST_REQUIRE(HARNESS_RUNTIME.sprite_state.sprites[0].defined,
|
||
|
|
"SPRSAV should have marked the sprite defined");
|
||
|
|
harness_stop();
|
||
|
|
|
||
|
|
/* An array too small to be a pattern is a program that has miscounted. */
|
||
|
|
TEST_REQUIRE_OK(run_program("10 DIM P#(8)\n"
|
||
|
|
"20 SPRSAV P#, 1\n"));
|
||
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "sprite pattern is 63") != NULL,
|
||
|
|
"a short array should be refused, got \"%s\"", HARNESS_OUTPUT);
|
||
|
|
harness_stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief SPRSAV takes a region SSHAPE saved, told apart by its handle prefix. */
|
||
|
|
static void test_sprsav_from_shape(void)
|
||
|
|
{
|
||
|
|
TEST_REQUIRE_OK(run_program("10 SSHAPE A$, 0, 0, 24, 21\n"
|
||
|
|
"20 SPRSAV A$, 2\n"));
|
||
|
|
TEST_REQUIRE(strstr(MOCK.log, "sprshape 2 from 0") != NULL,
|
||
|
|
"the saved region should have reached the sprite device, got \"%s\"",
|
||
|
|
MOCK.log);
|
||
|
|
harness_stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief SPRSAV takes an image file path, and the program's directory goes with it.
|
||
|
|
*
|
||
|
|
* Any string that is not an SSHAPE handle is a path. What can be asserted from a
|
||
|
|
* mock is the dispatch and the root -- whether a relative path actually resolves
|
||
|
|
* is libakgl's, and is asserted against real pixels in tests/akgl_backends.c.
|
||
|
|
*/
|
||
|
|
static void test_sprsav_from_file(void)
|
||
|
|
{
|
||
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
||
|
|
mock_devices_init();
|
||
|
|
TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, &MOCK_GRAPHICS, NULL, NULL,
|
||
|
|
&MOCK_SPRITES));
|
||
|
|
TEST_REQUIRE_OK(akbasic_runtime_set_source_path(&HARNESS_RUNTIME, "/games/asteroids/main.bas"));
|
||
|
|
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 SPRSAV \"ship.png\", 3\n"));
|
||
|
|
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
||
|
|
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
|
||
|
|
TEST_REQUIRE_STR(MOCK.log,
|
||
|
|
"sprfile 3 ship.png root=/games/asteroids\n"
|
||
|
|
"sprcfg 3 255,255,255 x0 y0 b0\n");
|
||
|
|
harness_stop();
|
||
|
|
|
||
|
|
/* No source path -- a REPL session, or a host that handed over a string. */
|
||
|
|
TEST_REQUIRE_OK(run_program("10 SPRSAV \"ship.png\", 3\n"));
|
||
|
|
TEST_REQUIRE(strstr(MOCK.log, "root=(none)") != NULL,
|
||
|
|
"with no program file the backend should be told so, got \"%s\"", MOCK.log);
|
||
|
|
harness_stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief Writing a sprite back out is a disk operation, and is refused by name. */
|
||
|
|
static void test_sprsav_refuses_to_write(void)
|
||
|
|
{
|
||
|
|
TEST_REQUIRE_OK(run_program("10 SPRSAV 1, \"ship.png\"\n"));
|
||
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "disk operation") != NULL,
|
||
|
|
"SPRSAV out to a file should be refused, got \"%s\"", HARNESS_OUTPUT);
|
||
|
|
harness_stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief COLLISION arms the interrupt, and refuses the two types nothing implements. */
|
||
|
|
static void test_collision_arming(void)
|
||
|
|
{
|
||
|
|
TEST_REQUIRE_OK(run_program("10 COLLISION 1, 100\n"));
|
||
|
|
TEST_REQUIRE(HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_SPRITE].armed,
|
||
|
|
"COLLISION 1 should have armed the sprite interrupt");
|
||
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_SPRITE].line, 100);
|
||
|
|
harness_stop();
|
||
|
|
|
||
|
|
/* A label is stored by name, not resolved here: the handler may be anywhere. */
|
||
|
|
TEST_REQUIRE_OK(run_program("10 COLLISION 1, BUMPED\n"));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_SPRITE].label, "BUMPED");
|
||
|
|
harness_stop();
|
||
|
|
|
||
|
|
/* Omitting the target disarms. */
|
||
|
|
TEST_REQUIRE_OK(run_program("10 COLLISION 1, 100\n"
|
||
|
|
"20 COLLISION 1\n"));
|
||
|
|
TEST_REQUIRE(!HARNESS_RUNTIME.interrupts[AKBASIC_INTERRUPT_SPRITE].armed,
|
||
|
|
"COLLISION with no target should disarm");
|
||
|
|
harness_stop();
|
||
|
|
|
||
|
|
TEST_REQUIRE_OK(run_program("10 COLLISION 2, 100\n"));
|
||
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "read back every frame") != NULL,
|
||
|
|
"COLLISION 2 should be refused with a reason, got \"%s\"", HARNESS_OUTPUT);
|
||
|
|
harness_stop();
|
||
|
|
|
||
|
|
TEST_REQUIRE_OK(run_program("10 COLLISION 3, 100\n"));
|
||
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "light pen") != NULL,
|
||
|
|
"COLLISION 3 should be refused with a reason, got \"%s\"", HARNESS_OUTPUT);
|
||
|
|
harness_stop();
|
||
|
|
|
||
|
|
TEST_REQUIRE_OK(run_program("10 COLLISION 4, 100\n"));
|
||
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "outside 1..3") != NULL,
|
||
|
|
"COLLISION 4 should be refused, got \"%s\"", HARNESS_OUTPUT);
|
||
|
|
harness_stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief A collision the device reports enters the handler and shows up in BUMP.
|
||
|
|
*
|
||
|
|
* The whole path: the mock says two sprites overlap, the service ORs that into
|
||
|
|
* the accumulator and raises the interrupt, the step loop enters the handler,
|
||
|
|
* and BUMP() reports it and clears.
|
||
|
|
*/
|
||
|
|
static void test_collision_fires(void)
|
||
|
|
{
|
||
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
||
|
|
mock_devices_init();
|
||
|
|
TEST_REQUIRE_OK(akbasic_runtime_set_devices(&HARNESS_RUNTIME, NULL, NULL, NULL,
|
||
|
|
&MOCK_SPRITES));
|
||
|
|
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
|
||
|
|
"1 COLLISION 1, 4\n"
|
||
|
|
"2 PRINT \"MAIN\"\n"
|
||
|
|
"3 GOTO 2\n"
|
||
|
|
"4 PRINT \"HIT\"\n"
|
||
|
|
"5 PRINT BUMP(1)\n"
|
||
|
|
"6 RETURN\n"));
|
||
|
|
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
||
|
|
|
||
|
|
/* Line 0 and the COLLISION, with nothing overlapping yet. */
|
||
|
|
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 2));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "");
|
||
|
|
|
||
|
|
/* Sprites 1 and 3 meet. */
|
||
|
|
mock_set_collisions(0x05);
|
||
|
|
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 2));
|
||
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "HIT") != NULL,
|
||
|
|
"the handler should have been entered, got \"%s\"", HARNESS_OUTPUT);
|
||
|
|
|
||
|
|
/* BUMP reports the mask, and clears it. */
|
||
|
|
mock_set_collisions(0);
|
||
|
|
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 1));
|
||
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "5\n") != NULL,
|
||
|
|
"BUMP should have reported sprites 1 and 3, got \"%s\"", HARNESS_OUTPUT);
|
||
|
|
TEST_REQUIRE_INT(HARNESS_RUNTIME.sprite_state.bumped, 0);
|
||
|
|
harness_stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief The readbacks answer from interpreter state, so they need no device. */
|
||
|
|
static void test_readbacks(void)
|
||
|
|
{
|
||
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
||
|
|
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
|
||
|
|
"10 MOVSPR 1, 120, 90\n"
|
||
|
|
"20 PRINT RSPPOS(1, 0)\n"
|
||
|
|
"30 PRINT RSPPOS(1, 1)\n"));
|
||
|
|
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
||
|
|
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
|
||
|
|
/*
|
||
|
|
* MOVSPR refuses for want of a device *after* it has moved the sprite, so
|
||
|
|
* the program stops on line 10 -- but the position took effect, which is
|
||
|
|
* what makes the state readable at all. Read it directly rather than through
|
||
|
|
* PRINT, since the program never got that far.
|
||
|
|
*/
|
||
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "MOVSPR needs a sprite device") != NULL,
|
||
|
|
"MOVSPR without a device should name itself, got \"%s\"", HARNESS_OUTPUT);
|
||
|
|
TEST_REQUIRE_FEQ(HARNESS_RUNTIME.sprite_state.sprites[0].x, 120.0);
|
||
|
|
TEST_REQUIRE_FEQ(HARNESS_RUNTIME.sprite_state.sprites[0].y, 90.0);
|
||
|
|
harness_stop();
|
||
|
|
|
||
|
|
/* With a device attached the same program prints. */
|
||
|
|
TEST_REQUIRE_OK(run_program("10 MOVSPR 1, 120, 90\n"
|
||
|
|
"20 PRINT RSPPOS(1, 0)\n"
|
||
|
|
"30 PRINT RSPPOS(1, 1)\n"
|
||
|
|
"40 PRINT RSPPOS(1, 2)\n"));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "120\n90\n0\n");
|
||
|
|
harness_stop();
|
||
|
|
|
||
|
|
TEST_REQUIRE_OK(run_program("10 SPRITE 1, 1, 6, 1, 1, 0\n"
|
||
|
|
"20 PRINT RSPRITE(1, 0)\n"
|
||
|
|
"30 PRINT RSPRITE(1, 1)\n"
|
||
|
|
"40 PRINT RSPRITE(1, 2)\n"
|
||
|
|
"50 PRINT RSPRITE(1, 3)\n"
|
||
|
|
"60 PRINT RSPRITE(1, 4)\n"));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "1\n6\n1\n1\n0\n");
|
||
|
|
harness_stop();
|
||
|
|
|
||
|
|
/* Out-of-range fields are refused rather than answered with a zero. */
|
||
|
|
TEST_REQUIRE_OK(run_program("10 PRINT RSPPOS(1, 9)\n"));
|
||
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "outside 0..2") != NULL,
|
||
|
|
"RSPPOS field 9 should be refused, got \"%s\"", HARNESS_OUTPUT);
|
||
|
|
harness_stop();
|
||
|
|
|
||
|
|
TEST_REQUIRE_OK(run_program("10 PRINT RSPCOLOR(3)\n"));
|
||
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "is 1 or 2") != NULL,
|
||
|
|
"RSPCOLOR register 3 should be refused, got \"%s\"", HARNESS_OUTPUT);
|
||
|
|
harness_stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* @brief Every verb that needs a device says so by name when it has none.
|
||
|
|
*
|
||
|
|
* This is the standalone driver's situation, so it is the common path. SPRCOLOR
|
||
|
|
* and the readbacks are excluded on purpose: they only touch interpreter state
|
||
|
|
* and must keep working before a host has lent the script anything.
|
||
|
|
*/
|
||
|
|
static void test_no_device(void)
|
||
|
|
{
|
||
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
||
|
|
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME, "10 SPRSAV \"ship.png\", 1\n"));
|
||
|
|
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
||
|
|
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
|
||
|
|
TEST_REQUIRE(strstr(HARNESS_OUTPUT, "SPRSAV needs a sprite device") != NULL,
|
||
|
|
"SPRSAV without a device should name itself, got \"%s\"", HARNESS_OUTPUT);
|
||
|
|
harness_stop();
|
||
|
|
|
||
|
|
TEST_REQUIRE_OK(harness_start(NULL));
|
||
|
|
TEST_REQUIRE_OK(akbasic_runtime_load(&HARNESS_RUNTIME,
|
||
|
|
"10 SPRCOLOR 5, 7\n"
|
||
|
|
"20 PRINT RSPCOLOR(1)\n"));
|
||
|
|
TEST_REQUIRE_OK(akbasic_runtime_start(&HARNESS_RUNTIME, AKBASIC_MODE_RUN));
|
||
|
|
TEST_REQUIRE_OK(akbasic_runtime_run(&HARNESS_RUNTIME, 0));
|
||
|
|
TEST_REQUIRE_STR(HARNESS_OUTPUT, "5\n");
|
||
|
|
harness_stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @brief NEW takes the sprites down with the program that defined them. */
|
||
|
|
static void test_new_clears_sprites(void)
|
||
|
|
{
|
||
|
|
akbasic_ASTLeaf *leaf = NULL;
|
||
|
|
akbasic_Value *out = NULL;
|
||
|
|
|
||
|
|
TEST_REQUIRE_OK(run_program("10 SPRITE 1, 1, 3\n"));
|
||
|
|
TEST_REQUIRE(HARNESS_RUNTIME.sprite_state.sprites[0].enabled, "SPRITE should have enabled it");
|
||
|
|
|
||
|
|
TEST_REQUIRE_OK(akbasic_environment_zero(HARNESS_RUNTIME.environment));
|
||
|
|
TEST_REQUIRE_OK(harness_parse("NEW", &leaf));
|
||
|
|
TEST_REQUIRE_OK(akbasic_runtime_evaluate(&HARNESS_RUNTIME, leaf, &out));
|
||
|
|
|
||
|
|
TEST_REQUIRE(!HARNESS_RUNTIME.sprite_state.sprites[0].enabled,
|
||
|
|
"NEW should have taken the sprite state down");
|
||
|
|
TEST_REQUIRE(strstr(MOCK.log, "sprshow 1 0") != NULL,
|
||
|
|
"NEW should have hidden the sprites on the device, got \"%s\"", MOCK.log);
|
||
|
|
harness_stop();
|
||
|
|
}
|
||
|
|
|
||
|
|
int main(void)
|
||
|
|
{
|
||
|
|
test_sprite();
|
||
|
|
test_movspr();
|
||
|
|
test_continuous_motion();
|
||
|
|
test_sprcolor();
|
||
|
|
test_pattern_bit_order();
|
||
|
|
test_sprsav_from_array();
|
||
|
|
test_sprsav_from_shape();
|
||
|
|
test_sprsav_from_file();
|
||
|
|
test_sprsav_refuses_to_write();
|
||
|
|
test_collision_arming();
|
||
|
|
test_collision_fires();
|
||
|
|
test_readbacks();
|
||
|
|
test_no_device();
|
||
|
|
test_new_clears_sprites();
|
||
|
|
return akbasic_test_failures;
|
||
|
|
}
|