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.
@@ -0,0 +1,8 @@
1
+ try:
2
+ from ._version import __version__
3
+ except ImportError:
4
+ __version__ = "dev"
5
+
6
+
7
+ def _jupyter_labextension_paths():
8
+ return [{"src": "labextension", "dest": "foxsnakecode"}]
@@ -0,0 +1,4 @@
1
+ # This file is auto-generated by Hatchling. As such, do not:
2
+ # - modify
3
+ # - track in version control e.g. be sure to add to .gitignore
4
+ __version__ = VERSION = '1.0.0'
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())