code-battles 1.3.0 → 1.5.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 CHANGED
@@ -3,6 +3,7 @@
3
3
  require('@mantine/core/styles.css');
4
4
  require('@mantine/dates/styles.css');
5
5
  require('@mantine/notifications/styles.css');
6
+ require('@mantine/dropzone/styles.css');
6
7
  var core = require('@mantine/core');
7
8
  var dates = require('@mantine/dates');
8
9
  var hooks = require('@mantine/hooks');
@@ -12,6 +13,7 @@ var auth = require('firebase/auth');
12
13
  var firestore = require('firebase/firestore');
13
14
  var React = require('react');
14
15
  var reactRouterDom = require('react-router-dom');
16
+ var dropzone = require('@mantine/dropzone');
15
17
  var jsxRuntime = require('react/jsx-runtime');
16
18
 
17
19
  var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
@@ -409,21 +411,30 @@ const toPlacing = (n) => {
409
411
  }
410
412
  return n.toString() + DIGITS[n % 10];
411
413
  };
412
- const runNoUI = (map, apis, playerBots, verbose) => {
414
+ const runNoUI = (map, apis, playerBots, seed, verbose) => {
413
415
  const players = playerBots.map((api) => (api === "None" ? "" : apis[api]));
414
- tryUntilFailure(() =>
416
+ tryUntilSuccess(() =>
415
417
  // @ts-ignore
416
- window._startSimulation(map, players, playerBots, true, false, verbose));
418
+ window._startSimulation(map, players, playerBots, true, false, verbose, seed));
417
419
  };
418
- const tryUntilFailure = (f, timeout = 500) => {
420
+ const tryUntilSuccess = (f, timeout = 500) => {
419
421
  try {
420
422
  f();
421
423
  }
422
424
  catch (error) {
423
425
  console.log("Failed, waiting for timeout...", error === null || error === void 0 ? void 0 : error.message);
424
- setTimeout(() => tryUntilFailure(f, timeout), timeout);
426
+ setTimeout(() => tryUntilSuccess(f, timeout), timeout);
425
427
  }
426
428
  };
429
+ const downloadFile = (filename, mimeType, contents) => {
430
+ const a = document.createElement("a");
431
+ a.style.display = "none";
432
+ document.body.appendChild(a);
433
+ a.href = `data:${mimeType};charset=utf-8,${encodeURIComponent(contents)}`;
434
+ a.download = filename;
435
+ a.click();
436
+ document.body.removeChild(a);
437
+ };
427
438
 
428
439
  const useLocalStorage = ({ key, defaultValue, }) => {
429
440
  const [value, setValue] = React.useState(getLocalStorage(key, defaultValue !== null && defaultValue !== void 0 ? defaultValue : null));
@@ -2493,6 +2504,10 @@ const RunSimulationBlock = () => {
2493
2504
  key: "Player Bots",
2494
2505
  defaultValue: ["None", "None"],
2495
2506
  });
2507
+ const [seed, setSeed] = useLocalStorage({
2508
+ key: "Seed",
2509
+ defaultValue: "",
2510
+ });
2496
2511
  const [runningNoUI, setRunningNoUI] = React.useState(false);
2497
2512
  const [runningNoUIN, setRunningNoUIN] = React.useState({});
2498
2513
  const navigate = reactRouterDom.useNavigate();
@@ -2501,15 +2516,18 @@ const RunSimulationBlock = () => {
2501
2516
  remaining = Math.max(...Object.values(runningNoUIN));
2502
2517
  }
2503
2518
  const run = () => {
2504
- navigate(`/simulation/${map.replaceAll(" ", "-")}/${playerBots.join("-")}`);
2519
+ // @ts-ignore
2520
+ window._isSimulationFromFile = false;
2521
+ navigate(`/simulation/${map.replaceAll(" ", "-")}/${playerBots.join("-")}?seed=${seed}`);
2505
2522
  };
2506
2523
  const startRunNoUI = () => {
2507
2524
  setRunningNoUI(true);
2508
- runNoUI(map, apis, playerBots, true);
2525
+ runNoUI(map, apis, playerBots, seed.toString(), true);
2509
2526
  };
2510
2527
  const startRunNoUIN = (n) => {
2528
+ setLocalStorage("Results", {});
2511
2529
  setRunningNoUIN({ [n.toString()]: n });
2512
- runNoUI(map, apis, playerBots, false);
2530
+ runNoUI(map, apis, playerBots, seed.toString(), false);
2513
2531
  };
2514
2532
  React.useEffect(() => {
2515
2533
  // @ts-ignore
@@ -2563,7 +2581,7 @@ const RunSimulationBlock = () => {
2563
2581
  setLocalStorage("Results", {});
2564
2582
  }
2565
2583
  else {
2566
- runNoUI(map, apis, playerBots, false);
2584
+ runNoUI(map, apis, playerBots, seed.toString(), false);
2567
2585
  }
2568
2586
  }
2569
2587
  }, [runningNoUIN]);
@@ -2574,6 +2592,7 @@ const RunSimulationBlock = () => {
2574
2592
  }
2575
2593
  } }),
2576
2594
  React.createElement(BotSelector, { playerCount: playerCount, setPlayerCount: setPlayerCount, playerBots: playerBots, setPlayerBots: setPlayerBots, apis: apis }),
