129 lines
4.5 KiB
Python
129 lines
4.5 KiB
Python
"""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()
|