From d4d3e397a0ee52c4e82567ff0c44bc47263dfdd1 Mon Sep 17 00:00:00 2001 From: Raymond Wright Date: Sat, 25 Apr 2020 21:40:14 +0100 Subject: [PATCH] Break up optimiser files --- CMakeLists.txt | 2 +- include/optimiser.hpp | 68 +------- include/processed.hpp | 93 ++++++++++ src/optimiser.cpp | 172 +------------------ src/processed.cpp | 189 ++++++++++++++++++++ tests/CMakeLists.txt | 6 +- tests/optimiser_unittest.cpp | 304 --------------------------------- tests/processed_unittest.cpp | 323 +++++++++++++++++++++++++++++++++++ 8 files changed, 618 insertions(+), 539 deletions(-) create mode 100644 include/processed.hpp create mode 100644 src/processed.cpp create mode 100644 tests/processed_unittest.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index e9e1f73e..daecbf5d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -22,7 +22,7 @@ function(set_warnings TARGET) ) endfunction() -add_executable(chopt src/main.cpp src/chart.cpp src/image.cpp src/optimiser.cpp src/points.cpp src/settings.cpp src/sp.cpp src/time.cpp) +add_executable(chopt src/main.cpp src/chart.cpp src/image.cpp src/optimiser.cpp src/points.cpp src/processed.cpp src/settings.cpp src/sp.cpp src/time.cpp) target_include_directories(chopt PRIVATE "${PROJECT_SOURCE_DIR}/include" "${PROJECT_SOURCE_DIR}/libs") set_cpp_standard(chopt) diff --git a/include/optimiser.hpp b/include/optimiser.hpp index 3ef8a078..bdf30639 100644 --- a/include/optimiser.hpp +++ b/include/optimiser.hpp @@ -21,77 +21,13 @@ #include #include -#include +#include #include -#include "chart.hpp" #include "points.hpp" -#include "sp.hpp" +#include "processed.hpp" #include "time.hpp" -struct ActivationCandidate { - PointPtr act_start; - PointPtr act_end; - Position earliest_activation_point {Beat(0.0), Measure(0.0)}; - SpBar sp_bar {0.0, 0.0}; -}; - -struct Activation { - PointPtr act_start; - PointPtr act_end; -}; - -// Part of the return value of ProcessedSong::is_candidate_valid. Says if an -// activation is valid, and if not whether the problem is too little or too much -// Star Power. -enum class ActValidity { success, insufficient_sp, surplus_sp }; - -// Return value of ProcessedSong::is_candidate_valid, providing information on -// whether an activation is valid, and if so the earliest position it can end. -struct ActResult { - Position ending_position; - ActValidity validity; -}; - -struct Path { - std::vector activations; - int score_boost; -}; - -// Represents a song processed for Star Power optimisation. The constructor -// should only fail due to OOM; invariants on the song are supposed to be -// upheld by the constructors of the arguments. -class ProcessedSong { -private: - // The order of these members is important. We must have m_converter before - // m_points. - TimeConverter m_converter; - PointSet m_points; - SpData m_sp_data; - int m_total_solo_boost; - -public: - ProcessedSong(const NoteTrack& track, int resolution, - const SyncTrack& sync_track, double early_whammy, - double squeeze); - - // Return the minimum and maximum amount of SP can be acquired between two - // points. Does not include SP from the point act_start. first_point is - // given for the purposes of counting SP grantings notes, e.g. if start is - // after the middle of first_point's timing window. - [[nodiscard]] SpBar total_available_sp(Beat start, PointPtr first_point, - PointPtr act_start) const; - // Returns an ActResult which says if an activation is valid, and if so the - // earliest position it can end. - [[nodiscard]] ActResult - is_candidate_valid(const ActivationCandidate& activation) const; - // Return the summary of a path. - [[nodiscard]] std::string path_summary(const Path& path) const; - - [[nodiscard]] const PointSet& points() const { return m_points; } - [[nodiscard]] const SpData& sp_data() const { return m_sp_data; } -}; - // The class that stores extra information needed on top of a ProcessedSong for // the purposes of optimisation, and finds the optimal path. The song passed to // Optimiser's constructor must outlive Optimiser; the class is done this way so diff --git a/include/processed.hpp b/include/processed.hpp new file mode 100644 index 00000000..a4f5361f --- /dev/null +++ b/include/processed.hpp @@ -0,0 +1,93 @@ +/* + * chopt - Star Power optimiser for Clone Hero + * Copyright (C) 2020 Raymond Wright + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef CHOPT_PROCESSED_HPP +#define CHOPT_PROCESSED_HPP + +#include +#include + +#include "chart.hpp" +#include "points.hpp" +#include "sp.hpp" +#include "time.hpp" + +struct ActivationCandidate { + PointPtr act_start; + PointPtr act_end; + Position earliest_activation_point {Beat(0.0), Measure(0.0)}; + SpBar sp_bar {0.0, 0.0}; +}; + +struct Activation { + PointPtr act_start; + PointPtr act_end; +}; + +// Part of the return value of ProcessedSong::is_candidate_valid. Says if an +// activation is valid, and if not whether the problem is too little or too much +// Star Power. +enum class ActValidity { success, insufficient_sp, surplus_sp }; + +// Return value of ProcessedSong::is_candidate_valid, providing information on +// whether an activation is valid, and if so the earliest position it can end. +struct ActResult { + Position ending_position; + ActValidity validity; +}; + +struct Path { + std::vector activations; + int score_boost; +}; + +// Represents a song processed for Star Power optimisation. The constructor +// should only fail due to OOM; invariants on the song are supposed to be +// upheld by the constructors of the arguments. +class ProcessedSong { +private: + // The order of these members is important. We must have m_converter before + // m_points. + TimeConverter m_converter; + PointSet m_points; + SpData m_sp_data; + int m_total_solo_boost; + +public: + ProcessedSong(const NoteTrack& track, int resolution, + const SyncTrack& sync_track, double early_whammy, + double squeeze); + + // Return the minimum and maximum amount of SP can be acquired between two + // points. Does not include SP from the point act_start. first_point is + // given for the purposes of counting SP grantings notes, e.g. if start is + // after the middle of first_point's timing window. + [[nodiscard]] SpBar total_available_sp(Beat start, PointPtr first_point, + PointPtr act_start) const; + // Returns an ActResult which says if an activation is valid, and if so the + // earliest position it can end. + [[nodiscard]] ActResult + is_candidate_valid(const ActivationCandidate& activation) const; + // Return the summary of a path. + [[nodiscard]] std::string path_summary(const Path& path) const; + + [[nodiscard]] const PointSet& points() const { return m_points; } + [[nodiscard]] const SpData& sp_data() const { return m_sp_data; } +}; + +#endif diff --git a/src/optimiser.cpp b/src/optimiser.cpp index c9002e73..6dd660c3 100644 --- a/src/optimiser.cpp +++ b/src/optimiser.cpp @@ -16,179 +16,15 @@ * along with this program. If not, see . */ +#include +#include #include #include #include #include -#include #include "optimiser.hpp" -static constexpr double MEASURES_PER_BAR = 8.0; -static constexpr double MINIMUM_SP_AMOUNT = 0.5; - -ProcessedSong::ProcessedSong(const NoteTrack& track, int resolution, - const SyncTrack& sync_track, double early_whammy, - double squeeze) - : m_converter {sync_track, resolution} - , m_points {track, resolution, m_converter, squeeze} - , m_sp_data {track, resolution, sync_track, early_whammy} -{ - m_total_solo_boost = std::accumulate( - track.solos().cbegin(), track.solos().cend(), 0, - [](const auto x, const auto& y) { return x + y.value; }); -} - -SpBar ProcessedSong::total_available_sp(Beat start, PointPtr first_point, - PointPtr act_start) const -{ - SpBar sp_bar {0.0, 0.0}; - for (auto p = first_point; p < act_start; ++p) { - if (p->is_sp_granting_note) { - sp_bar.add_phrase(); - } - } - - sp_bar.max() += m_sp_data.available_whammy(start, act_start->position.beat); - sp_bar.max() = std::min(sp_bar.max(), 1.0); - - return sp_bar; -} - -ActResult -ProcessedSong::is_candidate_valid(const ActivationCandidate& activation) const -{ - constexpr double SP_PHRASE_AMOUNT = 0.25; - const Position null_position {Beat(0.0), Measure(0.0)}; - - if (!activation.sp_bar.full_enough_to_activate()) { - return {null_position, ActValidity::insufficient_sp}; - } - - auto current_position = activation.act_start->hit_window_end; - - auto sp_bar = activation.sp_bar; - sp_bar.min() = std::max(sp_bar.min(), MINIMUM_SP_AMOUNT); - - auto starting_meas_diff = current_position.measure - - activation.earliest_activation_point.measure; - sp_bar.min() -= starting_meas_diff.value() / MEASURES_PER_BAR; - sp_bar.min() = std::max(sp_bar.min(), 0.0); - - for (auto p = activation.act_start; p < activation.act_end; ++p) { - if (p->is_sp_granting_note) { - auto sp_note_pos = p->hit_window_start; - sp_bar = m_sp_data.propagate_sp_over_whammy(current_position, - sp_note_pos, sp_bar); - if (sp_bar.max() < 0.0) { - return {null_position, ActValidity::insufficient_sp}; - } - - auto sp_note_end_pos = p->hit_window_end; - - auto latest_point_to_hit_sp = m_sp_data.activation_end_point( - sp_note_pos, sp_note_end_pos, sp_bar.max()); - sp_bar.min() += SP_PHRASE_AMOUNT; - sp_bar.min() = std::min(sp_bar.min(), 1.0); - sp_bar = m_sp_data.propagate_sp_over_whammy( - sp_note_pos, latest_point_to_hit_sp, sp_bar); - sp_bar.max() += SP_PHRASE_AMOUNT; - sp_bar.max() = std::min(sp_bar.max(), 1.0); - - current_position = latest_point_to_hit_sp; - } - } - - auto ending_pos = activation.act_end->hit_window_start; - sp_bar = m_sp_data.propagate_sp_over_whammy(current_position, ending_pos, - sp_bar); - if (sp_bar.max() < 0.0) { - return {null_position, ActValidity::insufficient_sp}; - } - if (activation.act_end->is_sp_granting_note) { - sp_bar.add_phrase(); - } - - const auto next_point = std::next(activation.act_end); - if (next_point == m_points.cend()) { - // Return value doesn't matter other than it being non-empty. - auto pos_inf = std::numeric_limits::infinity(); - return {{Beat(pos_inf), Measure(pos_inf)}, ActValidity::success}; - } - - auto end_meas - = ending_pos.measure + Measure(sp_bar.min() * MEASURES_PER_BAR); - if (end_meas >= next_point->hit_window_end.measure) { - return {null_position, ActValidity::surplus_sp}; - } - - auto end_beat = m_converter.measures_to_beats(end_meas); - return {{end_beat, end_meas}, ActValidity::success}; -} - -std::string ProcessedSong::path_summary(const Path& path) const -{ - // We use std::stringstream instead of std::string for better formating of - // floats (measure values). - std::stringstream stream; - stream << "Path: "; - - std::vector activation_summaries; - auto start_point = m_points.cbegin(); - for (const auto& act : path.activations) { - auto sp_before - = std::count_if(start_point, act.act_start, [](const auto& p) { - return p.is_sp_granting_note; - }); - auto sp_during = std::count_if( - act.act_start, std::next(act.act_end), - [](const auto& p) { return p.is_sp_granting_note; }); - auto summary = std::to_string(sp_before); - if (sp_during != 0) { - summary += "(+"; - summary += std::to_string(sp_during); - summary += ")"; - } - activation_summaries.push_back(summary); - start_point = std::next(act.act_end); - } - - auto spare_sp - = std::count_if(start_point, m_points.cend(), - [](const auto& p) { return p.is_sp_granting_note; }); - if (spare_sp != 0) { - activation_summaries.push_back(std::string("ES") - + std::to_string(spare_sp)); - } - - if (activation_summaries.empty()) { - stream << "None"; - } else { - stream << activation_summaries[0]; - for (std::size_t i = 1; i < activation_summaries.size(); ++i) { - stream << "-" << activation_summaries[i]; - } - } - - auto no_sp_score = std::accumulate( - m_points.cbegin(), m_points.cend(), 0, - [](const auto x, const auto& y) { return x + y.value; }); - no_sp_score += m_total_solo_boost; - stream << "\nNo SP score: " << no_sp_score; - - auto total_score = no_sp_score + path.score_boost; - stream << "\nTotal score: " << total_score; - - for (std::size_t i = 0; i < path.activations.size(); ++i) { - stream << "\nActivation " << i + 1 << ": Measure " - << path.activations[i].act_start->position.measure.value() + 1 - << " to Measure " - << path.activations[i].act_end->position.measure.value() + 1; - } - - return stream.str(); -} - Optimiser::Optimiser(const ProcessedSong* song) : m_song {song} { @@ -242,6 +78,8 @@ Optimiser::CacheKey Optimiser::advance_cache_key(CacheKey key) const PointPtr Optimiser::act_end_lower_bound(PointPtr point, Measure pos, double sp_bar_amount) const { + constexpr double MEASURES_PER_BAR = 8.0; + auto end_pos = pos + Measure(MEASURES_PER_BAR * sp_bar_amount); auto q = std::find_if(point, m_song->points().cend(), [=](const auto& pt) { return pt.hit_window_end.measure > end_pos; @@ -334,6 +172,8 @@ Optimiser::try_previous_best_subpaths(CacheKey key, const Cache& cache, Optimiser::CacheValue Optimiser::find_best_subpaths(CacheKey key, Cache& cache, bool has_full_sp) const { + constexpr double MINIMUM_SP_AMOUNT = 0.5; + auto subpath_from_prev = try_previous_best_subpaths(key, cache, has_full_sp); if (subpath_from_prev) { diff --git a/src/processed.cpp b/src/processed.cpp new file mode 100644 index 00000000..6bda7011 --- /dev/null +++ b/src/processed.cpp @@ -0,0 +1,189 @@ +/* + * chopt - Star Power optimiser for Clone Hero + * Copyright (C) 2020 Raymond Wright + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include +#include +#include +#include +#include + +#include "processed.hpp" + +ProcessedSong::ProcessedSong(const NoteTrack& track, int resolution, + const SyncTrack& sync_track, double early_whammy, + double squeeze) + : m_converter {sync_track, resolution} + , m_points {track, resolution, m_converter, squeeze} + , m_sp_data {track, resolution, sync_track, early_whammy} +{ + m_total_solo_boost = std::accumulate( + track.solos().cbegin(), track.solos().cend(), 0, + [](const auto x, const auto& y) { return x + y.value; }); +} + +SpBar ProcessedSong::total_available_sp(Beat start, PointPtr first_point, + PointPtr act_start) const +{ + SpBar sp_bar {0.0, 0.0}; + for (auto p = first_point; p < act_start; ++p) { + if (p->is_sp_granting_note) { + sp_bar.add_phrase(); + } + } + + sp_bar.max() += m_sp_data.available_whammy(start, act_start->position.beat); + sp_bar.max() = std::min(sp_bar.max(), 1.0); + + return sp_bar; +} + +ActResult +ProcessedSong::is_candidate_valid(const ActivationCandidate& activation) const +{ + constexpr double MEASURES_PER_BAR = 8.0; + constexpr double MINIMUM_SP_AMOUNT = 0.5; + constexpr double SP_PHRASE_AMOUNT = 0.25; + const Position null_position {Beat(0.0), Measure(0.0)}; + + if (!activation.sp_bar.full_enough_to_activate()) { + return {null_position, ActValidity::insufficient_sp}; + } + + auto current_position = activation.act_start->hit_window_end; + + auto sp_bar = activation.sp_bar; + sp_bar.min() = std::max(sp_bar.min(), MINIMUM_SP_AMOUNT); + + auto starting_meas_diff = current_position.measure + - activation.earliest_activation_point.measure; + sp_bar.min() -= starting_meas_diff.value() / MEASURES_PER_BAR; + sp_bar.min() = std::max(sp_bar.min(), 0.0); + + for (auto p = activation.act_start; p < activation.act_end; ++p) { + if (p->is_sp_granting_note) { + auto sp_note_pos = p->hit_window_start; + sp_bar = m_sp_data.propagate_sp_over_whammy(current_position, + sp_note_pos, sp_bar); + if (sp_bar.max() < 0.0) { + return {null_position, ActValidity::insufficient_sp}; + } + + auto sp_note_end_pos = p->hit_window_end; + + auto latest_point_to_hit_sp = m_sp_data.activation_end_point( + sp_note_pos, sp_note_end_pos, sp_bar.max()); + sp_bar.min() += SP_PHRASE_AMOUNT; + sp_bar.min() = std::min(sp_bar.min(), 1.0); + sp_bar = m_sp_data.propagate_sp_over_whammy( + sp_note_pos, latest_point_to_hit_sp, sp_bar); + sp_bar.max() += SP_PHRASE_AMOUNT; + sp_bar.max() = std::min(sp_bar.max(), 1.0); + + current_position = latest_point_to_hit_sp; + } + } + + auto ending_pos = activation.act_end->hit_window_start; + sp_bar = m_sp_data.propagate_sp_over_whammy(current_position, ending_pos, + sp_bar); + if (sp_bar.max() < 0.0) { + return {null_position, ActValidity::insufficient_sp}; + } + if (activation.act_end->is_sp_granting_note) { + sp_bar.add_phrase(); + } + + const auto next_point = std::next(activation.act_end); + if (next_point == m_points.cend()) { + // Return value doesn't matter other than it being non-empty. + auto pos_inf = std::numeric_limits::infinity(); + return {{Beat(pos_inf), Measure(pos_inf)}, ActValidity::success}; + } + + auto end_meas + = ending_pos.measure + Measure(sp_bar.min() * MEASURES_PER_BAR); + if (end_meas >= next_point->hit_window_end.measure) { + return {null_position, ActValidity::surplus_sp}; + } + + auto end_beat = m_converter.measures_to_beats(end_meas); + return {{end_beat, end_meas}, ActValidity::success}; +} + +std::string ProcessedSong::path_summary(const Path& path) const +{ + // We use std::stringstream instead of std::string for better formating of + // floats (measure values). + std::stringstream stream; + stream << "Path: "; + + std::vector activation_summaries; + auto start_point = m_points.cbegin(); + for (const auto& act : path.activations) { + auto sp_before + = std::count_if(start_point, act.act_start, [](const auto& p) { + return p.is_sp_granting_note; + }); + auto sp_during = std::count_if( + act.act_start, std::next(act.act_end), + [](const auto& p) { return p.is_sp_granting_note; }); + auto summary = std::to_string(sp_before); + if (sp_during != 0) { + summary += "(+"; + summary += std::to_string(sp_during); + summary += ")"; + } + activation_summaries.push_back(summary); + start_point = std::next(act.act_end); + } + + auto spare_sp + = std::count_if(start_point, m_points.cend(), + [](const auto& p) { return p.is_sp_granting_note; }); + if (spare_sp != 0) { + activation_summaries.push_back(std::string("ES") + + std::to_string(spare_sp)); + } + + if (activation_summaries.empty()) { + stream << "None"; + } else { + stream << activation_summaries[0]; + for (std::size_t i = 1; i < activation_summaries.size(); ++i) { + stream << "-" << activation_summaries[i]; + } + } + + auto no_sp_score = std::accumulate( + m_points.cbegin(), m_points.cend(), 0, + [](const auto x, const auto& y) { return x + y.value; }); + no_sp_score += m_total_solo_boost; + stream << "\nNo SP score: " << no_sp_score; + + auto total_score = no_sp_score + path.score_boost; + stream << "\nTotal score: " << total_score; + + for (std::size_t i = 0; i < path.activations.size(); ++i) { + stream << "\nActivation " << i + 1 << ": Measure " + << path.activations[i].act_start->position.measure.value() + 1 + << " to Measure " + << path.activations[i].act_end->position.measure.value() + 1; + } + + return stream.str(); +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 2eeae31f..f4d79e04 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -3,7 +3,7 @@ set(LIBS "${PROJECT_SOURCE_DIR}/libs") set(SRC "${PROJECT_SOURCE_DIR}/src") add_library(catch_main STATIC catch_main.cpp) -target_include_directories(catch_main PRIVATE "${LIBS}") +target_include_directories(catch_main PRIVATE ${LIBS}) set_cpp_standard(catch_main) set_warnings(catch_main) @@ -20,10 +20,12 @@ package_add_test(chart_unittest chart_unittest.cpp "${SRC}/chart.cpp") package_add_test(image_unittest image_unittest.cpp "${SRC}/chart.cpp" "${SRC}/image.cpp") -package_add_test(optimiser_unittest optimiser_unittest.cpp "${SRC}/chart.cpp" "${SRC}/optimiser.cpp" "${SRC}/points.cpp" "${SRC}/sp.cpp" "${SRC}/time.cpp") +package_add_test(optimiser_unittest optimiser_unittest.cpp "${SRC}/chart.cpp" "${SRC}/optimiser.cpp" "${SRC}/points.cpp" "${SRC}/processed.cpp" "${SRC}/sp.cpp" "${SRC}/time.cpp") package_add_test(points_unittest points_unittest.cpp "${SRC}/chart.cpp" "${SRC}/points.cpp" "${SRC}/time.cpp") +package_add_test(processed_unittest processed_unittest.cpp "${SRC}/chart.cpp" "${SRC}/points.cpp" "${SRC}/processed.cpp" "${SRC}/sp.cpp" "${SRC}/time.cpp") + package_add_test(sp_unittest sp_unittest.cpp "${SRC}/chart.cpp" "${SRC}/sp.cpp" "${SRC}/time.cpp") package_add_test(time_unittest time_unittest.cpp "${SRC}/chart.cpp" "${SRC}/time.cpp") diff --git a/tests/optimiser_unittest.cpp b/tests/optimiser_unittest.cpp index 6a386cc2..5217a693 100644 --- a/tests/optimiser_unittest.cpp +++ b/tests/optimiser_unittest.cpp @@ -18,12 +18,10 @@ #include #include -#include #include "catch.hpp" #include "optimiser.hpp" -#include "points.hpp" static bool operator==(const Activation& lhs, const Activation& rhs) { @@ -31,308 +29,6 @@ static bool operator==(const Activation& lhs, const Activation& rhs) == std::tie(rhs.act_start, rhs.act_end); } -TEST_CASE("total_available_sp counts SP correctly", "Available SP") -{ - std::vector notes {{0}, {192}, {384}, {576}, - {768, 192}, {1152}, {1344}, {1536}}; - std::vector phrases {{0, 50}, {384, 50}, {768, 400}, {1344, 50}}; - NoteTrack note_track {notes, phrases, {}}; - ProcessedSong song {note_track, 192, {}, 1.0, 1.0}; - const auto& points = song.points(); - - SECTION("Phrases are counted correctly") - { - REQUIRE(song.total_available_sp(Beat(0.0), points.cbegin(), - points.cbegin() + 1) - == SpBar {0.25, 0.25}); - REQUIRE(song.total_available_sp(Beat(0.0), points.cbegin(), - points.cbegin() + 2) - == SpBar {0.25, 0.25}); - REQUIRE(song.total_available_sp(Beat(0.5), points.cbegin() + 2, - points.cbegin() + 3) - == SpBar {0.25, 0.25}); - } - - SECTION("Whammy is counted correctly") - { - auto result = song.total_available_sp(Beat(4.0), points.cbegin() + 4, - points.cbegin() + 5); - REQUIRE(result.min() == Approx(0.0)); - REQUIRE(result.max() == Approx(0.00121528)); - } - - SECTION("Whammy is counted correctly even started mid hold") - { - auto result = song.total_available_sp(Beat(4.5), points.cend() - 3, - points.cend() - 3); - REQUIRE(result.min() == Approx(0.0)); - REQUIRE(result.max() == Approx(0.0166667)); - } - - SECTION("SP does not exceed full bar") - { - REQUIRE(song.total_available_sp(Beat(0.0), points.cbegin(), - points.cend() - 1) - == SpBar {1.0, 1.0}); - } - - SECTION("SP notes are counted from first_point when start is past middle") - { - REQUIRE(song.total_available_sp(Beat(0.05), points.cbegin(), - points.cbegin() + 1) - == SpBar {0.25, 0.25}); - } -} - -TEST_CASE("is_candidate_valid works with no whammy", "Valid no whammy acts") -{ - std::vector notes {{0}, {1536}, {3072}, {6144}}; - NoteTrack note_track {notes, {}, {}}; - ProcessedSong track {note_track, 192, {}, 1.0, 1.0}; - const auto& points = track.points(); - ActivationCandidate candidate {points.cbegin(), - points.cbegin() + 3, - {Beat(0.0), Measure(0.0)}, - {1.0, 1.0}}; - ProcessedSong second_track {note_track, 192, SyncTrack({{0, 3, 4}}, {}), - 1.0, 1.0}; - const auto& second_points = second_track.points(); - ActivationCandidate second_candidate {second_points.cbegin(), - second_points.cbegin() + 3, - {Beat(0.0), Measure(0.0)}, - {1.0, 1.0}}; - - SECTION("Full bar works with time signatures") - { - REQUIRE(track.is_candidate_valid(candidate).validity - == ActValidity::success); - REQUIRE(second_track.is_candidate_valid(second_candidate).validity - == ActValidity::insufficient_sp); - } - - SECTION("Half bar works with time signatures") - { - candidate.act_end = points.cbegin() + 2; - candidate.sp_bar = {0.5, 0.5}; - second_candidate.act_end = second_points.cbegin() + 2; - second_candidate.sp_bar = {0.5, 0.5}; - - REQUIRE(track.is_candidate_valid(candidate).validity - == ActValidity::success); - REQUIRE(second_track.is_candidate_valid(second_candidate).validity - == ActValidity::insufficient_sp); - } - - SECTION("Below half bar never works") - { - candidate.act_end = points.cbegin() + 1; - candidate.sp_bar.max() = 0.25; - - REQUIRE(track.is_candidate_valid(candidate).validity - == ActValidity::insufficient_sp); - } - - SECTION("Check next point needs to not lie in activation") - { - candidate.act_end = points.cbegin() + 1; - candidate.sp_bar.max() = 0.6; - - REQUIRE(track.is_candidate_valid(candidate).validity - == ActValidity::surplus_sp); - } - - SECTION("Check intermediate SP is accounted for") - { - std::vector phrases {{3000, 100}}; - NoteTrack overlap_notes {notes, phrases, {}}; - ProcessedSong overlap_track {overlap_notes, 192, {}, 1.0, 1.0}; - const auto& overlap_points = overlap_track.points(); - ActivationCandidate overlap_candidate {overlap_points.cbegin(), - overlap_points.cbegin() + 3, - {Beat(0.0), Measure(0.0)}, - {0.8, 0.8}}; - - REQUIRE(overlap_track.is_candidate_valid(overlap_candidate).validity - == ActValidity::success); - } - - SECTION("Check only reached intermediate SP is accounted for") - { - notes[2].position = 6000; - std::vector phrases {{6000, 100}}; - NoteTrack overlap_notes {notes, phrases, {}}; - ProcessedSong overlap_track {overlap_notes, 192, {}, 1.0, 1.0}; - const auto& overlap_points = overlap_track.points(); - ActivationCandidate overlap_candidate {overlap_points.cbegin(), - overlap_points.cbegin() + 3, - {Beat(0.0), Measure(0.0)}, - {0.8, 0.8}}; - - REQUIRE(overlap_track.is_candidate_valid(overlap_candidate).validity - == ActValidity::insufficient_sp); - } - - SECTION("Last note's SP status is not ignored") - { - notes[3].position = 4000; - std::vector phrases {{3072, 100}}; - NoteTrack overlap_notes {notes, phrases, {}}; - ProcessedSong overlap_track {overlap_notes, 192, {}, 1.0, 1.0}; - const auto& overlap_points = overlap_track.points(); - ActivationCandidate overlap_candidate {overlap_points.cbegin(), - overlap_points.cbegin() + 2, - {Beat(0.0), Measure(0.0)}, - {0.5, 0.5}}; - - REQUIRE(overlap_track.is_candidate_valid(overlap_candidate).validity - == ActValidity::surplus_sp); - } - - SECTION("SP bar does not exceed full bar") - { - std::vector overlap_notes {{0}, {2}, {7000}}; - std::vector phrases {{0, 1}, {2, 1}}; - NoteTrack overlap_note_track {overlap_notes, phrases, {}}; - ProcessedSong overlap_track {overlap_note_track, 192, {}, 1.0, 1.0}; - const auto& overlap_points = overlap_track.points(); - ActivationCandidate overlap_candidate {overlap_points.cbegin(), - overlap_points.cbegin() + 2, - {Beat(0.0), Measure(0.0)}, - {1.0, 1.0}}; - - REQUIRE(overlap_track.is_candidate_valid(overlap_candidate).validity - == ActValidity::insufficient_sp); - } - - SECTION("Earliest activation point is considered") - { - candidate.act_end = points.cbegin() + 1; - candidate.sp_bar = {0.53125, 0.53125}; - candidate.earliest_activation_point = {Beat(-2.0), Measure(-0.5)}; - - REQUIRE(track.is_candidate_valid(candidate).validity - == ActValidity::success); - } -} - -TEST_CASE("is_candidate_valid works with whammy", "Valid whammy acts") -{ - std::vector notes {{0, 960}, {3840}, {6144}}; - std::vector phrases {{0, 7000}}; - NoteTrack note_track {notes, phrases, {}}; - ProcessedSong track {note_track, 192, {}, 1.0, 1.0}; - const auto& points = track.points(); - ActivationCandidate candidate {points.cbegin(), - points.cend() - 2, - {Beat(0.0), Measure(0.0)}, - {0.5, 0.5}}; - - SECTION("Check whammy is counted") - { - REQUIRE(track.is_candidate_valid(candidate).validity - == ActValidity::success); - } - - SECTION("Check compressed activations are counted") - { - candidate.sp_bar.max() = 0.9; - REQUIRE(track.is_candidate_valid(candidate).validity - == ActValidity::success); - } -} - -TEST_CASE("is_candidate_valid takes into account minimum SP", "Min SP") -{ - std::vector notes {{0}, {1536}, {2304}, {3072}, {4608}}; - NoteTrack note_track {notes, {}, {}}; - ProcessedSong track {note_track, 192, {}, 1.0, 1.0}; - const auto& points = track.points(); - ActivationCandidate candidate {points.cbegin(), - points.cbegin() + 3, - {Beat(0.0), Measure(0.0)}, - {0.5, 1.0}}; - - SECTION("Lower SP is considered") - { - REQUIRE(track.is_candidate_valid(candidate).validity - == ActValidity::success); - } - - SECTION("Lower SP is only considered down to a half-bar") - { - candidate.act_end = points.cbegin() + 1; - candidate.sp_bar.min() = 0.25; - - REQUIRE(track.is_candidate_valid(candidate).validity - == ActValidity::surplus_sp); - } -} - -TEST_CASE("is_candidate_valid takes into account squeezing", "Valid squeezes") -{ - SECTION("Front end and back end of the activation endpoints are considered") - { - std::vector notes {{0}, {3110}}; - NoteTrack note_track {notes, {}, {}}; - ProcessedSong track {note_track, 192, {}, 1.0, 1.0}; - const auto& points = track.points(); - ActivationCandidate candidate {points.cbegin(), - points.cbegin() + 1, - {Beat(0.0), Measure(0.0)}, - {0.5, 0.5}}; - - REQUIRE(track.is_candidate_valid(candidate).validity - == ActValidity::success); - } - - SECTION("Next note can be squeezed late to avoid going too far") - { - std::vector notes {{0}, {3034}, {3053}}; - NoteTrack note_track {notes, {}, {}}; - ProcessedSong track {note_track, 192, {}, 1.0, 1.0}; - const auto& points = track.points(); - ActivationCandidate candidate {points.cbegin(), - points.cbegin() + 1, - {Beat(0.0), Measure(0.0)}, - {0.5, 0.5}}; - - REQUIRE(track.is_candidate_valid(candidate).validity - == ActValidity::success); - } - - SECTION("Intermediate SP can be hit early") - { - std::vector notes {{0}, {3102}, {4608}}; - std::vector phrases {{3100, 100}}; - NoteTrack note_track {notes, phrases, {}}; - ProcessedSong track {note_track, 192, {}, 1.0, 1.0}; - const auto& points = track.points(); - ActivationCandidate candidate {points.cbegin(), - points.cbegin() + 2, - {Beat(0.0), Measure(0.0)}, - {0.5, 0.5}}; - - REQUIRE(track.is_candidate_valid(candidate).validity - == ActValidity::success); - } - - SECTION("Intermediate SP can be hit late") - { - std::vector notes {{0}, {768}, {6942}}; - std::vector phrases {{768, 100}}; - NoteTrack note_track {notes, phrases, {}}; - ProcessedSong track {note_track, 192, {}, 1.0, 1.0}; - const auto& points = track.points(); - ActivationCandidate candidate {points.cbegin(), - points.cbegin() + 2, - {Beat(0.0), Measure(0.0)}, - {1.0, 1.0}}; - - REQUIRE(track.is_candidate_valid(candidate).validity - == ActValidity::success); - } -} - TEST_CASE("path_summary produces the correct output", "Path summary") { std::vector notes {{0}, {192}, {384}, {576}, {6144}}; diff --git a/tests/processed_unittest.cpp b/tests/processed_unittest.cpp new file mode 100644 index 00000000..9fa74dd8 --- /dev/null +++ b/tests/processed_unittest.cpp @@ -0,0 +1,323 @@ +/* + * chopt - Star Power optimiser for Clone Hero + * Copyright (C) 2020 Raymond Wright + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "catch.hpp" + +#include "processed.hpp" + +TEST_CASE("total_available_sp counts SP correctly", "Available SP") +{ + std::vector notes {{0}, {192}, {384}, {576}, + {768, 192}, {1152}, {1344}, {1536}}; + std::vector phrases {{0, 50}, {384, 50}, {768, 400}, {1344, 50}}; + NoteTrack note_track {notes, phrases, {}}; + ProcessedSong song {note_track, 192, {}, 1.0, 1.0}; + const auto& points = song.points(); + + SECTION("Phrases are counted correctly") + { + REQUIRE(song.total_available_sp(Beat(0.0), points.cbegin(), + points.cbegin() + 1) + == SpBar {0.25, 0.25}); + REQUIRE(song.total_available_sp(Beat(0.0), points.cbegin(), + points.cbegin() + 2) + == SpBar {0.25, 0.25}); + REQUIRE(song.total_available_sp(Beat(0.5), points.cbegin() + 2, + points.cbegin() + 3) + == SpBar {0.25, 0.25}); + } + + SECTION("Whammy is counted correctly") + { + auto result = song.total_available_sp(Beat(4.0), points.cbegin() + 4, + points.cbegin() + 5); + REQUIRE(result.min() == Approx(0.0)); + REQUIRE(result.max() == Approx(0.00121528)); + } + + SECTION("Whammy is counted correctly even started mid hold") + { + auto result = song.total_available_sp(Beat(4.5), points.cend() - 3, + points.cend() - 3); + REQUIRE(result.min() == Approx(0.0)); + REQUIRE(result.max() == Approx(0.0166667)); + } + + SECTION("SP does not exceed full bar") + { + REQUIRE(song.total_available_sp(Beat(0.0), points.cbegin(), + points.cend() - 1) + == SpBar {1.0, 1.0}); + } + + SECTION("SP notes are counted from first_point when start is past middle") + { + REQUIRE(song.total_available_sp(Beat(0.05), points.cbegin(), + points.cbegin() + 1) + == SpBar {0.25, 0.25}); + } +} + +TEST_CASE("is_candidate_valid works with no whammy", "Valid no whammy acts") +{ + std::vector notes {{0}, {1536}, {3072}, {6144}}; + NoteTrack note_track {notes, {}, {}}; + ProcessedSong track {note_track, 192, {}, 1.0, 1.0}; + const auto& points = track.points(); + ActivationCandidate candidate {points.cbegin(), + points.cbegin() + 3, + {Beat(0.0), Measure(0.0)}, + {1.0, 1.0}}; + ProcessedSong second_track {note_track, 192, SyncTrack({{0, 3, 4}}, {}), + 1.0, 1.0}; + const auto& second_points = second_track.points(); + ActivationCandidate second_candidate {second_points.cbegin(), + second_points.cbegin() + 3, + {Beat(0.0), Measure(0.0)}, + {1.0, 1.0}}; + + SECTION("Full bar works with time signatures") + { + REQUIRE(track.is_candidate_valid(candidate).validity + == ActValidity::success); + REQUIRE(second_track.is_candidate_valid(second_candidate).validity + == ActValidity::insufficient_sp); + } + + SECTION("Half bar works with time signatures") + { + candidate.act_end = points.cbegin() + 2; + candidate.sp_bar = {0.5, 0.5}; + second_candidate.act_end = second_points.cbegin() + 2; + second_candidate.sp_bar = {0.5, 0.5}; + + REQUIRE(track.is_candidate_valid(candidate).validity + == ActValidity::success); + REQUIRE(second_track.is_candidate_valid(second_candidate).validity + == ActValidity::insufficient_sp); + } + + SECTION("Below half bar never works") + { + candidate.act_end = points.cbegin() + 1; + candidate.sp_bar.max() = 0.25; + + REQUIRE(track.is_candidate_valid(candidate).validity + == ActValidity::insufficient_sp); + } + + SECTION("Check next point needs to not lie in activation") + { + candidate.act_end = points.cbegin() + 1; + candidate.sp_bar.max() = 0.6; + + REQUIRE(track.is_candidate_valid(candidate).validity + == ActValidity::surplus_sp); + } + + SECTION("Check intermediate SP is accounted for") + { + std::vector phrases {{3000, 100}}; + NoteTrack overlap_notes {notes, phrases, {}}; + ProcessedSong overlap_track {overlap_notes, 192, {}, 1.0, 1.0}; + const auto& overlap_points = overlap_track.points(); + ActivationCandidate overlap_candidate {overlap_points.cbegin(), + overlap_points.cbegin() + 3, + {Beat(0.0), Measure(0.0)}, + {0.8, 0.8}}; + + REQUIRE(overlap_track.is_candidate_valid(overlap_candidate).validity + == ActValidity::success); + } + + SECTION("Check only reached intermediate SP is accounted for") + { + notes[2].position = 6000; + std::vector phrases {{6000, 100}}; + NoteTrack overlap_notes {notes, phrases, {}}; + ProcessedSong overlap_track {overlap_notes, 192, {}, 1.0, 1.0}; + const auto& overlap_points = overlap_track.points(); + ActivationCandidate overlap_candidate {overlap_points.cbegin(), + overlap_points.cbegin() + 3, + {Beat(0.0), Measure(0.0)}, + {0.8, 0.8}}; + + REQUIRE(overlap_track.is_candidate_valid(overlap_candidate).validity + == ActValidity::insufficient_sp); + } + + SECTION("Last note's SP status is not ignored") + { + notes[3].position = 4000; + std::vector phrases {{3072, 100}}; + NoteTrack overlap_notes {notes, phrases, {}}; + ProcessedSong overlap_track {overlap_notes, 192, {}, 1.0, 1.0}; + const auto& overlap_points = overlap_track.points(); + ActivationCandidate overlap_candidate {overlap_points.cbegin(), + overlap_points.cbegin() + 2, + {Beat(0.0), Measure(0.0)}, + {0.5, 0.5}}; + + REQUIRE(overlap_track.is_candidate_valid(overlap_candidate).validity + == ActValidity::surplus_sp); + } + + SECTION("SP bar does not exceed full bar") + { + std::vector overlap_notes {{0}, {2}, {7000}}; + std::vector phrases {{0, 1}, {2, 1}}; + NoteTrack overlap_note_track {overlap_notes, phrases, {}}; + ProcessedSong overlap_track {overlap_note_track, 192, {}, 1.0, 1.0}; + const auto& overlap_points = overlap_track.points(); + ActivationCandidate overlap_candidate {overlap_points.cbegin(), + overlap_points.cbegin() + 2, + {Beat(0.0), Measure(0.0)}, + {1.0, 1.0}}; + + REQUIRE(overlap_track.is_candidate_valid(overlap_candidate).validity + == ActValidity::insufficient_sp); + } + + SECTION("Earliest activation point is considered") + { + candidate.act_end = points.cbegin() + 1; + candidate.sp_bar = {0.53125, 0.53125}; + candidate.earliest_activation_point = {Beat(-2.0), Measure(-0.5)}; + + REQUIRE(track.is_candidate_valid(candidate).validity + == ActValidity::success); + } +} + +TEST_CASE("is_candidate_valid works with whammy", "Valid whammy acts") +{ + std::vector notes {{0, 960}, {3840}, {6144}}; + std::vector phrases {{0, 7000}}; + NoteTrack note_track {notes, phrases, {}}; + ProcessedSong track {note_track, 192, {}, 1.0, 1.0}; + const auto& points = track.points(); + ActivationCandidate candidate {points.cbegin(), + points.cend() - 2, + {Beat(0.0), Measure(0.0)}, + {0.5, 0.5}}; + + SECTION("Check whammy is counted") + { + REQUIRE(track.is_candidate_valid(candidate).validity + == ActValidity::success); + } + + SECTION("Check compressed activations are counted") + { + candidate.sp_bar.max() = 0.9; + REQUIRE(track.is_candidate_valid(candidate).validity + == ActValidity::success); + } +} + +TEST_CASE("is_candidate_valid takes into account minimum SP", "Min SP") +{ + std::vector notes {{0}, {1536}, {2304}, {3072}, {4608}}; + NoteTrack note_track {notes, {}, {}}; + ProcessedSong track {note_track, 192, {}, 1.0, 1.0}; + const auto& points = track.points(); + ActivationCandidate candidate {points.cbegin(), + points.cbegin() + 3, + {Beat(0.0), Measure(0.0)}, + {0.5, 1.0}}; + + SECTION("Lower SP is considered") + { + REQUIRE(track.is_candidate_valid(candidate).validity + == ActValidity::success); + } + + SECTION("Lower SP is only considered down to a half-bar") + { + candidate.act_end = points.cbegin() + 1; + candidate.sp_bar.min() = 0.25; + + REQUIRE(track.is_candidate_valid(candidate).validity + == ActValidity::surplus_sp); + } +} + +TEST_CASE("is_candidate_valid takes into account squeezing", "Valid squeezes") +{ + SECTION("Front end and back end of the activation endpoints are considered") + { + std::vector notes {{0}, {3110}}; + NoteTrack note_track {notes, {}, {}}; + ProcessedSong track {note_track, 192, {}, 1.0, 1.0}; + const auto& points = track.points(); + ActivationCandidate candidate {points.cbegin(), + points.cbegin() + 1, + {Beat(0.0), Measure(0.0)}, + {0.5, 0.5}}; + + REQUIRE(track.is_candidate_valid(candidate).validity + == ActValidity::success); + } + + SECTION("Next note can be squeezed late to avoid going too far") + { + std::vector notes {{0}, {3034}, {3053}}; + NoteTrack note_track {notes, {}, {}}; + ProcessedSong track {note_track, 192, {}, 1.0, 1.0}; + const auto& points = track.points(); + ActivationCandidate candidate {points.cbegin(), + points.cbegin() + 1, + {Beat(0.0), Measure(0.0)}, + {0.5, 0.5}}; + + REQUIRE(track.is_candidate_valid(candidate).validity + == ActValidity::success); + } + + SECTION("Intermediate SP can be hit early") + { + std::vector notes {{0}, {3102}, {4608}}; + std::vector phrases {{3100, 100}}; + NoteTrack note_track {notes, phrases, {}}; + ProcessedSong track {note_track, 192, {}, 1.0, 1.0}; + const auto& points = track.points(); + ActivationCandidate candidate {points.cbegin(), + points.cbegin() + 2, + {Beat(0.0), Measure(0.0)}, + {0.5, 0.5}}; + + REQUIRE(track.is_candidate_valid(candidate).validity + == ActValidity::success); + } + + SECTION("Intermediate SP can be hit late") + { + std::vector notes {{0}, {768}, {6942}}; + std::vector phrases {{768, 100}}; + NoteTrack note_track {notes, phrases, {}}; + ProcessedSong track {note_track, 192, {}, 1.0, 1.0}; + const auto& points = track.points(); + ActivationCandidate candidate {points.cbegin(), + points.cbegin() + 2, + {Beat(0.0), Measure(0.0)}, + {1.0, 1.0}}; + + REQUIRE(track.is_candidate_valid(candidate).validity + == ActValidity::success); + } +}