code-battles 1.2.1 → 1.3.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.
- package/dist/cjs/index.js +1 -0
- package/dist/code_battles/__init__.py +13 -5
- package/dist/code_battles/battles.py +120 -18
- package/dist/code_battles/js.pyi +5 -0
- package/dist/code_battles/pyscript/__init__.pyi +4 -0
- package/dist/code_battles/utilities.py +81 -10
- package/dist/code_battles/worker.py +6 -0
- package/dist/esm/index.js +1 -0
- package/package.json +6 -6
package/dist/cjs/index.js
CHANGED
|
@@ -16020,6 +16020,7 @@ const Simulation = () => {
|
|
|
16020
16020
|
!location.search.includes("background=true") && (React.createElement(React.Fragment, null,
|
|
16021
16021
|
React.createElement("div", { style: { textAlign: "center", flexGrow: 1 } },
|
|
16022
16022
|
showcaseMode || (React.createElement(React.Fragment, null,
|
|
16023
|
+
React.createElement("span", { id: "render-status", style: { marginRight: 10 } }),
|
|
16023
16024
|
React.createElement(core.NumberInput, { id: "breakpoint", label: "Breakpoint", min: 0, leftSection: React.createElement("i", { className: "fa-solid fa-stopwatch" }), display: "inline-block", maw: "35%", mr: "xs" }),
|
|
16024
16025
|
React.createElement(core.Button, { style: { flex: "none" }, my: "xs", w: 100, leftSection: React.createElement("i", { className: "fa-solid fa-wand-magic" }), color: "grape", id: "step", mr: "xs", radius: "20px" }, "Step"))),
|
|
16025
16026
|
React.createElement(PlayPauseButton, null)),
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import sys
|
|
2
2
|
|
|
3
|
-
from code_battles.utilities import GameCanvas, Alignment
|
|
4
|
-
from code_battles.battles import CodeBattles
|
|
5
|
-
from js import window
|
|
6
|
-
from pyscript.ffi import create_proxy
|
|
3
|
+
from code_battles.utilities import GameCanvas, Alignment, is_web, is_worker
|
|
4
|
+
from code_battles.battles import CodeBattles
|
|
7
5
|
|
|
8
6
|
|
|
9
7
|
def run_game(battles: CodeBattles):
|
|
@@ -11,8 +9,18 @@ def run_game(battles: CodeBattles):
|
|
|
11
9
|
Binds the given code battles instance to the React code to enable all simulations.
|
|
12
10
|
"""
|
|
13
11
|
|
|
14
|
-
if is_web:
|
|
12
|
+
if is_web():
|
|
13
|
+
from js import window
|
|
14
|
+
from pyscript.ffi import create_proxy
|
|
15
|
+
|
|
15
16
|
window._startSimulation = create_proxy(battles._start_simulation)
|
|
17
|
+
elif is_worker():
|
|
18
|
+
setattr(
|
|
19
|
+
sys.modules["__main__"],
|
|
20
|
+
"_run_webworker_simulation",
|
|
21
|
+
battles._run_webworker_simulation,
|
|
22
|
+
)
|
|
23
|
+
setattr(sys.modules["__main__"], "__export__", ["_run_webworker_simulation"])
|
|
16
24
|
else:
|
|
17
25
|
battles._run_local_simulation()
|
|
18
26
|
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import base64
|
|
2
3
|
import json
|
|
3
4
|
import math
|
|
4
5
|
import time
|
|
@@ -13,25 +14,20 @@ from code_battles.utilities import (
|
|
|
13
14
|
download_image,
|
|
14
15
|
set_results,
|
|
15
16
|
show_alert,
|
|
17
|
+
web_only,
|
|
18
|
+
is_web,
|
|
16
19
|
)
|
|
17
|
-
|
|
18
|
-
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
import js
|
|
23
|
+
except Exception:
|
|
24
|
+
pass
|
|
19
25
|
|
|
20
26
|
GameStateType = TypeVar("GameStateType")
|
|
21
27
|
APIImplementationType = TypeVar("APIImplementationType")
|
|
22
28
|
APIType = TypeVar("APIType")
|
|
23
29
|
PlayerRequestsType = TypeVar("PlayerRequestsType")
|
|
24
30
|
|
|
25
|
-
is_web = "MicroPython" in sys.version or "pyodide" in sys.executable
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
def web_only(method):
|
|
29
|
-
def wrapper(*args, **kwargs):
|
|
30
|
-
if is_web:
|
|
31
|
-
return method(*args, **kwargs)
|
|
32
|
-
|
|
33
|
-
return wrapper
|
|
34
|
-
|
|
35
31
|
|
|
36
32
|
class CodeBattles(
|
|
37
33
|
Generic[GameStateType, APIImplementationType, APIType, PlayerRequestsType]
|
|
@@ -56,7 +52,7 @@ class CodeBattles(
|
|
|
56
52
|
"""The name of the players. This is populated before any of the overridable methods run."""
|
|
57
53
|
map: str
|
|
58
54
|
"""The name of the map. This is populated before any of the overridable methods run."""
|
|
59
|
-
map_image: Image
|
|
55
|
+
map_image: "js.Image"
|
|
60
56
|
"""The map image. This is populated before any of the overridable methods run."""
|
|
61
57
|
canvas: GameCanvas
|
|
62
58
|
"""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`."""
|
|
@@ -79,7 +75,8 @@ class CodeBattles(
|
|
|
79
75
|
_player_globals: List[Dict[str, Any]]
|
|
80
76
|
_initialized: bool
|
|
81
77
|
_eliminated: List[int]
|
|
82
|
-
_sounds: Dict[str, Audio] = {}
|
|
78
|
+
_sounds: Dict[str, "js.Audio"] = {}
|
|
79
|
+
_decisions: List[bytes]
|
|
83
80
|
|
|
84
81
|
def render(self) -> None:
|
|
85
82
|
"""
|
|
@@ -227,11 +224,12 @@ class CodeBattles(
|
|
|
227
224
|
@web_only
|
|
228
225
|
def download_images(
|
|
229
226
|
self, sources: List[Tuple[str, str]]
|
|
230
|
-
) -> asyncio.Future[Dict[str, Image]]:
|
|
227
|
+
) -> asyncio.Future[Dict[str, "js.Image"]]:
|
|
231
228
|
"""
|
|
232
229
|
:param sources: A list of ``(image_name, image_url)`` to download.
|
|
233
230
|
:returns: A future which can be ``await``'d containing a dictionary mapping each ``image_name`` to its loaded image.
|
|
234
231
|
"""
|
|
232
|
+
from js import Image
|
|
235
233
|
|
|
236
234
|
remaining_images: List[str] = []
|
|
237
235
|
result = asyncio.Future()
|
|
@@ -268,6 +266,7 @@ class CodeBattles(
|
|
|
268
266
|
@web_only
|
|
269
267
|
async def load_font(self, name: str, url: str) -> None:
|
|
270
268
|
"""Loads the font from the specified url as the specified name."""
|
|
269
|
+
from js import FontFace, document
|
|
271
270
|
|
|
272
271
|
ff = FontFace.new(name, f"url({url})")
|
|
273
272
|
await ff.load()
|
|
@@ -334,7 +333,7 @@ class CodeBattles(
|
|
|
334
333
|
|
|
335
334
|
For game-global log entries (not coming from a specific player), don't specify a ``player_index``.
|
|
336
335
|
"""
|
|
337
|
-
if is_web:
|
|
336
|
+
if is_web():
|
|
338
337
|
console_log(-1 if player_index is None else player_index, text, color)
|
|
339
338
|
else:
|
|
340
339
|
self._logs.append(
|
|
@@ -349,6 +348,7 @@ class CodeBattles(
|
|
|
349
348
|
@web_only
|
|
350
349
|
def play_sound(self, sound: str):
|
|
351
350
|
"""Plays the given sound, from the URL given by :func:`configure_sound_url`."""
|
|
351
|
+
from js import window, Audio
|
|
352
352
|
|
|
353
353
|
if sound not in self._sounds:
|
|
354
354
|
self._sounds[sound] = Audio.new(self.configure_sound_url(sound))
|
|
@@ -370,7 +370,11 @@ class CodeBattles(
|
|
|
370
370
|
|
|
371
371
|
return len(self.active_players) <= 1
|
|
372
372
|
|
|
373
|
+
@web_only
|
|
373
374
|
def _initialize(self):
|
|
375
|
+
from js import window, document
|
|
376
|
+
from pyscript.ffi import create_proxy
|
|
377
|
+
|
|
374
378
|
window.addEventListener("resize", create_proxy(lambda _: self._resize_canvas()))
|
|
375
379
|
document.getElementById("playpause").onclick = create_proxy(
|
|
376
380
|
lambda _: asyncio.get_event_loop().run_until_complete(self._play_pause())
|
|
@@ -383,6 +387,8 @@ class CodeBattles(
|
|
|
383
387
|
|
|
384
388
|
def _initialize_simulation(self, player_codes: List[str]):
|
|
385
389
|
self.step = 0
|
|
390
|
+
self._logs = []
|
|
391
|
+
self._decisions = []
|
|
386
392
|
self.active_players = list(range(len(self.player_names)))
|
|
387
393
|
self.active_players = list(range(len(self.player_names)))
|
|
388
394
|
self.state = self.create_initial_state()
|
|
@@ -392,6 +398,38 @@ class CodeBattles(
|
|
|
392
398
|
]
|
|
393
399
|
self._eliminated = []
|
|
394
400
|
self._player_globals = self._get_initial_player_globals(player_codes)
|
|
401
|
+
self._webworker_frame = 0
|
|
402
|
+
|
|
403
|
+
def _run_webworker_simulation(
|
|
404
|
+
self, map: str, player_names_str: str, player_codes_str: str
|
|
405
|
+
):
|
|
406
|
+
from pyscript import sync
|
|
407
|
+
|
|
408
|
+
# JS to Python
|
|
409
|
+
player_names = json.loads(player_names_str)
|
|
410
|
+
player_codes = json.loads(player_codes_str)
|
|
411
|
+
|
|
412
|
+
self.map = map
|
|
413
|
+
self.player_names = player_names
|
|
414
|
+
self.background = True
|
|
415
|
+
self.console_visible = False
|
|
416
|
+
self.verbose = False
|
|
417
|
+
self._initialize_simulation(player_codes)
|
|
418
|
+
while not self.over:
|
|
419
|
+
self._logs = []
|
|
420
|
+
decisions = self.make_decisions()
|
|
421
|
+
logs = self._logs
|
|
422
|
+
self._logs = []
|
|
423
|
+
self.apply_decisions(decisions)
|
|
424
|
+
|
|
425
|
+
sync.update_step(
|
|
426
|
+
base64.b64encode(decisions).decode(),
|
|
427
|
+
json.dumps(logs),
|
|
428
|
+
"true" if self.over else "false",
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
if not self.over:
|
|
432
|
+
self.step += 1
|
|
395
433
|
|
|
396
434
|
def _run_local_simulation(self):
|
|
397
435
|
self.map = sys.argv[1]
|
|
@@ -399,7 +437,6 @@ class CodeBattles(
|
|
|
399
437
|
self.background = True
|
|
400
438
|
self.console_visible = False
|
|
401
439
|
self.verbose = False
|
|
402
|
-
self._logs = []
|
|
403
440
|
player_codes = []
|
|
404
441
|
for filename in sys.argv[3:]:
|
|
405
442
|
with open(filename, "r") as f:
|
|
@@ -432,6 +469,7 @@ class CodeBattles(
|
|
|
432
469
|
loop = asyncio.get_event_loop()
|
|
433
470
|
loop.run_until_complete(self._start_simulation_async(*args, **kwargs))
|
|
434
471
|
|
|
472
|
+
@web_only
|
|
435
473
|
async def _start_simulation_async(
|
|
436
474
|
self,
|
|
437
475
|
map: str,
|
|
@@ -441,7 +479,18 @@ class CodeBattles(
|
|
|
441
479
|
console_visible: bool,
|
|
442
480
|
verbose: bool,
|
|
443
481
|
):
|
|
482
|
+
from js import document
|
|
483
|
+
from pyscript import workers
|
|
484
|
+
|
|
485
|
+
# JS to Python
|
|
486
|
+
player_names = [str(x) for x in player_names]
|
|
487
|
+
player_codes = [str(x) for x in player_codes]
|
|
488
|
+
|
|
444
489
|
try:
|
|
490
|
+
render_status = document.getElementById("render-status")
|
|
491
|
+
if render_status is not None:
|
|
492
|
+
render_status.textContent = "Rendering: Initializing..."
|
|
493
|
+
|
|
445
494
|
self.map = map
|
|
446
495
|
self.player_names = player_names
|
|
447
496
|
self.map_image = await download_image(self.configure_map_image_url(map))
|
|
@@ -472,11 +521,36 @@ class CodeBattles(
|
|
|
472
521
|
document.getElementById("loader").style.display = "none"
|
|
473
522
|
self.render()
|
|
474
523
|
|
|
524
|
+
self._worker = await workers["worker"]
|
|
525
|
+
self._worker.update_step = self._update_step
|
|
526
|
+
self._worker._run_webworker_simulation(
|
|
527
|
+
map, json.dumps(player_names), json.dumps(player_codes)
|
|
528
|
+
)
|
|
529
|
+
|
|
475
530
|
if self.background:
|
|
476
531
|
await self._play_pause()
|
|
477
532
|
except Exception:
|
|
478
533
|
traceback.print_exc()
|
|
479
534
|
|
|
535
|
+
def _update_step(self, decisions_str: str, logs_str: str, is_over_str: str):
|
|
536
|
+
from js import document
|
|
537
|
+
|
|
538
|
+
decisions = base64.b64decode(str(decisions_str))
|
|
539
|
+
logs: list = json.loads(str(logs_str))
|
|
540
|
+
is_over = str(is_over_str) == "true"
|
|
541
|
+
|
|
542
|
+
self._decisions.append(decisions)
|
|
543
|
+
self._logs.append(logs)
|
|
544
|
+
self._webworker_frame += 1
|
|
545
|
+
|
|
546
|
+
render_status = document.getElementById("render-status")
|
|
547
|
+
if render_status is not None:
|
|
548
|
+
render_status.textContent = (
|
|
549
|
+
"Rendering: Complete!"
|
|
550
|
+
if is_over
|
|
551
|
+
else f"Rendering: Frame {self._webworker_frame}"
|
|
552
|
+
)
|
|
553
|
+
|
|
480
554
|
def _get_initial_player_globals(self, player_codes: List[str]):
|
|
481
555
|
contexts = [
|
|
482
556
|
self.create_api_implementation(i) for i in range(len(self.player_names))
|
|
@@ -542,7 +616,10 @@ class CodeBattles(
|
|
|
542
616
|
|
|
543
617
|
return player_globals
|
|
544
618
|
|
|
619
|
+
@web_only
|
|
545
620
|
def _resize_canvas(self):
|
|
621
|
+
from js import document
|
|
622
|
+
|
|
546
623
|
if not hasattr(self, "canvas"):
|
|
547
624
|
return
|
|
548
625
|
|
|
@@ -555,9 +632,25 @@ class CodeBattles(
|
|
|
555
632
|
if not self.background:
|
|
556
633
|
self.render()
|
|
557
634
|
|
|
635
|
+
@web_only
|
|
558
636
|
def _step(self):
|
|
637
|
+
from js import document, setTimeout
|
|
638
|
+
from pyscript.ffi import create_proxy
|
|
639
|
+
|
|
559
640
|
if not self.over:
|
|
560
|
-
|
|
641
|
+
if len(self._decisions) == 0:
|
|
642
|
+
print("Warning: sleeping because decisions were not made yet!")
|
|
643
|
+
setTimeout(create_proxy(self._step), 100)
|
|
644
|
+
return
|
|
645
|
+
else:
|
|
646
|
+
logs = self._logs.pop(0)
|
|
647
|
+
for log in logs:
|
|
648
|
+
console_log(
|
|
649
|
+
-1 if log["player_index"] is None else log["player_index"],
|
|
650
|
+
log["text"],
|
|
651
|
+
log["color"],
|
|
652
|
+
)
|
|
653
|
+
self.apply_decisions(self._decisions.pop(0))
|
|
561
654
|
|
|
562
655
|
if not self.over:
|
|
563
656
|
self.step += 1
|
|
@@ -585,7 +678,10 @@ class CodeBattles(
|
|
|
585
678
|
if self.over:
|
|
586
679
|
document.getElementById("noui-progress").style.display = "none"
|
|
587
680
|
|
|
681
|
+
@web_only
|
|
588
682
|
def _should_play(self):
|
|
683
|
+
from js import document
|
|
684
|
+
|
|
589
685
|
if self.over:
|
|
590
686
|
return False
|
|
591
687
|
|
|
@@ -600,7 +696,10 @@ class CodeBattles(
|
|
|
600
696
|
|
|
601
697
|
return True
|
|
602
698
|
|
|
699
|
+
@web_only
|
|
603
700
|
def _get_playback_speed(self):
|
|
701
|
+
from js import document
|
|
702
|
+
|
|
604
703
|
return 2 ** float(
|
|
605
704
|
document.getElementById("timescale")
|
|
606
705
|
.getElementsByClassName("mantine-Slider-thumb")
|
|
@@ -608,7 +707,10 @@ class CodeBattles(
|
|
|
608
707
|
.ariaValueNow
|
|
609
708
|
)
|
|
610
709
|
|
|
710
|
+
@web_only
|
|
611
711
|
def _get_breakpoint(self):
|
|
712
|
+
from js import document
|
|
713
|
+
|
|
612
714
|
breakpoint_element = document.getElementById("breakpoint")
|
|
613
715
|
if breakpoint_element is None or breakpoint_element.value == "":
|
|
614
716
|
return -1
|
package/dist/code_battles/js.pyi
CHANGED
|
@@ -15,6 +15,7 @@ class TwoDContext:
|
|
|
15
15
|
font: str
|
|
16
16
|
fillStyle: str
|
|
17
17
|
strokeStyle: str
|
|
18
|
+
lineWidth: int
|
|
18
19
|
|
|
19
20
|
@staticmethod
|
|
20
21
|
def beginPath(): ...
|
|
@@ -28,6 +29,10 @@ class TwoDContext:
|
|
|
28
29
|
counterclockwise=False,
|
|
29
30
|
): ...
|
|
30
31
|
@staticmethod
|
|
32
|
+
def moveTo(x: int, y: int): ...
|
|
33
|
+
@staticmethod
|
|
34
|
+
def lineTo(x: int, y: int): ...
|
|
35
|
+
@staticmethod
|
|
31
36
|
def stroke(): ...
|
|
32
37
|
@staticmethod
|
|
33
38
|
def fill(): ...
|
|
@@ -2,10 +2,39 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import math
|
|
5
|
+
import sys
|
|
5
6
|
from typing import Callable, List, Union
|
|
6
7
|
from enum import Enum
|
|
7
8
|
|
|
8
|
-
|
|
9
|
+
try:
|
|
10
|
+
import js
|
|
11
|
+
except Exception:
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def is_worker():
|
|
16
|
+
try:
|
|
17
|
+
from js import window # noqa: F401
|
|
18
|
+
|
|
19
|
+
return False
|
|
20
|
+
except Exception:
|
|
21
|
+
return True
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def is_web():
|
|
25
|
+
return (
|
|
26
|
+
"MicroPython" in sys.version or "pyodide" in sys.executable
|
|
27
|
+
) and not is_worker()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def web_only(method):
|
|
31
|
+
if is_web():
|
|
32
|
+
return method
|
|
33
|
+
|
|
34
|
+
def wrapper(*args, **kwargs):
|
|
35
|
+
print(f"Warning: {method.__name__} should only be called in a web context.")
|
|
36
|
+
|
|
37
|
+
return wrapper
|
|
9
38
|
|
|
10
39
|
|
|
11
40
|
class Alignment(Enum):
|
|
@@ -13,7 +42,10 @@ class Alignment(Enum):
|
|
|
13
42
|
TOP_LEFT = 1
|
|
14
43
|
|
|
15
44
|
|
|
16
|
-
|
|
45
|
+
@web_only
|
|
46
|
+
def download_image(src: str):
|
|
47
|
+
from js import Image
|
|
48
|
+
|
|
17
49
|
result = asyncio.Future()
|
|
18
50
|
image = Image.new()
|
|
19
51
|
image.onload = lambda _: result.set_result(image)
|
|
@@ -24,14 +56,22 @@ def download_image(src: str) -> Image:
|
|
|
24
56
|
def show_alert(
|
|
25
57
|
title: str, alert: str, color: str, icon: str, limit_time: int = 5000, is_code=True
|
|
26
58
|
):
|
|
27
|
-
if
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
59
|
+
if is_web():
|
|
60
|
+
from js import window
|
|
61
|
+
|
|
62
|
+
if hasattr(window, "showAlert"):
|
|
63
|
+
try:
|
|
64
|
+
window.showAlert(title, alert, color, icon, limit_time, is_code)
|
|
65
|
+
except Exception as e:
|
|
66
|
+
print(e)
|
|
67
|
+
else:
|
|
68
|
+
print(f"[ALERT] {title}: {alert}")
|
|
32
69
|
|
|
33
70
|
|
|
71
|
+
@web_only
|
|
34
72
|
def set_results(player_names: List[str], places: List[int], map: str, verbose: bool):
|
|
73
|
+
from js import window
|
|
74
|
+
|
|
35
75
|
if hasattr(window, "setResults"):
|
|
36
76
|
try:
|
|
37
77
|
window.setResults(player_names, places, map, verbose)
|
|
@@ -39,7 +79,10 @@ def set_results(player_names: List[str], places: List[int], map: str, verbose: b
|
|
|
39
79
|
print(e)
|
|
40
80
|
|
|
41
81
|
|
|
82
|
+
@web_only
|
|
42
83
|
def download_json(filename: str, contents: str):
|
|
84
|
+
from js import window
|
|
85
|
+
|
|
43
86
|
if hasattr(window, "downloadJson"):
|
|
44
87
|
try:
|
|
45
88
|
window.downloadJson(filename, contents)
|
|
@@ -47,7 +90,10 @@ def download_json(filename: str, contents: str):
|
|
|
47
90
|
print(e)
|
|
48
91
|
|
|
49
92
|
|
|
93
|
+
@web_only
|
|
50
94
|
def console_log(player_index: int, text: str, color: str):
|
|
95
|
+
from js import window
|
|
96
|
+
|
|
51
97
|
if hasattr(window, "consoleLog"):
|
|
52
98
|
try:
|
|
53
99
|
window.consoleLog(player_index, text, color)
|
|
@@ -71,9 +117,9 @@ class GameCanvas:
|
|
|
71
117
|
|
|
72
118
|
def __init__(
|
|
73
119
|
self,
|
|
74
|
-
canvas: Element,
|
|
120
|
+
canvas: "js.Element",
|
|
75
121
|
player_count: int,
|
|
76
|
-
map_image: Image,
|
|
122
|
+
map_image: "js.Image",
|
|
77
123
|
max_width: int,
|
|
78
124
|
max_height: int,
|
|
79
125
|
extra_width: int,
|
|
@@ -89,7 +135,7 @@ class GameCanvas:
|
|
|
89
135
|
|
|
90
136
|
def draw_element(
|
|
91
137
|
self,
|
|
92
|
-
image: Image,
|
|
138
|
+
image: "js.Image",
|
|
93
139
|
x: int,
|
|
94
140
|
y: int,
|
|
95
141
|
width: int,
|
|
@@ -143,6 +189,26 @@ class GameCanvas:
|
|
|
143
189
|
self.context.fillStyle = color
|
|
144
190
|
self.context.fillText(text, x, y)
|
|
145
191
|
|
|
192
|
+
def draw_line(
|
|
193
|
+
self,
|
|
194
|
+
start_x: int,
|
|
195
|
+
start_y: int,
|
|
196
|
+
end_x: int,
|
|
197
|
+
end_y: int,
|
|
198
|
+
stroke="black",
|
|
199
|
+
stroke_width=10,
|
|
200
|
+
board_index=0,
|
|
201
|
+
):
|
|
202
|
+
start_x, start_y = self._translate_position(board_index, start_x, start_y)
|
|
203
|
+
end_x, end_y = self._translate_position(board_index, end_x, end_y)
|
|
204
|
+
|
|
205
|
+
self.context.strokeStyle = stroke
|
|
206
|
+
self.context.lineWidth = stroke_width * self._scale
|
|
207
|
+
self.context.beginPath()
|
|
208
|
+
self.context.moveTo(start_x, start_y)
|
|
209
|
+
self.context.lineTo(end_x, end_y)
|
|
210
|
+
self.context.stroke()
|
|
211
|
+
|
|
146
212
|
def draw_circle(
|
|
147
213
|
self,
|
|
148
214
|
x: int,
|
|
@@ -150,6 +216,7 @@ class GameCanvas:
|
|
|
150
216
|
radius: float,
|
|
151
217
|
fill="black",
|
|
152
218
|
stroke="transparent",
|
|
219
|
+
stroke_width=2,
|
|
153
220
|
board_index=0,
|
|
154
221
|
):
|
|
155
222
|
"""
|
|
@@ -157,8 +224,10 @@ class GameCanvas:
|
|
|
157
224
|
"""
|
|
158
225
|
|
|
159
226
|
x, y = self._translate_position(board_index, x, y)
|
|
227
|
+
|
|
160
228
|
self.context.fillStyle = fill
|
|
161
229
|
self.context.strokeStyle = stroke
|
|
230
|
+
self.context.lineWidth = stroke_width * self._scale
|
|
162
231
|
self.context.beginPath()
|
|
163
232
|
self.context.arc(x, y, radius * self._scale, 0, 2 * math.pi)
|
|
164
233
|
self.context.stroke()
|
|
@@ -187,6 +256,8 @@ class GameCanvas:
|
|
|
187
256
|
return self.map_image.width * self.player_count
|
|
188
257
|
|
|
189
258
|
def _fit_into(self, max_width: int, max_height: int):
|
|
259
|
+
from js import window
|
|
260
|
+
|
|
190
261
|
if self.map_image.width == 0 or self.map_image.height == 0:
|
|
191
262
|
raise Exception("Map image invalid!")
|
|
192
263
|
aspect_ratio = (self.map_image.width * self.player_count + self.extra_width) / (
|
package/dist/esm/index.js
CHANGED
|
@@ -16018,6 +16018,7 @@ const Simulation = () => {
|
|
|
16018
16018
|
!location.search.includes("background=true") && (React.createElement(React.Fragment, null,
|
|
16019
16019
|
React.createElement("div", { style: { textAlign: "center", flexGrow: 1 } },
|
|
16020
16020
|
showcaseMode || (React.createElement(React.Fragment, null,
|
|
16021
|
+
React.createElement("span", { id: "render-status", style: { marginRight: 10 } }),
|
|
16021
16022
|
React.createElement(NumberInput, { id: "breakpoint", label: "Breakpoint", min: 0, leftSection: React.createElement("i", { className: "fa-solid fa-stopwatch" }), display: "inline-block", maw: "35%", mr: "xs" }),
|
|
16022
16023
|
React.createElement(Button, { style: { flex: "none" }, my: "xs", w: 100, leftSection: React.createElement("i", { className: "fa-solid fa-wand-magic" }), color: "grape", id: "step", mr: "xs", radius: "20px" }, "Step"))),
|
|
16023
16024
|
React.createElement(PlayPauseButton, null)),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "code-battles",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.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",
|
|
@@ -52,16 +52,16 @@
|
|
|
52
52
|
"@rollup/plugin-commonjs": "^26.0.3",
|
|
53
53
|
"@rollup/plugin-node-resolve": "^15.3.0",
|
|
54
54
|
"@rollup/plugin-typescript": "^11.1.6",
|
|
55
|
-
"@types/prismjs": "^1.26.
|
|
56
|
-
"@types/react": "^18.3.
|
|
55
|
+
"@types/prismjs": "^1.26.5",
|
|
56
|
+
"@types/react": "^18.3.12",
|
|
57
57
|
"@types/react-dom": "^18.3.1",
|
|
58
58
|
"chokidar-cli": "^3.0.0",
|
|
59
|
-
"rollup": "^4.24.
|
|
59
|
+
"rollup": "^4.24.3",
|
|
60
60
|
"rollup-plugin-copy": "^3.5.0",
|
|
61
61
|
"rollup-plugin-dts": "^6.1.1",
|
|
62
|
-
"rollup-plugin-import-css": "^3.5.
|
|
62
|
+
"rollup-plugin-import-css": "^3.5.6",
|
|
63
63
|
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
64
|
-
"tslib": "^2.8.
|
|
64
|
+
"tslib": "^2.8.1",
|
|
65
65
|
"typescript": "^5.6.3"
|
|
66
66
|
},
|
|
67
67
|
"prettier": {
|