code-battles-cli 1.1.1__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.1 → code_battles_cli-1.2.0}/PKG-INFO +2 -2
- {code_battles_cli-1.1.1 → code_battles_cli-1.2.0}/README.md +1 -1
- {code_battles_cli-1.1.1 → code_battles_cli-1.2.0}/pyproject.toml +2 -2
- {code_battles_cli-1.1.1 → code_battles_cli-1.2.0}/requirements-dev.lock +7 -0
- {code_battles_cli-1.1.1 → code_battles_cli-1.2.0}/src/code_battles_cli/api.py +52 -44
- {code_battles_cli-1.1.1 → 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.1 → code_battles_cli-1.2.0}/.gitignore +0 -0
- {code_battles_cli-1.1.1 → code_battles_cli-1.2.0}/.python-version +0 -0
- {code_battles_cli-1.1.1 → code_battles_cli-1.2.0}/LICENSE.md +0 -0
- {code_battles_cli-1.1.1 → code_battles_cli-1.2.0}/requirements.lock +0 -0
- {code_battles_cli-1.1.1 → code_battles_cli-1.2.0}/src/code_battles_cli/__init__.py +0 -0
- {code_battles_cli-1.1.1 → code_battles_cli-1.2.0}/src/code_battles_cli/__main__.py +0 -0
- {code_battles_cli-1.1.1 → code_battles_cli-1.2.0}/src/code_battles_cli/cli.py +0 -0
- {code_battles_cli-1.1.1 → code_battles_cli-1.2.0}/src/code_battles_cli/commands/__init__.py +0 -0
- {code_battles_cli-1.1.1 → code_battles_cli-1.2.0}/src/code_battles_cli/commands/download.py +0 -0
- {code_battles_cli-1.1.1 → code_battles_cli-1.2.0}/src/code_battles_cli/commands/upload.py +0 -0
- {code_battles_cli-1.1.1 → 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:
|
|
@@ -197,9 +212,14 @@ class Client:
|
|
|
197
212
|
self.document.set(bots, merge)
|
|
198
213
|
|
|
199
214
|
def _possibly_download(self, force_download=False):
|
|
200
|
-
|
|
201
|
-
|
|
215
|
+
directory_name = "".join(
|
|
216
|
+
[
|
|
217
|
+
c
|
|
218
|
+
for c in self.url.removeprefix("https").removesuffix(".web.app")
|
|
219
|
+
if c.isalnum() or c == "-"
|
|
220
|
+
]
|
|
202
221
|
)
|
|
222
|
+
code_directory = os.path.expanduser(f"~/.cache/code-battles/{directory_name}")
|
|
203
223
|
|
|
204
224
|
if force_download and os.path.exists(code_directory):
|
|
205
225
|
shutil.rmtree(code_directory)
|
|
@@ -207,31 +227,11 @@ class Client:
|
|
|
207
227
|
if not os.path.exists(code_directory):
|
|
208
228
|
os.makedirs(code_directory)
|
|
209
229
|
|
|
210
|
-
with console.status("[blue]Fetching
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
)
|
|
215
|
-
|
|
216
|
-
with progress:
|
|
217
|
-
progress_id = progress.add_task(
|
|
218
|
-
"[blue]Fetching game files...",
|
|
219
|
-
total=len(pyscript_configuration["files"]),
|
|
220
|
-
)
|
|
221
|
-
|
|
222
|
-
for url_path, path in pyscript_configuration["files"].items():
|
|
223
|
-
if not path.endswith(".py"):
|
|
224
|
-
continue
|
|
225
|
-
|
|
226
|
-
local_path = os.path.join(code_directory, *path.split("/"))
|
|
227
|
-
os.makedirs(os.path.dirname(local_path), exist_ok=True)
|
|
228
|
-
text = requests.get(self.url + url_path).text
|
|
229
|
-
with open(local_path, "w") as f:
|
|
230
|
-
f.write(text)
|
|
231
|
-
progress.update(progress_id, advance=1)
|
|
232
|
-
progress.update(progress_id, visible=False)
|
|
233
|
-
|
|
234
|
-
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.")
|
|
235
235
|
|
|
236
236
|
return code_directory
|
|
237
237
|
|
|
@@ -257,7 +257,7 @@ class Client:
|
|
|
257
257
|
return output.decode()
|
|
258
258
|
|
|
259
259
|
output = json.loads(output)
|
|
260
|
-
|
|
260
|
+
result = SimulationResults(
|
|
261
261
|
output["winner_index"],
|
|
262
262
|
output["winner"],
|
|
263
263
|
output["steps"],
|
|
@@ -269,9 +269,16 @@ class Client:
|
|
|
269
269
|
],
|
|
270
270
|
)
|
|
271
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
|
+
|
|
272
279
|
def run_simulation(
|
|
273
280
|
self,
|
|
274
|
-
|
|
281
|
+
parameters: Dict[str, str],
|
|
275
282
|
bot_filenames: List[str],
|
|
276
283
|
bot_names: Optional[List[str]] = None,
|
|
277
284
|
seed: Optional[int] = None,
|
|
@@ -300,16 +307,17 @@ class Client:
|
|
|
300
307
|
p = subprocess.Popen(
|
|
301
308
|
[
|
|
302
309
|
sys.executable,
|
|
303
|
-
os.path.join(code_directory, "
|
|
310
|
+
os.path.join(code_directory, "packed.py"),
|
|
304
311
|
"simulate",
|
|
305
312
|
str(seed),
|
|
306
|
-
str(output_file),
|
|
307
|
-
|
|
313
|
+
str(os.path.abspath(output_file)),
|
|
314
|
+
json.dumps(parameters),
|
|
308
315
|
"-".join(bot_names),
|
|
309
316
|
]
|
|
310
317
|
+ [os.path.abspath(f) for f in bot_filenames],
|
|
311
318
|
env={"PYTHONPATH": os.path.join(code_directory, "code_battles")},
|
|
312
319
|
stdout=subprocess.PIPE,
|
|
320
|
+
stderr=subprocess.PIPE,
|
|
313
321
|
)
|
|
314
322
|
|
|
315
323
|
return self._get_simulation_output(p, json_output, on_step)
|
|
@@ -334,7 +342,7 @@ class Client:
|
|
|
334
342
|
p = subprocess.Popen(
|
|
335
343
|
[
|
|
336
344
|
sys.executable,
|
|
337
|
-
os.path.join(code_directory, "
|
|
345
|
+
os.path.join(code_directory, "packed.py"),
|
|
338
346
|
"simulate-from-file",
|
|
339
347
|
simulation_file,
|
|
340
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
|