From e1e2185d22fb9c86e7edd5d105de18b8b4af3631 Mon Sep 17 00:00:00 2001 From: Jakob Zahn Date: Mon, 25 Aug 2025 16:04:52 +0200 Subject: [PATCH 1/4] Add history to `TkViewer` with GUI controls The `STEP` and `ROUND` buttons are replaced with full history navigation controls --- pelita/ui/tk_canvas.py | 59 +++++++++++++++++++++++++++++++++++++----- pelita/ui/tk_viewer.py | 2 ++ 2 files changed, 55 insertions(+), 6 deletions(-) diff --git a/pelita/ui/tk_canvas.py b/pelita/ui/tk_canvas.py index ba885533d..85ebbbb03 100644 --- a/pelita/ui/tk_canvas.py +++ b/pelita/ui/tk_canvas.py @@ -290,6 +290,7 @@ def __init__(self, window, controller_address=None, self.init_bot_sprites([None] * 4) self._game_state = {} + self.history = {} self.ui_game_canvas = tkinter.Canvas(self.window) self.ui_game_canvas.configure(background="white", bd=0, highlightthickness=0, relief='flat') @@ -351,19 +352,22 @@ def __init__(self, window, controller_address=None, **LABEL_STYLE) self.ui_status_selected.pack(side=tkinter.RIGHT) + # previous tkinter.Button(self.ui_status_00, - text="PLAY/PAUSE", - command=self.toggle_running, + text="previous", + command=self.show_previous, **BUTTON_STYLE).pack(side=tkinter.LEFT, expand=True, **BUTTON_PADDING) + # play/pause tkinter.Button(self.ui_status_00, - text="STEP", - command=self.request_step, + text="play/pause", + command=self.toggle_running, **BUTTON_STYLE).pack(side=tkinter.LEFT, expand=True, **BUTTON_PADDING) + # next tkinter.Button(self.ui_status_00, - text="ROUND", - command=self.request_round, + text="next", + command=self.show_next, **BUTTON_STYLE).pack(side=tkinter.LEFT, expand=True, **BUTTON_PADDING) tkinter.Button(self.ui_status_01, @@ -1153,6 +1157,49 @@ def request_step(self): _logger.debug('---> play_step') self.controller_socket.send_json({"__action__": "play_step"}) + def get_current_pointer(self): + GS = self._game_state + + round = GS["round"] + turn = GS["turn"] + + if round is None: + return None + + return (round - 1) * 4 + turn + + def show_previous(self): + # get new game state + current_pointer = self.get_current_pointer() + + if current_pointer is None: + new_pointer = current_pointer + elif current_pointer == 0: + new_pointer = None + else: + new_pointer = current_pointer - 1 + self._game_state = self.history[new_pointer] + + # update ui + self.update() + + def show_next(self): + # get new game state + current_pointer = self.get_current_pointer() + + if current_pointer is None: + new_pointer = 0 + else: + new_pointer = current_pointer + 1 + + if new_pointer not in self.history: + self.request_step() + else: + self._game_state = self.history[new_pointer] + + # update ui + self.update() + def request_round(self): if not self.controller_socket: return diff --git a/pelita/ui/tk_viewer.py b/pelita/ui/tk_viewer.py index 440243d98..1a5d65d70 100644 --- a/pelita/ui/tk_viewer.py +++ b/pelita/ui/tk_viewer.py @@ -153,6 +153,8 @@ def read_queue(self): if game_state: self.app.observe(game_state, self.standalone_mode) + self.app.history[self.app.get_current_pointer()] = game_state + self._delay = 2 self._after(2, self.read_queue) except zmq.Again: From 4ece06753cad9b2141d1e2c78ead28bc2e15b2cd Mon Sep 17 00:00:00 2001 From: Jakob Zahn Date: Mon, 25 Aug 2025 16:28:50 +0200 Subject: [PATCH 2/4] Fix play button Play button continued to play from the end of history, regardless of currently shown game state --- pelita/ui/tk_canvas.py | 37 +++++++++++++++++++++---------------- pelita/ui/tk_viewer.py | 35 ++++++++++++++++++++--------------- 2 files changed, 41 insertions(+), 31 deletions(-) diff --git a/pelita/ui/tk_canvas.py b/pelita/ui/tk_canvas.py index 85ebbbb03..eae0b762e 100644 --- a/pelita/ui/tk_canvas.py +++ b/pelita/ui/tk_canvas.py @@ -1160,6 +1160,9 @@ def request_step(self): def get_current_pointer(self): GS = self._game_state + if not GS: + return None + round = GS["round"] turn = GS["turn"] @@ -1169,33 +1172,35 @@ def get_current_pointer(self): return (round - 1) * 4 + turn def show_previous(self): - # get new game state - current_pointer = self.get_current_pointer() + current = self.get_current_pointer() - if current_pointer is None: - new_pointer = current_pointer - elif current_pointer == 0: - new_pointer = None + if current in (None, 0): + new = None else: - new_pointer = current_pointer - 1 - self._game_state = self.history[new_pointer] + new = current - 1 + + self._game_state = self.history[new] # update ui self.update() - def show_next(self): - # get new game state - current_pointer = self.get_current_pointer() + def get_next_pointer(self): + current = self.get_current_pointer() - if current_pointer is None: - new_pointer = 0 + if current is None: + new = 0 else: - new_pointer = current_pointer + 1 + new = current + 1 + + return new + + def show_next(self): + pointer = self.get_next_pointer() - if new_pointer not in self.history: + if pointer not in self.history: self.request_step() else: - self._game_state = self.history[new_pointer] + self._game_state = self.history[pointer] # update ui self.update() diff --git a/pelita/ui/tk_viewer.py b/pelita/ui/tk_viewer.py index 1a5d65d70..88c0e4203 100644 --- a/pelita/ui/tk_viewer.py +++ b/pelita/ui/tk_viewer.py @@ -140,21 +140,26 @@ def read_queue(self): if self._delay > 100: self._delay = 100 try: - # read all events. - # if queue is empty, try again in a few ms - # we don’t want to block here and lock - # Tk animations - message = self.socket.recv_unicode(flags=zmq.NOBLOCK) - message = json.loads(message) - - _logger.debug(message["__action__"]) - # we currently don’t care about the action - game_state = message["__data__"] - if game_state: - self.app.observe(game_state, self.standalone_mode) - - self.app.history[self.app.get_current_pointer()] = game_state - + next_history_pointer = self.app.get_next_pointer() + if self.app.running and next_history_pointer in self.app.history: + # we are running in history, so just show the next game state + # in history until we run out of states + self.app.show_next() + else: + # read all events. + # if queue is empty, try again in a few ms + # we don’t want to block here and lock + # Tk animations + message = self.socket.recv_unicode(flags=zmq.NOBLOCK) + message = json.loads(message) + + _logger.debug(message["__action__"]) + # we currently don’t care about the action + game_state = message["__data__"] + + if game_state: + self.app.observe(game_state, self.standalone_mode) + self.app.history[self.app.get_current_pointer()] = game_state self._delay = 2 self._after(2, self.read_queue) except zmq.Again: From 75c1e085f94f7544f13bf1d5cb6803f12b3e24df Mon Sep 17 00:00:00 2001 From: Jakob Zahn Date: Mon, 25 Aug 2025 16:29:48 +0200 Subject: [PATCH 3/4] Add comments and docstrings to history navigation functions --- pelita/ui/tk_canvas.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/pelita/ui/tk_canvas.py b/pelita/ui/tk_canvas.py index eae0b762e..68bcf6de8 100644 --- a/pelita/ui/tk_canvas.py +++ b/pelita/ui/tk_canvas.py @@ -1160,31 +1160,46 @@ def request_step(self): def get_current_pointer(self): GS = self._game_state + # the game state might be empty; + # happens before any message has been received if not GS: return None round = GS["round"] turn = GS["turn"] + # the round is None on INIT game phase; + # return this game state to be saved under key `None` if round is None: return None + # convert 1-indexed round to 0-indexed, convert to total turns + # played and add the turns in the current round return (round - 1) * 4 + turn def show_previous(self): + """ + Show the previous game step. + """ current = self.get_current_pointer() if current in (None, 0): + # we are either in the first or second game state recorded; + # so the previous step is always the first game state new = None else: new = current - 1 + # set the currently displayed game state self._game_state = self.history[new] # update ui self.update() def get_next_pointer(self): + """ + Get the pointer to the next game step. + """ current = self.get_current_pointer() if current is None: @@ -1195,11 +1210,17 @@ def get_next_pointer(self): return new def show_next(self): + """ + Show the next step either from history or message queue. + """ pointer = self.get_next_pointer() if pointer not in self.history: + # this gamestate is not existing yet; + # we need to get it from the message queue self.request_step() else: + # set the currently displayed game state self._game_state = self.history[pointer] # update ui From 2f5ea92576e8516d2ff0d84bf338ed6a63fa9f8d Mon Sep 17 00:00:00 2001 From: Jakob Zahn Date: Sat, 23 Aug 2025 18:31:33 +0200 Subject: [PATCH 4/4] Pause the game on history navigation automatically --- pelita/ui/tk_canvas.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/pelita/ui/tk_canvas.py b/pelita/ui/tk_canvas.py index 68bcf6de8..130dcadaf 100644 --- a/pelita/ui/tk_canvas.py +++ b/pelita/ui/tk_canvas.py @@ -355,7 +355,7 @@ def __init__(self, window, controller_address=None, # previous tkinter.Button(self.ui_status_00, text="previous", - command=self.show_previous, + command=self.button_show_previous, **BUTTON_STYLE).pack(side=tkinter.LEFT, expand=True, **BUTTON_PADDING) # play/pause @@ -367,7 +367,7 @@ def __init__(self, window, controller_address=None, # next tkinter.Button(self.ui_status_00, text="next", - command=self.show_next, + command=self.button_show_next, **BUTTON_STYLE).pack(side=tkinter.LEFT, expand=True, **BUTTON_PADDING) tkinter.Button(self.ui_status_01, @@ -1226,6 +1226,16 @@ def show_next(self): # update ui self.update() + def button_show_previous(self): + # put game in pause automatically when pushing the button + self.running = False + self.show_previous() + + def button_show_next(self): + # put game in pause automatically when pushing the button + self.running = False + self.show_next() + def request_round(self): if not self.controller_socket: return