#include "state_manager.hpp" #include "graph_distances.hpp" #include "util.hpp" auto state_manager::synced_try_insert_state(const puzzle& state) -> size_t { if (state_indices.contains(state)) { return state_indices[state]; } const size_t index = state_pool.size(); state_pool.emplace_back(state); state_indices.emplace(state, index); visit_counts[index] = 0; // Queue an update to the physics engine state to keep in sync physics.add_mass_cmd(); return index; } auto state_manager::synced_insert_link(size_t first_index, size_t second_index) -> void { links.emplace_back(first_index, second_index); // Queue an update to the physics engine state to keep in sync physics.add_spring_cmd(first_index, second_index); } auto state_manager::synced_insert_statespace(const std::vector& states, const std::vector>& _links) -> void { if (!state_pool.empty() || !state_indices.empty() || !links.empty()) { warnln("Inserting statespace but collections haven't been cleared"); } for (const puzzle& state : states) { const size_t index = state_pool.size(); state_pool.emplace_back(state); state_indices.emplace(state, index); visit_counts[index] = 0; } for (const auto& [from, to] : _links) { links.emplace_back(from, to); } // Queue an update to the physics engine state to keep in sync physics.add_mass_springs_cmd(state_pool.size(), links); } auto state_manager::synced_clear_statespace() -> void { // Those are invalid without any states current_state_index = -1; previous_state_index = -1; starting_state_index = -1; state_pool.clear(); state_indices.clear(); links.clear(); node_target_distances.clear(); winning_indices.clear(); winning_path.clear(); path_indices.clear(); move_history.clear(); visit_counts.clear(); // Queue an update to the physics engine state to keep in sync physics.clear_cmd(); } auto state_manager::save_current_to_preset_file(const std::string& preset_comment) -> void { if (append_preset_file(preset_file, preset_comment, get_current_state())) { current_preset = preset_states.size(); reload_preset_file(); } } auto state_manager::reload_preset_file() -> void { const auto [presets, comments] = parse_preset_file(preset_file); if (!presets.empty()) { preset_states = presets; preset_comments = comments; } load_preset(current_preset); } auto state_manager::load_preset(const size_t preset) -> void { clear_graph_and_add_current(preset_states[preset]); current_preset = preset; edited = false; } auto state_manager::load_previous_preset() -> void { load_preset((preset_states.size() + current_preset - 1) % preset_states.size()); } auto state_manager::load_next_preset() -> void { load_preset((current_preset + 1) % preset_states.size()); } auto state_manager::update_current_state(const puzzle& p) -> void { if (!p.valid()) { return; } const size_t size_before = state_pool.size(); // If state is a duplicate, index will be the existing index, // if state is new, index will be state_pool.size() - 1 const size_t index = synced_try_insert_state(p); // Because synced_insert_link does not check for duplicates we do it here, // if the size grows, it was not a duplicate, and we can add the spring if (state_pool.size() > size_before) { // The order is important, as the position of the second mass will be updated depending on // the first synced_insert_link(current_state_index, index); } previous_state_index = current_state_index; current_state_index = index; if (current_state_index != previous_state_index) { move_history.emplace_back(previous_state_index); } if (p.goal_reached()) { winning_indices.insert(current_state_index); } // Adds the element with 0 if it doesn't exist visit_counts[current_state_index]++; total_moves++; // Recalculate distances only if the graph changed if (state_pool.size() > size_before) { populate_node_target_distances(); } populate_winning_path(); } auto state_manager::edit_starting_state(const puzzle& p) -> void { clear_graph_and_add_current(p); move_history.clear(); total_moves = 0; for (int& visits : visit_counts | std::views::values) { visits = 0; } visit_counts[current_state_index]++; edited = true; } auto state_manager::goto_starting_state() -> void { update_current_state(get_state(starting_state_index)); // Reset previous movement data since we're starting over (because we're fucking stupid) previous_state_index = current_state_index; for (int& visits : visit_counts | std::views::values) { visits = 0; } visit_counts[current_state_index]++; move_history.clear(); total_moves = 0; } auto state_manager::goto_optimal_next_state() -> void { if (node_target_distances.empty()) { return; } // Already there if (node_target_distances.distances[current_state_index] == 0) { return; } const size_t parent_index = node_target_distances.parents[current_state_index]; update_current_state(get_state(parent_index)); } auto state_manager::goto_previous_state() -> void { if (move_history.empty()) { return; } update_current_state(get_state(move_history.back())); // Pop twice because update_current_state adds the state again... move_history.pop_back(); move_history.pop_back(); } auto state_manager::goto_most_distant_state() -> void { if (node_target_distances.empty()) { return; } int max_distance = 0; size_t max_distance_index = 0; for (size_t i = 0; i < node_target_distances.distances.size(); ++i) { if (node_target_distances.distances[i] > max_distance) { max_distance = node_target_distances.distances[i]; max_distance_index = i; } } update_current_state(get_state(max_distance_index)); } auto state_manager::goto_closest_target_state() -> void { if (node_target_distances.empty()) { return; } update_current_state(get_state(node_target_distances.nearest_targets[current_state_index])); } auto state_manager::populate_graph() -> void { #ifdef TRACY ZoneScoped; #endif // Need to make a copy before clearing the state_pool const puzzle s = get_starting_state(); const puzzle p = get_current_state(); // Clear the graph first so we don't add duplicates somehow synced_clear_statespace(); // Explore the entire statespace starting from the current state const std::chrono::high_resolution_clock::time_point start = std::chrono::high_resolution_clock::now(); const auto& [states, _links] = s.explore_state_space(); synced_insert_statespace(states, _links); const std::chrono::high_resolution_clock::time_point end = std::chrono::high_resolution_clock::now(); infoln("Explored puzzle. Took {}ms.", std::chrono::duration_cast(end - start).count()); infoln("State space has size {} with {} transitions.", state_pool.size(), links.size()); current_state_index = state_indices[p]; previous_state_index = current_state_index; starting_state_index = state_indices[s]; // Search for cool stuff populate_winning_indices(); populate_node_target_distances(); populate_winning_path(); } auto state_manager::clear_graph_and_add_current(const puzzle& p) -> void { // We need to make a copy before clearing the state_pool const puzzle _p = p; // NOLINT(*-unnecessary-copy-initialization) synced_clear_statespace(); // Re-add the current state current_state_index = synced_try_insert_state(_p); // These states are no longer in the graph previous_state_index = current_state_index; starting_state_index = current_state_index; visit_counts[current_state_index]++; } auto state_manager::clear_graph_and_add_current() -> void { clear_graph_and_add_current(get_current_state()); } auto state_manager::populate_winning_indices() -> void { winning_indices.clear(); for (const auto& [state, index] : state_indices) { if (state.goal_reached()) { winning_indices.insert(index); } } } auto state_manager::populate_node_target_distances() -> void { #ifdef TRACY ZoneScoped; #endif if (links.empty() || winning_indices.empty()) { return; } const std::vector targets(winning_indices.begin(), winning_indices.end()); node_target_distances.calculate_distances(state_pool.size(), links, targets); } auto state_manager::populate_winning_path() -> void { if (node_target_distances.empty()) { return; } winning_path = node_target_distances.get_shortest_path(current_state_index); path_indices.clear(); for (const size_t index : winning_path) { path_indices.insert(index); } } auto state_manager::get_index(const puzzle& state) const -> size_t { return state_indices.at(state); } auto state_manager::get_current_index() const -> size_t { return current_state_index; } auto state_manager::get_starting_index() const -> size_t { return starting_state_index; } auto state_manager::get_state(const size_t index) const -> const puzzle& { return state_pool[index]; } auto state_manager::get_current_state() const -> const puzzle& { return get_state(current_state_index); } auto state_manager::get_starting_state() const -> const puzzle& { return get_state(starting_state_index); } auto state_manager::get_state_count() const -> size_t { return state_pool.size(); } auto state_manager::get_target_count() const -> size_t { return winning_indices.size(); } auto state_manager::get_link_count() const -> size_t { return links.size(); } auto state_manager::get_path_length() const -> size_t { return winning_path.size(); } auto state_manager::get_links() const -> const std::vector>& { return links; } auto state_manager::get_winning_indices() const -> const boost::unordered_flat_set& { return winning_indices; } auto state_manager::get_visit_counts() const -> const boost::unordered_flat_map& { return visit_counts; } auto state_manager::get_winning_path() const -> const std::vector& { return winning_path; } auto state_manager::get_path_indices() const -> const boost::unordered_flat_set& { return path_indices; } auto state_manager::get_current_visits() const -> int { return visit_counts.at(current_state_index); } auto state_manager::get_current_preset() const -> size_t { return current_preset; } auto state_manager::get_preset_count() const -> size_t { return preset_states.size(); } auto state_manager::get_current_preset_comment() const -> const std::string& { return preset_comments[current_preset]; } auto state_manager::has_history() const -> bool { return !move_history.empty(); } auto state_manager::has_distances() const -> bool { return !node_target_distances.empty(); } auto state_manager::get_total_moves() const -> size_t { return total_moves; } auto state_manager::was_edited() const -> bool { return edited; }