code-battles 1.0.0 → 1.1.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.
@@ -21,7 +21,9 @@ APIType = TypeVar("APIType")
21
21
  PlayerRequestsType = TypeVar("PlayerRequestsType")
22
22
 
23
23
 
24
- class CodeBattles(Generic[GameStateType, APIImplementationType, APIType, PlayerRequestsType]):
24
+ class CodeBattles(
25
+ Generic[GameStateType, APIImplementationType, APIType, PlayerRequestsType]
26
+ ):
25
27
  """
26
28
  The base class for a Code Battles game.
27
29
 
@@ -92,15 +94,15 @@ class CodeBattles(Generic[GameStateType, APIImplementationType, APIType, PlayerR
92
94
  """
93
95
 
94
96
  raise NotImplementedError("make_decisions")
95
-
97
+
96
98
  def apply_decisions(self, decisions: bytes) -> None:
97
99
  """
98
100
  **You must override this method.**
99
101
 
100
102
  Use the current state and the specified decisions to update the current state to be the next state.
101
-
103
+
102
104
  This function should not take a lot of time.
103
-
105
+
104
106
  Do NOT update :attr:`step`.
105
107
  """
106
108
 
@@ -123,13 +125,13 @@ class CodeBattles(Generic[GameStateType, APIImplementationType, APIType, PlayerR
123
125
  """
124
126
 
125
127
  raise NotImplementedError("create_initial_state")
126
-
128
+
127
129
  def create_initial_player_requests(self, player_index: int) -> PlayerRequestsType:
128
130
  """
129
131
  **You must override this method.**
130
132
 
131
133
  Create the initial player requests for each simulation, to store in the :attr:`player_requests` attribute.
132
-
134
+
133
135
  Should probably be empty.
134
136
  """
135
137
 
@@ -157,6 +159,11 @@ class CodeBattles(Generic[GameStateType, APIImplementationType, APIType, PlayerR
157
159
 
158
160
  pass
159
161
 
162
+ def configure_extra_width(self) -> int:
163
+ """Optionally add extra height to the right of the boards. 0 by default."""
164
+
165
+ return 0
166
+
160
167
  def configure_extra_height(self) -> int:
161
168
  """Optionally add extra height below the boards. 0 by default."""
162
169
 
@@ -187,6 +194,24 @@ class CodeBattles(Generic[GameStateType, APIImplementationType, APIType, PlayerR
187
194
 
188
195
  return "CodeBattlesBot"
189
196
 
197
+ def configure_bot_globals(self) -> Dict[str, Any]:
198
+ """
199
+ Configure additional available global items, such as libraries from the Python standard library, bots can use.
200
+
201
+ By default, this is math, time and random.
202
+
203
+ .. warning::
204
+ 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`.
205
+
206
+ Any additional imports will be stripped (not as a security mechanism).
207
+ """
208
+
209
+ return {
210
+ "math": math,
211
+ "time": time,
212
+ "random": random,
213
+ }
214
+
190
215
  def download_images(
191
216
  self, sources: List[Tuple[str, str]]
192
217
  ) -> asyncio.Future[Dict[str, Image]]:
@@ -237,7 +262,7 @@ class CodeBattles(Generic[GameStateType, APIImplementationType, APIType, PlayerR
237
262
  def run_bot_method(self, player_index: int, method_name: str):
238
263
  """
239
264
  Runs the specifid method of the given player.
240
-
265
+
241
266
  Upon exception, shows an alert (does not terminate the bot).
242
267
  """
243
268
 
@@ -268,7 +293,6 @@ class CodeBattles(Generic[GameStateType, APIImplementationType, APIType, PlayerR
268
293
  "fa-solid fa-exclamation",
269
294
  )
270
295
 
271
-
272
296
  def eliminate_player(self, player_index: int, reason=""):
273
297
  """Eliminate the specified player for the specified reason from the simulation."""
274
298
 
@@ -337,41 +361,47 @@ class CodeBattles(Generic[GameStateType, APIImplementationType, APIType, PlayerR
337
361
  console_visible: bool,
338
362
  verbose: bool,
339
363
  ):
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
+ try:
365
+ self.map = map
366
+ self.player_names = player_names
367
+ self.map_image = await download_image(self.configure_map_image_url(map))
368
+ self.background = background
369
+ self.console_visible = console_visible
370
+ self.verbose = verbose
371
+ self.step = 0
372
+ self.active_players = list(range(len(player_names)))
373
+ self.state = self.create_initial_state()
374
+ self.player_requests = [
375
+ self.create_initial_player_requests(i) for i in range(len(player_names))
376
+ ]
377
+ self._eliminated = []
378
+ self._player_globals = self._get_initial_player_globals(player_codes)
379
+ if not self.background:
380
+ self.canvas = GameCanvas(
381
+ document.getElementById("simulation"),
382
+ self.configure_board_count(),
383
+ self.map_image,
384
+ document.body.clientWidth - 440
385
+ if console_visible
386
+ else document.body.clientWidth - 40,
387
+ document.body.clientHeight - 280,
388
+ self.configure_extra_width(),
389
+ self.configure_extra_height(),
390
+ )
391
+ await self.setup()
364
392
 
365
- if not self.background:
366
- if not hasattr(self, "_initialized"):
367
- self._initialize()
393
+ if not self.background:
394
+ if not hasattr(self, "_initialized"):
395
+ self._initialize()
368
396
 
369
- # Show that loading finished
370
- document.getElementById("loader").style.display = "none"
371
- self.render()
397
+ # Show that loading finished
398
+ document.getElementById("loader").style.display = "none"
399
+ self.render()
372
400
 
373
- if self.background:
374
- await self._play_pause()
401
+ if self.background:
402
+ await self._play_pause()
403
+ except Exception:
404
+ traceback.print_exc()
375
405
 
376
406
  def _get_initial_player_globals(self, player_codes: List[str]):
377
407
  contexts = [
@@ -381,15 +411,13 @@ class CodeBattles(Generic[GameStateType, APIImplementationType, APIType, PlayerR
381
411
 
382
412
  player_globals = [
383
413
  {
384
- "math": math,
385
- "time": time,
386
- "random": random,
387
414
  "api": self.get_api(),
388
415
  bot_base_class_name: getattr(self.get_api(), bot_base_class_name),
389
416
  "player_api": None,
390
417
  "context": context,
391
418
  **self.get_api().__dict__,
392
419
  }
420
+ | self.configure_bot_globals()
393
421
  for context in contexts
394
422
  ]
395
423
  for index, api_code in enumerate(player_codes):
@@ -456,6 +484,8 @@ class CodeBattles(Generic[GameStateType, APIImplementationType, APIType, PlayerR
456
484
  def _step(self):
457
485
  if not self.over:
458
486
  self.apply_decisions(self.make_decisions())
487
+
488
+ if not self.over:
459
489
  self.step += 1
460
490
 
461
491
  if self.over:
@@ -518,11 +548,16 @@ class CodeBattles(Generic[GameStateType, APIImplementationType, APIType, PlayerR
518
548
  await asyncio.sleep(0.05)
519
549
  while self._should_play():
520
550
  start = time.time()
521
- self._step()
551
+ try:
552
+ self._step()
553
+ except Exception:
554
+ traceback.print_exc()
522
555
  if not self.background:
523
556
  await asyncio.sleep(
524
557
  max(
525
- 1 / self.configure_steps_per_second() / self._get_playback_speed()
558
+ 1
559
+ / self.configure_steps_per_second()
560
+ / self._get_playback_speed()
526
561
  - (time.time() - start),
527
562
  0,
528
563
  )
@@ -14,7 +14,23 @@ class TwoDContext:
14
14
  ]
15
15
  font: str
16
16
  fillStyle: str
17
+ strokeStyle: str
17
18
 
19
+ @staticmethod
20
+ def beginPath(): ...
21
+ @staticmethod
22
+ def arc(
23
+ x: int,
24
+ y: int,
25
+ radius: float,
26
+ startAngle: float,
27
+ endAngle: float,
28
+ counterclockwise=False,
29
+ ): ...
30
+ @staticmethod
31
+ def stroke(): ...
32
+ @staticmethod
33
+ def fill(): ...
18
34
  @staticmethod
19
35
  def fillText(text: str, x: int, y: int): ...
20
36
  @staticmethod
@@ -1,6 +1,7 @@
1
1
  """Generic useful utilities for creating games with PyScript."""
2
2
 
3
3
  import asyncio
4
+ import math
4
5
  from typing import Callable, List, Union
5
6
  from enum import Enum
6
7
 
@@ -75,12 +76,14 @@ class GameCanvas:
75
76
  map_image: Image,
76
77
  max_width: int,
77
78
  max_height: int,
79
+ extra_width: int,
78
80
  extra_height: int,
79
81
  ):
80
82
  self.canvas = canvas
81
83
  self.player_count = player_count
82
84
  self.map_image = map_image
83
85
  self.extra_height = extra_height
86
+ self.extra_width = extra_width
84
87
 
85
88
  self._fit_into(max_width, max_height)
86
89
 
@@ -140,6 +143,23 @@ class GameCanvas:
140
143
  self.context.fillStyle = color
141
144
  self.context.fillText(text, x, y)
142
145
 
146
+ def draw_circle(
147
+ self,
148
+ x: int,
149
+ y: int,
150
+ radius: float,
151
+ fill="black",
152
+ stroke="transparent",
153
+ board_index=0,
154
+ ):
155
+ x, y = self._translate_position(board_index, x, y)
156
+ self.context.fillStyle = fill
157
+ self.context.strokeStyle = stroke
158
+ self.context.beginPath()
159
+ self.context.arc(x, y, radius, 0, 2 * math.pi)
160
+ self.context.stroke()
161
+ self.context.fill()
162
+
143
163
  def clear(self):
144
164
  """Clears the canvas and re-draws the players' maps."""
145
165
 
@@ -165,10 +185,8 @@ class GameCanvas:
165
185
  def _fit_into(self, max_width: int, max_height: int):
166
186
  if self.map_image.width == 0 or self.map_image.height == 0:
167
187
  raise Exception("Map image invalid!")
168
- aspect_ratio = (
169
- self.map_image.width
170
- * self.player_count
171
- / (self.map_image.height + self.extra_height)
188
+ aspect_ratio = (self.map_image.width * self.player_count + self.extra_width) / (
189
+ self.map_image.height + self.extra_height
172
190
  )
173
191
  width = min(max_width, max_height * aspect_ratio)
174
192
  height = width / aspect_ratio
@@ -176,12 +194,16 @@ class GameCanvas:
176
194
  self.canvas.style.height = f"{height}px"
177
195
  self.canvas.width = width * window.devicePixelRatio
178
196
  self.canvas.height = height * window.devicePixelRatio
179
- self._scale = self.canvas.width / self.player_count / self.map_image.width
197
+ self._scale = self.canvas.width / (
198
+ self.player_count * self.map_image.width + self.extra_width
199
+ )
180
200
  self.context = self.canvas.getContext("2d")
181
201
  self.context.textAlign = "center"
182
202
  self.context.textBaseline = "middle"
183
203
 
184
- self.canvas_map_width = self.canvas.width / self.player_count
204
+ self.canvas_map_width = (
205
+ self.canvas.width - self._scale * self.extra_width
206
+ ) / self.player_count
185
207
  self.canvas_map_height = (
186
208
  self.canvas_map_width * self.map_image.height / self.map_image.width
187
209
  )
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-battles",
3
- "version": "1.0.0",
3
+ "version": "1.1.0",
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",