Python řešení

This commit is contained in:
Tomáš Sláma 2025-04-24 00:42:21 +02:00
parent fc2c8ac1aa
commit 0326eafd52
6 changed files with 16752 additions and 0 deletions
asteracer-python-search

View file

@ -0,0 +1,3 @@
# Asteracer v Pythonu
Mělo by stačit spustit `__main__.py`.

View 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.")

View 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!")

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View 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