mirror of
				https://github.com/luanti-org/luanti.git
				synced 2025-11-04 09:15:29 +01:00 
			
		
		
		
	Throw Hocroft-Karp onto shapeless recipes
This commit is contained in:
		
							
								
								
									
										177
									
								
								src/craftdef.cpp
									
									
									
									
									
								
							
							
						
						
									
										177
									
								
								src/craftdef.cpp
									
									
									
									
									
								
							@@ -493,6 +493,113 @@ std::string CraftDefinitionShapeless::getName() const
 | 
			
		||||
	return "shapeless";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
constexpr u16 SHAPELESS_GROUPS_MAX = 30000;
 | 
			
		||||
 | 
			
		||||
// Checks if there's a matching that matches all nodes in a given bipartite graph.
 | 
			
		||||
// bip_graph has graph_size nodes on each side. It is stored as list of lists of
 | 
			
		||||
// neighbors from one side.
 | 
			
		||||
// See https://en.wikipedia.org/w/index.php?title=Hopcroft-Karp_algorithm for
 | 
			
		||||
// details.
 | 
			
		||||
static bool hopcroft_karp_can_match_all(const std::vector<std::vector<u16>> &bip_graph)
 | 
			
		||||
{
 | 
			
		||||
	assert(bip_graph.size() <= SHAPELESS_GROUPS_MAX);
 | 
			
		||||
	u16 graph_size = bip_graph.size();
 | 
			
		||||
	const u16 nil = graph_size; // nil / dummy index
 | 
			
		||||
	constexpr u16 inf = UINT16_MAX; // bigger than any path length (> SHAPELESS_GROUPS_MAX * 2)
 | 
			
		||||
 | 
			
		||||
	auto pair_u = std::make_unique<u16[]>(graph_size + 1); // for each u (or nil) the matched v (or nil)
 | 
			
		||||
	auto pair_v = std::make_unique<u16[]>(graph_size + 1); // for each v (or nil) the matched u (or nil)
 | 
			
		||||
	auto dist = std::make_unique<u16[]>(graph_size + 1); // for each u (or nil) the bfs distance
 | 
			
		||||
	u16 num_matched;
 | 
			
		||||
	std::queue<u16> queue{};
 | 
			
		||||
 | 
			
		||||
	// calculates distances from unmatched nodes for augmentation paths until
 | 
			
		||||
	// dummy is reached
 | 
			
		||||
	// returns false if dummy can't be reached (and hence there are no further
 | 
			
		||||
	// augmentation paths)
 | 
			
		||||
	auto do_bfs = [&]() {
 | 
			
		||||
		assert(queue.empty());
 | 
			
		||||
 | 
			
		||||
		// enqueue all unmatched, give inf dist to the rest
 | 
			
		||||
		for (u16 u = 0; u < graph_size; ++u) {
 | 
			
		||||
			if (pair_u[u] == nil) {
 | 
			
		||||
				dist[u] = 0;
 | 
			
		||||
				queue.push(u);
 | 
			
		||||
			} else {
 | 
			
		||||
				dist[u] = inf;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		dist[nil] = inf;
 | 
			
		||||
 | 
			
		||||
		while (!queue.empty()) {
 | 
			
		||||
			u16 u = queue.front();
 | 
			
		||||
			queue.pop();
 | 
			
		||||
 | 
			
		||||
			if (dist[u] < dist[nil]) { // if dummy not yet reached
 | 
			
		||||
				for (u16 v : bip_graph[u]) { // for all adjanced of u
 | 
			
		||||
					u16 u_back = pair_v[v];
 | 
			
		||||
					// if u_back unvisited, go there
 | 
			
		||||
					if (dist[u_back] == inf) {
 | 
			
		||||
						dist[u_back] = dist[u] + 1;
 | 
			
		||||
						queue.push(u_back);
 | 
			
		||||
					}
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		return dist[nil] != inf;
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	// tries to find an augmenting path from u to the dummy
 | 
			
		||||
	// if successful, swaps all edges along path and returns true
 | 
			
		||||
	// otherwise returns false
 | 
			
		||||
	auto do_dfs_raw = [&](u16 u, auto &&recurse) -> bool {
 | 
			
		||||
		if (u == nil) // dummy => dest reached
 | 
			
		||||
			return true;
 | 
			
		||||
 | 
			
		||||
		for (u16 v : bip_graph[u]) { // for all adjanced of u
 | 
			
		||||
			u16 u_back = pair_v[v];
 | 
			
		||||
			// only walk according to bfs dists
 | 
			
		||||
			if (dist[u_back] != dist[u] + 1)
 | 
			
		||||
				continue;
 | 
			
		||||
 | 
			
		||||
			// if walk along u_back reached dummy, swap edges and backtrack
 | 
			
		||||
			if (recurse(u_back, recurse)) {
 | 
			
		||||
				pair_v[v] = u;
 | 
			
		||||
				pair_u[u] = v;
 | 
			
		||||
				return true;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		// unsuccessful path, don't walk here again
 | 
			
		||||
		dist[u] = inf;
 | 
			
		||||
		return false;
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	auto do_dfs = [&](u16 u) {
 | 
			
		||||
		return do_dfs_raw(u, do_dfs_raw);
 | 
			
		||||
	};
 | 
			
		||||
 | 
			
		||||
	// everyone starts as matched to dummy
 | 
			
		||||
	std::fill_n(&pair_u[0], graph_size + 1, nil);
 | 
			
		||||
	std::fill_n(&pair_v[0], graph_size + 1, nil);
 | 
			
		||||
 | 
			
		||||
	num_matched = 0;
 | 
			
		||||
 | 
			
		||||
	while (do_bfs()) {
 | 
			
		||||
		// try to match unmatched u nodes
 | 
			
		||||
		for (u16 u = 0; u < graph_size; ++u) {
 | 
			
		||||
			if (pair_u[u] == nil) {
 | 
			
		||||
				if (do_dfs(u)) {
 | 
			
		||||
					num_matched += 1;
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return num_matched == graph_size;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool CraftDefinitionShapeless::check(const CraftInput &input, IGameDef *gamedef) const
 | 
			
		||||
{
 | 
			
		||||
	if (input.method != CRAFT_METHOD_NORMAL)
 | 
			
		||||
@@ -513,35 +620,61 @@ bool CraftDefinitionShapeless::check(const CraftInput &input, IGameDef *gamedef)
 | 
			
		||||
		return false;
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Sort input and recipe
 | 
			
		||||
	std::sort(input_filtered.begin(), input_filtered.end());
 | 
			
		||||
 | 
			
		||||
	std::vector<std::string> recipe_copy;
 | 
			
		||||
	if (hash_inited)
 | 
			
		||||
	if (hash_inited) {
 | 
			
		||||
		recipe_copy = recipe_names;
 | 
			
		||||
	else {
 | 
			
		||||
	} else {
 | 
			
		||||
		recipe_copy = craftGetItemNames(recipe, gamedef);
 | 
			
		||||
		std::sort(recipe_copy.begin(), recipe_copy.end());
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Try with all permutations of the recipe,
 | 
			
		||||
	// start from the lexicographically first permutation (=sorted),
 | 
			
		||||
	// recipe_names is pre-sorted
 | 
			
		||||
	do {
 | 
			
		||||
		// If all items match, the recipe matches
 | 
			
		||||
		bool all_match = true;
 | 
			
		||||
		//dstream<<"Testing recipe (output="<<output<<"):";
 | 
			
		||||
		for (size_t i=0; i<recipe.size(); i++) {
 | 
			
		||||
			//dstream<<" ("<<input_filtered[i]<<" == "<<recipe_copy[i]<<")";
 | 
			
		||||
			if (!inputItemMatchesRecipe(input_filtered[i], recipe_copy[i],
 | 
			
		||||
					gamedef->idef())) {
 | 
			
		||||
				all_match = false;
 | 
			
		||||
				break;
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
		//dstream<<" -> match="<<all_match<<std::endl;
 | 
			
		||||
		if (all_match)
 | 
			
		||||
			return true;
 | 
			
		||||
	} while (std::next_permutation(recipe_copy.begin(), recipe_copy.end()));
 | 
			
		||||
	// Split recipe in group and non-group
 | 
			
		||||
	std::vector<std::string> recipe_nogroup;
 | 
			
		||||
	std::vector<std::string> recipe_onlygroup;
 | 
			
		||||
	std::partition_copy(recipe_copy.begin(), recipe_copy.end(),
 | 
			
		||||
			std::back_inserter(recipe_onlygroup),
 | 
			
		||||
			std::back_inserter(recipe_nogroup),
 | 
			
		||||
			[](const std::string &name) { return str_starts_with(name, "group:"); });
 | 
			
		||||
 | 
			
		||||
	return false;
 | 
			
		||||
	// Filter out non-group recipe slots, using sorted merge.
 | 
			
		||||
	// (This prefiltering is only a performance optimization and not strictly
 | 
			
		||||
	// necessary.)
 | 
			
		||||
	std::vector<std::string> input_for_group;
 | 
			
		||||
	std::set_difference(input_filtered.begin(), input_filtered.end(),
 | 
			
		||||
			recipe_nogroup.begin(), recipe_nogroup.end(),
 | 
			
		||||
			std::back_inserter(input_for_group));
 | 
			
		||||
 | 
			
		||||
	// All non-group slots must be satisfied
 | 
			
		||||
	if (input_filtered.size() - input_for_group.size() != recipe_nogroup.size())
 | 
			
		||||
		return false;
 | 
			
		||||
 | 
			
		||||
	// Find out which recipe slots each input item satisfies. This creates a
 | 
			
		||||
	// bipartite graph
 | 
			
		||||
	assert(recipe_onlygroup.size() == input_for_group.size());
 | 
			
		||||
	if (recipe_onlygroup.size() > SHAPELESS_GROUPS_MAX) {
 | 
			
		||||
		// SHAPELESS_GROUPS_MAX is large enough that this should never happen by
 | 
			
		||||
		// accident
 | 
			
		||||
		errorstream << "Too many groups in shapless craft." << std::endl;
 | 
			
		||||
		return false;
 | 
			
		||||
	}
 | 
			
		||||
	u16 graph_size = recipe_onlygroup.size();
 | 
			
		||||
	// bip_graph[i] are the group-slots that item i can satisfy
 | 
			
		||||
	std::vector<std::vector<u16>> bip_graph;
 | 
			
		||||
	bip_graph.resize(graph_size);
 | 
			
		||||
	for (u16 i = 0; i < graph_size; ++i) {
 | 
			
		||||
		std::vector<u16> &neighbors_i = bip_graph[i];
 | 
			
		||||
		for (u16 j = 0; j < graph_size; ++j) {
 | 
			
		||||
			if (inputItemMatchesRecipe(input_for_group[i], recipe_onlygroup[j],
 | 
			
		||||
					gamedef->idef()))
 | 
			
		||||
				neighbors_i.push_back(j);
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Check if the maximum cardinality matching of bip_graph matches all items
 | 
			
		||||
	return hopcroft_karp_can_match_all(bip_graph);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
CraftOutput CraftDefinitionShapeless::getOutput(const CraftInput &input, IGameDef *gamedef) const
 | 
			
		||||
 
 | 
			
		||||
@@ -266,7 +266,7 @@ private:
 | 
			
		||||
	std::string output;
 | 
			
		||||
	// Recipe list (itemstrings)
 | 
			
		||||
	std::vector<std::string> recipe;
 | 
			
		||||
	// Recipe list (item names)
 | 
			
		||||
	// Recipe list (item names), sorted
 | 
			
		||||
	std::vector<std::string> recipe_names;
 | 
			
		||||
	// bool indicating if initHash has been called already
 | 
			
		||||
	bool hash_inited = false;
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user