Games: Asteroids as a state machine, and a lean as a turn

A ship, rocks that break in two and again in two, short-lived bullets,
three ships and waves that grow, on a field that wraps round. Its turn
comes from -1 to 1, which tiltTurn makes from the device's lean around
the angle it was held at when the game began. Host-tested.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0162FokPdvY2KsS4NBfwyWPk
This commit is contained in:
2026-10-15 12:06:29 +02:00
co-authored by Claude Opus 5.5
parent b1e6242b29
commit af9077cb94
3 changed files with 421 additions and 0 deletions
+159
View File
@@ -1,5 +1,8 @@
#include <unity.h>
#include <cmath>
#include "asteroids.h"
#include "mines.h"
#include "twenty48.h"
@@ -285,6 +288,153 @@ void test_2048_new_tiles_are_twos_and_sometimes_fours() {
TEST_ASSERT_TRUE(fours > 20 && fours < 120); // about one in ten
}
// ---- Asteroids ----
namespace {
Asteroids::Rock rockAt(float x, float y, uint8_t size, float vx = 0, float vy = 0) {
Asteroids::Rock r{};
r.x = x;
r.y = y;
r.vx = vx;
r.vy = vy;
r.size = size;
return r;
}
void fly(Asteroids& a, float seconds, AsteroidsInput in = AsteroidsInput()) {
for (float t = 0; t < seconds; t += 0.02f) a.step(0.02f, in);
}
} // namespace
void test_asteroids_a_lean_is_a_turn() {
TEST_ASSERT_EQUAL_FLOAT(0, tiltTurn(0.02f, 0)); // a steady hand: straight on
TEST_ASSERT_EQUAL_FLOAT(0, tiltTurn(0.32f, 0.30f)); // around how it was held at the start
TEST_ASSERT_TRUE(tiltTurn(0.2f, 0) > 0.3f && tiltTurn(0.2f, 0) < 0.7f);
TEST_ASSERT_EQUAL_FLOAT(1, tiltTurn(0.6f, 0));
TEST_ASSERT_EQUAL_FLOAT(-1, tiltTurn(-0.6f, 0));
TEST_ASSERT_EQUAL_FLOAT(-tiltTurn(0.2f, 0), tiltTurn(-0.2f, 0));
}
void test_asteroids_a_wave_starts_away_from_the_ship() {
for (uint32_t seed = 1; seed <= 20; seed++) {
Asteroids a(seed);
TEST_ASSERT_EQUAL(1, a.wave());
TEST_ASSERT_EQUAL(3, a.rocks().size());
for (auto& r : a.rocks()) {
TEST_ASSERT_EQUAL(3, r.size);
TEST_ASSERT_TRUE(std::hypot(r.x - a.shipX(), r.y - a.shipY()) > 40);
}
TEST_ASSERT_EQUAL(Asteroids::kLives, a.lives());
}
}
void test_asteroids_the_ship_turns_and_thrusts() {
Asteroids a(1);
a.setRocks({rockAt(10, 10, 1)});
AsteroidsInput in;
in.turn = 1;
fly(a, 0.5f, in);
TEST_ASSERT_FLOAT_WITHIN(0.1, Asteroids::kTurnSpeed * 0.5f, a.shipAngle());
a.placeShip(120, 60, 0); // pointing up
in = AsteroidsInput();
in.thrust = true;
fly(a, 0.5f, in);
TEST_ASSERT_TRUE(a.shipY() < 50);
TEST_ASSERT_FLOAT_WITHIN(1, 120, a.shipX());
TEST_ASSERT_TRUE(a.thrusting());
float y = a.shipY();
fly(a, 3); // no thrust: it drifts on, slowing
TEST_ASSERT_TRUE(a.shipY() != y);
}
void test_asteroids_everything_wraps_round() {
Asteroids a(1);
a.setRocks({rockAt(235, 60, 1, 40, 0)});
fly(a, 0.5f);
TEST_ASSERT_TRUE(a.rocks()[0].x < 30);
a.placeShip(120, 2, 0);
AsteroidsInput in;
in.thrust = true;
fly(a, 0.3f, in);
TEST_ASSERT_TRUE(a.shipY() > 100); // out at the top, in at the bottom
}
void test_asteroids_a_bullet_breaks_a_rock_in_two() {
Asteroids a(1);
a.placeShip(120, 100, 0);
a.setRocks({rockAt(120, 60, 3), rockAt(10, 10, 1)});
AsteroidsInput in;
in.fire = true;
uint8_t all = a.step(0.02f, in);
TEST_ASSERT_TRUE(all & Asteroids::Shot);
for (int i = 0; i < 20; i++) all |= a.step(0.02f, AsteroidsInput());
TEST_ASSERT_TRUE(all & Asteroids::Hit);
TEST_ASSERT_EQUAL(20, a.score());
int middle = 0;
for (auto& r : a.rocks()) middle += r.size == 2;
TEST_ASSERT_EQUAL(2, middle);
TEST_ASSERT_EQUAL(0, a.bullets().size()); // spent on it
}
void test_asteroids_a_small_rock_is_gone_and_scores_most() {
Asteroids a(1);
a.placeShip(120, 100, 0);
a.setRocks({rockAt(120, 70, 1), rockAt(10, 10, 1)});
AsteroidsInput in;
in.fire = true;
a.step(0.02f, in);
fly(a, 0.4f);
TEST_ASSERT_EQUAL(100, a.score());
TEST_ASSERT_EQUAL(1, a.rocks().size());
}
void test_asteroids_bullets_are_few_and_short_lived() {
Asteroids a(1);
a.setRocks({rockAt(10, 10, 1)});
a.placeShip(120, 60, 1.57f); // to the right, where nothing is
AsteroidsInput in;
in.fire = true;
for (int i = 0; i < 10; i++) a.step(0.01f, in);
TEST_ASSERT_EQUAL(Asteroids::kMaxBullets, a.bullets().size());
fly(a, Asteroids::kBulletLife + 0.1f);
TEST_ASSERT_EQUAL(0, a.bullets().size());
}
void test_asteroids_a_rock_costs_a_ship_and_the_last_ends_it() {
Asteroids a(1);
a.setRocks({rockAt(120, 60, 3)});
a.placeShip(120, 60, 0);
fly(a, 0.5f); // shielded for a moment after coming in
TEST_ASSERT_EQUAL(Asteroids::kLives, a.lives());
uint8_t all = 0;
for (int i = 0; i < 200 && !(all & Asteroids::Died); i++) all |= a.step(0.02f, AsteroidsInput());
TEST_ASSERT_TRUE(all & Asteroids::Died);
TEST_ASSERT_EQUAL(Asteroids::kLives - 1, a.lives());
TEST_ASSERT_FALSE(a.shipShown()); // out for a moment, then back in the middle
fly(a, Asteroids::kRespawnWait + 0.1f);
TEST_ASSERT_TRUE(a.shipShown());
TEST_ASSERT_TRUE(a.shielded());
for (int life = 0; life < 2; life++) {
a.setRocks({rockAt(a.shipX(), a.shipY(), 3)});
fly(a, Asteroids::kShielded + Asteroids::kRespawnWait + 0.5f);
}
TEST_ASSERT_TRUE(a.over());
TEST_ASSERT_EQUAL(0, a.lives());
TEST_ASSERT_EQUAL(0, a.step(1, AsteroidsInput()));
}
void test_asteroids_the_last_rock_brings_the_next_wave() {
Asteroids a(1);
a.placeShip(120, 100, 0);
a.setRocks({rockAt(120, 70, 1)});
AsteroidsInput in;
in.fire = true;
uint8_t all = a.step(0.02f, in);
for (int i = 0; i < 30; i++) all |= a.step(0.02f, AsteroidsInput());
TEST_ASSERT_TRUE(all & Asteroids::Wave);
TEST_ASSERT_EQUAL(2, a.wave());
TEST_ASSERT_EQUAL(4, a.rocks().size()); // one more each wave
}
int main() {
UNITY_BEGIN();
RUN_TEST(test_mines_the_first_dig_is_never_a_mine_and_opens_something);
@@ -308,5 +458,14 @@ int main() {
RUN_TEST(test_2048_is_over_when_nothing_can_move);
RUN_TEST(test_2048_the_same_seed_is_the_same_game);
RUN_TEST(test_2048_new_tiles_are_twos_and_sometimes_fours);
RUN_TEST(test_asteroids_a_lean_is_a_turn);
RUN_TEST(test_asteroids_a_wave_starts_away_from_the_ship);
RUN_TEST(test_asteroids_the_ship_turns_and_thrusts);
RUN_TEST(test_asteroids_everything_wraps_round);
RUN_TEST(test_asteroids_a_bullet_breaks_a_rock_in_two);
RUN_TEST(test_asteroids_a_small_rock_is_gone_and_scores_most);
RUN_TEST(test_asteroids_bullets_are_few_and_short_lived);
RUN_TEST(test_asteroids_a_rock_costs_a_ship_and_the_last_ends_it);
RUN_TEST(test_asteroids_the_last_rock_brings_the_next_wave);
return UNITY_END();
}