Python řešení
This commit is contained in:
parent
fc2c8ac1aa
commit
0326eafd52
6 changed files with 16752 additions and 0 deletions
asteracer-python-search
3
asteracer-python-search/README.md
Normal file
3
asteracer-python-search/README.md
Normal file
|
@ -0,0 +1,3 @@
|
|||
# Asteracer v Pythonu
|
||||
|
||||
Mělo by stačit spustit `__main__.py`.
|
569
asteracer-python-search/__main__.py
Normal file
569
asteracer-python-search/__main__.py
Normal file
|
@ -0,0 +1,569 @@
|
|||
"""Tom's solution to Asteracer. Moves on the vertices of the asteroid graph by simulating all angles."""
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
import drawsvg as draw
|
||||
import networkx as nx
|
||||
import numpy as np
|
||||
|
||||
from asteracer import *
|
||||
|
||||
directional_instructions_cache = {}
|
||||
|
||||
Vertex = tuple[int, int]
|
||||
|
||||
|
||||
def load_asteroid_graph(path: str):
|
||||
with open(path) as file:
|
||||
lines = [line.strip() for line in file.readlines()]
|
||||
|
||||
# Filter out comments and empty lines
|
||||
contents = [line for line in lines if line and not line.startswith('#')]
|
||||
|
||||
iter_lines = iter(contents)
|
||||
first_line = list(map(int, next(iter_lines).split()))
|
||||
n_racer, n_asteroid, n_goal, m = first_line
|
||||
|
||||
vertices = []
|
||||
edges = []
|
||||
vertex_objects = []
|
||||
|
||||
# Load vertices
|
||||
for i in range(n_racer + n_asteroid + n_goal):
|
||||
line = list(map(int, next(iter_lines).split()))
|
||||
vertices.append((line[0], line[1]))
|
||||
|
||||
if i < n_racer:
|
||||
vertex_objects.append(('S', i))
|
||||
elif i < (n_racer + n_asteroid):
|
||||
vertex_objects.append(('A', line[2]))
|
||||
else:
|
||||
vertex_objects.append(('G', line[2]))
|
||||
|
||||
# Load edges
|
||||
for _ in range(m):
|
||||
line = list(map(int, next(iter_lines).split()))
|
||||
edges.append((line[0], line[1]))
|
||||
|
||||
return vertices, edges, vertex_objects
|
||||
|
||||
|
||||
def _yield_points_at_distance(x: float, y: float, r: float, n: int):
|
||||
"""Generate n points uniformly at distance r from the coordinates (x, y)."""
|
||||
for i in range(n):
|
||||
t = (i / n) * np.pi * 2
|
||||
yield x + np.cos(t) * r, y + np.sin(t) * r
|
||||
|
||||
|
||||
def _get_directional_instructions(
|
||||
n: int = 1000,
|
||||
sort_by_angle_to: tuple[float, float] = None,
|
||||
limit_angle=None
|
||||
) -> list[Instruction]:
|
||||
"""Generate normalized instructions in at most n different angles with maximum velocity."""
|
||||
instructions = []
|
||||
|
||||
if n in directional_instructions_cache:
|
||||
instructions = directional_instructions_cache[n]
|
||||
|
||||
else:
|
||||
for point in _yield_points_at_distance(0, 0, 127, n):
|
||||
i1 = Instruction(*point)
|
||||
|
||||
if i1 in instructions:
|
||||
continue
|
||||
|
||||
for i2 in instructions:
|
||||
if np.sign(i1.vx) == np.sign(i2.vx) and abs(i1.vx) <= abs(i2.vx) \
|
||||
and np.sign(i1.vy) == np.sign(i2.vy) and abs(i1.vy) <= abs(i2.vy):
|
||||
break
|
||||
else:
|
||||
instructions.append(i1)
|
||||
|
||||
directional_instructions_cache[n] = instructions
|
||||
|
||||
def instr_vector_angle(i_from: Instruction, v_to: tuple[float, float]) -> float:
|
||||
"""The angle between an instruction and a vector."""
|
||||
i_vec = np.array([i_from.vx, i_from.vy])
|
||||
o_vec = np.array(v_to)
|
||||
return (1 - np.dot(i_vec, o_vec) / (np.linalg.norm(i_vec) * np.linalg.norm(o_vec))) / 2
|
||||
|
||||
# possibly sort by an angle to a given instruction
|
||||
if sort_by_angle_to:
|
||||
instructions = sorted(instructions, key=lambda x: instr_vector_angle(x, sort_by_angle_to))
|
||||
|
||||
if limit_angle:
|
||||
return [i for i in instructions if abs(instr_vector_angle(i, sort_by_angle_to)) <= limit_angle]
|
||||
else:
|
||||
return instructions
|
||||
else:
|
||||
return instructions
|
||||
|
||||
|
||||
def instructions_to_position(
|
||||
simulation: Simulation,
|
||||
position: Vertex,
|
||||
try_ticks=7,
|
||||
max_distance=1.25,
|
||||
) -> Optional[list[Instruction]]:
|
||||
"""Return a sequence of instructions that brings us the closest to the provided coordinates.
|
||||
Returns None if there is no such set that brings us at most max_distance * racer_radius away."""
|
||||
x, y = position
|
||||
|
||||
best_distance = float('inf')
|
||||
best_instructions = None
|
||||
|
||||
def is_safe(simulation: Simulation) -> bool:
|
||||
"""Returns True if the racer has a sequence of instructions to not crash at this stage of the simulation."""
|
||||
simulation.push()
|
||||
for instruction in _get_directional_instructions(10):
|
||||
simulation.apply()
|
||||
|
||||
for _ in range(try_ticks):
|
||||
result = simulation.tick(instruction)
|
||||
|
||||
if result & TickFlag.COLLIDED:
|
||||
break
|
||||
else:
|
||||
simulation.pop()
|
||||
return True
|
||||
|
||||
simulation.pop()
|
||||
return False
|
||||
|
||||
simulation.push()
|
||||
for inst in _get_directional_instructions(
|
||||
sort_by_angle_to=(x - int(simulation.racer.x), y - int(simulation.racer.y)),
|
||||
limit_angle=0.5
|
||||
):
|
||||
simulation.apply()
|
||||
started_improving = False
|
||||
prev_dist = 0
|
||||
|
||||
i = 0
|
||||
while True:
|
||||
result = simulation.tick(inst)
|
||||
|
||||
if result & TickFlag.COLLIDED:
|
||||
break
|
||||
|
||||
distance = euclidean_distance(simulation.racer.x, simulation.racer.y, x, y)
|
||||
|
||||
# if we're closer than before, we started improving
|
||||
if distance < prev_dist:
|
||||
started_improving = True
|
||||
|
||||
# if we've started improving but are now getting worse, terminate this instruction
|
||||
if started_improving and distance > prev_dist:
|
||||
break
|
||||
|
||||
prev_dist = distance
|
||||
|
||||
# if we improved and are safe, save results
|
||||
if distance < best_distance and is_safe(simulation):
|
||||
best_distance = distance
|
||||
best_instructions = [inst] * i
|
||||
|
||||
i += 1
|
||||
|
||||
simulation.pop()
|
||||
|
||||
if best_distance > max_distance * simulation.racer.radius:
|
||||
return None
|
||||
|
||||
return best_instructions
|
||||
|
||||
|
||||
def get_preview(simulation: Simulation, size=1000):
|
||||
"""Generate preview of the simulation as an SVG."""
|
||||
d = draw.Drawing(size, size, origin='center')
|
||||
|
||||
# background
|
||||
d.append(
|
||||
draw.Rectangle(
|
||||
-size / 2, -size / 2,
|
||||
size, size,
|
||||
fill="White"
|
||||
)
|
||||
)
|
||||
|
||||
def draw_circles(circles: list[Asteroid | Goal | Racer], color: str):
|
||||
"""Draw a circle with a specific color on the SVG."""
|
||||
s = simulation.bounding_box.width() / size
|
||||
|
||||
for i, circle in enumerate(circles):
|
||||
d.append(draw.Circle(circle.x / s, circle.y / s, circle.radius / s, fill=color, stroke=color))
|
||||
|
||||
draw_circles(simulation.asteroids, "Black")
|
||||
draw_circles([g for i, g in enumerate(simulation.goals) if simulation.reached_goals[i]], "LightGreen")
|
||||
draw_circles([g for i, g in enumerate(simulation.goals) if not simulation.reached_goals[i]], "Red")
|
||||
draw_circles([simulation.racer], "Gray")
|
||||
|
||||
return d
|
||||
|
||||
|
||||
def get_solution_preview(
|
||||
simulation: Simulation,
|
||||
simulation_path: Optional[list[Vertex]],
|
||||
color: str = "Green"
|
||||
) -> draw.Drawing:
|
||||
"""Return an SVG object with the solution graph, possibly with the simulation too."""
|
||||
d = get_preview(simulation)
|
||||
s = simulation.bounding_box.width() / d.width
|
||||
|
||||
def draw_path(path, color, stroke_width, stroke_opacity) -> draw.Path:
|
||||
p = draw.Path(stroke_width=stroke_width, stroke=color, opacity=stroke_opacity, fill_opacity=0)
|
||||
|
||||
p.M(path[0][0] / s, path[0][1] / s)
|
||||
for i in range(1, len(path)):
|
||||
p.L(path[i][0] / s, path[i][1] / s)
|
||||
d.append(p)
|
||||
|
||||
if simulation_path:
|
||||
d.append(draw_path(simulation_path, color, simulation.racer.radius / (s * 2), 1))
|
||||
|
||||
return d
|
||||
|
||||
|
||||
def get_checkpoints_path(
|
||||
simulation, vertices, edges, goal_vertices,
|
||||
longest_edges,
|
||||
):
|
||||
"""Get a path that the rocket should follow to obtain all goals."""
|
||||
|
||||
def astar_heuristic(p1, p2):
|
||||
return np.linalg.norm(np.array(p1) - np.array(p2))
|
||||
|
||||
def perturb_shortest_path(G, best_path, penalty_factor=2.0):
|
||||
""" Perturbs the shortest path by increasing the weight of a random edge. """
|
||||
if len(best_path) < 2:
|
||||
return best_path # No modification possible
|
||||
|
||||
# Randomly choose an edge along the best path
|
||||
idx = random.randint(2, len(best_path) - 3)
|
||||
u, v = best_path[idx], best_path[idx + 1]
|
||||
|
||||
if G.has_edge(u, v):
|
||||
original_weight = G[u][v].get("weight", 1.0)
|
||||
G[u][v]["weight"] = original_weight * penalty_factor
|
||||
if G.has_edge(v, u): # If the graph is undirected
|
||||
G[v][u]["weight"] = original_weight * penalty_factor
|
||||
|
||||
perturbed_path = nx.astar_path(G, best_path[0], best_path[-1], heuristic=astar_heuristic)
|
||||
|
||||
return perturbed_path
|
||||
|
||||
# convert vertices, edges to nx.Graph
|
||||
G = nx.Graph()
|
||||
for v in vertices:
|
||||
G.add_node(v)
|
||||
for u, v in edges:
|
||||
G.add_edge(u, v, weight=np.linalg.norm(np.array(u) - np.array(v)))
|
||||
|
||||
# for more than 1 goal, we have to solve TSP (at least approximately)
|
||||
if len(simulation.goals) > 1:
|
||||
G_tsp = nx.DiGraph()
|
||||
|
||||
tsp_vertices = [(simulation.racer.x, simulation.racer.y)]
|
||||
|
||||
for g in simulation.goals:
|
||||
tsp_vertices.append((g.x, g.y))
|
||||
G_tsp.add_node(tsp_vertices[-1])
|
||||
|
||||
count = 0
|
||||
for i, u in enumerate(tsp_vertices):
|
||||
for v in tsp_vertices[i + 1:]:
|
||||
print(f"TSP: building graph {count + 1}/{len(tsp_vertices) * (len(tsp_vertices) - 1) // 2}")
|
||||
w = nx.astar_path_length(G, u, v, heuristic=astar_heuristic)
|
||||
G_tsp.add_edge(u, v, weight=w)
|
||||
G_tsp.add_edge(v, u, weight=w)
|
||||
|
||||
count += 1
|
||||
|
||||
print(f"TSP: graph built")
|
||||
|
||||
# nx doesn't play nice with infinite values, but this might as well be
|
||||
INF = 100000000000
|
||||
|
||||
# we solve TSP by adding a hack vertex to force us to try all possible ending vertices
|
||||
# this means that we have to solve #number_of_vertices instances of TSP
|
||||
G_tsp.add_node("hack")
|
||||
G_tsp.add_edge(vertices[0], "hack", weight=0)
|
||||
G_tsp.add_edge("hack", vertices[0], weight=INF)
|
||||
|
||||
best_cost = float('inf')
|
||||
best_path = None
|
||||
|
||||
for i, g in enumerate(simulation.goals):
|
||||
print(f"TSP: solving {i + 1}/{len(simulation.goals)}")
|
||||
|
||||
for g_hack in simulation.goals:
|
||||
v = (g_hack.x, g_hack.y)
|
||||
G_tsp.add_edge("hack", v, weight=0 if g is g_hack else INF)
|
||||
G_tsp.add_edge(v, "hack", weight=INF)
|
||||
|
||||
path = nx.approximation.simulated_annealing_tsp(G_tsp, init_cycle="greedy")
|
||||
path.pop()
|
||||
|
||||
cost = 0
|
||||
for j in range(len(path)):
|
||||
cost += G_tsp[path[j - 1]][path[j]]["weight"]
|
||||
|
||||
for g_hack in simulation.goals:
|
||||
v = (g_hack.x, g_hack.y)
|
||||
G_tsp.remove_edge("hack", v)
|
||||
G_tsp.remove_edge(v, "hack")
|
||||
|
||||
path = list(reversed(path))
|
||||
j = path.index("hack")
|
||||
path.remove("hack")
|
||||
path = path[j:] + path[:j]
|
||||
|
||||
assert len(path) == len(simulation.goals) + 1, "Not all goals visited!"
|
||||
|
||||
if cost < best_cost:
|
||||
best_path = path
|
||||
best_cost = cost
|
||||
print(f"TSP: new optimum of length {best_cost}")
|
||||
|
||||
path = [(int(simulation.racer.x), int(simulation.racer.y))]
|
||||
for i in range(len(best_path) - 1):
|
||||
path += nx.astar_path(G, best_path[i], best_path[i + 1], heuristic=astar_heuristic)[1:]
|
||||
|
||||
# last vertex is a center of some goal
|
||||
path = path[:-1]
|
||||
|
||||
# shorten the path by cutting out vertices
|
||||
while True:
|
||||
# try to remove vertices for goals
|
||||
for i in range(len(path) - 2):
|
||||
if not G.has_edge(path[i], path[i + 2]):
|
||||
continue
|
||||
|
||||
# if it's not a part of a goal, we can remove for free
|
||||
if path[i + 1] not in goal_vertices:
|
||||
path.pop(i + 1)
|
||||
break
|
||||
|
||||
# if it is and is not the only one, we can remove it too
|
||||
goal_vertices_count = 0
|
||||
for v in path:
|
||||
if v in goal_vertices and goal_vertices[v] == goal_vertices[path[i + 1]]:
|
||||
goal_vertices_count += 1
|
||||
|
||||
if goal_vertices_count != 1:
|
||||
path.pop(i + 1)
|
||||
break
|
||||
else:
|
||||
break
|
||||
|
||||
# rotate goal vertices (we can take an arbitrary one to get the goal)
|
||||
while True:
|
||||
improved = False
|
||||
|
||||
for i in range(len(path) - 2):
|
||||
u = path[i + 1]
|
||||
|
||||
if u not in goal_vertices:
|
||||
continue
|
||||
|
||||
for v in vertices:
|
||||
if v not in goal_vertices:
|
||||
continue
|
||||
|
||||
if goal_vertices[u] != goal_vertices[v]:
|
||||
continue
|
||||
|
||||
if not G.has_edge(path[i], v) or not G.has_edge(v, path[i + 2]):
|
||||
continue
|
||||
|
||||
u_dist = G[path[i]][u]["weight"] + G[u][path[i + 2]]["weight"]
|
||||
v_dist = G[path[i]][v]["weight"] + G[v][path[i + 2]]["weight"]
|
||||
|
||||
if u_dist > v_dist:
|
||||
path = path[:i + 1] + [v] + path[i + 2:]
|
||||
improved = True
|
||||
break
|
||||
|
||||
if not improved:
|
||||
break
|
||||
|
||||
# TODO: remove goal vertices entirely if the edge exists and intersects it
|
||||
# TODO: for each goal, attempt to remove vertex and connect astar to next and to previous
|
||||
|
||||
print("Solved with TSP.")
|
||||
else:
|
||||
path = nx.astar_path(
|
||||
G,
|
||||
(simulation.racer.x, simulation.racer.y),
|
||||
(simulation.goals[0].x, simulation.goals[0].y),
|
||||
heuristic=astar_heuristic,
|
||||
)
|
||||
|
||||
path = perturb_shortest_path(G, path, penalty_factor=1000)
|
||||
|
||||
print("Solved with A*.")
|
||||
|
||||
# split long edges
|
||||
while True:
|
||||
split = False
|
||||
|
||||
i = 0
|
||||
while i < len(path) - 1:
|
||||
if euclidean_distance(*path[i], *path[i + 1]) > longest_edges:
|
||||
x = path[i][0] + path[i + 1][0]
|
||||
y = path[i][1] + path[i + 1][1]
|
||||
path.insert(i + 1, (x / 2, y / 2))
|
||||
split = True
|
||||
i += 1
|
||||
|
||||
if not split:
|
||||
break
|
||||
|
||||
return path
|
||||
|
||||
|
||||
def finalize_instructions(simulation, instructions):
|
||||
"""Finalize the instructions (cut useless ones + extend if the simulation isn't finished)."""
|
||||
simulation.restart()
|
||||
for i, instruction in enumerate(instructions):
|
||||
simulation.tick(instruction)
|
||||
|
||||
# there might be some redundant instructions at the very end
|
||||
if simulation.finished():
|
||||
break
|
||||
|
||||
instructions = instructions[:i + 1]
|
||||
|
||||
# if we ended just before the goal, repeat last instruction
|
||||
simulation.simulate(instructions)
|
||||
i = 0
|
||||
while not simulation.finished():
|
||||
simulation.tick(instruction)
|
||||
instructions.append(instruction)
|
||||
i += 1
|
||||
|
||||
if i > 1000:
|
||||
break
|
||||
|
||||
return instructions
|
||||
|
||||
|
||||
def get_solution_path(simulation, instructions):
|
||||
"""Generate the path the racer took using the instructions."""
|
||||
simulation.restart()
|
||||
solution_path = [(simulation.racer.x, simulation.racer.y)]
|
||||
for i, instruction in enumerate(instructions):
|
||||
simulation.tick(instruction)
|
||||
solution_path.append((simulation.racer.x, simulation.racer.y))
|
||||
|
||||
return solution_path
|
||||
|
||||
|
||||
def solve(simulation, path):
|
||||
"""Solve the simulation by following the path."""
|
||||
steps_instructions = []
|
||||
randomize_step_counter = 0
|
||||
i = 1
|
||||
while i < len(path):
|
||||
v = path[i]
|
||||
|
||||
for goal in simulation.goals:
|
||||
if euclidean_distance(goal.x, goal.y, v[0], v[1]) < goal.radius + simulation.racer.radius:
|
||||
is_goal_position = True
|
||||
break
|
||||
else:
|
||||
is_goal_position = False
|
||||
|
||||
print(f"Simulating {i}/{len(path) - 1}{'' if not is_goal_position else ' to goal'}", end=": ", flush=True)
|
||||
|
||||
if randomize_step_counter:
|
||||
print("randomized, ", end="")
|
||||
|
||||
# only move towards the goal
|
||||
if is_goal_position:
|
||||
p2g_vector = np.array([goal.x, goal.y]) - np.array(v)
|
||||
v = (p2g_vector / np.linalg.norm(p2g_vector)) * np.random.normal(0, simulation.racer.radius, 1)
|
||||
else:
|
||||
v = np.array(v) + np.random.normal(0, simulation.racer.radius, 2)
|
||||
|
||||
new_instructions = instructions_to_position(simulation, v, max_distance=2 if not is_goal_position else 0.75)
|
||||
|
||||
if new_instructions is None:
|
||||
if len(steps_instructions) != 0:
|
||||
randomize_step_counter += 1
|
||||
|
||||
i -= randomize_step_counter
|
||||
for _ in range(randomize_step_counter):
|
||||
steps_instructions.pop()
|
||||
|
||||
print(f"(- backtrack {randomize_step_counter}x)")
|
||||
|
||||
if len(steps_instructions) != 0:
|
||||
simulation.simulate(list(np.hstack(steps_instructions)))
|
||||
else:
|
||||
simulation.restart()
|
||||
|
||||
continue
|
||||
else:
|
||||
randomize_step_counter = max(randomize_step_counter - 1, 0)
|
||||
|
||||
steps_instructions.append(new_instructions)
|
||||
simulation.simulate(list(np.hstack(steps_instructions)))
|
||||
|
||||
print(f"(+ {len(new_instructions)})")
|
||||
|
||||
i += 1
|
||||
|
||||
return finalize_instructions(simulation, list(np.hstack(steps_instructions)))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
iterations = 5000
|
||||
|
||||
for task in ["sprint"]:
|
||||
print(f"Solving {task} ({iterations} attempts):")
|
||||
|
||||
os.makedirs(task, exist_ok=True)
|
||||
|
||||
simulation = Simulation.load(f"../mapy/{task}.txt")
|
||||
print("Simulation loaded.")
|
||||
|
||||
vertices, edges, object_types = load_asteroid_graph(f"../grafy/{task}.txt")
|
||||
edges = [(vertices[u], vertices[v]) for u, v in edges]
|
||||
|
||||
print("Graph loaded.")
|
||||
|
||||
average_asteroid_radius = sum([asteroid.radius for asteroid in simulation.asteroids]) / len(simulation.asteroids)
|
||||
goal_vertices = {
|
||||
vertices[i]: (simulation.goals[j].x, simulation.goals[j].y)
|
||||
for (i, (c, j)) in enumerate(object_types)
|
||||
if c == "G"
|
||||
}
|
||||
|
||||
best_instructions = None
|
||||
best_instructions_length = float('inf')
|
||||
|
||||
i = 0
|
||||
while i < iterations:
|
||||
simulation.restart()
|
||||
path = get_checkpoints_path(
|
||||
simulation,
|
||||
vertices, edges, goal_vertices,
|
||||
longest_edges=average_asteroid_radius * np.random.uniform(0.5, 5),
|
||||
)
|
||||
|
||||
instructions = solve(simulation, path)
|
||||
i += 1
|
||||
|
||||
if len(instructions) < best_instructions_length:
|
||||
best_instructions = instructions
|
||||
best_instructions_length = len(instructions)
|
||||
print(f"Solved with {len(instructions)} instructions.")
|
||||
|
||||
solution_path = get_solution_path(simulation, best_instructions)
|
||||
d = get_solution_preview(simulation, solution_path, "Green")
|
||||
d.save_svg(f"{task}/solution.svg")
|
||||
save_instructions(f"{task}/solution.txt", best_instructions)
|
||||
|
||||
print(f"Best solution: {len(best_instructions)} instructions.")
|
429
asteracer-python-search/asteracer.py
Normal file
429
asteracer-python-search/asteracer.py
Normal file
|
@ -0,0 +1,429 @@
|
|||
"""The Asteracer game implementation. Includes the base movement code + couple of QOL additions (eg. save states)."""
|
||||
from __future__ import annotations
|
||||
|
||||
import dataclasses
|
||||
import random
|
||||
from collections import defaultdict
|
||||
from dataclasses import dataclass
|
||||
from math import isqrt
|
||||
|
||||
|
||||
class TickFlag:
|
||||
"""Flags returned by simulation.tick() for various events that can occur during a tick."""
|
||||
COLLIDED = 1
|
||||
GOAL_REACHED = 2
|
||||
|
||||
|
||||
@dataclass
|
||||
class Racer:
|
||||
x: int = 0
|
||||
y: int = 0
|
||||
vx: int = 0
|
||||
vy: int = 0
|
||||
radius: int = 1
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Asteroid:
|
||||
x: int = 0
|
||||
y: int = 0
|
||||
radius: int = 1
|
||||
|
||||
Goal = Asteroid
|
||||
|
||||
|
||||
class Instruction:
|
||||
MAX_ACCELERATION = 127
|
||||
|
||||
def __init__(self, vx: int | float = 0, vy: int | float = 0):
|
||||
"""Whatever values we get, normalize them."""
|
||||
vx = int(vx)
|
||||
vy = int(vy)
|
||||
|
||||
if distance_squared(vx, vy) > Instruction.MAX_ACCELERATION ** 2:
|
||||
vx = float(vx)
|
||||
vy = float(vy)
|
||||
|
||||
# use float to properly normalize here
|
||||
distance = (vx ** 2 + vy ** 2) ** (1/2)
|
||||
|
||||
vx = int(vx / distance * Instruction.MAX_ACCELERATION)
|
||||
vy = int(vy / distance * Instruction.MAX_ACCELERATION)
|
||||
|
||||
# if we're still over, decrement both values
|
||||
if distance_squared(vx, vy) > Instruction.MAX_ACCELERATION ** 2:
|
||||
vx -= signum(vx)
|
||||
vy -= signum(vy)
|
||||
|
||||
assert distance_squared(vx, vy) <= Instruction.MAX_ACCELERATION ** 2
|
||||
|
||||
self.vx = vx
|
||||
self.vy = vy
|
||||
|
||||
def __hash__(self):
|
||||
return hash((self.vx, self.vy))
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.vx == other.vx and self.vy == other.vy
|
||||
|
||||
def __str__(self):
|
||||
return f"Instruction({self.vx}, {self.vy})"
|
||||
|
||||
@classmethod
|
||||
def random(cls):
|
||||
return cls(
|
||||
random.randint(
|
||||
-cls.MAX_ACCELERATION,
|
||||
cls.MAX_ACCELERATION
|
||||
),
|
||||
random.randint(
|
||||
-cls.MAX_ACCELERATION,
|
||||
cls.MAX_ACCELERATION
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@dataclass
|
||||
class BoundingBox:
|
||||
min_x: int
|
||||
min_y: int
|
||||
max_x: int
|
||||
max_y: int
|
||||
|
||||
def width(self) -> int:
|
||||
return int(self.max_x - self.min_x)
|
||||
|
||||
def height(self) -> int:
|
||||
return int(self.max_y - self.min_y)
|
||||
|
||||
|
||||
def distance_squared(x1, y1, x2=0, y2=0) -> int:
|
||||
"""Squared Euclidean distance between two points."""
|
||||
return (int(x1) - int(x2)) ** 2 + (int(y1) - int(y2)) ** 2
|
||||
|
||||
|
||||
def euclidean_distance(x1, y1, x2=0, y2=0):
|
||||
"""Integer Euclidean distance between two points. Uses integer square root."""
|
||||
return int(isqrt(distance_squared(x1, y1, x2, y2)))
|
||||
|
||||
|
||||
def signum(x):
|
||||
return -1 if x < 0 else 0 if x == 0 else 1
|
||||
|
||||
|
||||
def division(a, b):
|
||||
"""Correctly implemented division, removing the fractional component."""
|
||||
return (abs(int(a)) // int(b)) * signum(a)
|
||||
|
||||
|
||||
|
||||
class Simulation:
|
||||
DRAG_FRACTION = (9, 10) # slowdown of the racer's velocity after each tick
|
||||
COLLISION_FRACTION = (1, 2) # slowdown of the racer's velocity after a tick where a collision occurred
|
||||
MAX_COLLISION_RESOLUTIONS = 5 # at most how many collision iterations to perform
|
||||
|
||||
CELL_SIZE = 10_000
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
racer: Racer = Racer(),
|
||||
asteroids: list[Asteroid] = None,
|
||||
goals: list[Goal] = None,
|
||||
bounding_box: BoundingBox = None,
|
||||
):
|
||||
# the initial racer state (used when resetting the simulation)
|
||||
self.initial_racer = dataclasses.replace(racer)
|
||||
|
||||
self.racer = racer
|
||||
self.asteroids = asteroids or []
|
||||
self.goals = goals or []
|
||||
self.bounding_box = bounding_box
|
||||
|
||||
# to speed up the computation, we divide the bounding box (if we have one) into a grid
|
||||
# we do this so we don't need to check all asteroids at each tick, only those that could collide with the racer
|
||||
self._grid: dict[tuple[int, int], list[Asteroid]] = defaultdict(list)
|
||||
|
||||
for asteroid in asteroids:
|
||||
min_x, min_y = self._coordinate_to_grid(
|
||||
asteroid.x - asteroid.radius - racer.radius,
|
||||
asteroid.y - asteroid.radius - racer.radius,
|
||||
)
|
||||
|
||||
max_x, max_y = self._coordinate_to_grid(
|
||||
asteroid.x + asteroid.radius + racer.radius,
|
||||
asteroid.y + asteroid.radius + racer.radius,
|
||||
)
|
||||
|
||||
for grid_x in range(min_x, max_x + 1):
|
||||
for grid_y in range(min_y, max_y + 1):
|
||||
self._grid[(grid_x, grid_y)].append(asteroid)
|
||||
|
||||
self.reached_goals: list[bool] = [False] * len(self.goals)
|
||||
|
||||
# a list of simulation states that can be popped (restored to)
|
||||
self._pushed_states = []
|
||||
|
||||
def _coordinate_to_grid(self, x: float, y: float) -> tuple[int, int]:
|
||||
"""Translate an (x,y) coordinate into a coordinate of the grid."""
|
||||
return (x // self.CELL_SIZE, y // self.CELL_SIZE)
|
||||
|
||||
def _move_racer(self, instruction: Instruction):
|
||||
"""Move the racer in the given direction."""
|
||||
vx, vy = instruction.vx, instruction.vy
|
||||
|
||||
# drag
|
||||
self.racer.vx = division(self.racer.vx * self.DRAG_FRACTION[0], self.DRAG_FRACTION[1])
|
||||
self.racer.vy = division(self.racer.vy * self.DRAG_FRACTION[0], self.DRAG_FRACTION[1])
|
||||
|
||||
# velocity
|
||||
self.racer.vx += int(vx)
|
||||
self.racer.vy += int(vy)
|
||||
|
||||
# movement
|
||||
self.racer.x += self.racer.vx
|
||||
self.racer.y += self.racer.vy
|
||||
|
||||
def _push_out(self, obj: Asteroid | BoundingBox) -> bool:
|
||||
"""Attempt to push the racer out of the object (if he's colliding), adjusting
|
||||
his velocity accordingly (based on the angle of collision). Returns True if the
|
||||
racer was pushed out, otherwise returns False."""
|
||||
if isinstance(obj, Asteroid):
|
||||
# not colliding, nothing to be done
|
||||
if euclidean_distance(self.racer.x, self.racer.y, obj.x, obj.y) > (self.racer.radius + obj.radius):
|
||||
return False
|
||||
|
||||
# the vector to push the racer out by
|
||||
nx = self.racer.x - obj.x
|
||||
ny = self.racer.y - obj.y
|
||||
|
||||
# how much to push by
|
||||
distance = euclidean_distance(self.racer.x, self.racer.y, obj.x, obj.y)
|
||||
push_by = distance - (self.racer.radius + obj.radius)
|
||||
|
||||
# the actual push
|
||||
self.racer.x -= division(nx * push_by, distance)
|
||||
self.racer.y -= division(ny * push_by, distance)
|
||||
|
||||
return True
|
||||
|
||||
elif isinstance(obj, BoundingBox):
|
||||
# not pretty but easy to read :)
|
||||
collided = False
|
||||
|
||||
if self.racer.x - self.racer.radius < obj.min_x:
|
||||
self.racer.x = obj.min_x + self.racer.radius
|
||||
collided = True
|
||||
if self.racer.x + self.racer.radius > obj.max_x:
|
||||
self.racer.x = obj.max_x - self.racer.radius
|
||||
collided = True
|
||||
if self.racer.y - self.racer.radius < obj.min_y:
|
||||
self.racer.y = obj.min_y + self.racer.radius
|
||||
collided = True
|
||||
if self.racer.y + self.racer.radius > obj.max_y:
|
||||
self.racer.y = obj.max_y - self.racer.radius
|
||||
collided = True
|
||||
|
||||
return collided
|
||||
|
||||
else:
|
||||
raise Exception("Attempted to collide with something other than asteroid / bounding box!")
|
||||
|
||||
def _check_goal(self) -> bool:
|
||||
"""Sets the _reached_goals variable to True according to if the racer is intersecting them, returning True if
|
||||
a new one was reached."""
|
||||
new_goal_reached = False
|
||||
|
||||
for i, goal in enumerate(self.goals):
|
||||
if euclidean_distance(self.racer.x, self.racer.y, goal.x, goal.y) <= (self.racer.radius + goal.radius):
|
||||
if not self.reached_goals[i]:
|
||||
new_goal_reached = True
|
||||
|
||||
self.reached_goals[i] = True
|
||||
|
||||
return new_goal_reached
|
||||
|
||||
def _resolve_collisions(self) -> bool:
|
||||
"""Resolve all collisions of the racer and asteroids, returning True if a collison occurred."""
|
||||
collided = False
|
||||
for _ in range(self.MAX_COLLISION_RESOLUTIONS):
|
||||
collided_this_iteration = False
|
||||
|
||||
for asteroid in self._grid[self._coordinate_to_grid(self.racer.x, self.racer.y)]:
|
||||
if self._push_out(asteroid):
|
||||
collided_this_iteration = collided = True
|
||||
break
|
||||
|
||||
if self.bounding_box is not None and self._push_out(self.bounding_box):
|
||||
collided_this_iteration = collided = True
|
||||
|
||||
if not collided_this_iteration:
|
||||
break
|
||||
|
||||
if collided:
|
||||
self.racer.vx = division(self.racer.vx * self.COLLISION_FRACTION[0], self.COLLISION_FRACTION[1])
|
||||
self.racer.vy = division(self.racer.vy * self.COLLISION_FRACTION[0], self.COLLISION_FRACTION[1])
|
||||
|
||||
return collided
|
||||
|
||||
def finished(self) -> bool:
|
||||
"""Returns True if the racer reached all goals."""
|
||||
return all(self.reached_goals)
|
||||
|
||||
def restart(self):
|
||||
"""Restart the simulation to its initial state."""
|
||||
self.racer.x = self.initial_racer.x
|
||||
self.racer.y = self.initial_racer.y
|
||||
self.racer.vx = 0
|
||||
self.racer.vy = 0
|
||||
|
||||
for i in range(len(self.reached_goals)):
|
||||
self.reached_goals[i] = False
|
||||
|
||||
def tick(self, instruction: Instruction):
|
||||
"""Simulate a single tick of the simulation."""
|
||||
self._move_racer(instruction)
|
||||
collided = self._resolve_collisions()
|
||||
goal = self._check_goal()
|
||||
|
||||
return (TickFlag.COLLIDED if collided else 0) | (TickFlag.GOAL_REACHED if goal else 0)
|
||||
|
||||
def simulate(self, instructions: list[Instruction]):
|
||||
"""Simulate a number of instructions for the simulation (from the start)."""
|
||||
self.restart()
|
||||
|
||||
results = []
|
||||
|
||||
for instruction in instructions:
|
||||
results.append(self.tick(instruction))
|
||||
|
||||
return results
|
||||
|
||||
def save(self, path: str):
|
||||
"""Save the simulation to a file:
|
||||
| 0 0 5
|
||||
| -100 -100 100 100 // bounding box (min_x/min_y/max_x/max_y)
|
||||
| 5 // number of asteroids
|
||||
| 10 -10 10 // asteroid 1 x/y/radius
|
||||
| 20 20 50 // asteroid 2 x/y/radius
|
||||
| -10 10 30 // asteroid 3 x/y/radius
|
||||
| 10 10 70 // asteroid 4 x/y/radius
|
||||
| -10 -10 10 // asteroid 5 x/y/radius
|
||||
| 1 // number of goals
|
||||
| 100 100 10 // goal 1 x/y/radius
|
||||
"""
|
||||
with open(path, "w") as f:
|
||||
f.write(f"{self.racer.x} {self.racer.y} {self.racer.radius}\n")
|
||||
|
||||
bbox = self.bounding_box
|
||||
f.write(f"{bbox.min_x} {bbox.min_y} {bbox.max_x} {bbox.max_y}\n")
|
||||
|
||||
f.write(f"{len(self.asteroids)}\n")
|
||||
for asteroid in self.asteroids:
|
||||
f.write(f"{asteroid.x} {asteroid.y} {asteroid.radius}\n")
|
||||
|
||||
f.write(f"{len(self.goals)}\n")
|
||||
for goal in self.goals:
|
||||
f.write(f"{goal.x} {goal.y} {goal.radius}\n")
|
||||
|
||||
@classmethod
|
||||
def load(cls, path: str) -> Simulation:
|
||||
"""Load the simulation from a file (see self.save for the format description)."""
|
||||
with open(path) as f:
|
||||
lines = f.read().splitlines()
|
||||
|
||||
racer_parts = lines[0].split()
|
||||
racer = Racer(x=int(racer_parts[0]), y=int(racer_parts[1]), radius=int(racer_parts[2]))
|
||||
|
||||
bb_parts = lines[1].split()
|
||||
bb = BoundingBox(int(bb_parts[0]), int(bb_parts[1]), int(bb_parts[2]), int(bb_parts[2]))
|
||||
|
||||
asteroid_count = int(lines[2])
|
||||
|
||||
asteroids = []
|
||||
for i in range(3, 3 + asteroid_count):
|
||||
asteroid_parts = lines[i].split()
|
||||
asteroids.append(
|
||||
Asteroid(
|
||||
x=int(asteroid_parts[0]),
|
||||
y=int(asteroid_parts[1]),
|
||||
radius=int(asteroid_parts[2]),
|
||||
)
|
||||
)
|
||||
|
||||
goal_count = int(lines[3 + asteroid_count])
|
||||
|
||||
goals = []
|
||||
for i in range(4 + asteroid_count, 4 + asteroid_count + goal_count):
|
||||
goal_parts = lines[i].split()
|
||||
goals.append(
|
||||
Asteroid(
|
||||
x=int(goal_parts[0]),
|
||||
y=int(goal_parts[1]),
|
||||
radius=int(goal_parts[2]),
|
||||
)
|
||||
)
|
||||
|
||||
return Simulation(racer=racer, bounding_box=bb, asteroids=asteroids, goals=goals)
|
||||
|
||||
def push(self):
|
||||
"""Push (save) the current state of the simulation. Can be popped (restored) later."""
|
||||
self._pushed_states.append(
|
||||
(
|
||||
dataclasses.replace(self.racer),
|
||||
list(self.reached_goals),
|
||||
)
|
||||
)
|
||||
|
||||
def pop(self):
|
||||
"""Pop (restore) the previously pushed state."""
|
||||
assert len(self._pushed_states) != 0, "No states to pop!"
|
||||
self.racer, self.reached_goals = self._pushed_states.pop()
|
||||
|
||||
def apply(self):
|
||||
"""Apply the previously pushed state without popping it."""
|
||||
self.racer = dataclasses.replace(self._pushed_states[-1][0])
|
||||
self.reached_goals = list(self._pushed_states[-1][1])
|
||||
|
||||
|
||||
def save_instructions(path: str, instructions: list[Instruction]):
|
||||
"""Save a list of instructions to a file:
|
||||
| 4 // number if instructions
|
||||
| -16 -127 // instructions...
|
||||
| -16 -127
|
||||
| -26 -125
|
||||
| -30 -124
|
||||
"""
|
||||
with open(path, "w") as f:
|
||||
f.write(f"{len(instructions)}\n")
|
||||
|
||||
for instruction in instructions:
|
||||
f.write(f"{instruction.vx} {instruction.vy}\n")
|
||||
|
||||
|
||||
def load_instructions(path: str) -> list[Instruction]:
|
||||
"""Load a list of instructions from a file (see save_instructions for the format description)."""
|
||||
instructions = []
|
||||
|
||||
with open(path) as f:
|
||||
for line in f.read().splitlines()[1:]:
|
||||
instruction_parts = list(map(int, line.split()))
|
||||
instructions.append(Instruction(*instruction_parts))
|
||||
|
||||
return instructions
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
map_path = "../../maps/test.txt"
|
||||
|
||||
simulation = Simulation.load(map_path)
|
||||
|
||||
tick_result = 0
|
||||
|
||||
print("Running simulation until collision...")
|
||||
|
||||
while tick_result & TickFlag.COLLIDED == 0:
|
||||
tick_result = simulation.tick(Instruction(0, Instruction.MAX_ACCELERATION))
|
||||
|
||||
print(simulation.racer)
|
||||
|
||||
print("Bam!")
|
14291
asteracer-python-search/marathon/14290.txt
Normal file
14291
asteracer-python-search/marathon/14290.txt
Normal file
File diff suppressed because it is too large
Load diff
1152
asteracer-python-search/sprint/1151.txt
Normal file
1152
asteracer-python-search/sprint/1151.txt
Normal file
File diff suppressed because it is too large
Load diff
308
asteracer-python-search/test/307.txt
Normal file
308
asteracer-python-search/test/307.txt
Normal file
|
@ -0,0 +1,308 @@
|
|||
307
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 71
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
105 70
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-126 -5
|
||||
-83 -95
|
||||
-83 -95
|
||||
-83 -95
|
||||
-83 -95
|
||||
-83 -95
|
||||
-83 -95
|
||||
-83 -95
|
||||
-83 -95
|
||||
-83 -95
|
||||
-83 -95
|
||||
-83 -95
|
||||
-83 -95
|
||||
-83 -95
|
||||
-83 -95
|
||||
-83 -95
|
||||
-83 -95
|
||||
-83 -95
|
||||
-83 -95
|
||||
-83 -95
|
||||
-83 -95
|
||||
-83 -95
|
||||
-83 -95
|
||||
-83 -95
|
||||
-83 -95
|
||||
-83 -95
|
||||
-83 -95
|
||||
-83 -95
|
||||
-83 -95
|
||||
-83 -95
|
||||
-83 -95
|
||||
-83 -95
|
||||
-83 -95
|
||||
-83 -95
|
||||
-38 -121
|
||||
-38 -121
|
||||
-38 -121
|
||||
-38 -121
|
||||
-38 -121
|
||||
-38 -121
|
||||
-38 -121
|
||||
-41 -120
|
||||
-41 -120
|
||||
-41 -120
|
||||
-41 -120
|
||||
-41 -120
|
||||
-41 -120
|
||||
-41 -120
|
||||
-41 -120
|
||||
-41 -120
|
||||
-41 -120
|
||||
-41 -120
|
||||
-41 -120
|
||||
-41 -120
|
||||
-41 -120
|
||||
-41 -120
|
||||
-41 -120
|
||||
-41 -120
|
||||
-41 -120
|
||||
-41 -120
|
||||
-41 -120
|
||||
-41 -120
|
||||
-41 -120
|
||||
-41 -120
|
||||
-41 -120
|
||||
-41 -120
|
||||
-41 -120
|
||||
-41 -120
|
||||
-41 -120
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
||||
-48 -117
|
Loading…
Reference in a new issue