|
|
@ -4,6 +4,7 @@ import json |
|
|
|
import logging |
|
|
|
import requests |
|
|
|
import shutil |
|
|
|
import sys |
|
|
|
import tempfile |
|
|
|
import time |
|
|
|
from subprocess import Popen, PIPE, TimeoutExpired |
|
|
@ -36,12 +37,7 @@ def main(args: argparse.Namespace) -> None: |
|
|
|
min_round = 0 |
|
|
|
|
|
|
|
while True: |
|
|
|
state, wait_time = get_state(min_round, args) |
|
|
|
if state is None: |
|
|
|
# retry later |
|
|
|
time.sleep(wait_time) |
|
|
|
continue |
|
|
|
|
|
|
|
state = get_state(min_round, args) |
|
|
|
round = state["round"] |
|
|
|
min_round = round + 1 |
|
|
|
# if requested, dump state to file instead |
|
|
@ -59,16 +55,14 @@ def main(args: argparse.Namespace) -> None: |
|
|
|
turn = json.loads(turn_json) |
|
|
|
except json.JSONDecodeError: |
|
|
|
logger.error(f"Invalid JSON returned by strategy code.") |
|
|
|
continue |
|
|
|
sys.exit(1) |
|
|
|
|
|
|
|
while not send_turn(turn, round, args): |
|
|
|
# if there was a connection error, retry later |
|
|
|
time.sleep(TIME_BEFORE_RETRY) |
|
|
|
send_turn(turn, round, args) |
|
|
|
|
|
|
|
|
|
|
|
def run_subprocess( |
|
|
|
program: str, state_json: str, timeout: Optional[float] |
|
|
|
) -> Optional[str]: |
|
|
|
program: str, state_json: str, timeout: Optional[float] |
|
|
|
) -> Optional[str]: |
|
|
|
"""Run user program and return its output.""" |
|
|
|
|
|
|
|
proc = Popen([program], encoding="utf-8", |
|
|
@ -83,82 +77,104 @@ def run_subprocess( |
|
|
|
logger.error(f"Strategy code stderr:\n{stderr}") |
|
|
|
if proc.returncode != 0: |
|
|
|
logger.error("Strategy code exited with non-zero exit code.") |
|
|
|
return None |
|
|
|
sys.exit(1) |
|
|
|
return stdout |
|
|
|
|
|
|
|
|
|
|
|
def get_state(min_round: int, args) -> Tuple[Optional[dict], float]: |
|
|
|
"""Get game data from the server. |
|
|
|
def get_state(min_round: int, args) -> dict: |
|
|
|
"""Iteratively try to get game data from the server.""" |
|
|
|
|
|
|
|
Returns None and wait time if there was an error.""" |
|
|
|
state, wait_time = get_state_once() |
|
|
|
while state is None: |
|
|
|
time.sleep(wait_time) |
|
|
|
state, wait_time = get_state_once() |
|
|
|
|
|
|
|
try: |
|
|
|
r = requests.get(f"{args.server}/api/state", params={ |
|
|
|
"game": args.game, |
|
|
|
"token": args.token, |
|
|
|
"min_round": min_round |
|
|
|
}) |
|
|
|
# retry later if there was an error |
|
|
|
except requests.exceptions.RequestException as e: |
|
|
|
logger.warning(f"Request error: {e}") |
|
|
|
return None, TIME_BEFORE_RETRY |
|
|
|
if not r.ok: |
|
|
|
logger.warning(f"Server error: {r.status_code} {r.reason}") |
|
|
|
return None, TIME_BEFORE_RETRY |
|
|
|
|
|
|
|
state = r.json() |
|
|
|
# also retry if the server is not willing to give us the state yet |
|
|
|
if state["status"] == "waiting": |
|
|
|
logger.info("Server is busy.") |
|
|
|
return None, state["wait"] |
|
|
|
if state["status"] == "too_early": |
|
|
|
logger.info("Round didn't start yet.") |
|
|
|
def get_state_once() -> Tuple[Optional[dict], float]: |
|
|
|
try: |
|
|
|
res = requests.get(f"{args.server}/api/state", params={ |
|
|
|
"game": args.game, |
|
|
|
"token": args.token, |
|
|
|
"min_round": min_round |
|
|
|
}) |
|
|
|
except requests.exceptions.RequestException as e: |
|
|
|
# server might be down, retry later |
|
|
|
logger.warning(f"Request error: {e}") |
|
|
|
return None, TIME_BEFORE_RETRY |
|
|
|
if not res.ok: |
|
|
|
logger.error(f"Server error: {res.status_code} {res.reason}") |
|
|
|
log_server_error(res) |
|
|
|
sys.exit(1) |
|
|
|
|
|
|
|
state = res.json() |
|
|
|
if state["status"] == "ok": |
|
|
|
return state, 0 |
|
|
|
|
|
|
|
# retry after some time |
|
|
|
if state["status"] == "waiting": |
|
|
|
logger.info("Server is busy.") |
|
|
|
if state["status"] == "too_early": |
|
|
|
logger.info("Round didn't start yet.") |
|
|
|
return None, state["wait"] |
|
|
|
|
|
|
|
logger.info("Received new state.") |
|
|
|
return state, 0 |
|
|
|
|
|
|
|
|
|
|
|
def send_turn(turn: dict, round: int, args) -> bool: |
|
|
|
"""Send turn to the server. |
|
|
|
def send_turn(turn: dict, round: int, args) -> None: |
|
|
|
"""Iteratively try to send turn to the server.""" |
|
|
|
|
|
|
|
Returns True if the server received the request.""" |
|
|
|
while not send_turn_once(): |
|
|
|
time.sleep(TIME_BEFORE_RETRY) |
|
|
|
|
|
|
|
try: |
|
|
|
r = requests.post( |
|
|
|
f"{args.server}/api/action", |
|
|
|
params={ |
|
|
|
"game": args.game, |
|
|
|
"token": args.token, |
|
|
|
"round": round |
|
|
|
}, |
|
|
|
json=turn |
|
|
|
) |
|
|
|
except requests.exceptions.RequestException as e: |
|
|
|
logger.warning(f"Request error: {e}") |
|
|
|
return False |
|
|
|
if not r.ok: |
|
|
|
logger.warning(f"Server error: {r.status_code} {r.reason}") |
|
|
|
return False |
|
|
|
|
|
|
|
# print errors |
|
|
|
# because such errors are caused by the submitted turn, |
|
|
|
# retrying will not help, so return True |
|
|
|
response = r.json() |
|
|
|
if response["status"] == "ok": |
|
|
|
logger.info("Turn accepted.") |
|
|
|
elif response["status"] == "too_late": |
|
|
|
logger.error("Turn submitted too late.") |
|
|
|
elif response["status"] == "error": |
|
|
|
logger.error(f"Turn error: {response['description']}") |
|
|
|
elif response["status"] == "warning": |
|
|
|
member_warns = [ |
|
|
|
f" {member['id']}: {member['description']}" |
|
|
|
for member in response["members"] |
|
|
|
] |
|
|
|
logger.warn("Member warnings:\n" + "\n".join(member_warns)) |
|
|
|
|
|
|
|
return True |
|
|
|
def send_turn_once() -> bool: |
|
|
|
"""Returns True if server answered.""" |
|
|
|
|
|
|
|
try: |
|
|
|
res = requests.post( |
|
|
|
f"{args.server}/api/action", |
|
|
|
params={ |
|
|
|
"game": args.game, |
|
|
|
"token": args.token, |
|
|
|
"round": round |
|
|
|
}, |
|
|
|
json=turn |
|
|
|
) |
|
|
|
except requests.exceptions.RequestException as e: |
|
|
|
# server might be down, retry later |
|
|
|
logger.warning(f"Request error: {e}") |
|
|
|
return False |
|
|
|
if not res.ok: |
|
|
|
log_server_error(res) |
|
|
|
sys.exit(1) |
|
|
|
|
|
|
|
# print errors |
|
|
|
# because such errors are caused by the submitted turn, |
|
|
|
# retrying will not help, so return True |
|
|
|
response = res.json() |
|
|
|
if response["status"] == "ok": |
|
|
|
logger.info("Turn accepted.") |
|
|
|
elif response["status"] == "too_late": |
|
|
|
logger.error("Turn submitted too late.") |
|
|
|
elif response["status"] == "error": |
|
|
|
logger.error(f"Turn error: {response['description']}") |
|
|
|
elif response["status"] == "warning": |
|
|
|
member_warns = [ |
|
|
|
f" {member['id']}: {member['description']}" |
|
|
|
for member in response["members"] |
|
|
|
] |
|
|
|
logger.warning( |
|
|
|
"Member warnings:\n" |
|
|
|
"\n".join(member_warns) |
|
|
|
) |
|
|
|
|
|
|
|
return True |
|
|
|
|
|
|
|
|
|
|
|
def log_server_error(res: requests.Response) -> None: |
|
|
|
logger.error( |
|
|
|
f"Server error: {res.status_code} {res.reason}\n" |
|
|
|
f"{res['description']}" |
|
|
|
) |
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
|