2595
+ React.createElement(core.NumberInput, { leftSection: React.createElement("i", { className: "fa-solid fa-dice" }), label: "Randomness Seed", min: 0, value: seed, onChange: setSeed }),
2577
2596
  React.createElement(core.Button.Group, { mt: "xs" },
2578
2597
  React.createElement(core.Button, { variant: "default", w: "50%", leftSection: React.createElement("i", { className: "fa-solid fa-play" }), onClick: run }, "Run"),
2579
2598
  React.createElement(core.Button, { variant: "default", w: "50%", leftSection: React.createElement("i", { className: "fa-solid fa-forward" }), onClick: startRunNoUI, loading: runningNoUI || loading }, "Run (No UI)")),
@@ -2583,7 +2602,31 @@ const RunSimulationBlock = () => {
2583
2602
  remaining !== 0 && (React.createElement("p", { style: { textAlign: "center", marginTop: 10 } },
2584
2603
  "Remaining Simulations: ",
2585
2604
  remaining)),
2586
- React.createElement("p", { style: { textAlign: "center", display: "none", marginTop: 10 }, id: "noui-progress" })));
2605
+ React.createElement("p", { style: { textAlign: "center", display: "none", marginTop: 10 }, id: "noui-progress" }),
2606
+ React.createElement(dropzone.Dropzone, { mt: "xs", multiple: false, onDrop: (files) => __awaiter(void 0, void 0, void 0, function* () {
2607
+ if (files.length !== 1) {
2608
+ return;
2609
+ }
2610
+ const file = files[0];
2611
+ const text = yield file.text();
2612
+ // @ts-ignore
2613
+ window._navigate = navigate;
2614
+ // @ts-ignore
2615
+ window._isSimulationFromFile = true;
2616
+ tryUntilSuccess(() => {
2617
+ // @ts-ignore
2618
+ window._startSimulationFromFile(text);
2619
+ });
2620
+ }), style: {
2621
+ textAlign: "center",
2622
+ paddingTop: 20,
2623
+ paddingBottom: 20,
2624
+ paddingLeft: 10,
2625
+ paddingRight: 10,
2626
+ } },
2627
+ React.createElement("span", null,
2628
+ React.createElement("i", { className: "fa-solid fa-file-code", style: { marginRight: 10 } }),
2629
+ "Drag a simulation file here or click to select a file to run a simulation from a file"))));
2587
2630
  };
2588
2631
 
