foxsnakecode 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- foxsnakecode/__init__.py +8 -0
- foxsnakecode/_version.py +4 -0
- foxsnakecode/engine.py +339 -0
- foxsnakecode/missions.json +1547 -0
- foxsnakecode-1.0.0.data/data/share/jupyter/labextensions/foxsnakecode/install.json +5 -0
- foxsnakecode-1.0.0.data/data/share/jupyter/labextensions/foxsnakecode/package.json +82 -0
- foxsnakecode-1.0.0.data/data/share/jupyter/labextensions/foxsnakecode/static/232.95f9b339dd39bfc1.js +1317 -0
- foxsnakecode-1.0.0.data/data/share/jupyter/labextensions/foxsnakecode/static/727.cb038f1218fc701b.js +243 -0
- foxsnakecode-1.0.0.data/data/share/jupyter/labextensions/foxsnakecode/static/remoteEntry.e67f3120757332a1.js +1 -0
- foxsnakecode-1.0.0.data/data/share/jupyter/labextensions/foxsnakecode/static/style.js +4 -0
- foxsnakecode-1.0.0.data/data/share/jupyter/labextensions/foxsnakecode/static/third-party-licenses.json +16 -0
- foxsnakecode-1.0.0.dist-info/METADATA +152 -0
- foxsnakecode-1.0.0.dist-info/RECORD +15 -0
- foxsnakecode-1.0.0.dist-info/WHEEL +4 -0
- foxsnakecode-1.0.0.dist-info/licenses/LICENSE +21 -0
foxsnakecode/__init__.py
ADDED
foxsnakecode/_version.py
ADDED
foxsnakecode/engine.py
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
import base64
|
|
2
|
+
import json
|
|
3
|
+
import math
|
|
4
|
+
import os
|
|
5
|
+
import sys
|
|
6
|
+
|
|
7
|
+
WIDTH = 800
|
|
8
|
+
HEIGHT = 500
|
|
9
|
+
BODY_RADIUS = 14
|
|
10
|
+
HAZARD_RADIUS = 8
|
|
11
|
+
PICKUP_RADIUS = 24
|
|
12
|
+
STRIDE = 2.0
|
|
13
|
+
WALK_SPEED = 240.0
|
|
14
|
+
MIN_COMMAND_TIME = 0.07
|
|
15
|
+
BUMP_TIME = 0.26
|
|
16
|
+
TURN_TIME = 0.35
|
|
17
|
+
GUARD_TOUCH = 32
|
|
18
|
+
GUARD_TICK = 1.0 / 60.0
|
|
19
|
+
MAX_GUARD_TIME = 600.0
|
|
20
|
+
MAX_COMMANDS = 2000
|
|
21
|
+
MAX_LINES = 2000000
|
|
22
|
+
SOURCE = "<your code>"
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class _Finished(BaseException):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
class _TooLong(BaseException):
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class Chip:
|
|
34
|
+
def __init__(self, x, y, trapped):
|
|
35
|
+
self._x = x
|
|
36
|
+
self._y = y
|
|
37
|
+
self._trapped = trapped
|
|
38
|
+
self._collected = False
|
|
39
|
+
|
|
40
|
+
x = property(lambda self: self._x)
|
|
41
|
+
y = property(lambda self: self._y)
|
|
42
|
+
trapped = property(lambda self: self._trapped)
|
|
43
|
+
collected = property(lambda self: self._collected)
|
|
44
|
+
|
|
45
|
+
def __repr__(self):
|
|
46
|
+
return "Chip(x=%g, y=%g, trapped=%s, collected=%s)" % (
|
|
47
|
+
self._x, self._y, self._trapped, self._collected)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _guard_pose(guard, time):
|
|
51
|
+
moment = time % guard["period"]
|
|
52
|
+
for leg in guard["legs"]:
|
|
53
|
+
if moment < leg["t1"]:
|
|
54
|
+
break
|
|
55
|
+
progress = (moment - leg["t0"]) / (leg["t1"] - leg["t0"])
|
|
56
|
+
return (
|
|
57
|
+
leg["x0"] + (leg["x1"] - leg["x0"]) * progress,
|
|
58
|
+
leg["y0"] + (leg["y1"] - leg["y0"]) * progress,
|
|
59
|
+
leg["a0"] + (leg["a1"] - leg["a0"]) * progress,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class Guard:
|
|
64
|
+
def __init__(self, guard, clock):
|
|
65
|
+
self._guard = guard
|
|
66
|
+
self._clock = clock
|
|
67
|
+
|
|
68
|
+
x = property(lambda self: round(_guard_pose(self._guard, self._clock())[0], 2))
|
|
69
|
+
y = property(lambda self: round(_guard_pose(self._guard, self._clock())[1], 2))
|
|
70
|
+
angle = property(lambda self: round(_guard_pose(self._guard, self._clock())[2] % 360.0, 2))
|
|
71
|
+
|
|
72
|
+
def __repr__(self):
|
|
73
|
+
return "Guard(x=%g, y=%g, angle=%g)" % (self.x, self.y, self.angle)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _circle_hits_rect(x, y, radius, rect):
|
|
77
|
+
nearest_x = max(rect["x"], min(x, rect["x"] + rect["w"]))
|
|
78
|
+
nearest_y = max(rect["y"], min(y, rect["y"] + rect["h"]))
|
|
79
|
+
return math.hypot(x - nearest_x, y - nearest_y) < radius
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _line_hits_rect(x0, y0, x1, y1, rect):
|
|
83
|
+
near, far = 0.0, 1.0
|
|
84
|
+
for origin, change, low, high in (
|
|
85
|
+
(x0, x1 - x0, rect["x"], rect["x"] + rect["w"]),
|
|
86
|
+
(y0, y1 - y0, rect["y"], rect["y"] + rect["h"]),
|
|
87
|
+
):
|
|
88
|
+
if change == 0:
|
|
89
|
+
if origin < low or origin > high:
|
|
90
|
+
return False
|
|
91
|
+
continue
|
|
92
|
+
first, second = (low - origin) / change, (high - origin) / change
|
|
93
|
+
near = max(near, min(first, second))
|
|
94
|
+
far = min(far, max(first, second))
|
|
95
|
+
return near <= far
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _guard_spots(guard, time, x, y, obstacles):
|
|
99
|
+
guard_x, guard_y, angle = _guard_pose(guard, time)
|
|
100
|
+
distance = math.hypot(x - guard_x, y - guard_y)
|
|
101
|
+
if distance < GUARD_TOUCH:
|
|
102
|
+
return True
|
|
103
|
+
if distance > guard["sight"]:
|
|
104
|
+
return False
|
|
105
|
+
bearing = math.degrees(math.atan2(y - guard_y, x - guard_x))
|
|
106
|
+
if abs((bearing - angle + 180.0) % 360.0 - 180.0) > guard["fov"] / 2.0:
|
|
107
|
+
return False
|
|
108
|
+
return not any(_line_hits_rect(guard_x, guard_y, x, y, r) for r in obstacles)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _number(value, command):
|
|
112
|
+
if isinstance(value, bool) or not isinstance(value, (int, float)):
|
|
113
|
+
raise TypeError("%s() needs a number, but got %r" % (command, value))
|
|
114
|
+
if not math.isfinite(value):
|
|
115
|
+
raise ValueError("%s() needs a finite number, but got %r" % (command, value))
|
|
116
|
+
return float(value)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def _target(value, command):
|
|
120
|
+
if isinstance(value, (Chip, Guard)):
|
|
121
|
+
return value.x, value.y
|
|
122
|
+
if isinstance(value, (tuple, list)) and len(value) == 2:
|
|
123
|
+
return _number(value[0], command), _number(value[1], command)
|
|
124
|
+
raise TypeError("%s() needs a chip, a guard or an (x, y) pair, but got %r" % (command, value))
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def _run(payload):
|
|
128
|
+
scene = payload["scene"]
|
|
129
|
+
start = scene["start"]
|
|
130
|
+
state = {"x": float(start["x"]), "y": float(start["y"]), "angle": float(start["angle"]), "commands": 0, "time": 0.0}
|
|
131
|
+
chips = [Chip(c["x"], c["y"], bool(c.get("trapped"))) for c in scene["chips"]]
|
|
132
|
+
guards = [Guard(g, lambda: state["time"]) for g in scene["guards"]]
|
|
133
|
+
events = []
|
|
134
|
+
result = {"status": "incomplete", "events": events}
|
|
135
|
+
|
|
136
|
+
def spend():
|
|
137
|
+
state["commands"] += 1
|
|
138
|
+
if state["commands"] > MAX_COMMANDS:
|
|
139
|
+
raise _TooLong("commands")
|
|
140
|
+
|
|
141
|
+
def finish(status, reason):
|
|
142
|
+
result["status"] = status
|
|
143
|
+
result["reason"] = reason
|
|
144
|
+
raise _Finished()
|
|
145
|
+
|
|
146
|
+
def blocked_at(x, y):
|
|
147
|
+
if x < BODY_RADIUS or y < BODY_RADIUS or x > WIDTH - BODY_RADIUS or y > HEIGHT - BODY_RADIUS:
|
|
148
|
+
return True
|
|
149
|
+
return any(_circle_hits_rect(x, y, BODY_RADIUS, r) for r in scene["obstacles"])
|
|
150
|
+
|
|
151
|
+
def hazard_at(x, y):
|
|
152
|
+
for hazard in scene["hazards"]:
|
|
153
|
+
if "r" in hazard:
|
|
154
|
+
if math.hypot(x - hazard["x"], y - hazard["y"]) < hazard["r"] + HAZARD_RADIUS:
|
|
155
|
+
return "light"
|
|
156
|
+
elif _circle_hits_rect(x, y, HAZARD_RADIUS, hazard):
|
|
157
|
+
return "laser"
|
|
158
|
+
return None
|
|
159
|
+
|
|
160
|
+
def spotted():
|
|
161
|
+
if not scene["guards"]:
|
|
162
|
+
return False
|
|
163
|
+
if state["time"] > MAX_GUARD_TIME:
|
|
164
|
+
raise _TooLong("time")
|
|
165
|
+
return any(_guard_spots(g, state["time"], state["x"], state["y"], scene["obstacles"]) for g in scene["guards"])
|
|
166
|
+
|
|
167
|
+
def pass_time(seconds):
|
|
168
|
+
if not scene["guards"]:
|
|
169
|
+
state["time"] += seconds
|
|
170
|
+
return seconds, False
|
|
171
|
+
elapsed = 0.0
|
|
172
|
+
while elapsed < seconds:
|
|
173
|
+
tick = min(GUARD_TICK, seconds - elapsed)
|
|
174
|
+
elapsed += tick
|
|
175
|
+
state["time"] += tick
|
|
176
|
+
if spotted():
|
|
177
|
+
return elapsed, True
|
|
178
|
+
return elapsed, False
|
|
179
|
+
|
|
180
|
+
def step(distance):
|
|
181
|
+
distance = _number(distance, "step")
|
|
182
|
+
spend()
|
|
183
|
+
heading = math.radians(state["angle"])
|
|
184
|
+
direction = 1.0 if distance >= 0 else -1.0
|
|
185
|
+
dx, dy = math.cos(heading) * direction, math.sin(heading) * direction
|
|
186
|
+
x, y = state["x"], state["y"]
|
|
187
|
+
remaining = abs(distance)
|
|
188
|
+
travelled = 0.0
|
|
189
|
+
blocked = False
|
|
190
|
+
hit = None
|
|
191
|
+
picked = []
|
|
192
|
+
while remaining > 0:
|
|
193
|
+
stride = min(STRIDE, remaining)
|
|
194
|
+
next_x, next_y = x + dx * stride, y + dy * stride
|
|
195
|
+
if blocked_at(next_x, next_y):
|
|
196
|
+
blocked = True
|
|
197
|
+
break
|
|
198
|
+
x, y = next_x, next_y
|
|
199
|
+
state["x"], state["y"] = x, y
|
|
200
|
+
state["time"] += stride / WALK_SPEED
|
|
201
|
+
remaining -= stride
|
|
202
|
+
travelled += stride
|
|
203
|
+
hit = hazard_at(x, y)
|
|
204
|
+
if hit:
|
|
205
|
+
break
|
|
206
|
+
for index, chip in enumerate(chips):
|
|
207
|
+
if not chip._collected and math.hypot(x - chip.x, y - chip.y) < PICKUP_RADIUS:
|
|
208
|
+
chip._collected = True
|
|
209
|
+
picked.append({"i": index, "at": round(travelled, 2)})
|
|
210
|
+
if chip.trapped:
|
|
211
|
+
hit = "trap"
|
|
212
|
+
if not hit and spotted():
|
|
213
|
+
hit = "guard"
|
|
214
|
+
if hit:
|
|
215
|
+
break
|
|
216
|
+
walk_time = travelled / WALK_SPEED
|
|
217
|
+
extra_time = 0.0
|
|
218
|
+
if not hit:
|
|
219
|
+
rest = max(MIN_COMMAND_TIME - walk_time, 0.0) + (BUMP_TIME if blocked else 0.0)
|
|
220
|
+
extra_time, caught = pass_time(rest)
|
|
221
|
+
if caught:
|
|
222
|
+
hit = "guard"
|
|
223
|
+
event = {
|
|
224
|
+
"t": "move",
|
|
225
|
+
"x": round(x, 2),
|
|
226
|
+
"y": round(y, 2),
|
|
227
|
+
"d": round(travelled, 2),
|
|
228
|
+
"s": round(walk_time + extra_time, 4),
|
|
229
|
+
"walk": round(max(walk_time, MIN_COMMAND_TIME), 4),
|
|
230
|
+
"blocked": blocked,
|
|
231
|
+
"picked": picked,
|
|
232
|
+
}
|
|
233
|
+
events.append(event)
|
|
234
|
+
if hit:
|
|
235
|
+
finish("failed", hit)
|
|
236
|
+
if all(c._collected for c in chips if not c.trapped):
|
|
237
|
+
finish("solved", "chips")
|
|
238
|
+
|
|
239
|
+
def finish_turn():
|
|
240
|
+
elapsed, caught = pass_time(TURN_TIME)
|
|
241
|
+
events.append({"t": "turn", "a": round(state["angle"], 2), "s": round(elapsed, 4)})
|
|
242
|
+
if caught:
|
|
243
|
+
finish("failed", "guard")
|
|
244
|
+
|
|
245
|
+
def turn(degrees):
|
|
246
|
+
degrees = _number(degrees, "turn")
|
|
247
|
+
spend()
|
|
248
|
+
state["angle"] += degrees
|
|
249
|
+
finish_turn()
|
|
250
|
+
|
|
251
|
+
def turn_to(target):
|
|
252
|
+
x, y = _target(target, "turn_to")
|
|
253
|
+
spend()
|
|
254
|
+
if x == state["x"] and y == state["y"]:
|
|
255
|
+
return
|
|
256
|
+
wanted = math.degrees(math.atan2(y - state["y"], x - state["x"]))
|
|
257
|
+
change = (wanted - state["angle"] + 180.0) % 360.0 - 180.0
|
|
258
|
+
state["angle"] += change
|
|
259
|
+
finish_turn()
|
|
260
|
+
|
|
261
|
+
def wait(seconds):
|
|
262
|
+
seconds = _number(seconds, "wait")
|
|
263
|
+
if seconds < 0:
|
|
264
|
+
raise ValueError("wait() can't use a negative number, but got %r" % (seconds,))
|
|
265
|
+
spend()
|
|
266
|
+
elapsed, caught = pass_time(seconds)
|
|
267
|
+
events.append({"t": "wait", "s": round(elapsed, 4)})
|
|
268
|
+
if caught:
|
|
269
|
+
finish("failed", "guard")
|
|
270
|
+
|
|
271
|
+
def distance_to(target):
|
|
272
|
+
x, y = _target(target, "distance_to")
|
|
273
|
+
return math.hypot(x - state["x"], y - state["y"])
|
|
274
|
+
|
|
275
|
+
namespace = {
|
|
276
|
+
"__name__": "__main__",
|
|
277
|
+
"step": step,
|
|
278
|
+
"turn": turn,
|
|
279
|
+
"turn_to": turn_to,
|
|
280
|
+
"distance_to": distance_to,
|
|
281
|
+
"wait": wait,
|
|
282
|
+
"chips": chips,
|
|
283
|
+
"guards": guards,
|
|
284
|
+
}
|
|
285
|
+
lines = [0]
|
|
286
|
+
|
|
287
|
+
def tracer(frame, event, arg):
|
|
288
|
+
if frame.f_code.co_filename != SOURCE:
|
|
289
|
+
return None
|
|
290
|
+
if event == "line":
|
|
291
|
+
lines[0] += 1
|
|
292
|
+
if lines[0] > MAX_LINES:
|
|
293
|
+
raise _TooLong("lines")
|
|
294
|
+
return tracer
|
|
295
|
+
|
|
296
|
+
try:
|
|
297
|
+
compiled = compile(payload["code"], SOURCE, "exec")
|
|
298
|
+
sys.settrace(tracer)
|
|
299
|
+
try:
|
|
300
|
+
exec(compiled, namespace)
|
|
301
|
+
finally:
|
|
302
|
+
sys.settrace(None)
|
|
303
|
+
except _Finished:
|
|
304
|
+
pass
|
|
305
|
+
except _TooLong as error:
|
|
306
|
+
result["status"] = "error"
|
|
307
|
+
result["error"] = {"kind": "too-long", "detail": str(error)}
|
|
308
|
+
except KeyboardInterrupt:
|
|
309
|
+
result["status"] = "stopped"
|
|
310
|
+
except SyntaxError as error:
|
|
311
|
+
result["status"] = "error"
|
|
312
|
+
result["error"] = {"type": type(error).__name__, "message": error.msg, "line": error.lineno}
|
|
313
|
+
except BaseException as error:
|
|
314
|
+
line = None
|
|
315
|
+
trace = error.__traceback__
|
|
316
|
+
while trace is not None:
|
|
317
|
+
if trace.tb_frame.f_code.co_filename == SOURCE:
|
|
318
|
+
line = trace.tb_lineno
|
|
319
|
+
trace = trace.tb_next
|
|
320
|
+
result["status"] = "error"
|
|
321
|
+
result["error"] = {"type": type(error).__name__, "message": str(error), "line": line}
|
|
322
|
+
return result
|
|
323
|
+
|
|
324
|
+
|
|
325
|
+
def _send(output, text):
|
|
326
|
+
output.write("\n\x1eFSC" + text + "\x1e\n")
|
|
327
|
+
output.flush()
|
|
328
|
+
|
|
329
|
+
|
|
330
|
+
def run_mission(encoded_payload):
|
|
331
|
+
output = sys.stdout
|
|
332
|
+
result = _run(json.loads(base64.b64decode(encoded_payload).decode("utf-8")))
|
|
333
|
+
_send(output, json.dumps(result))
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def load_missions():
|
|
337
|
+
path = os.path.join(os.path.dirname(__file__), "missions.json")
|
|
338
|
+
with open(path, encoding="utf-8") as file:
|
|
339
|
+
_send(sys.stdout, file.read())
|