code-battles 1.5.7 → 1.5.8

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 (62) hide show
  1. package/dist/cjs/index.js +10 -17
  2. package/dist/esm/index.js +10 -17
  3. package/package.json +9 -8
  4. package/dist/code_battles/__init__.py +0 -31
  5. package/dist/code_battles/__pycache__/__init__.cpython-312.pyc +0 -0
  6. package/dist/code_battles/__pycache__/battles.cpython-312.pyc +0 -0
  7. package/dist/code_battles/__pycache__/utilities.cpython-312.pyc +0 -0
  8. package/dist/code_battles/battles.py +0 -970
  9. package/dist/code_battles/js.pyi +0 -136
  10. package/dist/code_battles/pyodide/ffi/__init__.pyi +0 -2
  11. package/dist/code_battles/pyscript/__init__.pyi +0 -4
  12. package/dist/code_battles/pyscript/ffi/__init__.pyi +0 -3
  13. package/dist/code_battles/utilities.py +0 -321
  14. package/dist/code_battles/worker.py +0 -6
  15. package/dist/pyscript/codemirror-BEtcgaoQ.js +0 -2
  16. package/dist/pyscript/codemirror-BEtcgaoQ.js.map +0 -1
  17. package/dist/pyscript/codemirror_commands-DDxffOmd.js +0 -2
  18. package/dist/pyscript/codemirror_commands-DDxffOmd.js.map +0 -1
  19. package/dist/pyscript/codemirror_lang-python-CnWnFqxD.js +0 -2
  20. package/dist/pyscript/codemirror_lang-python-CnWnFqxD.js.map +0 -1
  21. package/dist/pyscript/codemirror_language-CjmvX4ix.js +0 -2
  22. package/dist/pyscript/codemirror_language-CjmvX4ix.js.map +0 -1
  23. package/dist/pyscript/codemirror_state-D1qTXrff.js +0 -2
  24. package/dist/pyscript/codemirror_state-D1qTXrff.js.map +0 -1
  25. package/dist/pyscript/codemirror_view-DVb8uYMr.js +0 -2
  26. package/dist/pyscript/codemirror_view-DVb8uYMr.js.map +0 -1
  27. package/dist/pyscript/core-CjO3FOKB.js +0 -2
  28. package/dist/pyscript/core-CjO3FOKB.js.map +0 -1
  29. package/dist/pyscript/core.css +0 -1
  30. package/dist/pyscript/core.js +0 -2
  31. package/dist/pyscript/core.js.map +0 -1
  32. package/dist/pyscript/deprecations-manager-pFtn19mE.js +0 -2
  33. package/dist/pyscript/deprecations-manager-pFtn19mE.js.map +0 -1
  34. package/dist/pyscript/error-tq-z48YI.js +0 -2
  35. package/dist/pyscript/error-tq-z48YI.js.map +0 -1
  36. package/dist/pyscript/index-S1Do43bx.js +0 -2
  37. package/dist/pyscript/index-S1Do43bx.js.map +0 -1
  38. package/dist/pyscript/mpy-DovD7Qjy.js +0 -2
  39. package/dist/pyscript/mpy-DovD7Qjy.js.map +0 -1
  40. package/dist/pyscript/py-BUsUWVJg.js +0 -2
  41. package/dist/pyscript/py-BUsUWVJg.js.map +0 -1
  42. package/dist/pyscript/py-editor-CeySmmer.js +0 -2
  43. package/dist/pyscript/py-editor-CeySmmer.js.map +0 -1
  44. package/dist/pyscript/py-terminal-CH_wV7wQ.js +0 -2
  45. package/dist/pyscript/py-terminal-CH_wV7wQ.js.map +0 -1
  46. package/dist/pyscript/storage.js +0 -2
  47. package/dist/pyscript/storage.js.map +0 -1
  48. package/dist/pyscript/toml-CvAfdf9_.js +0 -3
  49. package/dist/pyscript/toml-CvAfdf9_.js.map +0 -1
  50. package/dist/pyscript/toml-DiUM0_qs.js +0 -3
  51. package/dist/pyscript/toml-DiUM0_qs.js.map +0 -1
  52. package/dist/pyscript/xterm-BY7uk_OU.js +0 -2
  53. package/dist/pyscript/xterm-BY7uk_OU.js.map +0 -1
  54. package/dist/pyscript/xterm-readline-CZfBw7ic.js +0 -2
  55. package/dist/pyscript/xterm-readline-CZfBw7ic.js.map +0 -1
  56. package/dist/pyscript/xterm.css +0 -7
  57. package/dist/pyscript/xterm_addon-fit--gyF3PcZ.js +0 -2
  58. package/dist/pyscript/xterm_addon-fit--gyF3PcZ.js.map +0 -1
  59. package/dist/pyscript/xterm_addon-web-links-Cnej-nJ6.js +0 -2
  60. package/dist/pyscript/xterm_addon-web-links-Cnej-nJ6.js.map +0 -1
  61. package/dist/pyscript/zip-DrwYHuF9.js +0 -2
  62. package/dist/pyscript/zip-DrwYHuF9.js.map +0 -1
