"""Triangulate the End portal (stronghold) position from two thrown Eyes of Ender. Pure geometry and input-parsing helpers, plus a small interactive command-line flow. Uses only the Python standard library. """ import math import sys # Reliability thresholds (degrees) for the angle between the two throw lines. # Below PARALLEL_ANGLE_DEG the lines are considered parallel (unusable result, # error and non-zero exit code); below WARN_ANGLE_DEG the result is shown but # flagged as imprecise. PARALLEL_ANGLE_DEG = 0.5 WARN_ANGLE_DEG = 10.0 def parse_point(text): """Parse a line of user input into an (x, z) tuple of floats. Accepts exactly two numbers separated by whitespace and/or commas, e.g. "100 200", "100,200" or "100.5 , -200.25" (decimals allowed, as read from the F3 screen). Raises ValueError otherwise. """ try: parts = text.replace(",", " ").split() if len(parts) != 2: raise ValueError return (float(parts[0]), float(parts[1])) except ValueError: raise ValueError( "expected exactly two numbers, e.g. 'x z' or 'x,z'" ) from None def angle_between(a, b, c, d): """Return the angle in degrees between line (a, b) and line (c, d). a, b, c, d are (x, z) tuples. The angle is computed from the direction vectors via atan2(|cross|, |dot|), which yields the acute (0-90 degrees) angle between the two lines regardless of their orientations: an Eye of Ender may fly towards or away from the other throw, so the dot product can be negative even though the lines are nearly parallel. """ dx1, dz1 = b[0] - a[0], b[1] - a[1] dx2, dz2 = d[0] - c[0], d[1] - c[1] return math.degrees( math.atan2(abs(dx1 * dz2 - dz1 * dx2), abs(dx1 * dx2 + dz1 * dz2)) ) def intersect_lines(a, b, c, d): """Return the (x, z) intersection point of line (a, b) and line (c, d). Uses the closed-form vector product formula (robust for vertical lines, unlike slope-based forms). Raises ValueError if either direction vector is zero or if the lines are parallel (no single intersection point). """ dx1, dz1 = b[0] - a[0], b[1] - a[1] dx2, dz2 = d[0] - c[0], d[1] - c[1] if dx1 == 0 and dz1 == 0: raise ValueError("first line has a zero direction vector") if dx2 == 0 and dz2 == 0: raise ValueError("second line has a zero direction vector") cross_dir = dx1 * dz2 - dz1 * dx2 if cross_dir == 0: raise ValueError("lines are parallel (no unique intersection)") # t = cross(C - A, d2) / cross(d1, d2), then the point is A + t * d1. cx, cz = c[0] - a[0], c[1] - a[1] t = (cx * dz2 - cz * dx2) / cross_dir return (a[0] + t * dx1, a[1] + t * dz1) def read_point(prompt): """Prompt the user until they enter a valid (x, z) point.""" while True: try: return parse_point(input(prompt)) except ValueError: print( "Saisie invalide : entrez deux nombres, " "par exemple « 100 200 » ou « 100,200 »." ) def read_throw(number): """Prompt for the launch and fall positions of one thrown Eye of Ender. Re-prompts for the fall position if it is identical to the launch position (such a throw carries no direction). """ launch = read_point(f"Œil {number} — position de lancé (x z ou x,z) : ") while True: fall = read_point(f"Œil {number} — position de chute (x z ou x,z) : ") if fall != launch: return launch, fall print( "Lancer inexploitable : la position de chute est identique " "à celle du lancé." ) def main(): """Run the interactive triangulation flow.""" a, b = read_throw(1) c, d = read_throw(2) angle = angle_between(a, b, c, d) if angle < PARALLEL_ANGLE_DEG: print( f"Erreur : les deux lancers sont parallèles ou quasi parallèles " f"(angle = {angle:.2f}°), l'intersection est inexploitable." ) sys.exit(1) try: x, z = intersect_lines(a, b, c, d) except ValueError as error: # Defensive: the angle check above should print(f"Erreur : {error}") # already have filtered these cases out. sys.exit(1) if angle < WARN_ANGLE_DEG: print( f"Attention : les lancers sont presque parallèles " f"(angle = {angle:.2f}°), le résultat est peu précis." ) print(f"Portail de l'End (approx.) : x = {x:.1f}, z = {z:.1f}") if __name__ == "__main__": main()