You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

198 lines
6.4 KiB

#!/usr/bin/env python3
import argparse
import json
import logging
import os
import requests
import shutil
import sys
import tempfile
import time
from subprocess import Popen, PIPE, TimeoutExpired
from typing import List, Optional, Tuple
parser = argparse.ArgumentParser()
parser.add_argument("--token", type=str, required=True, help="API token.")
parser.add_argument("--server", type=str, default="http://localhost:5000", help="Adresa severu.")
parser.add_argument("--game", type=str, default="main", help="'main' nebo 'test_#'.")
parser.add_argument("--command", type=str, nargs="+", default=["python3", "%"], help="Příkaz pro spuštění strategie. '%%' bude nahrazeno programem.")
parser.add_argument("--program", type=str, default="strategy.py", help="Program, který má být použít v příkazu.")
parser.add_argument("--copy", default=False, action="store_true", help="Vyrob kopii souboru, aby se vždy použil stejný kód.")
parser.add_argument("--log-level", type=str, default="info", choices=["error", "warning", "info"])
parser.add_argument("--save-state", type=str, default=None, help="Pouze ulož herní info do souboru.")
TIME_BEFORE_RETRY = 2.0
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("client")
def main(args: argparse.Namespace) -> None:
logger.setLevel(args.log_level.upper())
min_round = 0
if args.save_state is None:
command = get_command(args)
while True:
state = get_state(min_round, args)
round = state["round"]
min_round = round + 1
# ulož herní stav a ukonči program
if args.save_state is not None:
if args.save_state == "-":
print(json.dumps(state))
else:
with open(args.save_state, "w") as f:
json.dump(state, f)
return
turn_json = run_subprocess(
command, json.dumps(state), state["time_to_response"]
)
if turn_json is None:
continue
try:
turn = json.loads(turn_json)
except json.JSONDecodeError:
logger.error(f"Strategie vypsala nevalidní JSON.")
sys.exit(1)
send_turn(turn, round, args)
def get_command(args) -> List[str]:
if args.program is None:
logger.error("Specifikuj program.")
sys.exit(1)
program = os.path.realpath(args.program)
if args.copy:
program = tempfile.mktemp()
shutil.copyfile(args.program, program)
command: List[str] = []
for arg in args.command:
command.append(arg.replace("%", program))
return command
def run_subprocess(
command: List[str], state_json: str, timeout: Optional[int]
) -> Optional[str]:
"""Spusť strategii a vypiš její výstup."""
proc = Popen(command, encoding="utf-8",
stdin=PIPE, stdout=PIPE, stderr=PIPE)
try:
stdout, stderr = proc.communicate(input=state_json, timeout=timeout)
except TimeoutExpired:
proc.kill()
logger.error("Strategie se počítala příliš dlouho.")
return None
if stderr:
logger.error(f"Chybový výstup strategie:\n{stderr}")
if proc.returncode != 0:
logger.error("Strategie skončila s chybovým návratovým kódem.")
sys.exit(1)
return stdout
def get_state(min_round: int, args) -> dict:
"""Opakovaně se pokus získat herní data ze serveru."""
def get_state_once() -> Tuple[Optional[dict], float]:
"""Vrací stav případě úspěchu, jinak None a dobu čekání."""
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 mohl vypadnout, zkus to znovu později
logger.warning(f"Chyba při dotazu na stav: {e}")
return None, TIME_BEFORE_RETRY
if not res.ok:
log_server_error(res)
sys.exit(1)
state = res.json()
if state["status"] == "ok":
logger.info("Nový stav obdržen.")
return state, 0
# musíme chvíli počkat
if state["status"] == "waiting":
logger.info("Server počítá.")
if state["status"] == "too_early":
logger.info("Kolo ještě nezačalo.")
return None, state["wait"]
state, wait_time = get_state_once()
while state is None:
time.sleep(wait_time)
state, wait_time = get_state_once()
return state
def send_turn(turn: dict, round: int, args) -> None:
"""Opakovaně se pokus poslat tah na server."""
def send_turn_once() -> bool:
"""Vrací True pokud server odpověděl."""
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 mohl vypadnout, zkus to znovu později
logger.warning(f"Chyba při posílání tahu: {e}")
return False
if not res.ok:
log_server_error(res)
sys.exit(1)
# vypiš chyby
# tyto chyby jsou způsobeny špatným tahem,
# opakování požadavku nepomůže, takže vracíme True
res_json = res.json()
if res_json["status"] == "ok":
logger.info("Tah úspěšně přijat.")
elif res_json["status"] == "too_late":
logger.error("Tah poslán příliš pozdě.")
elif res_json["status"] == "error":
logger.error(f"Chyba v tahu: {res_json['description']}")
elif res_json["status"] == "warning":
member_warns = "\n".join([
f" {member['id']}: {member['description']}"
for member in res_json["members"]
])
logger.info("Tah přijat s varováními.")
logger.warning(f"Varování pro vojáky:\n{member_warns}")
return True
while not send_turn_once():
time.sleep(TIME_BEFORE_RETRY)
def log_server_error(res: requests.Response) -> None:
res_json = res.json()
logger.error(
f"Chyba serveru: {res.status_code} {res.reason}: "
f"{res_json['description']}"
)
if __name__ == "__main__":
args = parser.parse_args()
main(args)