diff --git a/lib/games/src/pong.cpp b/lib/games/src/pong.cpp index c8acc76..803f37a 100644 --- a/lib/games/src/pong.cpp +++ b/lib/games/src/pong.cpp @@ -11,6 +11,17 @@ constexpr float kSteepest = 0.87f; // 50 degrees, in radians: off the very end float clampPaddle(float y) { return std::clamp(y, Pong::kPaddleH / 2, Pong::kHeight - Pong::kPaddleH / 2); } } // namespace +float tiltTarget(float pitch, float level) { + constexpr float kFullTilt = 0.26f; // 15 degrees either way covers the whole field + float travel = Pong::kHeight / 2 - Pong::kPaddleH / 2; + return Pong::kHeight / 2 + std::clamp((pitch - level) / kFullTilt, -1.0f, 1.0f) * travel; +} + +float moveToward(float paddleY, float targetY) { + constexpr float kSlowWithin = 10; // pixels: closer than that, it eases in + return std::clamp((targetY - paddleY) / kSlowWithin, -1.0f, 1.0f); +} + void Pong::start() { playerScore_ = deviceScore_ = rally_ = longest_ = 0; player_ = device_ = kHeight / 2; @@ -39,7 +50,7 @@ void Pong::bounceOff(float paddleY, bool toRight) { vy_ = std::sin(where * kSteepest) * speed_; } -uint8_t Pong::step(float seconds, int move) { +uint8_t Pong::step(float seconds, float move) { uint8_t events = 0; seconds = std::min(seconds, 0.25f); // a held-up loop is not a quarter of the field while (seconds > 0 && phase_ != Phase::Over) { @@ -50,8 +61,8 @@ uint8_t Pong::step(float seconds, int move) { return events; } -uint8_t Pong::tick(float dt, int move) { - player_ = clampPaddle(player_ + (move < 0 ? -1 : move > 0 ? 1 : 0) * kPlayerSpeed * dt); +uint8_t Pong::tick(float dt, float move) { + player_ = clampPaddle(player_ + std::clamp(move, -1.0f, 1.0f) * kPlayerSpeed * dt); // The device watches the ball once it comes its way, and waits in the middle otherwise. float target = vx_ > 0 && phase_ == Phase::Play ? y_ : kHeight / 2; float reach = kDeviceSpeed * dt; diff --git a/lib/games/src/pong.h b/lib/games/src/pong.h index 60ded1d..097b671 100644 --- a/lib/games/src/pong.h +++ b/lib/games/src/pong.h @@ -9,6 +9,15 @@ namespace roro::games { // Pong against the device (issue #101): the player's paddle on the left, the device's on the // right, first to 7. The ball goes faster with each return, and the device's paddle only moves so // fast: a steep, fast ball gets past it. Lengths are pixels of the 240 x 122 field, times seconds. +// Where the paddle is asked to be when the device is tilted (issue #101's follow-up): its top +// lowered, the paddle goes up, as a ball rolls to the low side (it felt the right way round on the +// device); held as it was when the game began, the middle. `pitch` and +// `level` are "up" along the device's top-bottom axis, -1 to 1, as the accelerometer gives it. +float tiltTarget(float pitch, float level); +// The move that brings the paddle at `paddleY` to `targetY` with no overshoot: full speed far away, +// slower close by. +float moveToward(float paddleY, float targetY); + class Pong { public: static constexpr float kWidth = 240, kHeight = 122; @@ -23,9 +32,9 @@ class Pong { explicit Pong(uint32_t seed) : rng_(seed) { start(); } void start(); - // `seconds` later, with the player's paddle asked up (-1), down (1) or left alone (0). - // What happened, as Events. - uint8_t step(float seconds, int move); + // `seconds` later, with the player's paddle asked up (-1), down (1) or left alone (0), or + // anything between for a slower move. What happened, as Events. + uint8_t step(float seconds, float move); Phase phase() const { return phase_; } float ballX() const { return x_; } @@ -40,7 +49,7 @@ class Pong { int longestRally() const { return longest_; } // in this game private: - uint8_t tick(float dt, int move); + uint8_t tick(float dt, float move); void serve(bool toPlayer); bool meets(float paddleY) const; void bounceOff(float paddleY, bool toRight); diff --git a/test/test_games/test_games.cpp b/test/test_games/test_games.cpp index 701070b..d488fda 100644 --- a/test/test_games/test_games.cpp +++ b/test/test_games/test_games.cpp @@ -121,6 +121,25 @@ void test_pong_the_device_can_be_beaten() { TEST_ASSERT_TRUE(playerPoints > 0); } +void test_pong_a_tilt_puts_the_paddle_where_it_points() { + TEST_ASSERT_FLOAT_WITHIN(0.01, Pong::kHeight / 2, tiltTarget(0.3f, 0.3f)); // held as at the start: the middle + TEST_ASSERT_TRUE(tiltTarget(0.2f, 0.3f) < Pong::kHeight / 2); // top lowered: up + TEST_ASSERT_TRUE(tiltTarget(0.4f, 0.3f) > Pong::kHeight / 2); + TEST_ASSERT_FLOAT_WITHIN(0.01, Pong::kPaddleH / 2, tiltTarget(-0.9f, 0.3f)); // and no further than the edge + TEST_ASSERT_FLOAT_WITHIN(0.01, Pong::kHeight - Pong::kPaddleH / 2, tiltTarget(0.9f, 0.3f)); + TEST_ASSERT_EQUAL_FLOAT(1, moveToward(20, 80)); + TEST_ASSERT_EQUAL_FLOAT(-1, moveToward(80, 20)); + TEST_ASSERT_FLOAT_WITHIN(0.01, 0.5f, moveToward(60, 65)); + TEST_ASSERT_EQUAL_FLOAT(0, moveToward(60, 60)); +} + +void test_pong_a_tilted_paddle_settles_on_its_target() { + Pong p(3); + float target = tiltTarget(0.4f, 0.3f); + for (int i = 0; i < 200; i++) p.step(0.01f, moveToward(p.playerY(), target)); + TEST_ASSERT_FLOAT_WITHIN(0.5, target, p.playerY()); +} + void test_pong_the_device_paddle_has_a_top_speed() { Pong p(13); float before = p.deviceY(); @@ -658,6 +677,8 @@ int main() { RUN_TEST(test_pong_a_miss_is_a_point_for_the_other_side); RUN_TEST(test_pong_ends_at_seven); RUN_TEST(test_pong_the_device_can_be_beaten); + RUN_TEST(test_pong_a_tilt_puts_the_paddle_where_it_points); + RUN_TEST(test_pong_a_tilted_paddle_settles_on_its_target); RUN_TEST(test_pong_the_device_paddle_has_a_top_speed); RUN_TEST(test_snake_starts_in_the_middle_three_long); RUN_TEST(test_snake_moves_at_its_pace);