HexGrid
The HexGrid class represents the hexagonal puzzle board and provides pathfinding utilities.
Source: src/utils/grid.py
Class Definition
class HexGrid:
grid: Dict[Tuple[int, int], Tuple[str, Tuple[int, int]]]
grid
Dict[Tuple[int, int], Tuple[str, Tuple[int, int]]]
Maps grid coordinates to a tuple of (aspect_name, pixel_coordinate)
Methods
def set_hex(self, coord: Tuple[int, int], value: str, pixel_coord: Tuple[int, int]) -> None
Sets a hexagon cell with an aspect name and its pixel location.Grid coordinates (x, y) using doubled-height coordinate system
Aspect name (e.g., “ignis”, “aqua”) or special values “Free”, “Missing”
Screen pixel coordinates for this hexagon
def get_value(self, coord: Tuple[int, int]) -> Optional[str]
Returns the aspect name at the given grid coordinate.
def get_pixel_location(self, coord: Tuple[int, int]) -> Tuple[int, int]
Returns the screen pixel coordinates for a grid coordinate.
@staticmethod
@lru_cache(maxsize=2000)
def calculate_distance(start: Coordinate, end: Coordinate) -> int
Calculates hexagonal distance between two grid coordinates using doubled-height coordinate system.Cached with LRU cache for performance.
@lru_cache(maxsize=1000)
def get_neighbors(self, coord: Tuple[int, int]) -> List[Tuple[int, int]]
Returns valid neighboring coordinates (excludes “Missing” cells).Cached for performance. The six possible neighbor deltas are:
[(0, 2), (1, 1), (1, -1), (0, -2), (-1, -1), (-1, 1)]
def pathfind_board_shortest(
self, start: Tuple[int, int], end: Tuple[int, int]
) -> List[Tuple[int, int]]
Finds the shortest path between two coordinates using BFS. Only traverses “Free” cells.
pathfind_board_lengths_to_many
def pathfind_board_lengths_to_many(
self, start: Coordinate, ends: List[Coordinate], n_list: List[int]
) -> List[List[List[Coordinate]]]
Finds all paths of specific lengths from start to multiple end coordinates.Returns paths grouped by destination: [destination_index][path_variant][step]
def pathfind_both_to_many(
self, start: Tuple[int, int], ends: List[Tuple[int, int]]
) -> List[tuple[List[str], List[Coordinate]]]
Combines aspect pathfinding and board pathfinding. Returns tuples of (aspect_path, board_path).
def hash_board(self) -> str
Creates a filesystem-friendly hash of the board state (ignoring pixel coordinates). Used for saving test inputs.
SolvingHexGrid
Extends HexGrid with solution tracking and connectivity analysis.
Source: src/utils/grid.py
Class Definition
class SolvingHexGrid(HexGrid):
applied_paths: List[List[Tuple[str, Tuple[int, int]]]]
_grid_cache: Dict[Tuple[int, int], str] | None
connected_positions_cache: List[set[tuple[int, int]]]
applied_paths
List[List[Tuple[str, Tuple[int, int]]]]
List of solution paths, where each path is a list of (aspect_name, grid_coordinate) tuples
_grid_cache
Dict[Tuple[int, int], str] | None
Cached view of the grid after applying all paths
connected_positions_cache
List[set[tuple[int, int]]]
Cache of connected position sets for performance
Methods
@classmethod
def from_hexgrid(cls, hexgrid: HexGrid) -> "SolvingHexGrid"
Creates a new SolvingHexGrid from an existing HexGrid by deep copying its grid.
def apply_path(self, path: List[Tuple[int, int]], element_path: List[str]) -> None
Applies a solution path to the grid and invalidates caches.
def calculate_cost(self) -> int
Calculates the total cost of the current solution based on aspect costs.
def are_positions_connected(self, start: Coordinate, end: Coordinate) -> bool
Checks if two positions are connected through applied aspect paths.
get_unconnected_filled_positions
def get_unconnected_filled_positions(self, target: Coordinate) -> List[Coordinate]
Returns all positions with aspects that are not connected to the target position.
def copy(self) -> HexGrid
Creates a deep copy of the solving grid (for branching during search).
RingSolver
The main puzzle solver using a backtracking search algorithm.
Source: src/solvers/ringsolver.py
Class Definition
class RingSolver:
solving: SolvingHexGrid
start_aspects: List[Tuple[int, int]]
best_solution: SolvingHexGrid
best_solution_cost: int
all_paths: List[List[Tuple[List[str], List[Tuple[int, int]]]]]
path_variation_indices: List[int]
next_path_index: int
iteration_count: int
initial_nodes: List[Tuple[int, int]]
Constructor
def __init__(self, solving: SolvingHexGrid, start_aspects: List[Tuple[int, int]])
Grid coordinates of initially given aspects
Methods
def solve(self) -> SolvingHexGrid
Main solving method. Runs the backtracking search algorithm until all positions are connected.Returns the best solution found.
def do_solver_iteration(self) -> bool
Performs one iteration of the solver:
- Finds unconnected nodes
- Attempts to pathfind to them
- Applies the best path found
- Backtracks if no valid path exists
Returns False when search is complete.
def alternate_previous_path(self) -> bool
Tries the next path variation for the most recent path. Backtracks if no alternatives exist.
def report_solution(self) -> None
Called when a complete solution is found. Updates best_solution if the new solution has lower cost.
Top-Level Function
def solve(grid: HexGrid, start_aspects: List[Tuple[int, int]]) -> SolvingHexGrid
Convenience function that creates a RingSolver instance and returns the solution.
Config
Configuration management using TOML files.
Source: src/utils/config.py
Class Definition
@dataclass
class Config:
game_window_title: str
next_board_hotkey: str | None
aspect_cost_overrides: dict[str, int]
disabled_aspects: list[str]
Title (or beginning of title) of the game window to targetDefault: "GT: New Horizons"
Global hotkey to process the next board (alternative to pressing Enter)Default: "ctrl+r"
Custom aspect costs for solution scoring. Overrides calculated costs.Example: {"instrumentum": 1} to treat instrumentum as cheap as primal aspects
List of aspect names to ignore (pretend they don’t exist)Example: ["caelum", "tabernus"]
Functions
@cache
def get_global_config() -> Config
Loads configuration from config.toml. Creates the file with defaults if it doesn’t exist.Cached to avoid repeated file I/O.
Configuration File
The bot uses a config.toml file in the project root:
[general]
game-window-title = "GT: New Horizons"
next-board-hotkey = "ctrl+r"
disabled-aspects = []
[aspect-costs]
# Example custom costs:
# instrumentum = 1
Aspects Module
Aspect graph, cost calculation, and pathfinding.
Source: src/utils/aspects.py
Data Structures
aspect_parents
dict[str, Tuple[str, str] | Tuple[None, None]]
aspect_parents: dict[str, Tuple[str, str] | Tuple[None, None]] = {
"aer": (None, None), # Primal aspect
"ignis": (None, None), # Primal aspect
"lux": ("aer", "ignis"), # Compound aspect
# ... 70+ aspects total
}
Maps each aspect to its two parent aspects. Primal aspects have (None, None).
aspect_costs: dict[str, int]
Calculated costs for each aspect:
- Primal aspects cost 1
- Compound aspects cost the sum of their parents
- Can be overridden via config
Examples:
aspect_costs["ignis"] = 1 (primal)
aspect_costs["lux"] = 2 (ignis + aer)
aspect_costs["instrumentum"] = 5 (calculated recursively)
aspect_graph
defaultdict[str, List[str]]
aspect_graph: defaultdict[str, List[str]]
Adjacency list representation of the aspect graph. Neighbors are sorted by cost (cheapest first).
Functions
find_cheapest_element_paths_many
def find_cheapest_element_paths_many(
start: str,
ends_list: List[str],
n_list: List[int]
) -> List[List[List[str]]]
Finds the cheapest aspect transformation paths from a start aspect to multiple end aspects.List of target aspect names
List of path lengths (one per end aspect)
Returns: [end_index][path_variant][step] - all minimum-cost paths for each destination.Cached with @lru_cache(maxsize=1000) for performance.
calculate_cost_of_aspect_path
def calculate_cost_of_aspect_path(path: List[str]) -> int
Calculates the total cost of an aspect path by summing individual aspect costs.
Aspect Examples
The bot includes all Thaumcraft 4 aspects plus GTNH custom aspects:
- Primal Aspects: aer, aqua, ordo, terra, ignis, perditio
- Compound Aspects: lux, motus, victus, etc.
- GTNH Custom: primordium, vesania, aequalitas, astrum, gloria
You can disable specific aspects via the disabled-aspects config option if your modpack doesn’t include them.