code-battles 1.3.0 → 1.4.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 +57 -7
- package/dist/cjs/types/index.d.ts +1 -0
- package/dist/cjs/types/utilities.d.ts +2 -1
- package/dist/code_battles/__init__.py +3 -0
- package/dist/code_battles/battles.py +181 -14
- package/dist/code_battles/utilities.py +22 -0
- package/dist/esm/index.js +57 -7
- package/dist/esm/types/index.d.ts +1 -0
- package/dist/esm/types/utilities.d.ts +2 -1
- package/package.json +8 -7
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 : {};
|
|
@@ -411,19 +413,28 @@ const toPlacing = (n) => {
|
|
|
411
413
|
};
|
|
412
414
|
const runNoUI = (map, apis, playerBots, verbose) => {
|
|
413
415
|
const players = playerBots.map((api) => (api === "None" ? "" : apis[api]));
|
|
414
|
-
|
|
416
|
+
tryUntilSuccess(() =>
|
|
415
417
|
// @ts-ignore
|
|
416
418
|
window._startSimulation(map, players, playerBots, true, false, verbose));
|
|
417
419
|
};
|
|
418
|
-
const
|
|
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(() =>
|
|
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));
|
|
@@ -2501,6 +2512,8 @@ const RunSimulationBlock = () => {
|
|
|
2501
2512
|
remaining = Math.max(...Object.values(runningNoUIN));
|
|
2502
2513
|
}
|
|
2503
2514
|
const run = () => {
|
|
2515
|
+
// @ts-ignore
|
|
2516
|
+
window._isSimulationFromFile = false;
|
|
2504
2517
|
navigate(`/simulation/${map.replaceAll(" ", "-")}/${playerBots.join("-")}`);
|
|
2505
2518
|
};
|
|
2506
2519
|
const startRunNoUI = () => {
|
|
@@ -2583,7 +2596,31 @@ const RunSimulationBlock = () => {
|
|
|
2583
2596
|
remaining !== 0 && (React.createElement("p", { style: { textAlign: "center", marginTop: 10 } },
|
|
2584
2597
|
"Remaining Simulations: ",
|
|
2585
2598
|
remaining)),
|
|
2586
|
-
React.createElement("p", { style: { textAlign: "center", display: "none", marginTop: 10 }, id: "noui-progress" })
|
|
2599
|
+
React.createElement("p", { style: { textAlign: "center", display: "none", marginTop: 10 }, id: "noui-progress" }),
|
|
2600
|
+
React.createElement(dropzone.Dropzone, { mt: "xs", multiple: false, onDrop: (files) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2601
|
+
if (files.length !== 1) {
|
|
2602
|
+
return;
|
|
2603
|
+
}
|
|
2604
|
+
const file = files[0];
|
|
2605
|
+
const text = yield file.text();
|
|
2606
|
+
// @ts-ignore
|
|
2607
|
+
window._navigate = navigate;
|
|
2608
|
+
// @ts-ignore
|
|
2609
|
+
window._isSimulationFromFile = true;
|
|
2610
|
+
tryUntilSuccess(() => {
|
|
2611
|
+
// @ts-ignore
|
|
2612
|
+
window._startSimulationFromFile(text);
|
|
2613
|
+
});
|
|
2614
|
+
}), style: {
|
|
2615
|
+
textAlign: "center",
|
|
2616
|
+
paddingTop: 20,
|
|
2617
|
+
paddingBottom: 20,
|
|
2618
|
+
paddingLeft: 10,
|
|
2619
|
+
paddingRight: 10,
|
|
2620
|
+
} },
|
|
2621
|
+
React.createElement("span", null,
|
|
2622
|
+
React.createElement("i", { className: "fa-solid fa-file-code", style: { marginRight: 10 } }),
|
|
2623
|
+
"Drag a simulation file here or click to select a file to run a simulation from a file"))));
|
|
2587
2624
|
};
|
|
2588
2625
|
|
|
2589
2626
|
const TournamentBlock = ({ pointModifier, inline, title }) => {
|
|
@@ -15902,12 +15939,17 @@ const Simulation = () => {
|
|
|
15902
15939
|
let { map, playerapis } = reactRouterDom.useParams();
|
|
15903
15940
|
const location = reactRouterDom.useLocation();
|
|
15904
15941
|
const [winner, setWinner] = React.useState();
|
|
15942
|
+
const [downloadBytes, setDownloadBytes] = React.useState(false);
|
|
15905
15943
|
const navigate = reactRouterDom.useNavigate();
|
|
15906
15944
|
const colorScheme = hooks.useColorScheme();
|
|
15907
15945
|
const showcaseMode = location.search.includes("showcase=true");
|
|
15908
15946
|
React.useEffect(() => {
|
|
15909
15947
|
n((engine) => __awaiter(void 0, void 0, void 0, function* () { return yield loadFull(engine); }));
|
|
15910
15948
|
// @ts-ignore
|
|
15949
|
+
window.showDownload = () => {
|
|
15950
|
+
setDownloadBytes(true);
|
|
15951
|
+
};
|
|
15952
|
+
// @ts-ignore
|
|
15911
15953
|
window.showWinner = (winner, verbose) => {
|
|
15912
15954
|
if (admin) {
|
|
15913
15955
|
setWinner(winner);
|
|
@@ -15937,8 +15979,12 @@ const Simulation = () => {
|
|
|
15937
15979
|
const playerNames = (_a = playerapis === null || playerapis === void 0 ? void 0 : playerapis.split("-")) !== null && _a !== void 0 ? _a : [];
|
|
15938
15980
|
const players = playerNames.map((api) => (api === "None" ? "" : apis[api]));
|
|
15939
15981
|
React.useEffect(() => {
|
|
15940
|
-
if (!loading &&
|
|
15941
|
-
|
|
15982
|
+
if (!loading &&
|
|
15983
|
+
players &&
|
|
15984
|
+
playerapis &&
|
|
15985
|
+
// @ts-ignore
|
|
15986
|
+
window._isSimulationFromFile !== true) {
|
|
15987
|
+
tryUntilSuccess(() =>
|
|
15942
15988
|
// @ts-ignore
|
|
15943
15989
|
window._startSimulation(map, players, playerNames, false, !showcaseMode, true));
|
|
15944
15990
|
}
|
|
@@ -16023,7 +16069,11 @@ const Simulation = () => {
|
|
|
16023
16069
|
React.createElement("span", { id: "render-status", style: { marginRight: 10 } }),
|
|
16024
16070
|
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
16071
|
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)
|
|
16072
|
+
React.createElement(PlayPauseButton, null),
|
|
16073
|
+
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",
|
|
16074
|
+
// @ts-ignore
|
|
16075
|
+
window.simulationToDownload) }, "Download Simulation"))),
|
|
16076
|
+
showcaseMode && React.createElement("span", { id: "render-status" }),
|
|
16027
16077
|
React.createElement("p", { style: { margin: 0 } }, "Playback Speed"),
|
|
16028
16078
|
React.createElement(core.Slider, { style: { flex: "none" }, mb: 30, w: 500, maw: "85%", min: -2, defaultValue: 0, marks: [
|
|
16029
16079
|
{ 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";
|
|
@@ -11,4 +11,5 @@ 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
13
|
export declare const runNoUI: (map: string, apis: Record<string, any>, playerBots: string[], verbose: boolean) => void;
|
|
14
|
-
export declare const
|
|
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
8
|
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,50 @@ 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
|
+
|
|
47
|
+
def dump(self):
|
|
48
|
+
return base64.b64encode(
|
|
49
|
+
gzip.compress(
|
|
50
|
+
json.dumps(
|
|
51
|
+
{
|
|
52
|
+
"map": self.map,
|
|
53
|
+
"playerNames": self.player_names,
|
|
54
|
+
"game": self.game,
|
|
55
|
+
"version": self.version,
|
|
56
|
+
"timestamp": self.timestamp.isoformat(),
|
|
57
|
+
"logs": self.logs,
|
|
58
|
+
"decisions": [
|
|
59
|
+
base64.b64encode(decision).decode()
|
|
60
|
+
for decision in self.decisions
|
|
61
|
+
],
|
|
62
|
+
}
|
|
63
|
+
).encode()
|
|
64
|
+
)
|
|
65
|
+
).decode()
|
|
66
|
+
|
|
67
|
+
@staticmethod
|
|
68
|
+
def load(file: str):
|
|
69
|
+
contents = json.loads(gzip.decompress(base64.b64decode(file)))
|
|
70
|
+
return Simulation(
|
|
71
|
+
contents["map"],
|
|
72
|
+
contents["playerNames"],
|
|
73
|
+
contents["game"],
|
|
74
|
+
contents["version"],
|
|
75
|
+
datetime.datetime.fromisoformat(contents["timestamp"]),
|
|
76
|
+
contents["logs"],
|
|
77
|
+
[base64.b64decode(decision) for decision in contents["decisions"]],
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
32
81
|
class CodeBattles(
|
|
33
82
|
Generic[GameStateType, APIImplementationType, APIType, PlayerRequestsType]
|
|
34
83
|
):
|
|
@@ -77,6 +126,7 @@ class CodeBattles(
|
|
|
77
126
|
_eliminated: List[int]
|
|
78
127
|
_sounds: Dict[str, "js.Audio"] = {}
|
|
79
128
|
_decisions: List[bytes]
|
|
129
|
+
_since_last_render: int
|
|
80
130
|
|
|
81
131
|
def render(self) -> None:
|
|
82
132
|
"""
|
|
@@ -188,12 +238,12 @@ class CodeBattles(
|
|
|
188
238
|
|
|
189
239
|
return 1
|
|
190
240
|
|
|
191
|
-
def configure_map_image_url(self, map: str):
|
|
241
|
+
def configure_map_image_url(self, map: str) -> str:
|
|
192
242
|
"""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
243
|
|
|
194
244
|
return "/images/maps/" + map.lower().replace(" ", "_") + ".png"
|
|
195
245
|
|
|
196
|
-
def configure_sound_url(self, name: str):
|
|
246
|
+
def configure_sound_url(self, name: str) -> str:
|
|
197
247
|
"""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
248
|
|
|
199
249
|
return "/sounds/" + name.lower().replace(" ", "_") + ".mp3"
|
|
@@ -203,6 +253,14 @@ class CodeBattles(
|
|
|
203
253
|
|
|
204
254
|
return "CodeBattlesBot"
|
|
205
255
|
|
|
256
|
+
def configure_render_rate(self, playback_speed: float) -> int:
|
|
257
|
+
"""
|
|
258
|
+
The amount of frames to simulate before each render.
|
|
259
|
+
|
|
260
|
+
For games with an intensive `render` method, this is useful for higher playback speeds.
|
|
261
|
+
"""
|
|
262
|
+
return 1
|
|
263
|
+
|
|
206
264
|
def configure_bot_globals(self) -> Dict[str, Any]:
|
|
207
265
|
"""
|
|
208
266
|
Configure additional available global items, such as libraries from the Python standard library, bots can use.
|
|
@@ -221,6 +279,10 @@ class CodeBattles(
|
|
|
221
279
|
"random": random,
|
|
222
280
|
}
|
|
223
281
|
|
|
282
|
+
def configure_version(self) -> str:
|
|
283
|
+
"""Configure the version of the game, which is stored in the simulation files."""
|
|
284
|
+
return "1.0.0"
|
|
285
|
+
|
|
224
286
|
@web_only
|
|
225
287
|
def download_images(
|
|
226
288
|
self, sources: List[Tuple[str, str]]
|
|
@@ -356,7 +418,16 @@ class CodeBattles(
|
|
|
356
418
|
volume = window.localStorage.getItem("Volume") or 0
|
|
357
419
|
s = self._sounds[sound].cloneNode(True)
|
|
358
420
|
s.volume = volume
|
|
359
|
-
|
|
421
|
+
|
|
422
|
+
async def p():
|
|
423
|
+
try:
|
|
424
|
+
await s.play()
|
|
425
|
+
except Exception:
|
|
426
|
+
print(
|
|
427
|
+
f"Warning: couldn't play sound '{sound}'. Make sure the `sound` and `configure_sound_url` are correct."
|
|
428
|
+
)
|
|
429
|
+
|
|
430
|
+
asyncio.get_event_loop().run_until_complete(p())
|
|
360
431
|
|
|
361
432
|
@property
|
|
362
433
|
def time(self) -> str:
|
|
@@ -389,6 +460,7 @@ class CodeBattles(
|
|
|
389
460
|
self.step = 0
|
|
390
461
|
self._logs = []
|
|
391
462
|
self._decisions = []
|
|
463
|
+
self._decision_index = 0
|
|
392
464
|
self.active_players = list(range(len(self.player_names)))
|
|
393
465
|
self.active_players = list(range(len(self.player_names)))
|
|
394
466
|
self.state = self.create_initial_state()
|
|
@@ -398,7 +470,8 @@ class CodeBattles(
|
|
|
398
470
|
]
|
|
399
471
|
self._eliminated = []
|
|
400
472
|
self._player_globals = self._get_initial_player_globals(player_codes)
|
|
401
|
-
self.
|
|
473
|
+
self._since_last_render = 1
|
|
474
|
+
self._start_time = time.time()
|
|
402
475
|
|
|
403
476
|
def _run_webworker_simulation(
|
|
404
477
|
self, map: str, player_names_str: str, player_codes_str: str
|
|
@@ -469,6 +542,71 @@ class CodeBattles(
|
|
|
469
542
|
loop = asyncio.get_event_loop()
|
|
470
543
|
loop.run_until_complete(self._start_simulation_async(*args, **kwargs))
|
|
471
544
|
|
|
545
|
+
def _start_simulation_from_file(self, contents: str):
|
|
546
|
+
loop = asyncio.get_event_loop()
|
|
547
|
+
loop.run_until_complete(self._start_simulation_from_file_async(contents))
|
|
548
|
+
|
|
549
|
+
async def _start_simulation_from_file_async(self, contents: str):
|
|
550
|
+
from js import document
|
|
551
|
+
|
|
552
|
+
try:
|
|
553
|
+
simulation = Simulation.load(str(contents))
|
|
554
|
+
navigate(
|
|
555
|
+
f"/simulation/{simulation.map}/{'-'.join(simulation.player_names)}"
|
|
556
|
+
)
|
|
557
|
+
show_alert(
|
|
558
|
+
"Loaded simulation file!",
|
|
559
|
+
f"{', '.join(simulation.player_names)} competed in {simulation.map} at {simulation.timestamp}",
|
|
560
|
+
"blue",
|
|
561
|
+
"fa-solid fa-file-code",
|
|
562
|
+
0,
|
|
563
|
+
)
|
|
564
|
+
if simulation.game != self.__class__.__name__:
|
|
565
|
+
show_alert(
|
|
566
|
+
"Warning: game mismatch!",
|
|
567
|
+
f"Simulation file is for game {simulation.game} while the website is running {self.__class__.__name__}!",
|
|
568
|
+
"yellow",
|
|
569
|
+
"fa-solid fa-exclamation",
|
|
570
|
+
0,
|
|
571
|
+
)
|
|
572
|
+
if simulation.version != self.configure_version():
|
|
573
|
+
show_alert(
|
|
574
|
+
"Warning: version mismatch!",
|
|
575
|
+
f"Simulation file is for version {simulation.version} while the website is running {self.configure_version()}!",
|
|
576
|
+
"yellow",
|
|
577
|
+
"fa-solid fa-exclamation",
|
|
578
|
+
0,
|
|
579
|
+
)
|
|
580
|
+
while document.getElementById("loader") is None:
|
|
581
|
+
await asyncio.sleep(0.01)
|
|
582
|
+
if not hasattr(self, "_initialized"):
|
|
583
|
+
self._initialize()
|
|
584
|
+
self.map = simulation.map
|
|
585
|
+
self.map_image = await download_image(
|
|
586
|
+
self.configure_map_image_url(simulation.map)
|
|
587
|
+
)
|
|
588
|
+
self.player_names = simulation.player_names
|
|
589
|
+
self.background = False
|
|
590
|
+
self.console_visible = True
|
|
591
|
+
self.verbose = False
|
|
592
|
+
self._initialize_simulation(["" for _ in simulation.player_names])
|
|
593
|
+
self._decisions = simulation.decisions
|
|
594
|
+
self._logs = simulation.logs
|
|
595
|
+
self.canvas = GameCanvas(
|
|
596
|
+
document.getElementById("simulation"),
|
|
597
|
+
self.configure_board_count(),
|
|
598
|
+
self.map_image,
|
|
599
|
+
document.body.clientWidth - 440,
|
|
600
|
+
document.body.clientHeight - 280,
|
|
601
|
+
self.configure_extra_width(),
|
|
602
|
+
self.configure_extra_height(),
|
|
603
|
+
)
|
|
604
|
+
document.getElementById("loader").style.display = "none"
|
|
605
|
+
await self.setup()
|
|
606
|
+
self.render()
|
|
607
|
+
except Exception as e:
|
|
608
|
+
print(e)
|
|
609
|
+
|
|
472
610
|
@web_only
|
|
473
611
|
async def _start_simulation_async(
|
|
474
612
|
self,
|
|
@@ -507,7 +645,9 @@ class CodeBattles(
|
|
|
507
645
|
document.body.clientWidth - 440
|
|
508
646
|
if console_visible
|
|
509
647
|
else document.body.clientWidth - 40,
|
|
510
|
-
document.body.clientHeight - 280
|
|
648
|
+
document.body.clientHeight - 280
|
|
649
|
+
if console_visible
|
|
650
|
+
else document.body.clientHeight - 160,
|
|
511
651
|
self.configure_extra_width(),
|
|
512
652
|
self.configure_extra_height(),
|
|
513
653
|
)
|
|
@@ -533,22 +673,38 @@ class CodeBattles(
|
|
|
533
673
|
traceback.print_exc()
|
|
534
674
|
|
|
535
675
|
def _update_step(self, decisions_str: str, logs_str: str, is_over_str: str):
|
|
536
|
-
from js import document
|
|
676
|
+
from js import window, document
|
|
537
677
|
|
|
678
|
+
now = time.time()
|
|
538
679
|
decisions = base64.b64decode(str(decisions_str))
|
|
539
680
|
logs: list = json.loads(str(logs_str))
|
|
540
681
|
is_over = str(is_over_str) == "true"
|
|
541
682
|
|
|
542
683
|
self._decisions.append(decisions)
|
|
543
684
|
self._logs.append(logs)
|
|
544
|
-
|
|
685
|
+
|
|
686
|
+
if is_over:
|
|
687
|
+
try:
|
|
688
|
+
simulation = Simulation(
|
|
689
|
+
self.map,
|
|
690
|
+
self.player_names,
|
|
691
|
+
self.__class__.__name__,
|
|
692
|
+
self.configure_version(),
|
|
693
|
+
datetime.datetime.now(),
|
|
694
|
+
self._logs,
|
|
695
|
+
self._decisions,
|
|
696
|
+
)
|
|
697
|
+
window.simulationToDownload = simulation.dump()
|
|
698
|
+
show_download()
|
|
699
|
+
except Exception as e:
|
|
700
|
+
print(e)
|
|
545
701
|
|
|
546
702
|
render_status = document.getElementById("render-status")
|
|
547
703
|
if render_status is not None:
|
|
548
704
|
render_status.textContent = (
|
|
549
|
-
"Rendering: Complete!"
|
|
705
|
+
f"Rendering: Complete! ({int(now - self._start_time)}s)"
|
|
550
706
|
if is_over
|
|
551
|
-
else f"Rendering: Frame {self.
|
|
707
|
+
else f"Rendering: Frame {len(self._decisions)} ({int(now - self._start_time)}s)"
|
|
552
708
|
)
|
|
553
709
|
|
|
554
710
|
def _get_initial_player_globals(self, player_codes: List[str]):
|
|
@@ -627,7 +783,9 @@ class CodeBattles(
|
|
|
627
783
|
document.body.clientWidth - 440
|
|
628
784
|
if self.console_visible
|
|
629
785
|
else document.body.clientWidth - 40,
|
|
630
|
-
document.body.clientHeight - 280
|
|
786
|
+
document.body.clientHeight - 280
|
|
787
|
+
if self.console_visible
|
|
788
|
+
else document.body.clientHeight - 160,
|
|
631
789
|
)
|
|
632
790
|
if not self.background:
|
|
633
791
|
self.render()
|
|
@@ -638,19 +796,20 @@ class CodeBattles(
|
|
|
638
796
|
from pyscript.ffi import create_proxy
|
|
639
797
|
|
|
640
798
|
if not self.over:
|
|
641
|
-
if len(self._decisions) ==
|
|
799
|
+
if len(self._decisions) == self._decision_index:
|
|
642
800
|
print("Warning: sleeping because decisions were not made yet!")
|
|
643
801
|
setTimeout(create_proxy(self._step), 100)
|
|
644
802
|
return
|
|
645
803
|
else:
|
|
646
|
-
logs = self._logs.
|
|
804
|
+
logs = self._logs[self._decision_index]
|
|
647
805
|
for log in logs:
|
|
648
806
|
console_log(
|
|
649
807
|
-1 if log["player_index"] is None else log["player_index"],
|
|
650
808
|
log["text"],
|
|
651
809
|
log["color"],
|
|
652
810
|
)
|
|
653
|
-
self.apply_decisions(self._decisions.
|
|
811
|
+
self.apply_decisions(self._decisions[self._decision_index])
|
|
812
|
+
self._decision_index += 1
|
|
654
813
|
|
|
655
814
|
if not self.over:
|
|
656
815
|
self.step += 1
|
|
@@ -661,9 +820,17 @@ class CodeBattles(
|
|
|
661
820
|
set_results(
|
|
662
821
|
self.player_names, self._eliminated[::-1], self.map, self.verbose
|
|
663
822
|
)
|
|
823
|
+
self.render()
|
|
664
824
|
|
|
665
825
|
if not self.background:
|
|
666
|
-
self.
|
|
826
|
+
if self._since_last_render >= self.configure_render_rate(
|
|
827
|
+
self._get_playback_speed()
|
|
828
|
+
):
|
|
829
|
+
self.render()
|
|
830
|
+
self._since_last_render = 1
|
|
831
|
+
else:
|
|
832
|
+
self._since_last_render += 1
|
|
833
|
+
|
|
667
834
|
if (
|
|
668
835
|
self.over
|
|
669
836
|
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';
|
|
@@ -10,6 +11,7 @@ import { onAuthStateChanged, signInWithEmailAndPassword, signOut, getAuth } from
|
|
|
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
13
|
import { useNavigate, useLocation, useParams, 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 : {};
|
|
@@ -409,19 +411,28 @@ const toPlacing = (n) => {
|
|
|
409
411
|
};
|
|
410
412
|
const runNoUI = (map, apis, playerBots, verbose) => {
|
|
411
413
|
const players = playerBots.map((api) => (api === "None" ? "" : apis[api]));
|
|
412
|
-
|
|
414
|
+
tryUntilSuccess(() =>
|
|
413
415
|
// @ts-ignore
|
|
414
416
|
window._startSimulation(map, players, playerBots, true, false, verbose));
|
|
415
417
|
};
|
|
416
|
-
const
|
|
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(() =>
|
|
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));
|
|
@@ -2499,6 +2510,8 @@ const RunSimulationBlock = () => {
|
|
|
2499
2510
|
remaining = Math.max(...Object.values(runningNoUIN));
|
|
2500
2511
|
}
|
|
2501
2512
|
const run = () => {
|
|
2513
|
+
// @ts-ignore
|
|
2514
|
+
window._isSimulationFromFile = false;
|
|
2502
2515
|
navigate(`/simulation/${map.replaceAll(" ", "-")}/${playerBots.join("-")}`);
|
|
2503
2516
|
};
|
|
2504
2517
|
const startRunNoUI = () => {
|
|
@@ -2581,7 +2594,31 @@ const RunSimulationBlock = () => {
|
|
|
2581
2594
|
remaining !== 0 && (React.createElement("p", { style: { textAlign: "center", marginTop: 10 } },
|
|
2582
2595
|
"Remaining Simulations: ",
|
|
2583
2596
|
remaining)),
|
|
2584
|
-
React.createElement("p", { style: { textAlign: "center", display: "none", marginTop: 10 }, id: "noui-progress" })
|
|
2597
|
+
React.createElement("p", { style: { textAlign: "center", display: "none", marginTop: 10 }, id: "noui-progress" }),
|
|
2598
|
+
React.createElement(Dropzone, { mt: "xs", multiple: false, onDrop: (files) => __awaiter(void 0, void 0, void 0, function* () {
|
|
2599
|
+
if (files.length !== 1) {
|
|
2600
|
+
return;
|
|
2601
|
+
}
|
|
2602
|
+
const file = files[0];
|
|
2603
|
+
const text = yield file.text();
|
|
2604
|
+
// @ts-ignore
|
|
2605
|
+
window._navigate = navigate;
|
|
2606
|
+
// @ts-ignore
|
|
2607
|
+
window._isSimulationFromFile = true;
|
|
2608
|
+
tryUntilSuccess(() => {
|
|
2609
|
+
// @ts-ignore
|
|
2610
|
+
window._startSimulationFromFile(text);
|
|
2611
|
+
});
|
|
2612
|
+
}), style: {
|
|
2613
|
+
textAlign: "center",
|
|
2614
|
+
paddingTop: 20,
|
|
2615
|
+
paddingBottom: 20,
|
|
2616
|
+
paddingLeft: 10,
|
|
2617
|
+
paddingRight: 10,
|
|
2618
|
+
} },
|
|
2619
|
+
React.createElement("span", null,
|
|
2620
|
+
React.createElement("i", { className: "fa-solid fa-file-code", style: { marginRight: 10 } }),
|
|
2621
|
+
"Drag a simulation file here or click to select a file to run a simulation from a file"))));
|
|
2585
2622
|
};
|
|
2586
2623
|
|
|
2587
2624
|
const TournamentBlock = ({ pointModifier, inline, title }) => {
|
|
@@ -15900,12 +15937,17 @@ const Simulation = () => {
|
|
|
15900
15937
|
let { map, playerapis } = useParams();
|
|
15901
15938
|
const location = useLocation();
|
|
15902
15939
|
const [winner, setWinner] = useState();
|
|
15940
|
+
const [downloadBytes, setDownloadBytes] = useState(false);
|
|
15903
15941
|
const navigate = useNavigate();
|
|
15904
15942
|
const colorScheme = useColorScheme();
|
|
15905
15943
|
const showcaseMode = location.search.includes("showcase=true");
|
|
15906
15944
|
useEffect(() => {
|
|
15907
15945
|
n((engine) => __awaiter(void 0, void 0, void 0, function* () { return yield loadFull(engine); }));
|
|
15908
15946
|
// @ts-ignore
|
|
15947
|
+
window.showDownload = () => {
|
|
15948
|
+
setDownloadBytes(true);
|
|
15949
|
+
};
|
|
15950
|
+
// @ts-ignore
|
|
15909
15951
|
window.showWinner = (winner, verbose) => {
|
|
15910
15952
|
if (admin) {
|
|
15911
15953
|
setWinner(winner);
|
|
@@ -15935,8 +15977,12 @@ const Simulation = () => {
|
|
|
15935
15977
|
const playerNames = (_a = playerapis === null || playerapis === void 0 ? void 0 : playerapis.split("-")) !== null && _a !== void 0 ? _a : [];
|
|
15936
15978
|
const players = playerNames.map((api) => (api === "None" ? "" : apis[api]));
|
|
15937
15979
|
useEffect(() => {
|
|
15938
|
-
if (!loading &&
|
|
15939
|
-
|
|
15980
|
+
if (!loading &&
|
|
15981
|
+
players &&
|
|
15982
|
+
playerapis &&
|
|
15983
|
+
// @ts-ignore
|
|
15984
|
+
window._isSimulationFromFile !== true) {
|
|
15985
|
+
tryUntilSuccess(() =>
|
|
15940
15986
|
// @ts-ignore
|
|
15941
15987
|
window._startSimulation(map, players, playerNames, false, !showcaseMode, true));
|
|
15942
15988
|
}
|
|
@@ -16021,7 +16067,11 @@ const Simulation = () => {
|
|
|
16021
16067
|
React.createElement("span", { id: "render-status", style: { marginRight: 10 } }),
|
|
16022
16068
|
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
16069
|
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)
|
|
16070
|
+
React.createElement(PlayPauseButton, null),
|
|
16071
|
+
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",
|
|
16072
|
+
// @ts-ignore
|
|
16073
|
+
window.simulationToDownload) }, "Download Simulation"))),
|
|
16074
|
+
showcaseMode && React.createElement("span", { id: "render-status" }),
|
|
16025
16075
|
React.createElement("p", { style: { margin: 0 } }, "Playback Speed"),
|
|
16026
16076
|
React.createElement(Slider, { style: { flex: "none" }, mb: 30, w: 500, maw: "85%", min: -2, defaultValue: 0, marks: [
|
|
16027
16077
|
{ 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";
|
|
@@ -11,4 +11,5 @@ 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
13
|
export declare const runNoUI: (map: string, apis: Record<string, any>, playerBots: string[], verbose: boolean) => void;
|
|
14
|
-
export declare const
|
|
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
|
+
"version": "1.4.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.
|
|
42
|
-
"@mantine/dates": "^7.13.
|
|
43
|
-
"@mantine/
|
|
44
|
-
"@mantine/
|
|
45
|
-
"
|
|
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.
|
|
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",
|