diff --git a/src/game/meson.build b/src/game/meson.build index 0399f90a..9b5f0f6c 100644 --- a/src/game/meson.build +++ b/src/game/meson.build @@ -11,6 +11,10 @@ graphics_src_files += files( 'grid.hpp', 'rotation.cpp', 'rotation.hpp', + 'simulated_tetrion.cpp', + 'simulated_tetrion.hpp', + 'simulation.cpp', + 'simulation.hpp', 'tetrion.cpp', 'tetrion.hpp', 'tetromino.cpp', diff --git a/src/game/simulated_tetrion.cpp b/src/game/simulated_tetrion.cpp new file mode 100644 index 00000000..1fbc7e72 --- /dev/null +++ b/src/game/simulated_tetrion.cpp @@ -0,0 +1,539 @@ +#include +#include +#include + +#include "helper/constants.hpp" +#include "helper/graphic_utils.hpp" +#include "helper/music_utils.hpp" +#include "manager/music_manager.hpp" +#include "simulated_tetrion.hpp" + +#include +#include + + +SimulatedTetrion::SimulatedTetrion( + const u8 tetrion_index, + const Random::Seed random_seed, + const u32 starting_level, + ServiceProvider* const service_provider, + std::optional> recording_writer +) + : m_lock_delay_step_index{ lock_delay }, + m_level{ starting_level }, + m_random{ random_seed }, + m_tetrion_index{ tetrion_index }, + m_next_gravity_simulation_step_index{ get_gravity_delay_frames() }, + m_recording_writer{ std::move(recording_writer) }, + m_service_provider{ service_provider } { } + +SimulatedTetrion::~SimulatedTetrion() = default; + +SimulatedTetrion::SimulatedTetrion(const SimulatedTetrion& other) = default; + +SimulatedTetrion::SimulatedTetrion(SimulatedTetrion&& other) noexcept = default; + +void SimulatedTetrion::update_step(const SimulationStep simulation_step_index) { + switch (m_game_state) { + case GameState::Playing: { + if (simulation_step_index >= m_next_gravity_simulation_step_index) { + assert(simulation_step_index == m_next_gravity_simulation_step_index and "frame skipped?!"); + if (m_is_accelerated_down_movement and not m_down_key_pressed) { + assert(m_next_gravity_simulation_step_index >= get_gravity_delay_frames() and "overflow"); + m_next_gravity_simulation_step_index -= get_gravity_delay_frames(); + m_is_accelerated_down_movement = false; + } else { + if (move_tetromino_down( + m_is_accelerated_down_movement ? MovementType::Forced : MovementType::Gravity, + simulation_step_index + )) { + reset_lock_delay(simulation_step_index); + } + } + m_next_gravity_simulation_step_index += get_gravity_delay_frames(); + } + + refresh_ghost_tetromino(); + break; + } + case GameState::GameOver: + default: + break; + } +} + +bool SimulatedTetrion::handle_input_command( + const input::GameInputCommand command, + const SimulationStep simulation_step_index +) { + switch (command) { + case input::GameInputCommand::RotateLeft: + if (rotate_tetromino_left()) { + reset_lock_delay(simulation_step_index); + return true; + } + return false; + case input::GameInputCommand::RotateRight: + if (rotate_tetromino_right()) { + reset_lock_delay(simulation_step_index); + return true; + } + return false; + case input::GameInputCommand::MoveLeft: + if (move_tetromino_left()) { + reset_lock_delay(simulation_step_index); + return true; + } + return false; + case input::GameInputCommand::MoveRight: + if (move_tetromino_right()) { + reset_lock_delay(simulation_step_index); + return true; + } + return false; + case input::GameInputCommand::MoveDown: + //TODO(Totto): use input_type() != InputType:Touch +#if not defined(__ANDROID__) + m_down_key_pressed = true; + m_is_accelerated_down_movement = true; + m_next_gravity_simulation_step_index = simulation_step_index + get_gravity_delay_frames(); +#endif + if (move_tetromino_down(MovementType::Forced, simulation_step_index)) { + reset_lock_delay(simulation_step_index); + return true; + } + return false; + case input::GameInputCommand::Drop: + m_lock_delay_step_index = simulation_step_index; // lock instantly + return drop_tetromino(simulation_step_index); + case input::GameInputCommand::ReleaseMoveDown: { + m_down_key_pressed = false; + return false; + } + case input::GameInputCommand::Hold: + if (m_allowed_to_hold) { + hold_tetromino(simulation_step_index); + reset_lock_delay(simulation_step_index); + m_allowed_to_hold = false; + return true; + } + return false; + default: + assert(false and "unknown GameInput"); + return false; + } +} + +void SimulatedTetrion::spawn_next_tetromino(const SimulationStep simulation_step_index) { + spawn_next_tetromino(get_next_tetromino_type(), simulation_step_index); +} + +void SimulatedTetrion::spawn_next_tetromino( + const helper::TetrominoType type, + const SimulationStep simulation_step_index +) { + constexpr GridPoint spawn_position{ 3, 0 }; + m_active_tetromino = Tetromino{ spawn_position, type }; + refresh_previews(); + if (not is_active_tetromino_position_valid()) { + m_game_state = GameState::GameOver; + + auto current_pieces = m_active_tetromino.value().minos(); + + bool all_valid{ false }; + u8 move_up = 0; + while (not all_valid) { + all_valid = true; + for (auto& mino : current_pieces) { + if (mino.position().y != 0) { + mino.position() = mino.position() - GridPoint{ 0, 1 }; + if (not is_valid_mino_position(mino.position())) { + all_valid = false; + } + } + } + + ++move_up; + } + + for (const Mino& mino : m_active_tetromino->minos()) { + auto position = mino.position(); + if (mino.position().y >= move_up && move_up != 0) { + position -= GridPoint{ 0, move_up }; + m_mino_stack.set(position, mino.type()); + } + } + + spdlog::info("game over"); + if (m_recording_writer.has_value()) { + spdlog::info("writing snapshot"); + std::ignore = m_recording_writer.value()->add_snapshot(simulation_step_index, core_information()); + } + m_active_tetromino = {}; + m_ghost_tetromino = {}; + return; + } + + m_next_gravity_simulation_step_index = simulation_step_index + get_gravity_delay_frames(); + refresh_ghost_tetromino(); +} + +bool SimulatedTetrion::rotate_tetromino_right() { + return with_lock_delay([&]() { return rotate(RotationDirection::Right); }); +} + +bool SimulatedTetrion::rotate_tetromino_left() { + return with_lock_delay([&]() { return rotate(RotationDirection::Left); }); +} + +bool SimulatedTetrion::move_tetromino_down(MovementType movement_type, const SimulationStep simulation_step_index) { + if (not m_active_tetromino.has_value()) { + return false; + } + if (movement_type == MovementType::Forced) { + m_score += 4; + } + + + if (tetromino_can_move_down(m_active_tetromino.value())) { + m_active_tetromino->move_down(); + return true; + } + + m_is_in_lock_delay = true; + if ((m_is_in_lock_delay and m_num_executed_lock_delays >= num_lock_delays) + or simulation_step_index >= m_lock_delay_step_index) { + lock_active_tetromino(simulation_step_index); + reset_lock_delay(simulation_step_index); + } else { + m_next_gravity_simulation_step_index = simulation_step_index + 1; + } + return false; +} + +bool SimulatedTetrion::move_tetromino_left() { + return with_lock_delay([&]() { return move(MoveDirection::Left); }); +} + +bool SimulatedTetrion::move_tetromino_right() { + return with_lock_delay([&]() { return move(MoveDirection::Right); }); +} + +bool SimulatedTetrion::drop_tetromino(const SimulationStep simulation_step_index) { + if (not m_active_tetromino.has_value()) { + return false; + } + u64 num_movements = 0; + while (tetromino_can_move_down(m_active_tetromino.value())) { + ++num_movements; + m_active_tetromino->move_down(); + } + + m_score += static_cast(4) * num_movements; + lock_active_tetromino(simulation_step_index); + return num_movements > 0; +} + +void SimulatedTetrion::hold_tetromino(const SimulationStep simulation_step_index) { + if (not m_active_tetromino.has_value()) { + return; + } + + if (not m_tetromino_on_hold.has_value()) { + m_tetromino_on_hold = Tetromino{ grid::hold_tetromino_position, m_active_tetromino->type() }; + spawn_next_tetromino(simulation_step_index); + } else { + const auto on_hold = m_tetromino_on_hold->type(); + m_tetromino_on_hold = Tetromino{ grid::hold_tetromino_position, m_active_tetromino->type() }; + spawn_next_tetromino(on_hold, simulation_step_index); + } +} + +[[nodiscard]] u8 SimulatedTetrion::tetrion_index() const { + return m_tetrion_index; +} + +[[nodiscard]] u32 SimulatedTetrion::level() const { + return m_level; +} + +[[nodiscard]] u64 SimulatedTetrion::score() const { + return m_score; +} + +[[nodiscard]] u32 SimulatedTetrion::lines_cleared() const { + return m_lines_cleared; +} + +[[nodiscard]] const MinoStack& SimulatedTetrion::mino_stack() const { + return m_mino_stack; +} + +[[nodiscard]] std::unique_ptr SimulatedTetrion::core_information() const { + + return std::make_unique(m_tetrion_index, m_level, m_score, m_lines_cleared, m_mino_stack); +} + +[[nodiscard]] bool SimulatedTetrion::is_game_over() const { + return m_game_state == GameState::GameOver; +} + +void SimulatedTetrion::reset_lock_delay(const SimulationStep simulation_step_index) { + m_lock_delay_step_index = simulation_step_index + lock_delay; +} + +void SimulatedTetrion::refresh_texts() { } + +void SimulatedTetrion::clear_fully_occupied_lines() { + bool cleared = false; + const u32 lines_cleared_before = m_lines_cleared; + do { // NOLINT(cppcoreguidelines-avoid-do-while) + cleared = false; + for (u8 row = 0; row < grid::height_in_tiles; ++row) { + bool fully_occupied = true; + for (u8 column = 0; column < grid::width_in_tiles; ++column) { + if (m_mino_stack.is_empty(GridPoint{ column, row })) { + fully_occupied = false; + break; + } + } + + if (fully_occupied) { + ++m_lines_cleared; + const auto level = m_lines_cleared / 10; + if (level > m_level) { + m_level = level; + spdlog::info("new level: {}", m_level); + if (level == constants::music_change_level) { + if (m_service_provider != nullptr) { + m_service_provider->music_manager() + .load_and_play_music( + utils::get_assets_folder() / "music" + / utils::get_supported_music_extension("03. Game Theme (50 Left)") + ) + .and_then(utils::log_error); + } + } + } + m_mino_stack.clear_row_and_let_sink(static_cast(row)); + cleared = true; + break; + } + } + } while (cleared); + const u32 num_lines_cleared = m_lines_cleared - lines_cleared_before; + static constexpr std::array score_per_line_multiplier{ 0, 40, 100, 300, 1200 }; + m_score += static_cast(score_per_line_multiplier.at(num_lines_cleared)) * static_cast(m_level + 1); +} + +void SimulatedTetrion::lock_active_tetromino(const SimulationStep simulation_step_index) { + assert(m_active_tetromino.has_value()); + for (const Mino& mino : m_active_tetromino->minos()) { // NOLINT(bugprone-unchecked-optional-access) + m_mino_stack.set(mino.position(), mino.type()); + } + m_allowed_to_hold = true; + m_is_in_lock_delay = false; + m_num_executed_lock_delays = 0; + clear_fully_occupied_lines(); + spawn_next_tetromino(simulation_step_index); + refresh_texts(); + reset_lock_delay(simulation_step_index); + + // save a snapshot on every freeze (only in debug builds) +#if !defined(NDEBUG) + if (m_recording_writer) { + spdlog::debug("adding snapshot at step {}", simulation_step_index); + std::ignore = (*m_recording_writer)->add_snapshot(simulation_step_index, core_information()); + } +#endif +} + +bool SimulatedTetrion::is_active_tetromino_position_valid() const { + if (not m_active_tetromino) { + return false; + } + return is_tetromino_position_valid(m_active_tetromino.value()); +} + +bool SimulatedTetrion::is_valid_mino_position(GridPoint position) const { + return position.x < grid::width_in_tiles and position.y < grid::height_in_tiles and m_mino_stack.is_empty(position); +} + +bool SimulatedTetrion::mino_can_move_down(GridPoint position) const { + if (position.y == (grid::height_in_tiles - 1)) { + return false; + } + + return is_valid_mino_position(position + GridPoint{ 0, 1 }); +} + + +void SimulatedTetrion::refresh_ghost_tetromino() { + if (not m_active_tetromino.has_value()) { + m_ghost_tetromino = {}; + return; + } + m_ghost_tetromino = m_active_tetromino.value(); + while (tetromino_can_move_down(m_ghost_tetromino.value())) { + m_ghost_tetromino->move_down(); + } +} + +void SimulatedTetrion::refresh_previews() { + auto sequence_index = m_sequence_index; + auto bag_index = usize{ 0 }; + for (std::remove_cvref_t i = 0; i < num_preview_tetrominos; ++i) { + m_preview_tetrominos.at(static_cast(i)) = Tetromino{ + grid::preview_tetromino_position + shapes::UPoint{ 0, static_cast(grid::preview_padding * i) }, + m_sequence_bags.at(bag_index)[sequence_index] + }; + ++sequence_index; + static constexpr auto bag_size = decltype(m_sequence_bags)::value_type::size(); + if (sequence_index >= bag_size) { + assert(sequence_index == bag_size); + sequence_index = 0; + ++bag_index; + assert(bag_index < m_sequence_bags.size()); + } + } +} + +helper::TetrominoType SimulatedTetrion::get_next_tetromino_type() { + const helper::TetrominoType next_type = m_sequence_bags[0][m_sequence_index]; + m_sequence_index = (m_sequence_index + 1) % Bag::size(); + if (m_sequence_index == 0) { + // we had a wrap-around + m_sequence_bags[0] = m_sequence_bags[1]; + m_sequence_bags[1] = Bag{ m_random }; + } + return next_type; +} + +bool SimulatedTetrion::tetromino_can_move_down(const Tetromino& tetromino) const { + return not std::ranges::any_of(tetromino.minos(), [this](const Mino& mino) { + return not mino_can_move_down(mino.position()); + }); +} + + +[[nodiscard]] u64 SimulatedTetrion::get_gravity_delay_frames() const { + const auto frames = (m_level >= frames_per_tile.size() ? frames_per_tile.back() : frames_per_tile.at(m_level)); + if (m_is_accelerated_down_movement) { + return std::max(u64{ 1 }, static_cast(std::round(static_cast(frames) / 20.0))); + } + return frames; +} + +u8 SimulatedTetrion::rotation_to_index(const Rotation from, const Rotation rotation_to) { + if (from == Rotation::North and rotation_to == Rotation::East) { + return 0; + } + if (from == Rotation::East and rotation_to == Rotation::North) { + return 1; + } + if (from == Rotation::East and rotation_to == Rotation::South) { + return 2; + } + if (from == Rotation::South and rotation_to == Rotation::East) { + return 3; + } + if (from == Rotation::South and rotation_to == Rotation::West) { + return 4; + } + if (from == Rotation::West and rotation_to == Rotation::South) { + return 5; + } + if (from == Rotation::West and rotation_to == Rotation::North) { + return 6; + } + if (from == Rotation::North and rotation_to == Rotation::West) { + return 7; + } + UNREACHABLE(); +} + +bool SimulatedTetrion::is_tetromino_position_valid(const Tetromino& tetromino) const { + return not std::ranges::any_of(tetromino.minos(), [this](const Mino& mino) { + return not is_valid_mino_position(mino.position()); + }); +} + +bool SimulatedTetrion::rotate(SimulatedTetrion::RotationDirection rotation_direction) { + if (not m_active_tetromino) { + return false; + } + + const auto wall_kick_table = get_wall_kick_table(); + if (not wall_kick_table.has_value()) { + return false; + } + + const auto from_rotation = m_active_tetromino->rotation(); + const auto to_rotation = from_rotation + static_cast(rotation_direction == RotationDirection::Left ? -1 : 1); + const auto table_index = rotation_to_index(from_rotation, to_rotation); + + if (rotation_direction == RotationDirection::Left) { + m_active_tetromino->rotate_left(); + } else { + m_active_tetromino->rotate_right(); + } + + for (const auto& translation : (*wall_kick_table)->at(table_index)) { + m_active_tetromino->move(translation); + if (is_active_tetromino_position_valid()) { + return true; + } + m_active_tetromino->move(-translation); + } + + if (rotation_direction == RotationDirection::Left) { + m_active_tetromino->rotate_right(); + } else { + m_active_tetromino->rotate_left(); + } + return false; +} + +bool SimulatedTetrion::move(const SimulatedTetrion::MoveDirection move_direction) { + if (not m_active_tetromino) { + return false; + } + + switch (move_direction) { + case MoveDirection::Left: + m_active_tetromino->move_left(); + if (not is_active_tetromino_position_valid()) { + m_active_tetromino->move_right(); + return false; + } + return true; + case MoveDirection::Right: + m_active_tetromino->move_right(); + if (not is_active_tetromino_position_valid()) { + m_active_tetromino->move_left(); + return false; + } + return true; + } + + UNREACHABLE(); +} + +std::optional SimulatedTetrion::get_wall_kick_table() const { + assert(m_active_tetromino.has_value() and "no active tetromino"); + const auto type = m_active_tetromino->type(); // NOLINT(bugprone-unchecked-optional-access) + switch (type) { + case helper::TetrominoType::J: + case helper::TetrominoType::L: + case helper::TetrominoType::T: + case helper::TetrominoType::S: + case helper::TetrominoType::Z: + return &wall_kick_data_jltsz; + case helper::TetrominoType::I: + return &wall_kick_data_i; + case helper::TetrominoType::O: + return {}; + default: + UNREACHABLE(); + } +} diff --git a/src/game/simulated_tetrion.hpp b/src/game/simulated_tetrion.hpp new file mode 100644 index 00000000..5cd6ef98 --- /dev/null +++ b/src/game/simulated_tetrion.hpp @@ -0,0 +1,302 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "bag.hpp" +#include "grid.hpp" +#include "input/game_input.hpp" +#include "manager/service_provider.hpp" +#include "tetromino.hpp" +#include "ui/layouts/grid_layout.hpp" +#include "ui/widget.hpp" + +#include + + +enum class GameState : u8 { + Playing, + GameOver, +}; + +enum class MovementType : u8 { + Gravity, + Forced, +}; + +struct SimulatedTetrion { +private: + using WallKickPoint = shapes::AbstractPoint; + using WallKickTable = std::array, 8>; + using GridPoint = Mino::GridPoint; + + static constexpr SimulationStep lock_delay = 30; + static constexpr int num_lock_delays = 30; + + enum class RotationDirection : u8 { + Left, + Right, + }; + + enum class MoveDirection : u8 { + Left, + Right, + }; + + + static constexpr u8 num_preview_tetrominos = 6; + + bool m_is_accelerated_down_movement = false; + bool m_down_key_pressed = false; + bool m_allowed_to_hold = true; + bool m_is_in_lock_delay = false; + + u32 m_num_executed_lock_delays = 0; + u64 m_lock_delay_step_index; + +protected: + MinoStack + m_mino_stack; // NOLINT(misc-non-private-member-variables-in-classes,cppcoreguidelines-non-private-member-variables-in-classes) + u32 m_level; // NOLINT(misc-non-private-member-variables-in-classes,cppcoreguidelines-non-private-member-variables-in-classes) + u32 m_lines_cleared = // NOLINT(misc-non-private-member-variables-in-classes,cppcoreguidelines-non-private-member-variables-in-classes) + 0; + u64 m_score = // NOLINT(misc-non-private-member-variables-in-classes,cppcoreguidelines-non-private-member-variables-in-classes) + 0; + + + std::optional + m_active_tetromino; // NOLINT(misc-non-private-member-variables-in-classes,cppcoreguidelines-non-private-member-variables-in-classes) + std::optional + m_ghost_tetromino; // NOLINT(misc-non-private-member-variables-in-classes,cppcoreguidelines-non-private-member-variables-in-classes) + std::optional + m_tetromino_on_hold; // NOLINT(misc-non-private-member-variables-in-classes,cppcoreguidelines-non-private-member-variables-in-classes) + std::array, num_preview_tetrominos> + m_preview_tetrominos{}; // NOLINT(misc-non-private-member-variables-in-classes,cppcoreguidelines-non-private-member-variables-in-classes) + +private: + Random m_random; + GameState m_game_state = GameState::Playing; + int m_sequence_index = 0; + std::array m_sequence_bags{ Bag{ m_random }, Bag{ m_random } }; + u8 m_tetrion_index; + u64 m_next_gravity_simulation_step_index; + std::optional> m_recording_writer; + +protected: + ServiceProvider* + m_service_provider; // NOLINT(misc-non-private-member-variables-in-classes,cppcoreguidelines-non-private-member-variables-in-classes) + +public: + SimulatedTetrion( + u8 tetrion_index, + Random::Seed random_seed, + u32 starting_level, + ServiceProvider* service_provider, + std::optional> recording_writer + ); + + virtual ~SimulatedTetrion(); + + SimulatedTetrion(const SimulatedTetrion& other); + SimulatedTetrion& operator=(const SimulatedTetrion& other) = delete; + + SimulatedTetrion(SimulatedTetrion&& other) noexcept; + SimulatedTetrion& operator=(SimulatedTetrion&& other) noexcept = delete; + + void update_step(SimulationStep simulation_step_index); + + // returns if the input event lead to a movement + bool handle_input_command(input::GameInputCommand command, SimulationStep simulation_step_index); + void spawn_next_tetromino(SimulationStep simulation_step_index); + void spawn_next_tetromino(helper::TetrominoType type, SimulationStep simulation_step_index); + bool rotate_tetromino_right(); + bool rotate_tetromino_left(); + bool move_tetromino_down(MovementType movement_type, SimulationStep simulation_step_index); + bool move_tetromino_left(); + bool move_tetromino_right(); + bool drop_tetromino(SimulationStep simulation_step_index); + void hold_tetromino(SimulationStep simulation_step_index); + + [[nodiscard]] u8 tetrion_index() const; + [[nodiscard]] u32 level() const; + [[nodiscard]] u64 score() const; + [[nodiscard]] u32 lines_cleared() const; + [[nodiscard]] const MinoStack& mino_stack() const; + [[nodiscard]] std::unique_ptr core_information() const; + + [[nodiscard]] bool is_game_over() const; + +private: + template + bool with_lock_delay(Callable movement) { + const auto result = movement(); + if (result and m_is_in_lock_delay) { + ++m_num_executed_lock_delays; + } + return result; + } + + bool rotate(RotationDirection rotation_direction); + bool move(MoveDirection move_direction); + [[nodiscard]] std::optional get_wall_kick_table() const; + void reset_lock_delay(SimulationStep simulation_step_index); + virtual void refresh_texts(); + void clear_fully_occupied_lines(); + void lock_active_tetromino(SimulationStep simulation_step_index); + [[nodiscard]] bool is_active_tetromino_position_valid() const; + [[nodiscard]] bool mino_can_move_down(GridPoint position) const; + [[nodiscard]] bool is_valid_mino_position(GridPoint position) const; + + void refresh_ghost_tetromino(); + void refresh_previews(); + helper::TetrominoType get_next_tetromino_type(); + + [[nodiscard]] bool is_tetromino_position_valid(const Tetromino& tetromino) const; + [[nodiscard]] bool tetromino_can_move_down(const Tetromino& tetromino) const; + + [[nodiscard]] u64 get_gravity_delay_frames() const; + + static u8 rotation_to_index(Rotation from, Rotation rotation_to); + + static constexpr auto wall_kick_data_jltsz = WallKickTable{ + // North -> East + std::array{ + WallKickPoint{ 0, 0 }, + WallKickPoint{ -1, 0 }, + WallKickPoint{ -1, -1 }, + WallKickPoint{ 0, 2 }, + WallKickPoint{ -1, 2 }, + }, + // East -> North + std::array{ + WallKickPoint{ 0, 0 }, + WallKickPoint{ 1, 0 }, + WallKickPoint{ 1, 1 }, + WallKickPoint{ 0, -2 }, + WallKickPoint{ 1, -2 }, + }, + // East -> South + std::array{ + WallKickPoint{ 0, 0 }, + WallKickPoint{ 1, 0 }, + WallKickPoint{ 1, 1 }, + WallKickPoint{ 0, -2 }, + WallKickPoint{ 1, -2 }, + }, + // South -> East + std::array{ + WallKickPoint{ 0, 0 }, + WallKickPoint{ -1, 0 }, + WallKickPoint{ -1, -1 }, + WallKickPoint{ 0, 2 }, + WallKickPoint{ -1, 2 }, + }, + // South -> West + std::array{ + WallKickPoint{ 0, 0 }, + WallKickPoint{ 1, 0 }, + WallKickPoint{ 1, -1 }, + WallKickPoint{ 0, 2 }, + WallKickPoint{ 1, 2 }, + }, + // West -> South + std::array{ + WallKickPoint{ 0, 0 }, + WallKickPoint{ -1, 0 }, + WallKickPoint{ -1, 1 }, + WallKickPoint{ 0, -2 }, + WallKickPoint{ -1, -2 }, + }, + // West -> North + std::array{ + WallKickPoint{ 0, 0 }, + WallKickPoint{ -1, 0 }, + WallKickPoint{ -1, 1 }, + WallKickPoint{ 0, -2 }, + WallKickPoint{ -1, -2 }, + }, + // North -> West + std::array{ + WallKickPoint{ 0, 0 }, + WallKickPoint{ 1, 0 }, + WallKickPoint{ 1, -1 }, + WallKickPoint{ 0, 2 }, + WallKickPoint{ 1, 2 }, + }, + }; + + static constexpr auto wall_kick_data_i = WallKickTable{ + // North -> East + std::array{ + WallKickPoint{ 0, 0 }, + WallKickPoint{ -2, 0 }, + WallKickPoint{ 1, 0 }, + WallKickPoint{ -2, 1 }, + WallKickPoint{ 1, -2 }, + }, + // East -> North + std::array{ + WallKickPoint{ 0, 0 }, + WallKickPoint{ 2, 0 }, + WallKickPoint{ -1, 0 }, + WallKickPoint{ 2, -1 }, + WallKickPoint{ -1, 2 }, + }, + // East -> South + std::array{ + WallKickPoint{ 0, 0 }, + WallKickPoint{ -1, 0 }, + WallKickPoint{ 2, 0 }, + WallKickPoint{ -1, -2 }, + WallKickPoint{ 2, 1 }, + }, + // South -> East + std::array{ + WallKickPoint{ 0, 0 }, + WallKickPoint{ 1, 0 }, + WallKickPoint{ -2, 0 }, + WallKickPoint{ 1, 2 }, + WallKickPoint{ -2, -1 }, + }, + // South -> West + std::array{ + WallKickPoint{ 0, 0 }, + WallKickPoint{ 2, 0 }, + WallKickPoint{ -1, 0 }, + WallKickPoint{ 2, -1 }, + WallKickPoint{ -1, 2 }, + }, + // West -> South + std::array{ + WallKickPoint{ 0, 0 }, + WallKickPoint{ -2, 0 }, + WallKickPoint{ 1, 0 }, + WallKickPoint{ -2, 1 }, + WallKickPoint{ 1, -2 }, + }, + // West -> North + std::array{ + WallKickPoint{ 0, 0 }, + WallKickPoint{ 1, 0 }, + WallKickPoint{ -2, 0 }, + WallKickPoint{ 1, 2 }, + WallKickPoint{ -2, -1 }, + }, + // North -> West + std::array{ + WallKickPoint{ 0, 0 }, + WallKickPoint{ -1, 0 }, + WallKickPoint{ 2, 0 }, + WallKickPoint{ -1, -2 }, + WallKickPoint{ 2, 1 }, + }, + }; + + static constexpr auto frames_per_tile = std::array{ 48, 43, 38, 33, 28, 23, 18, 13, 8, 6, 5, 5, 5, 4, 4, + 4, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1 }; + + friend struct TetrionSnapshot; +}; diff --git a/src/game/simulation.cpp b/src/game/simulation.cpp new file mode 100644 index 00000000..548424a2 --- /dev/null +++ b/src/game/simulation.cpp @@ -0,0 +1,103 @@ + +#include +#include + +#include "core/helper/expected.hpp" +#include "input/replay_input.hpp" +#include "simulation.hpp" + +#include + + +Simulation::Simulation( + const std::shared_ptr& input, + const tetrion::StartingParameters& starting_parameters +) + : m_input{ input } { + + + spdlog::info("[simulation] starting level for tetrion {}", starting_parameters.starting_level); + + m_tetrion = std::make_unique( + starting_parameters.tetrion_index, starting_parameters.seed, starting_parameters.starting_level, nullptr, + starting_parameters.recording_writer + ); + + m_tetrion->spawn_next_tetromino(0); + + m_input->set_target_tetrion(m_tetrion.get()); + if (starting_parameters.recording_writer.has_value()) { + const auto recording_writer = starting_parameters.recording_writer.value(); + const auto tetrion_index = starting_parameters.tetrion_index; + m_input->set_event_callback([recording_writer, + tetrion_index](InputEvent event, SimulationStep simulation_step_index) { + spdlog::debug("event: {} (step {})", magic_enum::enum_name(event), simulation_step_index); + + //TODO(Totto): Remove all occurrences of std::ignore, where we shouldn't ignore this return value + std::ignore = recording_writer->add_record(tetrion_index, simulation_step_index, event); + }); + } +} + +helper::expected Simulation::get_replay_simulation(std::filesystem::path& recording_path) { + + //TODO(Totto): Support multiple tetrions to be in the recorded file and simulated + + auto maybe_recording_reader = recorder::RecordingReader::from_path(recording_path); + + if (not maybe_recording_reader.has_value()) { + return helper::unexpected{ + fmt::format("an error occurred while reading recording: {}", maybe_recording_reader.error()) + }; + } + + const auto recording_reader = + std::make_shared(std::move(maybe_recording_reader.value())); + + + const auto tetrion_headers = recording_reader->tetrion_headers(); + + if (tetrion_headers.size() != 1) { + return helper::unexpected{ + fmt::format("Expected 1 recording in the recording file, but got : {}", tetrion_headers.size()) + }; + } + + const auto tetrion_index = 0; + + auto input = std::make_shared(recording_reader, nullptr); + + const auto& header = tetrion_headers.at(tetrion_index); + + const auto seed = header.seed; + const auto starting_level = header.starting_level; + + const tetrion::StartingParameters starting_parameters = { 0, seed, starting_level, tetrion_index, std::nullopt }; + + return Simulation{ input, starting_parameters }; +} + + +void Simulation::update() { + if (is_game_finished()) { + return; + } + + ++m_simulation_step_index; + m_input->update(m_simulation_step_index); + m_tetrion->update_step(m_simulation_step_index); + m_input->late_update(m_simulation_step_index); +} + +[[nodiscard]] bool Simulation::is_game_finished() const { + if (m_tetrion->is_game_over()) { + return true; + }; + + const auto input_as_replay = utils::is_child_class(m_input); + if (input_as_replay.has_value()) { + return input_as_replay.value()->is_end_of_recording(); + } + + return false; +} diff --git a/src/game/simulation.hpp b/src/game/simulation.hpp new file mode 100644 index 00000000..001c1e30 --- /dev/null +++ b/src/game/simulation.hpp @@ -0,0 +1,31 @@ +#pragma once + +#include + +#include "core/helper/expected.hpp" +#include "input/input_creator.hpp" +#include "input/replay_input.hpp" +#include "simulated_tetrion.hpp" + +struct Simulation { +private: + using TetrionHeaders = std::vector; + + SimulationStep m_simulation_step_index{ 0 }; + std::unique_ptr m_tetrion; + std::shared_ptr m_input; + +public: + explicit Simulation( + const std::shared_ptr& input, + const tetrion::StartingParameters& starting_parameters + ); + + + static helper::expected get_replay_simulation(std::filesystem::path& recording_path); + + void update(); + + + [[nodiscard]] bool is_game_finished() const; +}; diff --git a/src/game/tetrion.cpp b/src/game/tetrion.cpp index 8e2629b8..177a775c 100644 --- a/src/game/tetrion.cpp +++ b/src/game/tetrion.cpp @@ -1,17 +1,13 @@ #include #include -#include -#include "helper/constants.hpp" -#include "helper/graphic_utils.hpp" -#include "helper/music_utils.hpp" + +#include "game/simulated_tetrion.hpp" #include "helper/platform.hpp" -#include "manager/music_manager.hpp" #include "manager/resource_manager.hpp" #include "tetrion.hpp" #include "ui/components/label.hpp" -#include #include #include @@ -26,13 +22,7 @@ Tetrion::Tetrion( bool is_top_level ) : ui::Widget{ layout , ui::WidgetType::Component ,is_top_level}, - m_lock_delay_step_index{ lock_delay }, - m_service_provider{ service_provider }, - m_recording_writer{ std::move(recording_writer) }, - m_random{ random_seed }, - m_level{ starting_level }, - m_tetrion_index{ tetrion_index }, - m_next_gravity_simulation_step_index{ get_gravity_delay_frames() }, + SimulatedTetrion{tetrion_index,random_seed,starting_level, service_provider,std::move(recording_writer)}, m_main_layout{ utils::size_t_identity<2>(), 0, @@ -49,7 +39,6 @@ Tetrion::Tetrion( 1, 3, ui::Direction::Vertical, ui::AbsolutMargin{ 0 }, std::pair{ 0.0, 0.1 } ); - auto* text_layout = get_text_layout(); constexpr auto text_size = utils::get_orientation() == utils::Orientation::Landscape @@ -75,34 +64,7 @@ Tetrion::Tetrion( refresh_texts(); } -void Tetrion::update_step(const SimulationStep simulation_step_index) { - switch (m_game_state) { - case GameState::Playing: { - if (simulation_step_index >= m_next_gravity_simulation_step_index) { - assert(simulation_step_index == m_next_gravity_simulation_step_index and "frame skipped?!"); - if (m_is_accelerated_down_movement and not m_down_key_pressed) { - assert(m_next_gravity_simulation_step_index >= get_gravity_delay_frames() and "overflow"); - m_next_gravity_simulation_step_index -= get_gravity_delay_frames(); - m_is_accelerated_down_movement = false; - } else { - if (move_tetromino_down( - m_is_accelerated_down_movement ? MovementType::Forced : MovementType::Gravity, - simulation_step_index - )) { - reset_lock_delay(simulation_step_index); - } - } - m_next_gravity_simulation_step_index += get_gravity_delay_frames(); - } - - refresh_ghost_tetromino(); - break; - } - case GameState::GameOver: - default: - break; - } -} +Tetrion::~Tetrion() = default; void Tetrion::render(const ServiceProvider& service_provider) const { @@ -116,13 +78,13 @@ void Tetrion::render(const ServiceProvider& service_provider) const { const shapes::UPoint& tile_size = grid->tile_size(); helper::graphics::render_minos(m_mino_stack, service_provider, original_scale, to_screen_coords, tile_size); - if (m_active_tetromino) { + if (m_active_tetromino.has_value()) { m_active_tetromino->render( service_provider, MinoTransparency::Solid, original_scale, to_screen_coords, tile_size, grid::grid_position ); } - if (m_ghost_tetromino) { + if (m_ghost_tetromino.has_value()) { m_ghost_tetromino->render( service_provider, MinoTransparency::Ghost, original_scale, to_screen_coords, tile_size, grid::grid_position @@ -153,187 +115,6 @@ Tetrion::handle_event(const std::shared_ptr& /*input_manage return false; } -bool Tetrion::handle_input_command(const input::GameInputCommand command, const SimulationStep simulation_step_index) { - switch (command) { - case input::GameInputCommand::RotateLeft: - if (rotate_tetromino_left()) { - reset_lock_delay(simulation_step_index); - return true; - } - return false; - case input::GameInputCommand::RotateRight: - if (rotate_tetromino_right()) { - reset_lock_delay(simulation_step_index); - return true; - } - return false; - case input::GameInputCommand::MoveLeft: - if (move_tetromino_left()) { - reset_lock_delay(simulation_step_index); - return true; - } - return false; - case input::GameInputCommand::MoveRight: - if (move_tetromino_right()) { - reset_lock_delay(simulation_step_index); - return true; - } - return false; - case input::GameInputCommand::MoveDown: - //TODO(Totto): use input_type() != InputType:Touch -#if not defined(__ANDROID__) - m_down_key_pressed = true; - m_is_accelerated_down_movement = true; - m_next_gravity_simulation_step_index = simulation_step_index + get_gravity_delay_frames(); -#endif - if (move_tetromino_down(MovementType::Forced, simulation_step_index)) { - reset_lock_delay(simulation_step_index); - return true; - } - return false; - case input::GameInputCommand::Drop: - m_lock_delay_step_index = simulation_step_index; // lock instantly - return drop_tetromino(simulation_step_index); - case input::GameInputCommand::ReleaseMoveDown: { - m_down_key_pressed = false; - return false; - } - case input::GameInputCommand::Hold: - if (m_allowed_to_hold) { - hold_tetromino(simulation_step_index); - reset_lock_delay(simulation_step_index); - m_allowed_to_hold = false; - return true; - } - return false; - default: - assert(false and "unknown GameInput"); - return false; - } -} - -void Tetrion::spawn_next_tetromino(const SimulationStep simulation_step_index) { - spawn_next_tetromino(get_next_tetromino_type(), simulation_step_index); -} - -void Tetrion::spawn_next_tetromino(const helper::TetrominoType type, const SimulationStep simulation_step_index) { - constexpr GridPoint spawn_position{ 3, 0 }; - m_active_tetromino = Tetromino{ spawn_position, type }; - refresh_previews(); - if (not is_active_tetromino_position_valid()) { - m_game_state = GameState::GameOver; - - auto current_pieces = m_active_tetromino.value().minos(); - - bool all_valid{ false }; - u8 move_up = 0; - while (not all_valid) { - all_valid = true; - for (auto& mino : current_pieces) { - if (mino.position().y != 0) { - mino.position() = mino.position() - GridPoint{ 0, 1 }; - if (not is_valid_mino_position(mino.position())) { - all_valid = false; - } - } - } - - ++move_up; - } - - for (const Mino& mino : m_active_tetromino->minos()) { - auto position = mino.position(); - if (mino.position().y >= move_up && move_up != 0) { - position -= GridPoint{ 0, move_up }; - m_mino_stack.set(position, mino.type()); - } - } - - spdlog::info("game over"); - if (m_recording_writer.has_value()) { - spdlog::info("writing snapshot"); - std::ignore = m_recording_writer.value()->add_snapshot(simulation_step_index, core_information()); - } - m_active_tetromino = {}; - m_ghost_tetromino = {}; - return; - } - - m_next_gravity_simulation_step_index = simulation_step_index + get_gravity_delay_frames(); - refresh_ghost_tetromino(); -} - -bool Tetrion::rotate_tetromino_right() { - return with_lock_delay([&]() { return rotate(RotationDirection::Right); }); -} - -bool Tetrion::rotate_tetromino_left() { - return with_lock_delay([&]() { return rotate(RotationDirection::Left); }); -} - -bool Tetrion::move_tetromino_down(MovementType movement_type, const SimulationStep simulation_step_index) { - if (not m_active_tetromino.has_value()) { - return false; - } - if (movement_type == MovementType::Forced) { - m_score += 4; - } - - - if (tetromino_can_move_down(m_active_tetromino.value())) { - m_active_tetromino->move_down(); - return true; - } - - m_is_in_lock_delay = true; - if ((m_is_in_lock_delay and m_num_executed_lock_delays >= num_lock_delays) - or simulation_step_index >= m_lock_delay_step_index) { - lock_active_tetromino(simulation_step_index); - reset_lock_delay(simulation_step_index); - } else { - m_next_gravity_simulation_step_index = simulation_step_index + 1; - } - return false; -} - -bool Tetrion::move_tetromino_left() { - return with_lock_delay([&]() { return move(MoveDirection::Left); }); -} - -bool Tetrion::move_tetromino_right() { - return with_lock_delay([&]() { return move(MoveDirection::Right); }); -} - -bool Tetrion::drop_tetromino(const SimulationStep simulation_step_index) { - if (not m_active_tetromino.has_value()) { - return false; - } - u64 num_movements = 0; - while (tetromino_can_move_down(m_active_tetromino.value())) { - ++num_movements; - m_active_tetromino->move_down(); - } - - m_score += static_cast(4) * num_movements; - lock_active_tetromino(simulation_step_index); - return num_movements > 0; -} - -void Tetrion::hold_tetromino(const SimulationStep simulation_step_index) { - if (not m_active_tetromino.has_value()) { - return; - } - - if (not m_tetromino_on_hold.has_value()) { - m_tetromino_on_hold = Tetromino{ grid::hold_tetromino_position, m_active_tetromino->type() }; - spawn_next_tetromino(simulation_step_index); - } else { - const auto on_hold = m_tetromino_on_hold->type(); - m_tetromino_on_hold = Tetromino{ grid::hold_tetromino_position, m_active_tetromino->type() }; - spawn_next_tetromino(on_hold, simulation_step_index); - } -} - [[nodiscard]] Grid* Tetrion::get_grid() { return m_main_layout.get(0); } @@ -350,39 +131,6 @@ void Tetrion::hold_tetromino(const SimulationStep simulation_step_index) { return m_main_layout.get(1); } -[[nodiscard]] u8 Tetrion::tetrion_index() const { - return m_tetrion_index; -} - -[[nodiscard]] u32 Tetrion::level() const { - return m_level; -} - -[[nodiscard]] u64 Tetrion::score() const { - return m_score; -} - -[[nodiscard]] u32 Tetrion::lines_cleared() const { - return m_lines_cleared; -} - -[[nodiscard]] const MinoStack& Tetrion::mino_stack() const { - return m_mino_stack; -} - -[[nodiscard]] std::unique_ptr Tetrion::core_information() const { - - return std::make_unique(m_tetrion_index, m_level, m_score, m_lines_cleared, m_mino_stack); -} - -[[nodiscard]] bool Tetrion::is_game_over() const { - return m_game_state == GameState::GameOver; -} - -void Tetrion::reset_lock_delay(const SimulationStep simulation_step_index) { - m_lock_delay_step_index = simulation_step_index + lock_delay; -} - void Tetrion::refresh_texts() { auto* text_layout = get_text_layout(); @@ -398,255 +146,3 @@ void Tetrion::refresh_texts() { stream << "lines: " << m_lines_cleared; text_layout->get(2)->set_text(*m_service_provider, stream.str()); } - -void Tetrion::clear_fully_occupied_lines() { - bool cleared = false; - const u32 lines_cleared_before = m_lines_cleared; - do { // NOLINT(cppcoreguidelines-avoid-do-while) - cleared = false; - for (u8 row = 0; row < grid::height_in_tiles; ++row) { - bool fully_occupied = true; - for (u8 column = 0; column < grid::width_in_tiles; ++column) { - if (m_mino_stack.is_empty(GridPoint{ column, row })) { - fully_occupied = false; - break; - } - } - - if (fully_occupied) { - ++m_lines_cleared; - const auto level = m_lines_cleared / 10; - if (level > m_level) { - m_level = level; - spdlog::info("new level: {}", m_level); - if (level == constants::music_change_level) { - m_service_provider->music_manager() - .load_and_play_music( - utils::get_assets_folder() / "music" - / utils::get_supported_music_extension("03. Game Theme (50 Left)") - ) - .and_then(utils::log_error); - } - } - m_mino_stack.clear_row_and_let_sink(static_cast(row)); - cleared = true; - break; - } - } - } while (cleared); - const u32 num_lines_cleared = m_lines_cleared - lines_cleared_before; - static constexpr std::array score_per_line_multiplier{ 0, 40, 100, 300, 1200 }; - m_score += static_cast(score_per_line_multiplier.at(num_lines_cleared)) * static_cast(m_level + 1); -} - -void Tetrion::lock_active_tetromino(const SimulationStep simulation_step_index) { - assert(m_active_tetromino.has_value()); - for (const Mino& mino : m_active_tetromino->minos()) { - m_mino_stack.set(mino.position(), mino.type()); - } - m_allowed_to_hold = true; - m_is_in_lock_delay = false; - m_num_executed_lock_delays = 0; - clear_fully_occupied_lines(); - spawn_next_tetromino(simulation_step_index); - refresh_texts(); - reset_lock_delay(simulation_step_index); - - // save a snapshot on every freeze (only in debug builds) -#if !defined(NDEBUG) - if (m_recording_writer) { - spdlog::debug("adding snapshot at step {}", simulation_step_index); - std::ignore = (*m_recording_writer)->add_snapshot(simulation_step_index, core_information()); - } -#endif -} - -bool Tetrion::is_active_tetromino_position_valid() const { - if (not m_active_tetromino) { - return false; - } - return is_tetromino_position_valid(m_active_tetromino.value()); -} - -bool Tetrion::is_valid_mino_position(GridPoint position) const { - return position.x < grid::width_in_tiles and position.y < grid::height_in_tiles and m_mino_stack.is_empty(position); -} - -bool Tetrion::mino_can_move_down(GridPoint position) const { - if (position.y == (grid::height_in_tiles - 1)) { - return false; - } - - return is_valid_mino_position(position + GridPoint{ 0, 1 }); -} - - -void Tetrion::refresh_ghost_tetromino() { - if (not m_active_tetromino.has_value()) { - m_ghost_tetromino = {}; - return; - } - m_ghost_tetromino = m_active_tetromino.value(); - while (tetromino_can_move_down(m_ghost_tetromino.value())) { - m_ghost_tetromino->move_down(); - } -} - -void Tetrion::refresh_previews() { - auto sequence_index = m_sequence_index; - auto bag_index = usize{ 0 }; - for (std::remove_cvref_t i = 0; i < num_preview_tetrominos; ++i) { - m_preview_tetrominos.at(static_cast(i)) = Tetromino{ - grid::preview_tetromino_position + shapes::UPoint{ 0, static_cast(grid::preview_padding * i) }, - m_sequence_bags.at(bag_index)[sequence_index] - }; - ++sequence_index; - static constexpr auto bag_size = decltype(m_sequence_bags)::value_type::size(); - if (sequence_index >= bag_size) { - assert(sequence_index == bag_size); - sequence_index = 0; - ++bag_index; - assert(bag_index < m_sequence_bags.size()); - } - } -} - -helper::TetrominoType Tetrion::get_next_tetromino_type() { - const helper::TetrominoType next_type = m_sequence_bags[0][m_sequence_index]; - m_sequence_index = (m_sequence_index + 1) % Bag::size(); - if (m_sequence_index == 0) { - // we had a wrap-around - m_sequence_bags[0] = m_sequence_bags[1]; - m_sequence_bags[1] = Bag{ m_random }; - } - return next_type; -} - -bool Tetrion::tetromino_can_move_down(const Tetromino& tetromino) const { - return not std::ranges::any_of(tetromino.minos(), [this](const Mino& mino) { - return not mino_can_move_down(mino.position()); - }); -} - - -[[nodiscard]] u64 Tetrion::get_gravity_delay_frames() const { - const auto frames = (m_level >= frames_per_tile.size() ? frames_per_tile.back() : frames_per_tile.at(m_level)); - if (m_is_accelerated_down_movement) { - return std::max(u64{ 1 }, static_cast(std::round(static_cast(frames) / 20.0))); - } - return frames; -} - -u8 Tetrion::rotation_to_index(const Rotation from, const Rotation rotation_to) { - if (from == Rotation::North and rotation_to == Rotation::East) { - return 0; - } - if (from == Rotation::East and rotation_to == Rotation::North) { - return 1; - } - if (from == Rotation::East and rotation_to == Rotation::South) { - return 2; - } - if (from == Rotation::South and rotation_to == Rotation::East) { - return 3; - } - if (from == Rotation::South and rotation_to == Rotation::West) { - return 4; - } - if (from == Rotation::West and rotation_to == Rotation::South) { - return 5; - } - if (from == Rotation::West and rotation_to == Rotation::North) { - return 6; - } - if (from == Rotation::North and rotation_to == Rotation::West) { - return 7; - } - UNREACHABLE(); -} - -bool Tetrion::is_tetromino_position_valid(const Tetromino& tetromino) const { - return not std::ranges::any_of(tetromino.minos(), [this](const Mino& mino) { - return not is_valid_mino_position(mino.position()); - }); -} - -bool Tetrion::rotate(Tetrion::RotationDirection rotation_direction) { - if (not m_active_tetromino) { - return false; - } - - const auto wall_kick_table = get_wall_kick_table(); - if (not wall_kick_table.has_value()) { - return false; - } - - const auto from_rotation = m_active_tetromino->rotation(); - const auto to_rotation = from_rotation + static_cast(rotation_direction == RotationDirection::Left ? -1 : 1); - const auto table_index = rotation_to_index(from_rotation, to_rotation); - - if (rotation_direction == RotationDirection::Left) { - m_active_tetromino->rotate_left(); - } else { - m_active_tetromino->rotate_right(); - } - - for (const auto& translation : (*wall_kick_table)->at(table_index)) { - m_active_tetromino->move(translation); - if (is_active_tetromino_position_valid()) { - return true; - } - m_active_tetromino->move(-translation); - } - - if (rotation_direction == RotationDirection::Left) { - m_active_tetromino->rotate_right(); - } else { - m_active_tetromino->rotate_left(); - } - return false; -} - -bool Tetrion::move(const Tetrion::MoveDirection move_direction) { - if (not m_active_tetromino) { - return false; - } - - switch (move_direction) { - case MoveDirection::Left: - m_active_tetromino->move_left(); - if (not is_active_tetromino_position_valid()) { - m_active_tetromino->move_right(); - return false; - } - return true; - case MoveDirection::Right: - m_active_tetromino->move_right(); - if (not is_active_tetromino_position_valid()) { - m_active_tetromino->move_left(); - return false; - } - return true; - } - - UNREACHABLE(); -} - -std::optional Tetrion::get_wall_kick_table() const { - assert(m_active_tetromino.has_value() and "no active tetromino"); - const auto type = m_active_tetromino->type(); // NOLINT(bugprone-unchecked-optional-access) - switch (type) { - case helper::TetrominoType::J: - case helper::TetrominoType::L: - case helper::TetrominoType::T: - case helper::TetrominoType::S: - case helper::TetrominoType::Z: - return &wall_kick_data_jltsz; - case helper::TetrominoType::I: - return &wall_kick_data_i; - case helper::TetrominoType::O: - return {}; - default: - UNREACHABLE(); - } -} diff --git a/src/game/tetrion.hpp b/src/game/tetrion.hpp index cbff250f..bd4f37cb 100644 --- a/src/game/tetrion.hpp +++ b/src/game/tetrion.hpp @@ -5,77 +5,24 @@ #include #include -#include "bag.hpp" -#include "grid.hpp" #include "input/game_input.hpp" #include "manager/service_provider.hpp" -#include "tetromino.hpp" +#include "simulated_tetrion.hpp" #include "ui/layout.hpp" -#include "ui/layouts/grid_layout.hpp" #include "ui/layouts/tile_layout.hpp" #include "ui/widget.hpp" -#include namespace recorder { struct RecordingWriter; } -enum class GameState : u8 { - Playing, - GameOver, -}; - -enum class MovementType : u8 { - Gravity, - Forced, -}; -struct Tetrion final : public ui::Widget { +struct Tetrion final : public ui::Widget, SimulatedTetrion { private: - using WallKickPoint = shapes::AbstractPoint; - using WallKickTable = std::array, 8>; - using GridPoint = Mino::GridPoint; using ScreenCordsFunction = Mino::ScreenCordsFunction; + using GridPoint = Mino::GridPoint; - static constexpr SimulationStep lock_delay = 30; - static constexpr int num_lock_delays = 30; - - enum class RotationDirection : u8 { - Left, - Right, - }; - - enum class MoveDirection : u8 { - Left, - Right, - }; - - static constexpr u8 num_preview_tetrominos = 6; - - bool m_is_accelerated_down_movement = false; - bool m_down_key_pressed = false; - bool m_allowed_to_hold = true; - bool m_is_in_lock_delay = false; - - u32 m_num_executed_lock_delays = 0; - u64 m_lock_delay_step_index; - ServiceProvider* const m_service_provider; - std::optional> m_recording_writer; - MinoStack m_mino_stack; - Random m_random; - u32 m_level; - u32 m_lines_cleared = 0; - GameState m_game_state = GameState::Playing; - int m_sequence_index = 0; - u64 m_score = 0; - std::array m_sequence_bags{ Bag{ m_random }, Bag{ m_random } }; - std::optional m_active_tetromino; - std::optional m_ghost_tetromino; - std::optional m_tetromino_on_hold; - std::array, num_preview_tetrominos> m_preview_tetrominos{}; - u8 m_tetrion_index; - u64 m_next_gravity_simulation_step_index; ui::TileLayout m_main_layout; @@ -87,205 +34,24 @@ struct Tetrion final : public ui::Widget { std::optional> recording_writer, const ui::Layout& layout, bool is_top_level); - void update_step(SimulationStep simulation_step_index); + + ~Tetrion() override; + + Tetrion(const Tetrion& other) = delete; + Tetrion& operator=(const Tetrion& other) = delete; + + Tetrion(Tetrion&& other) noexcept = delete; + Tetrion& operator=(Tetrion&& other) noexcept = delete; + void render(const ServiceProvider& service_provider) const override; [[nodiscard]] Widget::EventHandleResult handle_event(const std::shared_ptr& input_manager, const SDL_Event& event) override; - // returns if the input event lead to a movement - bool handle_input_command(input::GameInputCommand command, SimulationStep simulation_step_index); - void spawn_next_tetromino(SimulationStep simulation_step_index); - void spawn_next_tetromino(helper::TetrominoType type, SimulationStep simulation_step_index); - bool rotate_tetromino_right(); - bool rotate_tetromino_left(); - bool move_tetromino_down(MovementType movement_type, SimulationStep simulation_step_index); - bool move_tetromino_left(); - bool move_tetromino_right(); - bool drop_tetromino(SimulationStep simulation_step_index); - void hold_tetromino(SimulationStep simulation_step_index); - [[nodiscard]] Grid* get_grid(); [[nodiscard]] const Grid* get_grid() const; [[nodiscard]] ui::GridLayout* get_text_layout(); [[nodiscard]] const ui::GridLayout* get_text_layout() const; - [[nodiscard]] u8 tetrion_index() const; - [[nodiscard]] u32 level() const; - [[nodiscard]] u64 score() const; - [[nodiscard]] u32 lines_cleared() const; - [[nodiscard]] const MinoStack& mino_stack() const; - [[nodiscard]] std::unique_ptr core_information() const; - - [[nodiscard]] bool is_game_over() const; - private: - template - bool with_lock_delay(Callable movement) { - const auto result = movement(); - if (result and m_is_in_lock_delay) { - ++m_num_executed_lock_delays; - } - return result; - } - - bool rotate(RotationDirection rotation_direction); - bool move(MoveDirection move_direction); - [[nodiscard]] std::optional get_wall_kick_table() const; - void reset_lock_delay(SimulationStep simulation_step_index); - void refresh_texts(); - void clear_fully_occupied_lines(); - void lock_active_tetromino(SimulationStep simulation_step_index); - [[nodiscard]] bool is_active_tetromino_position_valid() const; - [[nodiscard]] bool mino_can_move_down(GridPoint position) const; - [[nodiscard]] bool is_valid_mino_position(GridPoint position) const; - - void refresh_ghost_tetromino(); - void refresh_previews(); - helper::TetrominoType get_next_tetromino_type(); - - [[nodiscard]] bool is_tetromino_position_valid(const Tetromino& tetromino) const; - [[nodiscard]] bool tetromino_can_move_down(const Tetromino& tetromino) const; - - [[nodiscard]] u64 get_gravity_delay_frames() const; - - static u8 rotation_to_index(Rotation from, Rotation to); - - static constexpr auto wall_kick_data_jltsz = WallKickTable{ - // North -> East - std::array{ - WallKickPoint{ 0, 0 }, - WallKickPoint{ -1, 0 }, - WallKickPoint{ -1, -1 }, - WallKickPoint{ 0, 2 }, - WallKickPoint{ -1, 2 }, - }, - // East -> North - std::array{ - WallKickPoint{ 0, 0 }, - WallKickPoint{ 1, 0 }, - WallKickPoint{ 1, 1 }, - WallKickPoint{ 0, -2 }, - WallKickPoint{ 1, -2 }, - }, - // East -> South - std::array{ - WallKickPoint{ 0, 0 }, - WallKickPoint{ 1, 0 }, - WallKickPoint{ 1, 1 }, - WallKickPoint{ 0, -2 }, - WallKickPoint{ 1, -2 }, - }, - // South -> East - std::array{ - WallKickPoint{ 0, 0 }, - WallKickPoint{ -1, 0 }, - WallKickPoint{ -1, -1 }, - WallKickPoint{ 0, 2 }, - WallKickPoint{ -1, 2 }, - }, - // South -> West - std::array{ - WallKickPoint{ 0, 0 }, - WallKickPoint{ 1, 0 }, - WallKickPoint{ 1, -1 }, - WallKickPoint{ 0, 2 }, - WallKickPoint{ 1, 2 }, - }, - // West -> South - std::array{ - WallKickPoint{ 0, 0 }, - WallKickPoint{ -1, 0 }, - WallKickPoint{ -1, 1 }, - WallKickPoint{ 0, -2 }, - WallKickPoint{ -1, -2 }, - }, - // West -> North - std::array{ - WallKickPoint{ 0, 0 }, - WallKickPoint{ -1, 0 }, - WallKickPoint{ -1, 1 }, - WallKickPoint{ 0, -2 }, - WallKickPoint{ -1, -2 }, - }, - // North -> West - std::array{ - WallKickPoint{ 0, 0 }, - WallKickPoint{ 1, 0 }, - WallKickPoint{ 1, -1 }, - WallKickPoint{ 0, 2 }, - WallKickPoint{ 1, 2 }, - }, - }; - - static constexpr auto wall_kick_data_i = WallKickTable{ - // North -> East - std::array{ - WallKickPoint{ 0, 0 }, - WallKickPoint{ -2, 0 }, - WallKickPoint{ 1, 0 }, - WallKickPoint{ -2, 1 }, - WallKickPoint{ 1, -2 }, - }, - // East -> North - std::array{ - WallKickPoint{ 0, 0 }, - WallKickPoint{ 2, 0 }, - WallKickPoint{ -1, 0 }, - WallKickPoint{ 2, -1 }, - WallKickPoint{ -1, 2 }, - }, - // East -> South - std::array{ - WallKickPoint{ 0, 0 }, - WallKickPoint{ -1, 0 }, - WallKickPoint{ 2, 0 }, - WallKickPoint{ -1, -2 }, - WallKickPoint{ 2, 1 }, - }, - // South -> East - std::array{ - WallKickPoint{ 0, 0 }, - WallKickPoint{ 1, 0 }, - WallKickPoint{ -2, 0 }, - WallKickPoint{ 1, 2 }, - WallKickPoint{ -2, -1 }, - }, - // South -> West - std::array{ - WallKickPoint{ 0, 0 }, - WallKickPoint{ 2, 0 }, - WallKickPoint{ -1, 0 }, - WallKickPoint{ 2, -1 }, - WallKickPoint{ -1, 2 }, - }, - // West -> South - std::array{ - WallKickPoint{ 0, 0 }, - WallKickPoint{ -2, 0 }, - WallKickPoint{ 1, 0 }, - WallKickPoint{ -2, 1 }, - WallKickPoint{ 1, -2 }, - }, - // West -> North - std::array{ - WallKickPoint{ 0, 0 }, - WallKickPoint{ 1, 0 }, - WallKickPoint{ -2, 0 }, - WallKickPoint{ 1, 2 }, - WallKickPoint{ -2, -1 }, - }, - // North -> West - std::array{ - WallKickPoint{ 0, 0 }, - WallKickPoint{ -1, 0 }, - WallKickPoint{ 2, 0 }, - WallKickPoint{ -1, -2 }, - WallKickPoint{ 2, 1 }, - }, - }; - - static constexpr auto frames_per_tile = std::array{ 48, 43, 38, 33, 28, 23, 18, 13, 8, 6, 5, 5, 5, 4, 4, - 4, 3, 3, 3, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 1 }; - - friend struct TetrionSnapshot; + void refresh_texts() override; }; diff --git a/src/input/game_input.hpp b/src/input/game_input.hpp index bda37d19..12b2ae8d 100644 --- a/src/input/game_input.hpp +++ b/src/input/game_input.hpp @@ -4,13 +4,12 @@ #include #include -#include "helper/clock_source.hpp" -#include "manager/event_listener.hpp" +#include #include #include -struct Tetrion; +struct SimulatedTetrion; namespace input { @@ -48,7 +47,7 @@ namespace input { std::unordered_map m_keys_hold; GameInputType m_input_type; - Tetrion* m_target_tetrion{}; + SimulatedTetrion* m_target_tetrion{}; OnEventCallback m_on_event_callback; protected: @@ -56,7 +55,7 @@ namespace input { void handle_event(InputEvent event, SimulationStep simulation_step_index); - [[nodiscard]] const Tetrion* target_tetrion() const { + [[nodiscard]] const SimulatedTetrion* target_tetrion() const { return m_target_tetrion; } @@ -90,7 +89,7 @@ namespace input { return m_input_type != GameInputType::Touch; } - void set_target_tetrion(Tetrion* target_tetrion) { + void set_target_tetrion(SimulatedTetrion* target_tetrion) { m_target_tetrion = target_tetrion; } diff --git a/src/input/guid.hpp b/src/input/guid.hpp index c0c7b208..4d853265 100644 --- a/src/input/guid.hpp +++ b/src/input/guid.hpp @@ -52,78 +52,82 @@ struct fmt::formatter : formatter { namespace { //NOLINT(cert-dcl59-cpp,google-build-namespaces) - // decode a single_hex_number - [[nodiscard]] constexpr const_utils::Expected single_hex_number(char input) { - if (input >= '0' && input <= '9') { - return const_utils::Expected::good_result(static_cast(input - '0')); - } + namespace guid { - if (input >= 'A' && input <= 'F') { - return const_utils::Expected::good_result(static_cast(input - 'A' + 10)); - } + // decode a single_hex_number + [[nodiscard]] constexpr const_utils::Expected single_hex_number(char input) { + if (input >= '0' && input <= '9') { + return const_utils::Expected::good_result(static_cast(input - '0')); + } + + if (input >= 'A' && input <= 'F') { + return const_utils::Expected::good_result(static_cast(input - 'A' + 10)); + } - if (input >= 'a' && input <= 'f') { - return const_utils::Expected::good_result(static_cast(input - 'a' + 10)); + if (input >= 'a' && input <= 'f') { + return const_utils::Expected::good_result(static_cast(input - 'a' + 10)); + } + + return const_utils::Expected::error_result("the input must be a valid hex character"); } - return const_utils::Expected::error_result("the input must be a valid hex character"); - } + // decode a single 2 digit color value in hex + [[nodiscard]] constexpr const_utils::Expected single_hex_color_value(const char* input) { - // decode a single 2 digit color value in hex - [[nodiscard]] constexpr const_utils::Expected single_hex_color_value(const char* input) { + const auto first = single_hex_number(input[0]); //NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic) - const auto first = single_hex_number(input[0]); //NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic) + PROPAGATE(first, u8, std::string); - PROPAGATE(first, u8, std::string); + const auto second = single_hex_number(input[1]); //NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic) - const auto second = single_hex_number(input[1]); //NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic) + PROPAGATE(second, u8, std::string); - PROPAGATE(second, u8, std::string); + return const_utils::Expected::good_result((first.value() << 4) | second.value()); + } - return const_utils::Expected::good_result((first.value() << 4) | second.value()); - } + [[nodiscard]] constexpr const_utils::Expected + get_guid_from_string_impl(const char* input, std::size_t size) { - [[nodiscard]] constexpr const_utils::Expected - get_guid_from_string_impl(const char* input, std::size_t size) { + if (size == 0) { + return const_utils::Expected::error_result( + "not enough data to determine the literal type" + ); + } - if (size == 0) { - return const_utils::Expected::error_result( - "not enough data to determine the literal type" - ); - } + constexpr std::size_t amount = 16; - constexpr std::size_t amount = 16; + size_t width = 2; - size_t width = 2; + if (size == amount * 2) { + width = 2; + } else if (size == (amount * 2 + (amount - 1))) { + width = 3; + } else { - if (size == amount * 2) { - width = 2; - } else if (size == (amount * 2 + (amount - 1))) { - width = 3; - } else { + return const_utils::Expected::error_result("Unrecognized guid literal"); + } - return const_utils::Expected::error_result("Unrecognized guid literal"); - } + sdl::GUID::ArrayType result{}; - sdl::GUID::ArrayType result{}; + for (size_t i = 0; i < amount; ++i) { + const size_t offset = i * width; - for (size_t i = 0; i < amount; ++i) { - const size_t offset = i * width; + const auto temp_result = single_hex_color_value( + input + offset //NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic) + ); - const auto temp_result = - single_hex_color_value(input + offset); //NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic) + PROPAGATE(temp_result, sdl::GUID, std::string); - PROPAGATE(temp_result, sdl::GUID, std::string); + const auto value = temp_result.value(); - const auto value = temp_result.value(); + result.at(i) = value; + } - result.at(i) = value; + return const_utils::Expected::good_result(sdl::GUID{ result }); } - - return const_utils::Expected::good_result(sdl::GUID{ result }); - } + } // namespace guid } // namespace @@ -132,14 +136,14 @@ namespace detail { [[nodiscard]] constexpr const_utils::Expected get_guid_from_string(const std::string& input ) { - return get_guid_from_string_impl(input.c_str(), input.size()); + return guid::get_guid_from_string_impl(input.c_str(), input.size()); } } // namespace detail consteval sdl::GUID operator""_guid(const char* input, std::size_t size) { - const auto result = get_guid_from_string_impl(input, size); + const auto result = guid::get_guid_from_string_impl(input, size); CONSTEVAL_STATIC_ASSERT(result.has_value(), "incorrect guid literal"); diff --git a/tests/files/test_rec_valid.rec b/tests/files/test_rec_valid.rec new file mode 100644 index 00000000..14e9ae3d Binary files /dev/null and b/tests/files/test_rec_valid.rec differ diff --git a/tests/graphics/meson.build b/tests/graphics/meson.build index c1538b2d..dcb37735 100644 --- a/tests/graphics/meson.build +++ b/tests/graphics/meson.build @@ -1 +1,4 @@ -graphics_test_src += files('sdl_key.cpp') +graphics_test_src += files( + 'sdl_key.cpp', + 'tetrion_simulation.cpp', +) diff --git a/tests/graphics/tetrion_simulation.cpp b/tests/graphics/tetrion_simulation.cpp new file mode 100644 index 00000000..e57b0cf5 --- /dev/null +++ b/tests/graphics/tetrion_simulation.cpp @@ -0,0 +1,32 @@ + + +#include "game/simulation.hpp" +#include "utils/helper.hpp" + +#include +#include + + +TEST(Simulation, InvalidFilePath) { + + std::filesystem::path path = "__INVALID_PATH"; + + auto maybe_simulation = Simulation::get_replay_simulation(path); + + ASSERT_THAT(maybe_simulation, ExpectedHasError()) + << "Path was: " << path << "\nError: " << maybe_simulation.error(); + ASSERT_THAT( + maybe_simulation.error(), + ("an error occurred while reading recording: unable to load recording from file \"" + path.string() + "\"") + ); +} + +TEST(Simulation, ValidRecordingsFile) { + + std::filesystem::path path = "./test_rec_valid.rec"; + + auto maybe_simulation = Simulation::get_replay_simulation(path); + + ASSERT_THAT(maybe_simulation, ExpectedHasValue()) + << "Path was: " << path << "\nError: " << maybe_simulation.error(); +} diff --git a/tests/utils/helper.hpp b/tests/utils/helper.hpp index 15c3de0d..73645078 100644 --- a/tests/utils/helper.hpp +++ b/tests/utils/helper.hpp @@ -2,6 +2,7 @@ #pragma once +#include "core/helper/utils.hpp" #include "printer.hpp" #include @@ -22,3 +23,10 @@ MATCHER(OptionalHasValue, "optional has value") { MATCHER(OptionalHasNoValue, "optional has no value") { return not arg.has_value(); } + + +#define ASSERT_FAIL(x) /*NOLINT(cppcoreguidelines-macro-usage)*/ \ + do { \ + std::cerr << "assertion fail in " << __FILE__ << ":" << __LINE__ << ": " << (x) << "\n"; \ + utils::unreachable(); \ + } while (false) diff --git a/tests/utils/meson.build b/tests/utils/meson.build index 95435d7f..b2a61873 100644 --- a/tests/utils/meson.build +++ b/tests/utils/meson.build @@ -1 +1,6 @@ -test_src += files('files.cpp', 'files.hpp', 'helper.hpp', 'printer.hpp') +test_src += files( + 'files.cpp', + 'files.hpp', + 'helper.hpp', + 'printer.hpp' +)