#include #include #include #include "pong.h" #include "snake.h" #include "tetris.h" #include "tune.h" using namespace roro::games; void setUp() {} void tearDown() {} // ---- Pong ---- namespace { // Plays until the phase or an event says so; the player follows the ball if `follow`. uint8_t playPong(Pong& p, bool follow, uint8_t until, int maxSteps = 200000) { for (int i = 0; i < maxSteps; i++) { int move = !follow ? 0 : p.ballY() < p.playerY() - 2 ? -1 : p.ballY() > p.playerY() + 2 ? 1 : 0; uint8_t e = p.step(0.01f, move); if (e & until) return e; } return 0; } } // namespace void test_pong_starts_with_a_serve_from_the_middle() { Pong p(1); TEST_ASSERT_TRUE(p.phase() == Pong::Phase::Serve); TEST_ASSERT_FLOAT_WITHIN(0.01, Pong::kWidth / 2, p.ballX()); p.step(Pong::kServePause / 2, 0); TEST_ASSERT_FLOAT_WITHIN(0.01, Pong::kWidth / 2, p.ballX()); // it waits for (int i = 0; i < 100; i++) p.step(0.01f, 0); TEST_ASSERT_TRUE(p.phase() == Pong::Phase::Play); p.step(0.1f, 0); TEST_ASSERT_TRUE(std::fabs(p.ballX() - Pong::kWidth / 2) > 5); } void test_pong_the_same_seed_is_the_same_game() { Pong a(42), b(42), c(43); for (int i = 0; i < 500; i++) { a.step(0.01f, i % 3 - 1); b.step(0.01f, i % 3 - 1); c.step(0.01f, i % 3 - 1); } TEST_ASSERT_EQUAL_FLOAT(a.ballX(), b.ballX()); TEST_ASSERT_EQUAL_FLOAT(a.ballY(), b.ballY()); TEST_ASSERT_TRUE(a.ballY() != c.ballY() || a.ballX() != c.ballX()); } void test_pong_the_ball_stays_between_the_walls() { Pong p(7); bool wall = false; for (int i = 0; i < 20000; i++) { uint8_t e = p.step(0.01f, p.ballY() < p.playerY() ? -1 : 1); wall |= (e & Pong::Wall) != 0; TEST_ASSERT_TRUE(p.ballY() >= 0 && p.ballY() <= Pong::kHeight); if (p.phase() == Pong::Phase::Over) p.start(); } TEST_ASSERT_TRUE(wall); } void test_pong_a_paddle_stays_on_the_field() { Pong p(3); for (int i = 0; i < 300; i++) p.step(0.01f, -1); TEST_ASSERT_FLOAT_WITHIN(0.01, Pong::kPaddleH / 2, p.playerY()); for (int i = 0; i < 300; i++) p.step(0.01f, 1); TEST_ASSERT_FLOAT_WITHIN(0.01, Pong::kHeight - Pong::kPaddleH / 2, p.playerY()); } void test_pong_a_return_counts_and_speeds_the_ball_up() { Pong p(5); float served = Pong::kServeSpeed; // Follow the ball until the player returns it once. int before = p.rally(); for (int i = 0; i < 100000 && p.rally() == before; i++) { int move = p.ballY() < p.playerY() - 2 ? -1 : p.ballY() > p.playerY() + 2 ? 1 : 0; p.step(0.01f, move); } TEST_ASSERT_EQUAL(1, p.rally()); TEST_ASSERT_EQUAL(1, p.longestRally()); TEST_ASSERT_TRUE(p.speed() > served); TEST_ASSERT_TRUE(p.speed() <= Pong::kMaxSpeed); } void test_pong_a_miss_is_a_point_for_the_other_side() { Pong p(9); // The player never moves from the top corner: sooner or later the ball gets past. for (int i = 0; i < 300; i++) p.step(0.01f, -1); while (p.deviceScore() == 0 && p.phase() != Pong::Phase::Over) p.step(0.01f, -1); TEST_ASSERT_EQUAL(1, p.deviceScore()); TEST_ASSERT_TRUE(p.phase() == Pong::Phase::Serve); // served again, from the middle TEST_ASSERT_FLOAT_WITHIN(0.01, Pong::kWidth / 2, p.ballX()); TEST_ASSERT_EQUAL(0, p.rally()); } void test_pong_ends_at_seven() { Pong p(11); uint8_t e = playPong(p, false, Pong::Over); // a player who does nothing loses TEST_ASSERT_TRUE(e & Pong::Over); TEST_ASSERT_TRUE(p.phase() == Pong::Phase::Over); TEST_ASSERT_EQUAL(Pong::kWinScore, std::max(p.playerScore(), p.deviceScore())); TEST_ASSERT_EQUAL(0, p.step(1.0f, 1)); // nothing moves once it is over p.start(); TEST_ASSERT_EQUAL(0, p.deviceScore()); TEST_ASSERT_TRUE(p.phase() == Pong::Phase::Serve); } void test_pong_the_device_can_be_beaten() { // A player who always meets the ball: the device, slower than a steep fast ball, must lose // points over a few games. int playerPoints = 0; for (uint32_t seed = 1; seed <= 5; seed++) { Pong p(seed); playPong(p, true, Pong::Over, 2000000); playerPoints += p.playerScore(); } TEST_ASSERT_TRUE(playerPoints > 0); } void test_pong_the_device_paddle_has_a_top_speed() { Pong p(13); float before = p.deviceY(); for (int i = 0; i < 1000; i++) { p.step(0.01f, 0); TEST_ASSERT_TRUE(std::fabs(p.deviceY() - before) <= Pong::kDeviceSpeed * 0.01f + 0.01f); before = p.deviceY(); if (p.phase() == Pong::Phase::Over) p.start(); } } // ---- Snake ---- namespace { void moves(Snake& s, int n) { for (int i = 0; i < n && s.alive(); i++) s.step(s.intervalMs()); } } // namespace void test_snake_starts_in_the_middle_three_long() { Snake s(1); TEST_ASSERT_TRUE(s.alive()); TEST_ASSERT_EQUAL(3, s.body().size()); TEST_ASSERT_EQUAL(Snake::kCols / 2, s.body().front().col); TEST_ASSERT_EQUAL(Snake::kRows / 2, s.body().front().row); TEST_ASSERT_TRUE(s.heading() == Snake::Dir::Right); for (auto& c : s.body()) TEST_ASSERT_FALSE(c == s.food()); } void test_snake_moves_at_its_pace() { Snake s(1); s.putFood({0, 0}); TEST_ASSERT_EQUAL(0, s.step(Snake::kSlowestMs - 1)); TEST_ASSERT_EQUAL(Snake::Moved, s.step(1)); TEST_ASSERT_EQUAL(Snake::kCols / 2 + 1, s.body().front().col); TEST_ASSERT_EQUAL(3, s.body().size()); } void test_snake_turns_but_not_back_on_itself() { Snake s(1); s.putFood({0, 0}); s.turn(Snake::Dir::Left); // straight back: not a turn moves(s, 1); TEST_ASSERT_TRUE(s.heading() == Snake::Dir::Right); s.turn(Snake::Dir::Up); moves(s, 1); TEST_ASSERT_TRUE(s.heading() == Snake::Dir::Up); TEST_ASSERT_EQUAL(Snake::kRows / 2 - 1, s.body().front().row); } void test_snake_remembers_a_quick_corner() { Snake s(1); s.putFood({0, 0}); s.turn(Snake::Dir::Up); // both before the next move s.turn(Snake::Dir::Left); // would be "back" for a snake heading right, but not after the first s.turn(Snake::Dir::Down); // a third is not remembered moves(s, 1); TEST_ASSERT_TRUE(s.heading() == Snake::Dir::Up); moves(s, 1); TEST_ASSERT_TRUE(s.heading() == Snake::Dir::Left); moves(s, 1); TEST_ASSERT_TRUE(s.heading() == Snake::Dir::Left); TEST_ASSERT_TRUE(s.alive()); } void test_snake_eats_grows_and_gets_new_food() { Snake s(1); Snake::Cell head = s.body().front(); s.putFood({static_cast(head.col + 1), head.row}); uint8_t e = s.step(s.intervalMs()); TEST_ASSERT_TRUE(e & Snake::Ate); TEST_ASSERT_EQUAL(1, s.score()); TEST_ASSERT_EQUAL(4, s.body().size()); for (auto& c : s.body()) TEST_ASSERT_FALSE(c == s.food()); TEST_ASSERT_TRUE(s.food().col >= 0 && s.food().col < Snake::kCols && s.food().row >= 0 && s.food().row < Snake::kRows); } void test_snake_dies_at_a_wall() { Snake s(1); s.putFood({0, 0}); uint8_t last = 0; for (int i = 0; i < Snake::kCols && s.alive(); i++) last = s.step(s.intervalMs()); TEST_ASSERT_FALSE(s.alive()); TEST_ASSERT_TRUE(last & Snake::Died); TEST_ASSERT_EQUAL(Snake::kCols - 1, s.body().front().col); // it stopped at the edge TEST_ASSERT_EQUAL(3, s.body().size()); // whole TEST_ASSERT_EQUAL(0, s.step(1000)); } void test_snake_dies_on_itself() { Snake s(1); // Fed four times in a row, it is long enough to bite itself in a tight turn. for (int i = 0; i < 4; i++) { Snake::Cell head = s.body().front(); s.putFood({static_cast(head.col + 1), head.row}); s.step(s.intervalMs()); } s.putFood({0, 0}); TEST_ASSERT_EQUAL(7, s.body().size()); s.turn(Snake::Dir::Down); moves(s, 1); s.turn(Snake::Dir::Left); moves(s, 1); s.turn(Snake::Dir::Up); moves(s, 1); TEST_ASSERT_FALSE(s.alive()); } void test_snake_may_follow_its_own_tail() { Snake s(1); Snake::Cell head = s.body().front(); s.putFood({static_cast(head.col + 1), head.row}); s.step(s.intervalMs()); // four long: a square of 2 x 2 is its own tail chased s.putFood({0, 0}); for (auto d : {Snake::Dir::Down, Snake::Dir::Left, Snake::Dir::Up, Snake::Dir::Right, Snake::Dir::Down}) { s.turn(d); moves(s, 1); } TEST_ASSERT_TRUE(s.alive()); } void test_snake_gets_faster_as_it_eats() { Snake s(1); uint32_t start = s.intervalMs(); TEST_ASSERT_EQUAL(Snake::kSlowestMs, start); for (int i = 0; i < Snake::kFoodsAStep; i++) { Snake::Cell head = s.body().front(); s.putFood({static_cast(head.col + 1), head.row}); s.step(s.intervalMs()); } TEST_ASSERT_EQUAL(start - Snake::kFasterMs, s.intervalMs()); } void test_snake_the_same_seed_puts_the_food_in_the_same_places() { Snake a(77), b(77), c(78); TEST_ASSERT_TRUE(a.food() == b.food()); bool differs = !(a.food() == c.food()); for (int i = 0; i < 3; i++) { for (Snake* s : {&a, &b, &c}) { Snake::Cell head = s->body().front(); s->putFood({static_cast(head.col + 1), head.row}); s->step(s->intervalMs()); } TEST_ASSERT_TRUE(a.food() == b.food()); differs |= !(a.food() == c.food()); } TEST_ASSERT_TRUE(differs); } // ---- Tetris ---- namespace { const TetrisInput kNothing; TetrisInput press(bool TetrisInput::*key) { TetrisInput in; in.*key = true; return in; } int filled(const Tetris& t) { int n = 0; for (int r = 0; r < Tetris::kRows; r++) for (int c = 0; c < Tetris::kCols; c++) n += t.cell(c, r) != 0; return n; } // A row filled but for one column. void fillRow(Tetris& t, int row, int gap) { for (int c = 0; c < Tetris::kCols; c++) if (c != gap) t.put(c, row, 7); } } // namespace void test_tetris_deals_each_piece_once_in_seven() { Tetris t(5); for (int bag = 0; bag < 4; bag++) { std::set seen; for (int i = 0; i < 7; i++) { seen.insert(t.piece().kind); t.step(0, press(&TetrisInput::hardDrop)); if (t.over()) t.start(), i = -1, seen.clear(); // start over on a full well } TEST_ASSERT_EQUAL(7, seen.size()); t.start(); } } void test_tetris_shows_the_piece_that_comes_next() { Tetris t(9); uint8_t next = t.next(); t.step(0, press(&TetrisInput::hardDrop)); TEST_ASSERT_EQUAL(next, t.piece().kind); } void test_tetris_a_piece_falls_at_the_levels_pace() { Tetris t(1); t.give(3); // a T int row = t.piece().row; TEST_ASSERT_EQUAL(0, t.step(Tetris::kSlowestMs - 1, kNothing)); TEST_ASSERT_EQUAL(row, t.piece().row); TEST_ASSERT_EQUAL(Tetris::Moved, t.step(1, kNothing)); TEST_ASSERT_EQUAL(row + 1, t.piece().row); } void test_tetris_a_soft_drop_is_faster_and_scores() { Tetris t(1); t.give(3); int row = t.piece().row; t.step(Tetris::kSoftMs * 3, press(&TetrisInput::soft)); TEST_ASSERT_EQUAL(row + 3, t.piece().row); TEST_ASSERT_EQUAL(3, t.score()); } void test_tetris_a_held_soft_drop_stops_with_its_piece() { Tetris t(1); t.give(2); TetrisInput soft = press(&TetrisInput::soft); uint8_t all = 0; for (int i = 0; i < 40 && !(all & Tetris::Locked); i++) all |= t.step(Tetris::kSoftMs, soft); TEST_ASSERT_TRUE(all & Tetris::Locked); // The key is still down: the next piece falls at the level's pace, not the fast one. int row = t.piece().row, score = t.score(); t.step(Tetris::kSoftMs * 4, soft); TEST_ASSERT_EQUAL(row, t.piece().row); TEST_ASSERT_EQUAL(score, t.score()); t.step(0, kNothing); // let go, and pressed again: it works t.step(Tetris::kSoftMs * 2, soft); TEST_ASSERT_TRUE(t.piece().row > row); } void test_tetris_moves_stop_at_the_walls() { Tetris t(1); t.give(2); // an O: two wide for (int i = 0; i < 20; i++) { t.step(0, press(&TetrisInput::left)); t.step(0, kNothing); // released: the next press is a press } int leftmost = Tetris::kCols; for (auto& c : Tetris::cells(t.piece())) leftmost = std::min(leftmost, c.col); TEST_ASSERT_EQUAL(0, leftmost); for (int i = 0; i < 20; i++) { t.step(0, press(&TetrisInput::right)); t.step(0, kNothing); } int rightmost = 0; for (auto& c : Tetris::cells(t.piece())) rightmost = std::max(rightmost, c.col); TEST_ASSERT_EQUAL(Tetris::kCols - 1, rightmost); } void test_tetris_a_held_key_repeats() { Tetris t(1); t.give(2); int col = t.piece().col; TetrisInput left = press(&TetrisInput::left); t.step(0, left); // the press: one step at once TEST_ASSERT_EQUAL(col - 1, t.piece().col); t.step(Tetris::kRepeatAfterMs - 1, left); TEST_ASSERT_EQUAL(col - 1, t.piece().col); // not yet t.step(1, left); TEST_ASSERT_EQUAL(col - 2, t.piece().col); t.step(Tetris::kRepeatEveryMs, left); TEST_ASSERT_EQUAL(col - 3, t.piece().col); TetrisInput both = left; both.right = true; // both held: neither t.step(Tetris::kRepeatEveryMs * 3, both); TEST_ASSERT_EQUAL(col - 3, t.piece().col); } void test_tetris_turns_and_turns_back() { Tetris t(1); t.give(3); auto before = Tetris::cells(t.piece()); TEST_ASSERT_EQUAL(Tetris::Turned, t.step(0, press(&TetrisInput::rotate))); TEST_ASSERT_EQUAL(1, t.piece().turn); t.step(0, press(&TetrisInput::rotateBack)); TEST_ASSERT_EQUAL(0, t.piece().turn); auto after = Tetris::cells(t.piece()); for (int i = 0; i < 4; i++) { TEST_ASSERT_EQUAL(before[i].col, after[i].col); TEST_ASSERT_EQUAL(before[i].row, after[i].row); } for (int i = 0; i < 4; i++) t.step(0, press(&TetrisInput::rotate)); TEST_ASSERT_EQUAL(0, t.piece().turn); // four quarter turns: as it was } void test_tetris_a_turn_against_a_wall_shifts_the_piece() { Tetris t(1); t.give(1); // the long one t.step(0, press(&TetrisInput::rotate)); // upright for (int i = 0; i < 10; i++) { t.step(0, press(&TetrisInput::right)); t.step(0, kNothing); } for (auto& c : Tetris::cells(t.piece())) TEST_ASSERT_EQUAL(Tetris::kCols - 1, c.col); // against the right wall TEST_ASSERT_EQUAL(Tetris::Turned, t.step(0, press(&TetrisInput::rotate))); // flat again: it needs four columns for (auto& c : Tetris::cells(t.piece())) TEST_ASSERT_TRUE(c.col >= Tetris::kCols - 4 && c.col < Tetris::kCols); } void test_tetris_a_turn_with_no_room_does_not_happen() { Tetris t(1); t.give(1); t.step(0, press(&TetrisInput::rotate)); // Walled in on both sides, a column wide. int col = Tetris::cells(t.piece())[0].col; for (int r = 0; r < Tetris::kRows; r++) for (int c = 0; c < Tetris::kCols; c++) if (c != col) t.put(c, r, 7); TEST_ASSERT_EQUAL(0, t.step(0, press(&TetrisInput::rotate))); TEST_ASSERT_EQUAL(1, t.piece().turn); } void test_tetris_a_hard_drop_lands_locks_and_scores() { Tetris t(1); t.give(2); // an O int from = t.piece().row; uint8_t e = t.step(0, press(&TetrisInput::hardDrop)); TEST_ASSERT_TRUE(e & Tetris::Locked); TEST_ASSERT_EQUAL(4, filled(t)); TEST_ASSERT_NOT_EQUAL(0, t.cell(4, Tetris::kRows - 1)); TEST_ASSERT_NOT_EQUAL(0, t.cell(5, Tetris::kRows - 2)); TEST_ASSERT_EQUAL(2 * (Tetris::kRows - 2 - from), t.score()); // two a row fallen TEST_ASSERT_EQUAL(0, t.lastCleared()); } void test_tetris_a_piece_locks_when_it_can_fall_no_more() { Tetris t(1); t.give(2); uint8_t all = 0; for (int i = 0; i < Tetris::kRows + 2 && !(all & Tetris::Locked); i++) all |= t.step(Tetris::kSlowestMs, kNothing); TEST_ASSERT_TRUE(all & Tetris::Locked); TEST_ASSERT_EQUAL(4, filled(t)); } void test_tetris_a_full_row_goes_and_the_rest_comes_down() { Tetris t(1); fillRow(t, Tetris::kRows - 1, 4); // the gap where the O will not fit: use the long one upright t.put(0, Tetris::kRows - 2, 5); // something above, to see it come down t.give(1); t.step(0, press(&TetrisInput::rotate)); // upright, in column 5 t.step(0, press(&TetrisInput::left)); // column 4 uint8_t e = t.step(0, press(&TetrisInput::hardDrop)); TEST_ASSERT_TRUE(e & Tetris::Lines); TEST_ASSERT_EQUAL(1, t.lastCleared()); TEST_ASSERT_EQUAL(1, t.lines()); TEST_ASSERT_EQUAL(5, t.cell(0, Tetris::kRows - 1)); // what was above TEST_ASSERT_EQUAL(1, t.cell(4, Tetris::kRows - 1)); // and the rest of the long piece TEST_ASSERT_EQUAL(0, t.cell(1, Tetris::kRows - 1)); TEST_ASSERT_EQUAL(1 + 3, filled(t)); } void test_tetris_four_rows_at_once_score_most() { Tetris t(1); for (int r = Tetris::kRows - 4; r < Tetris::kRows; r++) fillRow(t, r, 9); t.give(1); t.step(0, press(&TetrisInput::rotate)); for (int i = 0; i < 10; i++) { t.step(0, press(&TetrisInput::right)); t.step(0, kNothing); } int before = t.score(); uint8_t e = t.step(0, press(&TetrisInput::hardDrop)); TEST_ASSERT_TRUE(e & Tetris::Lines); TEST_ASSERT_EQUAL(4, t.lastCleared()); TEST_ASSERT_EQUAL(0, filled(t)); int dropped = (t.score() - before) - 800; TEST_ASSERT_TRUE(dropped > 0 && dropped <= 2 * Tetris::kRows); // 800 for the four, and the drop } void test_tetris_ten_lines_are_a_level_and_a_faster_fall() { Tetris t(1); TEST_ASSERT_EQUAL(0, t.level()); TEST_ASSERT_EQUAL(Tetris::kSlowestMs, t.fallMs()); uint8_t all = 0; for (int i = 0; i < 3; i++) { // 4 + 4 + 4 for (int r = Tetris::kRows - 4; r < Tetris::kRows; r++) fillRow(t, r, 9); t.give(1); t.step(0, press(&TetrisInput::rotate)); for (int k = 0; k < 10; k++) { t.step(0, press(&TetrisInput::right)); t.step(0, kNothing); } all |= t.step(0, press(&TetrisInput::hardDrop)); } TEST_ASSERT_EQUAL(12, t.lines()); TEST_ASSERT_EQUAL(1, t.level()); TEST_ASSERT_TRUE(all & Tetris::LevelUp); TEST_ASSERT_EQUAL(Tetris::kSlowestMs * 85 / 100, t.fallMs()); } void test_tetris_the_fall_never_gets_faster_than_its_limit() { Tetris t(1); for (int i = 0; i < 80; i++) { for (int r = Tetris::kRows - 4; r < Tetris::kRows; r++) fillRow(t, r, 9); t.give(1); t.step(0, press(&TetrisInput::rotate)); for (int k = 0; k < 10; k++) { t.step(0, press(&TetrisInput::right)); t.step(0, kNothing); } t.step(0, press(&TetrisInput::hardDrop)); } TEST_ASSERT_TRUE(t.level() >= 30); TEST_ASSERT_EQUAL(Tetris::kFastestMs, t.fallMs()); } void test_tetris_is_over_when_a_piece_has_no_room() { Tetris t(3); uint8_t last = 0; for (int i = 0; i < 40 && !t.over(); i++) last = t.step(0, press(&TetrisInput::hardDrop)); // all in the middle TEST_ASSERT_TRUE(t.over()); TEST_ASSERT_TRUE(last & Tetris::Over); TEST_ASSERT_EQUAL(0, t.step(1000, press(&TetrisInput::hardDrop))); t.start(); TEST_ASSERT_FALSE(t.over()); TEST_ASSERT_EQUAL(0, filled(t)); TEST_ASSERT_EQUAL(0, t.score()); } void test_tetris_the_same_seed_deals_the_same_pieces() { Tetris a(21), b(21), c(22); bool differs = false; for (int i = 0; i < 14; i++) { TEST_ASSERT_EQUAL(a.piece().kind, b.piece().kind); differs |= a.piece().kind != c.piece().kind; for (Tetris* t : {&a, &b, &c}) { t->step(0, press(&TetrisInput::hardDrop)); if (t->over()) t->start(); } } TEST_ASSERT_TRUE(differs); } void test_tetris_every_piece_in_every_turn_has_four_cells_in_its_box() { for (uint8_t kind = 1; kind <= 7; kind++) for (uint8_t turn = 0; turn < 4; turn++) { Tetris::Piece p{kind, turn, 0, 0}; std::set where; for (auto& c : Tetris::cells(p)) { TEST_ASSERT_TRUE(c.col >= 0 && c.col < 4 && c.row >= 0 && c.row < 4); where.insert(c.row * 4 + c.col); } TEST_ASSERT_EQUAL(4, where.size()); } } // ---- The tune ---- void test_a_tune_gives_its_notes_in_time() { static const Note kThree[] = {{440, 2}, {0, 1}, {880, 1}}; TunePlayer p([](size_t i) { return kThree[i]; }, 3, 100); Note n{}; uint32_t ms = 0; TEST_ASSERT_TRUE(p.step(0, n, ms)); // the first note at once TEST_ASSERT_EQUAL(440, n.hz); TEST_ASSERT_EQUAL(200, ms); TEST_ASSERT_FALSE(p.step(199, n, ms)); TEST_ASSERT_TRUE(p.step(1, n, ms)); // a rest is a note too: the caller plays nothing TEST_ASSERT_EQUAL(0, n.hz); TEST_ASSERT_EQUAL(100, ms); TEST_ASSERT_TRUE(p.step(100, n, ms)); TEST_ASSERT_EQUAL(880, n.hz); TEST_ASSERT_TRUE(p.step(100, n, ms)); // and round again TEST_ASSERT_EQUAL(440, n.hz); TEST_ASSERT_EQUAL(0, p.position()); } void test_a_tune_keeps_time_over_uneven_steps() { static const Note kTwo[] = {{440, 1}, {880, 1}}; TunePlayer p([](size_t i) { return kTwo[i]; }, 2, 100); Note n{}; uint32_t ms = 0; p.step(0, n, ms); int notes = 1; for (int i = 0; i < 300; i++) notes += p.step(33, n, ms); // 9.9 seconds in steps of 33 ms TEST_ASSERT_EQUAL(100, notes); // one every 100 ms: the 33s do not add up to a drift } void test_a_tune_skips_what_a_long_gap_missed() { static const Note kFour[] = {{100, 1}, {200, 1}, {300, 1}, {400, 1}}; TunePlayer p([](size_t i) { return kFour[i]; }, 4, 100); Note n{}; uint32_t ms = 0; p.step(0, n, ms); TEST_ASSERT_TRUE(p.step(250, n, ms)); // 200 and half of 300 went by TEST_ASSERT_EQUAL(300, n.hz); TEST_ASSERT_FALSE(p.step(49, n, ms)); TEST_ASSERT_TRUE(p.step(1, n, ms)); TEST_ASSERT_EQUAL(400, n.hz); p.restart(); TEST_ASSERT_TRUE(p.step(0, n, ms)); TEST_ASSERT_EQUAL(100, n.hz); } void test_korobeiniki_is_whole_bars() { // Each part is eight bars of eight eighths, and it begins on its famous E. int first = 0, second = 0; for (size_t i = 0; i < korobeiniki::kFirstCount; i++) first += korobeiniki::kFirst[i].eighths; for (size_t i = 0; i < korobeiniki::kSecondCount; i++) second += korobeiniki::kSecond[i].eighths; TEST_ASSERT_EQUAL(64, first); TEST_ASSERT_EQUAL(64, second); TEST_ASSERT_EQUAL(659, korobeiniki::at(0).hz); TEST_ASSERT_EQUAL(659, korobeiniki::at(korobeiniki::kFirstCount).hz); // the tune again TEST_ASSERT_EQUAL(659, korobeiniki::at(2 * korobeiniki::kFirstCount).hz); // then the slow part TEST_ASSERT_EQUAL(4, korobeiniki::at(2 * korobeiniki::kFirstCount).eighths); TEST_ASSERT_EQUAL(0, korobeiniki::at(korobeiniki::kCount - 1).hz); // and a breath before it starts over } void test_the_tune_hurries_with_the_level() { TEST_ASSERT_EQUAL(200, korobeiniki::eighthMsAt(0)); TEST_ASSERT_EQUAL(192, korobeiniki::eighthMsAt(1)); TEST_ASSERT_EQUAL(160, korobeiniki::eighthMsAt(5)); TEST_ASSERT_EQUAL(korobeiniki::kFastestEighthMs, korobeiniki::eighthMsAt(12)); TEST_ASSERT_EQUAL(korobeiniki::kFastestEighthMs, korobeiniki::eighthMsAt(99)); for (int level = 0; level < 20; level++) TEST_ASSERT_TRUE(korobeiniki::eighthMsAt(level + 1) <= korobeiniki::eighthMsAt(level)); } void test_a_new_pace_starts_at_the_next_note() { static const Note kTwo[] = {{440, 1}, {880, 1}}; TunePlayer p([](size_t i) { return kTwo[i]; }, 2, 100); Note n{}; uint32_t ms = 0; p.step(0, n, ms); p.setEighthMs(50); TEST_ASSERT_FALSE(p.step(99, n, ms)); // the note that had begun keeps its length TEST_ASSERT_TRUE(p.step(1, n, ms)); TEST_ASSERT_EQUAL(50, ms); TEST_ASSERT_TRUE(p.step(50, n, ms)); } int main() { UNITY_BEGIN(); RUN_TEST(test_pong_starts_with_a_serve_from_the_middle); RUN_TEST(test_pong_the_same_seed_is_the_same_game); RUN_TEST(test_pong_the_ball_stays_between_the_walls); RUN_TEST(test_pong_a_paddle_stays_on_the_field); RUN_TEST(test_pong_a_return_counts_and_speeds_the_ball_up); 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_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); RUN_TEST(test_snake_turns_but_not_back_on_itself); RUN_TEST(test_snake_remembers_a_quick_corner); RUN_TEST(test_snake_eats_grows_and_gets_new_food); RUN_TEST(test_snake_dies_at_a_wall); RUN_TEST(test_snake_dies_on_itself); RUN_TEST(test_snake_may_follow_its_own_tail); RUN_TEST(test_snake_gets_faster_as_it_eats); RUN_TEST(test_snake_the_same_seed_puts_the_food_in_the_same_places); RUN_TEST(test_tetris_deals_each_piece_once_in_seven); RUN_TEST(test_tetris_shows_the_piece_that_comes_next); RUN_TEST(test_tetris_a_piece_falls_at_the_levels_pace); RUN_TEST(test_tetris_a_soft_drop_is_faster_and_scores); RUN_TEST(test_tetris_a_held_soft_drop_stops_with_its_piece); RUN_TEST(test_tetris_moves_stop_at_the_walls); RUN_TEST(test_tetris_a_held_key_repeats); RUN_TEST(test_tetris_turns_and_turns_back); RUN_TEST(test_tetris_a_turn_against_a_wall_shifts_the_piece); RUN_TEST(test_tetris_a_turn_with_no_room_does_not_happen); RUN_TEST(test_tetris_a_hard_drop_lands_locks_and_scores); RUN_TEST(test_tetris_a_piece_locks_when_it_can_fall_no_more); RUN_TEST(test_tetris_a_full_row_goes_and_the_rest_comes_down); RUN_TEST(test_tetris_four_rows_at_once_score_most); RUN_TEST(test_tetris_ten_lines_are_a_level_and_a_faster_fall); RUN_TEST(test_tetris_the_fall_never_gets_faster_than_its_limit); RUN_TEST(test_tetris_is_over_when_a_piece_has_no_room); RUN_TEST(test_tetris_the_same_seed_deals_the_same_pieces); RUN_TEST(test_tetris_every_piece_in_every_turn_has_four_cells_in_its_box); RUN_TEST(test_a_tune_gives_its_notes_in_time); RUN_TEST(test_a_tune_keeps_time_over_uneven_steps); RUN_TEST(test_a_tune_skips_what_a_long_gap_missed); RUN_TEST(test_korobeiniki_is_whole_bars); RUN_TEST(test_the_tune_hurries_with_the_level); RUN_TEST(test_a_new_pace_starts_at_the_next_note); return UNITY_END(); }