2589
2632
  const TournamentBlock = ({ pointModifier, inline, title }) => {
@@ -15901,13 +15944,19 @@ const Simulation = () => {
15901
15944
  const [apis, loading] = useAPIs();
15902
15945
  let { map, playerapis } = reactRouterDom.useParams();
15903
15946
  const location = reactRouterDom.useLocation();
15947
+ const [searchParams] = reactRouterDom.useSearchParams();
15904
15948
  const [winner, setWinner] = React.useState();
15949
+ const [downloadBytes, setDownloadBytes] = React.useState(false);
15905
15950
  const navigate = reactRouterDom.useNavigate();
15906
15951
  const colorScheme = hooks.useColorScheme();
15907
15952
  const showcaseMode = location.search.includes("showcase=true");
15908
15953
  React.useEffect(() => {
15909
15954
  n((engine) => __awaiter(void 0, void 0, void 0, function* () { return yield loadFull(engine); }));
15910
15955
  // @ts-ignore
15956
+ window.showDownload = () => {
15957
+ setDownloadBytes(true);
15958
+ };
15959
+ // @ts-ignore
15911
15960
  window.showWinner = (winner, verbose) => {
15912
15961
  if (admin) {
15913
15962
  setWinner(winner);
@@ -15937,10 +15986,16 @@ const Simulation = () => {
15937
15986
  const playerNames = (_a = playerapis === null || playerapis === void 0 ? void 0 : playerapis.split("-")) !== null && _a !== void 0 ? _a : [];
15938
15987
  const players = playerNames.map((api) => (api === "None" ? "" : apis[api]));
15939
15988
  React.useEffect(() => {
15940
- if (!loading && players && playerapis) {
15941
- tryUntilFailure(() =>
15989
+ var _a;
15990
+ if (!loading &&
15991
+ players &&
15992
+ playerapis &&
15993
+ // @ts-ignore
15994
+ window._isSimulationFromFile !== true) {
15995
+ const seed = (_a = searchParams.get("seed")) !== null && _a !== void 0 ? _a : "";
15996
+ tryUntilSuccess(() =>
15942
15997
  // @ts-ignore
15943
- window._startSimulation(map, players, playerNames, false, !showcaseMode, true));
15998
+ window._startSimulation(map, players, playerNames, false, !showcaseMode, true, seed));
15944
15999
  }
15945
16000
  }, [loading]);
15946
16001
  const newRank = getRank(getLocalStorage("Cached tournament/info"), winner, getLocalStorage("Point Modifier")) + 1;
@@ -16023,7 +16078,11 @@ const Simulation = () => {
16023
16078
  React.createElement("span", { id: "render-status", style: { marginRight: 10 } }),
16024
16079
  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" }),
16025
16080
  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"))),
16026
- React.createElement(PlayPauseButton, null)),
16081
+ React.createElement(PlayPauseButton, null),
16082
+ 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",
16083
+ // @ts-ignore
16084
+ window.simulationToDownload) }, "Download Simulation"))),
16085
+ showcaseMode && React.createElement("span", { id: "render-status" }),
16027
16086
  React.createElement("p", { style: { margin: 0 } }, "Playback Speed"),
16028
16087
  React.createElement(core.Slider, { style: { flex: "none" }, mb: 30, w: 500, maw: "85%", min: -2, defaultValue: 0, marks: [
16029
16088
  { value: -2, label: "1/4" },
@@ -2,6 +2,7 @@ import "@fortawesome/fontawesome-free/css/all.css";
2
2
  import "@mantine/core/styles.css";
3
3
  import "@mantine/dates/styles.css";
4
4
  import "@mantine/notifications/styles.css";
5
+ import "@mantine/dropzone/styles.css";
5
6
  import "prismjs/plugins/line-numbers/prism-line-numbers.css";
6
7
  import "prismjs/themes/prism.css";
7
8
  import "./index.css";
@@ -10,5 +10,6 @@ export declare const getRank: (tournamentInfo: any, team: string, pointModifier:
10
10
  export declare const updatePointModifier: () => void;
11
11
  export declare const toPlacing: (n: number) => string;
12
12
  export declare const zeroPad: (s: string, l: number) => string;
13
- export declare const runNoUI: (map: string, apis: Record<string, any>, playerBots: string[], verbose: boolean) => void;
14
- export declare const tryUntilFailure: (f: () => void, timeout?: number) => void;
13
+ export declare const runNoUI: (map: string, apis: Record<string, any>, playerBots: string[], seed: string, verbose: boolean) => void;
14
+ export declare const tryUntilSuccess: (f: () => void, timeout?: number) => void;
15
+ export declare const downloadFile: (filename: string, mimeType: string, contents: string) => void;
@@ -14,6 +14,9 @@ def run_game(battles: CodeBattles):
14
14
  from pyscript.ffi import create_proxy
15
15
 
16
16
  window._startSimulation = create_proxy(battles._start_simulation)
17
+ window._startSimulationFromFile = create_proxy(
18
+ battles._start_simulation_from_file
19
+ )
17
20
  elif is_worker():
18
21
  setattr(
19
22
  sys.modules["__main__"],
@@ -1,19 +1,24 @@
1
1
  import asyncio
2
2
  import base64
3
+ from dataclasses import dataclass
4
+ import datetime
3
5
  import json
4
6
  import math
5
7
  import time
6
- import random
8
+ from random import Random
7
9
  import sys
8
10
  import traceback
11
+ import gzip
9
12
 
10
13
  from typing import Any, Dict, Generic, List, Optional, Tuple, TypeVar
11
14
  from code_battles.utilities import (
12
15
  GameCanvas,
13
16
  console_log,
14
17
  download_image,
18
+ navigate,
15
19
  set_results,
16
20
  show_alert,
21
+ show_download,
17
22
  web_only,
18
23
  is_web,
19
24
  )
@@ -29,6 +34,53 @@ APIType = TypeVar("APIType")
29
34
  PlayerRequestsType = TypeVar("PlayerRequestsType")
30
35
 
31
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
+
32
84
  class CodeBattles(
33
85
  Generic[GameStateType, APIImplementationType, APIType, PlayerRequestsType]
34
86
  ):
@@ -60,6 +112,12 @@ class CodeBattles(
60
112
  """The current state of the game. You should modify this in :func:`apply_decisions`."""
61
113
  player_requests: List[PlayerRequestsType]
62
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`."""
63
121
 
64
122
  background: bool
65
123
  """Whether the current simulation is occuring in the background (without UI)."""
@@ -77,6 +135,7 @@ class CodeBattles(
77
135
  _eliminated: List[int]
78
136
  _sounds: Dict[str, "js.Audio"] = {}
79
137
  _decisions: List[bytes]
138
+ _since_last_render: int
80
139
 
81
140
  def render(self) -> None:
82
141
  """
@@ -94,12 +153,12 @@ class CodeBattles(
94
153
  Use the current state and bots to make decisions in order to reach the next state.
95
154
  You may use :func:`run_bot_method` to run a specific player's method (for instance, `run`).
96
155
 
156
+ If you need any randomness, use :attr:`make_decisions_random`.
157
+
97
158
  This function may take a lot of time to execute.
98
159
 
99
160
  .. warning::
100
161
  Do not call any other method other than :func:`run_bot_method` in here. This method will run in a web worker.
101
-
102
- Do NOT update :attr:`state` or :attr:`step`.
103
162
  """
104
163
 
105
164
  raise NotImplementedError("make_decisions")
@@ -188,12 +247,12 @@ class CodeBattles(
188
247
 
189
248
  return 1
190
249
 
191
- def configure_map_image_url(self, map: str):
250
+ def configure_map_image_url(self, map: str) -> str:
192
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."""
193
252
 
194
253
  return "/images/maps/" + map.lower().replace(" ", "_") + ".png"
195
254
 
196
- def configure_sound_url(self, name: str):
255
+ def configure_sound_url(self, name: str) -> str:
197
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."""
198
257
 
199
258
  return "/sounds/" + name.lower().replace(" ", "_") + ".mp3"
@@ -203,11 +262,19 @@ class CodeBattles(
203
262
 
204
263
  return "CodeBattlesBot"
205
264
 
206
- def configure_bot_globals(self) -> Dict[str, Any]:
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]:
207
274
  """
208
275
  Configure additional available global items, such as libraries from the Python standard library, bots can use.
209
276
 
210
- By default, this is math, time and random.
277
+ By default, this is math, time and random, where random is the corresponding :attr:`player_randoms`.
211
278
 
212
279
  .. warning::
213
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`.
@@ -218,9 +285,13 @@ class CodeBattles(
218
285
  return {
219
286
  "math": math,
220
287
  "time": time,
221
- "random": random,
288
+ "random": self.player_randoms[player_index],
222
289
  }
223
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
+
224
295
  @web_only
225
296
  def download_images(
226
297
  self, sources: List[Tuple[str, str]]
@@ -356,7 +427,16 @@ class CodeBattles(
356
427
  volume = window.localStorage.getItem("Volume") or 0
357
428
  s = self._sounds[sound].cloneNode(True)
358
429
  s.volume = volume
359
- s.play()
430
+
431
+ async def p():
432
+ try:
433
+ await s.play()
434
+ except Exception:
435
+ print(
436
+ f"Warning: couldn't play sound '{sound}'. Make sure the `sound` and `configure_sound_url` are correct."
437
+ )
438
+
439
+ asyncio.get_event_loop().run_until_complete(p())
360
440
 
361
441
  @property
362
442
  def time(self) -> str:
@@ -385,12 +465,19 @@ class CodeBattles(
385
465
 
386
466
  self._initialized = True
387
467
 
388
- def _initialize_simulation(self, player_codes: List[str]):
389
- self.step = 0
468
+ def _initialize_simulation(
469
+ self, player_codes: List[str], seed: Optional[int] = None
470
+ ):
471
+ if seed is None:
472
+ seed = Random().randint(0, 2**128)
390
473
  self._logs = []
391
474
  self._decisions = []
475
+ self._decision_index = 0
476
+ self._seed = seed
477
+ self.step = 0
392
478
  self.active_players = list(range(len(self.player_names)))
393
- self.active_players = list(range(len(self.player_names)))
479
+ self.random = Random(seed)
480
+ self.player_randoms = [Random(self.random.random()) for _ in self.player_names]
394
481
  self.state = self.create_initial_state()
395
482
  self.player_requests = [
396
483
  self.create_initial_player_requests(i)
@@ -398,10 +485,15 @@ class CodeBattles(
398
485
  ]
399
486
  self._eliminated = []
400
487
  self._player_globals = self._get_initial_player_globals(player_codes)
401
- self._webworker_frame = 0
488
+ self._since_last_render = 1
489
+ self._start_time = time.time()
402
490
 
403
491
  def _run_webworker_simulation(
404
- self, map: str, player_names_str: str, player_codes_str: str
492
+ self,
493
+ map: str,
494
+ player_names_str: str,
495
+ player_codes_str: str,
496
+ seed: Optional[int] = None,
405
497
  ):
406
498
  from pyscript import sync
407
499
 
@@ -414,7 +506,7 @@ class CodeBattles(
414
506
  self.background = True
415
507
  self.console_visible = False
416
508
  self.verbose = False
417
- self._initialize_simulation(player_codes)
509
+ self._initialize_simulation(player_codes, seed)
418
510
  while not self.over:
419
511
  self._logs = []
420
512
  decisions = self.make_decisions()
@@ -432,16 +524,17 @@ class CodeBattles(
432
524
  self.step += 1
433
525
 
434
526
  def _run_local_simulation(self):
435
- self.map = sys.argv[1]
436
- self.player_names = sys.argv[2].split("-")
527
+ seed = None if sys.argv[1] == "None" else int(sys.argv[1])
528
+ self.map = sys.argv[2]
529
+ self.player_names = sys.argv[3].split("-")
437
530
  self.background = True
438
531
  self.console_visible = False
439
532
  self.verbose = False
440
533
  player_codes = []
441
- for filename in sys.argv[3:]:
534
+ for filename in sys.argv[4:]:
442
535
  with open(filename, "r") as f:
443
536
  player_codes.append(f.read())
444
- self._initialize_simulation(player_codes)
537
+ self._initialize_simulation(player_codes, seed)
445
538
 
446
539
  while not self.over:
447
540
  self.apply_decisions(self.make_decisions())
@@ -469,6 +562,73 @@ class CodeBattles(
469
562
  loop = asyncio.get_event_loop()
470
563
  loop.run_until_complete(self._start_simulation_async(*args, **kwargs))
471
564
 
565
+ def _start_simulation_from_file(self, contents: str):
566
+ loop = asyncio.get_event_loop()
567
+ loop.run_until_complete(self._start_simulation_from_file_async(contents))
568
+
569
+ async def _start_simulation_from_file_async(self, contents: str):
570
+ from js import document
571
+
572
+ try:
573
+ simulation = Simulation.load(str(contents))
574
+ navigate(
575
+ f"/simulation/{simulation.map}/{'-'.join(simulation.player_names)}"
576
+ )
577
+ show_alert(
578
+ "Loaded simulation file!",
579
+ f"{', '.join(simulation.player_names)} competed in {simulation.map} at {simulation.timestamp}",
580
+ "blue",
581
+ "fa-solid fa-file-code",
582
+ 0,
583
+ )
584
+ if simulation.game != self.__class__.__name__:
585
+ show_alert(
586
+ "Warning: game mismatch!",
587
+ f"Simulation file is for game {simulation.game} while the website is running {self.__class__.__name__}!",
588
+ "yellow",
589
+ "fa-solid fa-exclamation",
590
+ 0,
591
+ )
592
+ if simulation.version != self.configure_version():
593
+ show_alert(
594
+ "Warning: version mismatch!",
595
+ f"Simulation file is for version {simulation.version} while the website is running {self.configure_version()}!",
596
+ "yellow",
597
+ "fa-solid fa-exclamation",
598
+ 0,
599
+ )
600
+ while document.getElementById("loader") is None:
601
+ await asyncio.sleep(0.01)
602
+ if not hasattr(self, "_initialized"):
603
+ self._initialize()
604
+ self.map = simulation.map
605
+ self.map_image = await download_image(
606
+ self.configure_map_image_url(simulation.map)
607
+ )
608
+ self.player_names = simulation.player_names
609
+ self.background = False
610
+ self.console_visible = True
611
+ self.verbose = False
612
+ self._initialize_simulation(
613
+ ["" for _ in simulation.player_names], simulation.seed
614
+ )
615
+ self._decisions = simulation.decisions
616
+ self._logs = simulation.logs
617
+ self.canvas = GameCanvas(
618
+ document.getElementById("simulation"),
619
+ self.configure_board_count(),
620
+ self.map_image,
621
+ document.body.clientWidth - 440,
622
+ document.body.clientHeight - 280,
623
+ self.configure_extra_width(),
624
+ self.configure_extra_height(),
625
+ )
626
+ document.getElementById("loader").style.display = "none"
627
+ await self.setup()
628
+ self.render()
629
+ except Exception as e:
630
+ print(e)
631
+
472
632
  @web_only
473
633
  async def _start_simulation_async(
474
634
  self,
@@ -478,6 +638,7 @@ class CodeBattles(
478
638
  background: bool,
479
639
  console_visible: bool,
480
640
  verbose: bool,
641
+ seed="",
481
642
  ):
482
643
  from js import document
483
644
  from pyscript import workers
@@ -497,7 +658,7 @@ class CodeBattles(
497
658
  self.background = background
498
659
  self.console_visible = console_visible
499
660
  self.verbose = verbose
500
- self._initialize_simulation(player_codes)
661
+ self._initialize_simulation(player_codes, None if seed == "" else int(seed))
501
662
 
502
663
  if not self.background:
503
664
  self.canvas = GameCanvas(
@@ -507,7 +668,9 @@ class CodeBattles(
507
668
  document.body.clientWidth - 440
508
669
  if console_visible
509
670
  else document.body.clientWidth - 40,
510
- document.body.clientHeight - 280,
671
+ document.body.clientHeight - 280
672
+ if console_visible
673
+ else document.body.clientHeight - 160,
511
674
  self.configure_extra_width(),
512
675
  self.configure_extra_height(),
513
676
  )
@@ -521,11 +684,11 @@ class CodeBattles(
521
684
  document.getElementById("loader").style.display = "none"
522
685
  self.render()
523
686
 
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
- )
687
+ self._worker = await workers["worker"]
688
+ self._worker.update_step = self._update_step
689
+ self._worker._run_webworker_simulation(
690
+ map, json.dumps(player_names), json.dumps(player_codes), self._seed
691
+ )
529
692
 
530
693
  if self.background:
531
694
  await self._play_pause()
@@ -533,22 +696,39 @@ class CodeBattles(
533
696
  traceback.print_exc()
534
697
 
535
698
  def _update_step(self, decisions_str: str, logs_str: str, is_over_str: str):
536
- from js import document
699
+ from js import window, document
537
700
 
701
+ now = time.time()
538
702
  decisions = base64.b64decode(str(decisions_str))
539
703
  logs: list = json.loads(str(logs_str))
540
704
  is_over = str(is_over_str) == "true"
541
705
 
542
706
  self._decisions.append(decisions)
543
707
  self._logs.append(logs)
544
- self._webworker_frame += 1
708
+
709
+ if is_over:
710
+ try:
711
+ simulation = Simulation(
712
+ self.map,
713
+ self.player_names,
714
+ self.__class__.__name__,
715
+ self.configure_version(),
716
+ datetime.datetime.now(),
717
+ self._logs,
718
+ self._decisions,
719
+ self._seed,
720
+ )
721
+ window.simulationToDownload = simulation.dump()
722
+ show_download()
723
+ except Exception as e:
724
+ print(e)
545
725
 
546
726
  render_status = document.getElementById("render-status")
547
727
  if render_status is not None:
548
728
  render_status.textContent = (
549
- "Rendering: Complete!"
729
+ f"Rendering: Complete! ({int(now - self._start_time)}s)"
550
730
  if is_over
551
- else f"Rendering: Frame {self._webworker_frame}"
731
+ else f"Rendering: Frame {len(self._decisions)} ({int(now - self._start_time)}s)"
552
732
  )
553
733
 
554
734
  def _get_initial_player_globals(self, player_codes: List[str]):
@@ -565,8 +745,8 @@ class CodeBattles(
565
745
  "context": context,
566
746
  **self.get_api().__dict__,
567
747
  }
568
- | self.configure_bot_globals()
569
- for context in contexts
748
+ | self.configure_bot_globals(player_index)
749
+ for player_index, context in enumerate(contexts)
570
750
  ]
571
751
  for index, api_code in enumerate(player_codes):
572
752
  if api_code != "" and api_code is not None:
@@ -627,7 +807,9 @@ class CodeBattles(
627
807
  document.body.clientWidth - 440
628
808
  if self.console_visible
629
809
  else document.body.clientWidth - 40,
630
- document.body.clientHeight - 280,
810
+ document.body.clientHeight - 280
811
+ if self.console_visible
812
+ else document.body.clientHeight - 160,
631
813
  )
632
814
  if not self.background:
633
815
  self.render()
@@ -638,19 +820,20 @@ class CodeBattles(
638
820
  from pyscript.ffi import create_proxy
639
821
 
640
822
  if not self.over:
641
- if len(self._decisions) == 0:
823
+ if len(self._decisions) == self._decision_index:
642
824
  print("Warning: sleeping because decisions were not made yet!")
643
825
  setTimeout(create_proxy(self._step), 100)
644
826
  return
645
827
  else:
646
- logs = self._logs.pop(0)
828
+ logs = self._logs[self._decision_index]
647
829
  for log in logs:
648
830
  console_log(
649
831
  -1 if log["player_index"] is None else log["player_index"],
650
832
  log["text"],
651
833
  log["color"],
652
834
  )
653
- self.apply_decisions(self._decisions.pop(0))
835
+ self.apply_decisions(self._decisions[self._decision_index])
836
+ self._decision_index += 1
654
837
 
655
838
  if not self.over:
656
839
  self.step += 1
@@ -661,9 +844,18 @@ class CodeBattles(
661
844
  set_results(
662
845
  self.player_names, self._eliminated[::-1], self.map, self.verbose
663
846
  )
847
+ if not self.background:
848
+ self.render()
664
849
 
665
850
  if not self.background:
666
- self.render()
851
+ if self._since_last_render >= self.configure_render_rate(
852
+ self._get_playback_speed()
853
+ ):
854
+ self.render()
855
+ self._since_last_render = 1
856
+ else:
857
+ self._since_last_render += 1
858
+
667
859
  if (
668
860
  self.over
669
861
  and "Pause" in document.getElementById("playpause").textContent
@@ -79,6 +79,28 @@ def set_results(player_names: List[str], places: List[int], map: str, verbose: b
79
79
  print(e)
80
80
 
81
81
 
82
+ @web_only
83
+ def show_download():
84
+ from js import window
85
+
86
+ if hasattr(window, "showDownload"):
87
+ try:
88
+ window.showDownload()
89
+ except Exception as e:
90
+ print(e)
91
+
92
+
93
+ @web_only
94
+ def navigate(route: str):
95
+ from js import window
96
+
97
+ if hasattr(window, "_navigate"):
98
+ try:
99
+ window._navigate(route)
100
+ except Exception as e:
101
+ print(e)
102
+
103
+
82
104
  @web_only
83
105
  def download_json(filename: str, contents: str):
84
106
  from js import window
package/dist/esm/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import '@mantine/core/styles.css';
2
2
  import '@mantine/dates/styles.css';
3
3
  import '@mantine/notifications/styles.css';
4
+ import '@mantine/dropzone/styles.css';
4
5
  import { TextInput, Button, Alert, Slider, Box, Select, NumberInput, Table, Badge, Autocomplete, Loader, MantineProvider } from '@mantine/core';
5
6
  import { TimeInput, DatesProvider } from '@mantine/dates';
6
7
  import { useColorScheme, useViewportSize } from '@mantine/hooks';
@@ -9,7 +10,8 @@ import { initializeApp } from 'firebase/app';
9
10
  import { onAuthStateChanged, signInWithEmailAndPassword, signOut, getAuth } from 'firebase/auth';
10
11
  import { onSnapshot, refEqual, doc, getDoc, setDoc, Timestamp, getFirestore } from 'firebase/firestore';
11
12
  import React, { useEffect, useReducer, useCallback, useMemo, createContext, useContext, useState, useRef, cloneElement, Component, createElement } from 'react';
12
- import { useNavigate, useLocation, useParams, Routes, Route, BrowserRouter } from 'react-router-dom';
13
+ import { useNavigate, useLocation, useParams, useSearchParams, Routes, Route, BrowserRouter } from 'react-router-dom';
14
+ import { Dropzone } from '@mantine/dropzone';
13
15
  import { jsx } from 'react/jsx-runtime';
14
16
 
15
17
  var commonjsGlobal = typeof globalThis !== 'undefined' ? globalThis : typeof window !== 'undefined' ? window : typeof global !== 'undefined' ? global : typeof self !== 'undefined' ? self : {};
@@ -407,21 +409,30 @@ const toPlacing = (n) => {
407
409
  }
408
410
  return n.toString() + DIGITS[n % 10];
409
411
  };
410
- const runNoUI = (map, apis, playerBots, verbose) => {
412
+ const runNoUI = (map, apis, playerBots, seed, verbose) => {
411
413
  const players = playerBots.map((api) => (api === "None" ? "" : apis[api]));
412
- tryUntilFailure(() =>
414
+ tryUntilSuccess(() =>
413
415
  // @ts-ignore
414
- window._startSimulation(map, players, playerBots, true, false, verbose));
416
+ window._startSimulation(map, players, playerBots, true, false, verbose, seed));
415
417
  };
416
- const tryUntilFailure = (f, timeout = 500) => {
418
+ const tryUntilSuccess = (f, timeout = 500) => {
417
419
  try {
418
420
  f();
419
421
  }
420
422
  catch (error) {
421
423
  console.log("Failed, waiting for timeout...", error === null || error === void 0 ? void 0 : error.message);
422
- setTimeout(() => tryUntilFailure(f, timeout), timeout);
424
+ setTimeout(() => tryUntilSuccess(f, timeout), timeout);
423
425
  }
424
426
  };
427
+ const downloadFile = (filename, mimeType, contents) => {
428
+ const a = document.createElement("a");
429
+ a.style.display = "none";
430
+ document.body.appendChild(a);
431
+ a.href = `data:${mimeType};charset=utf-8,${encodeURIComponent(contents)}`;
432
+ a.download = filename;
433
+ a.click();
434
+ document.body.removeChild(a);
435
+ };
425
436
 
426
437
  const useLocalStorage = ({ key, defaultValue, }) => {
427
438
  const [value, setValue] = useState(getLocalStorage(key, defaultValue !== null && defaultValue !== void 0 ? defaultValue : null));
@@ -2491,6 +2502,10 @@ const RunSimulationBlock = () => {
2491
2502
  key: "Player Bots",
2492
2503
  defaultValue: ["None", "None"],
2493
2504
  });
2505
+ const [seed, setSeed] = useLocalStorage({
2506
+ key: "Seed",
2507
+ defaultValue: "",
2508
+ });
2494
2509
  const [runningNoUI, setRunningNoUI] = useState(false);
2495
2510
  const [runningNoUIN, setRunningNoUIN] = useState({});
2496
2511
  const navigate = useNavigate();
@@ -2499,15 +2514,18 @@ const RunSimulationBlock = () => {
2499
2514
  remaining = Math.max(...Object.values(runningNoUIN));
2500
2515
  }
2501
2516
  const run = () => {
2502
- navigate(`/simulation/${map.replaceAll(" ", "-")}/${playerBots.join("-")}`);
2517
+ // @ts-ignore
2518
+ window._isSimulationFromFile = false;
2519
+ navigate(`/simulation/${map.replaceAll(" ", "-")}/${playerBots.join("-")}?seed=${seed}`);
2503
2520
  };
2504
2521
  const startRunNoUI = () => {
2505
2522
  setRunningNoUI(true);
2506
- runNoUI(map, apis, playerBots, true);
2523
+ runNoUI(map, apis, playerBots, seed.toString(), true);
2507
2524
  };
2508
2525
  const startRunNoUIN = (n) => {
2526
+ setLocalStorage("Results", {});
2509
2527
  setRunningNoUIN({ [n.toString()]: n });
2510
- runNoUI(map, apis, playerBots, false);
2528
+ runNoUI(map, apis, playerBots, seed.toString(), false);
2511
2529
  };
2512
2530
  useEffect(() => {
2513
2531
  // @ts-ignore
@@ -2561,7 +2579,7 @@ const RunSimulationBlock = () => {
2561
2579
  setLocalStorage("Results", {});
2562
2580
  }
2563
2581
  else {
2564
- runNoUI(map, apis, playerBots, false);
2582
+ runNoUI(map, apis, playerBots, seed.toString(), false);
2565
2583
  }
2566
2584
  }
2567
2585
  }, [runningNoUIN]);
@@ -2572,6 +2590,7 @@ const RunSimulationBlock = () => {
2572
2590
  }
2573
2591
  } }),
2574
2592
  React.createElement(BotSelector, { playerCount: playerCount, setPlayerCount: setPlayerCount, playerBots: playerBots, setPlayerBots: setPlayerBots, apis: apis }),
2593
+ React.createElement(NumberInput, { leftSection: React.createElement("i", { className: "fa-solid fa-dice" }), label: "Randomness Seed", min: 0, value: seed, onChange: setSeed }),
2575
2594
  React.createElement(Button.Group, { mt: "xs" },
2576
2595
  React.createElement(Button, { variant: "default", w: "50%", leftSection: React.createElement("i", { className: "fa-solid fa-play" }), onClick: run }, "Run"),
2577
2596
  React.createElement(Button, { variant: "default", w: "50%", leftSection: React.createElement("i", { className: "fa-solid fa-forward" }), onClick: startRunNoUI, loading: runningNoUI || loading }, "Run (No UI)")),
@@ -2581,7 +2600,31 @@ const RunSimulationBlock = () => {
2581
2600
  remaining !== 0 && (React.createElement("p", { style: { textAlign: "center", marginTop: 10 } },
2582
2601
  "Remaining Simulations: ",
2583
2602
  remaining)),
2584
- React.createElement("p", { style: { textAlign: "center", display: "none", marginTop: 10 }, id: "noui-progress" })));
2603
+ React.createElement("p", { style: { textAlign: "center", display: "none", marginTop: 10 }, id: "noui-progress" }),
2604
+ React.createElement(Dropzone, { mt: "xs", multiple: false, onDrop: (files) => __awaiter(void 0, void 0, void 0, function* () {
2605
+ if (files.length !== 1) {
2606
+ return;
2607
+ }
2608
+ const file = files[0];
2609
+ const text = yield file.text();
2610
+ // @ts-ignore
2611
+ window._navigate = navigate;
2612
+ // @ts-ignore
2613
+ window._isSimulationFromFile = true;
2614
+ tryUntilSuccess(() => {
2615
+ // @ts-ignore
2616
+ window._startSimulationFromFile(text);
2617
+ });
2618
+ }), style: {
2619
+ textAlign: "center",
2620
+ paddingTop: 20,
2621
+ paddingBottom: 20,
2622
+ paddingLeft: 10,
2623
+ paddingRight: 10,
2624
+ } },
2625
+ React.createElement("span", null,
2626
+ React.createElement("i", { className: "fa-solid fa-file-code", style: { marginRight: 10 } }),
2627
+ "Drag a simulation file here or click to select a file to run a simulation from a file"))));
2585
2628
  };
2586
2629
 
2587
2630
  const TournamentBlock = ({ pointModifier, inline, title }) => {
@@ -15899,13 +15942,19 @@ const Simulation = () => {
15899
15942
  const [apis, loading] = useAPIs();
15900
15943
  let { map, playerapis } = useParams();
15901
15944
  const location = useLocation();
15945
+ const [searchParams] = useSearchParams();
15902
15946
  const [winner, setWinner] = useState();
15947
+ const [downloadBytes, setDownloadBytes] = useState(false);
15903
15948
  const navigate = useNavigate();
15904
15949
  const colorScheme = useColorScheme();
15905
15950
  const showcaseMode = location.search.includes("showcase=true");
15906
15951
  useEffect(() => {
15907
15952
  n((engine) => __awaiter(void 0, void 0, void 0, function* () { return yield loadFull(engine); }));
15908
15953
  // @ts-ignore
15954
+ window.showDownload = () => {
15955
+ setDownloadBytes(true);
15956
+ };
15957
+ // @ts-ignore
15909
15958
  window.showWinner = (winner, verbose) => {
15910
15959
  if (admin) {
15911
15960
  setWinner(winner);
@@ -15935,10 +15984,16 @@ const Simulation = () => {
15935
15984
  const playerNames = (_a = playerapis === null || playerapis === void 0 ? void 0 : playerapis.split("-")) !== null && _a !== void 0 ? _a : [];
15936
15985
  const players = playerNames.map((api) => (api === "None" ? "" : apis[api]));
15937
15986
  useEffect(() => {
15938
- if (!loading && players && playerapis) {
15939
- tryUntilFailure(() =>
15987
+ var _a;
15988
+ if (!loading &&
15989
+ players &&
15990
+ playerapis &&
15991
+ // @ts-ignore
15992
+ window._isSimulationFromFile !== true) {
15993
+ const seed = (_a = searchParams.get("seed")) !== null && _a !== void 0 ? _a : "";
15994
+ tryUntilSuccess(() =>
15940
15995
  // @ts-ignore
15941
- window._startSimulation(map, players, playerNames, false, !showcaseMode, true));
15996
+ window._startSimulation(map, players, playerNames, false, !showcaseMode, true, seed));
15942
15997
  }
15943
15998
  }, [loading]);
15944
15999
  const newRank = getRank(getLocalStorage("Cached tournament/info"), winner, getLocalStorage("Point Modifier")) + 1;
@@ -16021,7 +16076,11 @@ const Simulation = () => {
16021
16076
  React.createElement("span", { id: "render-status", style: { marginRight: 10 } }),
16022
16077
  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" }),
16023
16078
  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"))),
16024
- React.createElement(PlayPauseButton, null)),
16079
+ React.createElement(PlayPauseButton, null),
16080
+ 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",
16081
+ // @ts-ignore
16082
+ window.simulationToDownload) }, "Download Simulation"))),
16083
+ showcaseMode && React.createElement("span", { id: "render-status" }),
16025
16084
  React.createElement("p", { style: { margin: 0 } }, "Playback Speed"),
16026
16085
  React.createElement(Slider, { style: { flex: "none" }, mb: 30, w: 500, maw: "85%", min: -2, defaultValue: 0, marks: [
16027
16086
  { value: -2, label: "1/4" },
@@ -2,6 +2,7 @@ import "@fortawesome/fontawesome-free/css/all.css";
2
2
  import "@mantine/core/styles.css";
3
3
  import "@mantine/dates/styles.css";
4
4
  import "@mantine/notifications/styles.css";
5
+ import "@mantine/dropzone/styles.css";
5
6
  import "prismjs/plugins/line-numbers/prism-line-numbers.css";
6
7
  import "prismjs/themes/prism.css";
7
8
  import "./index.css";
@@ -10,5 +10,6 @@ export declare const getRank: (tournamentInfo: any, team: string, pointModifier:
10
10
  export declare const updatePointModifier: () => void;
11
11
  export declare const toPlacing: (n: number) => string;
12
12
  export declare const zeroPad: (s: string, l: number) => string;
13
- export declare const runNoUI: (map: string, apis: Record<string, any>, playerBots: string[], verbose: boolean) => void;
14
- export declare const tryUntilFailure: (f: () => void, timeout?: number) => void;
13
+ export declare const runNoUI: (map: string, apis: Record<string, any>, playerBots: string[], seed: string, verbose: boolean) => void;
14
+ export declare const tryUntilSuccess: (f: () => void, timeout?: number) => void;
15
+ export declare const downloadFile: (filename: string, mimeType: string, contents: string) => void;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "code-battles",
3
- "version": "1.3.0",
3
+ "version": "1.5.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",
@@ -38,11 +38,12 @@
38
38
  "tsparticles": "^3.5.0"
39
39
  },
40
40
  "peerDependencies": {
41
- "@mantine/core": "^7.13.3",
42
- "@mantine/dates": "^7.13.3",
43
- "@mantine/hooks": "^7.13.3",
44
- "@mantine/notifications": "^7.13.3",
45
- "firebase": "^10.14.1",
41
+ "@mantine/core": "^7.13.4",
42
+ "@mantine/dates": "^7.13.4",
43
+ "@mantine/dropzone": "^7.13.4",
44
+ "@mantine/hooks": "^7.13.4",
45
+ "@mantine/notifications": "^7.13.4",
46
+ "firebase": "^11.0.1",
46
47
  "react": "^18.3.1",
47
48
  "react-dom": "^18.3.1",
48
49
  "react-router-dom": "^6.27.0"
@@ -56,7 +57,7 @@
56
57
  "@types/react": "^18.3.12",
57
58
  "@types/react-dom": "^18.3.1",
58
59
  "chokidar-cli": "^3.0.0",
59
- "rollup": "^4.24.3",
60
+ "rollup": "^4.24.4",
60
61
  "rollup-plugin-copy": "^3.5.0",
61
62
  "rollup-plugin-dts": "^6.1.1",
62
63
  "rollup-plugin-import-css": "^3.5.6",