Pong: a paddle steered by tilting

The player's move goes from -1 to 1 instead of three steps, and two
rules make it from the device's tilt: where the paddle is asked to be
(it rolls to the low side, about 15 degrees for the whole height) and
how to get there without overshooting. 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 af9077cb94
commit e1d1ec899e
3 changed files with 48 additions and 7 deletions
+14 -3
View File
@@ -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;
+13 -4
View File
@@ -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);
+21
View File
@@ -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);