Public Access
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:
@@ -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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user