Skip to content

Commit 3d6a72f

Browse files
committed
Added a library for track-like games
1 parent 0e9bd66 commit 3d6a72f

File tree

1 file changed

+288
-0
lines changed

1 file changed

+288
-0
lines changed

2018/racetrack.py

Lines changed: 288 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,288 @@
1+
from math import sqrt
2+
3+
# Cardinal directions
4+
north = 1j
5+
south = -1j
6+
west = -1
7+
east = 1
8+
directions_all = [north, south, west, east]
9+
directions_horizontal = [west, east]
10+
directions_vertical = [north, south]
11+
12+
# To be multiplied by the current cartinal direction
13+
relative_directions = {
14+
"left": 1j,
15+
"right": -1j,
16+
"ahead": 1,
17+
"back": -1,
18+
}
19+
20+
21+
class PlayerBlocked(Exception):
22+
pass
23+
24+
25+
def min_real(complexes):
26+
real_values = [x.real for x in complexes]
27+
return min(real_values)
28+
29+
30+
def min_imag(complexes):
31+
real_values = [x.imag for x in complexes]
32+
return min(real_values)
33+
34+
35+
def max_real(complexes):
36+
real_values = [x.real for x in complexes]
37+
return max(real_values)
38+
39+
40+
def max_imag(complexes):
41+
real_values = [x.imag for x in complexes]
42+
return max(real_values)
43+
44+
45+
def complex_sort(complexes, mode=""):
46+
# Sorts by real, then by imaginary component (x then y)
47+
if mode == "xy":
48+
complexes.sort(key=lambda a: (a.real, a.imag))
49+
# Sorts by imaginary, then by real component (y then x)
50+
elif mode == "yx":
51+
complexes.sort(key=lambda a: (a.imag, a.real))
52+
# Sorts by distance from 0,0 (kind of polar coordinates)
53+
else:
54+
complexes.sort(key=lambda a: sqrt(a.imag ** 2 + a.real ** 2))
55+
return complexes
56+
57+
58+
def collisions(players):
59+
positions = [x.position for x in players]
60+
if positions == set(positions):
61+
return None
62+
else:
63+
return [x for x in set(positions) if positions.count(x) > 1]
64+
65+
66+
class RaceTrack:
67+
vertices = {}
68+
edges = {}
69+
"""
70+
Represents which directions are allowed based on the track piece
71+
72+
Structure:
73+
track_piece: allowed directions
74+
"""
75+
allowed_directions = {
76+
"/": directions_all,
77+
"\\": directions_all,
78+
"+": directions_all,
79+
"|": directions_vertical,
80+
"-": directions_horizontal,
81+
"^": directions_vertical,
82+
"v": directions_vertical,
83+
">": directions_horizontal,
84+
"<": directions_horizontal,
85+
}
86+
87+
# Usual replacements
88+
player_replace = {
89+
">": "-",
90+
"<": "-",
91+
"^": "|",
92+
"v": "|",
93+
}
94+
95+
def __init__(self, vertices=[], edges={}):
96+
self.vertices = vertices
97+
self.edges = edges
98+
99+
def text_to_track(self, text, allowed_directions={}):
100+
"""
101+
Converts a text to a set of coordinates
102+
103+
The text is expected to be separated by newline characters
104+
The vertices will have x-y*j as coordinates (so y axis points south)
105+
Edges will be calculated as well
106+
107+
:param string text: The text to convert
108+
:param str elements: How to interpret the track
109+
:return: True if the text was converted
110+
"""
111+
self.vertices = {}
112+
self.allowed_directions.update(allowed_directions)
113+
114+
for y, line in enumerate(text.splitlines()):
115+
for x in range(len(line)):
116+
if line[x] in self.allowed_directions:
117+
self.vertices[x - y * 1j] = line[x]
118+
119+
for source, track in self.vertices.items():
120+
for direction in self.allowed_directions[track]:
121+
target = source + direction
122+
if not target in self.vertices:
123+
continue
124+
125+
target_dirs = self.allowed_directions[self.vertices[target]]
126+
if -direction not in target_dirs:
127+
continue
128+
129+
if source in self.edges:
130+
self.edges[source].append(target)
131+
else:
132+
self.edges[source] = [target]
133+
134+
return True
135+
136+
def track_to_text(self, mark_coords={}, wall=" "):
137+
"""
138+
Converts a set of coordinates to a text
139+
140+
The text will be separated by newline characters
141+
142+
:param dict mark_coords: List of coordinates to mark, with letter to use
143+
:param string wall: Which character to use as walls
144+
:return: the converted text
145+
"""
146+
147+
min_y, max_y = int(max_imag(self.vertices)), int(min_imag(self.vertices))
148+
min_x, max_x = int(min_real(self.vertices)), int(max_real(self.vertices))
149+
150+
text = ""
151+
152+
for y in range(min_y, max_y - 1, -1):
153+
for x in range(min_x, max_x + 1):
154+
if x + y * 1j in mark_coords:
155+
text += mark_coords[x + y * 1j]
156+
else:
157+
text += self.vertices.get(x + y * 1j, wall)
158+
text += "\n"
159+
160+
return text
161+
162+
def replace_elements(self, replace_map=None):
163+
"""
164+
Replaces elements in the track (useful to remove players)
165+
166+
:param dict replace_map: Replacement map
167+
:return: True
168+
"""
169+
170+
if replace_map is None:
171+
replace_map = self.player_replace
172+
self.vertices = {x: replace_map.get(y, y) for x, y in self.vertices.items()}
173+
return True
174+
175+
def find_elements(self, elements):
176+
"""
177+
Finds elements in the track
178+
179+
:param dict elements: elements to find
180+
:return: True
181+
"""
182+
183+
found = {x: y for x, y in self.vertices.items() if y in elements}
184+
return found
185+
186+
187+
class Player:
188+
"""
189+
Represents which directions are allowed based on the track piece
190+
191+
Structure:
192+
track_piece: source direction: allowed target direction
193+
"""
194+
195+
allowed_directions = {
196+
"/": {north: [east], south: [west], east: [north], west: [south],},
197+
"\\": {north: [west], south: [east], east: [south], west: [north],},
198+
"+": {
199+
north: directions_all,
200+
south: directions_all,
201+
east: directions_all,
202+
west: directions_all,
203+
},
204+
"|": {
205+
north: directions_vertical,
206+
south: directions_vertical,
207+
east: None,
208+
west: None,
209+
},
210+
"-": {
211+
north: None,
212+
south: None,
213+
east: directions_horizontal,
214+
west: directions_horizontal,
215+
},
216+
}
217+
218+
initial_directions = {
219+
">": east,
220+
"<": west,
221+
"^": north,
222+
"v": south,
223+
}
224+
225+
position = 0
226+
direction = 0
227+
228+
def __init__(self, racetrack, position=0, direction=None):
229+
self.position = position
230+
if direction is None:
231+
self.direction = self.initial_directions[racetrack.vertices[position]]
232+
else:
233+
self.direction = direction
234+
235+
def move(self, racetrack, steps=1):
236+
"""
237+
Moves the player in the direction provided
238+
239+
:param RaceTrack racetrack: The track to use
240+
:param int steps: The number of steps to take
241+
:return: nothing
242+
"""
243+
for step in range(steps):
244+
# First, let's move the player
245+
self.before_move()
246+
247+
self.position += self.direction
248+
249+
if self.position not in racetrack.vertices:
250+
raise PlayerBlocked
251+
252+
self.after_move()
253+
254+
# Then, let's make him turn
255+
self.before_rotation()
256+
257+
track = racetrack.vertices[self.position]
258+
possible_directions = self.allowed_directions[track][self.direction]
259+
260+
if possible_directions is None:
261+
raise PlayerBlocked
262+
elif len(possible_directions) == 1:
263+
self.direction = possible_directions[0]
264+
else:
265+
self.choose_direction(possible_directions)
266+
267+
self.after_rotation()
268+
269+
def before_move(self):
270+
pass
271+
272+
def after_move(self):
273+
pass
274+
275+
def before_rotation(self):
276+
pass
277+
278+
def after_rotation(self):
279+
pass
280+
281+
def choose_direction(self, possible_directions):
282+
self.direction = possible_directions[0]
283+
284+
def turn_left(self):
285+
self.direction *= 1j
286+
287+
def turn_right(self):
288+
self.direction *= -1j

0 commit comments

Comments
 (0)