From 91396c9144a1e9019c03f1c075879bbf7b55d347 Mon Sep 17 00:00:00 2001 From: Rike-Benjamin Schuppner Date: Thu, 18 Jul 2024 15:05:44 +0200 Subject: [PATCH 1/5] WIP: Adding publish mode and handling to pelita tournament --- pelita/scripts/pelita_tournament.py | 7 +++++ pelita/tournament/__init__.py | 48 ++++++++++++++++++++++++++--- 2 files changed, 51 insertions(+), 4 deletions(-) diff --git a/pelita/scripts/pelita_tournament.py b/pelita/scripts/pelita_tournament.py index 7515561c1..763a78d61 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 tcp://127.0.0.1:5559) (y/n)", [], "yn") + if res == "y": + config['publish'] = "tcp://127.0.0.1:5559" + 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..3ee1d473d 100644 --- a/pelita/tournament/__init__.py +++ b/pelita/tournament/__init__.py @@ -98,7 +98,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 +135,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 = ['--publish', 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 +144,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, @@ -270,6 +274,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 +295,13 @@ def __init__(self, config): self.tournament_log_folder = None self.tournament_log_file = None + if self.publish: + ctx = zmq.Context() + self.socket = ctx.socket(zmq.PUB) + self.socket.connect(self.publish) + else: + self.socket = None + @property def team_ids(self): return self.teams.keys() @@ -306,6 +318,15 @@ 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.socket: + return + if data is None: + publish_string = {"__action__": action} + else: + publish_string = {"__action__": action, "__data__": data} + self.socket.send_json(publish_string) + def _print(self, *args, **kwargs): print(*args, **kwargs) if self.tournament_log_file: @@ -317,12 +338,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 +406,11 @@ def input(self, str, values=None): except IndexError: pass + def init_tournament(self): + self.send_remote("INIT") + + def clear_page(self): + self.send_remote("CLEAR") def wait_for_keypress(self): if self.interactive: @@ -424,7 +453,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 +484,9 @@ 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() team1, team2 = teams if config.tournament_log_folder: @@ -479,6 +511,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 +541,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 +660,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 +690,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 +733,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 +767,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 +780,6 @@ def play_round2(config, teams, state, rng): match_id.next_match() config.wait_for_keypress() + config.clear_page() return last_match.winner From 9f9977f24277881f404bee76e6d6646c5c82220a Mon Sep 17 00:00:00 2001 From: Rike-Benjamin Schuppner Date: Fri, 18 Jul 2025 20:22:32 +0200 Subject: [PATCH 2/5] TST: Fix tests --- test/test_tournament.py | 3 +++ 1 file changed, 3 insertions(+) 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 From 4b39fbcee17a96850a92ea9beb7661c854167bda Mon Sep 17 00:00:00 2001 From: Rike-Benjamin Schuppner Date: Fri, 18 Jul 2025 20:33:13 +0200 Subject: [PATCH 3/5] NF: Send tournament metadata to the tournament viewer --- pelita/game.py | 2 ++ pelita/tournament/__init__.py | 21 ++++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/pelita/game.py b/pelita/game.py index 5651ae884..2e7991854 100644 --- a/pelita/game.py +++ b/pelita/game.py @@ -419,6 +419,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/tournament/__init__.py b/pelita/tournament/__init__.py index 3ee1d473d..80dddeb3f 100644 --- a/pelita/tournament/__init__.py +++ b/pelita/tournament/__init__.py @@ -406,8 +406,25 @@ 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): - self.send_remote("INIT") + 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") @@ -487,6 +504,8 @@ def set_name(team): # 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: From 8c2cb0c2455f05d282249ec431c84301a9638b16 Mon Sep 17 00:00:00 2001 From: Rike-Benjamin Schuppner Date: Fri, 17 Apr 2026 11:35:07 +0200 Subject: [PATCH 4/5] ENH: Add colour --- pelita/tournament/__init__.py | 3 ++- tournament.yaml | 13 ++++--------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/pelita/tournament/__init__.py b/pelita/tournament/__init__.py index 80dddeb3f..f8747c1d3 100644 --- a/pelita/tournament/__init__.py +++ b/pelita/tournament/__init__.py @@ -264,7 +264,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"] diff --git a/tournament.yaml b/tournament.yaml index c3c48fb67..34ac9ab42 100644 --- a/tournament.yaml +++ b/tournament.yaml @@ -3,22 +3,17 @@ location: Munich date: 2015 seed: null bonusmatch: True +publish: tcp://127.0.0.1:5559 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" From c8c11604919eedbff826e8a67b57b74e7fbd95e1 Mon Sep 17 00:00:00 2001 From: Rike-Benjamin Schuppner Date: Tue, 14 Apr 2026 16:26:43 +0200 Subject: [PATCH 5/5] NF: Introduce HTTP POST publishing --- pelita/game.py | 5 ++++- pelita/network.py | 25 +++++++++++++++++++++++++ pelita/scripts/pelita_main.py | 4 ++++ pelita/scripts/pelita_tournament.py | 4 ++-- pelita/tournament/__init__.py | 14 +++++++------- pyproject.toml | 1 + tournament.yaml | 2 +- 7 files changed, 44 insertions(+), 11 deletions(-) diff --git a/pelita/game.py b/pelita/game.py index 2e7991854..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) 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 763a78d61..f0cb9f733 100755 --- a/pelita/scripts/pelita_tournament.py +++ b/pelita/scripts/pelita_tournament.py @@ -163,9 +163,9 @@ def setup(): del config["bonusmatch"] break - res = input_choice("Should the web-viewer be activated? (publishes to tcp://127.0.0.1:5559) (y/n)", [], "yn") + res = input_choice("Should the web-viewer be activated? (publishes to http://localhost:3000/api/collect) (y/n)", [], "yn") if res == "y": - config['publish'] = "tcp://127.0.0.1:5559" + config['publish'] = "http://localhost:3000/api/collect" elif res == "n": config['publish'] = None diff --git a/pelita/tournament/__init__.py b/pelita/tournament/__init__.py index f8747c1d3..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 @@ -135,7 +136,7 @@ def call_pelita(team_specs, *, rounds, size, viewer, seed, publish=None, size = ['--size', size] if size else [] viewer = ['--' + viewer] if viewer else [] seed = ['--seed', seed] if seed else [] - publish = ['--publish', publish] if publish 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 [] @@ -297,11 +298,9 @@ def __init__(self, config): self.tournament_log_file = None if self.publish: - ctx = zmq.Context() - self.socket = ctx.socket(zmq.PUB) - self.socket.connect(self.publish) + self.http_session = httpx.Client() else: - self.socket = None + self.http_session = None @property def team_ids(self): @@ -320,13 +319,14 @@ def team_spec(self, team): return self.teams[team]["spec"] def send_remote(self, action, data=None): - if not self.socket: + if not self.http_session: return + if data is None: publish_string = {"__action__": action} else: publish_string = {"__action__": action, "__data__": data} - self.socket.send_json(publish_string) + self.http_session.post(self.publish, content=json.dumps(publish_string)) def _print(self, *args, **kwargs): print(*args, **kwargs) 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/tournament.yaml b/tournament.yaml index 34ac9ab42..d1e505a09 100644 --- a/tournament.yaml +++ b/tournament.yaml @@ -3,7 +3,7 @@ location: Munich date: 2015 seed: null bonusmatch: True -publish: tcp://127.0.0.1:5559 +#publish: http://localhost:3000/api/collect teams: - spec: pelita/player/StoppingPlayer members: