code-battles 1.5.9 → 1.6.1

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.
package/dist/cjs/index.js CHANGED
@@ -2520,7 +2520,7 @@ const RunSimulationBlock = () => {
2520
2520
  };
2521
2521
  const startRunNoUI = () => {
2522
2522
  setRunningNoUI(true);
2523
- runNoUI(map, apis, playerBots, seed.toString(), true);
2523
+ runNoUI(map, apis, playerBots, seed.toString(), false);
2524
2524
  };
2525
2525
  const startRunNoUIN = (n) => {
2526
2526
  setLocalStorage("Results", {});
@@ -14575,7 +14575,7 @@ const Round = () => {
14575
14575
  React.createElement(core.Button, { leftSection: React.createElement("i", { className: "fa-solid fa-play" }), size: "xs", onClick: () => navigate(`/simulation/${round.map.replaceAll(" ", "-")}/${round.players.join("-")}?showcase=true`) }, "Simulate"),
14576
14576
  React.createElement(core.Button, { leftSection: React.createElement("i", { className: "fa-solid fa-forward" }), size: "xs", onClick: () => {
14577
14577
  if (roundIterations === 1) {
14578
- runNoUI(round.map, apis, round.players, "", true);
14578
+ runNoUI(round.map, apis, round.players, "", false);
14579
14579
  }
14580
14580
  else {
14581
14581
  currentMap = round.map;
@@ -16086,7 +16086,7 @@ const Simulation = () => {
16086
16086
  React.createElement(PlayPauseButton, null),
16087
16087
  downloadBytes && (React.createElement(core.Button, { ml: "xs", radius: "20px", leftSection: React.createElement("i", { className: "fa-solid fa-download" }), color: "blue", onClick: () => downloadFile(`${playerNames.join("-")}.btl`, "text/plain",
16088
16088
  // @ts-ignore
16089
- window.simulationToDownload) }, "Download Simulation"))),
16089
+ window.simulationToDownload) }, "Download"))),
16090
16090
  showcaseMode && React.createElement("span", { id: "render-status" }),
16091
16091
  React.createElement("p", { style: { margin: 0 } }, "Playback Speed"),
16092
16092
  React.createElement(core.Slider, { style: { flex: "none" }, mb: 30, w: 500, maw: "85%", min: -2, defaultValue: 0, marks: [
@@ -10,7 +10,7 @@ import sys
10
10
  import traceback
11
11
  import gzip
12
12
 
13
- from typing import Any, Dict, Generic, List, Optional, Tuple, TypeVar
13
+ from typing import Any, Dict, Generic, List, Optional, Set, Tuple, TypeVar
14
14
  from code_battles.utilities import (
15
15
  GameCanvas,
16
16
  console_log,
@@ -135,6 +135,7 @@ class CodeBattles(
135
135
  _eliminated: List[int]
136
136
  _sounds: Dict[str, "js.Audio"] = {}
137
137
  _decisions: List[bytes]
138
+ _breakpoints: Set[int]
138
139
  _since_last_render: int
139
140
 
140
141
  def render(self) -> None:
@@ -292,6 +293,16 @@ class CodeBattles(
292
293
  """Configure the version of the game, which is stored in the simulation files."""
293
294
  return "1.0.0"
294
295
 
296
+ @web_only
297
+ def download_image(self, url: str) -> "asyncio.Future[js.Image]":
298
+ from js import Image
299
+
300
+ result = asyncio.Future()
301
+ image = Image.new()
302
+ image.onload = lambda _: result.set_result(image)
303
+ image.src = url
304
+ return result
305
+
295
306
  @web_only
296
307
  def download_images(
297
308
  self, sources: List[Tuple[str, str]]
@@ -393,7 +404,7 @@ class CodeBattles(
393
404
  self.play_sound("player_eliminated")
394
405
  self._eliminated.append(player_index)
395
406
  self.log(
396
- f"[Game T{self.step}] Player #{player_index + 1} ({self.player_names[player_index]}) was eliminated: {reason}",
407
+ f"[Battles {self.step + 1}] Player #{player_index + 1} ({self.player_names[player_index]}) was eliminated: {reason}",
397
408
  -1,
398
409
  "white",
399
410
  )
@@ -420,10 +431,17 @@ class CodeBattles(
420
431
  )
421
432
 
422
433
  @web_only
423
- def play_sound(self, sound: str):
424
- """Plays the given sound, from the URL given by :func:`configure_sound_url`."""
434
+ def play_sound(self, sound: str, force=False):
435
+ """
436
+ Plays the given sound, from the URL given by :func:`configure_sound_url`.
437
+
438
+ If ``force`` is set, will play the sound even if the simulation is not :attr:`verbose`.
439
+ """
425
440
  from js import window, Audio
426
441
 
442
+ if not force and not self.verbose:
443
+ return
444
+
427
445
  if sound not in self._sounds:
428
446
  self._sounds[sound] = Audio.new(self.configure_sound_url(sound))
429
447
 
@@ -441,6 +459,14 @@ class CodeBattles(
441
459
 
442
460
  asyncio.get_event_loop().run_until_complete(p())
443
461
 
462
+ def pause(self):
463
+ """
464
+ Pauses the current simulation. Useful for letting bots insert breakpoints wherever they wish.
465
+
466
+ **Important:** call this method only from the :func:`make_decisions` method.
467
+ """
468
+ self._should_pause = True
469
+
444
470
  @property
445
471
  def time(self) -> str:
446
472
  """The current step of the simulation, as a string with justification to fill 5 characters."""
@@ -473,6 +499,7 @@ class CodeBattles(
473
499
  seed = Random().randint(0, 2**128)
474
500
  self._logs = []
475
501
  self._decisions = []
502
+ self._breakpoints = set()
476
503
  self._decision_index = 0
477
504
  self._seed = seed
478
505
  self.step = 0
@@ -511,6 +538,7 @@ class CodeBattles(
511
538
  self.verbose = False
512
539
  self._initialize_simulation(player_codes, seed)
513
540
  while not self.over:
541
+ self._should_pause = False
514
542
  self._logs = []
515
543
  decisions = self.make_decisions()
516
544
  logs = self._logs
@@ -521,6 +549,7 @@ class CodeBattles(
521
549
  base64.b64encode(decisions).decode(),
522
550
  json.dumps(logs),
523
551
  "true" if self.over else "false",
552
+ "true" if self._should_pause else "false",
524
553
  )
525
554
 
526
555
  if not self.over:
@@ -644,7 +673,7 @@ class CodeBattles(
644
673
  self.player_names = simulation.player_names
645
674
  self.background = False
646
675
  self.console_visible = True
647
- self.verbose = False
676
+ self.verbose = True
648
677
  self._initialize_simulation(
649
678
  ["" for _ in simulation.player_names], simulation.seed
650
679
  )
@@ -654,8 +683,8 @@ class CodeBattles(
654
683
  document.getElementById("simulation"),
655
684
  self.configure_board_count(),
656
685
  self.map_image,
657
- document.body.clientWidth - 440,
658
- document.body.clientHeight - 280,
686
+ self._get_canvas_width(),
687
+ self._get_canvas_height(),
659
688
  self.configure_extra_width(),
660
689
  self.configure_extra_height(),
661
690
  )
@@ -701,12 +730,8 @@ class CodeBattles(
701
730
  document.getElementById("simulation"),
702
731
  self.configure_board_count(),
703
732
  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,
733
+ self._get_canvas_width(),
734
+ self._get_canvas_height(),
710
735
  self.configure_extra_width(),
711
736
  self.configure_extra_height(),
712
737
  )
@@ -730,6 +755,26 @@ class CodeBattles(
730
755
  except Exception:
731
756
  traceback.print_exc()
732
757
 
758
+ @web_only
759
+ def _get_canvas_width(self):
760
+ from js import document
761
+
762
+ return (
763
+ document.body.clientWidth - 440
764
+ if self.console_visible
765
+ else document.body.clientWidth - 40
766
+ )
767
+
768
+ @web_only
769
+ def _get_canvas_height(self):
770
+ from js import document
771
+
772
+ return (
773
+ document.body.clientHeight - 255
774
+ if self.console_visible
775
+ else document.body.clientHeight - 180
776
+ )
777
+
733
778
  def _get_simulation(self):
734
779
  return Simulation(
735
780
  self.map,
@@ -742,14 +787,19 @@ class CodeBattles(
742
787
  self._seed,
743
788
  )
744
789
 
745
- def _update_step(self, decisions_str: str, logs_str: str, is_over_str: str):
790
+ def _update_step(
791
+ self, decisions_str: str, logs_str: str, is_over_str: str, should_pause_str: str
792
+ ):
746
793
  from js import window, document
747
794
 
748
795
  now = time.time()
749
796
  decisions = base64.b64decode(str(decisions_str))
750
797
  logs: list = json.loads(str(logs_str))
751
798
  is_over = str(is_over_str) == "true"
799
+ should_pause = str(should_pause_str) == "true"
752
800
 
801
+ if should_pause:
802
+ self._breakpoints.add(len(self._decisions))
753
803
  self._decisions.append(decisions)
754
804
  self._logs.append(logs)
755
805
 
@@ -836,22 +886,24 @@ class CodeBattles(
836
886
 
837
887
  @web_only
838
888
  def _resize_canvas(self):
839
- from js import document
840
-
841
889
  if not hasattr(self, "canvas"):
842
890
  return
843
891
 
844
892
  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,
893
+ self._get_canvas_width(),
894
+ self._get_canvas_height(),
851
895
  )
852
896
  if not self.background:
853
897
  self.render()
854
898
 
899
+ @web_only
900
+ def _ensure_paused(self):
901
+ from js import document
902
+
903
+ if "Pause" in document.getElementById("playpause").textContent:
904
+ # Make it apparent that the game is stopped.
905
+ document.getElementById("playpause").click()
906
+
855
907
  @web_only
856
908
  def _step(self):
857
909
  from js import document, setTimeout
@@ -894,12 +946,8 @@ class CodeBattles(
894
946
  else:
895
947
  self._since_last_render += 1
896
948
 
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()
949
+ if self.over:
950
+ self._ensure_paused()
903
951
  elif self.verbose:
904
952
  document.getElementById("noui-progress").style.display = "block"
905
953
  document.getElementById(
@@ -924,6 +972,12 @@ class CodeBattles(
924
972
  if self.step == self._get_breakpoint():
925
973
  return False
926
974
 
975
+ if self.step in self._breakpoints and self.console_visible:
976
+ self._breakpoints.remove(self.step)
977
+ self._ensure_paused()
978
+ self.log(f"[Battles {self.step + 1}] Pause requested!")
979
+ return False
980
+
927
981
  return True
928
982
 
929
983
  @web_only
@@ -5,6 +5,7 @@ import math
5
5
  import sys
6
6
  from typing import Callable, List, Union
7
7
  from enum import Enum
8
+ from functools import wraps
8
9
 
9
10
  try:
10
11
  import js
@@ -33,6 +34,7 @@ def web_only(method):
33
34
  if is_web():
34
35
  return method
35
36
 
37
+ @wraps(method)
36
38
  def wrapper(*args, **kwargs):
37
39
  print(f"Warning: {method.__name__} should only be called in a web context.")
38
40
 
@@ -223,6 +225,10 @@ class GameCanvas:
223
225
  stroke_width=10,
224
226
  board_index=0,
225
227
  ):
228
+ """
229
+ Draws a line between the given ``(start_x, start_y)`` and ``(end_x, end_y)`` coordinates (in map pixels) with the given stroke.
230
+ """
231
+
226
232
  start_x, start_y = self._translate_position(board_index, start_x, start_y)
227
233
  end_x, end_y = self._translate_position(board_index, end_x, end_y)
228
234
 
@@ -233,6 +239,33 @@ class GameCanvas:
233
239
  self.context.lineTo(end_x, end_y)
234
240
  self.context.stroke()
235
241
 
242
+ def draw_rectangle(
243
+ self,
244
+ start_x: int,
245
+ start_y: int,
246
+ width: int,
247
+ height: int,
248
+ fill="black",
249
+ stroke="transparent",
250
+ stroke_width=2,
251
+ board_index=0,
252
+ ):
253
+ """
254
+ Draws the given rectangle with the top-left corner at `(start_x, start_y)` (in map pixels) and with the specified `width` and `height` (in map pixels) with the given stroke and fill.
255
+ """
256
+
257
+ start_x, start_y = self._translate_position(board_index, start_x, start_y)
258
+ width *= self._scale
259
+ height *= self._scale
260
+
261
+ self.context.fillStyle = fill
262
+ self.context.strokeStyle = stroke
263
+ self.context.lineWidth = stroke_width * self._scale
264
+ self.context.beginPath()
265
+ self.context.rect(start_x, start_y, width, height)
266
+ self.context.stroke()
267
+ self.context.fill()
268
+
236
269
  def draw_circle(
237
270
  self,
238
271
  x: int,
package/dist/esm/index.js CHANGED
@@ -2518,7 +2518,7 @@ const RunSimulationBlock = () => {
2518
2518
  };
2519
2519
  const startRunNoUI = () => {
2520
2520
  setRunningNoUI(true);
2521
- runNoUI(map, apis, playerBots, seed.toString(), true);
2521
+ runNoUI(map, apis, playerBots, seed.toString(), false);
2522
2522
  };
2523
2523
  const startRunNoUIN = (n) => {
2524
2524
  setLocalStorage("Results", {});
@@ -14573,7 +14573,7 @@ const Round = () => {
14573
14573
  React.createElement(Button, { leftSection: React.createElement("i", { className: "fa-solid fa-play" }), size: "xs", onClick: () => navigate(`/simulation/${round.map.replaceAll(" ", "-")}/${round.players.join("-")}?showcase=true`) }, "Simulate"),
14574
14574
  React.createElement(Button, { leftSection: React.createElement("i", { className: "fa-solid fa-forward" }), size: "xs", onClick: () => {
14575
14575
  if (roundIterations === 1) {
14576
- runNoUI(round.map, apis, round.players, "", true);
14576
+ runNoUI(round.map, apis, round.players, "", false);
14577
14577
  }
14578
14578
  else {
14579
14579
  currentMap = round.map;
@@ -16084,7 +16084,7 @@ const Simulation = () => {
16084
16084
  React.createElement(PlayPauseButton, null),
16085
16085
  downloadBytes && (React.createElement(Button, { ml: "xs", radius: "20px", leftSection: React.createElement("i", { className: "fa-solid fa-download" }), color: "blue", onClick: () => downloadFile(`${playerNames.join("-")}.btl`, "text/plain",
16086
16086
  // @ts-ignore
16087
- window.simulationToDownload) }, "Download Simulation"))),
16087
+ window.simulationToDownload) }, "Download"))),
16088
16088
  showcaseMode && React.createElement("span", { id: "render-status" }),
16089
16089
  React.createElement("p", { style: { margin: 0 } }, "Playback Speed"),
16090
16090
  React.createElement(Slider, { style: { flex: "none" }, mb: 30, w: 500, maw: "85%", min: -2, defaultValue: 0, marks: [
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-battles",
3
- "version": "1.5.9",
3
+ "version": "1.6.1",
4
4
  "description": "A library for building interactive competitive coding battles",
5
5
  "repository": "https://github.com/noamzaks/code-battles",
6
6
  "homepage": "https://code-battles.readthedocs.org",