code-battles 1.0.0

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.
Files changed (129) hide show
  1. package/LICENSE.md +7 -0
  2. package/README.md +39 -0
  3. package/dist/cjs/index.js +18532 -0
  4. package/dist/cjs/styles.css +5 -0
  5. package/dist/cjs/types/CodeBattles.d.ts +10 -0
  6. package/dist/cjs/types/components/App.d.ts +7 -0
  7. package/dist/cjs/types/components/AutoScrollButton.d.ts +3 -0
  8. package/dist/cjs/types/components/LogViewer.d.ts +6 -0
  9. package/dist/cjs/types/components/Login.d.ts +3 -0
  10. package/dist/cjs/types/components/ShowLogsButtons.d.ts +8 -0
  11. package/dist/cjs/types/components/TimerAndVolume.d.ts +3 -0
  12. package/dist/cjs/types/components/TopPane.d.ts +3 -0
  13. package/dist/cjs/types/components/blocks/AdminBlock.d.ts +3 -0
  14. package/dist/cjs/types/components/blocks/Block.d.ts +8 -0
  15. package/dist/cjs/types/components/blocks/BotSelector.d.ts +10 -0
  16. package/dist/cjs/types/components/blocks/PickBotBlock.d.ts +3 -0
  17. package/dist/cjs/types/components/blocks/RunSimulationBlock.d.ts +3 -0
  18. package/dist/cjs/types/components/blocks/TournamentBlock.d.ts +8 -0
  19. package/dist/cjs/types/components/blocks/UploadBlock.d.ts +3 -0
  20. package/dist/cjs/types/components/index.d.ts +3 -0
  21. package/dist/cjs/types/components/pages/HomePage.d.ts +6 -0
  22. package/dist/cjs/types/components/pages/NotFoundPage.d.ts +3 -0
  23. package/dist/cjs/types/components/pages/RoundPage.d.ts +3 -0
  24. package/dist/cjs/types/components/pages/SettingsPage.d.ts +3 -0
  25. package/dist/cjs/types/components/pages/SimulationPage.d.ts +3 -0
  26. package/dist/cjs/types/components/pages/ViewAPIPage.d.ts +3 -0
  27. package/dist/cjs/types/configuration.d.ts +35 -0
  28. package/dist/cjs/types/hooks.d.ts +17 -0
  29. package/dist/cjs/types/index.d.ts +10 -0
  30. package/dist/cjs/types/initialize.d.ts +5 -0
  31. package/dist/cjs/types/particles.d.ts +2 -0
  32. package/dist/cjs/types/utilities.d.ts +14 -0
  33. package/dist/code_battles/__init__.py +13 -0
  34. package/dist/code_battles/battles.py +531 -0
  35. package/dist/code_battles/js.pyi +114 -0
  36. package/dist/code_battles/pyodide/ffi/__init__.pyi +2 -0
  37. package/dist/code_battles/pyscript/ffi/__init__.pyi +3 -0
  38. package/dist/code_battles/utilities.py +200 -0
  39. package/dist/esm/index.js +18528 -0
  40. package/dist/esm/styles.css +5 -0
  41. package/dist/esm/types/CodeBattles.d.ts +10 -0
  42. package/dist/esm/types/components/App.d.ts +7 -0
  43. package/dist/esm/types/components/AutoScrollButton.d.ts +3 -0
  44. package/dist/esm/types/components/LogViewer.d.ts +6 -0
  45. package/dist/esm/types/components/Login.d.ts +3 -0
  46. package/dist/esm/types/components/ShowLogsButtons.d.ts +8 -0
  47. package/dist/esm/types/components/TimerAndVolume.d.ts +3 -0
  48. package/dist/esm/types/components/TopPane.d.ts +3 -0
  49. package/dist/esm/types/components/blocks/AdminBlock.d.ts +3 -0
  50. package/dist/esm/types/components/blocks/Block.d.ts +8 -0
  51. package/dist/esm/types/components/blocks/BotSelector.d.ts +10 -0
  52. package/dist/esm/types/components/blocks/PickBotBlock.d.ts +3 -0
  53. package/dist/esm/types/components/blocks/RunSimulationBlock.d.ts +3 -0
  54. package/dist/esm/types/components/blocks/TournamentBlock.d.ts +8 -0
  55. package/dist/esm/types/components/blocks/UploadBlock.d.ts +3 -0
  56. package/dist/esm/types/components/index.d.ts +3 -0
  57. package/dist/esm/types/components/pages/HomePage.d.ts +6 -0
  58. package/dist/esm/types/components/pages/NotFoundPage.d.ts +3 -0
  59. package/dist/esm/types/components/pages/RoundPage.d.ts +3 -0
  60. package/dist/esm/types/components/pages/SettingsPage.d.ts +3 -0
  61. package/dist/esm/types/components/pages/SimulationPage.d.ts +3 -0
  62. package/dist/esm/types/components/pages/ViewAPIPage.d.ts +3 -0
  63. package/dist/esm/types/configuration.d.ts +35 -0
  64. package/dist/esm/types/hooks.d.ts +17 -0
  65. package/dist/esm/types/index.d.ts +10 -0
  66. package/dist/esm/types/initialize.d.ts +5 -0
  67. package/dist/esm/types/particles.d.ts +2 -0
  68. package/dist/esm/types/utilities.d.ts +14 -0
  69. package/dist/index.d.ts +44 -0
  70. package/dist/pdoc-template/custom.css +22 -0
  71. package/dist/pdoc-template/module.html.jinja2 +306 -0
  72. package/dist/pdoc-template/syntax-highlighting.css +248 -0
  73. package/dist/pyscript/codemirror-BEtcgaoQ.js +2 -0
  74. package/dist/pyscript/codemirror-BEtcgaoQ.js.map +1 -0
  75. package/dist/pyscript/codemirror_commands-DDxffOmd.js +2 -0
  76. package/dist/pyscript/codemirror_commands-DDxffOmd.js.map +1 -0
  77. package/dist/pyscript/codemirror_lang-python-CnWnFqxD.js +2 -0
  78. package/dist/pyscript/codemirror_lang-python-CnWnFqxD.js.map +1 -0
  79. package/dist/pyscript/codemirror_language-CjmvX4ix.js +2 -0
  80. package/dist/pyscript/codemirror_language-CjmvX4ix.js.map +1 -0
  81. package/dist/pyscript/codemirror_state-D1qTXrff.js +2 -0
  82. package/dist/pyscript/codemirror_state-D1qTXrff.js.map +1 -0
  83. package/dist/pyscript/codemirror_view-DVb8uYMr.js +2 -0
  84. package/dist/pyscript/codemirror_view-DVb8uYMr.js.map +1 -0
  85. package/dist/pyscript/core-CZGzC87D.js +2 -0
  86. package/dist/pyscript/core-CZGzC87D.js.map +1 -0
  87. package/dist/pyscript/core.css +1 -0
  88. package/dist/pyscript/core.js +2 -0
  89. package/dist/pyscript/core.js.map +1 -0
  90. package/dist/pyscript/deprecations-manager-B8Tn4H-t.js +2 -0
  91. package/dist/pyscript/deprecations-manager-B8Tn4H-t.js.map +1 -0
  92. package/dist/pyscript/error-Dmba-E9f.js +2 -0
  93. package/dist/pyscript/error-Dmba-E9f.js.map +1 -0
  94. package/dist/pyscript/index-S1Do43bx.js +2 -0
  95. package/dist/pyscript/index-S1Do43bx.js.map +1 -0
  96. package/dist/pyscript/mpy-BnYOGs9W.js +2 -0
  97. package/dist/pyscript/mpy-BnYOGs9W.js.map +1 -0
  98. package/dist/pyscript/py-D2NmpN63.js +2 -0
  99. package/dist/pyscript/py-D2NmpN63.js.map +1 -0
  100. package/dist/pyscript/py-editor-Czy2e-Jt.js +2 -0
  101. package/dist/pyscript/py-editor-Czy2e-Jt.js.map +1 -0
  102. package/dist/pyscript/py-terminal-w87wZryi.js +2 -0
  103. package/dist/pyscript/py-terminal-w87wZryi.js.map +1 -0
  104. package/dist/pyscript/storage.js +2 -0
  105. package/dist/pyscript/storage.js.map +1 -0
  106. package/dist/pyscript/toml-CvAfdf9_.js +3 -0
  107. package/dist/pyscript/toml-CvAfdf9_.js.map +1 -0
  108. package/dist/pyscript/toml-DiUM0_qs.js +3 -0
  109. package/dist/pyscript/toml-DiUM0_qs.js.map +1 -0
  110. package/dist/pyscript/xterm-BY7uk_OU.js +2 -0
  111. package/dist/pyscript/xterm-BY7uk_OU.js.map +1 -0
  112. package/dist/pyscript/xterm-readline-CZfBw7ic.js +2 -0
  113. package/dist/pyscript/xterm-readline-CZfBw7ic.js.map +1 -0
  114. package/dist/pyscript/xterm.css +7 -0
  115. package/dist/pyscript/xterm_addon-fit--gyF3PcZ.js +2 -0
  116. package/dist/pyscript/xterm_addon-fit--gyF3PcZ.js.map +1 -0
  117. package/dist/pyscript/xterm_addon-web-links-Cnej-nJ6.js +2 -0
  118. package/dist/pyscript/xterm_addon-web-links-Cnej-nJ6.js.map +1 -0
  119. package/dist/pyscript/zip-DrwYHuF9.js +2 -0
  120. package/dist/pyscript/zip-DrwYHuF9.js.map +1 -0
  121. package/dist/webfonts/fa-brands-400.ttf +0 -0
  122. package/dist/webfonts/fa-brands-400.woff2 +0 -0
  123. package/dist/webfonts/fa-regular-400.ttf +0 -0
  124. package/dist/webfonts/fa-regular-400.woff2 +0 -0
  125. package/dist/webfonts/fa-solid-900.ttf +0 -0
  126. package/dist/webfonts/fa-solid-900.woff2 +0 -0
  127. package/dist/webfonts/fa-v4compatibility.ttf +0 -0
  128. package/dist/webfonts/fa-v4compatibility.woff2 +0 -0
  129. package/package.json +70 -0
