code-battles-cli 0.1.1__tar.gz → 1.1.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-0.1.1 → code_battles_cli-1.1.0}/PKG-INFO +12 -4
- {code_battles_cli-0.1.1 → code_battles_cli-1.1.0}/README.md +11 -3
- {code_battles_cli-0.1.1 → code_battles_cli-1.1.0}/pyproject.toml +1 -1
- code_battles_cli-1.1.0/src/code_battles_cli/api.py +347 -0
- {code_battles_cli-0.1.1 → code_battles_cli-1.1.0}/src/code_battles_cli/cli.py +4 -1
- {code_battles_cli-0.1.1 → code_battles_cli-1.1.0}/src/code_battles_cli/commands/download.py +2 -1
- code_battles_cli-1.1.0/src/code_battles_cli/commands/run.py +114 -0
- {code_battles_cli-0.1.1 → code_battles_cli-1.1.0}/src/code_battles_cli/commands/upload.py +2 -1
- code_battles_cli-0.1.1/src/code_battles_cli/logging.py → code_battles_cli-1.1.0/src/code_battles_cli/log.py +2 -1
- code_battles_cli-0.1.1/src/code_battles_cli/api.py +0 -122
- {code_battles_cli-0.1.1 → code_battles_cli-1.1.0}/.gitignore +0 -0
- {code_battles_cli-0.1.1 → code_battles_cli-1.1.0}/.python-version +0 -0
- {code_battles_cli-0.1.1 → code_battles_cli-1.1.0}/LICENSE.md +0 -0
- {code_battles_cli-0.1.1 → code_battles_cli-1.1.0}/requirements-dev.lock +0 -0
- {code_battles_cli-0.1.1 → code_battles_cli-1.1.0}/requirements.lock +0 -0
- {code_battles_cli-0.1.1 → code_battles_cli-1.1.0}/src/code_battles_cli/__init__.py +0 -0
- {code_battles_cli-0.1.1 → code_battles_cli-1.1.0}/src/code_battles_cli/__main__.py +0 -0
- {code_battles_cli-0.1.1 → code_battles_cli-1.1.0}/src/code_battles_cli/commands/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: code-battles-cli
|
|
3
|
-
Version:
|
|
3
|
+
Version: 1.1.0
|
|
4
4
|
Summary: A CLI for competitors in any Code Battles event!
|
|
5
5
|
License-File: LICENSE.md
|
|
6
6
|
Requires-Python: >=3.8
|
|
@@ -16,6 +16,7 @@ Description-Content-Type: text/markdown
|
|
|
16
16
|
<h1 align="center">
|
|
17
17
|
Code Battles CLI
|
|
18
18
|
<br />
|
|
19
|
+
<img src="https://img.shields.io/pypi/v/code-battles-cli">
|
|
19
20
|
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg">
|
|
20
21
|
<img src="https://img.shields.io/badge/license-MIT-blue.svg">
|
|
21
22
|
</h1>
|
|
@@ -34,7 +35,7 @@ Description-Content-Type: text/markdown
|
|
|
34
35
|
# Getting Started
|
|
35
36
|
|
|
36
37
|
- Install the library by running `pip install code-battles-cli`.
|
|
37
|
-
- Make sure you have the Python scripts folder in your
|
|
38
|
+
- Make sure you have the Python scripts folder in your `PATH` by running `code-battles --help`.
|
|
38
39
|
- If that fails, run `py -m code_battles_cli --help`.
|
|
39
40
|
|
|
40
41
|
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.
|
|
@@ -51,7 +52,13 @@ You can update a bot in the website by running `code-battles upload bots/example
|
|
|
51
52
|
|
|
52
53
|
If you want to choose a custom bot name, you can run `code-battles upload bots/example.py -n myamazingbot`.
|
|
53
54
|
|
|
54
|
-
##
|
|
55
|
+
## Running a simulation
|
|
56
|
+
|
|
57
|
+
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.
|
|
58
|
+
|
|
59
|
+
You will probably find the scripting API nicer to work with if you want to run simulations locally.
|
|
60
|
+
|
|
61
|
+
## Usage in scripts
|
|
55
62
|
|
|
56
63
|
You can import the `code_battles_cli.api` module and utilize its `Client` class to hack Code Battles for your needs!
|
|
57
64
|
|
|
@@ -63,11 +70,12 @@ from code_battles_cli.api import Client
|
|
|
63
70
|
client = Client()
|
|
64
71
|
|
|
65
72
|
print(client.get_bots())
|
|
73
|
+
print(client.run_simulation("NYC", ["bots/example.py", "bots/another_example.py"]))
|
|
66
74
|
```
|
|
67
75
|
|
|
68
76
|
# Roadmap
|
|
69
77
|
|
|
70
78
|
- [x] Download bots.
|
|
71
79
|
- [x] Upload bots.
|
|
72
|
-
- [
|
|
80
|
+
- [x] Run no-UI simulations locally.
|
|
73
81
|
- [x] Be usable in scripts.
|
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
<h1 align="center">
|
|
5
5
|
Code Battles CLI
|
|
6
6
|
<br />
|
|
7
|
+
<img src="https://img.shields.io/pypi/v/code-battles-cli">
|
|
7
8
|
<img src="https://img.shields.io/badge/PRs-welcome-brightgreen.svg">
|
|
8
9
|
<img src="https://img.shields.io/badge/license-MIT-blue.svg">
|
|
9
10
|
</h1>
|
|
@@ -22,7 +23,7 @@
|
|
|
22
23
|
# Getting Started
|
|
23
24
|
|
|
24
25
|
- Install the library by running `pip install code-battles-cli`.
|
|
25
|
-
- Make sure you have the Python scripts folder in your
|
|
26
|
+
- Make sure you have the Python scripts folder in your `PATH` by running `code-battles --help`.
|
|
26
27
|
- If that fails, run `py -m code_battles_cli --help`.
|
|
27
28
|
|
|
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.
|
|
@@ -39,7 +40,13 @@ You can update a bot in the website by running `code-battles upload bots/example
|
|
|
39
40
|
|
|
40
41
|
If you want to choose a custom bot name, you can run `code-battles upload bots/example.py -n myamazingbot`.
|
|
41
42
|
|
|
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
|
|
43
50
|
|
|
44
51
|
You can import the `code_battles_cli.api` module and utilize its `Client` class to hack Code Battles for your needs!
|
|
45
52
|
|
|
@@ -51,11 +58,12 @@ from code_battles_cli.api import Client
|
|
|
51
58
|
client = Client()
|
|
52
59
|
|
|
53
60
|
print(client.get_bots())
|
|
61
|
+
print(client.run_simulation("NYC", ["bots/example.py", "bots/another_example.py"]))
|
|
54
62
|
```
|
|
55
63
|
|
|
56
64
|
# Roadmap
|
|
57
65
|
|
|
58
66
|
- [x] Download bots.
|
|
59
67
|
- [x] Upload bots.
|
|
60
|
-
- [
|
|
68
|
+
- [x] Run no-UI simulations locally.
|
|
61
69
|
- [x] Be usable in scripts.
|
|
@@ -0,0 +1,347 @@
|
|
|
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
|
+
stderr=subprocess.PIPE,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
return self._get_simulation_output(p, json_output, on_step)
|
|
317
|
+
|
|
318
|
+
def run_simulation_from_file(
|
|
319
|
+
self,
|
|
320
|
+
simulation_file: str,
|
|
321
|
+
force_download=False,
|
|
322
|
+
json_output=False,
|
|
323
|
+
on_step: Optional[Callable[[], None]] = None,
|
|
324
|
+
) -> Union[SimulationResults, str]:
|
|
325
|
+
"""
|
|
326
|
+
Runs the given simulation without UI locally from the given simulation file.
|
|
327
|
+
|
|
328
|
+
If required (or ``force_download``), this method downloads the simulation code from the website.
|
|
329
|
+
|
|
330
|
+
If ``json_output`` is ``True``, returns the JSON string of the results instead.
|
|
331
|
+
"""
|
|
332
|
+
|
|
333
|
+
code_directory = self._possibly_download(force_download=force_download)
|
|
334
|
+
|
|
335
|
+
p = subprocess.Popen(
|
|
336
|
+
[
|
|
337
|
+
sys.executable,
|
|
338
|
+
os.path.join(code_directory, "main.py"),
|
|
339
|
+
"simulate-from-file",
|
|
340
|
+
simulation_file,
|
|
341
|
+
],
|
|
342
|
+
env={"PYTHONPATH": os.path.join(code_directory, "code_battles")},
|
|
343
|
+
stdout=subprocess.PIPE,
|
|
344
|
+
stderr=subprocess.PIPE,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
return self._get_simulation_output(p, json_output, on_step)
|
|
@@ -4,20 +4,23 @@ import click
|
|
|
4
4
|
|
|
5
5
|
|
|
6
6
|
@click.group()
|
|
7
|
+
@click.version_option(message="%(prog)s %(version)s")
|
|
7
8
|
def cli():
|
|
8
9
|
pass
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
def main() -> int:
|
|
12
|
-
from code_battles_cli.
|
|
13
|
+
from code_battles_cli.log import setup_logging, log
|
|
13
14
|
|
|
14
15
|
setup_logging()
|
|
15
16
|
|
|
16
17
|
from code_battles_cli.commands.download import download
|
|
17
18
|
from code_battles_cli.commands.upload import upload
|
|
19
|
+
from code_battles_cli.commands.run import run
|
|
18
20
|
|
|
19
21
|
cli.add_command(download)
|
|
20
22
|
cli.add_command(upload)
|
|
23
|
+
cli.add_command(run)
|
|
21
24
|
cli(standalone_mode=False)
|
|
22
25
|
|
|
23
26
|
return 0
|
|
@@ -4,7 +4,7 @@ from typing import Optional
|
|
|
4
4
|
import click
|
|
5
5
|
|
|
6
6
|
from code_battles_cli.api import Client
|
|
7
|
-
from code_battles_cli.
|
|
7
|
+
from code_battles_cli.log import log
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
@click.command()
|
|
@@ -15,6 +15,7 @@ from code_battles_cli.logging import log
|
|
|
15
15
|
@click.option("-p", "--password", help="Your team's password")
|
|
16
16
|
@click.option(
|
|
17
17
|
"--dump-credentials",
|
|
18
|
+
is_flag=True,
|
|
18
19
|
default=True,
|
|
19
20
|
show_default=True,
|
|
20
21
|
help="Dump the URL, username and password to a `code-battles.json` file",
|
|
@@ -0,0 +1,114 @@
|
|
|
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", "--url", help="The competitions URL, for example https://code-battles.web.app"
|
|
17
|
+
)
|
|
18
|
+
@click.option("-u", "--username", help="Your team's username, for example mercedes")
|
|
19
|
+
@click.option("-p", "--password", help="Your team's password")
|
|
20
|
+
@click.option(
|
|
21
|
+
"--dump-credentials",
|
|
22
|
+
is_flag=True,
|
|
23
|
+
default=True,
|
|
24
|
+
show_default=True,
|
|
25
|
+
help="Dump the URL, username and password to a `code-battles.json` file",
|
|
26
|
+
)
|
|
27
|
+
@click.option(
|
|
28
|
+
"--force-download",
|
|
29
|
+
is_flag=True,
|
|
30
|
+
default=False,
|
|
31
|
+
show_default=True,
|
|
32
|
+
help="Re-downloads the simulation code from the website",
|
|
33
|
+
)
|
|
34
|
+
@click.option(
|
|
35
|
+
"-s", "--seed", type=int, help="Sets the given randomness seed for the simulation"
|
|
36
|
+
)
|
|
37
|
+
@click.option(
|
|
38
|
+
"-S",
|
|
39
|
+
"--simulation-file",
|
|
40
|
+
type=click.Path(exists=True, dir_okay=False),
|
|
41
|
+
help="Re-downloads the simulation code from the website",
|
|
42
|
+
)
|
|
43
|
+
@click.option(
|
|
44
|
+
"-O",
|
|
45
|
+
"output_file",
|
|
46
|
+
type=click.Path(exists=False),
|
|
47
|
+
help="Output file to dump the simulation into",
|
|
48
|
+
)
|
|
49
|
+
def run(
|
|
50
|
+
url: Optional[str],
|
|
51
|
+
username: Optional[str],
|
|
52
|
+
password: Optional[str],
|
|
53
|
+
dump_credentials: bool,
|
|
54
|
+
map: str,
|
|
55
|
+
bots: Tuple[str],
|
|
56
|
+
force_download: bool,
|
|
57
|
+
seed: Optional[int],
|
|
58
|
+
simulation_file: Optional[str],
|
|
59
|
+
output_file: Optional[str],
|
|
60
|
+
):
|
|
61
|
+
logging.getLogger("rich").setLevel(logging.WARNING)
|
|
62
|
+
client = Client(url, username, password, dump_credentials)
|
|
63
|
+
client._possibly_download(force_download)
|
|
64
|
+
start = time.time()
|
|
65
|
+
|
|
66
|
+
with progress:
|
|
67
|
+
progress_id = progress.add_task("[green]Simulating...", total=None)
|
|
68
|
+
|
|
69
|
+
def on_step():
|
|
70
|
+
progress.update(progress_id, advance=1)
|
|
71
|
+
|
|
72
|
+
if simulation_file is not None:
|
|
73
|
+
assert (
|
|
74
|
+
map is None
|
|
75
|
+
), "The simulation's map is given from the simulation file!"
|
|
76
|
+
assert (
|
|
77
|
+
len(bots) == 0
|
|
78
|
+
), "The simulation's bots are given from the simulation file!"
|
|
79
|
+
assert (
|
|
80
|
+
seed is None
|
|
81
|
+
), "The simulation's seed is given from the simulation file!"
|
|
82
|
+
assert (
|
|
83
|
+
output_file is None
|
|
84
|
+
), "Shouldn't dump simualtion file from existing simulation file!"
|
|
85
|
+
|
|
86
|
+
with open(simulation_file, "r") as f:
|
|
87
|
+
simulation_text = f.read()
|
|
88
|
+
simulation = Simulation.load(simulation_text)
|
|
89
|
+
logging.info(
|
|
90
|
+
f"Detected simulation file of {simulation.game} {simulation.version}"
|
|
91
|
+
)
|
|
92
|
+
logging.info(
|
|
93
|
+
f"Simulation happened in {simulation.map} between {', '.join(simulation.player_names)} with seed {simulation.seed}"
|
|
94
|
+
)
|
|
95
|
+
logging.info(f"Simulation contains {len(simulation.decisions)} decisions")
|
|
96
|
+
progress.update(progress_id, total=len(simulation.decisions) - 1)
|
|
97
|
+
results = client.run_simulation_from_file(
|
|
98
|
+
simulation_file, force_download=False, on_step=on_step
|
|
99
|
+
)
|
|
100
|
+
else:
|
|
101
|
+
assert map is not None, "Map is required for simulation!"
|
|
102
|
+
assert len(bots) > 0, "Bots are required for simulation!"
|
|
103
|
+
|
|
104
|
+
results = client.run_simulation(
|
|
105
|
+
map,
|
|
106
|
+
list(bots),
|
|
107
|
+
seed=seed,
|
|
108
|
+
force_download=False,
|
|
109
|
+
on_step=on_step,
|
|
110
|
+
output_file=output_file,
|
|
111
|
+
)
|
|
112
|
+
print("The winner is", results.winner, "after", results.steps, "steps.")
|
|
113
|
+
end = time.time()
|
|
114
|
+
log.info(f"Simulation took {(end - start) :.2f}s.")
|
|
@@ -4,7 +4,7 @@ from typing import Optional
|
|
|
4
4
|
import click
|
|
5
5
|
|
|
6
6
|
from code_battles_cli.api import Client
|
|
7
|
-
from code_battles_cli.
|
|
7
|
+
from code_battles_cli.log import log
|
|
8
8
|
|
|
9
9
|
|
|
10
10
|
@click.command()
|
|
@@ -15,6 +15,7 @@ from code_battles_cli.logging import log
|
|
|
15
15
|
@click.option("-p", "--password", help="Your team's password")
|
|
16
16
|
@click.option(
|
|
17
17
|
"--dump-credentials",
|
|
18
|
+
is_flag=True,
|
|
18
19
|
default=True,
|
|
19
20
|
show_default=True,
|
|
20
21
|
help="Dump the URL, username and password to a `code-battles.json` file",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import os
|
|
1
2
|
import logging
|
|
2
3
|
|
|
3
4
|
from rich.console import Console
|
|
@@ -25,7 +26,7 @@ def setup_logging():
|
|
|
25
26
|
FORMAT = "%(message)s"
|
|
26
27
|
|
|
27
28
|
logging.basicConfig(
|
|
28
|
-
level=
|
|
29
|
+
level=os.environ.get("CODE_BATTLES_LOG_LEVEL", "INFO"),
|
|
29
30
|
format=FORMAT,
|
|
30
31
|
datefmt="[%X]",
|
|
31
32
|
handlers=[RichHandler(console=console, markup=True)],
|
|
@@ -1,122 +0,0 @@
|
|
|
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 os
|
|
8
|
-
import json
|
|
9
|
-
from typing import Dict, List, Optional
|
|
10
|
-
|
|
11
|
-
import requests
|
|
12
|
-
from google.oauth2.credentials import Credentials
|
|
13
|
-
from google.cloud.firestore import Client as FirestoreClient
|
|
14
|
-
from rich.prompt import Prompt
|
|
15
|
-
from code_battles_cli.logging import console, log
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
class Client:
|
|
19
|
-
def __init__(
|
|
20
|
-
self,
|
|
21
|
-
url: Optional[str] = None,
|
|
22
|
-
username: Optional[str] = None,
|
|
23
|
-
password: Optional[str] = None,
|
|
24
|
-
dump_credentials=True,
|
|
25
|
-
):
|
|
26
|
-
"""
|
|
27
|
-
Creates a client for getting and setting the bots for a Code Battles hosted at `url`
|
|
28
|
-
and signing in as `username` with `password`.
|
|
29
|
-
"""
|
|
30
|
-
|
|
31
|
-
if url is None or username is None or password is None:
|
|
32
|
-
self._get_credentials()
|
|
33
|
-
else:
|
|
34
|
-
self.url = url
|
|
35
|
-
self.username = username
|
|
36
|
-
self.password = password
|
|
37
|
-
|
|
38
|
-
self._get_firebase_data()
|
|
39
|
-
self._sign_in()
|
|
40
|
-
|
|
41
|
-
if dump_credentials:
|
|
42
|
-
self._dump_credentials()
|
|
43
|
-
|
|
44
|
-
def _get_credentials(self):
|
|
45
|
-
if os.path.exists("code-battles.json"):
|
|
46
|
-
try:
|
|
47
|
-
with open("code-battles.json", "r") as f:
|
|
48
|
-
configuration = json.load(f)
|
|
49
|
-
self.url: str = configuration["url"]
|
|
50
|
-
self.username: str = configuration["username"]
|
|
51
|
-
self.password: str = configuration["password"]
|
|
52
|
-
except Exception:
|
|
53
|
-
pass
|
|
54
|
-
|
|
55
|
-
if (
|
|
56
|
-
not hasattr(self, "url")
|
|
57
|
-
or not hasattr(self, "username")
|
|
58
|
-
or not hasattr(self, "password")
|
|
59
|
-
):
|
|
60
|
-
self.url = Prompt.ask("Enter your competition's URL", console=console)
|
|
61
|
-
|
|
62
|
-
if not self.url.startswith("https://"):
|
|
63
|
-
log.warning("Your URL should most likely start with 'https://'.")
|
|
64
|
-
if not self.url.endswith(".web.app"):
|
|
65
|
-
log.warning("Your URL should most likely end with '.web.app'.")
|
|
66
|
-
|
|
67
|
-
self.username = Prompt.ask("Enter your team's username", console=console)
|
|
68
|
-
|
|
69
|
-
if not self.username == self.username.lower():
|
|
70
|
-
log.warning("Your username should most likely be lowercased.")
|
|
71
|
-
|
|
72
|
-
self.password = Prompt.ask(
|
|
73
|
-
"Enter your team's password", console=console, password=True
|
|
74
|
-
)
|
|
75
|
-
|
|
76
|
-
def _dump_credentials(self):
|
|
77
|
-
with open("code-battles.json", "w") as f:
|
|
78
|
-
json.dump(
|
|
79
|
-
{"url": self.url, "username": self.username, "password": self.password},
|
|
80
|
-
f,
|
|
81
|
-
)
|
|
82
|
-
log.info(
|
|
83
|
-
"Credentials were dumped to `code-battles.json`. Make sure other teams don't have access to this file!"
|
|
84
|
-
)
|
|
85
|
-
|
|
86
|
-
def _get_firebase_data(self):
|
|
87
|
-
configuration = requests.get(self.url + "/firebase-configuration.json").json()
|
|
88
|
-
self.firebase_api_key: str = configuration["apiKey"]
|
|
89
|
-
self.firebase_project_id: str = configuration["projectId"]
|
|
90
|
-
# print(
|
|
91
|
-
# f"Got API Key: '{self.firebase_api_key}' and Project ID: '{self.firebase_project_id}'"
|
|
92
|
-
# )
|
|
93
|
-
|
|
94
|
-
def _sign_in(self, email_domain="gmail.com"):
|
|
95
|
-
try:
|
|
96
|
-
response = requests.post(
|
|
97
|
-
f"https://identitytoolkit.googleapis.com/v1/accounts:signInWithPassword?key={self.firebase_api_key}",
|
|
98
|
-
json={
|
|
99
|
-
"email": self.username + "@" + email_domain,
|
|
100
|
-
"password": self.password,
|
|
101
|
-
"returnSecureToken": True,
|
|
102
|
-
},
|
|
103
|
-
).json()
|
|
104
|
-
except Exception:
|
|
105
|
-
raise Exception(
|
|
106
|
-
"Sign in failed! Make sure the username and password are correct."
|
|
107
|
-
)
|
|
108
|
-
|
|
109
|
-
self.credentials = Credentials(response["idToken"], response["refreshToken"])
|
|
110
|
-
self.client = FirestoreClient(self.firebase_project_id, self.credentials)
|
|
111
|
-
self.document = self.client.document(f"bots/{self.username}")
|
|
112
|
-
|
|
113
|
-
def get_bots(self) -> Dict[str, str]:
|
|
114
|
-
"""Returns a mapping from a bot's name to their Python code."""
|
|
115
|
-
return self.document.get().to_dict()
|
|
116
|
-
|
|
117
|
-
def set_bots(self, bots: Dict[str, str], merge=True) -> None:
|
|
118
|
-
"""
|
|
119
|
-
Sets the bots in the website to the specified bots.
|
|
120
|
-
Doesn't remove any bot unless `merge` is `False`, in which case only bots specified in `bots` will remain.
|
|
121
|
-
"""
|
|
122
|
-
self.document.set(bots, merge)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|