code-battles-cli 1.1.2__tar.gz → 1.2.0__tar.gz
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.
- code_battles_cli-1.2.0/.github/workflows/test.yml +21 -0
- {code_battles_cli-1.1.2 → code_battles_cli-1.2.0}/PKG-INFO +2 -2
- {code_battles_cli-1.1.2 → code_battles_cli-1.2.0}/README.md +1 -1
- {code_battles_cli-1.1.2 → code_battles_cli-1.2.0}/pyproject.toml +2 -2
- {code_battles_cli-1.1.2 → code_battles_cli-1.2.0}/requirements-dev.lock +7 -0
- {code_battles_cli-1.1.2 → code_battles_cli-1.2.0}/src/code_battles_cli/api.py +46 -43
- {code_battles_cli-1.1.2 → code_battles_cli-1.2.0}/src/code_battles_cli/commands/run.py +19 -18
- code_battles_cli-1.2.0/tests/test_cli.py +2 -0
- {code_battles_cli-1.1.2 → code_battles_cli-1.2.0}/.gitignore +0 -0
- {code_battles_cli-1.1.2 → code_battles_cli-1.2.0}/.python-version +0 -0
- {code_battles_cli-1.1.2 → code_battles_cli-1.2.0}/LICENSE.md +0 -0
- {code_battles_cli-1.1.2 → code_battles_cli-1.2.0}/requirements.lock +0 -0
- {code_battles_cli-1.1.2 → code_battles_cli-1.2.0}/src/code_battles_cli/__init__.py +0 -0
- {code_battles_cli-1.1.2 → code_battles_cli-1.2.0}/src/code_battles_cli/__main__.py +0 -0
- {code_battles_cli-1.1.2 → code_battles_cli-1.2.0}/src/code_battles_cli/cli.py +0 -0
- {code_battles_cli-1.1.2 → code_battles_cli-1.2.0}/src/code_battles_cli/commands/__init__.py +0 -0
- {code_battles_cli-1.1.2 → code_battles_cli-1.2.0}/src/code_battles_cli/commands/download.py +0 -0
- {code_battles_cli-1.1.2 → code_battles_cli-1.2.0}/src/code_battles_cli/commands/upload.py +0 -0
- {code_battles_cli-1.1.2 → code_battles_cli-1.2.0}/src/code_battles_cli/log.py +0 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: Test
|
|
2
|
+
on: [push]
|
|
3
|
+
jobs:
|
|
4
|
+
test:
|
|
5
|
+
name: Test
|
|
6
|
+
strategy:
|
|
7
|
+
matrix:
|
|
8
|
+
version: ["3.8", "3.12"]
|
|
9
|
+
os: [ubuntu-latest, windows-latest, macos-latest]
|
|
10
|
+
runs-on: ${{ matrix.os }}
|
|
11
|
+
steps:
|
|
12
|
+
- name: Clone the repository
|
|
13
|
+
uses: actions/checkout@v4
|
|
14
|
+
- name: Install the latest version of rye
|
|
15
|
+
uses: eifinger/setup-rye@v4
|
|
16
|
+
- name: Pin Python version
|
|
17
|
+
run: rye pin ${{ matrix.version }}
|
|
18
|
+
- name: Sync rye
|
|
19
|
+
run: rye sync
|
|
20
|
+
- name: Run tests
|
|
21
|
+
run: rye test
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: code-battles-cli
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.2.0
|
|
4
4
|
Summary: A CLI for competitors in any Code Battles event!
|
|
5
5
|
Requires-Python: >=3.8
|
|
6
6
|
Requires-Dist: click>=8.1.7
|
|
@@ -53,7 +53,7 @@ If you want to choose a custom bot name, you can run `code-battles upload bots/e
|
|
|
53
53
|
|
|
54
54
|
## Running a simulation
|
|
55
55
|
|
|
56
|
-
You can run a simulation by running `code-battles run NYC bots/example.py bots/another_example.py`, where `NYC` is an example of a map.
|
|
56
|
+
You can run a simulation by running `code-battles run '{"map": "NYC"}' bots/example.py bots/another_example.py`, where `NYC` is an example of a map.
|
|
57
57
|
|
|
58
58
|
You will probably find the scripting API nicer to work with if you want to run simulations locally.
|
|
59
59
|
|
|
@@ -42,7 +42,7 @@ If you want to choose a custom bot name, you can run `code-battles upload bots/e
|
|
|
42
42
|
|
|
43
43
|
## Running a simulation
|
|
44
44
|
|
|
45
|
-
You can run a simulation by running `code-battles run NYC bots/example.py bots/another_example.py`, where `NYC` is an example of a map.
|
|
45
|
+
You can run a simulation by running `code-battles run '{"map": "NYC"}' bots/example.py bots/another_example.py`, where `NYC` is an example of a map.
|
|
46
46
|
|
|
47
47
|
You will probably find the scripting API nicer to work with if you want to run simulations locally.
|
|
48
48
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "code-battles-cli"
|
|
3
|
-
version = "1.
|
|
3
|
+
version = "1.2.0"
|
|
4
4
|
description = "A CLI for competitors in any Code Battles event!"
|
|
5
5
|
dependencies = [
|
|
6
6
|
"requests>=2.32.3",
|
|
@@ -20,7 +20,7 @@ build-backend = "hatchling.build"
|
|
|
20
20
|
|
|
21
21
|
[tool.rye]
|
|
22
22
|
managed = true
|
|
23
|
-
dev-dependencies = []
|
|
23
|
+
dev-dependencies = ["pytest>=8.3.5"]
|
|
24
24
|
|
|
25
25
|
[tool.hatch.metadata]
|
|
26
26
|
allow-direct-references = true
|
|
@@ -39,10 +39,16 @@ grpcio-status==1.67.0
|
|
|
39
39
|
# via google-api-core
|
|
40
40
|
idna==3.10
|
|
41
41
|
# via requests
|
|
42
|
+
iniconfig==2.1.0
|
|
43
|
+
# via pytest
|
|
42
44
|
markdown-it-py==3.0.0
|
|
43
45
|
# via rich
|
|
44
46
|
mdurl==0.1.2
|
|
45
47
|
# via markdown-it-py
|
|
48
|
+
packaging==25.0
|
|
49
|
+
# via pytest
|
|
50
|
+
pluggy==1.5.0
|
|
51
|
+
# via pytest
|
|
46
52
|
proto-plus==1.24.0
|
|
47
53
|
# via google-api-core
|
|
48
54
|
# via google-cloud-firestore
|
|
@@ -59,6 +65,7 @@ pyasn1-modules==0.4.1
|
|
|
59
65
|
# via google-auth
|
|
60
66
|
pygments==2.18.0
|
|
61
67
|
# via rich
|
|
68
|
+
pytest==8.3.5
|
|
62
69
|
requests==2.32.3
|
|
63
70
|
# via code-battles-cli
|
|
64
71
|
# via google-api-core
|
|
@@ -6,27 +6,40 @@ Firestore client implementation inspired by https://medium.com/@bobthomas295/cli
|
|
|
6
6
|
|
|
7
7
|
import base64
|
|
8
8
|
import datetime
|
|
9
|
-
import logging
|
|
10
9
|
import gzip
|
|
11
|
-
import os
|
|
12
10
|
import json
|
|
11
|
+
import logging
|
|
12
|
+
import os
|
|
13
13
|
import shutil
|
|
14
14
|
import subprocess
|
|
15
15
|
import sys
|
|
16
|
-
from typing import Any, Callable, Dict, List, Optional, Union
|
|
17
16
|
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any, Callable, Dict, List, Optional, Union
|
|
18
19
|
|
|
19
20
|
import requests
|
|
20
|
-
from google.oauth2.credentials import Credentials
|
|
21
21
|
from google.cloud.firestore import Client as FirestoreClient
|
|
22
|
+
from google.oauth2.credentials import Credentials
|
|
22
23
|
from rich.prompt import Prompt
|
|
23
|
-
from code_battles_cli.log import console, log, progress
|
|
24
24
|
|
|
25
|
+
from code_battles_cli.log import console, log
|
|
25
26
|
|
|
26
27
|
SIMULATION_FINISHED_MARK = b"--- SIMULATION FINISHED ---"
|
|
27
28
|
SIMULATION_STEP_MARK = b"__CODE_BATTLES_ADVANCE_STEP"
|
|
28
29
|
|
|
29
30
|
|
|
31
|
+
class SimulationException(Exception):
|
|
32
|
+
def __init__(self, stderr: bytes, exit_code: int):
|
|
33
|
+
self.stderr = stderr
|
|
34
|
+
self.exit_code = exit_code
|
|
35
|
+
|
|
36
|
+
def __str__(self):
|
|
37
|
+
return (
|
|
38
|
+
f"Simulation failed with exit code {self.exit_code}. Output:\n"
|
|
39
|
+
+ self.stderr.decode()
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
30
43
|
@dataclass
|
|
31
44
|
class LogEntry:
|
|
32
45
|
step: int
|
|
@@ -45,12 +58,13 @@ class SimulationResults:
|
|
|
45
58
|
|
|
46
59
|
@dataclass
|
|
47
60
|
class Simulation:
|
|
48
|
-
|
|
61
|
+
parameters: Dict[str, str]
|
|
49
62
|
player_names: str
|
|
50
63
|
game: str
|
|
51
64
|
version: str
|
|
52
65
|
timestamp: datetime.datetime
|
|
53
66
|
logs: list
|
|
67
|
+
alerts: list
|
|
54
68
|
decisions: List[bytes]
|
|
55
69
|
seed: int
|
|
56
70
|
|
|
@@ -59,12 +73,13 @@ class Simulation:
|
|
|
59
73
|
gzip.compress(
|
|
60
74
|
json.dumps(
|
|
61
75
|
{
|
|
62
|
-
"
|
|
76
|
+
"parameters": self.parameters,
|
|
63
77
|
"playerNames": self.player_names,
|
|
64
78
|
"game": self.game,
|
|
65
79
|
"version": self.version,
|
|
66
80
|
"timestamp": self.timestamp.isoformat(),
|
|
67
81
|
"logs": self.logs,
|
|
82
|
+
"alerts": self.alerts,
|
|
68
83
|
"decisions": [
|
|
69
84
|
base64.b64encode(decision).decode()
|
|
70
85
|
for decision in self.decisions
|
|
@@ -79,12 +94,15 @@ class Simulation:
|
|
|
79
94
|
def load(file: str):
|
|
80
95
|
contents: Dict[str, Any] = json.loads(gzip.decompress(base64.b64decode(file)))
|
|
81
96
|
return Simulation(
|
|
82
|
-
contents["
|
|
97
|
+
contents["parameters"]
|
|
98
|
+
if "parameters" in contents
|
|
99
|
+
else {"map": contents["map"]},
|
|
83
100
|
contents["playerNames"],
|
|
84
101
|
contents["game"],
|
|
85
102
|
contents["version"],
|
|
86
103
|
datetime.datetime.fromisoformat(contents["timestamp"]),
|
|
87
104
|
contents["logs"],
|
|
105
|
+
contents["alerts"],
|
|
88
106
|
[base64.b64decode(decision) for decision in contents["decisions"]],
|
|
89
107
|
contents["seed"],
|
|
90
108
|
)
|
|
@@ -162,9 +180,6 @@ class Client:
|
|
|
162
180
|
configuration = requests.get(self.url + "/firebase-configuration.json").json()
|
|
163
181
|
self.firebase_api_key: str = configuration["apiKey"]
|
|
164
182
|
self.firebase_project_id: str = configuration["projectId"]
|
|
165
|
-
# print(
|
|
166
|
-
# f"Got API Key: '{self.firebase_api_key}' and Project ID: '{self.firebase_project_id}'"
|
|
167
|
-
# )
|
|
168
183
|
|
|
169
184
|
def _sign_in(self, email_domain="gmail.com"):
|
|
170
185
|
try:
|
|
@@ -201,7 +216,7 @@ class Client:
|
|
|
201
216
|
[
|
|
202
217
|
c
|
|
203
218
|
for c in self.url.removeprefix("https").removesuffix(".web.app")
|
|
204
|
-
if c.isalnum()
|
|
219
|
+
if c.isalnum() or c == "-"
|
|
205
220
|
]
|
|
206
221
|
)
|
|
207
222
|
code_directory = os.path.expanduser(f"~/.cache/code-battles/{directory_name}")
|
|
@@ -212,31 +227,11 @@ class Client:
|
|
|
212
227
|
if not os.path.exists(code_directory):
|
|
213
228
|
os.makedirs(code_directory)
|
|
214
229
|
|
|
215
|
-
with console.status("[blue]Fetching
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
)
|
|
220
|
-
|
|
221
|
-
with progress:
|
|
222
|
-
progress_id = progress.add_task(
|
|
223
|
-
"[blue]Fetching game files...",
|
|
224
|
-
total=len(pyscript_configuration["files"]),
|
|
225
|
-
)
|
|
226
|
-
|
|
227
|
-
for url_path, path in pyscript_configuration["files"].items():
|
|
228
|
-
if not path.endswith(".py"):
|
|
229
|
-
continue
|
|
230
|
-
|
|
231
|
-
local_path = os.path.join(code_directory, *path.split("/"))
|
|
232
|
-
os.makedirs(os.path.dirname(local_path), exist_ok=True)
|
|
233
|
-
text = requests.get(self.url + url_path).text
|
|
234
|
-
with open(local_path, "w") as f:
|
|
235
|
-
f.write(text)
|
|
236
|
-
progress.update(progress_id, advance=1)
|
|
237
|
-
progress.update(progress_id, visible=False)
|
|
238
|
-
|
|
239
|
-
logging.info("Finished fetching game files.")
|
|
230
|
+
with console.status("[blue]Fetching packed Python file..."):
|
|
231
|
+
packed_file = requests.get(self.url + "/scripts/packed.py").text
|
|
232
|
+
local_path = Path(code_directory) / "packed.py"
|
|
233
|
+
local_path.write_text(packed_file)
|
|
234
|
+
logging.info("Fetched packed Python file.")
|
|
240
235
|
|
|
241
236
|
return code_directory
|
|
242
237
|
|
|
@@ -262,7 +257,7 @@ class Client:
|
|
|
262
257
|
return output.decode()
|
|
263
258
|
|
|
264
259
|
output = json.loads(output)
|
|
265
|
-
|
|
260
|
+
result = SimulationResults(
|
|
266
261
|
output["winner_index"],
|
|
267
262
|
output["winner"],
|
|
268
263
|
output["steps"],
|
|
@@ -274,9 +269,16 @@ class Client:
|
|
|
274
269
|
],
|
|
275
270
|
)
|
|
276
271
|
|
|
272
|
+
error: bytes = p.stderr.read()
|
|
273
|
+
exit_code = p.wait()
|
|
274
|
+
if exit_code != 0:
|
|
275
|
+
raise SimulationException(error, exit_code)
|
|
276
|
+
|
|
277
|
+
return result
|
|
278
|
+
|
|
277
279
|
def run_simulation(
|
|
278
280
|
self,
|
|
279
|
-
|
|
281
|
+
parameters: Dict[str, str],
|
|
280
282
|
bot_filenames: List[str],
|
|
281
283
|
bot_names: Optional[List[str]] = None,
|
|
282
284
|
seed: Optional[int] = None,
|
|
@@ -305,16 +307,17 @@ class Client:
|
|
|
305
307
|
p = subprocess.Popen(
|
|
306
308
|
[
|
|
307
309
|
sys.executable,
|
|
308
|
-
os.path.join(code_directory, "
|
|
310
|
+
os.path.join(code_directory, "packed.py"),
|
|
309
311
|
"simulate",
|
|
310
312
|
str(seed),
|
|
311
|
-
str(output_file),
|
|
312
|
-
|
|
313
|
+
str(os.path.abspath(output_file)),
|
|
314
|
+
json.dumps(parameters),
|
|
313
315
|
"-".join(bot_names),
|
|
314
316
|
]
|
|
315
317
|
+ [os.path.abspath(f) for f in bot_filenames],
|
|
316
318
|
env={"PYTHONPATH": os.path.join(code_directory, "code_battles")},
|
|
317
319
|
stdout=subprocess.PIPE,
|
|
320
|
+
stderr=subprocess.PIPE,
|
|
318
321
|
)
|
|
319
322
|
|
|
320
323
|
return self._get_simulation_output(p, json_output, on_step)
|
|
@@ -339,7 +342,7 @@ class Client:
|
|
|
339
342
|
p = subprocess.Popen(
|
|
340
343
|
[
|
|
341
344
|
sys.executable,
|
|
342
|
-
os.path.join(code_directory, "
|
|
345
|
+
os.path.join(code_directory, "packed.py"),
|
|
343
346
|
"simulate-from-file",
|
|
344
347
|
simulation_file,
|
|
345
348
|
],
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import json
|
|
2
2
|
import logging
|
|
3
3
|
import sys
|
|
4
|
+
import time
|
|
4
5
|
from typing import List, Optional, Tuple
|
|
5
6
|
|
|
6
7
|
import click
|
|
@@ -10,7 +11,7 @@ from code_battles_cli.log import log, progress
|
|
|
10
11
|
|
|
11
12
|
|
|
12
13
|
@click.command()
|
|
13
|
-
@click.argument("
|
|
14
|
+
@click.argument("parameters", required=False)
|
|
14
15
|
@click.argument("bots", nargs=-1)
|
|
15
16
|
@click.option(
|
|
16
17
|
"-U",
|
|
@@ -53,7 +54,7 @@ def run(
|
|
|
53
54
|
username: Optional[str],
|
|
54
55
|
password: Optional[str],
|
|
55
56
|
dump_credentials: bool,
|
|
56
|
-
|
|
57
|
+
parameters: Optional[str],
|
|
57
58
|
bots: Tuple[str],
|
|
58
59
|
force_download: bool,
|
|
59
60
|
seed: Optional[int],
|
|
@@ -72,18 +73,18 @@ def run(
|
|
|
72
73
|
progress.update(progress_id, advance=1)
|
|
73
74
|
|
|
74
75
|
if simulation_file is not None:
|
|
75
|
-
assert (
|
|
76
|
-
map is
|
|
77
|
-
)
|
|
78
|
-
assert (
|
|
79
|
-
|
|
80
|
-
)
|
|
81
|
-
assert (
|
|
82
|
-
seed is
|
|
83
|
-
)
|
|
84
|
-
assert (
|
|
85
|
-
|
|
86
|
-
)
|
|
76
|
+
assert parameters is None, (
|
|
77
|
+
"The simulation's map is given from the simulation file!"
|
|
78
|
+
)
|
|
79
|
+
assert len(bots) == 0, (
|
|
80
|
+
"The simulation's bots are given from the simulation file!"
|
|
81
|
+
)
|
|
82
|
+
assert seed is None, (
|
|
83
|
+
"The simulation's seed is given from the simulation file!"
|
|
84
|
+
)
|
|
85
|
+
assert output_file is None, (
|
|
86
|
+
"Shouldn't dump simualtion file from existing simulation file!"
|
|
87
|
+
)
|
|
87
88
|
|
|
88
89
|
with open(simulation_file, "r") as f:
|
|
89
90
|
simulation_text = f.read()
|
|
@@ -92,7 +93,7 @@ def run(
|
|
|
92
93
|
f"Detected simulation file of {simulation.game} {simulation.version}"
|
|
93
94
|
)
|
|
94
95
|
logging.info(
|
|
95
|
-
f"Simulation happened in {simulation.map} between {', '.join(simulation.player_names)} with seed {simulation.seed}"
|
|
96
|
+
f"Simulation happened in {simulation.parameters.get('map')} between {', '.join(simulation.player_names)} with seed {simulation.seed}"
|
|
96
97
|
)
|
|
97
98
|
logging.info(f"Simulation contains {len(simulation.decisions)} decisions")
|
|
98
99
|
progress.update(progress_id, total=len(simulation.decisions) - 1)
|
|
@@ -104,7 +105,7 @@ def run(
|
|
|
104
105
|
assert len(bots) > 0, "Bots are required for simulation!"
|
|
105
106
|
|
|
106
107
|
results = client.run_simulation(
|
|
107
|
-
|
|
108
|
+
json.loads(parameters),
|
|
108
109
|
list(bots),
|
|
109
110
|
seed=seed,
|
|
110
111
|
force_download=False,
|
|
@@ -113,4 +114,4 @@ def run(
|
|
|
113
114
|
)
|
|
114
115
|
print("The winner is", results.winner, "after", results.steps, "steps.")
|
|
115
116
|
end = time.time()
|
|
116
|
-
log.info(f"Simulation took {(end - start)
|
|
117
|
+
log.info(f"Simulation took {(end - start):.2f}s.")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|