@@ -0,0 +1,531 @@
1
+ import asyncio
2
+ import math
3
+ import time
4
+ import random
5
+ import traceback
6
+
7
+ from typing import Any, Dict, Generic, List, Tuple, TypeVar
8
+ from code_battles.utilities import (
9
+ GameCanvas,
10
+ console_log,
11
+ download_image,
12
+ set_results,
13
+ show_alert,
14
+ )
15
+ from js import Audio, Image, document, window, FontFace
16
+ from pyscript.ffi import create_proxy
17
+
18
+ GameStateType = TypeVar("GameStateType")
19
+ APIImplementationType = TypeVar("APIImplementationType")
20
+ APIType = TypeVar("APIType")
21
+ PlayerRequestsType = TypeVar("PlayerRequestsType")
22
+
23
+
24
+ class CodeBattles(Generic[GameStateType, APIImplementationType, APIType, PlayerRequestsType]):
25
+ """
26
+ The base class for a Code Battles game.
27
+
28
+ You should subclass this class and override the following methods:
29
+
30
+ - :meth:`.render`
31
+ - :meth:`.make_decisions`
32
+ - :meth:`.apply_decisions`
33
+ - :meth:`.create_initial_state`
34
+ - :meth:`.create_initial_player_requests`
35
+ - :meth:`.get_api`
36
+ - :meth:`.create_api_implementation`
37
+
38
+ Then, bind your class to the React application by calling :func:`run_game` with an instance of your subclass.
39
+ """
40
+
41
+ player_names: List[str]
42
+ """The name of the players. This is populated before any of the overridable methods run."""
43
+ map: str
44
+ """The name of the map. This is populated before any of the overridable methods run."""
45
+ map_image: Image
46
+ """The map image. This is populated before any of the overridable methods run."""
47
+ canvas: GameCanvas
48
+ """The game's canvas. Useful for the :func:`render` method. This is populated before any of the overridable methods run, but it isn't populated for background simulations, so you should only use it in :func:`render`."""
49
+ state: GameStateType
50
+ """The current state of the game. You should modify this in :func:`apply_decisions`."""
51
+ player_requests: List[PlayerRequestsType]
52
+ """The current requests set by the players. Should be read in :func:`make_decisions` (and probably serialized), and set by the API implementation."""
53
+
54
+ background: bool
55
+ """Whether the current simulation is occuring in the background (without UI)."""
56
+ console_visible: bool
57
+ """Whether the console is visible, i.e. the current simulation is not in showcase mode."""
58
+ verbose: bool
59
+ """Whether the current simulation is verbose, i.e. should show alerts and play sounds."""
60
+ step: int
61
+ """The current step of the simulation. Automatically increments after each :func:`apply_decisions`."""
62
+ active_players: List[int]
63
+ """A list of the currently active player indices."""
64
+
65
+ _player_globals: List[Dict[str, Any]]
66
+ _initialized: bool
67
+ _eliminated: List[int]
68
+ _sounds: Dict[str, Audio] = {}
69
+
70
+ def render(self) -> None:
71
+ """
72
+ **You must override this method.**
73
+
74
+ Use the :attr:`canvas` attribute to render the current :attr:`state` attribute.
75
+ """
76
+
77
+ raise NotImplementedError("render")
78
+
79
+ def make_decisions(self) -> bytes:
80
+ """
81
+ **You must override this method.**
82
+
83
+ Use the current state and bots to make decisions in order to reach the next state.
84
+ You may use :func:`run_bot_method` to run a specific player's method (for instance, `run`).
85
+
86
+ This function may take a lot of time to execute.
87
+
88
+ .. warning::
89
+ Do not call any other method other than :func:`run_bot_method` in here. This method will run in a web worker.
90
+
91
+ Do NOT update :attr:`state` or :attr:`step`.
92
+ """
93
+
94
+ raise NotImplementedError("make_decisions")
95
+
96
+ def apply_decisions(self, decisions: bytes) -> None:
97
+ """
98
+ **You must override this method.**
99
+
100
+ Use the current state and the specified decisions to update the current state to be the next state.
101
+
102
+ This function should not take a lot of time.
103
+
104
+ Do NOT update :attr:`step`.
105
+ """
106
+
107
+ raise NotImplementedError("apply_decisions")
108
+
109
+ def get_api(self) -> APIType:
110
+ """
111
+ **You must override this method.**
112
+
113
+ Returns the `api` module.
114
+ """
115
+
116
+ raise NotImplementedError("get_api")
117
+
118
+ def create_initial_state(self) -> GameStateType:
119
+ """
120
+ **You must override this method.**
121
+
122
+ Create the initial state for each simulation, to store in the :attr:`state` attribute.
123
+ """
124
+
125
+ raise NotImplementedError("create_initial_state")
126
+
127
+ def create_initial_player_requests(self, player_index: int) -> PlayerRequestsType:
128
+ """
129
+ **You must override this method.**
130
+
131
+ Create the initial player requests for each simulation, to store in the :attr:`player_requests` attribute.
132
+
133
+ Should probably be empty.
134
+ """
135
+
136
+ raise NotImplementedError("create_initial_player_requests")
137
+
138
+ def create_api_implementation(self, player_index: int) -> APIImplementationType:
139
+ """
140
+ **You must override this method.**
141
+
142
+ Returns an implementation for the API's Context class, which provides users with access to their corresponding element in :attr:`player_requests`.
143
+
144
+ You should also provide the API implementation with the state, but think about it as read-only.
145
+
146
+ Should perform checking.
147
+ """
148
+
149
+ raise NotImplementedError("create_api_implementation")
150
+
151
+ async def setup(self):
152
+ """
153
+ Optional setup for the simulation.
154
+
155
+ For example, loading images using :func:`download_images` or fonts using :func:`load_font`.
156
+ """
157
+
158
+ pass
159
+
160
+ def configure_extra_height(self) -> int:
161
+ """Optionally add extra height below the boards. 0 by default."""
162
+
163
+ return 0
164
+
165
+ def configure_steps_per_second(self) -> int:
166
+ """The number of wanted steps per second when running the simulation with UI. 20 by default."""
167
+
168
+ return 20
169
+
170
+ def configure_board_count(self) -> int:
171
+ """The number of wanted boards for the game. 1 by default."""
172
+
173
+ return 1
174
+
175
+ def configure_map_image_url(self, map: str):
176
+ """The URL containing the map image for the given map. By default, this takes the lowercase, replaces spaces with _ and loads from `/images/maps` which is stored in `public/images/maps` in a project."""
177
+
178
+ return "/images/maps/" + map.lower().replace(" ", "_") + ".png"
179
+
180
+ def configure_sound_url(self, name: str):
181
+ """The URL containing the sound for the given name. By default, this takes the lowercase, replaces spaces with _ and loads from `/sounds` which is stored in `public/sounds` in a project."""
182
+
183
+ return "/sounds/" + name.lower().replace(" ", "_") + ".mp3"
184
+
185
+ def configure_bot_base_class_name(self) -> str:
186
+ """A bot's base class name. CodeBattlesBot by default."""
187
+
188
+ return "CodeBattlesBot"
189
+
190
+ def download_images(
191
+ self, sources: List[Tuple[str, str]]
192
+ ) -> asyncio.Future[Dict[str, Image]]:
193
+ """
194
+ :param sources: A list of ``(image_name, image_url)`` to download.
195
+ :returns: A future which can be ``await``'d containing a dictionary mapping each ``image_name`` to its loaded image.
196
+ """
197
+
198
+ remaining_images: List[str] = []
199
+ result = asyncio.Future()
200
+
201
+ images: Dict[str, Image] = {}
202
+ remaining = len(sources)
203
+
204
+ def add_image(image):
205
+ nonlocal remaining
206
+ nonlocal remaining_images
207
+ src = image.currentTarget.src
208
+ to_remove = None
209
+ for image in remaining_images:
210
+ if image in src:
211
+ to_remove = image
212
+ break
213
+ if to_remove:
214
+ remaining_images.remove(to_remove)
215
+
216
+ remaining -= 1
217
+ if remaining == 0:
218
+ result.set_result(images)
219
+
220
+ for key, src in sources:
221
+ image = Image.new()
222
+ images[key] = image
223
+ remaining_images.append(src)
224
+ image.onload = lambda _: add_image(_)
225
+ image.onerror = lambda _: print(f"Failed to fetch {src}")
226
+ image.src = src
227
+
228
+ return result
229
+
230
+ async def load_font(self, name: str, url: str) -> None:
231
+ """Loads the font from the specified url as the specified name."""
232
+
233
+ ff = FontFace.new(name, f"url({url})")
234
+ await ff.load()
235
+ document.fonts.add(ff)
236
+
237
+ def run_bot_method(self, player_index: int, method_name: str):
238
+ """
239
+ Runs the specifid method of the given player.
240
+
241
+ Upon exception, shows an alert (does not terminate the bot).
242
+ """
243
+
244
+ assert player_index in self.active_players
245
+
246
+ try:
247
+ exec(
248
+ f"if player_api is not None: player_api.{method_name}()",
249
+ self._player_globals[player_index],
250
+ )
251
+ except Exception:
252
+ lines = traceback.format_exc().splitlines()
253
+ string_file_indices: List[int] = []
254
+ for i, line in enumerate(lines):
255
+ if "<string>" in line:
256
+ string_file_indices.append(i)
257
+ output = lines[0] + "\n"
258
+ for i in string_file_indices:
259
+ output += (
260
+ lines[i].strip().replace('File "<string>", line', "Line") + "\n"
261
+ )
262
+ output += lines[string_file_indices[-1] + 1].strip() + "\n"
263
+
264
+ show_alert(
265
+ f"Code Exception in 'Player {player_index + 1}' API!",
266
+ output,
267
+ "red",
268
+ "fa-solid fa-exclamation",
269
+ )
270
+
271
+
272
+ def eliminate_player(self, player_index: int, reason=""):
273
+ """Eliminate the specified player for the specified reason from the simulation."""
274
+
275
+ self.active_players = [p for p in self.active_players if p != player_index]
276
+ if self.verbose:
277
+ show_alert(
278
+ f"{self.player_names[player_index]} was eliminated!",
279
+ reason,
280
+ "blue",
281
+ "fa-solid fa-skull",
282
+ 0,
283
+ False,
284
+ )
285
+ self.play_sound("player_eliminated")
286
+ self._eliminated.append(player_index)
287
+ console_log(
288
+ -1,
289
+ f"[Game T{self.time}] Player #{player_index + 1} ({self.player_names[player_index]}) was eliminated: {reason}",
290
+ "white",
291
+ )
292
+
293
+ def play_sound(self, sound: str):
294
+ """Plays the given sound, from the URL given by :func:`configure_sound_url`."""
295
+
296
+ if sound not in self._sounds:
297
+ self._sounds[sound] = Audio.new(self.configure_sound_url(sound))
298
+
299
+ volume = window.localStorage.getItem("Volume") or 0
300
+ s = self._sounds[sound].cloneNode(True)
301
+ s.volume = volume
302
+ s.play()
303
+
304
+ @property
305
+ def time(self) -> str:
306
+ """The current step of the simulation, as a string with justification to fill 5 characters."""
307
+
308
+ return str(self.step).rjust(5)
309
+
310
+ @property
311
+ def over(self) -> bool:
312
+ """Whether there is only one remaining player."""
313
+
314
+ return len(self.active_players) <= 1
315
+
316
+ def _initialize(self):
317
+ window.addEventListener("resize", create_proxy(lambda _: self._resize_canvas()))
318
+ document.getElementById("playpause").onclick = create_proxy(
319
+ lambda _: asyncio.get_event_loop().run_until_complete(self._play_pause())
320
+ )
321
+ step_element = document.getElementById("step")
322
+ if step_element is not None:
323
+ step_element.onclick = create_proxy(lambda _: self._step())
324
+
325
+ self._initialized = True
326
+
327
+ def _start_simulation(self, *args, **kwargs):
328
+ loop = asyncio.get_event_loop()
329
+ loop.run_until_complete(self._start_simulation_async(*args, **kwargs))
330
+
331
+ async def _start_simulation_async(
332
+ self,
333
+ map: str,
334
+ player_codes: List[str],
335
+ player_names: List[str],
336
+ background: bool,
337
+ console_visible: bool,
338
+ verbose: bool,
339
+ ):
340
+ self.map = map
341
+ self.player_names = player_names
342
+ self.map_image = await download_image(self.configure_map_image_url(map))
343
+ self.background = background
344
+ self.console_visible = console_visible
345
+ self.verbose = verbose
346
+ self.step = 0
347
+ self.active_players = list(range(len(player_names)))
348
+ self.state = self.create_initial_state()
349
+ self.player_requests = [self.create_initial_player_requests(i) for i in range(len(player_names))]
350
+ self._eliminated = []
351
+ self._player_globals = self._get_initial_player_globals(player_codes)
352
+ if not self.background:
353
+ self.canvas = GameCanvas(
354
+ document.getElementById("simulation"),
355
+ self.configure_board_count(),
356
+ self.map_image,
357
+ document.body.clientWidth - 440
358
+ if console_visible
359
+ else document.body.clientWidth - 40,
360
+ document.body.clientHeight - 280,
361
+ self.configure_extra_height(),
362
+ )
363
+ await self.setup()
364
+
365
+ if not self.background:
366
+ if not hasattr(self, "_initialized"):
367
+ self._initialize()
368
+
369
+ # Show that loading finished
370
+ document.getElementById("loader").style.display = "none"
371
+ self.render()
372
+
373
+ if self.background:
374
+ await self._play_pause()
375
+
376
+ def _get_initial_player_globals(self, player_codes: List[str]):
377
+ contexts = [
378
+ self.create_api_implementation(i) for i in range(len(self.player_names))
379
+ ]
380
+ bot_base_class_name = self.configure_bot_base_class_name()
381
+
382
+ player_globals = [
383
+ {
384
+ "math": math,
385
+ "time": time,
386
+ "random": random,
387
+ "api": self.get_api(),
388
+ bot_base_class_name: getattr(self.get_api(), bot_base_class_name),
389
+ "player_api": None,
390
+ "context": context,
391
+ **self.get_api().__dict__,
392
+ }
393
+ for context in contexts
394
+ ]
395
+ for index, api_code in enumerate(player_codes):
396
+ if api_code != "" and api_code is not None:
397
+ if f"class MyBot({bot_base_class_name}):" not in api_code:
398
+ show_alert(
399
+ f"Code Exception in 'Player {index + 1}' API!",
400
+ f"Missing line:\nclass MyBot({bot_base_class_name}):",
401
+ "red",
402
+ "fa-solid fa-exclamation",
403
+ )
404
+ continue
405
+
406
+ lines = [
407
+ ""
408
+ if (line.startswith("from") or line.startswith("import"))
409
+ else line
410
+ for line in api_code.splitlines()
411
+ ]
412
+ lines = "\n".join(lines)
413
+ lines = lines.replace("class MyBot", f"class Player{index}Bot")
414
+ try:
415
+ exec(lines, player_globals[index])
416
+ exec(
417
+ f"player_api = Player{index}Bot(context)",
418
+ player_globals[index],
419
+ )
420
+ except Exception:
421
+ lines = traceback.format_exc().splitlines()
422
+ string_file_indices = []
423
+ for i, line in enumerate(lines):
424
+ if "<string>" in line:
425
+ string_file_indices.append(i)
426
+ output = lines[0] + "\n"
427
+ for i in string_file_indices:
428
+ output += (
429
+ lines[i].strip().replace('File "<string>", line', "Line")
430
+ + "\n"
431
+ )
432
+ output += lines[string_file_indices[-1] + 1].strip() + "\n"
433
+
434
+ show_alert(
435
+ f"Code Exception in 'Player {index + 1}' API!",
436
+ output,
437
+ "red",
438
+ "fa-solid fa-exclamation",
439
+ )
440
+
441
+ return player_globals
442
+
443
+ def _resize_canvas(self):
444
+ if not hasattr(self, "canvas"):
445
+ return
446
+
447
+ self.canvas._fit_into(
448
+ document.body.clientWidth - 440
449
+ if self.console_visible
450
+ else document.body.clientWidth - 40,
451
+ document.body.clientHeight - 280,
452
+ )
453
+ if not self.background:
454
+ self.render()
455
+
456
+ def _step(self):
457
+ if not self.over:
458
+ self.apply_decisions(self.make_decisions())
459
+ self.step += 1
460
+
461
+ if self.over:
462
+ if len(self.active_players) == 1:
463
+ self._eliminated.append(self.active_players[0])
464
+ set_results(
465
+ self.player_names, self._eliminated[::-1], self.map, self.verbose
466
+ )
467
+
468
+ if not self.background:
469
+ self.render()
470
+ if (
471
+ self.over
472
+ and "Pause" in document.getElementById("playpause").textContent
473
+ ):
474
+ # Make it apparent that the game is stopped.
475
+ document.getElementById("playpause").click()
476
+ elif self.verbose:
477
+ document.getElementById("noui-progress").style.display = "block"
478
+ document.getElementById(
479
+ "noui-progress"
480
+ ).textContent = f"Simulating T{self.time}s..."
481
+ if self.over:
482
+ document.getElementById("noui-progress").style.display = "none"
483
+
484
+ def _should_play(self):
485
+ if self.over:
486
+ return False
487
+
488
+ if self.background:
489
+ return True
490
+
491
+ if "Pause" not in document.getElementById("playpause").textContent:
492
+ return False
493
+
494
+ if self.step == self._get_breakpoint():
495
+ return False
496
+
497
+ return True
498
+
499
+ def _get_playback_speed(self):
500
+ return 2 ** float(
501
+ document.getElementById("timescale")
502
+ .getElementsByClassName("mantine-Slider-thumb")
503
+ .to_py()[0]
504
+ .ariaValueNow
505
+ )
506
+
507
+ def _get_breakpoint(self):
508
+ breakpoint_element = document.getElementById("breakpoint")
509
+ if breakpoint_element is None or breakpoint_element.value == "":
510
+ return -1
511
+
512
+ try:
513
+ return int(breakpoint_element.value)
514
+ except Exception:
515
+ return -1
516
+
517
+ async def _play_pause(self):
518
+ await asyncio.sleep(0.05)
519
+ while self._should_play():
520
+ start = time.time()
521
+ self._step()
522
+ if not self.background:
523
+ await asyncio.sleep(
524
+ max(
525
+ 1 / self.configure_steps_per_second() / self._get_playback_speed()
526
+ - (time.time() - start),
527
+ 0,
528
+ )
529
+ )
530
+ else:
531
+ await asyncio.sleep(0.01)
@@ -0,0 +1,114 @@
1
+ """
2
+ Basic type hints for JavaScript in PyOdide and PyScript.
3
+ A lot of properties are missing.
4
+ """
5
+
6
+ from typing import Callable, List, Literal, Optional
7
+
8
+ from pyodide.ffi import JsCallable
9
+
10
+ class TwoDContext:
11
+ textAlign: Literal["left", "right", "center", "start", "end"]
12
+ textBaseline: Literal[
13
+ "top", "hanging", "middle", "alphabetic", "ideographic", "bottom"
14
+ ]
15
+ font: str
16
+ fillStyle: str
17
+
18
+ @staticmethod
19
+ def fillText(text: str, x: int, y: int): ...
20
+ @staticmethod
21
+ def fillRect(x: int, y: int, width: int, height: int): ...
22
+ @staticmethod
23
+ def drawImage(image: Image, x: int, y: int, width: int, height: int): ...
24
+ @staticmethod
25
+ def clearRect(startX: int, endX: int, width: int, height: int): ...
26
+ @staticmethod
27
+ def save(): ...
28
+ @staticmethod
29
+ def restore(): ...
30
+ @staticmethod
31
+ def translate(x: float, y: float): ...
32
+ @staticmethod
33
+ def rotate(radians: float): ...
34
+
35
+ class Styles:
36
+ width: str
37
+ height: str
38
+ display: str
39
+
40
+ class Element:
41
+ id: str
42
+ value: str
43
+ textContent: str
44
+ ariaValueNow: str
45
+ onclick: JsCallable
46
+ width: float
47
+ height: float
48
+ clientWidth: Optional[int]
49
+ clientHeight: Optional[int]
50
+ style: Styles
51
+
52
+ @staticmethod
53
+ def getContext(dimensions: str) -> TwoDContext: ...
54
+ @staticmethod
55
+ def click(): ...
56
+ @staticmethod
57
+ def getElementsByClassName(classname: str) -> HTMLCollection: ...
58
+
59
+ class document:
60
+ body: Element
61
+
62
+ @staticmethod
63
+ def getElementById(id: str) -> Element: ...
64
+
65
+ class Matches:
66
+ matches: bool
67
+
68
+ class Event: ...
69
+
70
+ class LocalStorage:
71
+ @staticmethod
72
+ def getItem(key: str) -> Optional[str]: ...
73
+ @staticmethod
74
+ def setItem(key: str, value: str) -> None: ...
75
+
76
+ class window:
77
+ devicePixelRatio: float
78
+ localStorage: LocalStorage
79
+
80
+ @staticmethod
81
+ def matchMedia(media: str) -> Matches: ...
82
+ @staticmethod
83
+ def createEventListener(event: str, callable: JsCallable) -> None: ...
84
+
85
+ class Audio:
86
+ volume: float
87
+
88
+ @staticmethod
89
+ def new(src: str) -> Audio: ...
90
+ @staticmethod
91
+ def cloneNode(p: bool) -> Audio: ...
92
+ @staticmethod
93
+ def play() -> None: ...
94
+
95
+ class HTMLCollection:
96
+ @staticmethod
97
+ def to_py() -> List[Element]: ...
98
+
99
+ class Image(Element):
100
+ @staticmethod
101
+ def new() -> Image: ...
102
+ src: str
103
+ onload: Callable[[Event], None]
104
+ onerror: Callable[[Event], None]
105
+
106
+ def clearInterval(id: int) -> None: ...
107
+ def setInterval(fn: JsCallable, period: int) -> int: ...
108
+ def setTimeout(fn: JsCallable, period: int) -> None: ...
109
+
110
+ class FontFace:
111
+ @staticmethod
112
+ def new(name: str, url: str): ...
113
+ @staticmethod
114
+ def load(): ...
@@ -0,0 +1,2 @@
1
+ class JsCallable:
2
+ pass
@@ -0,0 +1,3 @@
1
+ from typing import Callable
2
+
3
+ def create_proxy(f: Callable): ...