code-battles-cli 1.1.1__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.
@@ -0,0 +1,13 @@
1
+ # python generated files
2
+ __pycache__/
3
+ *.py[oc]
4
+ build/
5
+ dist/
6
+ wheels/
7
+ *.egg-info
8
+
9
+ # venv
10
+ .venv
11
+
12
+ bots
13
+ code-battles.json
@@ -0,0 +1 @@
1
+ 3.12.2
@@ -0,0 +1,7 @@
1
+ Copyright 2024 Noam Zaks and Code Battles Contributors
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,80 @@
1
+ Metadata-Version: 2.3
2
+ Name: code-battles-cli
3
+ Version: 1.1.1
4
+ Summary: A CLI for competitors in any Code Battles event!
5
+ Requires-Python: >=3.8
6
+ Requires-Dist: click>=8.1.7
7
+ Requires-Dist: google-cloud-firestore>=2.19.0
8
+ Requires-Dist: requests>=2.32.3
9
+ Requires-Dist: rich>=13.9.3
10
+ Description-Content-Type: text/markdown
11
+
12
+ <p align="center">
13
+ <img src="https://github.com/noamzaks/code-battles/assets/63877260/b330aa14-7003-4204-8907-e77a5c6e8d81" height="140">
14
+ </p>
15
+ <h1 align="center">
16
+ Code Battles CLI
17
+ <br />
18
+ <img src="https://img.shields.io/pypi/v/code-battles-cli">
19
+ <img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg">
20
+ <img src="https://img.shields.io/badge/license-MIT-blue.svg">
21
+ </h1>
22
+
23
+ <p align="center">
24
+ A CLI for competitors in any Code Battles event!
25
+ </p>
26
+ <p align="center">
27
+ 🏃 <a href="#getting-started">Getting Started</a>
28
+ &nbsp;&middot&nbsp;
29
+ 💡 <a href="#features">Features</a>
30
+ &nbsp;&middot&nbsp;
31
+ 🚗 <a href="#roadmap">Roadmap</a>
32
+ </p>
33
+
34
+ # Getting Started
35
+
36
+ - Install the library by running `pip install code-battles-cli`.
37
+ - Make sure you have the Python scripts folder in your `PATH` by running `code-battles --help`.
38
+ - If that fails, run `py -m code_battles_cli --help`.
39
+
40
+ The first time you run any command you will be asked for your URL, username and password, and they'll be saved in a `code-battles.json` file for successive calls.
41
+
42
+ # Features
43
+
44
+ ## Downloading the bots
45
+
46
+ You can download all of the bots your team has by running `code-battles download`.
47
+
48
+ ## Uploading a bot
49
+
50
+ You can update a bot in the website by running `code-battles upload bots/example.py` (in this case the bot's name will be `example`).
51
+
52
+ If you want to choose a custom bot name, you can run `code-battles upload bots/example.py -n myamazingbot`.
53
+
54
+ ## Running a simulation
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.
57
+
58
+ You will probably find the scripting API nicer to work with if you want to run simulations locally.
59
+
60
+ ## Usage in scripts
61
+
62
+ You can import the `code_battles_cli.api` module and utilize its `Client` class to hack Code Battles for your needs!
63
+
64
+ Example:
65
+
66
+ ```python
67
+ from code_battles_cli.api import Client
68
+
69
+ client = Client()
70
+
71
+ print(client.get_bots())
72
+ print(client.run_simulation("NYC", ["bots/example.py", "bots/another_example.py"]))
73
+ ```
74
+
75
+ # Roadmap
76
+
77
+ - [x] Download bots.
78
+ - [x] Upload bots.
79
+ - [x] Run no-UI simulations locally.
80
+ - [x] Be usable in scripts.
@@ -0,0 +1,69 @@
1
+ <p align="center">
2
+ <img src="https://github.com/noamzaks/code-battles/assets/63877260/b330aa14-7003-4204-8907-e77a5c6e8d81" height="140">
3
+ </p>
4
+ <h1 align="center">
5
+ Code Battles CLI
6
+ <br />
7
+ <img src="https://img.shields.io/pypi/v/code-battles-cli">
8
+ <img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg">
9
+ <img src="https://img.shields.io/badge/license-MIT-blue.svg">
10
+ </h1>
11
+
12
+ <p align="center">
13
+ A CLI for competitors in any Code Battles event!
14
+ </p>
15
+ <p align="center">
16
+ 🏃 <a href="#getting-started">Getting Started</a>
17
+ &nbsp;&middot&nbsp;
18
+ 💡 <a href="#features">Features</a>
19
+ &nbsp;&middot&nbsp;
20
+ 🚗 <a href="#roadmap">Roadmap</a>
21
+ </p>
22
+
23
+ # Getting Started
24
+
25
+ - Install the library by running `pip install code-battles-cli`.
26
+ - Make sure you have the Python scripts folder in your `PATH` by running `code-battles --help`.
27
+ - If that fails, run `py -m code_battles_cli --help`.
28
+
29
+ The first time you run any command you will be asked for your URL, username and password, and they'll be saved in a `code-battles.json` file for successive calls.
30
+
31
+ # Features
32
+
33
+ ## Downloading the bots
34
+
35
+ You can download all of the bots your team has by running `code-battles download`.
36
+
37
+ ## Uploading a bot
38
+
39
+ You can update a bot in the website by running `code-battles upload bots/example.py` (in this case the bot's name will be `example`).
40
+
41
+ If you want to choose a custom bot name, you can run `code-battles upload bots/example.py -n myamazingbot`.
42
+
43
+ ## Running a simulation
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.
46
+
47
+ You will probably find the scripting API nicer to work with if you want to run simulations locally.
48
+
49
+ ## Usage in scripts
50
+
51
+ You can import the `code_battles_cli.api` module and utilize its `Client` class to hack Code Battles for your needs!
52
+
53
+ Example:
54
+
55
+ ```python
56
+ from code_battles_cli.api import Client
57
+
58
+ client = Client()
59
+
60
+ print(client.get_bots())
61
+ print(client.run_simulation("NYC", ["bots/example.py", "bots/another_example.py"]))
62
+ ```
63
+
64
+ # Roadmap
65
+
66
+ - [x] Download bots.
67
+ - [x] Upload bots.
68
+ - [x] Run no-UI simulations locally.
69
+ - [x] Be usable in scripts.
@@ -0,0 +1,29 @@
1
+ [project]
2
+ name = "code-battles-cli"
3
+ version = "1.1.1"
4
+ description = "A CLI for competitors in any Code Battles event!"
5
+ dependencies = [
6
+ "requests>=2.32.3",
7
+ "google-cloud-firestore>=2.19.0",
8
+ "rich>=13.9.3",
9
+ "click>=8.1.7",
10
+ ]
11
+ readme = "README.md"
12
+ requires-python = ">= 3.8"
13
+
14
+ [project.scripts]
15
+ "code-battles" = "code_battles_cli:main"
16
+
17
+ [build-system]
18
+ requires = ["hatchling==1.26.3"]
19
+ build-backend = "hatchling.build"
20
+
21
+ [tool.rye]
22
+ managed = true
23
+ dev-dependencies = []
24
+
25
+ [tool.hatch.metadata]
26
+ allow-direct-references = true
27
+
28
+ [tool.hatch.build.targets.wheel]
29
+ packages = ["src/code_battles_cli"]
@@ -0,0 +1,70 @@
1
+ # generated by rye
2
+ # use `rye lock` or `rye sync` to update this lockfile
3
+ #
4
+ # last locked with the following flags:
5
+ # pre: false
6
+ # features: []
7
+ # all-features: false
8
+ # with-sources: false
9
+ # generate-hashes: false
10
+ # universal: false
11
+
12
+ -e file:.
13
+ cachetools==5.5.0
14
+ # via google-auth
15
+ certifi==2024.8.30
16
+ # via requests
17
+ charset-normalizer==3.4.0
18
+ # via requests
19
+ click==8.1.7
20
+ # via code-battles-cli
21
+ google-api-core==2.21.0
22
+ # via google-cloud-core
23
+ # via google-cloud-firestore
24
+ google-auth==2.35.0
25
+ # via google-api-core
26
+ # via google-cloud-core
27
+ # via google-cloud-firestore
28
+ google-cloud-core==2.4.1
29
+ # via google-cloud-firestore
30
+ google-cloud-firestore==2.19.0
31
+ # via code-battles-cli
32
+ googleapis-common-protos==1.65.0
33
+ # via google-api-core
34
+ # via grpcio-status
35
+ grpcio==1.67.0
36
+ # via google-api-core
37
+ # via grpcio-status
38
+ grpcio-status==1.67.0
39
+ # via google-api-core
40
+ idna==3.10
41
+ # via requests
42
+ markdown-it-py==3.0.0
43
+ # via rich
44
+ mdurl==0.1.2
45
+ # via markdown-it-py
46
+ proto-plus==1.24.0
47
+ # via google-api-core
48
+ # via google-cloud-firestore
49
+ protobuf==5.28.2
50
+ # via google-api-core
51
+ # via google-cloud-firestore
52
+ # via googleapis-common-protos
53
+ # via grpcio-status
54
+ # via proto-plus
55
+ pyasn1==0.6.1
56
+ # via pyasn1-modules
57
+ # via rsa
58
+ pyasn1-modules==0.4.1
59
+ # via google-auth
60
+ pygments==2.18.0
61
+ # via rich
62
+ requests==2.32.3
63
+ # via code-battles-cli
64
+ # via google-api-core
65
+ rich==13.9.3
66
+ # via code-battles-cli
67
+ rsa==4.9
68
+ # via google-auth
69
+ urllib3==2.2.3
70
+ # via requests
@@ -0,0 +1,70 @@
1
+ # generated by rye
2
+ # use `rye lock` or `rye sync` to update this lockfile
3
+ #
4
+ # last locked with the following flags:
5
+ # pre: false
6
+ # features: []
7
+ # all-features: false
8
+ # with-sources: false
9
+ # generate-hashes: false
10
+ # universal: false
11
+
12
+ -e file:.
13
+ cachetools==5.5.0
14
+ # via google-auth
15
+ certifi==2024.8.30
16
+ # via requests
17
+ charset-normalizer==3.4.0
18
+ # via requests
19
+ click==8.1.7
20
+ # via code-battles-cli
21
+ google-api-core==2.21.0
22
+ # via google-cloud-core
23
+ # via google-cloud-firestore
24
+ google-auth==2.35.0
25
+ # via google-api-core
26
+ # via google-cloud-core
27
+ # via google-cloud-firestore
28
+ google-cloud-core==2.4.1
29
+ # via google-cloud-firestore
30
+ google-cloud-firestore==2.19.0
31
+ # via code-battles-cli
32
+ googleapis-common-protos==1.65.0
33
+ # via google-api-core
34
+ # via grpcio-status
35
+ grpcio==1.67.0
36
+ # via google-api-core
37
+ # via grpcio-status
38
+ grpcio-status==1.67.0
39
+ # via google-api-core
40
+ idna==3.10
41
+ # via requests
42
+ markdown-it-py==3.0.0
43
+ # via rich
44
+ mdurl==0.1.2
45
+ # via markdown-it-py
46
+ proto-plus==1.24.0
47
+ # via google-api-core
48
+ # via google-cloud-firestore
49
+ protobuf==5.28.2
50
+ # via google-api-core
51
+ # via google-cloud-firestore
52
+ # via googleapis-common-protos
53
+ # via grpcio-status
54
+ # via proto-plus
55
+ pyasn1==0.6.1
56
+ # via pyasn1-modules
57
+ # via rsa
58
+ pyasn1-modules==0.4.1
59
+ # via google-auth
60
+ pygments==2.18.0
61
+ # via rich
62
+ requests==2.32.3
63
+ # via code-battles-cli
64
+ # via google-api-core
65
+ rich==13.9.3
66
+ # via code-battles-cli
67
+ rsa==4.9
68
+ # via google-auth
69
+ urllib3==2.2.3
70
+ # via requests
@@ -0,0 +1 @@
1
+ from code_battles_cli.cli import main
@@ -0,0 +1,4 @@
1
+ from code_battles_cli.cli import main
2
+ import sys
3
+
4
+ sys.exit(main())
@@ -0,0 +1,346 @@
1
+ """
2
+ Code Battles Python Client API
3
+
4
+ Firestore client implementation inspired by https://medium.com/@bobthomas295/client-side-authentication-with-python-firestore-and-firebase-352e484a2634
5
+ """
6
+
7
+ import base64
8
+ import datetime
9
+ import logging
10
+ import gzip
11
+ import os
12
+ import json
13
+ import shutil
14
+ import subprocess
15
+ import sys
16
+ from typing import Any, Callable, Dict, List, Optional, Union
17
+ from dataclasses import dataclass
18
+
19
+ import requests
20
+ from google.oauth2.credentials import Credentials
21
+ from google.cloud.firestore import Client as FirestoreClient
22
+ from rich.prompt import Prompt
23
+ from code_battles_cli.log import console, log, progress
24
+
25
+
26
+ SIMULATION_FINISHED_MARK = b"--- SIMULATION FINISHED ---"
27
+ SIMULATION_STEP_MARK = b"__CODE_BATTLES_ADVANCE_STEP"
28
+
29
+
30
+ @dataclass
31
+ class LogEntry:
32
+ step: int
33
+ text: str
34
+ color: str
35
+ player_index: Optional[int]
36
+
37
+
38
+ @dataclass
39
+ class SimulationResults:
40
+ winner_index: int
41
+ winner: str
42
+ steps: int
43
+ logs: List[LogEntry]
44
+
45
+
46
+ @dataclass
47
+ class Simulation:
48
+ map: str
49
+ player_names: str
50
+ game: str
51
+ version: str
52
+ timestamp: datetime.datetime
53
+ logs: list
54
+ decisions: List[bytes]
55
+ seed: int
56
+
57
+ def dump(self):
58
+ return base64.b64encode(
59
+ gzip.compress(
60
+ json.dumps(
61
+ {
62
+ "map": self.map,
63
+ "playerNames": self.player_names,
64
+ "game": self.game,
65
+ "version": self.version,
66
+ "timestamp": self.timestamp.isoformat(),
67
+ "logs": self.logs,
68
+ "decisions": [
69
+ base64.b64encode(decision).decode()
70
+ for decision in self.decisions
71
+ ],
72
+ "seed": self.seed,
73
+ }
74
+ ).encode()
75
+ )
76
+ ).decode()
77
+
78
+ @staticmethod
79
+ def load(file: str):
80
+ contents: Dict[str, Any] = json.loads(gzip.decompress(base64.b64decode(file)))
81
+ return Simulation(
82
+ contents["map"],
83
+ contents["playerNames"],
84
+ contents["game"],
85
+ contents["version"],
86
+ datetime.datetime.fromisoformat(contents["timestamp"]),
87
+ contents["logs"],
88
+ [base64.b64decode(decision) for decision in contents["decisions"]],
89
+ contents["seed"],
90
+ )
91
+
92
+
93
+ class Client:
94
+ def __init__(
95
+ self,
96
+ url: Optional[str] = None,
97
+ username: Optional[str] = None,
98
+ password: Optional[str] = None,
99
+ dump_credentials=True,
100
+ ):
101
+ """
102
+ Creates a client for getting and setting the bots for a Code Battles hosted at `url`
103
+ and signing in as `username` with `password`.
104
+ """
105
+
106
+ if url is None or username is None or password is None:
107
+ self._get_credentials()
108
+ else:
109
+ self.url = url
110
+ self.username = username
111
+ self.password = password
112
+
113
+ self._get_firebase_data()
114
+ self._sign_in()
115
+
116
+ if dump_credentials:
117
+ self._dump_credentials()
118
+
119
+ def _get_credentials(self):
120
+ if os.path.exists("code-battles.json"):
121
+ try:
122
+ with open("code-battles.json", "r") as f:
123
+ configuration = json.load(f)
124
+ self.url: str = configuration["url"]
125
+ self.username: str = configuration["username"]
126
+ self.password: str = configuration["password"]
127
+ except Exception:
128
+ pass
129
+
130
+ if (
131
+ not hasattr(self, "url")
132
+ or not hasattr(self, "username")
133
+ or not hasattr(self, "password")
134
+ ):
135
+ self.url = Prompt.ask("Enter your competition's URL", console=console)
136
+
137
+ if not self.url.startswith("https://"):
138
+ log.warning("Your URL should most likely start with 'https://'.")
139
+ if not self.url.endswith(".web.app"):
140
+ log.warning("Your URL should most likely end with '.web.app'.")
141
+
142
+ self.username = Prompt.ask("Enter your team's username", console=console)
143
+
144
+ if not self.username == self.username.lower():
145
+ log.warning("Your username should most likely be lowercased.")
146
+
147
+ self.password = Prompt.ask(
148
+ "Enter your team's password", console=console, password=True
149
+ )
150
+
151
+ def _dump_credentials(self):
152
+ with open("code-battles.json", "w") as f:
153
+ json.dump(
154
+ {"url": self.url, "username": self.username, "password": self.password},
155
+ f,
156
+ )
157
+ log.info(
158
+ "Credentials were dumped to `code-battles.json`. Make sure other teams don't have access to this file!"
159
+ )
160
+
161
+ def _get_firebase_data(self):
162
+ configuration = requests.get(self.url + "/firebase-configuration.json").json()
163
+ self.firebase_api_key: str = configuration["apiKey"]
164
+ 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
+
169
+ def _sign_in(self, email_domain="gmail.com"):
170
+ try:
171
+ response = requests.post(
172
+ f"https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key={self.firebase_api_key}",
173
+ json={
174
+ "email": self.username + "@" + email_domain,
175
+ "password": self.password,
176
+ "returnSecureToken": True,
177
+ },
178
+ ).json()
179
+ except Exception:
180
+ raise Exception(
181
+ "Sign in failed! Make sure the username and password are correct."
182
+ )
183
+
184
+ self.credentials = Credentials(response["idToken"], response["refreshToken"])
185
+ self.client = FirestoreClient(self.firebase_project_id, self.credentials)
186
+ self.document = self.client.document(f"bots/{self.username}")
187
+
188
+ def get_bots(self) -> Dict[str, str]:
189
+ """Returns a mapping from a bot's name to their Python code."""
190
+ return self.document.get().to_dict()
191
+
192
+ def set_bots(self, bots: Dict[str, str], merge=True) -> None:
193
+ """
194
+ Sets the bots in the website to the specified bots.
195
+ Doesn't remove any bot unless ``merge`` is ``False``, in which case only bots specified in ``bots`` will remain.
196
+ """
197
+ self.document.set(bots, merge)
198
+
199
+ def _possibly_download(self, force_download=False):
200
+ code_directory = os.path.expanduser(
201
+ f"~/.cache/code-battles/{''.join([c for c in self.url.removeprefix("https").removesuffix(".web.app") if c.isalnum()])}"
202
+ )
203
+
204
+ if force_download and os.path.exists(code_directory):
205
+ shutil.rmtree(code_directory)
206
+
207
+ if not os.path.exists(code_directory):
208
+ os.makedirs(code_directory)
209
+
210
+ with console.status("[blue]Fetching configuration from game website..."):
211
+ pyscript_configuration = requests.get(self.url + "/config.json").json()
212
+ logging.info(
213
+ f"Fetched configuration containing {len(pyscript_configuration["files"])} game files."
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.")
235
+
236
+ return code_directory
237
+
238
+ def _get_simulation_output(
239
+ self,
240
+ p: subprocess.Popen,
241
+ json_output=False,
242
+ on_step: Optional[Callable[[], None]] = None,
243
+ ) -> Union[SimulationResults, str]:
244
+ while True:
245
+ line: bytes = p.stdout.readline()
246
+ line = line.strip()
247
+ if line == SIMULATION_FINISHED_MARK:
248
+ break
249
+ elif line == SIMULATION_STEP_MARK:
250
+ if on_step is not None:
251
+ on_step()
252
+ elif len(line) != 0:
253
+ logging.info(line.decode())
254
+
255
+ output: bytes = p.stdout.read()
256
+ if json_output:
257
+ return output.decode()
258
+
259
+ output = json.loads(output)
260
+ return SimulationResults(
261
+ output["winner_index"],
262
+ output["winner"],
263
+ output["steps"],
264
+ [
265
+ LogEntry(
266
+ entry["step"], entry["text"], entry["color"], entry["player_index"]
267
+ )
268
+ for entry in output["logs"]
269
+ ],
270
+ )
271
+
272
+ def run_simulation(
273
+ self,
274
+ map: str,
275
+ bot_filenames: List[str],
276
+ bot_names: Optional[List[str]] = None,
277
+ seed: Optional[int] = None,
278
+ force_download=False,
279
+ json_output=False,
280
+ on_step: Optional[Callable[[], None]] = None,
281
+ output_file: Optional[str] = None,
282
+ ) -> Union[SimulationResults, str]:
283
+ """
284
+ Runs the given simulation without UI locally.
285
+ If ``bot_names`` is not specified, they will be the filenames without the extension.
286
+
287
+ If required (or ``force_download``), this method downloads the simulation code from the website.
288
+
289
+ If ``json_output`` is ``True``, returns the JSON string of the results instead.
290
+ """
291
+
292
+ if bot_names is None:
293
+ bot_names = [
294
+ os.path.splitext(os.path.basename(filename))[0]
295
+ for filename in bot_filenames
296
+ ]
297
+
298
+ code_directory = self._possibly_download(force_download=force_download)
299
+
300
+ p = subprocess.Popen(
301
+ [
302
+ sys.executable,
303
+ os.path.join(code_directory, "main.py"),
304
+ "simulate",
305
+ str(seed),
306
+ str(output_file),
307
+ map,
308
+ "-".join(bot_names),
309
+ ]
310
+ + [os.path.abspath(f) for f in bot_filenames],
311
+ env={"PYTHONPATH": os.path.join(code_directory, "code_battles")},
312
+ stdout=subprocess.PIPE,
313
+ )
314
+
315
+ return self._get_simulation_output(p, json_output, on_step)
316
+
317
+ def run_simulation_from_file(
318
+ self,
319
+ simulation_file: str,
320
+ force_download=False,
321
+ json_output=False,
322
+ on_step: Optional[Callable[[], None]] = None,
323
+ ) -> Union[SimulationResults, str]:
324
+ """
325
+ Runs the given simulation without UI locally from the given simulation file.
326
+
327
+ If required (or ``force_download``), this method downloads the simulation code from the website.
328
+
329
+ If ``json_output`` is ``True``, returns the JSON string of the results instead.
330
+ """
331
+
332
+ code_directory = self._possibly_download(force_download=force_download)
333
+
334
+ p = subprocess.Popen(
335
+ [
336
+ sys.executable,
337
+ os.path.join(code_directory, "main.py"),
338
+ "simulate-from-file",
339
+ simulation_file,
340
+ ],
341
+ env={"PYTHONPATH": os.path.join(code_directory, "code_battles")},
342
+ stdout=subprocess.PIPE,
343
+ stderr=subprocess.PIPE,
344
+ )
345
+
346
+ return self._get_simulation_output(p, json_output, on_step)
@@ -0,0 +1,26 @@
1
+ import datetime
2
+
3
+ import click
4
+
5
+
6
+ @click.group()
7
+ @click.version_option(message="%(prog)s %(version)s")
8
+ def cli():
9
+ pass
10
+
11
+
12
+ def main() -> int:
13
+ from code_battles_cli.log import setup_logging, log
14
+
15
+ setup_logging()
16
+
17
+ from code_battles_cli.commands.download import download
18
+ from code_battles_cli.commands.upload import upload
19
+ from code_battles_cli.commands.run import run
20
+
21
+ cli.add_command(download)
22
+ cli.add_command(upload)
23
+ cli.add_command(run)
24
+ cli(standalone_mode=False)
25
+
26
+ return 0
@@ -0,0 +1,61 @@
1
+ import os
2
+ from typing import Optional
3
+
4
+ import click
5
+
6
+ from code_battles_cli.api import Client
7
+ from code_battles_cli.log import log
8
+
9
+
10
+ @click.command()
11
+ @click.option(
12
+ "-U",
13
+ "--url",
14
+ help="The competition's URL, for example https://code-battles.web.app",
15
+ )
16
+ @click.option("-u", "--username", help="Your team's username, for example mercedes")
17
+ @click.option("-p", "--password", help="Your team's password")
18
+ @click.option(
19
+ "--dump-credentials",
20
+ is_flag=True,
21
+ default=True,
22
+ show_default=True,
23
+ help="Dump the URL, username and password to a `code-battles.json` file",
24
+ )
25
+ @click.option(
26
+ "-b",
27
+ "--bot",
28
+ help="A specific bot name to download (downloads everything by default)",
29
+ )
30
+ @click.option(
31
+ "-d",
32
+ type=click.Path(file_okay=False),
33
+ default="bots",
34
+ show_default=True,
35
+ help="The directory to download the bots into",
36
+ )
37
+ def download(
38
+ url: Optional[str],
39
+ username: Optional[str],
40
+ password: Optional[str],
41
+ dump_credentials: bool,
42
+ bot: Optional[str],
43
+ d: str,
44
+ ):
45
+ client = Client(url, username, password, dump_credentials)
46
+ bots = client.get_bots()
47
+ if bot is not None:
48
+ if bot not in bots:
49
+ raise Exception(f"The specified bot '{bot}' does not exist!")
50
+ bots = {bot: bots[bot]}
51
+
52
+ if not os.path.exists(d):
53
+ os.makedirs(d)
54
+
55
+ for bot_name, bot_code in bots.items():
56
+ with open(os.path.join(d, bot_name + ".py"), "w") as f:
57
+ f.write(bot_code)
58
+
59
+ log.info(
60
+ f"{len(bots)} {'bots were' if len(bots) != 1 else 'bot was'} downloaded to [blue]{d}[/blue]."
61
+ )
@@ -0,0 +1,116 @@
1
+ import time
2
+ import logging
3
+ import sys
4
+ from typing import List, Optional, Tuple
5
+
6
+ import click
7
+
8
+ from code_battles_cli.api import Client, Simulation
9
+ from code_battles_cli.log import log, progress
10
+
11
+
12
+ @click.command()
13
+ @click.argument("map", required=False)
14
+ @click.argument("bots", nargs=-1)
15
+ @click.option(
16
+ "-U",
17
+ "--url",
18
+ help="The competition's URL, for example https://code-battles.web.app",
19
+ )
20
+ @click.option("-u", "--username", help="Your team's username, for example mercedes")
21
+ @click.option("-p", "--password", help="Your team's password")
22
+ @click.option(
23
+ "--dump-credentials",
24
+ is_flag=True,
25
+ default=True,
26
+ show_default=True,
27
+ help="Dump the URL, username and password to a `code-battles.json` file",
28
+ )
29
+ @click.option(
30
+ "--force-download",
31
+ is_flag=True,
32
+ default=False,
33
+ show_default=True,
34
+ help="Re-downloads the simulation code from the website",
35
+ )
36
+ @click.option(
37
+ "-s", "--seed", type=int, help="Sets the given randomness seed for the simulation"
38
+ )
39
+ @click.option(
40
+ "-S",
41
+ "--simulation-file",
42
+ type=click.Path(exists=True, dir_okay=False),
43
+ help="Re-downloads the simulation code from the website",
44
+ )
45
+ @click.option(
46
+ "-O",
47
+ "output_file",
48
+ type=click.Path(exists=False),
49
+ help="Output file to dump the simulation into",
50
+ )
51
+ def run(
52
+ url: Optional[str],
53
+ username: Optional[str],
54
+ password: Optional[str],
55
+ dump_credentials: bool,
56
+ map: str,
57
+ bots: Tuple[str],
58
+ force_download: bool,
59
+ seed: Optional[int],
60
+ simulation_file: Optional[str],
61
+ output_file: Optional[str],
62
+ ):
63
+ logging.getLogger("rich").setLevel(logging.WARNING)
64
+ client = Client(url, username, password, dump_credentials)
65
+ client._possibly_download(force_download)
66
+ start = time.time()
67
+
68
+ with progress:
69
+ progress_id = progress.add_task("[green]Simulating...", total=None)
70
+
71
+ def on_step():
72
+ progress.update(progress_id, advance=1)
73
+
74
+ if simulation_file is not None:
75
+ assert (
76
+ map is None
77
+ ), "The simulation's map is given from the simulation file!"
78
+ assert (
79
+ len(bots) == 0
80
+ ), "The simulation's bots are given from the simulation file!"
81
+ assert (
82
+ seed is None
83
+ ), "The simulation's seed is given from the simulation file!"
84
+ assert (
85
+ output_file is None
86
+ ), "Shouldn't dump simualtion file from existing simulation file!"
87
+
88
+ with open(simulation_file, "r") as f:
89
+ simulation_text = f.read()
90
+ simulation = Simulation.load(simulation_text)
91
+ logging.info(
92
+ f"Detected simulation file of {simulation.game} {simulation.version}"
93
+ )
94
+ logging.info(
95
+ f"Simulation happened in {simulation.map} between {', '.join(simulation.player_names)} with seed {simulation.seed}"
96
+ )
97
+ logging.info(f"Simulation contains {len(simulation.decisions)} decisions")
98
+ progress.update(progress_id, total=len(simulation.decisions) - 1)
99
+ results = client.run_simulation_from_file(
100
+ simulation_file, force_download=False, on_step=on_step
101
+ )
102
+ else:
103
+ assert map is not None, "Map is required for simulation!"
104
+ assert len(bots) > 0, "Bots are required for simulation!"
105
+
106
+ results = client.run_simulation(
107
+ map,
108
+ list(bots),
109
+ seed=seed,
110
+ force_download=False,
111
+ on_step=on_step,
112
+ output_file=output_file,
113
+ )
114
+ print("The winner is", results.winner, "after", results.steps, "steps.")
115
+ end = time.time()
116
+ log.info(f"Simulation took {(end - start) :.2f}s.")
@@ -0,0 +1,48 @@
1
+ import os
2
+ from typing import Optional
3
+
4
+ import click
5
+
6
+ from code_battles_cli.api import Client
7
+ from code_battles_cli.log import log
8
+
9
+
10
+ @click.command()
11
+ @click.option(
12
+ "-U",
13
+ "--url",
14
+ help="The competition's URL, for example https://code-battles.web.app",
15
+ )
16
+ @click.option("-u", "--username", help="Your team's username, for example mercedes")
17
+ @click.option("-p", "--password", help="Your team's password")
18
+ @click.option(
19
+ "--dump-credentials",
20
+ is_flag=True,
21
+ default=True,
22
+ show_default=True,
23
+ help="Dump the URL, username and password to a `code-battles.json` file",
24
+ )
25
+ @click.argument(
26
+ "file",
27
+ type=click.Path(exists=True, readable=True, dir_okay=False),
28
+ )
29
+ @click.option(
30
+ "-n",
31
+ "--name",
32
+ help="The name of the bot to upload. By default, the file name without the extension will be used as the bot's name.",
33
+ )
34
+ def upload(
35
+ file: str,
36
+ name: Optional[str],
37
+ url: Optional[str],
38
+ username: Optional[str],
39
+ password: Optional[str],
40
+ dump_credentials: bool,
41
+ ):
42
+ bot_name = name if name is not None else os.path.splitext(os.path.basename(file))[0]
43
+ with open(file, "r") as f:
44
+ bot_code = f.read()
45
+
46
+ client = Client(url, username, password, dump_credentials)
47
+ client.set_bots({bot_name: bot_code})
48
+ log.info(f"{bot_code.count('\n') + 1} lines of code were uploaded as '{bot_name}'.")
@@ -0,0 +1,35 @@
1
+ import os
2
+ import logging
3
+
4
+ from rich.console import Console
5
+ from rich.progress import BarColumn, MofNCompleteColumn, Progress, TextColumn
6
+ from rich.table import Column
7
+ from rich.traceback import install
8
+ from rich.logging import RichHandler
9
+
10
+ log = logging.getLogger("rich")
11
+ console = Console()
12
+ progress = Progress(
13
+ TextColumn(
14
+ "[progress.description]{task.description}",
15
+ table_column=Column(width=50),
16
+ ),
17
+ BarColumn(bar_width=None),
18
+ MofNCompleteColumn(table_column=Column(width=10, justify="right")),
19
+ console=console,
20
+ expand=True,
21
+ transient=True,
22
+ )
23
+
24
+
25
+ def setup_logging():
26
+ FORMAT = "%(message)s"
27
+
28
+ logging.basicConfig(
29
+ level=os.environ.get("CODE_BATTLES_LOG_LEVEL", "INFO"),
30
+ format=FORMAT,
31
+ datefmt="[%X]",
32
+ handlers=[RichHandler(console=console, markup=True)],
33
+ )
34
+
35
+ install()