diff --git a/pelita/game.py b/pelita/game.py index 5651ae884..f90597821 100644 --- a/pelita/game.py +++ b/pelita/game.py @@ -16,7 +16,7 @@ from .exceptions import NoFoodWarning, PelitaBotError, PelitaIllegalGameState from .gamestate_filters import noiser, relocate_expired_food, update_food_age, in_homezone from .layout import get_legal_positions, initial_positions -from .network import Controller, RemotePlayerFailure, RemotePlayerRecvTimeout, RemotePlayerSendError, ZMQPublisher +from .network import Controller, RemotePlayerFailure, RemotePlayerRecvTimeout, RemotePlayerSendError, ZMQPublisher, POSTPublisher from .team import RemoteTeam, make_team from .viewer import (AsciiViewer, ProgressViewer, ReplayWriter, ReplyToViewer, ResultPrinter) @@ -274,6 +274,9 @@ def setup_viewers(viewers, print_result=True): zmq_context = zmq.Context() zmq_external_publisher = ZMQPublisher(address=viewer_opts, bind=False, zmq_context=zmq_context) viewer_state['viewers'].append(zmq_external_publisher) + elif viewer == 'http-post-to': + post_publisher = POSTPublisher(address=viewer_opts) + viewer_state['viewers'].append(post_publisher) elif viewer == 'tk': zmq_context = zmq.Context() zmq_publisher = ZMQPublisher(address='tcp://127.0.0.1', zmq_context=zmq_context) @@ -419,6 +422,8 @@ def setup_game(team_specs, *, layout_dict, max_rounds=300, rng=None, #: Name of the teams. Tuple of str team_names=team_names, + team_specs=team_specs, + #: Additional team info. Tuple of str|None team_infos=team_infos, diff --git a/pelita/network.py b/pelita/network.py index ca5dd7438..f98befac2 100644 --- a/pelita/network.py +++ b/pelita/network.py @@ -333,6 +333,31 @@ def recv_timeout(self, expected_id, timeout): def __repr__(self): return "RemotePlayerConnection(%r)" % self.socket + +class POSTPublisher: + """ A viewer which dumps to a given stream. + """ + def __init__(self, address): + import httpx + self.url = address + self.http_session = httpx.Client() + + def _send(self, action, data): + # import requests + + info = {'round': data['round'], 'turn': data['turn']} + # TODO: this should be game_phase + if data['gameover']: + info['gameover'] = True + _logger.debug(f"--#> [{action}] %r", info) + message = {"__action__": action, "__data__": data} + as_json = json.dumps(message, cls=SetEncoder) + self.http_session.post(self.url, content=as_json) + + def show_state(self, game_state): + self._send(action="observe", data=game_state) + + class ZMQPublisher: """ Sets up a simple Publisher which sends all viewed events over a zmq connection. diff --git a/pelita/scripts/pelita_main.py b/pelita/scripts/pelita_main.py index 4dbdaf559..820a68779 100755 --- a/pelita/scripts/pelita_main.py +++ b/pelita/scripts/pelita_main.py @@ -283,6 +283,8 @@ def long_help(s): help=long_help('Communicate the result of the game on this channel.')) advanced_settings.add_argument('--publish', type=str, metavar='URL', dest='publish_to', help=long_help('Publish the game to this zmq socket.')) +advanced_settings.add_argument('--http-post', type=str, metavar='URL', dest='http_post_to', + help=long_help('POST the game to this http socket.')) advanced_settings.add_argument('--controller', type=str, metavar='URL', default="tcp://127.0.0.1", help=long_help('Channel for controlling the game.')) @@ -373,6 +375,8 @@ def main(): viewers.append(('reply-to', args.reply_to)) if args.publish_to: viewers.append(('publish-to', args.publish_to)) + if args.http_post_to: + viewers.append(('http-post-to', args.http_post_to)) if args.write_replay: viewers.append(('write-replay-to', args.write_replay)) diff --git a/pelita/scripts/pelita_tournament.py b/pelita/scripts/pelita_tournament.py index 7515561c1..f0cb9f733 100755 --- a/pelita/scripts/pelita_tournament.py +++ b/pelita/scripts/pelita_tournament.py @@ -163,6 +163,12 @@ def setup(): del config["bonusmatch"] break + res = input_choice("Should the web-viewer be activated? (publishes to http://localhost:3000/api/collect) (y/n)", [], "yn") + if res == "y": + config['publish'] = "http://localhost:3000/api/collect" + elif res == "n": + config['publish'] = None + print("Specify the folder where we should look for teams (or none)") folder = input().strip() if folder: @@ -294,6 +300,7 @@ def escape(s): winner = tournament.play_round2(config, rr_ranking, state, rng) config.print('The winner of the %s Pelita tournament is...' % config.location, wait=2, end=" ") + config.print() config.print('{team_group}: {team_name}. Congratulations'.format( team_group=config.team_group(winner), team_name=config.team_name(winner)), wait=2) diff --git a/pelita/tournament/__init__.py b/pelita/tournament/__init__.py index a2193edac..0fe3546c9 100644 --- a/pelita/tournament/__init__.py +++ b/pelita/tournament/__init__.py @@ -12,6 +12,7 @@ import time from dataclasses import dataclass +import httpx import yaml import zmq @@ -98,7 +99,8 @@ def run_and_terminate_process(args, **kwargs): p.kill() -def call_pelita(team_specs, *, rounds, size, viewer, seed, team_infos=None, write_replay=False, store_output=False): +def call_pelita(team_specs, *, rounds, size, viewer, seed, publish=None, + team_infos=None, write_replay=False, store_output=False): """ Starts a new process with the given command line arguments and waits until finished. Returns @@ -134,6 +136,7 @@ def call_pelita(team_specs, *, rounds, size, viewer, seed, team_infos=None, writ size = ['--size', size] if size else [] viewer = ['--' + viewer] if viewer else [] seed = ['--seed', seed] if seed else [] + publish = ['--http-post', publish] if publish else [] write_replay = ['--write-replay', write_replay] if write_replay else [] store_output = ['--store-output', store_output] if store_output else [] append_blue = ['--append-blue', team_infos[0]] if team_infos[0] else [] @@ -142,6 +145,8 @@ def call_pelita(team_specs, *, rounds, size, viewer, seed, team_infos=None, writ cmd = [sys.executable, '-m', 'pelita.scripts.pelita_main', team1, team2, '--reply-to', reply_addr, + '--stop-at', '0', + *publish, *append_blue, *append_red, *rounds, @@ -260,7 +265,8 @@ def __init__(self, config): self.teams[team_id] = { "spec": team_spec, "name": team_name, - "members": team["members"] + "members": team["members"], + "color": team.get("color"), } self.location = config["location"] @@ -270,6 +276,7 @@ def __init__(self, config): self.size = config.get("size") self.viewer = config.get("viewer") + self.publish = config.get("publish") self.interactive = config.get("interactive") self.statefile = config.get("statefile") @@ -290,6 +297,11 @@ def __init__(self, config): self.tournament_log_folder = None self.tournament_log_file = None + if self.publish: + self.http_session = httpx.Client() + else: + self.http_session = None + @property def team_ids(self): return self.teams.keys() @@ -306,6 +318,16 @@ def team_name_group(self, team): def team_spec(self, team): return self.teams[team]["spec"] + def send_remote(self, action, data=None): + if not self.http_session: + return + + if data is None: + publish_string = {"__action__": action} + else: + publish_string = {"__action__": action, "__data__": data} + self.http_session.post(self.publish, content=json.dumps(publish_string)) + def _print(self, *args, **kwargs): print(*args, **kwargs) if self.tournament_log_file: @@ -317,12 +339,15 @@ def print(self, *args, **kwargs): """Speak while you print. To disable set speak=False. You need the program %s to be able to speak. Set wait=X to wait X seconds after speaking.""" + if len(args) == 0: + self.send_remote("SPEAK", " ".join(args)) self._print() return stream = io.StringIO() wait = kwargs.pop('wait', 0.5) want_speak = kwargs.pop('speak', None) + self.send_remote("SPEAK", " ".join(args)) if (want_speak is False) or not self.speak: self._print(*args, **kwargs) else: @@ -382,6 +407,28 @@ def input(self, str, values=None): except IndexError: pass + def metadata(self): + return { + 'teams': self.teams, + 'location': self.location, + 'date': self.date, + 'rounds': self.rounds, + 'size': self.size, + 'greeting': self.greeting, + 'farewell': self.farewell, + 'host': self.host, + 'seed': self.seed, + 'bonusmatch': self.bonusmatch + } + + def init_tournament(self): + metadata = self.metadata() + print("Sending tournament metadata to the server:") + print(metadata) + self.send_remote("INIT", metadata) + + def clear_page(self): + self.send_remote("CLEAR") def wait_for_keypress(self): if self.interactive: @@ -424,7 +471,9 @@ def load(cls, config, filename): def present_teams(config): + config.init_tournament() config.wait_for_keypress() + config.clear_page() print("\33[H\33[2J") # clear the screen greeting = config.greeting @@ -453,8 +502,11 @@ def set_name(team): print(sys.stderr) raise - -def play_game_with_config(config, teams, rng, *, match_id=None): +# TODO: Log tournament match cmdline +def play_game_with_config(config: Config, teams, rng, *, match_id=None): + config.clear_page() + metadata = config.metadata() + config.send_remote("INIT", metadata) team1, team2 = teams if config.tournament_log_folder: @@ -479,6 +531,7 @@ def play_game_with_config(config, teams, rng, *, match_id=None): rounds=config.rounds, size=config.size, viewer=config.viewer, + publish=config.publish, team_infos=team_infos, seed=seed, **log_kwargs) @@ -508,6 +561,7 @@ def start_match(config, teams, rng, *, shuffle=False, match_id=None): config.print('Starting match: '+ config.team_name_group(team1)+' vs ' + config.team_name_group(team2)) config.print() config.wait_for_keypress() + config.clear_page() (final_state, stdout, stderr) = play_game_with_config(config, teams, rng=rng, match_id=match_id) try: @@ -626,6 +680,7 @@ def play_round1(config, state, rng): rr_played = state.round1["played"] config.wait_for_keypress() + config.clear_page() config.print() config.print("ROUND 1 (Everybody vs Everybody)") config.print('================================', speak=False) @@ -655,6 +710,7 @@ def play_round1(config, state, rng): winner = start_match_with_replay(config, match, rng=rng, match_id=match_id) match_id.next_match() config.wait_for_keypress() + config.clear_page() if winner is False or winner is None: rr_played.append({ "match": match, "winner": False }) @@ -697,9 +753,11 @@ def recur_match_winner(match): def play_round2(config, teams, state, rng): """Run the second round and return the name of the winning team. - teams is the list [group0, group1, ...] not the names of the agens, sorted + teams is the list [group0, group1, ...] not the names of the agents, sorted by the result of the first round. """ + config.wait_for_keypress() + config.clear_page() config.print() config.print('ROUND 2 (K.O.)') config.print('==============', speak=False) @@ -729,6 +787,7 @@ def play_round2(config, teams, state, rng): winner = start_deathmatch(config, t1_id, t2_id, rng=rng, match_id=match_id) match.winner = winner + config.clear_page() config.print(knockout_mode.print_knockout(last_match, config.team_name, highlight=[match]), speak=False) state.round2["tournament"] = tournament @@ -741,5 +800,6 @@ def play_round2(config, teams, state, rng): match_id.next_match() config.wait_for_keypress() + config.clear_page() return last_match.winner diff --git a/pyproject.toml b/pyproject.toml index 9d0a20a05..5a3180a18 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -32,6 +32,7 @@ dependencies = [ "zeroconf", "rich", "click", + "httpx" ] dynamic = ["version"] diff --git a/test/test_tournament.py b/test/test_tournament.py index 27f21c60a..26e2f7b32 100644 --- a/test/test_tournament.py +++ b/test/test_tournament.py @@ -297,6 +297,7 @@ def test_play_game_with_config(self): config.viewer = 'ascii' config.size = 'small' config.tournament_log_folder = None + config.publish = None teams = ["pelita/player/StoppingPlayer", "pelita/player/StoppingPlayer"] (state, stdout, stderr) = tournament.play_game_with_config(config, teams, rng=RNG) @@ -338,6 +339,7 @@ def mock_print(str="", *args, **kwargs): config.size = 'small' config.print = mock_print config.tournament_log_folder = None + config.publish = None team_ids = ["first_id", "first_id"] result = tournament.start_match(config, team_ids, rng=RNG) @@ -376,6 +378,7 @@ def mock_print(str="", *args, **kwargs): config.size = 'small' config.print = mock_print config.tournament_log_folder = None + config.publish = None result = tournament.start_deathmatch(config, *teams.keys(), rng=RNG) assert result is not None diff --git a/tournament.yaml b/tournament.yaml index c3c48fb67..d1e505a09 100644 --- a/tournament.yaml +++ b/tournament.yaml @@ -3,22 +3,17 @@ location: Munich date: 2015 seed: null bonusmatch: True +#publish: http://localhost:3000/api/collect teams: - spec: pelita/player/StoppingPlayer members: - "Stopper" + color: "#eee" - spec: pelita/player/FoodEatingPlayer members: - "Food Eater" + color: "#e00" - spec: pelita/player/RandomExplorerPlayer members: - "Random Explorer" - - spec: pelita/player/RandomPlayers - members: - - "Random Player" - - spec: pelita/player/SmartEatingPlayer - members: - - "Smart Eating Player" - - spec: pelita/player/SmartRandomPlayer - members: - - "Smarter Random Player" + color: "#e0e"