code-battles 1.2.1 → 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 +58 -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 +16 -5
- package/dist/code_battles/battles.py +291 -22
- package/dist/code_battles/js.pyi +5 -0
- package/dist/code_battles/pyscript/__init__.pyi +4 -0
- package/dist/code_battles/utilities.py +103 -10
- package/dist/code_battles/worker.py +6 -0
- package/dist/esm/index.js +58 -7
- package/dist/esm/types/index.d.ts +1 -0
- package/dist/esm/types/utilities.d.ts +2 -1
- package/package.json +12 -11
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
|
}
|
|
@@ -16020,9 +16066,14 @@ const Simulation = () => {
|
|
|
16020
16066
|
!location.search.includes("background=true") && (React.createElement(React.Fragment, null,
|
|
16021
16067
|
React.createElement("div", { style: { textAlign: "center", flexGrow: 1 } },
|
|
16022
16068
|
showcaseMode || (React.createElement(React.Fragment, null,
|
|
16069
|
+
React.createElement("span", { id: "render-status", style: { marginRight: 10 } }),
|
|
16023
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" }),
|
|
16024
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"))),
|
|
16025
|
-
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" }),
|
|
16026
16077
|
React.createElement("p", { style: { margin: 0 } }, "Playback Speed"),
|
|
16027
16078
|
React.createElement(core.Slider, { style: { flex: "none" }, mb: 30, w: 500, maw: "85%", min: -2, defaultValue: 0, marks: [
|
|
16028
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;
|
|
@@ -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,21 @@ 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
|
+
window._startSimulationFromFile = create_proxy(
|
|
18
|
+
battles._start_simulation_from_file
|
|
19
|
+
)
|
|
20
|
+
elif is_worker():
|
|
21
|
+
setattr(
|
|
22
|
+
sys.modules["__main__"],
|
|
23
|
+
"_run_webworker_simulation",
|
|
24
|
+
battles._run_webworker_simulation,
|
|
25
|
+
)
|
|
26
|
+
setattr(sys.modules["__main__"], "__export__", ["_run_webworker_simulation"])
|
|
16
27
|
else:
|
|
17
28
|
battles._run_local_simulation()
|
|
18
29
|
|
|
@@ -1,36 +1,81 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import base64
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
import datetime
|
|
2
5
|
import json
|
|
3
6
|
import math
|
|
4
7
|
import time
|
|
5
8
|
import random
|
|
6
9
|
import sys
|
|
7
10
|
import traceback
|
|
11
|
+
import gzip
|
|
8
12
|
|
|
9
13
|
from typing import Any, Dict, Generic, List, Optional, Tuple, TypeVar
|
|
10
14
|
from code_battles.utilities import (
|
|
11
15
|
GameCanvas,
|
|
12
16
|
console_log,
|
|
13
17
|
download_image,
|
|
18
|
+
navigate,
|
|
14
19
|
set_results,
|
|
15
20
|
show_alert,
|
|
21
|
+
show_download,
|
|
22
|
+
web_only,
|
|
23
|
+
is_web,
|
|
16
24
|
)
|
|
17
|
-
|
|
18
|
-
|
|
25
|
+
|
|
26
|
+
try:
|
|
27
|
+
import js
|
|
28
|
+
except Exception:
|
|
29
|
+
pass
|
|
19
30
|
|
|
20
31
|
GameStateType = TypeVar("GameStateType")
|
|
21
32
|
APIImplementationType = TypeVar("APIImplementationType")
|
|
22
33
|
APIType = TypeVar("APIType")
|
|
23
34
|
PlayerRequestsType = TypeVar("PlayerRequestsType")
|
|
24
35
|
|
|
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
36
|
|
|
33
|
-
|
|
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
|
+
)
|
|
34
79
|
|
|
35
80
|
|
|
36
81
|
class CodeBattles(
|
|
@@ -56,7 +101,7 @@ class CodeBattles(
|
|
|
56
101
|
"""The name of the players. This is populated before any of the overridable methods run."""
|
|
57
102
|
map: str
|
|
58
103
|
"""The name of the map. This is populated before any of the overridable methods run."""
|
|
59
|
-
map_image: Image
|
|
104
|
+
map_image: "js.Image"
|
|
60
105
|
"""The map image. This is populated before any of the overridable methods run."""
|
|
61
106
|
canvas: GameCanvas
|
|
62
107
|
"""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 +124,9 @@ class CodeBattles(
|
|
|
79
124
|
_player_globals: List[Dict[str, Any]]
|
|
80
125
|
_initialized: bool
|
|
81
126
|
_eliminated: List[int]
|
|
82
|
-
_sounds: Dict[str, Audio] = {}
|
|
127
|
+
_sounds: Dict[str, "js.Audio"] = {}
|
|
128
|
+
_decisions: List[bytes]
|
|
129
|
+
_since_last_render: int
|
|
83
130
|
|
|
84
131
|
def render(self) -> None:
|
|
85
132
|
"""
|
|
@@ -191,12 +238,12 @@ class CodeBattles(
|
|
|
191
238
|
|
|
192
239
|
return 1
|
|
193
240
|
|
|
194
|
-
def configure_map_image_url(self, map: str):
|
|
241
|
+
def configure_map_image_url(self, map: str) -> str:
|
|
195
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."""
|
|
196
243
|
|
|
197
244
|
return "/images/maps/" + map.lower().replace(" ", "_") + ".png"
|
|
198
245
|
|
|
199
|
-
def configure_sound_url(self, name: str):
|
|
246
|
+
def configure_sound_url(self, name: str) -> str:
|
|
200
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."""
|
|
201
248
|
|
|
202
249
|
return "/sounds/" + name.lower().replace(" ", "_") + ".mp3"
|
|
@@ -206,6 +253,14 @@ class CodeBattles(
|
|
|
206
253
|
|
|
207
254
|
return "CodeBattlesBot"
|
|
208
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
|
+
|
|
209
264
|
def configure_bot_globals(self) -> Dict[str, Any]:
|
|
210
265
|
"""
|
|
211
266
|
Configure additional available global items, such as libraries from the Python standard library, bots can use.
|
|
@@ -224,14 +279,19 @@ class CodeBattles(
|
|
|
224
279
|
"random": random,
|
|
225
280
|
}
|
|
226
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
|
+
|
|
227
286
|
@web_only
|
|
228
287
|
def download_images(
|
|
229
288
|
self, sources: List[Tuple[str, str]]
|
|
230
|
-
) -> asyncio.Future[Dict[str, Image]]:
|
|
289
|
+
) -> asyncio.Future[Dict[str, "js.Image"]]:
|
|
231
290
|
"""
|
|
232
291
|
:param sources: A list of ``(image_name, image_url)`` to download.
|
|
233
292
|
:returns: A future which can be ``await``'d containing a dictionary mapping each ``image_name`` to its loaded image.
|
|
234
293
|
"""
|
|
294
|
+
from js import Image
|
|
235
295
|
|
|
236
296
|
remaining_images: List[str] = []
|
|
237
297
|
result = asyncio.Future()
|
|
@@ -268,6 +328,7 @@ class CodeBattles(
|
|
|
268
328
|
@web_only
|
|
269
329
|
async def load_font(self, name: str, url: str) -> None:
|
|
270
330
|
"""Loads the font from the specified url as the specified name."""
|
|
331
|
+
from js import FontFace, document
|
|
271
332
|
|
|
272
333
|
ff = FontFace.new(name, f"url({url})")
|
|
273
334
|
await ff.load()
|
|
@@ -334,7 +395,7 @@ class CodeBattles(
|
|
|
334
395
|
|
|
335
396
|
For game-global log entries (not coming from a specific player), don't specify a ``player_index``.
|
|
336
397
|
"""
|
|
337
|
-
if is_web:
|
|
398
|
+
if is_web():
|
|
338
399
|
console_log(-1 if player_index is None else player_index, text, color)
|
|
339
400
|
else:
|
|
340
401
|
self._logs.append(
|
|
@@ -349,6 +410,7 @@ class CodeBattles(
|
|
|
349
410
|
@web_only
|
|
350
411
|
def play_sound(self, sound: str):
|
|
351
412
|
"""Plays the given sound, from the URL given by :func:`configure_sound_url`."""
|
|
413
|
+
from js import window, Audio
|
|
352
414
|
|
|
353
415
|
if sound not in self._sounds:
|
|
354
416
|
self._sounds[sound] = Audio.new(self.configure_sound_url(sound))
|
|
@@ -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:
|
|
@@ -370,7 +441,11 @@ class CodeBattles(
|
|
|
370
441
|
|
|
371
442
|
return len(self.active_players) <= 1
|
|
372
443
|
|
|
444
|
+
@web_only
|
|
373
445
|
def _initialize(self):
|
|
446
|
+
from js import window, document
|
|
447
|
+
from pyscript.ffi import create_proxy
|
|
448
|
+
|
|
374
449
|
window.addEventListener("resize", create_proxy(lambda _: self._resize_canvas()))
|
|
375
450
|
document.getElementById("playpause").onclick = create_proxy(
|
|
376
451
|
lambda _: asyncio.get_event_loop().run_until_complete(self._play_pause())
|
|
@@ -383,6 +458,9 @@ class CodeBattles(
|
|
|
383
458
|
|
|
384
459
|
def _initialize_simulation(self, player_codes: List[str]):
|
|
385
460
|
self.step = 0
|
|
461
|
+
self._logs = []
|
|
462
|
+
self._decisions = []
|
|
463
|
+
self._decision_index = 0
|
|
386
464
|
self.active_players = list(range(len(self.player_names)))
|
|
387
465
|
self.active_players = list(range(len(self.player_names)))
|
|
388
466
|
self.state = self.create_initial_state()
|
|
@@ -392,6 +470,39 @@ class CodeBattles(
|
|
|
392
470
|
]
|
|
393
471
|
self._eliminated = []
|
|
394
472
|
self._player_globals = self._get_initial_player_globals(player_codes)
|
|
473
|
+
self._since_last_render = 1
|
|
474
|
+
self._start_time = time.time()
|
|
475
|
+
|
|
476
|
+
def _run_webworker_simulation(
|
|
477
|
+
self, map: str, player_names_str: str, player_codes_str: str
|
|
478
|
+
):
|
|
479
|
+
from pyscript import sync
|
|
480
|
+
|
|
481
|
+
# JS to Python
|
|
482
|
+
player_names = json.loads(player_names_str)
|
|
483
|
+
player_codes = json.loads(player_codes_str)
|
|
484
|
+
|
|
485
|
+
self.map = map
|
|
486
|
+
self.player_names = player_names
|
|
487
|
+
self.background = True
|
|
488
|
+
self.console_visible = False
|
|
489
|
+
self.verbose = False
|
|
490
|
+
self._initialize_simulation(player_codes)
|
|
491
|
+
while not self.over:
|
|
492
|
+
self._logs = []
|
|
493
|
+
decisions = self.make_decisions()
|
|
494
|
+
logs = self._logs
|
|
495
|
+
self._logs = []
|
|
496
|
+
self.apply_decisions(decisions)
|
|
497
|
+
|
|
498
|
+
sync.update_step(
|
|
499
|
+
base64.b64encode(decisions).decode(),
|
|
500
|
+
json.dumps(logs),
|
|
501
|
+
"true" if self.over else "false",
|
|
502
|
+
)
|
|
503
|
+
|
|
504
|
+
if not self.over:
|
|
505
|
+
self.step += 1
|
|
395
506
|
|
|
396
507
|
def _run_local_simulation(self):
|
|
397
508
|
self.map = sys.argv[1]
|
|
@@ -399,7 +510,6 @@ class CodeBattles(
|
|
|
399
510
|
self.background = True
|
|
400
511
|
self.console_visible = False
|
|
401
512
|
self.verbose = False
|
|
402
|
-
self._logs = []
|
|
403
513
|
player_codes = []
|
|
404
514
|
for filename in sys.argv[3:]:
|
|
405
515
|
with open(filename, "r") as f:
|
|
@@ -432,6 +542,72 @@ class CodeBattles(
|
|
|
432
542
|
loop = asyncio.get_event_loop()
|
|
433
543
|
loop.run_until_complete(self._start_simulation_async(*args, **kwargs))
|
|
434
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
|
+
|
|
610
|
+
@web_only
|
|
435
611
|
async def _start_simulation_async(
|
|
436
612
|
self,
|
|
437
613
|
map: str,
|
|
@@ -441,7 +617,18 @@ class CodeBattles(
|
|
|
441
617
|
console_visible: bool,
|
|
442
618
|
verbose: bool,
|
|
443
619
|
):
|
|
620
|
+
from js import document
|
|
621
|
+
from pyscript import workers
|
|
622
|
+
|
|
623
|
+
# JS to Python
|
|
624
|
+
player_names = [str(x) for x in player_names]
|
|
625
|
+
player_codes = [str(x) for x in player_codes]
|
|
626
|
+
|
|
444
627
|
try:
|
|
628
|
+
render_status = document.getElementById("render-status")
|
|
629
|
+
if render_status is not None:
|
|
630
|
+
render_status.textContent = "Rendering: Initializing..."
|
|
631
|
+
|
|
445
632
|
self.map = map
|
|
446
633
|
self.player_names = player_names
|
|
447
634
|
self.map_image = await download_image(self.configure_map_image_url(map))
|
|
@@ -458,7 +645,9 @@ class CodeBattles(
|
|
|
458
645
|
document.body.clientWidth - 440
|
|
459
646
|
if console_visible
|
|
460
647
|
else document.body.clientWidth - 40,
|
|
461
|
-
document.body.clientHeight - 280
|
|
648
|
+
document.body.clientHeight - 280
|
|
649
|
+
if console_visible
|
|
650
|
+
else document.body.clientHeight - 160,
|
|
462
651
|
self.configure_extra_width(),
|
|
463
652
|
self.configure_extra_height(),
|
|
464
653
|
)
|
|
@@ -472,11 +661,52 @@ class CodeBattles(
|
|
|
472
661
|
document.getElementById("loader").style.display = "none"
|
|
473
662
|
self.render()
|
|
474
663
|
|
|
664
|
+
self._worker = await workers["worker"]
|
|
665
|
+
self._worker.update_step = self._update_step
|
|
666
|
+
self._worker._run_webworker_simulation(
|
|
667
|
+
map, json.dumps(player_names), json.dumps(player_codes)
|
|
668
|
+
)
|
|
669
|
+
|
|
475
670
|
if self.background:
|
|
476
671
|
await self._play_pause()
|
|
477
672
|
except Exception:
|
|
478
673
|
traceback.print_exc()
|
|
479
674
|
|
|
675
|
+
def _update_step(self, decisions_str: str, logs_str: str, is_over_str: str):
|
|
676
|
+
from js import window, document
|
|
677
|
+
|
|
678
|
+
now = time.time()
|
|
679
|
+
decisions = base64.b64decode(str(decisions_str))
|
|
680
|
+
logs: list = json.loads(str(logs_str))
|
|
681
|
+
is_over = str(is_over_str) == "true"
|
|
682
|
+
|
|
683
|
+
self._decisions.append(decisions)
|
|
684
|
+
self._logs.append(logs)
|
|
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)
|
|
701
|
+
|
|
702
|
+
render_status = document.getElementById("render-status")
|
|
703
|
+
if render_status is not None:
|
|
704
|
+
render_status.textContent = (
|
|
705
|
+
f"Rendering: Complete! ({int(now - self._start_time)}s)"
|
|
706
|
+
if is_over
|
|
707
|
+
else f"Rendering: Frame {len(self._decisions)} ({int(now - self._start_time)}s)"
|
|
708
|
+
)
|
|
709
|
+
|
|
480
710
|
def _get_initial_player_globals(self, player_codes: List[str]):
|
|
481
711
|
contexts = [
|
|
482
712
|
self.create_api_implementation(i) for i in range(len(self.player_names))
|
|
@@ -542,7 +772,10 @@ class CodeBattles(
|
|
|
542
772
|
|
|
543
773
|
return player_globals
|
|
544
774
|
|
|
775
|
+
@web_only
|
|
545
776
|
def _resize_canvas(self):
|
|
777
|
+
from js import document
|
|
778
|
+
|
|
546
779
|
if not hasattr(self, "canvas"):
|
|
547
780
|
return
|
|
548
781
|
|
|
@@ -550,14 +783,33 @@ class CodeBattles(
|
|
|
550
783
|
document.body.clientWidth - 440
|
|
551
784
|
if self.console_visible
|
|
552
785
|
else document.body.clientWidth - 40,
|
|
553
|
-
document.body.clientHeight - 280
|
|
786
|
+
document.body.clientHeight - 280
|
|
787
|
+
if self.console_visible
|
|
788
|
+
else document.body.clientHeight - 160,
|
|
554
789
|
)
|
|
555
790
|
if not self.background:
|
|
556
791
|
self.render()
|
|
557
792
|
|
|
793
|
+
@web_only
|
|
558
794
|
def _step(self):
|
|
795
|
+
from js import document, setTimeout
|
|
796
|
+
from pyscript.ffi import create_proxy
|
|
797
|
+
|
|
559
798
|
if not self.over:
|
|
560
|
-
self.
|
|
799
|
+
if len(self._decisions) == self._decision_index:
|
|
800
|
+
print("Warning: sleeping because decisions were not made yet!")
|
|
801
|
+
setTimeout(create_proxy(self._step), 100)
|
|
802
|
+
return
|
|
803
|
+
else:
|
|
804
|
+
logs = self._logs[self._decision_index]
|
|
805
|
+
for log in logs:
|
|
806
|
+
console_log(
|
|
807
|
+
-1 if log["player_index"] is None else log["player_index"],
|
|
808
|
+
log["text"],
|
|
809
|
+
log["color"],
|
|
810
|
+
)
|
|
811
|
+
self.apply_decisions(self._decisions[self._decision_index])
|
|
812
|
+
self._decision_index += 1
|
|
561
813
|
|
|
562
814
|
if not self.over:
|
|
563
815
|
self.step += 1
|
|
@@ -568,9 +820,17 @@ class CodeBattles(
|
|
|
568
820
|
set_results(
|
|
569
821
|
self.player_names, self._eliminated[::-1], self.map, self.verbose
|
|
570
822
|
)
|
|
823
|
+
self.render()
|
|
571
824
|
|
|
572
825
|
if not self.background:
|
|
573
|
-
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
|
+
|
|
574
834
|
if (
|
|
575
835
|
self.over
|
|
576
836
|
and "Pause" in document.getElementById("playpause").textContent
|
|
@@ -585,7 +845,10 @@ class CodeBattles(
|
|
|
585
845
|
if self.over:
|
|
586
846
|
document.getElementById("noui-progress").style.display = "none"
|
|
587
847
|
|
|
848
|
+
@web_only
|
|
588
849
|
def _should_play(self):
|
|
850
|
+
from js import document
|
|
851
|
+
|
|
589
852
|
if self.over:
|
|
590
853
|
return False
|
|
591
854
|
|
|
@@ -600,7 +863,10 @@ class CodeBattles(
|
|
|
600
863
|
|
|
601
864
|
return True
|
|
602
865
|
|
|
866
|
+
@web_only
|
|
603
867
|
def _get_playback_speed(self):
|
|
868
|
+
from js import document
|
|
869
|
+
|
|
604
870
|
return 2 ** float(
|
|
605
871
|
document.getElementById("timescale")
|
|
606
872
|
.getElementsByClassName("mantine-Slider-thumb")
|
|
@@ -608,7 +874,10 @@ class CodeBattles(
|
|
|
608
874
|
.ariaValueNow
|
|
609
875
|
)
|
|
610
876
|
|
|
877
|
+
@web_only
|
|
611
878
|
def _get_breakpoint(self):
|
|
879
|
+
from js import document
|
|
880
|
+
|
|
612
881
|
breakpoint_element = document.getElementById("breakpoint")
|
|
613
882
|
if breakpoint_element is None or breakpoint_element.value == "":
|
|
614
883
|
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
|
-
window.showAlert(title, alert, color, icon, limit_time, is_code)
|
|
30
|
-
except Exception as e:
|
|
31
|
-
print(e)
|
|
59
|
+
if is_web():
|
|
60
|
+
from js import window
|
|
32
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}")
|
|
33
69
|
|
|
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,32 @@ 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
|
|
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
|
+
|
|
104
|
+
@web_only
|
|
42
105
|
def download_json(filename: str, contents: str):
|
|
106
|
+
from js import window
|
|
107
|
+
|
|
43
108
|
if hasattr(window, "downloadJson"):
|
|
44
109
|
try:
|
|
45
110
|
window.downloadJson(filename, contents)
|
|
@@ -47,7 +112,10 @@ def download_json(filename: str, contents: str):
|
|
|
47
112
|
print(e)
|
|
48
113
|
|
|
49
114
|
|
|
115
|
+
@web_only
|
|
50
116
|
def console_log(player_index: int, text: str, color: str):
|
|
117
|
+
from js import window
|
|
118
|
+
|
|
51
119
|
if hasattr(window, "consoleLog"):
|
|
52
120
|
try:
|
|
53
121
|
window.consoleLog(player_index, text, color)
|
|
@@ -71,9 +139,9 @@ class GameCanvas:
|
|
|
71
139
|
|
|
72
140
|
def __init__(
|
|
73
141
|
self,
|
|
74
|
-
canvas: Element,
|
|
142
|
+
canvas: "js.Element",
|
|
75
143
|
player_count: int,
|
|
76
|
-
map_image: Image,
|
|
144
|
+
map_image: "js.Image",
|
|
77
145
|
max_width: int,
|
|
78
146
|
max_height: int,
|
|
79
147
|
extra_width: int,
|
|
@@ -89,7 +157,7 @@ class GameCanvas:
|
|
|
89
157
|
|
|
90
158
|
def draw_element(
|
|
91
159
|
self,
|
|
92
|
-
image: Image,
|
|
160
|
+
image: "js.Image",
|
|
93
161
|
x: int,
|
|
94
162
|
y: int,
|
|
95
163
|
width: int,
|
|
@@ -143,6 +211,26 @@ class GameCanvas:
|
|
|
143
211
|
self.context.fillStyle = color
|
|
144
212
|
self.context.fillText(text, x, y)
|
|
145
213
|
|
|
214
|
+
def draw_line(
|
|
215
|
+
self,
|
|
216
|
+
start_x: int,
|
|
217
|
+
start_y: int,
|
|
218
|
+
end_x: int,
|
|
219
|
+
end_y: int,
|
|
220
|
+
stroke="black",
|
|
221
|
+
stroke_width=10,
|
|
222
|
+
board_index=0,
|
|
223
|
+
):
|
|
224
|
+
start_x, start_y = self._translate_position(board_index, start_x, start_y)
|
|
225
|
+
end_x, end_y = self._translate_position(board_index, end_x, end_y)
|
|
226
|
+
|
|
227
|
+
self.context.strokeStyle = stroke
|
|
228
|
+
self.context.lineWidth = stroke_width * self._scale
|
|
229
|
+
self.context.beginPath()
|
|
230
|
+
self.context.moveTo(start_x, start_y)
|
|
231
|
+
self.context.lineTo(end_x, end_y)
|
|
232
|
+
self.context.stroke()
|
|
233
|
+
|
|
146
234
|
def draw_circle(
|
|
147
235
|
self,
|
|
148
236
|
x: int,
|
|
@@ -150,6 +238,7 @@ class GameCanvas:
|
|
|
150
238
|
radius: float,
|
|
151
239
|
fill="black",
|
|
152
240
|
stroke="transparent",
|
|
241
|
+
stroke_width=2,
|
|
153
242
|
board_index=0,
|
|
154
243
|
):
|
|
155
244
|
"""
|
|
@@ -157,8 +246,10 @@ class GameCanvas:
|
|
|
157
246
|
"""
|
|
158
247
|
|
|
159
248
|
x, y = self._translate_position(board_index, x, y)
|
|
249
|
+
|
|
160
250
|
self.context.fillStyle = fill
|
|
161
251
|
self.context.strokeStyle = stroke
|
|
252
|
+
self.context.lineWidth = stroke_width * self._scale
|
|
162
253
|
self.context.beginPath()
|
|
163
254
|
self.context.arc(x, y, radius * self._scale, 0, 2 * math.pi)
|
|
164
255
|
self.context.stroke()
|
|
@@ -187,6 +278,8 @@ class GameCanvas:
|
|
|
187
278
|
return self.map_image.width * self.player_count
|
|
188
279
|
|
|
189
280
|
def _fit_into(self, max_width: int, max_height: int):
|
|
281
|
+
from js import window
|
|
282
|
+
|
|
190
283
|
if self.map_image.width == 0 or self.map_image.height == 0:
|
|
191
284
|
raise Exception("Map image invalid!")
|
|
192
285
|
aspect_ratio = (self.map_image.width * self.player_count + self.extra_width) / (
|
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
|
}
|
|
@@ -16018,9 +16064,14 @@ const Simulation = () => {
|
|
|
16018
16064
|
!location.search.includes("background=true") && (React.createElement(React.Fragment, null,
|
|
16019
16065
|
React.createElement("div", { style: { textAlign: "center", flexGrow: 1 } },
|
|
16020
16066
|
showcaseMode || (React.createElement(React.Fragment, null,
|
|
16067
|
+
React.createElement("span", { id: "render-status", style: { marginRight: 10 } }),
|
|
16021
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" }),
|
|
16022
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"))),
|
|
16023
|
-
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" }),
|
|
16024
16075
|
React.createElement("p", { style: { margin: 0 } }, "Playback Speed"),
|
|
16025
16076
|
React.createElement(Slider, { style: { flex: "none" }, mb: 30, w: 500, maw: "85%", min: -2, defaultValue: 0, marks: [
|
|
16026
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"
|
|
@@ -52,16 +53,16 @@
|
|
|
52
53
|
"@rollup/plugin-commonjs": "^26.0.3",
|
|
53
54
|
"@rollup/plugin-node-resolve": "^15.3.0",
|
|
54
55
|
"@rollup/plugin-typescript": "^11.1.6",
|
|
55
|
-
"@types/prismjs": "^1.26.
|
|
56
|
-
"@types/react": "^18.3.
|
|
56
|
+
"@types/prismjs": "^1.26.5",
|
|
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
|
-
"rollup-plugin-import-css": "^3.5.
|
|
63
|
+
"rollup-plugin-import-css": "^3.5.6",
|
|
63
64
|
"rollup-plugin-peer-deps-external": "^2.2.4",
|
|
64
|
-
"tslib": "^2.8.
|
|
65
|
+
"tslib": "^2.8.1",
|
|
65
66
|
"typescript": "^5.6.3"
|
|
66
67
|
},
|
|
67
68
|
"prettier": {
|