@@ -1,970 +0,0 @@
1
- import asyncio
2
- import base64
3
- from dataclasses import dataclass
4
- import datetime
5
- import json
6
- import math
7
- import time
8
- from random import Random
9
- import sys
10
- import traceback
11
- import gzip
12
-
13
- from typing import Any, Dict, Generic, List, Optional, Tuple, TypeVar
14
- from code_battles.utilities import (
15
- GameCanvas,
16
- console_log,
17
- download_image,
18
- navigate,
19
- set_results,
20
- show_alert,
21
- show_download,
22
- web_only,
23
- is_web,
24
- )
25
-
26
- try:
27
- import js
28
- except Exception:
29
- pass
30
-
31
- GameStateType = TypeVar("GameStateType")
32
- APIImplementationType = TypeVar("APIImplementationType")
33
- APIType = TypeVar("APIType")
34
- PlayerRequestsType = TypeVar("PlayerRequestsType")
35
-
36
-
37
- @dataclass
38
- class Simulation:
39
- map: str
40
- player_names: str
41
- game: str
42
- version: str
43
- timestamp: datetime.datetime
44
- logs: list
45
- decisions: List[bytes]
46
- seed: int
47
-
48
- def dump(self):
49
- return base64.b64encode(
50
- gzip.compress(
51
- json.dumps(
52
- {
53
- "map": self.map,
54
- "playerNames": self.player_names,
55
- "game": self.game,
56
- "version": self.version,
57
- "timestamp": self.timestamp.isoformat(),
58
- "logs": self.logs,
59
- "decisions": [
60
- base64.b64encode(decision).decode()
61
- for decision in self.decisions
62
- ],
63
- "seed": self.seed,
64
- }
65
- ).encode()
66
- )
67
- ).decode()
68
-
69
- @staticmethod
70
- def load(file: str):
71
- contents: Dict[str, Any] = json.loads(gzip.decompress(base64.b64decode(file)))
72
- return Simulation(
73
- contents["map"],
74
- contents["playerNames"],
75
- contents["game"],
76
- contents["version"],
77
- datetime.datetime.fromisoformat(contents["timestamp"]),
78
- contents["logs"],
79
- [base64.b64decode(decision) for decision in contents["decisions"]],
80
- contents["seed"],
81
- )
82
-
83
-
84
- class CodeBattles(
85
- Generic[GameStateType, APIImplementationType, APIType, PlayerRequestsType]
86
- ):
87
- """
88
- The base class for a Code Battles game.
89
-
90
- You should subclass this class and override the following methods:
91
-
92
- - :meth:`.render`
93
- - :meth:`.make_decisions`
94
- - :meth:`.apply_decisions`
95
- - :meth:`.create_initial_state`
96
- - :meth:`.create_initial_player_requests`
97
- - :meth:`.get_api`
98
- - :meth:`.create_api_implementation`
99
-
100
- Then, bind your class to the React application by calling :func:`run_game` with an instance of your subclass.
101
- """
102
-
103
- player_names: List[str]
104
- """The name of the players. This is populated before any of the overridable methods run."""
105
- map: str
106
- """The name of the map. This is populated before any of the overridable methods run."""
107
- map_image: "js.Image"
108
- """The map image. This is populated before any of the overridable methods run."""
109
- canvas: GameCanvas
110
- """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`."""
111
- state: GameStateType
112
- """The current state of the game. You should modify this in :func:`apply_decisions`."""
113
- player_requests: List[PlayerRequestsType]
114
- """The current requests set by the players. Should be read in :func:`make_decisions` (and probably serialized), and set by the API implementation."""
115
- random: Random
116
- """A pseudorandom generator that should be used for all randomness purposes (except :func:`make_decisions`)"""
117
- player_randoms: List[Random]
118
- """A pseudorandom generator that should be used for all randomness purposes in a player's bot. Given to the bots as a global via :func:`configure_bot_globals`."""
119
- make_decisions_random: Random
120
- """A pseudorandom generator that should be used for all randomness purposes in :func:`make_decisions`."""
121
-
122
- background: bool
123
- """Whether the current simulation is occuring in the background (without UI)."""
124
- console_visible: bool
125
- """Whether the console is visible, i.e. the current simulation is not in showcase mode."""
126
- verbose: bool
127
- """Whether the current simulation is verbose, i.e. should show alerts and play sounds."""
128
- step: int
129
- """The current step of the simulation. Automatically increments after each :func:`apply_decisions`."""
130
- active_players: List[int]
131
- """A list of the currently active player indices."""
132
-
133
- _player_globals: List[Dict[str, Any]]
134
- _initialized: bool
135
- _eliminated: List[int]
136
- _sounds: Dict[str, "js.Audio"] = {}
137
- _decisions: List[bytes]
138
- _since_last_render: int
139
-
140
- def render(self) -> None:
141
- """
142
- **You must override this method.**
143
-
144
- Use the :attr:`canvas` attribute to render the current :attr:`state` attribute.
145
- """
146
-
147
- raise NotImplementedError("render")
148
-
149
- def make_decisions(self) -> bytes:
150
- """
151
- **You must override this method.**
152
-
153
- Use the current state and bots to make decisions in order to reach the next state.
154
- You may use :func:`run_bot_method` to run a specific player's method (for instance, `run`).
155
-
156
- If you need any randomness, use :attr:`make_decisions_random`.
157
-
158
- This function may take a lot of time to execute.
159
-
160
- .. warning::
161
- Do not call any other method other than :func:`run_bot_method` in here. This method will run in a web worker.
162
- """
163
-
164
- raise NotImplementedError("make_decisions")
165
-
166
- def apply_decisions(self, decisions: bytes) -> None:
167
- """
168
- **You must override this method.**
169
-
170
- Use the current state and the specified decisions to update the current state to be the next state.
171
-
172
- This function should not take a lot of time.
173
-
174
- Do NOT update :attr:`step`.
175
- """
176
-
177
- raise NotImplementedError("apply_decisions")
178
-
179
- def get_api(self) -> APIType:
180
- """
181
- **You must override this method.**
182
-
183
- Returns the `api` module.
184
- """
185
-
186
- raise NotImplementedError("get_api")
187
-
188
- def create_initial_state(self) -> GameStateType:
189
- """
190
- **You must override this method.**
191
-
192
- Create the initial state for each simulation, to store in the :attr:`state` attribute.
193
- """
194
-
195
- raise NotImplementedError("create_initial_state")
196
-
197
- def create_initial_player_requests(self, player_index: int) -> PlayerRequestsType:
198
- """
199
- **You must override this method.**
200
-
201
- Create the initial player requests for each simulation, to store in the :attr:`player_requests` attribute.
202
-
203
- Should probably be empty.
204
- """
205
-
206
- raise NotImplementedError("create_initial_player_requests")
207
-
208
- def create_api_implementation(self, player_index: int) -> APIImplementationType:
209
- """
210
- **You must override this method.**
211
-
212
- Returns an implementation for the API's Context class, which provides users with access to their corresponding element in :attr:`player_requests`.
213
-
214
- You should also provide the API implementation with the state, but think about it as read-only.
215
-
216
- Should perform checking.
217
- """
218
-
219
- raise NotImplementedError("create_api_implementation")
220
-
221
- async def setup(self):
222
- """
223
- Optional setup for the simulation.
224
-
225
- For example, loading images using :func:`download_images` or fonts using :func:`load_font`.
226
- """
227
-
228
- pass
229
-
230
- def configure_extra_width(self) -> int:
231
- """Optionally add extra height to the right of the boards. 0 by default."""
232
-
233
- return 0
234
-
235
- def configure_extra_height(self) -> int:
236
- """Optionally add extra height below the boards. 0 by default."""
237
-
238
- return 0
239
-
240
- def configure_steps_per_second(self) -> int:
241
- """The number of wanted steps per second when running the simulation with UI. 20 by default."""
242
-
243
- return 20
244
-
245
- def configure_board_count(self) -> int:
246
- """The number of wanted boards for the game. 1 by default."""
247
-
248
- return 1
249
-
250
- def configure_map_image_url(self, map: str) -> str:
251
- """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."""
252
-
253
- return "/images/maps/" + map.lower().replace(" ", "_") + ".png"
254
-
255
- def configure_sound_url(self, name: str) -> str:
256
- """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."""
257
-
258
- return "/sounds/" + name.lower().replace(" ", "_") + ".mp3"
259
-
260
- def configure_bot_base_class_name(self) -> str:
261
- """A bot's base class name. CodeBattlesBot by default."""
262
-
263
- return "CodeBattlesBot"
264
-
265
- def configure_render_rate(self, playback_speed: float) -> int:
266
- """
267
- The amount of frames to simulate before each render.
268
-
269
- For games with an intensive `render` method, this is useful for higher playback speeds.
270
- """
271
- return 1
272
-
273
- def configure_bot_globals(self, player_index: int) -> Dict[str, Any]:
274
- """
275
- Configure additional available global items, such as libraries from the Python standard library, bots can use.
276
-
277
- By default, this is math, time and random, where random is the corresponding :attr:`player_randoms`.
278
-
279
- .. warning::
280
- Bots will also have `api`, `context`, `player_api`, and the bot base class name (CodeBattlesBot by default) available as part of the globals, alongside everything in `api`.
281
-
282
- Any additional imports will be stripped (not as a security mechanism).
283
- """
284
-
285
- return {
286
- "math": math,
287
- "time": time,
288
- "random": self.player_randoms[player_index],
289
- }
290
-
291
- def configure_version(self) -> str:
292
- """Configure the version of the game, which is stored in the simulation files."""
293
- return "1.0.0"
294
-
295
- @web_only
296
- def download_images(
297
- self, sources: List[Tuple[str, str]]
298
- ) -> asyncio.Future[Dict[str, "js.Image"]]:
299
- """
300
- :param sources: A list of ``(image_name, image_url)`` to download.
301
- :returns: A future which can be ``await``'d containing a dictionary mapping each ``image_name`` to its loaded image.
302
- """
303
- from js import Image
304
-
305
- remaining_images: List[str] = []
306
- result = asyncio.Future()
307
-
308
- images: Dict[str, Image] = {}
309
- remaining = len(sources)
310
-
311
- def add_image(image):
312
- nonlocal remaining
313
- nonlocal remaining_images
314
- src = image.currentTarget.src
315
- to_remove = None
316
- for image in remaining_images:
317
- if image in src:
318
- to_remove = image
319
- break
320
- if to_remove:
321
- remaining_images.remove(to_remove)
322
-
323
- remaining -= 1
324
- if remaining == 0:
325
- result.set_result(images)
326
-
327
- for key, src in sources:
328
- image = Image.new()
329
- images[key] = image
330
- remaining_images.append(src)
331
- image.onload = lambda _: add_image(_)
332
- image.onerror = lambda _: print(f"Failed to fetch {src}")
333
- image.src = src
334
-
335
- return result
336
-
337
- @web_only
338
- async def load_font(self, name: str, url: str) -> None:
339
- """Loads the font from the specified url as the specified name."""
340
- from js import FontFace, document
341
-
342
- ff = FontFace.new(name, f"url({url})")
343
- await ff.load()
344
- document.fonts.add(ff)
345
-
346
- def run_bot_method(self, player_index: int, method_name: str):
347
- """
348
- Runs the specifid method of the given player.
349
-
350
- Upon exception, shows an alert (does not terminate the bot).
351
- """
352
-
353
- assert player_index in self.active_players
354
-
355
- try:
356
- exec(
357
- f"if player_api is not None: player_api.{method_name}()",
358
- self._player_globals[player_index],
359
- )
360
- except Exception:
361
- lines = traceback.format_exc().splitlines()
362
- string_file_indices: List[int] = []
363
- for i, line in enumerate(lines):
364
- if "<string>" in line:
365
- string_file_indices.append(i)
366
- output = lines[0] + "\n"
367
- for i in string_file_indices:
368
- output += (
369
- lines[i].strip().replace('File "<string>", line', "Line") + "\n"
370
- )
371
- output += lines[string_file_indices[-1] + 1].strip() + "\n"
372
-
373
- show_alert(
374
- f"Code Exception in 'Player {player_index + 1}' API!",
375
- output,
376
- "red",
377
- "fa-solid fa-exclamation",
378
- )
379
-
380
- def eliminate_player(self, player_index: int, reason=""):
381
- """Eliminate the specified player for the specified reason from the simulation."""
382
-
383
- self.active_players = [p for p in self.active_players if p != player_index]
384
- if self.verbose:
385
- show_alert(
386
- f"{self.player_names[player_index]} was eliminated!",
387
- reason,
388
- "blue",
389
- "fa-solid fa-skull",
390
- 0,
391
- False,
392
- )
393
- self.play_sound("player_eliminated")
394
- self._eliminated.append(player_index)
395
- self.log(
396
- f"[Game T{self.step}] Player #{player_index + 1} ({self.player_names[player_index]}) was eliminated: {reason}",
397
- -1,
398
- "white",
399
- )
400
-
401
- def log(self, text: str, player_index: Optional[int] = None, color="white"):
402
- """
403
- Logs the given entry with the given color.
404
-
405
- For game-global log entries (not coming from a specific player), don't specify a ``player_index``.
406
- """
407
- if not isinstance(text, str):
408
- text = str(text)
409
-
410
- if is_web():
411
- console_log(-1 if player_index is None else player_index, text, color)
412
- else:
413
- self._logs.append(
414
- {
415
- "step": self.step,
416
- "text": text,
417
- "player_index": player_index,
418
- "color": color,
419
- }
420
- )
421
-
422
- @web_only
423
- def play_sound(self, sound: str):
424
- """Plays the given sound, from the URL given by :func:`configure_sound_url`."""
425
- from js import window, Audio
426
-
427
- if sound not in self._sounds:
428
- self._sounds[sound] = Audio.new(self.configure_sound_url(sound))
429
-
430
- volume = window.localStorage.getItem("Volume") or 0
431
- s = self._sounds[sound].cloneNode(True)
432
- s.volume = volume
433
-
434
- async def p():
435
- try:
436
- await s.play()
437
- except Exception:
438
- print(
439
- f"Warning: couldn't play sound '{sound}'. Make sure the `sound` and `configure_sound_url` are correct."
440
- )
441
-
442
- asyncio.get_event_loop().run_until_complete(p())
443
-
444
- @property
445
- def time(self) -> str:
446
- """The current step of the simulation, as a string with justification to fill 5 characters."""
447
-
448
- return str(self.step).rjust(5)
449
-
450
- @property
451
- def over(self) -> bool:
452
- """Whether there is only one remaining player."""
453
-
454
- return len(self.active_players) <= 1
455
-
456
- @web_only
457
- def _initialize(self):
458
- from js import window, document
459
- from pyscript.ffi import create_proxy
460
-
461
- window.addEventListener("resize", create_proxy(lambda _: self._resize_canvas()))
462
- document.getElementById("playpause").onclick = create_proxy(
463
- lambda _: asyncio.get_event_loop().run_until_complete(self._play_pause())
464
- )
465
- step_element = document.getElementById("step")
466
- if step_element is not None:
467
- step_element.onclick = create_proxy(lambda _: self._step())
468
-
469
- def _initialize_simulation(
470
- self, player_codes: List[str], seed: Optional[int] = None
471
- ):
472
- if seed is None:
473
- seed = Random().randint(0, 2**128)
474
- self._logs = []
475
- self._decisions = []
476
- self._decision_index = 0
477
- self._seed = seed
478
- self.step = 0
479
- self.active_players = list(range(len(self.player_names)))
480
- self.random = Random(seed)
481
- self.player_randoms = [
482
- Random(self.random.randint(0, 2**128)) for _ in self.player_names
483
- ]
484
- self.state = self.create_initial_state()
485
- self.player_requests = [
486
- self.create_initial_player_requests(i)
487
- for i in range(len(self.player_names))
488
- ]
489
- self._eliminated = []
490
- self._player_globals = self._get_initial_player_globals(player_codes)
491
- self._since_last_render = 1
492
- self._start_time = time.time()
493
-
494
- def _run_webworker_simulation(
495
- self,
496
- map: str,
497
- player_names_str: str,
498
- player_codes_str: str,
499
- seed: Optional[int] = None,
500
- ):
501
- from pyscript import sync
502
-
503
- # JS to Python
504
- player_names = json.loads(player_names_str)
505
- player_codes = json.loads(player_codes_str)
506
-
507
- self.map = map
508
- self.player_names = player_names
509
- self.background = True
510
- self.console_visible = False
511
- self.verbose = False
512
- self._initialize_simulation(player_codes, seed)
513
- while not self.over:
514
- self._logs = []
515
- decisions = self.make_decisions()
516
- logs = self._logs
517
- self._logs = []
518
- self.apply_decisions(decisions)
519
-
520
- sync.update_step(
521
- base64.b64encode(decisions).decode(),
522
- json.dumps(logs),
523
- "true" if self.over else "false",
524
- )
525
-
526
- if not self.over:
527
- self.step += 1
528
-
529
- def _run_local_simulation(self):
530
- command = sys.argv[1]
531
- output_file = None
532
- decisions = []
533
- if command == "simulate":
534
- seed = None if sys.argv[2] == "None" else int(sys.argv[2])
535
- output_file = None if sys.argv[3] == "None" else sys.argv[3]
536
- self.map = sys.argv[4]
537
- self.player_names = sys.argv[5].split("-")
538
- player_codes = []
539
- for filename in sys.argv[6:]:
540
- with open(filename, "r") as f:
541
- player_codes.append(f.read())
542
- elif command == "simulate-from-file":
543
- with open(sys.argv[2], "r") as f:
544
- contents = f.read()
545
- simulation = Simulation.load(contents)
546
- seed = simulation.seed
547
- self.map = simulation.map
548
- self.player_names = simulation.player_names
549
- decisions = simulation.decisions
550
- player_codes = ["" for _ in simulation.player_names]
551
- else:
552
- print(f"invalid command {sys.argv[1]}", file=sys.stderr)
553
- exit(-1)
554
- self.background = True
555
- self.console_visible = False
556
- self.verbose = False
557
- self._initialize_simulation(player_codes, seed)
558
-
559
- all_logs = []
560
- while not self.over:
561
- print("__CODE_BATTLES_ADVANCE_STEP")
562
- if len(decisions) != 0:
563
- self.apply_decisions(decisions.pop(0))
564
- else:
565
- self._logs = []
566
- _decisions = self.make_decisions()
567
- all_logs.append(self._logs)
568
- self._logs = []
569
- if output_file is not None:
570
- self._decisions.append(_decisions)
571
- self.apply_decisions(_decisions)
572
-
573
- if not self.over:
574
- self.step += 1
575
- self._logs = all_logs
576
-
577
- print("--- SIMULATION FINISHED ---")
578
- print(
579
- json.dumps(
580
- {
581
- "winner_index": self.active_players[0]
582
- if len(self.active_players) > 0
583
- else None,
584
- "winner": self.player_names[self.active_players[0]]
585
- if len(self.active_players) > 0
586
- else None,
587
- "steps": self.step,
588
- "logs": [log for logs in self._logs for log in logs],
589
- }
590
- )
591
- )
592
-
593
- if output_file is not None:
594
- simulation_str = self._get_simulation().dump()
595
- with open(output_file, "w") as f:
596
- f.write(simulation_str)
597
-
598
- def _start_simulation(self, *args, **kwargs):
599
- loop = asyncio.get_event_loop()
600
- loop.run_until_complete(self._start_simulation_async(*args, **kwargs))
601
-
602
- def _start_simulation_from_file(self, contents: str):
603
- loop = asyncio.get_event_loop()
604
- loop.run_until_complete(self._start_simulation_from_file_async(contents))
605
-
606
- async def _start_simulation_from_file_async(self, contents: str):
607
- from js import document
608
-
609
- try:
610
- simulation = Simulation.load(str(contents))
611
- navigate(
612
- f"/simulation/{simulation.map}/{'-'.join(simulation.player_names)}?seed={simulation.seed}"
613
- )
614
- show_alert(
615
- "Loaded simulation file!",
616
- f"{', '.join(simulation.player_names)} competed in {simulation.map} at {simulation.timestamp}",
617
- "blue",
618
- "fa-solid fa-file-code",
619
- 0,
620
- )
621
- if simulation.game != self.__class__.__name__:
622
- show_alert(
623
- "Warning: game mismatch!",
624
- f"Simulation file is for game {simulation.game} while the website is running {self.__class__.__name__}!",
625
- "yellow",
626
- "fa-solid fa-exclamation",
627
- 0,
628
- )
629
- if simulation.version != self.configure_version():
630
- show_alert(
631
- "Warning: version mismatch!",
632
- f"Simulation file is for version {simulation.version} while the website is running {self.configure_version()}!",
633
- "yellow",
634
- "fa-solid fa-exclamation",
635
- 0,
636
- )
637
- while document.getElementById("loader") is None:
638
- await asyncio.sleep(0.01)
639
- self._initialize()
640
- self.map = simulation.map
641
- self.map_image = await download_image(
642
- self.configure_map_image_url(simulation.map)
643
- )
644
- self.player_names = simulation.player_names
645
- self.background = False
646
- self.console_visible = True
647
- self.verbose = False
648
- self._initialize_simulation(
649
- ["" for _ in simulation.player_names], simulation.seed
650
- )
651
- self._decisions = simulation.decisions
652
- self._logs = simulation.logs
653
- self.canvas = GameCanvas(
654
- document.getElementById("simulation"),
655
- self.configure_board_count(),
656
- self.map_image,
657
- document.body.clientWidth - 440,
658
- document.body.clientHeight - 280,
659
- self.configure_extra_width(),
660
- self.configure_extra_height(),
661
- )
662
- document.getElementById("loader").style.display = "none"
663
- await self.setup()
664
- self.render()
665
- except Exception as e:
666
- print(e)
667
-
668
- @web_only
669
- async def _start_simulation_async(
670
- self,
671
- map: str,
672
- player_codes: List[str],
673
- player_names: List[str],
674
- background: bool,
675
- console_visible: bool,
676
- verbose: bool,
677
- seed="",
678
- ):
679
- from js import document
680
- from pyscript import workers
681
-
682
- # JS to Python
683
- player_names = [str(x) for x in player_names]
684
- player_codes = [str(x) for x in player_codes]
685
-
686
- try:
687
- render_status = document.getElementById("render-status")
688
- if render_status is not None:
689
- render_status.textContent = "Rendering: Initializing..."
690
-
691
- self.map = map
692
- self.player_names = player_names
693
- self.map_image = await download_image(self.configure_map_image_url(map))
694
- self.background = background
695
- self.console_visible = console_visible
696
- self.verbose = verbose
697
- self._initialize_simulation(player_codes, None if seed == "" else int(seed))
698
-
699
- if not self.background:
700
- self.canvas = GameCanvas(
701
- document.getElementById("simulation"),
702
- self.configure_board_count(),
703
- self.map_image,
704
- document.body.clientWidth - 440
705
- if console_visible
706
- else document.body.clientWidth - 40,
707
- document.body.clientHeight - 280
708
- if console_visible
709
- else document.body.clientHeight - 160,
710
- self.configure_extra_width(),
711
- self.configure_extra_height(),
712
- )
713
- await self.setup()
714
-
715
- if not self.background:
716
- self._initialize()
717
-
718
- # Show that loading finished
719
- document.getElementById("loader").style.display = "none"
720
- self.render()
721
-
722
- self._worker = await workers["worker"]
723
- self._worker.update_step = self._update_step
724
- self._worker._run_webworker_simulation(
725
- map, json.dumps(player_names), json.dumps(player_codes), self._seed
726
- )
727
-
728
- if self.background:
729
- await self._play_pause()
730
- except Exception:
731
- traceback.print_exc()
732
-
733
- def _get_simulation(self):
734
- return Simulation(
735
- self.map,
736
- self.player_names,
737
- self.__class__.__name__,
738
- self.configure_version(),
739
- datetime.datetime.now(),
740
- self._logs,
741
- self._decisions,
742
- self._seed,
743
- )
744
-
745
- def _update_step(self, decisions_str: str, logs_str: str, is_over_str: str):
746
- from js import window, document
747
-
748
- now = time.time()
749
- decisions = base64.b64decode(str(decisions_str))
750
- logs: list = json.loads(str(logs_str))
751
- is_over = str(is_over_str) == "true"
752
-
753
- self._decisions.append(decisions)
754
- self._logs.append(logs)
755
-
756
- if is_over:
757
- try:
758
- simulation = self._get_simulation()
759
- window.simulationToDownload = simulation.dump()
760
- show_download()
761
- except Exception as e:
762
- print(e)
763
-
764
- render_status = document.getElementById("render-status")
765
- if render_status is not None:
766
- render_status.textContent = (
767
- f"Rendering: Complete! ({int(now - self._start_time)}s)"
768
- if is_over
769
- else f"Rendering: Frame {len(self._decisions)} ({int(now - self._start_time)}s)"
770
- )
771
-
772
- def _get_initial_player_globals(self, player_codes: List[str]):
773
- contexts = [
774
- self.create_api_implementation(i) for i in range(len(self.player_names))
775
- ]
776
- bot_base_class_name = self.configure_bot_base_class_name()
777
-
778
- player_globals = [
779
- {
780
- "api": self.get_api(),
781
- bot_base_class_name: getattr(self.get_api(), bot_base_class_name),
782
- "player_api": None,
783
- "context": context,
784
- **self.get_api().__dict__,
785
- }
786
- | self.configure_bot_globals(player_index)
787
- for player_index, context in enumerate(contexts)
788
- ]
789
- for index, api_code in enumerate(player_codes):
790
- if api_code != "" and api_code is not None:
791
- if f"class MyBot({bot_base_class_name}):" not in api_code:
792
- show_alert(
793
- f"Code Exception in 'Player {index + 1}' API!",
794
- f"Missing line:\nclass MyBot({bot_base_class_name}):",
795
- "red",
796
- "fa-solid fa-exclamation",
797
- )
798
- continue
799
-
800
- lines = [
801
- ""
802
- if (line.startswith("from") or line.startswith("import"))
803
- else line
804
- for line in api_code.splitlines()
805
- ]
806
- lines = "\n".join(lines)
807
- lines = lines.replace("class MyBot", f"class Player{index}Bot")
808
- try:
809
- exec(lines, player_globals[index])
810
- exec(
811
- f"player_api = Player{index}Bot(context)",
812
- player_globals[index],
813
- )
814
- except Exception:
815
- lines = traceback.format_exc().splitlines()
816
- string_file_indices = []
817
- for i, line in enumerate(lines):
818
- if "<string>" in line:
819
- string_file_indices.append(i)
820
- output = lines[0] + "\n"
821
- for i in string_file_indices:
822
- output += (
823
- lines[i].strip().replace('File "<string>", line', "Line")
824
- + "\n"
825
- )
826
- output += lines[string_file_indices[-1] + 1].strip() + "\n"
827
-
828
- show_alert(
829
- f"Code Exception in 'Player {index + 1}' API!",
830
- output,
831
- "red",
832
- "fa-solid fa-exclamation",
833
- )
834
-
835
- return player_globals
836
-
837
- @web_only
838
- def _resize_canvas(self):
839
- from js import document
840
-
841
- if not hasattr(self, "canvas"):
842
- return
843
-
844
- self.canvas._fit_into(
845
- document.body.clientWidth - 440
846
- if self.console_visible
847
- else document.body.clientWidth - 40,
848
- document.body.clientHeight - 280
849
- if self.console_visible
850
- else document.body.clientHeight - 160,
851
- )
852
- if not self.background:
853
- self.render()
854
-
855
- @web_only
856
- def _step(self):
857
- from js import document, setTimeout
858
- from pyscript.ffi import create_proxy
859
-
860
- if not self.over:
861
- if len(self._decisions) == self._decision_index:
862
- print("Warning: sleeping because decisions were not made yet!")
863
- setTimeout(create_proxy(self._step), 100)
864
- return
865
- else:
866
- logs = self._logs[self._decision_index]
867
- for log in logs:
868
- console_log(
869
- -1 if log["player_index"] is None else log["player_index"],
870
- log["text"],
871
- log["color"],
872
- )
873
- self.apply_decisions(self._decisions[self._decision_index])
874
- self._decision_index += 1
875
-
876
- if not self.over:
877
- self.step += 1
878
-
879
- if self.over:
880
- if len(self.active_players) == 1:
881
- self._eliminated.append(self.active_players[0])
882
- set_results(
883
- self.player_names, self._eliminated[::-1], self.map, self.verbose
884
- )
885
- if not self.background:
886
- self.render()
887
-
888
- if not self.background:
889
- if self._since_last_render >= self.configure_render_rate(
890
- self._get_playback_speed()
891
- ):
892
- self.render()
893
- self._since_last_render = 1
894
- else:
895
- self._since_last_render += 1
896
-
897
- if (
898
- self.over
899
- and "Pause" in document.getElementById("playpause").textContent
900
- ):
901
- # Make it apparent that the game is stopped.
902
- document.getElementById("playpause").click()
903
- elif self.verbose:
904
- document.getElementById("noui-progress").style.display = "block"
905
- document.getElementById(
906
- "noui-progress"
907
- ).textContent = f"Simulating T{self.time}s..."
908
- if self.over:
909
- document.getElementById("noui-progress").style.display = "none"
910
-
911
- @web_only
912
- def _should_play(self):
913
- from js import document
914
-
915
- if self.over:
916
- return False
917
-
918
- if self.background:
919
- return True
920
-
921
- if "Pause" not in document.getElementById("playpause").textContent:
922
- return False
923
-
924
- if self.step == self._get_breakpoint():
925
- return False
926
-
927
- return True
928
-
929
- @web_only
930
- def _get_playback_speed(self):
931
- from js import document
932
-
933
- return 2 ** float(
934
- document.getElementById("timescale")
935
- .getElementsByClassName("mantine-Slider-thumb")
936
- .to_py()[0]
937
- .ariaValueNow
938
- )
939
-
940
- @web_only
941
- def _get_breakpoint(self):
942
- from js import document
943
-
944
- breakpoint_element = document.getElementById("breakpoint")
945
- if breakpoint_element is None or breakpoint_element.value == "":
946
- return -1
947
-
948
- try:
949
- return int(breakpoint_element.value)
950
- except Exception:
951
- return -1
952
-
953
- async def _play_pause(self):
954
- await asyncio.sleep(0.05)
955
- while self._should_play():
956
- start = time.time()
957
- try:
958
- self._step()
959
- except Exception:
960
- traceback.print_exc()
961
- if not self.background:
962
- await asyncio.sleep(
963
- max(
964
- 1
965
- / self.configure_steps_per_second()
966
- / self._get_playback_speed()
967
- - (time.time() - start),
968
- 0,
969
- )
970
- )