initial commit
This commit is contained in:
+128
@@ -0,0 +1,128 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user