The puzzle recognition system uses pixel-perfect RGB color matching to convert a screenshot into a structured HexGrid representation. This process is critical for reliable automation across different Minecraft GUI scales and screen resolutions.
Each Thaumcraft aspect has a unique RGB color signature defined in colors.py:
aspect_colors = { "ordo": "D5D4EC", "terra": "56C000", "aqua": "3CD4FC", "ignis": "FF5A01", "aer": "FFFF7E", "perditio": "404040", # ... 65+ total aspects}# Convert to RGB tuples for pixel matchingaspect_to_rgb_map = { name: hex_to_rgb(hex_code) for name, hex_code in aspect_colors.items()}rgb_to_aspect_map = { (r, g, b): name for name, (r, g, b) in aspect_to_rgb_map.items()}
The rgb_to_aspect() function performs instant O(1) lookups:
def rgb_to_aspect(rgb_color): return rgb_to_aspect_map.get(rgb_color) # Returns aspect name or None
The color matching is exact - it requires pixel-perfect RGB values. This ensures no false positives but requires careful calibration for texture packs or visual mods.
The find_frame_slow() method scans each pixel and looks for consecutive runs of the frame color:
def has_consecutive_pixels(image, pixels, x, y, dx, dy): target_color = pixels[x, y] for i in range(10): nx, ny = x + i * dx, y + i * dy if not (0 <= nx < image.width and 0 <= ny < image.height) or pixels[nx, ny] != target_color: return False return True
This ensures we find the inner boundary of potentially thick frames.
Once the frame is located, find_aspects_in_frame() scans for aspect pixels:
def find_aspects_in_frame(frame, pixels) -> List[Tuple[Tuple[int, int, int, int], str]]: min_x, min_y, max_x, max_y = frame visited = set() found_aspects = [] for y in range(min_y, max_y + 1): for x in range(min_x, max_x + 1): if (x, y) in visited: continue color = pixels[x, y] aspect_name = rgb_to_aspect(color) if aspect_name is not None: # Found a valid aspect pixel - flood fill to get full region bounding_box = flood_fill(pixels, x, y, color, visited, frame_bounds) bb_min_x, bb_min_y, bb_max_x, bb_max_y = bounding_box smaller_side = min(bb_max_x - bb_min_x, bb_max_y - bb_min_y) # Filter out text pixels (smaller than 8px) if smaller_side > 8: found_aspects.append((bounding_box, aspect_name))