Skip to main content

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

set_hex
method
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.
coord
Tuple[int, int]
Grid coordinates (x, y) using doubled-height coordinate system
value
str
Aspect name (e.g., “ignis”, “aqua”) or special values “Free”, “Missing”
pixel_coord
Tuple[int, int]
Screen pixel coordinates for this hexagon
get_value
method
def get_value(self, coord: Tuple[int, int]) -> Optional[str]
Returns the aspect name at the given grid coordinate.
get_pixel_location
method
def get_pixel_location(self, coord: Tuple[int, int]) -> Tuple[int, int]
Returns the screen pixel coordinates for a grid coordinate.
calculate_distance
static method
@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.
get_neighbors
method
@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)]
pathfind_board_shortest
method
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
method
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]
pathfind_both_to_many
method
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).
hash_board
method
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

from_hexgrid
classmethod
@classmethod
def from_hexgrid(cls, hexgrid: HexGrid) -> "SolvingHexGrid"
Creates a new SolvingHexGrid from an existing HexGrid by deep copying its grid.
apply_path
method
def apply_path(self, path: List[Tuple[int, int]], element_path: List[str]) -> None
Applies a solution path to the grid and invalidates caches.
calculate_cost
method
def calculate_cost(self) -> int
Calculates the total cost of the current solution based on aspect costs.
are_positions_connected
method
def are_positions_connected(self, start: Coordinate, end: Coordinate) -> bool
Checks if two positions are connected through applied aspect paths.
get_unconnected_filled_positions
method
def get_unconnected_filled_positions(self, target: Coordinate) -> List[Coordinate]
Returns all positions with aspects that are not connected to the target position.
copy
method
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]])
solving
SolvingHexGrid
The hex grid to solve
start_aspects
List[Tuple[int, int]]
Grid coordinates of initially given aspects

Methods

solve
method
def solve(self) -> SolvingHexGrid
Main solving method. Runs the backtracking search algorithm until all positions are connected.Returns the best solution found.
do_solver_iteration
method
def do_solver_iteration(self) -> bool
Performs one iteration of the solver:
  1. Finds unconnected nodes
  2. Attempts to pathfind to them
  3. Applies the best path found
  4. Backtracks if no valid path exists
Returns False when search is complete.
alternate_previous_path
method
def alternate_previous_path(self) -> bool
Tries the next path variation for the most recent path. Backtracks if no alternatives exist.
report_solution
method
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]
game_window_title
str
Title (or beginning of title) of the game window to targetDefault: "GT: New Horizons"
next_board_hotkey
str | None
Global hotkey to process the next board (alternative to pressing Enter)Default: "ctrl+r"
aspect_cost_overrides
dict[str, int]
Custom aspect costs for solution scoring. Overrides calculated costs.Example: {"instrumentum": 1} to treat instrumentum as cheap as primal aspects
disabled_aspects
list[str]
List of aspect names to ignore (pretend they don’t exist)Example: ["caelum", "tabernus"]

Functions

get_global_config
function
@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]
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
function
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.
start
str
Starting aspect name
ends_list
List[str]
List of target aspect names
n_list
List[int]
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
function
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.

Build docs developers (and LLMs) love