From 67cf01465c0a9e2359b8b34a88c7d50995fbe6af Mon Sep 17 00:00:00 2001 From: Andrey Dubilyer <92729835+bananabr3d@users.noreply.github.com> Date: Fri, 8 Mar 2024 17:32:44 +0100 Subject: [PATCH 01/77] Update Client/profile_save.py Co-authored-by: Julian Lemmerich <41118534+JM-Lemmi@users.noreply.github.com> --- Client/profile_save.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/Client/profile_save.py b/Client/profile_save.py index 22435bd..53c06d1 100644 --- a/Client/profile_save.py +++ b/Client/profile_save.py @@ -47,23 +47,24 @@ def get_profile(self,profile_uuid): except: print("json error: Make sure profiles.json is formatted correctly") return None - def set_profile(self, profile_uuid , profile_name = None, profile_color = None): + def set_profile(self, profile_uuid , profile_name, profile_color): """ This method sets the profile name and/or color by the uuid :param profile_uuid: :param profile_name: *optional* :param profile_color: *optional* """ + if profile_name == None || profile_color == None: + raise ValueError("name or color cannot be none") + if self.check_file(): try: with open(path, 'r+') as file: data = json.load(file) for profile in data: if profile["profile_uuid"] == profile_uuid: - if profile_name != None: - profile["profile_name"] = profile_name - if profile_color != None: - profile["profile_color"] = profile_color + profile["profile_name"] = profile_name + profile["profile_color"] = profile_color break with open(path, 'w') as file: json.dump(data, file) From 40893e836e8912494d3aaa10d7e1147618e00743 Mon Sep 17 00:00:00 2001 From: bananabr3d Date: Fri, 8 Mar 2024 18:20:44 +0100 Subject: [PATCH 02/77] fix: TTTK-19 profile_save.py: merged feat: TTT-19 profile_save.py: throws now errors if profile_color or profile_name is None, delete Functions added --- Client/profile_save.py | 105 +++++++++++++++++++++++++++++++---------- 1 file changed, 80 insertions(+), 25 deletions(-) diff --git a/Client/profile_save.py b/Client/profile_save.py index 53c06d1..534845e 100644 --- a/Client/profile_save.py +++ b/Client/profile_save.py @@ -1,19 +1,21 @@ import json from os.path import exists -# Path to the profiles.json file -path = ('../json_schema/profiles.json') class Profile: """ This class is used to handle the profiles.json file. It is used to get, set, and add profiles to the file. """ + + def __init__(self, path='../json_schema/profiles.json'): + self.path = path + def check_file(self): """ This method checks if the file exists :return: True if the file exists, False if it does not """ - if exists(path): + if exists(self.path): print("found") return True else: @@ -26,12 +28,13 @@ def get_profiles(self): :return: An array of all profiles """ if self.check_file(): - with open(path, 'r') as file: + with open(self.path, 'r') as file: data = json.load(file) return data else: return None - def get_profile(self,profile_uuid): + + def get_profile(self, profile_uuid): """ This method returns a profile by its uuid :param profile_uuid: @@ -39,7 +42,7 @@ def get_profile(self,profile_uuid): """ if self.check_file(): try: - with open(path, 'r') as file: + with open(self.path, 'r') as file: data = json.load(file) for profile in data: if profile["profile_uuid"] == profile_uuid: @@ -47,30 +50,34 @@ def get_profile(self,profile_uuid): except: print("json error: Make sure profiles.json is formatted correctly") return None - def set_profile(self, profile_uuid , profile_name, profile_color): + + def set_profile(self, profile_uuid, profile_name, profile_color): """ This method sets the profile name and/or color by the uuid :param profile_uuid: - :param profile_name: *optional* - :param profile_color: *optional* + :param profile_name: + :param profile_color: """ - if profile_name == None || profile_color == None: + if (profile_name or profile_color) == None: raise ValueError("name or color cannot be none") - + if self.check_file(): try: - with open(path, 'r+') as file: + with open(self.path, 'r+') as file: data = json.load(file) for profile in data: if profile["profile_uuid"] == profile_uuid: - profile["profile_name"] = profile_name - profile["profile_color"] = profile_color + if profile_name != None: + profile["profile_name"] = profile_name + if profile_color != None: + profile["profile_color"] = profile_color break - with open(path, 'w') as file: + with open(self.path, 'w') as file: json.dump(data, file) except: print("json error: Make sure profiles.json is formatted correctly") return None + def get_profile_by_name(self, profile_name): if self.check_file(): """ @@ -79,7 +86,7 @@ def get_profile_by_name(self, profile_name): :return: profile matching given name """ try: - with open(path, 'r') as file: + with open(self.path, 'r') as file: data = json.load(file) for profile in data: if profile["profile_name"] == profile_name: @@ -98,7 +105,7 @@ def add_new_profile(self, profile_name, profile_uuid, profile_color): if self.check_file(): entry = {"profile_name": profile_name, "profile_uuid": profile_uuid, "profile_color": profile_color} try: - with open(path, 'r+') as file: + with open(self.path, 'r+') as file: data = json.load(file) file.seek(0) data.append(entry) @@ -108,14 +115,62 @@ def add_new_profile(self, profile_name, profile_uuid, profile_color): print("json error: Make sure profiles.json is formatted correctly") else: - with open(path, 'w') as file: + with open(self.path, 'w') as file: entry = [{"profile_name": profile_name, "profile_uuid": profile_uuid, "profile_color": profile_color}] json.dump(entry, file) -#Testing -#profile = Profile() -#profile.add_new_profile("test", "test", "test") -#print(profile.get_profiles()) -#print(profile.get_profile("test")) -#profile.set_profile("test", "test2", "test3") -#print(profile.get_profiles()) + def delete_profile(self, profile_uuid): + """ + This method deletes a profile by its uuid + :param profile_uuid: + """ + if self.check_file(): + try: + with open(self.path, 'r+') as file: + data = json.load(file) + for profile in data: + if profile["profile_uuid"] == profile_uuid: + data.remove(profile) + break + else: + raise ValueError(f"Profile with given uuid: {profile_uuid} not found") + with open(self.path, 'w') as file: + json.dump(data, file) + except: + print("json error: Make sure profiles.json is formatted correctly") + + def delete_profile_by_name(self, profile_name): + """ + This method deletes a profile by its name + :param profile_name: + """ + if self.check_file(): + try: + with open(self.path, 'r+') as file: + data = json.load(file) + for profile in data: + if profile["profile_name"] == profile_name: + data.remove(profile) + break + else: + raise ValueError(f"Profile with given name: {profile_name} not found") + with open(self.path, 'w') as file: + json.dump(data, file) + except: + print("json error: Make sure profiles.json is formatted correctly") + + def delete_all_profiles(self): + """ + This method deletes all profiles + """ + if self.check_file(): + with open(self.path, 'w') as file: + file.write("[]") + +# Testing +# profile = Profile() +# profile.add_new_profile("test", "test", "test") +# print(profile.get_profiles()) +# print(profile.get_profile("test")) +# profile.set_profile("test", "test2", "test3") +# print(profile.get_profiles()) From 6fc1f407ab9376d1e55767e5dc70046c0ab21ee6 Mon Sep 17 00:00:00 2001 From: bananabr3d Date: Fri, 8 Mar 2024 18:22:52 +0100 Subject: [PATCH 03/77] fix: TTTK-19 profile_save.py: prints in check_file() deleted --- Client/profile_save.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/Client/profile_save.py b/Client/profile_save.py index 534845e..3bff6fd 100644 --- a/Client/profile_save.py +++ b/Client/profile_save.py @@ -16,10 +16,8 @@ def check_file(self): :return: True if the file exists, False if it does not """ if exists(self.path): - print("found") return True else: - print("not found") return False def get_profiles(self): From 4350f64ffff9c83fa62e8af80f48a4447cf17e4a Mon Sep 17 00:00:00 2001 From: bananabr3d Date: Fri, 8 Mar 2024 18:24:38 +0100 Subject: [PATCH 04/77] feat: TTTK-19 test_profile_save.py: file created, class set up, first two tests written --- Client/test_profile_save.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 Client/test_profile_save.py diff --git a/Client/test_profile_save.py b/Client/test_profile_save.py new file mode 100644 index 0000000..fe41984 --- /dev/null +++ b/Client/test_profile_save.py @@ -0,0 +1,24 @@ +import unittest +from profile_save import Profile + + +class TestProfileSave(unittest.TestCase): + + @classmethod + def setUpClass(cls): + cls.profile = Profile("../json_schema/test/test_profiles.json") + + def setUp(self): + self.profile.delete_all_profiles() + + def test_get_profile_by_name(self): + self.profile.add_new_profile("test", "test", "test") + self.assertEqual(self.profile.get_profile_by_name("test"), + {"profile_name": "test", "profile_uuid": "test", "profile_color": "test"}) + self.profile.delete_all_profiles() + + def test_get_profile_by_uuid(self): + self.profile.add_new_profile("test", "test", "test") + self.assertEqual(self.profile.get_profile("test"), + {"profile_name": "test", "profile_uuid": "test", "profile_color": "test"}) + self.profile.delete_all_profiles() \ No newline at end of file From 59c3a61cb6ca4f73f6b768f01a0cb03c2883e605 Mon Sep 17 00:00:00 2001 From: bananabr3d Date: Fri, 8 Mar 2024 18:25:21 +0100 Subject: [PATCH 05/77] feat: TTTK-19 test_profile.json: file created --- json_schema/test/test_profiles.json | 1 + 1 file changed, 1 insertion(+) create mode 100644 json_schema/test/test_profiles.json diff --git a/json_schema/test/test_profiles.json b/json_schema/test/test_profiles.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/json_schema/test/test_profiles.json @@ -0,0 +1 @@ +[] \ No newline at end of file From d3306771de0ed966f6151b44f6039e057fd62d6a Mon Sep 17 00:00:00 2001 From: bananabr3d Date: Tue, 12 Mar 2024 11:31:16 +0100 Subject: [PATCH 06/77] feat: test_profile_save.py tests implemented --- Client/test_profile_save.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/Client/test_profile_save.py b/Client/test_profile_save.py index fe41984..e44ac6d 100644 --- a/Client/test_profile_save.py +++ b/Client/test_profile_save.py @@ -1,6 +1,7 @@ import unittest from profile_save import Profile +#todo kda durch 0 teilen fixen class TestProfileSave(unittest.TestCase): @@ -16,9 +17,31 @@ def test_get_profile_by_name(self): self.assertEqual(self.profile.get_profile_by_name("test"), {"profile_name": "test", "profile_uuid": "test", "profile_color": "test"}) self.profile.delete_all_profiles() + self.assertEqual(self.profile.get_profiles(), []) def test_get_profile_by_uuid(self): self.profile.add_new_profile("test", "test", "test") self.assertEqual(self.profile.get_profile("test"), {"profile_name": "test", "profile_uuid": "test", "profile_color": "test"}) - self.profile.delete_all_profiles() \ No newline at end of file + self.profile.delete_all_profiles() + self.assertEqual(self.profile.get_profiles(), []) + def test_add_new_profile(self): + self.profile.add_new_profile("test", "test", "test") + self.assertEqual(self.profile.get_profile("test"), + {"profile_name": "test", "profile_uuid": "test", "profile_color": "test"}) + self.profile.delete_all_profiles() + self.assertEqual(self.profile.get_profiles(), []) + + def test_delete_profile(self): + self.profile.add_new_profile("test", "test", "test") + self.profile.add_new_profile("test2", "test", "test") + self.profile.delete_profile("test") + self.assertEqual(self.profile.get_profiles(), [{"profile_name": "test2", "profile_uuid": "test", "profile_color": "test"}]) + self.profile.delete_all_profiles() + + def test_delete_profile_by_name(self): + self.profile.add_new_profile("test", "test", "test") + self.profile.add_new_profile("test2", "test", "test") + self.profile.delete_profile_by_name("test") + self.assertEqual(self.profile.get_profiles(), [{"profile_name": "test2", "profile_uuid": "test", "profile_color": "test"}]) + self.profile.delete_all_profiles() From 5386438b531d6cec33e018154e3c5b7c4ecd50ff Mon Sep 17 00:00:00 2001 From: bananabr3d Date: Tue, 12 Mar 2024 13:43:19 +0100 Subject: [PATCH 07/77] fix: test_profile_save.py, abs path fixed, import fixed fix: profile_save.py: abs path added --- Client/profile_save.py | 11 ++--------- Client/test_profile_save.py | 5 +++-- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/Client/profile_save.py b/Client/profile_save.py index 3bff6fd..54b1fb0 100644 --- a/Client/profile_save.py +++ b/Client/profile_save.py @@ -1,5 +1,5 @@ import json -from os.path import exists +from os.path import exists, abspath class Profile: @@ -7,7 +7,7 @@ class Profile: This class is used to handle the profiles.json file. It is used to get, set, and add profiles to the file. """ - def __init__(self, path='../json_schema/profiles.json'): + def __init__(self, path=abspath('../json_schema/profiles.json')): self.path = path def check_file(self): @@ -165,10 +165,3 @@ def delete_all_profiles(self): with open(self.path, 'w') as file: file.write("[]") -# Testing -# profile = Profile() -# profile.add_new_profile("test", "test", "test") -# print(profile.get_profiles()) -# print(profile.get_profile("test")) -# profile.set_profile("test", "test2", "test3") -# print(profile.get_profiles()) diff --git a/Client/test_profile_save.py b/Client/test_profile_save.py index e44ac6d..f7c8141 100644 --- a/Client/test_profile_save.py +++ b/Client/test_profile_save.py @@ -1,5 +1,6 @@ import unittest -from profile_save import Profile +from Client.profile_save import Profile +import os #todo kda durch 0 teilen fixen @@ -7,7 +8,7 @@ class TestProfileSave(unittest.TestCase): @classmethod def setUpClass(cls): - cls.profile = Profile("../json_schema/test/test_profiles.json") + cls.profile = Profile(os.path.abspath("json_schema/test/test_profiles.json")) def setUp(self): self.profile.delete_all_profiles() From 04036349b20bca51dac9a2986b80f170d2f5cb5e Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Thu, 14 Mar 2024 01:53:25 +0100 Subject: [PATCH 08/77] feat(UI-UX): Add chat to lobby and playfield [TTTK-16] --- UI/chat.py | 41 +++++++++++++++++++++++++++++++++++++++++ UI/field_frame.py | 11 +++++++---- UI/lib/tttk_tk.py | 3 ++- UI/multi.py | 14 +++++++++----- 4 files changed, 59 insertions(+), 10 deletions(-) create mode 100644 UI/chat.py diff --git a/UI/chat.py b/UI/chat.py new file mode 100644 index 0000000..ff51a47 --- /dev/null +++ b/UI/chat.py @@ -0,0 +1,41 @@ +from .lib import tttk_tk as tk + +class Chat(tk.Frame): + def __init__(self, master, root, chat='', *args, **kwargs): + super().__init__(master) + self.root = root + self._create_widgets(chat) + self._display_widgets() + self.root.network_events['chat/receive'] = self._chat_receive + + def _create_widgets(self, chat): + #self.txtChat = tk.Text(self.widget, state=tk.DISABLED) + self.txtChat = tk.Text(self.widget, width=0) + self.txtScroll = tk.Scrollbar(self.widget, command=self.txtChat.yview) + self.txtChat.config(yscrollcommand=self.txtScroll.set) + self.txtChat.insert(tk.END, chat) + self.txtChat.config(state=tk.DISABLED) + self.etrMessage = tk.Entry(self.widget) + self.btnSend = tk.Button(self.widget, text="Send", command=lambda *args: self._send()) + + def _display_widgets(self): + self.widget.columnconfigure([0], weight=5) + self.widget.columnconfigure([1], weight=1) + self.widget.rowconfigure([0], weight=1) + self.widget.rowconfigure([1,2], weight=0) + self.txtChat.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=0, row=0, columnspan=2) + self.txtScroll.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=0) + self.etrMessage.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=0, row=1) + self.btnSend.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=1, row=1, columnspan=2) + + def _send(self): + self.root.out_queue.put({'message_type': 'chat/message', 'args' : {'message': self.etrMessage.val}}) + self.etrMessage.val = "" + self.txtChat.config(state=tk.NORMAL) + self.txtChat.insert(tk.END, "hallo\n") + self.txtChat.config(state=tk.DISABLED) + + def _chat_receive(self, queue): + self.txtChat.config(state=tk.NORMAL) + self.txtChat.insert(tk.END, f"{queue['sender'].display_name}: {queue['message']}\n") + self.txtChat.config(state=tk.DISABLED) \ No newline at end of file diff --git a/UI/field_frame.py b/UI/field_frame.py index b211965..832230f 100644 --- a/UI/field_frame.py +++ b/UI/field_frame.py @@ -6,6 +6,7 @@ from .gamefield import gamefield, gamefield_controller from .endscreen import EndScreen from .messages import messages +from .chat import Chat class player_type(Enum): local = auto() @@ -71,9 +72,9 @@ def error(self, queue, *args): msg.display() class Field(base_frame): - def __init__(self, master, *args, starting_player, starting_player_symbol, opponent, opponent_symbol, **kwargs): + def __init__(self, master, chat, *args, starting_player, starting_player_symbol, opponent, opponent_symbol, **kwargs): super().__init__(master) - self._create_widgets() + self._create_widgets(chat) self.controller = field_controller(self, [starting_player, opponent]) self._display_widgets() #self.bind("<>", self.controller.sub_controller.turn) @@ -83,22 +84,24 @@ def __init__(self, master, *args, starting_player, starting_player_symbol, oppon self.master.network_events['game/end'] = self.controller.end self.master.network_events['game/error'] = self.controller.error - def _create_widgets(self): + def _create_widgets(self, chat): self.heading = tk.Label(self, text="Tic Tac Toe Kojote", font=self.master.title_font) self.player = [] self.player.append(player(self, 1)) self.player.append(player(self, 2)) self.gamefield = gamefield(self) + self.chat = Chat(self, self.master, chat) self.close = tk.Button(self, text="close") def _display_widgets(self): - self.columnconfigure(1, weight=1) + self.columnconfigure([1,3], weight=1) self.rowconfigure(2, weight=1) self.heading.grid(row=0, column=0, columnspan=3) self.player[0].grid(row=1, column=0) self.player[1].grid(row=1, column=2) self.gamefield.grid(sticky=tk.N+tk.S+tk.E+tk.W, row=2, column=1) + self.chat.grid(sticky=tk.N+tk.S+tk.E+tk.W, row=1, column=3, rowspan=3) self.close.grid(row=3, column=2) def on_destroy(self): diff --git a/UI/lib/tttk_tk.py b/UI/lib/tttk_tk.py index 28f9e62..0e26abc 100644 --- a/UI/lib/tttk_tk.py +++ b/UI/lib/tttk_tk.py @@ -21,7 +21,8 @@ def __init__(self, Widget: tk.Widget, master: tk.Misc, *args, **kwargs): kwargs['defaultValues'] = { 'font': ("Arial bold", 12), 'margin': 5, - 'bg': '#FFFFFF',} | defaultValues + 'bg': '#FFFFFF', + 'width': 0,} | defaultValues if(Widget == tk.Frame): del kwargs['defaultValues']['font'] super().__init__(Widget, master, *args, **kwargs) diff --git a/UI/multi.py b/UI/multi.py index 37d6324..dfeee07 100644 --- a/UI/multi.py +++ b/UI/multi.py @@ -10,6 +10,7 @@ from Server.main import server_thread from AI.ai_context import AIContext from AI.ai_strategy import AIStrategy, WeakAIStrategy, AdvancedAIStrategy +from .chat import Chat class Join(base_frame): def __init__(self, master, *args, opponent=player_type.unknown, **kwargs): @@ -36,8 +37,9 @@ def _create_widgets(self, opponent): if opponent == player_type.local: self.btnRdy2 = tk.Button(self, text='Start', command=lambda *args: self.master.out_queue.put({'message_type': 'lobby/ready', 'args' : {'ready': True}})) self.btnExit = tk.Button(self, text='Menu', command=lambda: self.master.show_menu()) + self.chat = Chat(self, self.master) - def _display_widgets(self,): + def _display_widgets(self): self.columnconfigure([0, 6], weight=1) self.columnconfigure([1, 5], weight=2) self.columnconfigure([2, 4], weight=4) @@ -46,11 +48,13 @@ def _display_widgets(self,): self.rowconfigure([2], weight=2) self.rowconfigure([4, 6, 8, 10], weight=4) self.rowconfigure([3, 5, 7, 9, 11], weight=2) + self.grid_configure() # display the buttons created in the _create_widgets method self.lblTitle.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=2, columnspan=3) - self.btnRdy.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=4, row=10) + self.btnRdy.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=10) if hasattr(self, 'btnRdy2'): self.btnRdy2.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=10) + self.chat.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=4, row=4, columnspan=2, rowspan=7) self.btnExit.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=5, row=1) def _update_lobby(self, queue): @@ -65,13 +69,13 @@ def _update_lobby(self, queue): else: self.btnRdy.config(text="Start") for i, player in enumerate(self.playerlist): - player[0].grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=4+i, columnspan=2) - player[1].grid(sticky=tk.E+tk.W+tk.N+tk.S, column=4, row=4+i) + player[0].grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=4+i) + player[1].grid(sticky=tk.E+tk.W+tk.N+tk.S, column=3, row=4+i) def _start_game(self, queue): print(queue) - self.master.show(Field, **queue) + self.master.show(Field, self.chat.txtChat.get("1.0", tk.END+"-1c")+"Game starting\n", **queue) def on_destroy(self): del self.master.network_events['lobby/status'] From 428cf7e9a4c6495023782d0ff603082044c7bb7a Mon Sep 17 00:00:00 2001 From: Petzys <87223648+Petzys@users.noreply.github.com> Date: Thu, 14 Mar 2024 08:16:56 +0100 Subject: [PATCH 09/77] Fix(client): Fixed missing chat messages in client --- Client/ui_client.py | 2 +- UI/chat.py | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/Client/ui_client.py b/Client/ui_client.py index dbfa112..ecf4695 100644 --- a/Client/ui_client.py +++ b/Client/ui_client.py @@ -132,7 +132,7 @@ async def await_commands(self): case "game/make_move": await self.game_make_move(**message["args"]) case "chat/message": - pass + await self.chat_message(**message["args"]) case "server/terminate": await self.terminate() case "game/gamestate": diff --git a/UI/chat.py b/UI/chat.py index ff51a47..88a3435 100644 --- a/UI/chat.py +++ b/UI/chat.py @@ -31,9 +31,6 @@ def _display_widgets(self): def _send(self): self.root.out_queue.put({'message_type': 'chat/message', 'args' : {'message': self.etrMessage.val}}) self.etrMessage.val = "" - self.txtChat.config(state=tk.NORMAL) - self.txtChat.insert(tk.END, "hallo\n") - self.txtChat.config(state=tk.DISABLED) def _chat_receive(self, queue): self.txtChat.config(state=tk.NORMAL) From 91f8f8f7c2952463326d3adcb0f683408c5f9a47 Mon Sep 17 00:00:00 2001 From: bananabr3d Date: Thu, 14 Mar 2024 08:25:39 +0100 Subject: [PATCH 10/77] fix: json files moved --- {json_schema => Client/Data}/profiles.json | 0 {json_schema => Client/Data}/test/test_profiles.json | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename {json_schema => Client/Data}/profiles.json (100%) rename {json_schema => Client/Data}/test/test_profiles.json (100%) diff --git a/json_schema/profiles.json b/Client/Data/profiles.json similarity index 100% rename from json_schema/profiles.json rename to Client/Data/profiles.json diff --git a/json_schema/test/test_profiles.json b/Client/Data/test/test_profiles.json similarity index 100% rename from json_schema/test/test_profiles.json rename to Client/Data/test/test_profiles.json From 127b7ace8e18700a029e0b03b655244ade0b7355 Mon Sep 17 00:00:00 2001 From: bananabr3d Date: Thu, 14 Mar 2024 08:26:01 +0100 Subject: [PATCH 11/77] fix: abs path added, test code deleted --- Client/profile_save.py | 6 +++--- Client/test_profile_save.py | 3 +-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/Client/profile_save.py b/Client/profile_save.py index 54b1fb0..e523a78 100644 --- a/Client/profile_save.py +++ b/Client/profile_save.py @@ -1,13 +1,13 @@ import json -from os.path import exists, abspath - +from os.path import exists +import os class Profile: """ This class is used to handle the profiles.json file. It is used to get, set, and add profiles to the file. """ - def __init__(self, path=abspath('../json_schema/profiles.json')): + def __init__(self, path=os.path.abspath('Client/Data/profiles.json')): self.path = path def check_file(self): diff --git a/Client/test_profile_save.py b/Client/test_profile_save.py index f7c8141..ff10efd 100644 --- a/Client/test_profile_save.py +++ b/Client/test_profile_save.py @@ -2,13 +2,12 @@ from Client.profile_save import Profile import os -#todo kda durch 0 teilen fixen class TestProfileSave(unittest.TestCase): @classmethod def setUpClass(cls): - cls.profile = Profile(os.path.abspath("json_schema/test/test_profiles.json")) + cls.profile = Profile(os.path.abspath("Client/Data/test/test_profiles.json")) def setUp(self): self.profile.delete_all_profiles() From a761b4106fb2b4e3746c5b0a203b994de2329bfa Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Thu, 14 Mar 2024 08:28:21 +0100 Subject: [PATCH 12/77] fix(UI-UX): delete player now works [TTTK-75] --- UI/profile.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/UI/profile.py b/UI/profile.py index 48df264..2b5e120 100644 --- a/UI/profile.py +++ b/UI/profile.py @@ -64,7 +64,7 @@ def _create_widgets(self): self.lblUUDI = tk.Label(self, text='User ID') self.lblUUIDValue = tk.Label(self, text=self.master.player.uuid) self.btnEdit = tk.Button(self, text='Edit Profile', command=lambda *args: self.master.show(NewProfile, 'edit')) - self.btnDelete = tk.Button(self, text='Delete profile', command=lambda*args : self.master.show(NewProfile, 'delete')) + self.btnDelete = tk.Button(self, text='Delete profile', command=lambda *args: self._delete()) self.btnMenu = tk.Button(self, text='Menu', command=lambda: self.master.show_menu()) def _display_widgets(self): @@ -87,4 +87,8 @@ def _display_widgets(self): self.lblUUIDValue.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=4, row=6, columnspan=5) self.btnDelete.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=8, columnspan=3) self.btnEdit.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=6, row=8, columnspan=3) - self.btnMenu.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=9, row=1) \ No newline at end of file + self.btnMenu.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=9, row=1) + + def _delete(self): + self.master.player = None + self.master.show(NewProfile, 'delete') \ No newline at end of file From e8ab224f7df7de72d64cb9b606ccc67a6da6c499 Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Thu, 14 Mar 2024 08:30:36 +0100 Subject: [PATCH 13/77] fix(UI-UX): fix Main Menu Button on end screen [TTTK-76] --- UI/endscreen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UI/endscreen.py b/UI/endscreen.py index 18773d0..b43f82c 100644 --- a/UI/endscreen.py +++ b/UI/endscreen.py @@ -13,7 +13,7 @@ def _create_widgets(self, win:bool): message = "You won the game!" if win else "You lost the game!" self.lblWinner = tk.Label(self, width=20, height=5, bg="white", text=message) #self.btnPlayAgain = tk.Button(self, width=20, height=5, text="Play Again", command=lambda: self.master.show(Field)) - self.btnMainMenu = tk.Button(self, text="Main Menu", width=20, height=5, command=lambda: self.master.show_menu) + self.btnMainMenu = tk.Button(self, text="Main Menu", width=20, height=5, command=lambda: self.master.show_menu()) def _display_widgets(self): self.lblWinner.pack() From 9612e869715879940e2bcb8116b4fd18175ba52d Mon Sep 17 00:00:00 2001 From: bananabr3d Date: Thu, 14 Mar 2024 08:32:20 +0100 Subject: [PATCH 14/77] fix: profile_save.py: print to error --- Client/profile_save.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Client/profile_save.py b/Client/profile_save.py index e523a78..d3e33d2 100644 --- a/Client/profile_save.py +++ b/Client/profile_save.py @@ -46,7 +46,7 @@ def get_profile(self, profile_uuid): if profile["profile_uuid"] == profile_uuid: return profile except: - print("json error: Make sure profiles.json is formatted correctly") + raise RuntimeError("json error: Make sure profiles.json is formatted correctly") return None def set_profile(self, profile_uuid, profile_name, profile_color): @@ -73,7 +73,7 @@ def set_profile(self, profile_uuid, profile_name, profile_color): with open(self.path, 'w') as file: json.dump(data, file) except: - print("json error: Make sure profiles.json is formatted correctly") + raise RuntimeError("json error: Make sure profiles.json is formatted correctly") return None def get_profile_by_name(self, profile_name): @@ -110,7 +110,7 @@ def add_new_profile(self, profile_name, profile_uuid, profile_color): json.dump(data, file) file.truncate() except: - print("json error: Make sure profiles.json is formatted correctly") + raise RuntimeError("json error: Make sure profiles.json is formatted correctly") else: with open(self.path, 'w') as file: @@ -135,7 +135,7 @@ def delete_profile(self, profile_uuid): with open(self.path, 'w') as file: json.dump(data, file) except: - print("json error: Make sure profiles.json is formatted correctly") + raise RuntimeError("json error: Make sure profiles.json is formatted correctly") def delete_profile_by_name(self, profile_name): """ @@ -155,7 +155,7 @@ def delete_profile_by_name(self, profile_name): with open(self.path, 'w') as file: json.dump(data, file) except: - print("json error: Make sure profiles.json is formatted correctly") + raise RuntimeError("json error: Make sure profiles.json is formatted correctly") def delete_all_profiles(self): """ From 67e5e62dcc3b396d279b740a1afc3436d0928b9b Mon Sep 17 00:00:00 2001 From: bananabr3d Date: Thu, 14 Mar 2024 09:02:58 +0100 Subject: [PATCH 15/77] fix: input and output changed to player obj --- Client/profile_save.py | 55 +++++++++++++++++++++---------------- Client/test_profile_save.py | 37 +++++++++++++------------ 2 files changed, 52 insertions(+), 40 deletions(-) diff --git a/Client/profile_save.py b/Client/profile_save.py index d3e33d2..a7e427c 100644 --- a/Client/profile_save.py +++ b/Client/profile_save.py @@ -1,6 +1,7 @@ import json from os.path import exists import os +from Server.player import Player class Profile: """ @@ -28,35 +29,40 @@ def get_profiles(self): if self.check_file(): with open(self.path, 'r') as file: data = json.load(file) - return data + output = [] + for profile in data: + output.append(Player.from_dict(profile)) + return output else: return None - def get_profile(self, profile_uuid): + def get_profile(self, player : Player): """ This method returns a profile by its uuid :param profile_uuid: :return: profile matching given uuid """ + player_dict = player.as_dict() if self.check_file(): try: with open(self.path, 'r') as file: data = json.load(file) for profile in data: - if profile["profile_uuid"] == profile_uuid: - return profile + if profile["uuid"] == player_dict["uuid"]: + return Player.from_dict(profile) except: raise RuntimeError("json error: Make sure profiles.json is formatted correctly") return None - def set_profile(self, profile_uuid, profile_name, profile_color): + def set_profile(self, player : Player): """ This method sets the profile name and/or color by the uuid :param profile_uuid: :param profile_name: :param profile_color: """ - if (profile_name or profile_color) == None: + player_dict = player.as_dict() + if (player_dict['display_name'] or player_dict['color']) == None: raise ValueError("name or color cannot be none") if self.check_file(): @@ -64,11 +70,11 @@ def set_profile(self, profile_uuid, profile_name, profile_color): with open(self.path, 'r+') as file: data = json.load(file) for profile in data: - if profile["profile_uuid"] == profile_uuid: - if profile_name != None: - profile["profile_name"] = profile_name - if profile_color != None: - profile["profile_color"] = profile_color + if profile["uuid"] == player_dict["uuid"]: + if player_dict['display_name'] != None: + profile["display_name"] = player_dict['display_name'] + if player_dict['color'] != None: + profile["color"] = player_dict['color'] break with open(self.path, 'w') as file: json.dump(data, file) @@ -76,24 +82,25 @@ def set_profile(self, profile_uuid, profile_name, profile_color): raise RuntimeError("json error: Make sure profiles.json is formatted correctly") return None - def get_profile_by_name(self, profile_name): + def get_profile_by_name(self, player : Player): if self.check_file(): """ This method returns a profile by its name :param profile_name: :return: profile matching given name """ + player_dict = player.as_dict() try: with open(self.path, 'r') as file: data = json.load(file) for profile in data: - if profile["profile_name"] == profile_name: - return profile + if profile["display_name"] == player_dict["display_name"]: + return Player.from_dict(profile) except: print("json error: Make sure profiles.json is formatted correctly") return None - def add_new_profile(self, profile_name, profile_uuid, profile_color): + def add_new_profile(self, player : Player): """ This method adds a new profile to the file :param profile_name: @@ -101,7 +108,7 @@ def add_new_profile(self, profile_name, profile_uuid, profile_color): :param profile_color: """ if self.check_file(): - entry = {"profile_name": profile_name, "profile_uuid": profile_uuid, "profile_color": profile_color} + entry = player.as_dict() try: with open(self.path, 'r+') as file: data = json.load(file) @@ -114,44 +121,46 @@ def add_new_profile(self, profile_name, profile_uuid, profile_color): else: with open(self.path, 'w') as file: - entry = [{"profile_name": profile_name, "profile_uuid": profile_uuid, "profile_color": profile_color}] + entry = [player.as_dict()] json.dump(entry, file) - def delete_profile(self, profile_uuid): + def delete_profile(self, player : Player): """ This method deletes a profile by its uuid :param profile_uuid: """ + player_dict = player.as_dict() if self.check_file(): try: with open(self.path, 'r+') as file: data = json.load(file) for profile in data: - if profile["profile_uuid"] == profile_uuid: + if profile["uuid"] == player_dict["uuid"]: data.remove(profile) break else: - raise ValueError(f"Profile with given uuid: {profile_uuid} not found") + raise ValueError(f"Profile with given uuid: {player_dict['uuid']} not found") with open(self.path, 'w') as file: json.dump(data, file) except: raise RuntimeError("json error: Make sure profiles.json is formatted correctly") - def delete_profile_by_name(self, profile_name): + def delete_profile_by_name(self, player : Player): """ This method deletes a profile by its name :param profile_name: """ + player_dict = player.as_dict() if self.check_file(): try: with open(self.path, 'r+') as file: data = json.load(file) for profile in data: - if profile["profile_name"] == profile_name: + if profile["display_name"] == player_dict["display_name"]: data.remove(profile) break else: - raise ValueError(f"Profile with given name: {profile_name} not found") + raise ValueError(f"Profile with given name: {player_dict['display_name']} not found") with open(self.path, 'w') as file: json.dump(data, file) except: diff --git a/Client/test_profile_save.py b/Client/test_profile_save.py index ff10efd..5ad1354 100644 --- a/Client/test_profile_save.py +++ b/Client/test_profile_save.py @@ -1,6 +1,7 @@ import unittest from Client.profile_save import Profile import os +from Server.player import Player class TestProfileSave(unittest.TestCase): @@ -8,40 +9,42 @@ class TestProfileSave(unittest.TestCase): @classmethod def setUpClass(cls): cls.profile = Profile(os.path.abspath("Client/Data/test/test_profiles.json")) + cls.player1 = Player("test", 0, "test", False) + cls.player2 = Player("test2", 0, "test2", False) def setUp(self): self.profile.delete_all_profiles() def test_get_profile_by_name(self): - self.profile.add_new_profile("test", "test", "test") - self.assertEqual(self.profile.get_profile_by_name("test"), - {"profile_name": "test", "profile_uuid": "test", "profile_color": "test"}) + self.profile.add_new_profile(self.player1) + self.assertEqual(self.profile.get_profile_by_name(self.player1), + self.player1) self.profile.delete_all_profiles() self.assertEqual(self.profile.get_profiles(), []) def test_get_profile_by_uuid(self): - self.profile.add_new_profile("test", "test", "test") - self.assertEqual(self.profile.get_profile("test"), - {"profile_name": "test", "profile_uuid": "test", "profile_color": "test"}) + self.profile.add_new_profile(self.player1) + self.assertEqual(self.profile.get_profile(self.player1), + self.player1) self.profile.delete_all_profiles() self.assertEqual(self.profile.get_profiles(), []) def test_add_new_profile(self): - self.profile.add_new_profile("test", "test", "test") - self.assertEqual(self.profile.get_profile("test"), - {"profile_name": "test", "profile_uuid": "test", "profile_color": "test"}) + self.profile.add_new_profile(self.player1) + self.assertEqual(self.profile.get_profile(self.player1), + self.player1) self.profile.delete_all_profiles() self.assertEqual(self.profile.get_profiles(), []) def test_delete_profile(self): - self.profile.add_new_profile("test", "test", "test") - self.profile.add_new_profile("test2", "test", "test") - self.profile.delete_profile("test") - self.assertEqual(self.profile.get_profiles(), [{"profile_name": "test2", "profile_uuid": "test", "profile_color": "test"}]) + self.profile.add_new_profile(self.player1) + self.profile.add_new_profile(self.player2) + self.profile.delete_profile(self.player1) + self.assertEqual(self.profile.get_profiles(), [self.player2]) self.profile.delete_all_profiles() def test_delete_profile_by_name(self): - self.profile.add_new_profile("test", "test", "test") - self.profile.add_new_profile("test2", "test", "test") - self.profile.delete_profile_by_name("test") - self.assertEqual(self.profile.get_profiles(), [{"profile_name": "test2", "profile_uuid": "test", "profile_color": "test"}]) + self.profile.add_new_profile(self.player1) + self.profile.add_new_profile(self.player2) + self.profile.delete_profile_by_name(self.player1) + self.assertEqual(self.profile.get_profiles(), [self.player2]) self.profile.delete_all_profiles() From 55ace3eec29c3e704b711111cac7509733eb73b6 Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Thu, 14 Mar 2024 09:07:24 +0100 Subject: [PATCH 16/77] fix(UI_UX): changed lobby button to "ready" - "not ready" --- UI/multi.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/UI/multi.py b/UI/multi.py index dfeee07..76dd655 100644 --- a/UI/multi.py +++ b/UI/multi.py @@ -65,9 +65,9 @@ def _update_lobby(self, queue): if(str(player.uuid) == str(self.master.player.uuid)): self.ready = player.ready if(player.ready): - self.btnRdy.config(text="Ready") + self.btnRdy.config(text="not Ready") else: - self.btnRdy.config(text="Start") + self.btnRdy.config(text="Ready") for i, player in enumerate(self.playerlist): player[0].grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=4+i) player[1].grid(sticky=tk.E+tk.W+tk.N+tk.S, column=3, row=4+i) From 72394939f005f48cc2eddf93864dccb4b40f6f77 Mon Sep 17 00:00:00 2001 From: bananabr3d Date: Thu, 14 Mar 2024 09:13:10 +0100 Subject: [PATCH 17/77] fix: set_profiles added, test_set_profiles added --- Client/profile_save.py | 19 +++++++++++++++++++ Client/test_profile_save.py | 6 ++++++ 2 files changed, 25 insertions(+) diff --git a/Client/profile_save.py b/Client/profile_save.py index a7e427c..ebd7412 100644 --- a/Client/profile_save.py +++ b/Client/profile_save.py @@ -82,6 +82,25 @@ def set_profile(self, player : Player): raise RuntimeError("json error: Make sure profiles.json is formatted correctly") return None + def set_profiles(self, players: list): + """ + This method sets the profile name and/or color by the uuid + :param profile_uuid: + :param profile_name: + :param profile_color: + """ + + if self.check_file(): + try: + with open(self.path, 'w') as file: + entry = [] + for player in players: + entry.append(player.as_dict()) + json.dump(entry, file) + except: + raise RuntimeError("json error: Make sure profiles.json is formatted correctly") + return None + def get_profile_by_name(self, player : Player): if self.check_file(): """ diff --git a/Client/test_profile_save.py b/Client/test_profile_save.py index 5ad1354..792714e 100644 --- a/Client/test_profile_save.py +++ b/Client/test_profile_save.py @@ -48,3 +48,9 @@ def test_delete_profile_by_name(self): self.profile.delete_profile_by_name(self.player1) self.assertEqual(self.profile.get_profiles(), [self.player2]) self.profile.delete_all_profiles() + + def test_set_profiles(self): + data = [self.player1, self.player2] + self.profile.set_profiles(data) + self.assertEqual(self.profile.get_profiles(), data) + self.profile.delete_all_profiles() \ No newline at end of file From 759d892e3b5a0614b74f2c08ecfaffbd5070a37b Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Thu, 14 Mar 2024 09:12:35 +0100 Subject: [PATCH 18/77] feat(UI_UX): added ready status of all players to lobby --- UI/multi.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/UI/multi.py b/UI/multi.py index 76dd655..b4bf02d 100644 --- a/UI/multi.py +++ b/UI/multi.py @@ -60,7 +60,8 @@ def _display_widgets(self): def _update_lobby(self, queue): self.playerlist = [] for player in queue['player']: - self.playerlist.append([tk.Label(self, text=player.display_name), + rdy = '\u2611' if player.ready else '' + self.playerlist.append([tk.Label(self, text=rdy + ' ' + player.display_name), tk.Button(self, text='Kick', command=lambda uuid=player.uuid, *args: self.master.out_queue.put({'message_type': 'lobby/kick', 'args' : {'player_to_kick': uuid}}))]) if(str(player.uuid) == str(self.master.player.uuid)): self.ready = player.ready From 124f28736f2e2a2edc8a119fd7ab28e458483f1c Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Thu, 14 Mar 2024 09:20:51 +0100 Subject: [PATCH 19/77] feat(UI-UX): initial playfield is filled --- UI/field_frame.py | 1 + 1 file changed, 1 insertion(+) diff --git a/UI/field_frame.py b/UI/field_frame.py index 832230f..6257d8b 100644 --- a/UI/field_frame.py +++ b/UI/field_frame.py @@ -83,6 +83,7 @@ def __init__(self, master, chat, *args, starting_player, starting_player_symbol, self.master.network_events['game/turn'] = self.controller.sub_controller.turn self.master.network_events['game/end'] = self.controller.end self.master.network_events['game/error'] = self.controller.error + self.master.out_queue.put({'message_type': 'game/gamestate', 'args' :{} }) def _create_widgets(self, chat): self.heading = tk.Label(self, text="Tic Tac Toe Kojote", font=self.master.title_font) From 915eeb73061e1ad51b537476d63671d7a88c6827 Mon Sep 17 00:00:00 2001 From: Andrey Dubilyer <92729835+bananabr3d@users.noreply.github.com> Date: Thu, 14 Mar 2024 09:43:57 +0100 Subject: [PATCH 20/77] Update Client/profile_save.py Co-authored-by: Hauke Platte <53154723+HOOK-Hawkins@users.noreply.github.com> --- Client/profile_save.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Client/profile_save.py b/Client/profile_save.py index ebd7412..a4bd256 100644 --- a/Client/profile_save.py +++ b/Client/profile_save.py @@ -189,7 +189,7 @@ def delete_all_profiles(self): """ This method deletes all profiles """ - if self.check_file(): + if exists(self.path): with open(self.path, 'w') as file: file.write("[]") From e8f20ed6b5955b90519e262b53d06ccd72c79e73 Mon Sep 17 00:00:00 2001 From: Andrey Dubilyer <92729835+bananabr3d@users.noreply.github.com> Date: Thu, 14 Mar 2024 09:44:08 +0100 Subject: [PATCH 21/77] Update Client/profile_save.py Co-authored-by: Hauke Platte <53154723+HOOK-Hawkins@users.noreply.github.com> --- Client/profile_save.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Client/profile_save.py b/Client/profile_save.py index a4bd256..8d2b1ab 100644 --- a/Client/profile_save.py +++ b/Client/profile_save.py @@ -26,7 +26,7 @@ def get_profiles(self): This method returns all the profiles from the file :return: An array of all profiles """ - if self.check_file(): + if exists(self.path): with open(self.path, 'r') as file: data = json.load(file) output = [] From 56d71e607b50f5b3db5552efa30d521f35bb835d Mon Sep 17 00:00:00 2001 From: Andrey Dubilyer <92729835+bananabr3d@users.noreply.github.com> Date: Thu, 14 Mar 2024 09:44:23 +0100 Subject: [PATCH 22/77] Update Client/profile_save.py Co-authored-by: Hauke Platte <53154723+HOOK-Hawkins@users.noreply.github.com> --- Client/profile_save.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/Client/profile_save.py b/Client/profile_save.py index 8d2b1ab..5f3bfc2 100644 --- a/Client/profile_save.py +++ b/Client/profile_save.py @@ -11,16 +11,6 @@ class Profile: def __init__(self, path=os.path.abspath('Client/Data/profiles.json')): self.path = path - def check_file(self): - """ - This method checks if the file exists - :return: True if the file exists, False if it does not - """ - if exists(self.path): - return True - else: - return False - def get_profiles(self): """ This method returns all the profiles from the file From ed3b241aace6206c0658befb1e6202fa6e32855c Mon Sep 17 00:00:00 2001 From: Andrey Dubilyer <92729835+bananabr3d@users.noreply.github.com> Date: Thu, 14 Mar 2024 09:44:32 +0100 Subject: [PATCH 23/77] Update Client/profile_save.py Co-authored-by: Hauke Platte <53154723+HOOK-Hawkins@users.noreply.github.com> --- Client/profile_save.py | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/Client/profile_save.py b/Client/profile_save.py index 5f3bfc2..88e3625 100644 --- a/Client/profile_save.py +++ b/Client/profile_save.py @@ -26,24 +26,6 @@ def get_profiles(self): else: return None - def get_profile(self, player : Player): - """ - This method returns a profile by its uuid - :param profile_uuid: - :return: profile matching given uuid - """ - player_dict = player.as_dict() - if self.check_file(): - try: - with open(self.path, 'r') as file: - data = json.load(file) - for profile in data: - if profile["uuid"] == player_dict["uuid"]: - return Player.from_dict(profile) - except: - raise RuntimeError("json error: Make sure profiles.json is formatted correctly") - return None - def set_profile(self, player : Player): """ This method sets the profile name and/or color by the uuid From c7dd2110fcae92d625e9c85e6cfb5b68e250dcb9 Mon Sep 17 00:00:00 2001 From: Andrey Dubilyer <92729835+bananabr3d@users.noreply.github.com> Date: Thu, 14 Mar 2024 09:44:46 +0100 Subject: [PATCH 24/77] Update Client/profile_save.py Co-authored-by: Hauke Platte <53154723+HOOK-Hawkins@users.noreply.github.com> --- Client/profile_save.py | 24 ------------------------ 1 file changed, 24 deletions(-) diff --git a/Client/profile_save.py b/Client/profile_save.py index 88e3625..f6db3b6 100644 --- a/Client/profile_save.py +++ b/Client/profile_save.py @@ -91,30 +91,6 @@ def get_profile_by_name(self, player : Player): print("json error: Make sure profiles.json is formatted correctly") return None - def add_new_profile(self, player : Player): - """ - This method adds a new profile to the file - :param profile_name: - :param profile_uuid: - :param profile_color: - """ - if self.check_file(): - entry = player.as_dict() - try: - with open(self.path, 'r+') as file: - data = json.load(file) - file.seek(0) - data.append(entry) - json.dump(data, file) - file.truncate() - except: - raise RuntimeError("json error: Make sure profiles.json is formatted correctly") - - else: - with open(self.path, 'w') as file: - entry = [player.as_dict()] - json.dump(entry, file) - def delete_profile(self, player : Player): """ This method deletes a profile by its uuid From 7bd7e182b5c210f2566f07543506d2284424ab51 Mon Sep 17 00:00:00 2001 From: Andrey Dubilyer <92729835+bananabr3d@users.noreply.github.com> Date: Thu, 14 Mar 2024 09:45:42 +0100 Subject: [PATCH 25/77] Update Client/profile_save.py Co-authored-by: Hauke Platte <53154723+HOOK-Hawkins@users.noreply.github.com> --- Client/profile_save.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/Client/profile_save.py b/Client/profile_save.py index f6db3b6..8c89a2b 100644 --- a/Client/profile_save.py +++ b/Client/profile_save.py @@ -91,27 +91,6 @@ def get_profile_by_name(self, player : Player): print("json error: Make sure profiles.json is formatted correctly") return None - def delete_profile(self, player : Player): - """ - This method deletes a profile by its uuid - :param profile_uuid: - """ - player_dict = player.as_dict() - if self.check_file(): - try: - with open(self.path, 'r+') as file: - data = json.load(file) - for profile in data: - if profile["uuid"] == player_dict["uuid"]: - data.remove(profile) - break - else: - raise ValueError(f"Profile with given uuid: {player_dict['uuid']} not found") - with open(self.path, 'w') as file: - json.dump(data, file) - except: - raise RuntimeError("json error: Make sure profiles.json is formatted correctly") - def delete_profile_by_name(self, player : Player): """ This method deletes a profile by its name From 224ea07360886f08627b9773ca2074446e555274 Mon Sep 17 00:00:00 2001 From: Andrey Dubilyer <92729835+bananabr3d@users.noreply.github.com> Date: Thu, 14 Mar 2024 09:45:55 +0100 Subject: [PATCH 26/77] Update Client/profile_save.py Co-authored-by: Hauke Platte <53154723+HOOK-Hawkins@users.noreply.github.com> --- Client/profile_save.py | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/Client/profile_save.py b/Client/profile_save.py index 8c89a2b..f33f849 100644 --- a/Client/profile_save.py +++ b/Client/profile_save.py @@ -91,27 +91,6 @@ def get_profile_by_name(self, player : Player): print("json error: Make sure profiles.json is formatted correctly") return None - def delete_profile_by_name(self, player : Player): - """ - This method deletes a profile by its name - :param profile_name: - """ - player_dict = player.as_dict() - if self.check_file(): - try: - with open(self.path, 'r+') as file: - data = json.load(file) - for profile in data: - if profile["display_name"] == player_dict["display_name"]: - data.remove(profile) - break - else: - raise ValueError(f"Profile with given name: {player_dict['display_name']} not found") - with open(self.path, 'w') as file: - json.dump(data, file) - except: - raise RuntimeError("json error: Make sure profiles.json is formatted correctly") - def delete_all_profiles(self): """ This method deletes all profiles From b0d5f6b16cfe757b8ae2f0e5c0f7e5cbd662221a Mon Sep 17 00:00:00 2001 From: Andrey Dubilyer <92729835+bananabr3d@users.noreply.github.com> Date: Thu, 14 Mar 2024 09:46:14 +0100 Subject: [PATCH 27/77] Update Client/profile_save.py Co-authored-by: Hauke Platte <53154723+HOOK-Hawkins@users.noreply.github.com> --- Client/profile_save.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Client/profile_save.py b/Client/profile_save.py index f33f849..c5041f2 100644 --- a/Client/profile_save.py +++ b/Client/profile_save.py @@ -62,7 +62,6 @@ def set_profiles(self, players: list): :param profile_color: """ - if self.check_file(): try: with open(self.path, 'w') as file: entry = [] From d584ba51082aa77867919be800d9c0ee676f8dac Mon Sep 17 00:00:00 2001 From: Andrey Dubilyer <92729835+bananabr3d@users.noreply.github.com> Date: Thu, 14 Mar 2024 09:46:27 +0100 Subject: [PATCH 28/77] Update Client/profile_save.py Co-authored-by: Hauke Platte <53154723+HOOK-Hawkins@users.noreply.github.com> --- Client/profile_save.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Client/profile_save.py b/Client/profile_save.py index c5041f2..e8be096 100644 --- a/Client/profile_save.py +++ b/Client/profile_save.py @@ -70,7 +70,6 @@ def set_profiles(self, players: list): json.dump(entry, file) except: raise RuntimeError("json error: Make sure profiles.json is formatted correctly") - return None def get_profile_by_name(self, player : Player): if self.check_file(): From 77720a1e47d81f06438a4eec40caf77bf427132e Mon Sep 17 00:00:00 2001 From: Andrey Dubilyer <92729835+bananabr3d@users.noreply.github.com> Date: Thu, 14 Mar 2024 09:48:05 +0100 Subject: [PATCH 29/77] Update Client/profile_save.py Co-authored-by: Hauke Platte <53154723+HOOK-Hawkins@users.noreply.github.com> --- Client/profile_save.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Client/profile_save.py b/Client/profile_save.py index e8be096..c9a9f65 100644 --- a/Client/profile_save.py +++ b/Client/profile_save.py @@ -24,7 +24,7 @@ def get_profiles(self): output.append(Player.from_dict(profile)) return output else: - return None + return [] def set_profile(self, player : Player): """ From 5b2fb92c2d3df0fa1fb4b7d8abf8e85a12e2e75e Mon Sep 17 00:00:00 2001 From: bananabr3d Date: Thu, 14 Mar 2024 09:50:03 +0100 Subject: [PATCH 30/77] fix: useless functions deleted, indentation error fixed --- Client/profile_save.py | 64 ++++++------------------------------------ 1 file changed, 9 insertions(+), 55 deletions(-) diff --git a/Client/profile_save.py b/Client/profile_save.py index c9a9f65..82b1d62 100644 --- a/Client/profile_save.py +++ b/Client/profile_save.py @@ -3,6 +3,7 @@ import os from Server.player import Player + class Profile: """ This class is used to handle the profiles.json file. It is used to get, set, and add profiles to the file. @@ -26,34 +27,6 @@ def get_profiles(self): else: return [] - def set_profile(self, player : Player): - """ - This method sets the profile name and/or color by the uuid - :param profile_uuid: - :param profile_name: - :param profile_color: - """ - player_dict = player.as_dict() - if (player_dict['display_name'] or player_dict['color']) == None: - raise ValueError("name or color cannot be none") - - if self.check_file(): - try: - with open(self.path, 'r+') as file: - data = json.load(file) - for profile in data: - if profile["uuid"] == player_dict["uuid"]: - if player_dict['display_name'] != None: - profile["display_name"] = player_dict['display_name'] - if player_dict['color'] != None: - profile["color"] = player_dict['color'] - break - with open(self.path, 'w') as file: - json.dump(data, file) - except: - raise RuntimeError("json error: Make sure profiles.json is formatted correctly") - return None - def set_profiles(self, players: list): """ This method sets the profile name and/or color by the uuid @@ -62,32 +35,14 @@ def set_profiles(self, players: list): :param profile_color: """ - try: - with open(self.path, 'w') as file: - entry = [] - for player in players: - entry.append(player.as_dict()) - json.dump(entry, file) - except: - raise RuntimeError("json error: Make sure profiles.json is formatted correctly") - - def get_profile_by_name(self, player : Player): - if self.check_file(): - """ - This method returns a profile by its name - :param profile_name: - :return: profile matching given name - """ - player_dict = player.as_dict() - try: - with open(self.path, 'r') as file: - data = json.load(file) - for profile in data: - if profile["display_name"] == player_dict["display_name"]: - return Player.from_dict(profile) - except: - print("json error: Make sure profiles.json is formatted correctly") - return None + try: + with open(self.path, 'w') as file: + entry = [] + for player in players: + entry.append(player.as_dict()) + json.dump(entry, file) + except: + raise RuntimeError("json error: Make sure profiles.json is formatted correctly") def delete_all_profiles(self): """ @@ -96,4 +51,3 @@ def delete_all_profiles(self): if exists(self.path): with open(self.path, 'w') as file: file.write("[]") - From e31ec77e1d352f8fbe1814a157072c6cfeb6826a Mon Sep 17 00:00:00 2001 From: bananabr3d Date: Thu, 14 Mar 2024 09:57:46 +0100 Subject: [PATCH 31/77] fix: test_profile_save.py: tests adjusted --- Client/test_profile_save.py | 39 +++---------------------------------- 1 file changed, 3 insertions(+), 36 deletions(-) diff --git a/Client/test_profile_save.py b/Client/test_profile_save.py index 792714e..a210a08 100644 --- a/Client/test_profile_save.py +++ b/Client/test_profile_save.py @@ -15,42 +15,9 @@ def setUpClass(cls): def setUp(self): self.profile.delete_all_profiles() - def test_get_profile_by_name(self): - self.profile.add_new_profile(self.player1) - self.assertEqual(self.profile.get_profile_by_name(self.player1), - self.player1) - self.profile.delete_all_profiles() - self.assertEqual(self.profile.get_profiles(), []) - - def test_get_profile_by_uuid(self): - self.profile.add_new_profile(self.player1) - self.assertEqual(self.profile.get_profile(self.player1), - self.player1) - self.profile.delete_all_profiles() - self.assertEqual(self.profile.get_profiles(), []) - def test_add_new_profile(self): - self.profile.add_new_profile(self.player1) - self.assertEqual(self.profile.get_profile(self.player1), - self.player1) - self.profile.delete_all_profiles() - self.assertEqual(self.profile.get_profiles(), []) - - def test_delete_profile(self): - self.profile.add_new_profile(self.player1) - self.profile.add_new_profile(self.player2) - self.profile.delete_profile(self.player1) - self.assertEqual(self.profile.get_profiles(), [self.player2]) - self.profile.delete_all_profiles() - - def test_delete_profile_by_name(self): - self.profile.add_new_profile(self.player1) - self.profile.add_new_profile(self.player2) - self.profile.delete_profile_by_name(self.player1) - self.assertEqual(self.profile.get_profiles(), [self.player2]) - self.profile.delete_all_profiles() - - def test_set_profiles(self): + def test_all(self): data = [self.player1, self.player2] self.profile.set_profiles(data) self.assertEqual(self.profile.get_profiles(), data) - self.profile.delete_all_profiles() \ No newline at end of file + self.profile.delete_all_profiles() + self.assertEqual(self.profile.get_profiles(), []) From f2a717d167e54b580552b68ad765bd33234400f9 Mon Sep 17 00:00:00 2001 From: bananabr3d Date: Thu, 14 Mar 2024 10:06:18 +0100 Subject: [PATCH 32/77] fix: test_profile_save.py: deleted file case implemented --- Client/test_profile_save.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Client/test_profile_save.py b/Client/test_profile_save.py index a210a08..96a6f0c 100644 --- a/Client/test_profile_save.py +++ b/Client/test_profile_save.py @@ -2,6 +2,7 @@ from Client.profile_save import Profile import os from Server.player import Player +from os.path import exists class TestProfileSave(unittest.TestCase): @@ -16,6 +17,8 @@ def setUp(self): self.profile.delete_all_profiles() def test_all(self): + if exists(self.profile.path): + os.remove(self.profile.path) data = [self.player1, self.player2] self.profile.set_profiles(data) self.assertEqual(self.profile.get_profiles(), data) From 84056a2d80422b0f8e6721eff61cc7147723e4ae Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Thu, 14 Mar 2024 10:10:11 +0100 Subject: [PATCH 33/77] fix(UI-UX): edit screen loads previous value --- UI/profile.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/UI/profile.py b/UI/profile.py index 2b5e120..12ee12c 100644 --- a/UI/profile.py +++ b/UI/profile.py @@ -9,17 +9,19 @@ class NewProfile(base_frame): def __init__(self, master, *args, **kwargs): super().__init__(master) - self._create_widgets() - self._display_widgets() self.address_toogle = False self.next = kwargs.pop('return_to', Profile) + self.edit = kwargs.pop('edit', False) + self._create_widgets() + self._display_widgets() def _create_widgets(self): - self.lblTitle = tk.Label(self, text='Create profile', font=self.master.title_font) + task = 'Edit' if self.edit else 'Create' + self.lblTitle = tk.Label(self, text=f'{task} profile', font=self.master.title_font) self.lblName = tk.Label(self, text='Name') self.etrName = tk.Entry(self) - self.varName = self.etrName.var - self.btnCreate = tk.Button(self, text='Create profile', command=lambda *args: self._create()) + self.etrName.val = self.master.player.display_name if self.edit else '' + self.btnCreate = tk.Button(self, text=f'{task} profile', command=lambda *args: self._create()) self.btnMenu = tk.Button(self, text='Menu', command=lambda: self.master.show_menu()) def _display_widgets(self): @@ -42,7 +44,7 @@ def _display_widgets(self): self.btnMenu.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=9, row=1) def _create(self): - self.master.player = Player(self.varName.get(), 0) + self.master.player = Player(self.etrName.val, 0) self.master.show(self.next) class Profile(base_frame): @@ -63,7 +65,7 @@ def _create_widgets(self): self.lblNameValue = tk.Label(self, text=self.master.player.display_name) self.lblUUDI = tk.Label(self, text='User ID') self.lblUUIDValue = tk.Label(self, text=self.master.player.uuid) - self.btnEdit = tk.Button(self, text='Edit Profile', command=lambda *args: self.master.show(NewProfile, 'edit')) + self.btnEdit = tk.Button(self, text='Edit Profile', command=lambda *args: self.master.show(NewProfile, edit=True)) self.btnDelete = tk.Button(self, text='Delete profile', command=lambda *args: self._delete()) self.btnMenu = tk.Button(self, text='Menu', command=lambda: self.master.show_menu()) @@ -91,4 +93,4 @@ def _display_widgets(self): def _delete(self): self.master.player = None - self.master.show(NewProfile, 'delete') \ No newline at end of file + self.master.show(NewProfile) \ No newline at end of file From 64718a7b0b10c27f599ca17babbad177db96d79a Mon Sep 17 00:00:00 2001 From: Petzys <87223648+Petzys@users.noreply.github.com> Date: Thu, 14 Mar 2024 15:45:32 +0100 Subject: [PATCH 34/77] Feat(statistics UI): TTTK-55 Added statistics request for UI --- Client/ui_client.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Client/ui_client.py b/Client/ui_client.py index ecf4695..5cb2d9a 100644 --- a/Client/ui_client.py +++ b/Client/ui_client.py @@ -137,6 +137,12 @@ async def await_commands(self): await self.terminate() case "game/gamestate": self.send_gamestate_to_ui() + case "statistics/statistics": + self._out_queue.put({ + "message_type": "statistics/statistics", + "statistics": self._statistics + }) + self._tk_root.event_generate("<>", when="tail") case _: logger.error(f"Unknown message type received from UI in in_queue: {message['message_type']}") return From 6fb5281504b3c75d314aa0a9987171a916d00682 Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Thu, 14 Mar 2024 16:09:06 +0100 Subject: [PATCH 35/77] feat(UI-UX): adds support for multiple profiles [TTTK-20] --- .gitignore | 3 ++- Client/Data/profiles.json | 1 - Client/profile_save.py | 38 +++++++++++++++++++---------------- Server/player.py | 6 +++--- UI/lib/custom_tk.py | 4 ++-- UI/lib/tttk_tk.py | 3 ++- UI/main.py | 5 ++++- UI/multi.py | 8 ++++---- UI/profile.py | 42 +++++++++++++++++++++++++++++++-------- UI/single.py | 2 +- 10 files changed, 73 insertions(+), 39 deletions(-) delete mode 100644 Client/Data/profiles.json diff --git a/.gitignore b/.gitignore index 1a40556..4e5c086 100644 --- a/.gitignore +++ b/.gitignore @@ -49,4 +49,5 @@ dist/** package.nls.*.json l10n/ launch.json -venv \ No newline at end of file +venv +Client/Data/profiles.json diff --git a/Client/Data/profiles.json b/Client/Data/profiles.json deleted file mode 100644 index 0637a08..0000000 --- a/Client/Data/profiles.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/Client/profile_save.py b/Client/profile_save.py index 82b1d62..333037c 100644 --- a/Client/profile_save.py +++ b/Client/profile_save.py @@ -3,31 +3,34 @@ import os from Server.player import Player +path=os.path.abspath('Client/Data/profiles.json') class Profile: """ This class is used to handle the profiles.json file. It is used to get, set, and add profiles to the file. """ - def __init__(self, path=os.path.abspath('Client/Data/profiles.json')): - self.path = path - def get_profiles(self): + def get_profiles(): + global path """ This method returns all the profiles from the file :return: An array of all profiles """ - if exists(self.path): - with open(self.path, 'r') as file: + if exists(path): + with open(path, 'r') as file: data = json.load(file) output = [] - for profile in data: + profile_data = data[0] + selected = data[1] + for profile in profile_data: output.append(Player.from_dict(profile)) - return output + return output, selected else: - return [] + return [], 0 - def set_profiles(self, players: list): + def set_profiles( players: list, selected: int): + global path """ This method sets the profile name and/or color by the uuid :param profile_uuid: @@ -35,19 +38,20 @@ def set_profiles(self, players: list): :param profile_color: """ - try: - with open(self.path, 'w') as file: + #try: + with open(path, 'w') as file: entry = [] for player in players: entry.append(player.as_dict()) - json.dump(entry, file) - except: - raise RuntimeError("json error: Make sure profiles.json is formatted correctly") + json.dump([entry, selected], file) + #except: + # raise RuntimeError("json error: Make sure profiles.json is formatted correctly") - def delete_all_profiles(self): + def delete_all_profiles(): + global path """ This method deletes all profiles """ - if exists(self.path): - with open(self.path, 'w') as file: + if exists(path): + with open(path, 'w') as file: file.write("[]") diff --git a/Server/player.py b/Server/player.py index e1e192b..7f44169 100644 --- a/Server/player.py +++ b/Server/player.py @@ -13,8 +13,8 @@ class Player: color (int): The color of the player. ready (bool): Whether the player is ready to start the game. """ - def __init__(self, display_name: str, color: int, uuid: UUID = uuid4(), ready:bool = False): - self.uuid: UUID = uuid + def __init__(self, display_name: str, color: int, uuid: UUID = None, ready:bool = False): + self.uuid: UUID = uuid if uuid else uuid4() self.display_name = display_name self.color = color self.ready = ready @@ -34,4 +34,4 @@ def __eq__(self, other) -> bool: @classmethod def from_dict(cls, player_dict: dict): - return cls(player_dict["display_name"], player_dict["color"], player_dict["uuid"], player_dict["ready"]) \ No newline at end of file + return cls(player_dict["display_name"], player_dict["color"], UUID(player_dict["uuid"]), player_dict["ready"]) \ No newline at end of file diff --git a/UI/lib/custom_tk.py b/UI/lib/custom_tk.py index 5575df1..6028fbf 100644 --- a/UI/lib/custom_tk.py +++ b/UI/lib/custom_tk.py @@ -445,14 +445,14 @@ def cget(self, key): __getitem__ = cget class OptionMenu(tk.OptionMenu): - def __init__(self, master, variable, value, *values, **kwargs) -> None: + def __init__(self, master, variable, *values, **kwargs) -> None: filter = ('font', 'bg') attributes = dict() kwargs_copy = dict(kwargs) for key, value in kwargs_copy.items(): if key in filter: attributes[key] = kwargs.pop(key, None) - super().__init__(master, variable, value, *values, **kwargs) + super().__init__(master, variable, *values, **kwargs) for key, value in attributes.items(): self.config(**{key: value}) diff --git a/UI/lib/tttk_tk.py b/UI/lib/tttk_tk.py index 0e26abc..f71cef4 100644 --- a/UI/lib/tttk_tk.py +++ b/UI/lib/tttk_tk.py @@ -22,7 +22,8 @@ def __init__(self, Widget: tk.Widget, master: tk.Misc, *args, **kwargs): 'font': ("Arial bold", 12), 'margin': 5, 'bg': '#FFFFFF', - 'width': 0,} | defaultValues + #'width': 0, + } | defaultValues if(Widget == tk.Frame): del kwargs['defaultValues']['font'] super().__init__(Widget, master, *args, **kwargs) diff --git a/UI/main.py b/UI/main.py index 86c82d2..585289c 100644 --- a/UI/main.py +++ b/UI/main.py @@ -4,6 +4,7 @@ from queue import Queue from .menu import Menu +from Client.profile_save import Profile as ProfileIO class Root(tk.Tk): def __init__(self): @@ -14,12 +15,14 @@ def __init__(self): min_height = 250 self.devOptions = False - self.player = None + self.players, self.player = ProfileIO.get_profiles() self.ai_thread = None self.network_events = {} self.out_queue = Queue() self.in_queue = Queue() + + self.geometry(f"{start_width}x{start_height}") self.minsize(width=min_width, height=min_height) self.grid_columnconfigure(0, weight=1) diff --git a/UI/multi.py b/UI/multi.py index b4bf02d..4510a04 100644 --- a/UI/multi.py +++ b/UI/multi.py @@ -25,7 +25,7 @@ def __init__(self, master, *args, opponent=player_type.unknown, **kwargs): self.bind('Destroy', lambda *args: self.on_destroy()) self.ready = False if opponent != player_type.unknown: - server_thread(self.master.player) + server_thread(self.master.players[self.master.player]) if opponent in [player_type.ai_strong, player_type.ai_weak]: ai_context = AIContext(AdvancedAIStrategy() if opponent == player_type.ai_strong else WeakAIStrategy()) self.master.ai = ai_context.run_strategy() @@ -63,7 +63,7 @@ def _update_lobby(self, queue): rdy = '\u2611' if player.ready else '' self.playerlist.append([tk.Label(self, text=rdy + ' ' + player.display_name), tk.Button(self, text='Kick', command=lambda uuid=player.uuid, *args: self.master.out_queue.put({'message_type': 'lobby/kick', 'args' : {'player_to_kick': uuid}}))]) - if(str(player.uuid) == str(self.master.player.uuid)): + if(str(player.uuid) == str(self.master.players[self.master.player].uuid)): self.ready = player.ready if(player.ready): self.btnRdy.config(text="not Ready") @@ -114,7 +114,7 @@ def manually(self): def _connect(self): root = self.master.master - root.network_client = client_thread(root, in_queue=root.out_queue, out_queue=root.in_queue, player=root.player, ip=self.etrAddress.get()) + root.network_client = client_thread(root, in_queue=root.out_queue, out_queue=root.in_queue, player=root.players[root.player], ip=self.etrAddress.get()) root.show(Join) class Multiplayer(base_frame): @@ -153,5 +153,5 @@ def _display_widgets(self): self.btnMenu.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=5, row=1) def _create_online_game(self): - self.master.network_thread = client_thread(self.master, in_queue=self.master.out_queue, out_queue=self.master.in_queue, player=self.master.player, ip='localhost') + self.master.network_thread = client_thread(self.master, in_queue=self.master.out_queue, out_queue=self.master.in_queue, player=self.master.players[self.master.player], ip='localhost') self.master.show(Join, opponent=player_type.network) \ No newline at end of file diff --git a/UI/profile.py b/UI/profile.py index 12ee12c..98fc22f 100644 --- a/UI/profile.py +++ b/UI/profile.py @@ -5,6 +5,7 @@ from .base_frame import base_frame from .field_frame import Field from Server.player import Player +from Client.profile_save import Profile as ProfileIO class NewProfile(base_frame): def __init__(self, master, *args, **kwargs): @@ -12,6 +13,7 @@ def __init__(self, master, *args, **kwargs): self.address_toogle = False self.next = kwargs.pop('return_to', Profile) self.edit = kwargs.pop('edit', False) + self.id = kwargs.pop('id', None) self._create_widgets() self._display_widgets() @@ -20,7 +22,7 @@ def _create_widgets(self): self.lblTitle = tk.Label(self, text=f'{task} profile', font=self.master.title_font) self.lblName = tk.Label(self, text='Name') self.etrName = tk.Entry(self) - self.etrName.val = self.master.player.display_name if self.edit else '' + self.etrName.val = self.master.players[self.master.player].display_name if self.edit else '' self.btnCreate = tk.Button(self, text=f'{task} profile', command=lambda *args: self._create()) self.btnMenu = tk.Button(self, text='Menu', command=lambda: self.master.show_menu()) @@ -44,12 +46,20 @@ def _display_widgets(self): self.btnMenu.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=9, row=1) def _create(self): - self.master.player = Player(self.etrName.val, 0) + if(self.edit): + for i, player in enumerate(self.master.players): + if player.uuid == self.master.players[self.master.player].uuid: + self.master.players[i] = Player(self.etrName.val, 0) + else: + self.master.players.append(Player(self.etrName.val, 0)) + #self.master.player = Player(self.etrName.val, 0) + print([o.uuid for o in self.master.players]) + ProfileIO.set_profiles(self.master.players, self.master.player) self.master.show(self.next) class Profile(base_frame): def __new__(cls, master, *args, **kwargs): - if not master.player: + if len(master.players) == 0: return NewProfile(master, *args, **kwargs) return super().__new__(cls, *args, **kwargs) @@ -62,11 +72,16 @@ def __init__(self, master, *args, **kwargs): def _create_widgets(self): self.lblTitle = tk.Label(self, text='Multiplayer', font=self.master.title_font) self.lblName = tk.Label(self, text='Name') - self.lblNameValue = tk.Label(self, text=self.master.player.display_name) + #self.lblNameValue = tk.Label(self, text=self.master.player.display_name) + self.lblvar = tk.StringVar(self, self.master.players[self.master.player].display_name) + self.lblvar.trace_add('write', self._dropdown_changed) + #print([o.display_name for o in self.master.players]) + self.lblNameValue = tk.OptionMenu(self, self.lblvar, *[o.display_name for o in self.master.players]) #[o.attr for o in objs] self.lblUUDI = tk.Label(self, text='User ID') - self.lblUUIDValue = tk.Label(self, text=self.master.player.uuid) - self.btnEdit = tk.Button(self, text='Edit Profile', command=lambda *args: self.master.show(NewProfile, edit=True)) + self.lblUUIDValue = tk.Label(self, text=self.master.players[self.master.player].uuid) + self.btnEdit = tk.Button(self, text='Edit Profile', command=lambda *args: self.master.show(NewProfile, edit=True, id=self.master.player)) self.btnDelete = tk.Button(self, text='Delete profile', command=lambda *args: self._delete()) + self.btnAdd = tk.Button(self, text='Add profile', command=lambda *args: self.master.show(NewProfile)) self.btnMenu = tk.Button(self, text='Menu', command=lambda: self.master.show_menu()) def _display_widgets(self): @@ -88,9 +103,20 @@ def _display_widgets(self): self.lblUUDI.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=6) self.lblUUIDValue.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=4, row=6, columnspan=5) self.btnDelete.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=8, columnspan=3) + self.btnAdd.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=4, row=8, columnspan=3) self.btnEdit.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=6, row=8, columnspan=3) self.btnMenu.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=9, row=1) def _delete(self): - self.master.player = None - self.master.show(NewProfile) \ No newline at end of file + del self.master.players[self.master.player] + self.master.player = 0 + self.master.show(Profile) + ProfileIO.set_profiles(self.master.players, self.master.player) + + def _dropdown_changed(self, *args): + for i, player in enumerate(self.master.players): + if player.display_name == self.lblvar.get(): + self.master.player = i + break + ProfileIO.set_profiles(self.master.players, self.master.player) + self.lblUUIDValue.config(text=self.master.players[self.master.player].uuid) \ No newline at end of file diff --git a/UI/single.py b/UI/single.py index 94df777..03ca6e2 100644 --- a/UI/single.py +++ b/UI/single.py @@ -41,7 +41,7 @@ def _display_widgets(self): self.btnExit.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=5, row=1) def join_ai(self, strong: bool): - self.master.network_thread = client_thread(self.master, in_queue=self.master.out_queue, out_queue=self.master.in_queue, player=self.master.player, ip='localhost') + self.master.network_thread = client_thread(self.master, in_queue=self.master.out_queue, out_queue=self.master.in_queue, player=self.master.players[self.master.player], ip='localhost') if strong: self.master.show(Join, opponent=player_type.ai_strong) else: From 056526728e321628c2273618a6da82a015f58b59 Mon Sep 17 00:00:00 2001 From: Petzys <87223648+Petzys@users.noreply.github.com> Date: Thu, 14 Mar 2024 16:38:10 +0100 Subject: [PATCH 36/77] Fix(ui client): TTTK-81 Now using generic UI event --- Client/ui_client.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/Client/ui_client.py b/Client/ui_client.py index 5cb2d9a..d111d9d 100644 --- a/Client/ui_client.py +++ b/Client/ui_client.py @@ -63,7 +63,7 @@ async def _message_handler(self, message_type: str): "message_type": "lobby/status", "player": self._lobby_status }) - self._tk_root.event_generate("<>", when="tail") + self._tk_root.event_generate("<>", when="tail") case "game/start": self._out_queue.put({ "message_type": "game/start", @@ -72,7 +72,7 @@ async def _message_handler(self, message_type: str): "opponent": self._opponent, "opponent_symbol": self._symbol != "X" }) - self._tk_root.event_generate("<>", when="tail") + self._tk_root.event_generate("<>", when="tail") case "game/end": self._out_queue.put({ "message_type": "game/end", @@ -80,25 +80,23 @@ async def _message_handler(self, message_type: str): "win": self._winner == self._player, "final_playfield": self._playfield }) - self._tk_root.event_generate("<>", when="tail") + self._tk_root.event_generate("<>", when="tail") await self.close() case "game/turn": self.send_gamestate_to_ui() - case "statistics/statistics": - pass case "game/error": self._out_queue.put({ "message_type": "game/error", "error_message": self._error_history[-1] }) - self._tk_root.event_generate("<>", when="tail") + self._tk_root.event_generate("<>", when="tail") case "chat/receive": self._out_queue.put({ "message_type": "chat/receive", "sender": self._chat_history[-1][0], "message": self._chat_history[-1][1] }) - self._tk_root.event_generate("<>", when="tail") + self._tk_root.event_generate("<>", when="tail") return @@ -108,7 +106,7 @@ def send_gamestate_to_ui(self): "next_player": int(self._current_player == self._opponent), "playfield": self._playfield }) - self._tk_root.event_generate("<>", when="tail") + self._tk_root.event_generate("<>", when="tail") async def await_commands(self): # Send messages to the server @@ -142,7 +140,7 @@ async def await_commands(self): "message_type": "statistics/statistics", "statistics": self._statistics }) - self._tk_root.event_generate("<>", when="tail") + self._tk_root.event_generate("<>", when="tail") case _: logger.error(f"Unknown message type received from UI in in_queue: {message['message_type']}") return From 76efcb836fa6abb11dad0681502462841a31ea5a Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Sun, 17 Mar 2024 13:45:47 +0100 Subject: [PATCH 37/77] feat(UI-UX): UI implement generic event listener and using message_type from queue to differentiate [TTTK-81] --- UI/main.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/UI/main.py b/UI/main.py index 585289c..e0522d6 100644 --- a/UI/main.py +++ b/UI/main.py @@ -33,12 +33,7 @@ def __init__(self): self.geometry("700x500") self.frames = {} self.current_frame = None - self.bind("<>", lambda *args: self.network_event_handler('lobby/status')) - self.bind("<>", lambda *args: self.network_event_handler('game/start')) - self.bind("<>", lambda *args: self.network_event_handler('game/end')) - self.bind("<>", lambda *args: self.network_event_handler('game/error')) - self.bind("<>", lambda *args: self.network_event_handler('game/turn')) - self.bind("<>", lambda *args: self.network_event_handler('chat/receive')) + self.bind("<>", lambda *args: self.network_event_handler()) self.show(Menu, True) def show(self, Frame, *args, cache=False, **kwargs): @@ -72,10 +67,10 @@ def start_mainloop(self): def start_server(self): pass - def network_event_handler(self, event): + def network_event_handler(self): try: queue = self.in_queue.get() - self.network_events[event](queue) + self.network_events[queue.pop('message_type', 'message type not found')](queue) except KeyError: pass From 5412d0657b68832facc0a01af37dff34f7a9da5b Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Sun, 17 Mar 2024 15:53:45 +0100 Subject: [PATCH 38/77] feat(UI-UX): added support for local multiplayer, refactored out_queue handling --- Client/ui_client.py | 2 +- UI/chat.py | 7 +++- UI/field_frame.py | 31 +++++++++------- UI/gamefield.py | 38 +++++++++++++++---- UI/main.py | 8 ++-- UI/multi.py | 89 +++++++++++++++++++++++++++++++++++++-------- UI/single.py | 11 ++++-- 7 files changed, 141 insertions(+), 45 deletions(-) diff --git a/Client/ui_client.py b/Client/ui_client.py index d111d9d..2897697 100644 --- a/Client/ui_client.py +++ b/Client/ui_client.py @@ -103,7 +103,7 @@ async def _message_handler(self, message_type: str): def send_gamestate_to_ui(self): self._out_queue.put({ "message_type": "game/turn", - "next_player": int(self._current_player == self._opponent), + "next_player": self._current_player.uuid, "playfield": self._playfield }) self._tk_root.event_generate("<>", when="tail") diff --git a/UI/chat.py b/UI/chat.py index 88a3435..e501b41 100644 --- a/UI/chat.py +++ b/UI/chat.py @@ -29,10 +29,13 @@ def _display_widgets(self): self.btnSend.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=1, row=1, columnspan=2) def _send(self): - self.root.out_queue.put({'message_type': 'chat/message', 'args' : {'message': self.etrMessage.val}}) + self.root.out_queue.values[0].put({'message_type': 'chat/message', 'args' : {'message': self.etrMessage.val}}) self.etrMessage.val = "" def _chat_receive(self, queue): self.txtChat.config(state=tk.NORMAL) self.txtChat.insert(tk.END, f"{queue['sender'].display_name}: {queue['message']}\n") - self.txtChat.config(state=tk.DISABLED) \ No newline at end of file + self.txtChat.config(state=tk.DISABLED) + + def on_destroy(self): + del self.master.network_events['chat/receive'] \ No newline at end of file diff --git a/UI/field_frame.py b/UI/field_frame.py index 6257d8b..3b7cf5b 100644 --- a/UI/field_frame.py +++ b/UI/field_frame.py @@ -7,6 +7,7 @@ from .endscreen import EndScreen from .messages import messages from .chat import Chat +from .lib.colors import color class player_type(Enum): local = auto() @@ -16,25 +17,27 @@ class player_type(Enum): unknown = auto() class player(tk.Container): - def __init__(self, master, number): + def __init__(self, master, number, uuid=None): super().__init__(master) + self.uuid = uuid self._create_widgets(number) self._display_widgets() def _create_widgets(self, number): - - self.heading = tk.Label(self, text=f'Player {number}', font=self.master.master.title_font) - self.name = tk.Label(self, text=f'Player {number}') - self.symbol = tk.Label(self, text="unknown") + self.frame = tk.Frame(self) + self.heading = tk.Label(self.frame.widget, text=f'Player {number}', font=self.master.master.title_font) + self.name = tk.Label(self.frame.widget, text=f'Player {number}') + self.symbol = tk.Label(self.frame.widget, text="unknown") def highlight(self, highlight=True): if(highlight): - pass + self.frame.config(border_color='green') else: - pass + self.frame.config(border_color=color.anthracite) - def set(self, name, type): + def set(self, name, type, uuid): self.name.config(text=name) + self.uuid = uuid match type: case player_type.local: self.symbol.config(text="Lokal") @@ -47,16 +50,17 @@ def set(self, name, type): #durch pictogramme ersetzen def _display_widgets(self): + self.frame.pack(fill=tk.BOTH, expand=True) self.heading.grid(row=0, column=0, columnspan=2) self.name.grid(row=1, column=0) self.symbol.grid(row=1, column=1) class field_controller(): - def __init__(self, view, players): + def __init__(self, view, players, starting_uuid, **kwargs): self.view = view - self.sub_controller = gamefield_controller(self.view.gamefield) + self.sub_controller = gamefield_controller(self.view.gamefield, starting_uuid, **kwargs) for player_lbl, player in zip(self.view.player, players): - player_lbl.set(player.display_name, player_type.unknown) + player_lbl.set(player.display_name, player_type.unknown, player.uuid) self._bind() def _bind(self): @@ -75,15 +79,16 @@ class Field(base_frame): def __init__(self, master, chat, *args, starting_player, starting_player_symbol, opponent, opponent_symbol, **kwargs): super().__init__(master) self._create_widgets(chat) - self.controller = field_controller(self, [starting_player, opponent]) + self.controller = field_controller(self, [starting_player, opponent], starting_player.uuid, **kwargs) self._display_widgets() + print("wigets displayed") #self.bind("<>", self.controller.sub_controller.turn) #self.bind("<>", self.controller.end) #self.bind("<>", self.controller.error) self.master.network_events['game/turn'] = self.controller.sub_controller.turn self.master.network_events['game/end'] = self.controller.end self.master.network_events['game/error'] = self.controller.error - self.master.out_queue.put({'message_type': 'game/gamestate', 'args' :{} }) + self.master.out_queue[starting_player.uuid].put({'message_type': 'game/gamestate', 'args' :{} }) def _create_widgets(self, chat): self.heading = tk.Label(self, text="Tic Tac Toe Kojote", font=self.master.title_font) diff --git a/UI/gamefield.py b/UI/gamefield.py index 0e47c05..a00a522 100644 --- a/UI/gamefield.py +++ b/UI/gamefield.py @@ -1,6 +1,11 @@ import tkinter as tk +from enum import Enum, auto +from uuid import UUID -from .endscreen import EndScreen +class input_methods(Enum): + mouse = auto() + qeyc = auto() + uom= auto() class gamefield(tk.Frame): def __init__(self, master): @@ -21,13 +26,22 @@ def _display_widgets(self): field.grid(sticky=tk.N+tk.S+tk.E+tk.W, row=position[0], column=position[1]) class gamefield_controller: - def __init__(self, view: gamefield): + def __init__(self, view: gamefield, starting_uuid: UUID, **kwargs): self.view = view + self.currentplayer = starting_uuid + self.input_methods = {input_methods.mouse: [], input_methods.qeyc: [], input_methods.uom: []} + for uuid, input_method in kwargs.items(): + print(uuid, input_method) + self.input_methods[input_method].append(UUID(uuid)) self._bind() def _bind(self): for position, button in self.view.fields.items(): - button.config(command=lambda e=position: self._game_input(e)) + button.config(command=lambda e=position: self._game_input(e, input_methods.mouse)) + for position, button in zip(self.view.fields.keys(),['q', 'w', 'e', 'a', 's', 'd', 'y', 'x', 'c']): + self.view.bind(f'', lambda e=position: self._game_input(e, input_methods.qeyc)) + for position, button in zip(self.view.fields.keys(),['u', 'i', 'o', 'j', 'k', 'l', 'm', ',', '.']): + self.view.bind(f'', lambda e=position: self._game_input(e, input_methods.uom)) def draw_field(self, matrix=None, position=None, value=None): #either matrix as a 3x3 list or position and value need to be provided if matrix != None: @@ -38,14 +52,24 @@ def draw_field(self, matrix=None, position=None, value=None): #either matrix as self.view.fields[position].config(text=value) def change_active_player(self, player_id: int): - for i, player in enumerate(self.view.master.player): - player.highlight(i == player_id) + self.currentplayer = player_id + for player in self.view.master.player: + player.highlight(highlight=(self.currentplayer == player.uuid)) def turn(self, queue, *args): root = self.view.master.master self.draw_field(matrix=queue['playfield']) self.change_active_player(queue['next_player']) - def _game_input(self, position): + def _game_input(self, position, type: input_methods): root = self.view.master.master - root.out_queue.put({'message_type': 'game/make_move', 'args': {'x': position[0], 'y': position[1]}}) \ No newline at end of file + match(len(self.input_methods[type])): + case 1: + root.out_queue[self.input_methods[type][0]].put({'message_type': 'game/make_move', 'args': {'x': position[0], 'y': position[1]}}) + case 2: + if(self.currentplayer in self.input_methods[type]): + root.out_queue[self.currentplayer].put({'message_type': 'game/make_move', 'args': {'x': position[0], 'y': position[1]}}) + else: + pass + case _: + pass \ No newline at end of file diff --git a/UI/main.py b/UI/main.py index e0522d6..86c1f3c 100644 --- a/UI/main.py +++ b/UI/main.py @@ -1,4 +1,5 @@ -import tkinter as tk +#import tkinter as tk +from .lib import tttk_tk as tk from tkinter import _tkinter from tkinter import font as tkfont from queue import Queue @@ -18,10 +19,9 @@ def __init__(self): self.players, self.player = ProfileIO.get_profiles() self.ai_thread = None self.network_events = {} - self.out_queue = Queue() + self.out_queue = {} self.in_queue = Queue() - - + self.dummy = tk.Container() self.geometry(f"{start_width}x{start_height}") self.minsize(width=min_width, height=min_height) diff --git a/UI/multi.py b/UI/multi.py index 4d6c665..b9c03b5 100644 --- a/UI/multi.py +++ b/UI/multi.py @@ -1,30 +1,34 @@ #import tkinter as tk from .lib import tttk_tk as tk from uuid import UUID +from queue import Queue from .base_frame import base_frame from .field_frame import Field -from .profile import Profile +from .profile import Profile, NewProfile from Client.ui_client import client_thread from .field_frame import player_type from Server.main import server_thread from AI.ai_context import AIContext -from AI.ai_strategy import AIStrategy, WeakAIStrategy, AdvancedAIStrategy +from AI.ai_strategy import WeakAIStrategy, AdvancedAIStrategy from .chat import Chat +from .messages import messages +from .gamefield import input_methods class Join(base_frame): - def __init__(self, master, *args, opponent=player_type.unknown, **kwargs): + def __init__(self, master, *args, opponent=player_type.unknown, local_players, **kwargs): super().__init__(master) self._create_widgets(opponent) self._display_widgets() self.playerlist = [] + self.local_players = local_players #self.bind('<>', self._update_lobby) #self.bind('<>', self._start_game) self.master.network_events['lobby/status'] = self._update_lobby self.master.network_events['game/start'] = self._start_game self.bind('Destroy', lambda *args: self.on_destroy()) self.ready = False - if opponent != player_type.unknown: + if opponent not in [player_type.unknown, player_type.local]: server_thread(self.master.players[self.master.player]) if opponent in [player_type.ai_strong, player_type.ai_weak]: ai_context = AIContext(AdvancedAIStrategy() if opponent == player_type.ai_strong else WeakAIStrategy()) @@ -33,9 +37,7 @@ def __init__(self, master, *args, opponent=player_type.unknown, **kwargs): def _create_widgets(self, opponent): title = 'Waiting for players to join' if opponent in [player_type.network, player_type.unknown] else 'Play local game against AI' if opponent in [player_type.ai_weak, player_type.ai_strong] else 'Play local game against a friend' self.lblTitle = tk.Label(self, text=title, font=self.master.title_font) - self.btnRdy = tk.Button(self, text='Start', command=lambda *args: self.master.out_queue.put({'message_type': 'lobby/ready', 'args' : {'ready': not self.ready}})) - if opponent == player_type.local: - self.btnRdy2 = tk.Button(self, text='Start', command=lambda *args: self.master.out_queue.put({'message_type': 'lobby/ready', 'args' : {'ready': True}})) + self.btnRdy = tk.Button(self, text='Start', command=lambda *args: list(self.master.out_queue.values())[0].put({'message_type': 'lobby/ready', 'args' : {'ready': not self.ready}})) self.btnExit = tk.Button(self, text='Menu', command=lambda: self.master.show_menu()) self.chat = Chat(self, self.master) @@ -62,7 +64,7 @@ def _update_lobby(self, queue): for player in queue['player']: rdy = '\u2611' if player.ready else '' self.playerlist.append([tk.Label(self, text=rdy + ' ' + player.display_name), - tk.Button(self, text='Kick', command=lambda uuid=player.uuid, *args: self.master.out_queue.put({'message_type': 'lobby/kick', 'args' : {'player_to_kick': uuid}}))]) + tk.Button(self, text='Kick', command=lambda uuid=player.uuid, *args: list(self.master.out_queue.values())[0].put({'message_type': 'lobby/kick', 'args' : {'player_to_kick': uuid}}))]) if(str(player.uuid) == str(self.master.players[self.master.player].uuid)): self.ready = player.ready if(player.ready): @@ -75,8 +77,7 @@ def _update_lobby(self, queue): def _start_game(self, queue): - print(queue) - self.master.show(Field, self.chat.txtChat.get("1.0", tk.END+"-1c")+"Game starting\n", **queue) + self.master.show(Field, self.chat.txtChat.get("1.0", tk.END+"-1c")+"Game starting\n", **queue, **{str(p): input_methods.mouse for p in self.local_players}) def on_destroy(self): del self.master.network_events['lobby/status'] @@ -114,8 +115,61 @@ def manually(self): def _connect(self): root = self.master.master - root.network_client = client_thread(root, in_queue=root.out_queue, out_queue=root.in_queue, player=root.players[root.player], ip=self.etrAddress.get()) - root.show(Join) + root.out_queue = {root.players[root.player].uuid: Queue()} + root.network_client = client_thread(root, in_queue=list(root.out_queue.values())[0], out_queue=root.in_queue, player=root.players[root.player], ip=self.etrAddress.get()) + root.show(Join, local_players=[root.players[root.player].uuid]) + +class LocalProfileSelection(base_frame): + def __init__(self, master, *args, **kwargs): + super().__init__(master) + self._create_widgets() + self._display_widgets() + + def _create_widgets(self): + self.lblTitle = tk.Label(self, text='Select your profiles', font=self.master.title_font) + self.lblPlayer1 = tk.Label(self, text='Player 1') + self.lblPlayer2 = tk.Label(self, text='Player 2') + self.varPlayer1 = tk.StringVar(self, value='Select') + self.varPlayer2 = tk.StringVar(self, value='Select') + self.drpPlayer1 = tk.OptionMenu(self, self.varPlayer1, *[o.display_name for o in self.master.players]) + self.drpPlayer2 = tk.OptionMenu(self, self.varPlayer2, *[o.display_name for o in self.master.players]) + self.btnnew = tk.Button(self, text='New Profile', command=lambda *args: self.master.show(NewProfile, return_to=LocalProfileSelection)) + self.btnStart = tk.Button(self, text='Start', command=lambda *args: self._start_game()) + + def _display_widgets(self): + self.columnconfigure([0, 6], weight=1) + self.columnconfigure([1, 5], weight=2) + self.columnconfigure([2, 4], weight=4) + self.columnconfigure([3], weight=2) + self.rowconfigure([0, 11], weight=1) + self.rowconfigure([2], weight=2) + self.rowconfigure([4, 6, 8, 10], weight=4) + self.rowconfigure([3, 5, 7, 9, 11], weight=2) + self.lblTitle.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=2, columnspan=3) + self.lblPlayer1.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=3) + self.lblPlayer2.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=4) + self.drpPlayer1.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=3, row=3, columnspan=2) + self.drpPlayer2.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=3, row=4, columnspan=2) + self.btnnew.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=5) + self.btnStart.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=4, row=5, columnspan=2) + + def _start_game(self): + if(self.varPlayer1.get() == 'Select' or self.varPlayer2.get() == 'Select' or self.varPlayer1.get() == self.varPlayer2.get()): + msg = messages(type='info', message='Please select two different players') + msg.display() + return + for player in self.master.players: + if player.display_name == self.varPlayer1.get(): + player1 = player + if player.display_name == self.varPlayer2.get(): + player2 = player + self.master.out_queue = {player1.uuid: Queue(), player2.uuid: Queue()} + server_thread(player1) + self.master.network_thread = client_thread(self.master, in_queue=self.master.out_queue[player1.uuid], out_queue=self.master.in_queue, player=player1, ip='localhost') + client_thread(self.master.dummy, in_queue=self.master.out_queue[player2.uuid], out_queue=Queue(), player=player2, ip='localhost') + self.master.out_queue[player1.uuid].put({'message_type': 'lobby/ready', 'args' : {'ready': True}}) + self.master.out_queue[player2.uuid].put({'message_type': 'lobby/ready', 'args' : {'ready': True}}) + self.master.show(Join, opponent=player_type.local, local_players=[player1.uuid, player2.uuid]) class Multiplayer(base_frame): def __new__(cls, master, *args, **kwargs): @@ -125,6 +179,7 @@ def __new__(cls, master, *args, **kwargs): def __init__(self, master, *args): super().__init__(master) + self.master.out_queue = {} self._create_widgets() self._display_widgets() self.address_toogle = False @@ -132,7 +187,7 @@ def __init__(self, master, *args): def _create_widgets(self): self.lblTitle = tk.Label(self, text='Multiplayer', font=self.master.title_font) self.btnNew = tk.Button(self, text='Create a new online game', command=lambda *args: self._create_online_game()) - self.btnLocal = tk.Button(self, text='Create local Game', command=lambda*args : self.master.show(Join, opponent=player_type.local)) + self.btnLocal = tk.Button(self, text='Create local Game', command=lambda*args : self._create_local_game()) self.lobbyOverview = Lobby_Overview(self) self.btnMenu = tk.Button(self, text='Menu', command=lambda: self.master.show_menu()) @@ -153,5 +208,9 @@ def _display_widgets(self): self.btnMenu.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=5, row=1) def _create_online_game(self): - self.master.network_thread = client_thread(self.master, in_queue=self.master.out_queue, out_queue=self.master.in_queue, player=self.master.players[self.master.player], ip='localhost') - self.master.show(Join, opponent=player_type.network) \ No newline at end of file + self.master.out_queue = {self.master.players[self.master.player].uuid: Queue()} + self.master.network_thread = client_thread(self.master, in_queue=list(self.master.out_queue.values())[0], out_queue=self.master.in_queue, player=self.master.players[self.master.player], ip='localhost') + self.master.show(Join, opponent=player_type.network, local_players=[self.master.players[self.master.player].uuid]) + + def _create_local_game(self): + self.master.show(LocalProfileSelection, opponent=player_type.local) \ No newline at end of file diff --git a/UI/single.py b/UI/single.py index 03ca6e2..98a2d2f 100644 --- a/UI/single.py +++ b/UI/single.py @@ -1,5 +1,6 @@ #import tkinter as tk from .lib import tttk_tk as tk +from queue import Queue from .base_frame import base_frame from .multi import Join @@ -15,6 +16,7 @@ def __new__(cls, master, *args, **kwargs): def __init__(self, master, *args, **kwargs): super().__init__(master) + self.master.out_queue = {} self._create_widgets() self._display_widgets() self.address_toogle = False @@ -41,8 +43,11 @@ def _display_widgets(self): self.btnExit.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=5, row=1) def join_ai(self, strong: bool): - self.master.network_thread = client_thread(self.master, in_queue=self.master.out_queue, out_queue=self.master.in_queue, player=self.master.players[self.master.player], ip='localhost') + self.master.out_queue = {self.master.players[self.master.player].uuid: Queue()} + test = list(self.master.out_queue.values())[0] + self.master.network_thread = client_thread(self.master, in_queue=list(self.master.out_queue.values())[0], out_queue=self.master.in_queue, player=self.master.players[self.master.player], ip='localhost') if strong: - self.master.show(Join, opponent=player_type.ai_strong) + ai = player_type.ai_strong else: - self.master.show(Join, opponent=player_type.ai_weak) + ai = player_type.ai_weak + self.master.show(Join, opponent=ai, local_players=[self.master.players[self.master.player].uuid]) From 09a5f89e87937cd28e619e9218e3d8e8ed8547c9 Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Sun, 17 Mar 2024 16:32:06 +0100 Subject: [PATCH 39/77] feat(UI-UX): set focus and bin enter keys --- UI/chat.py | 9 ++++-- UI/field_frame.py | 13 +++++--- UI/multi.py | 80 +++++++++++++++++++++++++---------------------- 3 files changed, 57 insertions(+), 45 deletions(-) diff --git a/UI/chat.py b/UI/chat.py index e501b41..6dfcbbc 100644 --- a/UI/chat.py +++ b/UI/chat.py @@ -6,6 +6,7 @@ def __init__(self, master, root, chat='', *args, **kwargs): self.root = root self._create_widgets(chat) self._display_widgets() + self.root.bind('', lambda *args: self._enter()) self.root.network_events['chat/receive'] = self._chat_receive def _create_widgets(self, chat): @@ -29,7 +30,7 @@ def _display_widgets(self): self.btnSend.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=1, row=1, columnspan=2) def _send(self): - self.root.out_queue.values[0].put({'message_type': 'chat/message', 'args' : {'message': self.etrMessage.val}}) + list(self.root.out_queue.values())[0].put({'message_type': 'chat/message', 'args' : {'message': self.etrMessage.val}}) self.etrMessage.val = "" def _chat_receive(self, queue): @@ -38,4 +39,8 @@ def _chat_receive(self, queue): self.txtChat.config(state=tk.DISABLED) def on_destroy(self): - del self.master.network_events['chat/receive'] \ No newline at end of file + del self.master.network_events['chat/receive'] + + def _enter(self): + if(self.focus_get() == self.etrMessage.widget): + self._send() \ No newline at end of file diff --git a/UI/field_frame.py b/UI/field_frame.py index 3b7cf5b..799f619 100644 --- a/UI/field_frame.py +++ b/UI/field_frame.py @@ -78,7 +78,7 @@ def error(self, queue, *args): class Field(base_frame): def __init__(self, master, chat, *args, starting_player, starting_player_symbol, opponent, opponent_symbol, **kwargs): super().__init__(master) - self._create_widgets(chat) + self._create_widgets(chat, display_chat=len(kwargs)==1) self.controller = field_controller(self, [starting_player, opponent], starting_player.uuid, **kwargs) self._display_widgets() print("wigets displayed") @@ -90,24 +90,27 @@ def __init__(self, master, chat, *args, starting_player, starting_player_symbol, self.master.network_events['game/error'] = self.controller.error self.master.out_queue[starting_player.uuid].put({'message_type': 'game/gamestate', 'args' :{} }) - def _create_widgets(self, chat): + def _create_widgets(self, chat, display_chat=True): self.heading = tk.Label(self, text="Tic Tac Toe Kojote", font=self.master.title_font) self.player = [] self.player.append(player(self, 1)) self.player.append(player(self, 2)) self.gamefield = gamefield(self) - self.chat = Chat(self, self.master, chat) + if(display_chat): + self.chat = Chat(self, self.master, chat) self.close = tk.Button(self, text="close") def _display_widgets(self): - self.columnconfigure([1,3], weight=1) + self.columnconfigure([1], weight=1) self.rowconfigure(2, weight=1) self.heading.grid(row=0, column=0, columnspan=3) self.player[0].grid(row=1, column=0) self.player[1].grid(row=1, column=2) self.gamefield.grid(sticky=tk.N+tk.S+tk.E+tk.W, row=2, column=1) - self.chat.grid(sticky=tk.N+tk.S+tk.E+tk.W, row=1, column=3, rowspan=3) + if(hasattr(self, 'chat')): + self.columnconfigure(3, weight=1) + self.chat.grid(sticky=tk.N+tk.S+tk.E+tk.W, row=1, column=3, rowspan=3) self.close.grid(row=3, column=2) def on_destroy(self): diff --git a/UI/multi.py b/UI/multi.py index b9c03b5..7f2d58e 100644 --- a/UI/multi.py +++ b/UI/multi.py @@ -54,8 +54,6 @@ def _display_widgets(self): # display the buttons created in the _create_widgets method self.lblTitle.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=2, columnspan=3) self.btnRdy.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=10) - if hasattr(self, 'btnRdy2'): - self.btnRdy2.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=10) self.chat.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=4, row=4, columnspan=2, rowspan=7) self.btnExit.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=5, row=1) @@ -83,42 +81,6 @@ def on_destroy(self): del self.master.network_events['lobby/status'] del self.master.network_events['game/start'] -class Lobby_Overview(tk.Container): - def __init__(self, master): - super().__init__(master) - self._create_widgets() - self._display_widgets() - - def _create_widgets(self): - self.frame = tk.Frame(self) - self.innerframe = self.frame.widget - self.lblHeading = tk.Label(self.innerframe, text="Join public lobbies", font=self.master.master.title_font) - - self.btnManual = tk.Button(self.innerframe, text="Join by address", command=lambda *args: self.manually()) - self.etrAddress = tk.Entry(self.innerframe) - self.btnConnect = tk.Button(self.innerframe, text="Connect", command=lambda *args: self._connect()) - - def _display_widgets(self): - self.frame.pack(expand=True, fill=tk.BOTH) - self.innerframe.columnconfigure([0, 2, 4], weight=1) - self.innerframe.columnconfigure([1, 3], weight=5) - self.innerframe.rowconfigure([0, 4], weight=2) - self.innerframe.rowconfigure([1, 3], weight=1) - self.innerframe.rowconfigure([2], weight=40) - self.lblHeading.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=1, row=0, columnspan=3) - self.btnManual.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=1, row=4, columnspan=3) - - def manually(self): - self.btnManual.grid_forget() - self.etrAddress.grid(column=1, row=10, sticky=tk.E+tk.W+tk.N+tk.S) - self.btnConnect.grid(column=3, row=10, sticky=tk.E+tk.W+tk.N+tk.S) - - def _connect(self): - root = self.master.master - root.out_queue = {root.players[root.player].uuid: Queue()} - root.network_client = client_thread(root, in_queue=list(root.out_queue.values())[0], out_queue=root.in_queue, player=root.players[root.player], ip=self.etrAddress.get()) - root.show(Join, local_players=[root.players[root.player].uuid]) - class LocalProfileSelection(base_frame): def __init__(self, master, *args, **kwargs): super().__init__(master) @@ -171,6 +133,48 @@ def _start_game(self): self.master.out_queue[player2.uuid].put({'message_type': 'lobby/ready', 'args' : {'ready': True}}) self.master.show(Join, opponent=player_type.local, local_players=[player1.uuid, player2.uuid]) +class Lobby_Overview(tk.Container): + def __init__(self, master): + super().__init__(master) + self._create_widgets() + self._display_widgets() + + def _create_widgets(self): + self.frame = tk.Frame(self) + self.innerframe = self.frame.widget + self.lblHeading = tk.Label(self.innerframe, text="Join public lobbies", font=self.master.master.title_font) + + self.btnManual = tk.Button(self.innerframe, text="Join by address", command=lambda *args: self.manually()) + self.etrAddress = tk.Entry(self.innerframe) + self.btnConnect = tk.Button(self.innerframe, text="Connect", command=lambda *args: self._connect()) + self.master.master.bind('', lambda *args: self._enter()) + + def _display_widgets(self): + self.frame.pack(expand=True, fill=tk.BOTH) + self.innerframe.columnconfigure([0, 2, 4], weight=1) + self.innerframe.columnconfigure([1, 3], weight=5) + self.innerframe.rowconfigure([0, 4], weight=2) + self.innerframe.rowconfigure([1, 3], weight=1) + self.innerframe.rowconfigure([2], weight=40) + self.lblHeading.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=1, row=0, columnspan=3) + self.btnManual.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=1, row=4, columnspan=3) + + def manually(self): + self.btnManual.grid_forget() + self.etrAddress.grid(column=1, row=10, sticky=tk.E+tk.W+tk.N+tk.S) + self.btnConnect.grid(column=3, row=10, sticky=tk.E+tk.W+tk.N+tk.S) + self.etrAddress.focus_set() + + def _enter(self): + if(self.focus_get() == self.etrAddress.widget): + self._connect() + + def _connect(self): + root = self.master.master + root.out_queue = {root.players[root.player].uuid: Queue()} + root.network_client = client_thread(root, in_queue=list(root.out_queue.values())[0], out_queue=root.in_queue, player=root.players[root.player], ip=self.etrAddress.get()) + root.show(Join, local_players=[root.players[root.player].uuid]) + class Multiplayer(base_frame): def __new__(cls, master, *args, **kwargs): if(len(master.players) == 0 or master.player == None): From 4b0548be46874beb8ba5e157b8c7917ef89d2dc0 Mon Sep 17 00:00:00 2001 From: Petzys <87223648+Petzys@users.noreply.github.com> Date: Sun, 17 Mar 2024 18:44:30 +0100 Subject: [PATCH 40/77] Fix(ui-client): Changed game/start message contents for ui --- Client/ui_client.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/Client/ui_client.py b/Client/ui_client.py index 2897697..25e932d 100644 --- a/Client/ui_client.py +++ b/Client/ui_client.py @@ -68,9 +68,10 @@ async def _message_handler(self, message_type: str): self._out_queue.put({ "message_type": "game/start", "starting_player": self._current_player, - "starting_player_symbol": self._symbol == "X", - "opponent": self._opponent, - "opponent_symbol": self._symbol != "X" + "player1": self._player, + "player2": self._opponent, + "player1_symbol": self._symbol == "X", + "player2_symbol": self._symbol != "X" }) self._tk_root.event_generate("<>", when="tail") case "game/end": From 38caae51637d3e2300dd6740cda03b3ee876ef3b Mon Sep 17 00:00:00 2001 From: Petzys <87223648+Petzys@users.noreply.github.com> Date: Sun, 17 Mar 2024 18:58:35 +0100 Subject: [PATCH 41/77] Feat(ui-client): Raise connection error and try again --- Client/ui_client.py | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/Client/ui_client.py b/Client/ui_client.py index 25e932d..8c7dd14 100644 --- a/Client/ui_client.py +++ b/Client/ui_client.py @@ -151,11 +151,28 @@ async def await_commands(self): async def client_thread_function(tk_root:tk.Tk, out_queue:Queue, in_queue:Queue, player: Player, ip:str, port:int) -> None: """The function that is run in the client thread. It connects to the server. It sends and receives messages.""" - client = await GameClientUI.join_game(player=player, ip=ip, tk_root=tk_root, out_queue=out_queue, in_queue=in_queue, port=port) + for _ in range(5): + try: + client = await GameClientUI.join_game(player=player, ip=ip, tk_root=tk_root, out_queue=out_queue, in_queue=in_queue, port=port) - while client._websocket.open: - await asyncio.create_task(client.listen()) - await asyncio.create_task(client.await_commands()) + while client._websocket.open: + try: + await asyncio.create_task(client.listen()) + await asyncio.create_task(client.await_commands()) + except Exception as e: + logger.error(e) + out_queue.put({"message_type": "python/error", "error": e}) + tk_root.event_generate("<>", when="tail") + break + + # If the client is not able to connect to the server, try again + except Exception as e: + logger.error(e) + await asyncio.sleep(1) + + # If the client is not able to connect to the server after 5 tries, send an error message to the UI + out_queue.put({"message_type": "python/error", "error": e}) + tk_root.event_generate("<>", when="tail") def asyncio_thread_wrapper(tk_root:tk.Tk, out_queue:Queue, in_queue:Queue, player: Player, ip:str, port:int): """Wrapper function to run the client thread function in an asyncio event loop.""" From eb42eb4f602d23673c503d31ed493734a5a5db88 Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Sun, 17 Mar 2024 18:59:14 +0100 Subject: [PATCH 42/77] feat(UI-UX): added visuals for current player --- UI/field_frame.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/UI/field_frame.py b/UI/field_frame.py index 799f619..932482c 100644 --- a/UI/field_frame.py +++ b/UI/field_frame.py @@ -25,15 +25,22 @@ def __init__(self, master, number, uuid=None): def _create_widgets(self, number): self.frame = tk.Frame(self) - self.heading = tk.Label(self.frame.widget, text=f'Player {number}', font=self.master.master.title_font) - self.name = tk.Label(self.frame.widget, text=f'Player {number}') - self.symbol = tk.Label(self.frame.widget, text="unknown") + self.heading = tk.Label(self.frame.widget, text=f'Player {number}', font=self.master.master.title_font, border=0, margin=0) + self.name = tk.Label(self.frame.widget, text=f'Player {number}', border=0, margin=0) + self.symbol = tk.Label(self.frame.widget, text="unknown", border=0, margin=0) def highlight(self, highlight=True): + obj = [self.heading, self.name, self.symbol] if(highlight): - self.frame.config(border_color='green') + self.frame.config(bg=color.green) + for o in obj: + o.config(bg=color.green) + o.config(fg=color.green.complement) else: - self.frame.config(border_color=color.anthracite) + self.frame.config(bg=color.white) + for o in obj: + o.config(bg=color.white) + o.config(fg=color.white.complement) def set(self, name, type, uuid): self.name.config(text=name) From 59153bc4f87ac1918fa4efb9b7b6d02761894bfa Mon Sep 17 00:00:00 2001 From: Petzys <87223648+Petzys@users.noreply.github.com> Date: Sun, 17 Mar 2024 19:01:51 +0100 Subject: [PATCH 43/77] Fix(ui-client): Fixed unassigned variable in try catch --- Client/ui_client.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Client/ui_client.py b/Client/ui_client.py index 8c7dd14..640a8f6 100644 --- a/Client/ui_client.py +++ b/Client/ui_client.py @@ -171,7 +171,7 @@ async def client_thread_function(tk_root:tk.Tk, out_queue:Queue, in_queue:Queue, await asyncio.sleep(1) # If the client is not able to connect to the server after 5 tries, send an error message to the UI - out_queue.put({"message_type": "python/error", "error": e}) + out_queue.put({"message_type": "python/error", "error": ConnectionError("Could not connect to server after 5 tries. Please try again later.")}) tk_root.event_generate("<>", when="tail") def asyncio_thread_wrapper(tk_root:tk.Tk, out_queue:Queue, in_queue:Queue, player: Player, ip:str, port:int): From 71d1a0042072f1d189b84ae4b0feb589c7eb1ed1 Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Sun, 17 Mar 2024 19:02:28 +0100 Subject: [PATCH 44/77] fix(UI-UX): check if local profile exists when starting singleplayer --- UI/single.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/UI/single.py b/UI/single.py index 98a2d2f..72ef7cb 100644 --- a/UI/single.py +++ b/UI/single.py @@ -10,7 +10,7 @@ class Singleplayer(base_frame): def __new__(cls, master, *args, **kwargs): - if(master.player == None): + if(len(master.players) == 0 or master.player == None): return Profile(master, *args, return_to=Singleplayer, **kwargs) return super().__new__(cls, *args, **kwargs) From 078b3cf4ab9f497cf630d7d5105de9e99abd20f3 Mon Sep 17 00:00:00 2001 From: Petzys <87223648+Petzys@users.noreply.github.com> Date: Sun, 17 Mar 2024 19:06:05 +0100 Subject: [PATCH 45/77] Feat(ui-client): Now telling UI that the server connected successfully --- Client/ui_client.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Client/ui_client.py b/Client/ui_client.py index 640a8f6..f56e358 100644 --- a/Client/ui_client.py +++ b/Client/ui_client.py @@ -155,6 +155,9 @@ async def client_thread_function(tk_root:tk.Tk, out_queue:Queue, in_queue:Queue, try: client = await GameClientUI.join_game(player=player, ip=ip, tk_root=tk_root, out_queue=out_queue, in_queue=in_queue, port=port) + out_queue.put({"message_type": "lobby/connect"}) + tk_root.event_generate("<>", when="tail") + while client._websocket.open: try: await asyncio.create_task(client.listen()) From 2d8a9d0dade4dc0cf74e2fd329adf01b469e1eab Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Sun, 17 Mar 2024 19:12:46 +0100 Subject: [PATCH 46/77] feat(UI-UX): close server and ui_network_client when exiting lobby or game --- UI/field_frame.py | 6 +++++- UI/multi.py | 6 +++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/UI/field_frame.py b/UI/field_frame.py index 932482c..41321bb 100644 --- a/UI/field_frame.py +++ b/UI/field_frame.py @@ -71,7 +71,11 @@ def __init__(self, view, players, starting_uuid, **kwargs): self._bind() def _bind(self): - self.view.close.config(command=self.view.master.show_menu) + self.view.close.config(command=lambda *args: self.close()) + + def close(self): + list(self.view.master.out_queue.values())[0].put({'message_type': 'server/terminate', 'args' :{} }) + self.view.master.show_menu() def end(self, queue, *args): root = self.view.master diff --git a/UI/multi.py b/UI/multi.py index 7f2d58e..0f591e5 100644 --- a/UI/multi.py +++ b/UI/multi.py @@ -38,7 +38,7 @@ def _create_widgets(self, opponent): title = 'Waiting for players to join' if opponent in [player_type.network, player_type.unknown] else 'Play local game against AI' if opponent in [player_type.ai_weak, player_type.ai_strong] else 'Play local game against a friend' self.lblTitle = tk.Label(self, text=title, font=self.master.title_font) self.btnRdy = tk.Button(self, text='Start', command=lambda *args: list(self.master.out_queue.values())[0].put({'message_type': 'lobby/ready', 'args' : {'ready': not self.ready}})) - self.btnExit = tk.Button(self, text='Menu', command=lambda: self.master.show_menu()) + self.btnExit = tk.Button(self, text='Menu', command=lambda *args: self._menu()) self.chat = Chat(self, self.master) def _display_widgets(self): @@ -57,6 +57,10 @@ def _display_widgets(self): self.chat.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=4, row=4, columnspan=2, rowspan=7) self.btnExit.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=5, row=1) + def _menu(self): + list(self.master.out_queue.values())[0].put({'message_type': 'server/terminate', 'args' :{} }) + self.master.show_menu() + def _update_lobby(self, queue): self.playerlist = [] for player in queue['player']: From c6cdf5a2e72c7098669f669a4321ae8d879aa7bd Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Sun, 17 Mar 2024 19:14:54 +0100 Subject: [PATCH 47/77] fix(UI-UX): detect KeyErrors in queue-data from ui_network_client --- UI/main.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/UI/main.py b/UI/main.py index 86c1f3c..97a5bcf 100644 --- a/UI/main.py +++ b/UI/main.py @@ -68,11 +68,13 @@ def start_server(self): pass def network_event_handler(self): + queue = self.in_queue.get() + message_type = queue.pop('message_type', 'message type not found') try: - queue = self.in_queue.get() - self.network_events[queue.pop('message_type', 'message type not found')](queue) + function = self.network_events[message_type] except KeyError: - pass + print(f"message type not found {message_type}") + function(queue) def main(): app = Root() From 8e9bbf47d10cba2e49a9d538df747e0d7dd6202b Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Sun, 17 Mar 2024 19:15:55 +0100 Subject: [PATCH 48/77] fix(UI-UX): adjust for changed game/start message contents --- UI/field_frame.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/UI/field_frame.py b/UI/field_frame.py index 41321bb..7e4dc0d 100644 --- a/UI/field_frame.py +++ b/UI/field_frame.py @@ -87,10 +87,10 @@ def error(self, queue, *args): msg.display() class Field(base_frame): - def __init__(self, master, chat, *args, starting_player, starting_player_symbol, opponent, opponent_symbol, **kwargs): + def __init__(self, master, chat, *args, starting_player, player1, player1_symbol, player2, player2_symbol, **kwargs): super().__init__(master) self._create_widgets(chat, display_chat=len(kwargs)==1) - self.controller = field_controller(self, [starting_player, opponent], starting_player.uuid, **kwargs) + self.controller = field_controller(self, [player1, player2], starting_player.uuid, **kwargs) self._display_widgets() print("wigets displayed") #self.bind("<>", self.controller.sub_controller.turn) From b5970dda1698a00888a90cc0a446753760584bb9 Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Sun, 17 Mar 2024 19:16:45 +0100 Subject: [PATCH 49/77] refactor(UI-UX): remove debugging prints --- UI/field_frame.py | 1 - UI/gamefield.py | 1 - 2 files changed, 2 deletions(-) diff --git a/UI/field_frame.py b/UI/field_frame.py index 7e4dc0d..6539bff 100644 --- a/UI/field_frame.py +++ b/UI/field_frame.py @@ -92,7 +92,6 @@ def __init__(self, master, chat, *args, starting_player, player1, player1_symbol self._create_widgets(chat, display_chat=len(kwargs)==1) self.controller = field_controller(self, [player1, player2], starting_player.uuid, **kwargs) self._display_widgets() - print("wigets displayed") #self.bind("<>", self.controller.sub_controller.turn) #self.bind("<>", self.controller.end) #self.bind("<>", self.controller.error) diff --git a/UI/gamefield.py b/UI/gamefield.py index a00a522..5ab4ce3 100644 --- a/UI/gamefield.py +++ b/UI/gamefield.py @@ -31,7 +31,6 @@ def __init__(self, view: gamefield, starting_uuid: UUID, **kwargs): self.currentplayer = starting_uuid self.input_methods = {input_methods.mouse: [], input_methods.qeyc: [], input_methods.uom: []} for uuid, input_method in kwargs.items(): - print(uuid, input_method) self.input_methods[input_method].append(UUID(uuid)) self._bind() From a3f9723079c9edcb63054a5ea80ff4a130fba422 Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Sun, 17 Mar 2024 19:19:28 +0100 Subject: [PATCH 50/77] feat(UI-UX): Qality of life feature - Larger playfield now below player info - Menu button to get back on config screen for local multiplayer - error message when a profile is created with a name already in use - binding of enter key in profile creation - correct focus on creating and editing a profile --- UI/field_frame.py | 2 +- UI/multi.py | 6 ++++++ UI/profile.py | 11 +++++++++++ 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/UI/field_frame.py b/UI/field_frame.py index 6539bff..a63e2de 100644 --- a/UI/field_frame.py +++ b/UI/field_frame.py @@ -117,7 +117,7 @@ def _display_widgets(self): self.heading.grid(row=0, column=0, columnspan=3) self.player[0].grid(row=1, column=0) self.player[1].grid(row=1, column=2) - self.gamefield.grid(sticky=tk.N+tk.S+tk.E+tk.W, row=2, column=1) + self.gamefield.grid(sticky=tk.N+tk.S+tk.E+tk.W, row=2, column=0, columnspan=3) if(hasattr(self, 'chat')): self.columnconfigure(3, weight=1) self.chat.grid(sticky=tk.N+tk.S+tk.E+tk.W, row=1, column=3, rowspan=3) diff --git a/UI/multi.py b/UI/multi.py index 0f591e5..7502ec3 100644 --- a/UI/multi.py +++ b/UI/multi.py @@ -101,6 +101,7 @@ def _create_widgets(self): self.drpPlayer2 = tk.OptionMenu(self, self.varPlayer2, *[o.display_name for o in self.master.players]) self.btnnew = tk.Button(self, text='New Profile', command=lambda *args: self.master.show(NewProfile, return_to=LocalProfileSelection)) self.btnStart = tk.Button(self, text='Start', command=lambda *args: self._start_game()) + self.btnMenu = tk.Button(self, text='Menu', command=lambda: self.master.show_menu()) def _display_widgets(self): self.columnconfigure([0, 6], weight=1) @@ -118,6 +119,7 @@ def _display_widgets(self): self.drpPlayer2.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=3, row=4, columnspan=2) self.btnnew.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=5) self.btnStart.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=4, row=5, columnspan=2) + self.btnMenu.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=5, row=1) def _start_game(self): if(self.varPlayer1.get() == 'Select' or self.varPlayer2.get() == 'Select' or self.varPlayer1.get() == self.varPlayer2.get()): @@ -142,6 +144,7 @@ def __init__(self, master): super().__init__(master) self._create_widgets() self._display_widgets() + self.master.master.network_events['lobby/connect'] = self._lobby_connect def _create_widgets(self): self.frame = tk.Frame(self) @@ -177,6 +180,9 @@ def _connect(self): root = self.master.master root.out_queue = {root.players[root.player].uuid: Queue()} root.network_client = client_thread(root, in_queue=list(root.out_queue.values())[0], out_queue=root.in_queue, player=root.players[root.player], ip=self.etrAddress.get()) + + def _lobby_connect(self, queue): + root = self.master.master root.show(Join, local_players=[root.players[root.player].uuid]) class Multiplayer(base_frame): diff --git a/UI/profile.py b/UI/profile.py index 98fc22f..ea390ae 100644 --- a/UI/profile.py +++ b/UI/profile.py @@ -6,6 +6,7 @@ from .field_frame import Field from Server.player import Player from Client.profile_save import Profile as ProfileIO +from .messages import messages class NewProfile(base_frame): def __init__(self, master, *args, **kwargs): @@ -16,6 +17,7 @@ def __init__(self, master, *args, **kwargs): self.id = kwargs.pop('id', None) self._create_widgets() self._display_widgets() + self.etrName.focus_set() def _create_widgets(self): task = 'Edit' if self.edit else 'Create' @@ -25,6 +27,7 @@ def _create_widgets(self): self.etrName.val = self.master.players[self.master.player].display_name if self.edit else '' self.btnCreate = tk.Button(self, text=f'{task} profile', command=lambda *args: self._create()) self.btnMenu = tk.Button(self, text='Menu', command=lambda: self.master.show_menu()) + self.master.bind('', lambda *args: self._enter()) def _display_widgets(self): self.columnconfigure([0, 10], weight=1) @@ -45,7 +48,15 @@ def _display_widgets(self): self.btnCreate.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=6, row=8, columnspan=3) self.btnMenu.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=9, row=1) + def _enter(self): + if(self.focus_get() == self.etrName.widget): + self._create() + def _create(self): + if(self.etrName.val in [p.display_name for p in self.master.players]): + msg = messages(type='info', message='This name is already in use.\nPlease select a differnt name!') + msg.display() + return if(self.edit): for i, player in enumerate(self.master.players): if player.uuid == self.master.players[self.master.player].uuid: From 4acb72f13a6c7f1429dbae6122cdebf452090d61 Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Sun, 17 Mar 2024 19:33:36 +0100 Subject: [PATCH 51/77] feat(UI-UX): added error handling, when connection to server fails --- UI/main.py | 5 +++++ UI/multi.py | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/UI/main.py b/UI/main.py index 97a5bcf..b16b801 100644 --- a/UI/main.py +++ b/UI/main.py @@ -70,6 +70,11 @@ def start_server(self): def network_event_handler(self): queue = self.in_queue.get() message_type = queue.pop('message_type', 'message type not found') + if(message_type == 'python/error'): + try: + raise queue['error'] + except ConnectionError: + message_type = 'lobby/connect_error' try: function = self.network_events[message_type] except KeyError: diff --git a/UI/multi.py b/UI/multi.py index 7502ec3..cdce69e 100644 --- a/UI/multi.py +++ b/UI/multi.py @@ -145,6 +145,7 @@ def __init__(self, master): self._create_widgets() self._display_widgets() self.master.master.network_events['lobby/connect'] = self._lobby_connect + self.master.master.network_events['lobby/connect_error'] = self._connect_error def _create_widgets(self): self.frame = tk.Frame(self) @@ -180,6 +181,14 @@ def _connect(self): root = self.master.master root.out_queue = {root.players[root.player].uuid: Queue()} root.network_client = client_thread(root, in_queue=list(root.out_queue.values())[0], out_queue=root.in_queue, player=root.players[root.player], ip=self.etrAddress.get()) + self.etrAddress.config(state=tk.DISABLED) + self.btnConnect.config(text="Connecting...", state=tk.DISABLED) + + def _connect_error(self, queue): + msg = messages(type='info', message=f'Could not connect to the server "{self.etrAddress.get()}"') + self.etrAddress.config(state=tk.NORMAL) + self.btnConnect.config(text="Connect", state=tk.NORMAL) + msg.display() def _lobby_connect(self, queue): root = self.master.master From e42fe0b7aa222e6bf51e30cc546e51c3e413c167 Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Sun, 17 Mar 2024 20:00:00 +0100 Subject: [PATCH 52/77] feat(UI-UX): removed kick button for local and ai player --- AI/ai_context.py | 3 +++ AI/ai_strategy.py | 4 ++++ UI/multi.py | 14 ++++++++++---- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/AI/ai_context.py b/AI/ai_context.py index 0f80366..0c0744d 100644 --- a/AI/ai_context.py +++ b/AI/ai_context.py @@ -21,6 +21,9 @@ def run_strategy(self): thread = Thread(target=self._strategy.thread_entry, daemon=True) thread.start() return thread + + def get_uuid(self): + return self._strategy.get_uuid() if __name__ == "__main__": weak_ai = ai_strategy.WeakAIStrategy() diff --git a/AI/ai_strategy.py b/AI/ai_strategy.py index 085bac8..7bf0186 100644 --- a/AI/ai_strategy.py +++ b/AI/ai_strategy.py @@ -7,6 +7,7 @@ import random import logging import copy +from uuid import UUID logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__name__) @@ -29,6 +30,9 @@ def post_init(self): #needs to be called by inheriting classes at the end of their __init__ function super().__init__(self._ip, self._port, self._player) + def get_uuid(self): + return UUID(self._current_uuid) + def thread_entry(self): asyncio.run(self.run()) diff --git a/UI/multi.py b/UI/multi.py index cdce69e..5df3aa1 100644 --- a/UI/multi.py +++ b/UI/multi.py @@ -22,6 +22,7 @@ def __init__(self, master, *args, opponent=player_type.unknown, local_players, * self._display_widgets() self.playerlist = [] self.local_players = local_players + self.ai_players = [] #self.bind('<>', self._update_lobby) #self.bind('<>', self._start_game) self.master.network_events['lobby/status'] = self._update_lobby @@ -32,6 +33,7 @@ def __init__(self, master, *args, opponent=player_type.unknown, local_players, * server_thread(self.master.players[self.master.player]) if opponent in [player_type.ai_strong, player_type.ai_weak]: ai_context = AIContext(AdvancedAIStrategy() if opponent == player_type.ai_strong else WeakAIStrategy()) + self.ai_players.append(ai_context.get_uuid()) self.master.ai = ai_context.run_strategy() def _create_widgets(self, opponent): @@ -65,8 +67,12 @@ def _update_lobby(self, queue): self.playerlist = [] for player in queue['player']: rdy = '\u2611' if player.ready else '' - self.playerlist.append([tk.Label(self, text=rdy + ' ' + player.display_name), - tk.Button(self, text='Kick', command=lambda uuid=player.uuid, *args: list(self.master.out_queue.values())[0].put({'message_type': 'lobby/kick', 'args' : {'player_to_kick': uuid}}))]) + buffer = [] + buffer.append(tk.Label(self, text=rdy + ' ' + player.display_name)) + print(player.uuid, self.local_players + self.ai_players) + if(player.uuid not in (self.local_players + self.ai_players)): + buffer.append(tk.Button(self, text='Kick', command=lambda uuid=player.uuid, *args: list(self.master.out_queue.values())[0].put({'message_type': 'lobby/kick', 'args' : {'player_to_kick': uuid}}))) + self.playerlist.append(buffer) if(str(player.uuid) == str(self.master.players[self.master.player].uuid)): self.ready = player.ready if(player.ready): @@ -74,8 +80,8 @@ def _update_lobby(self, queue): else: self.btnRdy.config(text="Ready") for i, player in enumerate(self.playerlist): - player[0].grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=4+i) - player[1].grid(sticky=tk.E+tk.W+tk.N+tk.S, column=3, row=4+i) + for j, object in enumerate(player): + object.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2+j, row=4+i) def _start_game(self, queue): From f7b44f8dfca51dde94d1e233447eb85bc760797b Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Mon, 18 Mar 2024 00:11:42 +0100 Subject: [PATCH 53/77] fix(UI-UX): fixed on_destroy handler to unregister event handler --- UI/chat.py | 6 ++++-- UI/field_frame.py | 2 +- UI/multi.py | 7 ++++++- 3 files changed, 11 insertions(+), 4 deletions(-) diff --git a/UI/chat.py b/UI/chat.py index 6dfcbbc..4232969 100644 --- a/UI/chat.py +++ b/UI/chat.py @@ -8,6 +8,7 @@ def __init__(self, master, root, chat='', *args, **kwargs): self._display_widgets() self.root.bind('', lambda *args: self._enter()) self.root.network_events['chat/receive'] = self._chat_receive + self.widget.bind('', lambda *args: self._on_destroy()) def _create_widgets(self, chat): #self.txtChat = tk.Text(self.widget, state=tk.DISABLED) @@ -38,8 +39,9 @@ def _chat_receive(self, queue): self.txtChat.insert(tk.END, f"{queue['sender'].display_name}: {queue['message']}\n") self.txtChat.config(state=tk.DISABLED) - def on_destroy(self): - del self.master.network_events['chat/receive'] + def _on_destroy(self): + print(self.root.network_events) + del self.root.network_events['chat/receive'] def _enter(self): if(self.focus_get() == self.etrMessage.widget): diff --git a/UI/field_frame.py b/UI/field_frame.py index a63e2de..b3413cf 100644 --- a/UI/field_frame.py +++ b/UI/field_frame.py @@ -98,7 +98,7 @@ def __init__(self, master, chat, *args, starting_player, player1, player1_symbol self.master.network_events['game/turn'] = self.controller.sub_controller.turn self.master.network_events['game/end'] = self.controller.end self.master.network_events['game/error'] = self.controller.error - self.master.out_queue[starting_player.uuid].put({'message_type': 'game/gamestate', 'args' :{} }) + self.bind('', lambda *args: self.on_destroy()) def _create_widgets(self, chat, display_chat=True): self.heading = tk.Label(self, text="Tic Tac Toe Kojote", font=self.master.title_font) diff --git a/UI/multi.py b/UI/multi.py index 5df3aa1..9c11d01 100644 --- a/UI/multi.py +++ b/UI/multi.py @@ -27,7 +27,7 @@ def __init__(self, master, *args, opponent=player_type.unknown, local_players, * #self.bind('<>', self._start_game) self.master.network_events['lobby/status'] = self._update_lobby self.master.network_events['game/start'] = self._start_game - self.bind('Destroy', lambda *args: self.on_destroy()) + self.bind('', lambda *args: self.on_destroy()) self.ready = False if opponent not in [player_type.unknown, player_type.local]: server_thread(self.master.players[self.master.player]) @@ -152,6 +152,7 @@ def __init__(self, master): self._display_widgets() self.master.master.network_events['lobby/connect'] = self._lobby_connect self.master.master.network_events['lobby/connect_error'] = self._connect_error + self.bind('', lambda *args: self.on_destroy()) def _create_widgets(self): self.frame = tk.Frame(self) @@ -200,6 +201,10 @@ def _lobby_connect(self, queue): root = self.master.master root.show(Join, local_players=[root.players[root.player].uuid]) + def on_destroy(self): + del self.master.master.network_events['lobby/connect'] + del self.master.master.network_events['lobby/connect_error'] + class Multiplayer(base_frame): def __new__(cls, master, *args, **kwargs): if(len(master.players) == 0 or master.player == None): From 030ad18dd73fb1ef78349b77b95740e479435914 Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Mon, 18 Mar 2024 00:15:39 +0100 Subject: [PATCH 54/77] feat(UI-UX): added game menu, fixed frame caching --- UI/field_frame.py | 49 ++++++++++++++++++++++++++++++++++++++++------- UI/main.py | 16 +++++++++++++--- 2 files changed, 55 insertions(+), 10 deletions(-) diff --git a/UI/field_frame.py b/UI/field_frame.py index b3413cf..1ac9ed7 100644 --- a/UI/field_frame.py +++ b/UI/field_frame.py @@ -62,6 +62,39 @@ def _display_widgets(self): self.name.grid(row=1, column=0) self.symbol.grid(row=1, column=1) +class game_menu(base_frame): + def __init__(self, master, *args, **kwargs): + super().__init__(master) + self._create_widgets() + self._display_widgets() + self.address_toogle = False + + def _create_widgets(self): + self.lblTitle = tk.Label(self, text='TicTacToe-Kojote', font=self.master.title_font) + self.btnBack = tk.Button(self, text='Continue', command=lambda *args: self.master.show(Field)) + self.btnMenu = tk.Button(self, text='Main menu', command=lambda *args: self._menu()) + self.btnExit = tk.Button(self, text='Exit', command=lambda: self.master.destroy()) + + def _display_widgets(self): + self.columnconfigure([0, 6], weight=1) + self.columnconfigure([1, 5], weight=2) + self.columnconfigure([2, 4], weight=4) + self.columnconfigure([3], weight=2) + self.rowconfigure([0, 11], weight=1) + self.rowconfigure([2], weight=2) + self.rowconfigure([4, 6, 10, 12], weight=4) + self.rowconfigure([3, 5, 7, 11, 13], weight=2) + # display the buttons created in the _create_widgets method + self.lblTitle.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=2, columnspan=3) + self.btnBack.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=4, columnspan=3) + self.btnMenu.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=6, columnspan=3) + self.btnExit.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=5, row=1) + + def _menu(self): + self.master.remove_cached_frame(Field) + list(self.master.out_queue.values())[0].put({'message_type': 'server/terminate', 'args' :{} }) + self.master.show_menu() + class field_controller(): def __init__(self, view, players, starting_uuid, **kwargs): self.view = view @@ -71,11 +104,11 @@ def __init__(self, view, players, starting_uuid, **kwargs): self._bind() def _bind(self): - self.view.close.config(command=lambda *args: self.close()) + self.view.menu.config(command=lambda *args: self._menu()) - def close(self): - list(self.view.master.out_queue.values())[0].put({'message_type': 'server/terminate', 'args' :{} }) - self.view.master.show_menu() + def _menu(self): + self.view.master.cache_current_frame() + self.view.master.show(game_menu) def end(self, queue, *args): root = self.view.master @@ -98,7 +131,9 @@ def __init__(self, master, chat, *args, starting_player, player1, player1_symbol self.master.network_events['game/turn'] = self.controller.sub_controller.turn self.master.network_events['game/end'] = self.controller.end self.master.network_events['game/error'] = self.controller.error + self.master.out_queue[player1.uuid].put({'message_type': 'game/gamestate', 'args' :{} }) self.bind('', lambda *args: self.on_destroy()) + self.master.remove_cached_frame(Field) def _create_widgets(self, chat, display_chat=True): self.heading = tk.Label(self, text="Tic Tac Toe Kojote", font=self.master.title_font) @@ -108,7 +143,7 @@ def _create_widgets(self, chat, display_chat=True): self.gamefield = gamefield(self) if(display_chat): self.chat = Chat(self, self.master, chat) - self.close = tk.Button(self, text="close") + self.menu = tk.Button(self, text="Menu") def _display_widgets(self): self.columnconfigure([1], weight=1) @@ -120,8 +155,8 @@ def _display_widgets(self): self.gamefield.grid(sticky=tk.N+tk.S+tk.E+tk.W, row=2, column=0, columnspan=3) if(hasattr(self, 'chat')): self.columnconfigure(3, weight=1) - self.chat.grid(sticky=tk.N+tk.S+tk.E+tk.W, row=1, column=3, rowspan=3) - self.close.grid(row=3, column=2) + self.chat.grid(sticky=tk.N+tk.S+tk.E+tk.W, row=1, column=3, rowspan=3, columnspan=2) + self.menu.grid(row=0, column=4) def on_destroy(self): del self.master.network_events['game/turn'] diff --git a/UI/main.py b/UI/main.py index b16b801..fd19b90 100644 --- a/UI/main.py +++ b/UI/main.py @@ -40,20 +40,30 @@ def show(self, Frame, *args, cache=False, **kwargs): if(self.current_frame != None): try: self.current_frame.grid_forget() - self.current_frame.destroy() + if(self.current_frame.__class__.__name__ not in self.frames): + self.current_frame.destroy() except _tkinter.TclError: pass if(cache): if(Frame.__name__ not in self.frames): self.add_frame(Frame) frame = self.frames[Frame.__name__] + elif(Frame.__name__ in self.frames): + frame = self.frames[Frame.__name__] else: frame = Frame(self, *args, **kwargs) - if(frame != None): - frame.grid(row=0, column=0, sticky="nsew") + if(frame != None): + frame.grid(row=0, column=0, sticky="nsew") self.current_frame = frame return frame + def cache_current_frame(self): + self.frames[self.current_frame.__class__.__name__] = self.current_frame + + def remove_cached_frame(self, Frame): + if(Frame.__name__ in self.frames): + self.frames.pop(Frame.__name__) + def add_frame(self, Frame): self.frames[Frame.__name__] = Frame(self) self.frames[Frame.__name__].grid(row=0, column=0, sticky="nsew") From faf5da582fda909c79dc4d6a7d5fea1ea7e59183 Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Mon, 18 Mar 2024 00:16:16 +0100 Subject: [PATCH 55/77] refactor(UI-UX): removed unused function to start server --- UI/main.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/UI/main.py b/UI/main.py index fd19b90..3b3dacf 100644 --- a/UI/main.py +++ b/UI/main.py @@ -74,9 +74,6 @@ def show_menu(self): def start_mainloop(self): self.mainloop() - def start_server(self): - pass - def network_event_handler(self): queue = self.in_queue.get() message_type = queue.pop('message_type', 'message type not found') From a0d6fff6e8b2052bae68ffc96bf154d61ae60d4e Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Mon, 18 Mar 2024 00:17:20 +0100 Subject: [PATCH 56/77] fix(ui-UX): moved execution of event handler function in else statement of exception handler to unly be executed when found --- UI/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/UI/main.py b/UI/main.py index 3b3dacf..6c84bdc 100644 --- a/UI/main.py +++ b/UI/main.py @@ -86,7 +86,8 @@ def network_event_handler(self): function = self.network_events[message_type] except KeyError: print(f"message type not found {message_type}") - function(queue) + else: + function(queue) def main(): app = Root() From cd650e4ffd964827ee793b7986c7624c9ee10ea3 Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Mon, 18 Mar 2024 00:18:36 +0100 Subject: [PATCH 57/77] feat(UI-UX): Translates int to tic tac toe symboles X and O --- UI/gamefield.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/UI/gamefield.py b/UI/gamefield.py index 5ab4ce3..7c20b4f 100644 --- a/UI/gamefield.py +++ b/UI/gamefield.py @@ -46,6 +46,13 @@ def draw_field(self, matrix=None, position=None, value=None): #either matrix as if matrix != None: for i, row in enumerate(matrix): for j, e in enumerate(row): + match(e): + case 1: + e='X' + case 2: + e='O' + case _: + e = '' self.view.fields[(i, j)].config(text=e) else: self.view.fields[position].config(text=value) From f44ea57183ac0f395cf3e7372a03e788d6376998 Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Mon, 18 Mar 2024 00:49:44 +0100 Subject: [PATCH 58/77] feat(UI-UX): Lobby kick popup in ui, redirect to main menu [TTTK-80] --- UI/multi.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/UI/multi.py b/UI/multi.py index 9c11d01..11747c9 100644 --- a/UI/multi.py +++ b/UI/multi.py @@ -27,6 +27,7 @@ def __init__(self, master, *args, opponent=player_type.unknown, local_players, * #self.bind('<>', self._start_game) self.master.network_events['lobby/status'] = self._update_lobby self.master.network_events['game/start'] = self._start_game + self.master.network_events['lobby/kick'] = self._lobby_kick self.bind('', lambda *args: self.on_destroy()) self.ready = False if opponent not in [player_type.unknown, player_type.local]: @@ -91,6 +92,11 @@ def on_destroy(self): del self.master.network_events['lobby/status'] del self.master.network_events['game/start'] + def _lobby_kick(self, queue): + msg = messages(type='info', message=f'You have been kicked from the lobby by the host') + msg.display() + self._menu() + class LocalProfileSelection(base_frame): def __init__(self, master, *args, **kwargs): super().__init__(master) From 0e8f804c7191dce95b7d712e5abf31b25f21d65a Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Mon, 18 Mar 2024 00:50:24 +0100 Subject: [PATCH 59/77] feat(UI-UX): add draw message and final playfield to endscreen [TTTK-83] --- UI/endscreen.py | 23 ++++++++++++++++------- UI/field_frame.py | 2 +- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/UI/endscreen.py b/UI/endscreen.py index b43f82c..2f72c84 100644 --- a/UI/endscreen.py +++ b/UI/endscreen.py @@ -4,18 +4,27 @@ #from .field_frame import Field class EndScreen(base_frame): - def __init__(self, master, win:bool, *args, **kwargs): + def __init__(self, master, win:bool, winner, final_playfield, *args, **kwargs): super().__init__(master) - self._create_widgets(win) + self._create_widgets(win, winner, final_playfield) self._display_widgets() - def _create_widgets(self, win:bool): - message = "You won the game!" if win else "You lost the game!" + def _create_widgets(self, win:bool, winner, fp): + message = "You won the game!" if win else "It's a draw!\nThat's barely more than a loss." if winner == None else "You lost the game!" self.lblWinner = tk.Label(self, width=20, height=5, bg="white", text=message) - #self.btnPlayAgain = tk.Button(self, width=20, height=5, text="Play Again", command=lambda: self.master.show(Field)) + for i in range(3): + for j in range(3): + match fp[i][j]: + case 1: + fp[i][j] = "X" + case 2: + fp[i][j] = "O" + case _: + fp[i][j] = " " + self.lblPlayfield = tk.Label(self, width=20, height=5, bg="white", text=f'{fp[0][0]}|{fp[0][1]}|{fp[0][2]}\n_ _ _\n{fp[1][0]}|{fp[1][1]}|{fp[1][2]}\n_ _ _\n{fp[2][0]}|{fp[2][1]}|{fp[2][2]}') self.btnMainMenu = tk.Button(self, text="Main Menu", width=20, height=5, command=lambda: self.master.show_menu()) def _display_widgets(self): - self.lblWinner.pack() - #self.btnPlayAgain.pack() + self.lblWinner.pack(fill=tk.X) + self.lblPlayfield.pack() self.btnMainMenu.pack() \ No newline at end of file diff --git a/UI/field_frame.py b/UI/field_frame.py index f7d7a6c..152fee8 100644 --- a/UI/field_frame.py +++ b/UI/field_frame.py @@ -112,7 +112,7 @@ def _menu(self): def end(self, queue, *args): root = self.view.master - root.show(EndScreen, queue['win']) + root.show(EndScreen, queue['win'], queue['winner'], queue['final_playfield']) def error(self, queue, *args): root = self.view.master From 90857be6cb1346fe01f33f0014ee01c4ddb8510a Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Mon, 18 Mar 2024 01:30:33 +0100 Subject: [PATCH 60/77] feat(UI-UX): Add statistics to lobby and game [TTTK-17] --- UI/field_frame.py | 5 ++++- UI/multi.py | 7 +++++++ UI/statistics.py | 53 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 1 deletion(-) create mode 100644 UI/statistics.py diff --git a/UI/field_frame.py b/UI/field_frame.py index 152fee8..26c5ffc 100644 --- a/UI/field_frame.py +++ b/UI/field_frame.py @@ -8,6 +8,7 @@ from .messages import messages from .chat import Chat from .lib.colors import color +from .statistics import Statistics class player_type(Enum): local = auto() @@ -72,6 +73,7 @@ def __init__(self, master, *args, **kwargs): def _create_widgets(self): self.lblTitle = tk.Label(self, text='TicTacToe-Kojote', font=self.master.title_font) self.btnBack = tk.Button(self, text='Continue', command=lambda *args: self.master.show(Field)) + self.btnSatistics = tk.Button(self, text='Statistics', command=lambda *args: self.master.show(Statistics, return_to=game_menu)) self.btnMenu = tk.Button(self, text='Main menu', command=lambda *args: self._menu()) self.btnExit = tk.Button(self, text='Exit', command=lambda: self.master.destroy()) @@ -87,7 +89,8 @@ def _display_widgets(self): # display the buttons created in the _create_widgets method self.lblTitle.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=2, columnspan=3) self.btnBack.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=4, columnspan=3) - self.btnMenu.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=6, columnspan=3) + self.btnSatistics.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=6, columnspan=3) + self.btnMenu.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=8, columnspan=3) self.btnExit.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=5, row=1) def _menu(self): diff --git a/UI/multi.py b/UI/multi.py index 11747c9..187dbb2 100644 --- a/UI/multi.py +++ b/UI/multi.py @@ -14,6 +14,7 @@ from .chat import Chat from .messages import messages from .gamefield import input_methods +from .statistics import Statistics class Join(base_frame): def __init__(self, master, *args, opponent=player_type.unknown, local_players, **kwargs): @@ -43,6 +44,7 @@ def _create_widgets(self, opponent): self.btnRdy = tk.Button(self, text='Start', command=lambda *args: list(self.master.out_queue.values())[0].put({'message_type': 'lobby/ready', 'args' : {'ready': not self.ready}})) self.btnExit = tk.Button(self, text='Menu', command=lambda *args: self._menu()) self.chat = Chat(self, self.master) + self.btnStatistics = tk.Button(self, text='Statistics', command=lambda *args: self._show_statistics()) def _display_widgets(self): self.columnconfigure([0, 6], weight=1) @@ -59,6 +61,7 @@ def _display_widgets(self): self.btnRdy.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=10) self.chat.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=4, row=4, columnspan=2, rowspan=7) self.btnExit.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=5, row=1) + self.btnStatistics.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=5, row=2) def _menu(self): list(self.master.out_queue.values())[0].put({'message_type': 'server/terminate', 'args' :{} }) @@ -97,6 +100,10 @@ def _lobby_kick(self, queue): msg.display() self._menu() + def _show_statistics(self): + self.master.cache_current_frame() + self.master.show(Statistics, return_to=Join) + class LocalProfileSelection(base_frame): def __init__(self, master, *args, **kwargs): super().__init__(master) diff --git a/UI/statistics.py b/UI/statistics.py new file mode 100644 index 0000000..863f187 --- /dev/null +++ b/UI/statistics.py @@ -0,0 +1,53 @@ +#import tkinter as tk +from .lib import tttk_tk as tk + +from .base_frame import base_frame +from Server.player import Player + +class Statistics_data(tk.Container): + def __init__(self, master, *args, **kwargs): + super().__init__(master) + + def _update_statistics(self, queue): + print(queue) + heading = {Player('Player', 0): {'wins': 'Wins', 'losses': 'Losses', 'draws': 'Draws', 'moves': 'Moves', 'emojis': 'Emojis'}} + for i, (player, values) in enumerate((heading | queue['statistics']).items()): + tk.Label(self, text=player.display_name).grid(sticky=tk.E+tk.W+tk.N+tk.S, column=0, row=i) + for j, (headline, value) in enumerate(values.items()): + tk.Label(self, text=value).grid(sticky=tk.E+tk.W+tk.N+tk.S, column=j+1, row=i) + +class Statistics(base_frame): + def __init__(self, master, return_to, *args, **kwargs): + super().__init__(master) + self.return_to = return_to + self._create_widgets() + self._display_widgets() + self.master.network_events['statistics/statistics'] = self.data._update_statistics + self.bind('', lambda *args: self._on_destroy()) + list(self.master.out_queue.values())[0].put({'message_type': 'statistics/statistics', 'args': {}}) + + def _create_widgets(self): + self.lblTitle = tk.Label(self, text='TicTacToe-Kojote', font=self.master.title_font) + self.btnBack = tk.Button(self, text='Back', command=lambda *args: self._back()) + self.data = Statistics_data(self) + + def _back(self): + self.master.show(self.return_to) + self.master.remove_cached_frame(self.return_to) + + def _display_widgets(self): + self.columnconfigure([0, 6], weight=1) + self.columnconfigure([1, 5], weight=2) + self.columnconfigure([2, 4], weight=4) + self.columnconfigure([3], weight=2) + self.rowconfigure([0, 11], weight=1) + self.rowconfigure([2], weight=2) + self.rowconfigure([4, 6, 10, 12], weight=4) + self.rowconfigure([3, 5, 7, 11, 13], weight=2) + # display the buttons created in the _create_widgets method + self.lblTitle.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=2, columnspan=3) + self.btnBack.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=5, row=1) + self.data.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=4, columnspan=3) + + def _on_destroy(self): + del self.master.network_events['statistics/statistics'] \ No newline at end of file From 8278199e74d9ef75f316ca825c9b519e5c0a783b Mon Sep 17 00:00:00 2001 From: Petzys <87223648+Petzys@users.noreply.github.com> Date: Mon, 18 Mar 2024 08:02:50 +0100 Subject: [PATCH 61/77] Fix(client): TTTK-80 Fixed that ui player was kicked every time lobby/kick was sent --- Client/client.py | 9 +++++++-- Client/ui_client.py | 9 +++++---- 2 files changed, 12 insertions(+), 6 deletions(-) diff --git a/Client/client.py b/Client/client.py index 216ebee..16c67f5 100644 --- a/Client/client.py +++ b/Client/client.py @@ -61,6 +61,7 @@ def __init__(self, ip:str, port:int, player:Player) -> None: self._player: Player = player self._player_number: int = None self._symbol: str = None + self._kicked: bool = False # Opponent info self._opponent: Player = None @@ -152,6 +153,10 @@ async def listen(self): if message_type == "game/end": await self.terminate() + break + elif self._kicked: + await self.close() + break def get_player_by_uuid(self, uuid:str) -> Player: for player in self._lobby_status: @@ -216,8 +221,8 @@ async def _preprocess_message(self, message:str) -> str: await self.join_lobby() case "lobby/kick": if str(self._player.uuid) == message_json["kick_player_uuid"]: - logger.info("You have been kicked from the lobby.") - await self.close() + logger.info("You have been kicked from the lobby. Closing after processing the message...") + self._kicked = True case _: logger.error(f"Unknown message type: {message_json['message_type']}") raise ValidationError("Game start message received, but lobby does not contain 2 players. This should not happen and should be investigated.") diff --git a/Client/ui_client.py b/Client/ui_client.py index 24bb53b..0f19c74 100644 --- a/Client/ui_client.py +++ b/Client/ui_client.py @@ -99,10 +99,11 @@ async def _message_handler(self, message_type: str): }) self._tk_root.event_generate("<>", when="tail") case "lobby/kick": - self._out_queue.put({ - "message_type": "lobby/kick", - }) - self._tk_root.event_generate("<>", when="tail") + if self._kicked: + self._out_queue.put({ + "message_type": "lobby/kick", + }) + self._tk_root.event_generate("<>", when="tail") return def send_gamestate_to_ui(self): From 8c44ac5d289cbc39c3ba72b316af835ff5b34e49 Mon Sep 17 00:00:00 2001 From: Petzys <87223648+Petzys@users.noreply.github.com> Date: Mon, 18 Mar 2024 08:38:41 +0100 Subject: [PATCH 62/77] Fix(profile save): Fixed failing tests and prettified profile_save a little bit --- Client/Data/test/test_profiles.json | 1 - Client/profile_save.py | 21 ++++++--------------- Client/test_profile_save.py | 26 +++++++++----------------- 3 files changed, 15 insertions(+), 33 deletions(-) delete mode 100644 Client/Data/test/test_profiles.json diff --git a/Client/Data/test/test_profiles.json b/Client/Data/test/test_profiles.json deleted file mode 100644 index 0637a08..0000000 --- a/Client/Data/test/test_profiles.json +++ /dev/null @@ -1 +0,0 @@ -[] \ No newline at end of file diff --git a/Client/profile_save.py b/Client/profile_save.py index 333037c..52dd496 100644 --- a/Client/profile_save.py +++ b/Client/profile_save.py @@ -10,9 +10,8 @@ class Profile: This class is used to handle the profiles.json file. It is used to get, set, and add profiles to the file. """ - + @staticmethod def get_profiles(): - global path """ This method returns all the profiles from the file :return: An array of all profiles @@ -20,6 +19,8 @@ def get_profiles(): if exists(path): with open(path, 'r') as file: data = json.load(file) + if not data: + return [], 0 output = [] profile_data = data[0] selected = data[1] @@ -29,26 +30,16 @@ def get_profiles(): else: return [], 0 - def set_profiles( players: list, selected: int): - global path - """ - This method sets the profile name and/or color by the uuid - :param profile_uuid: - :param profile_name: - :param profile_color: - """ - - #try: + @staticmethod + def set_profiles(players: list, selected: int): with open(path, 'w') as file: entry = [] for player in players: entry.append(player.as_dict()) json.dump([entry, selected], file) - #except: - # raise RuntimeError("json error: Make sure profiles.json is formatted correctly") + @staticmethod def delete_all_profiles(): - global path """ This method deletes all profiles """ diff --git a/Client/test_profile_save.py b/Client/test_profile_save.py index 96a6f0c..c97ddd9 100644 --- a/Client/test_profile_save.py +++ b/Client/test_profile_save.py @@ -1,26 +1,18 @@ import unittest from Client.profile_save import Profile -import os from Server.player import Player -from os.path import exists - +from uuid import uuid4 class TestProfileSave(unittest.TestCase): - @classmethod - def setUpClass(cls): - cls.profile = Profile(os.path.abspath("Client/Data/test/test_profiles.json")) - cls.player1 = Player("test", 0, "test", False) - cls.player2 = Player("test2", 0, "test2", False) - def setUp(self): - self.profile.delete_all_profiles() + self.player1 = Player("test", 0, uuid4(), False) + self.player2 = Player("test2", 0, uuid4(), False) + Profile.delete_all_profiles() def test_all(self): - if exists(self.profile.path): - os.remove(self.profile.path) - data = [self.player1, self.player2] - self.profile.set_profiles(data) - self.assertEqual(self.profile.get_profiles(), data) - self.profile.delete_all_profiles() - self.assertEqual(self.profile.get_profiles(), []) + data = ([self.player1, self.player2], 0) + Profile.set_profiles(data[0], data[1]) + self.assertEqual(Profile.get_profiles(), data) + Profile.delete_all_profiles() + self.assertEqual(Profile.get_profiles(), ([], 0)) From fe8672463bd9f9564a583ccc54fc2afa60517631 Mon Sep 17 00:00:00 2001 From: Petzys <87223648+Petzys@users.noreply.github.com> Date: Mon, 18 Mar 2024 08:44:15 +0100 Subject: [PATCH 63/77] Fix(profile save): Fixed failing tests --- Client/profile_save.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Client/profile_save.py b/Client/profile_save.py index 52dd496..689d0de 100644 --- a/Client/profile_save.py +++ b/Client/profile_save.py @@ -32,7 +32,7 @@ def get_profiles(): @staticmethod def set_profiles(players: list, selected: int): - with open(path, 'w') as file: + with open(path, 'w+') as file: entry = [] for player in players: entry.append(player.as_dict()) From 2137cdcaa3e4c5c604b9b88c4e72377eceb6e8dd Mon Sep 17 00:00:00 2001 From: bananabr3d Date: Mon, 18 Mar 2024 09:26:00 +0100 Subject: [PATCH 64/77] fix: profile_save.py: checks for folder and creates if it doesn't exist --- Client/profile_save.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/Client/profile_save.py b/Client/profile_save.py index 689d0de..0f08b92 100644 --- a/Client/profile_save.py +++ b/Client/profile_save.py @@ -16,6 +16,7 @@ def get_profiles(): This method returns all the profiles from the file :return: An array of all profiles """ + Profile._check_folder() if exists(path): with open(path, 'r') as file: data = json.load(file) @@ -32,6 +33,7 @@ def get_profiles(): @staticmethod def set_profiles(players: list, selected: int): + Profile._check_folder() with open(path, 'w+') as file: entry = [] for player in players: @@ -43,6 +45,16 @@ def delete_all_profiles(): """ This method deletes all profiles """ + Profile._check_folder() if exists(path): with open(path, 'w') as file: file.write("[]") + + @staticmethod + def _check_folder(): + dir = os.path.abspath('Client/Data/') + if not os.path.exists(os.path.abspath(dir)): + try: + os.makedirs(os.path.abspath(dir)) + except OSError: + raise OSError(f"Creation of the directory {dir} failed") \ No newline at end of file From 130ac467178126bf4101dea665d4bd2ee1e54bd0 Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Mon, 18 Mar 2024 14:14:49 +0100 Subject: [PATCH 65/77] feat(UI-UX): display winner name for local multiplayer [TTTK-89] --- UI/endscreen.py | 13 ++++++++----- UI/field_frame.py | 10 ++++++---- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/UI/endscreen.py b/UI/endscreen.py index 2f72c84..774d279 100644 --- a/UI/endscreen.py +++ b/UI/endscreen.py @@ -4,13 +4,16 @@ #from .field_frame import Field class EndScreen(base_frame): - def __init__(self, master, win:bool, winner, final_playfield, *args, **kwargs): + def __init__(self, master, win:bool, winner, final_playfield, *args, local_mp=False, **kwargs): super().__init__(master) - self._create_widgets(win, winner, final_playfield) + self._create_widgets(win, winner, final_playfield, local_mp) self._display_widgets() - def _create_widgets(self, win:bool, winner, fp): - message = "You won the game!" if win else "It's a draw!\nThat's barely more than a loss." if winner == None else "You lost the game!" + def _create_widgets(self, win:bool, winner, fp, local_mp:bool): + if(not local_mp): + message = "You won the game!" if win else "It's a draw!\nThat's barely more than a loss." if winner == None else "You lost the game!" + else: + message = "It's a draw!" if winner == None else f"{winner.display_name} won the game!" self.lblWinner = tk.Label(self, width=20, height=5, bg="white", text=message) for i in range(3): for j in range(3): @@ -20,7 +23,7 @@ def _create_widgets(self, win:bool, winner, fp): case 2: fp[i][j] = "O" case _: - fp[i][j] = " " + fp[i][j] = " " self.lblPlayfield = tk.Label(self, width=20, height=5, bg="white", text=f'{fp[0][0]}|{fp[0][1]}|{fp[0][2]}\n_ _ _\n{fp[1][0]}|{fp[1][1]}|{fp[1][2]}\n_ _ _\n{fp[2][0]}|{fp[2][1]}|{fp[2][2]}') self.btnMainMenu = tk.Button(self, text="Main Menu", width=20, height=5, command=lambda: self.master.show_menu()) diff --git a/UI/field_frame.py b/UI/field_frame.py index 26c5ffc..c9993cc 100644 --- a/UI/field_frame.py +++ b/UI/field_frame.py @@ -99,8 +99,9 @@ def _menu(self): self.master.show_menu() class field_controller(): - def __init__(self, view, players, starting_uuid, **kwargs): + def __init__(self, view, players, starting_uuid, local_mp, **kwargs): self.view = view + self.local_mp = local_mp self.sub_controller = gamefield_controller(self.view.gamefield, starting_uuid, **kwargs) for player_lbl, player in zip(self.view.player, players): player_lbl.set(player.display_name, player_type.unknown, player.uuid) @@ -115,7 +116,7 @@ def _menu(self): def end(self, queue, *args): root = self.view.master - root.show(EndScreen, queue['win'], queue['winner'], queue['final_playfield']) + root.show(EndScreen, queue['win'], queue['winner'], queue['final_playfield'], local_mp=self.local_mp) def error(self, queue, *args): root = self.view.master @@ -130,8 +131,9 @@ def _close(self): class Field(base_frame): def __init__(self, master, chat, *args, starting_player, player1, player1_symbol, player2, player2_symbol, **kwargs): super().__init__(master) - self._create_widgets(chat, display_chat=len(kwargs)==1) - self.controller = field_controller(self, [player1, player2], starting_player.uuid, **kwargs) + local_mp = not (len(kwargs)==1) + self._create_widgets(chat, display_chat=not local_mp) + self.controller = field_controller(self, [player1, player2], starting_player.uuid, local_mp, **kwargs) self._display_widgets() #self.bind("<>", self.controller.sub_controller.turn) #self.bind("<>", self.controller.end) From 439487d7e6e6b9eff12d0005e461627317a3b228 Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Mon, 18 Mar 2024 14:19:47 +0100 Subject: [PATCH 66/77] fix(UI-UX): resize and reorder buttons on profile frame [TTTK-86] --- UI/profile.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/UI/profile.py b/UI/profile.py index ea390ae..b604318 100644 --- a/UI/profile.py +++ b/UI/profile.py @@ -113,9 +113,9 @@ def _display_widgets(self): self.lblNameValue.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=4, row=4, columnspan=5) self.lblUUDI.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=6) self.lblUUIDValue.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=4, row=6, columnspan=5) - self.btnDelete.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=8, columnspan=3) - self.btnAdd.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=4, row=8, columnspan=3) - self.btnEdit.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=6, row=8, columnspan=3) + self.btnAdd.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=8, columnspan=2) + self.btnEdit.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=5, row=8, columnspan=3) + self.btnDelete.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=8, row=8) self.btnMenu.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=9, row=1) def _delete(self): From fbb1182a92aab38a12027b3e59c4d7bb52dd4915 Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Mon, 18 Mar 2024 16:42:57 +0100 Subject: [PATCH 67/77] feat(UI_UX): added colors to player [TTTK-85] --- Client/profile_save.py | 2 +- Server/player.py | 14 +++++++++++- UI/endscreen.py | 10 +++++++++ UI/field_frame.py | 29 +++++++++++-------------- UI/gamefield.py | 13 ++++++----- UI/multi.py | 28 ++++++++++++++++++------ UI/profile.py | 49 +++++++++++++++++++++++++++++++++--------- 7 files changed, 105 insertions(+), 40 deletions(-) diff --git a/Client/profile_save.py b/Client/profile_save.py index 0f08b92..610e50d 100644 --- a/Client/profile_save.py +++ b/Client/profile_save.py @@ -29,7 +29,7 @@ def get_profiles(): output.append(Player.from_dict(profile)) return output, selected else: - return [], 0 + return [], None @staticmethod def set_profiles(players: list, selected: int): diff --git a/Server/player.py b/Server/player.py index ce716c2..cdb2399 100644 --- a/Server/player.py +++ b/Server/player.py @@ -37,4 +37,16 @@ def __hash__(self) -> int: @classmethod def from_dict(cls, player_dict: dict): - return cls(player_dict["display_name"], player_dict["color"], UUID(player_dict["uuid"]), player_dict["ready"]) \ No newline at end of file + return cls(player_dict["display_name"], player_dict["color"], UUID(player_dict["uuid"]), player_dict["ready"]) + + @classmethod + def with_color_str(cls, display_name: str, color_str: str, uuid: UUID = None, ready:bool = False): + return cls(display_name, int(color_str[1:], 16), uuid, ready) + + @property + def color_str(self) -> str: + return f"#{self.color:06x}" + + @color_str.setter + def color_str(self, color: str): + self.color = int(color.removeprefix('#'), 16) \ No newline at end of file diff --git a/UI/endscreen.py b/UI/endscreen.py index 774d279..95567f5 100644 --- a/UI/endscreen.py +++ b/UI/endscreen.py @@ -3,6 +3,12 @@ from .base_frame import base_frame #from .field_frame import Field +def too_dark(hex_color: str): + r = int(hex_color[1:3], 16) + g = int(hex_color[3:5], 16) + b = int(hex_color[5:7], 16) + return (r+g+b)/3 < 85 + class EndScreen(base_frame): def __init__(self, master, win:bool, winner, final_playfield, *args, local_mp=False, **kwargs): super().__init__(master) @@ -15,6 +21,10 @@ def _create_widgets(self, win:bool, winner, fp, local_mp:bool): else: message = "It's a draw!" if winner == None else f"{winner.display_name} won the game!" self.lblWinner = tk.Label(self, width=20, height=5, bg="white", text=message) + if(winner != None): + self.lblWinner.config(bg=winner.color_str) + if too_dark(winner.color_str): + self.lblWinner.config(fg="white") for i in range(3): for j in range(3): match fp[i][j]: diff --git a/UI/field_frame.py b/UI/field_frame.py index c9993cc..a0b0b44 100644 --- a/UI/field_frame.py +++ b/UI/field_frame.py @@ -43,19 +43,14 @@ def highlight(self, highlight=True): o.config(bg=color.white) o.config(fg=color.white.complement) - def set(self, name, type, uuid): + def set(self, name, symbol, uuid): self.name.config(text=name) self.uuid = uuid - match type: - case player_type.local: - self.symbol.config(text="Lokal") - case player_type.ai_strong, player_type.ai_weak: - self.symbol.config( text="Computer") - case player_type.network: - self.symbol.config(text="Online") - case player_type.unknown: - self.symbol.config(text="unkown") - #durch pictogramme ersetzen + match symbol: + case 0: + self.symbol.config(text="O") + case 1: + self.symbol.config(text="X") def _display_widgets(self): self.frame.pack(fill=tk.BOTH, expand=True) @@ -68,7 +63,6 @@ def __init__(self, master, *args, **kwargs): super().__init__(master) self._create_widgets() self._display_widgets() - self.address_toogle = False def _create_widgets(self): self.lblTitle = tk.Label(self, text='TicTacToe-Kojote', font=self.master.title_font) @@ -99,12 +93,13 @@ def _menu(self): self.master.show_menu() class field_controller(): - def __init__(self, view, players, starting_uuid, local_mp, **kwargs): + def __init__(self, view, players, player_symbols, starting_uuid, local_mp, **kwargs): self.view = view self.local_mp = local_mp - self.sub_controller = gamefield_controller(self.view.gamefield, starting_uuid, **kwargs) - for player_lbl, player in zip(self.view.player, players): - player_lbl.set(player.display_name, player_type.unknown, player.uuid) + symbol_colors = {player_symbols[0]: players[1].color_str, player_symbols[1]: players[0].color_str} + self.sub_controller = gamefield_controller(self.view.gamefield, starting_uuid, symbol_colors, **kwargs) + for player_lbl, player, symbol in zip(self.view.player, players, player_symbols): + player_lbl.set(player.display_name, symbol, player.uuid) self._bind() def _bind(self): @@ -133,7 +128,7 @@ def __init__(self, master, chat, *args, starting_player, player1, player1_symbol super().__init__(master) local_mp = not (len(kwargs)==1) self._create_widgets(chat, display_chat=not local_mp) - self.controller = field_controller(self, [player1, player2], starting_player.uuid, local_mp, **kwargs) + self.controller = field_controller(self, [player1, player2], [player1_symbol, player2_symbol], starting_player.uuid, local_mp, **kwargs) self._display_widgets() #self.bind("<>", self.controller.sub_controller.turn) #self.bind("<>", self.controller.end) diff --git a/UI/gamefield.py b/UI/gamefield.py index 7c20b4f..014a8a3 100644 --- a/UI/gamefield.py +++ b/UI/gamefield.py @@ -26,9 +26,10 @@ def _display_widgets(self): field.grid(sticky=tk.N+tk.S+tk.E+tk.W, row=position[0], column=position[1]) class gamefield_controller: - def __init__(self, view: gamefield, starting_uuid: UUID, **kwargs): + def __init__(self, view: gamefield, starting_uuid: UUID, symbol_colors, **kwargs): self.view = view self.currentplayer = starting_uuid + self.symbol_colors = symbol_colors self.input_methods = {input_methods.mouse: [], input_methods.qeyc: [], input_methods.uom: []} for uuid, input_method in kwargs.items(): self.input_methods[input_method].append(UUID(uuid)) @@ -48,12 +49,14 @@ def draw_field(self, matrix=None, position=None, value=None): #either matrix as for j, e in enumerate(row): match(e): case 1: - e='X' + f='X' case 2: - e='O' + f='O' case _: - e = '' - self.view.fields[(i, j)].config(text=e) + f = '' + self.view.fields[(i, j)].config(text=f) + if(e != 0): + self.view.fields[(i, j)].config( fg=self.symbol_colors[e-1]) else: self.view.fields[position].config(text=value) diff --git a/UI/multi.py b/UI/multi.py index 187dbb2..c1746dd 100644 --- a/UI/multi.py +++ b/UI/multi.py @@ -116,8 +116,12 @@ def _create_widgets(self): self.lblPlayer2 = tk.Label(self, text='Player 2') self.varPlayer1 = tk.StringVar(self, value='Select') self.varPlayer2 = tk.StringVar(self, value='Select') + self.varPlayer1.trace_add('write', lambda *args: self._dropdown_changed(1)) + self.varPlayer2.trace_add('write', lambda *args: self._dropdown_changed(2)) self.drpPlayer1 = tk.OptionMenu(self, self.varPlayer1, *[o.display_name for o in self.master.players]) self.drpPlayer2 = tk.OptionMenu(self, self.varPlayer2, *[o.display_name for o in self.master.players]) + self.lblColor1 = tk.Label(self) + self.lblColor2 = tk.Label(self) self.btnnew = tk.Button(self, text='New Profile', command=lambda *args: self.master.show(NewProfile, return_to=LocalProfileSelection)) self.btnStart = tk.Button(self, text='Start', command=lambda *args: self._start_game()) self.btnMenu = tk.Button(self, text='Menu', command=lambda: self.master.show_menu()) @@ -132,14 +136,26 @@ def _display_widgets(self): self.rowconfigure([4, 6, 8, 10], weight=4) self.rowconfigure([3, 5, 7, 9, 11], weight=2) self.lblTitle.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=2, columnspan=3) - self.lblPlayer1.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=3) - self.lblPlayer2.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=4) - self.drpPlayer1.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=3, row=3, columnspan=2) - self.drpPlayer2.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=3, row=4, columnspan=2) - self.btnnew.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=5) - self.btnStart.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=4, row=5, columnspan=2) + self.lblPlayer1.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=4) + self.lblPlayer2.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=6) + self.drpPlayer1.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=3, row=4, columnspan=2) + self.drpPlayer2.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=3, row=6, columnspan=2) + self.lblColor1.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=5, row=4) + self.lblColor2.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=5, row=6) + self.btnnew.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=8) + self.btnStart.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=4, row=8, columnspan=2) self.btnMenu.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=5, row=1) + def _dropdown_changed(self, player): + if(player == 1): + for profile in self.master.players: + if(profile.display_name == self.varPlayer1.get()): + self.lblColor1.config(bg=profile.color_str) + else: + for profile in self.master.players: + if(profile.display_name == self.varPlayer2.get()): + self.lblColor2.config(bg=profile.color_str) + def _start_game(self): if(self.varPlayer1.get() == 'Select' or self.varPlayer2.get() == 'Select' or self.varPlayer1.get() == self.varPlayer2.get()): msg = messages(type='info', message='Please select two different players') diff --git a/UI/profile.py b/UI/profile.py index b604318..af0bc21 100644 --- a/UI/profile.py +++ b/UI/profile.py @@ -1,5 +1,6 @@ #import tkinter as tk from .lib import tttk_tk as tk +from tkinter import colorchooser from uuid import UUID, uuid4 from .base_frame import base_frame @@ -11,7 +12,7 @@ class NewProfile(base_frame): def __init__(self, master, *args, **kwargs): super().__init__(master) - self.address_toogle = False + self.color_str = None self.next = kwargs.pop('return_to', Profile) self.edit = kwargs.pop('edit', False) self.id = kwargs.pop('id', None) @@ -25,6 +26,11 @@ def _create_widgets(self): self.lblName = tk.Label(self, text='Name') self.etrName = tk.Entry(self) self.etrName.val = self.master.players[self.master.player].display_name if self.edit else '' + self.lblColor = tk.Label(self, text='Color') + self.btnColor = tk.Button(self, command=lambda *args: self._color()) + if self.edit: + self.color_str = self.master.players[self.master.player].color_str + self.btnColor.config(bg=self.color_str) self.btnCreate = tk.Button(self, text=f'{task} profile', command=lambda *args: self._create()) self.btnMenu = tk.Button(self, text='Menu', command=lambda: self.master.show_menu()) self.master.bind('', lambda *args: self._enter()) @@ -45,6 +51,8 @@ def _display_widgets(self): self.lblTitle.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=2, columnspan=7) self.lblName.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=4) self.etrName.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=4, row=4, columnspan=5) + self.lblColor.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=6) + self.btnColor.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=4, row=6, columnspan=5) self.btnCreate.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=6, row=8, columnspan=3) self.btnMenu.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=9, row=1) @@ -53,21 +61,36 @@ def _enter(self): self._create() def _create(self): - if(self.etrName.val in [p.display_name for p in self.master.players]): + tmp = self.master.players.copy() + if(self.master.player != None): + tmp.remove(self.master.players[self.master.player]) + if(self.etrName.val in [p.display_name for p in tmp]): msg = messages(type='info', message='This name is already in use.\nPlease select a differnt name!') msg.display() return + if(self.color_str==None): + msg = messages(type='info', message='Please select a color!') + msg.display() + return if(self.edit): for i, player in enumerate(self.master.players): if player.uuid == self.master.players[self.master.player].uuid: - self.master.players[i] = Player(self.etrName.val, 0) + self.master.players[i] = Player.with_color_str(self.etrName.val, self.color_str) else: - self.master.players.append(Player(self.etrName.val, 0)) + self.master.players.append(Player.with_color_str(self.etrName.val, self.color_str)) #self.master.player = Player(self.etrName.val, 0) print([o.uuid for o in self.master.players]) ProfileIO.set_profiles(self.master.players, self.master.player) self.master.show(self.next) + def _color(self): + color_str = colorchooser.askcolor(title ="Choose color") + if(color_str[1] == None): + return + self.color_str = color_str[1] + print(self.color_str) + self.btnColor.config(bg=self.color_str) + class Profile(base_frame): def __new__(cls, master, *args, **kwargs): if len(master.players) == 0: @@ -84,12 +107,15 @@ def _create_widgets(self): self.lblTitle = tk.Label(self, text='Multiplayer', font=self.master.title_font) self.lblName = tk.Label(self, text='Name') #self.lblNameValue = tk.Label(self, text=self.master.player.display_name) - self.lblvar = tk.StringVar(self, self.master.players[self.master.player].display_name) + self.lblvar = tk.StringVar(self, self.master.players[self.master.player].display_name) if self.master.player != None else tk.StringVar(self, "Please select a profile") self.lblvar.trace_add('write', self._dropdown_changed) #print([o.display_name for o in self.master.players]) self.lblNameValue = tk.OptionMenu(self, self.lblvar, *[o.display_name for o in self.master.players]) #[o.attr for o in objs] self.lblUUDI = tk.Label(self, text='User ID') - self.lblUUIDValue = tk.Label(self, text=self.master.players[self.master.player].uuid) + self.lblUUIDValue = tk.Label(self, text=self.master.players[self.master.player].uuid) if self.master.player != None else tk.Label(self) + self.lblColor = tk.Label(self, text='Color') + self.lblColorValue = tk.Label(self) + if(self.master.player != None): self.lblColorValue.config(bg=self.master.players[self.master.player].color_str) self.btnEdit = tk.Button(self, text='Edit Profile', command=lambda *args: self.master.show(NewProfile, edit=True, id=self.master.player)) self.btnDelete = tk.Button(self, text='Delete profile', command=lambda *args: self._delete()) self.btnAdd = tk.Button(self, text='Add profile', command=lambda *args: self.master.show(NewProfile)) @@ -113,9 +139,11 @@ def _display_widgets(self): self.lblNameValue.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=4, row=4, columnspan=5) self.lblUUDI.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=6) self.lblUUIDValue.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=4, row=6, columnspan=5) - self.btnAdd.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=8, columnspan=2) - self.btnEdit.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=5, row=8, columnspan=3) - self.btnDelete.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=8, row=8) + self.lblColor.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=8) + self.lblColorValue.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=4, row=8, columnspan=5) + self.btnAdd.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=10, columnspan=2) + self.btnEdit.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=5, row=10, columnspan=3) + self.btnDelete.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=8, row=10) self.btnMenu.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=9, row=1) def _delete(self): @@ -130,4 +158,5 @@ def _dropdown_changed(self, *args): self.master.player = i break ProfileIO.set_profiles(self.master.players, self.master.player) - self.lblUUIDValue.config(text=self.master.players[self.master.player].uuid) \ No newline at end of file + self.lblUUIDValue.config(text=self.master.players[self.master.player].uuid) + self.lblColorValue.config(bg=self.master.players[self.master.player].color_str) \ No newline at end of file From 464c8a12dcd35a20a0ea43a1614c5f42ee47a3ad Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Mon, 18 Mar 2024 16:43:47 +0100 Subject: [PATCH 68/77] fix(UI-UX): fixed sizing of chat, added heading --- UI/chat.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/UI/chat.py b/UI/chat.py index 4232969..4a3ebe4 100644 --- a/UI/chat.py +++ b/UI/chat.py @@ -11,7 +11,7 @@ def __init__(self, master, root, chat='', *args, **kwargs): self.widget.bind('', lambda *args: self._on_destroy()) def _create_widgets(self, chat): - #self.txtChat = tk.Text(self.widget, state=tk.DISABLED) + self.lblheading = tk.Label(self.widget, text="Chat") self.txtChat = tk.Text(self.widget, width=0) self.txtScroll = tk.Scrollbar(self.widget, command=self.txtChat.yview) self.txtChat.config(yscrollcommand=self.txtScroll.set) @@ -25,10 +25,13 @@ def _display_widgets(self): self.widget.columnconfigure([1], weight=1) self.widget.rowconfigure([0], weight=1) self.widget.rowconfigure([1,2], weight=0) - self.txtChat.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=0, row=0, columnspan=2) - self.txtScroll.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=0) - self.etrMessage.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=0, row=1) - self.btnSend.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=1, row=1, columnspan=2) + self.widget.rowconfigure([0,2], weight=0) + self.widget.rowconfigure([1], weight=1) + self.lblheading.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=0, row=0, columnspan=3) + self.txtChat.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=0, row=1, columnspan=2) + self.txtScroll.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=1) + self.etrMessage.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=0, row=2) + self.btnSend.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=1, row=2, columnspan=2) def _send(self): list(self.root.out_queue.values())[0].put({'message_type': 'chat/message', 'args' : {'message': self.etrMessage.val}}) From 1ebc82fe203712eb249a4b3e443f06e202e8e19a Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Mon, 18 Mar 2024 16:44:54 +0100 Subject: [PATCH 69/77] feat(UI-UX): added silent mode for frames. local multiplayer now visually recirects to playfield --- UI/main.py | 6 +++--- UI/multi.py | 16 ++++++++++------ 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/UI/main.py b/UI/main.py index 6c84bdc..cc2987c 100644 --- a/UI/main.py +++ b/UI/main.py @@ -36,8 +36,8 @@ def __init__(self): self.bind("<>", lambda *args: self.network_event_handler()) self.show(Menu, True) - def show(self, Frame, *args, cache=False, **kwargs): - if(self.current_frame != None): + def show(self, Frame, *args, cache=False, display=True, **kwargs): + if(self.current_frame != None and display): try: self.current_frame.grid_forget() if(self.current_frame.__class__.__name__ not in self.frames): @@ -52,7 +52,7 @@ def show(self, Frame, *args, cache=False, **kwargs): frame = self.frames[Frame.__name__] else: frame = Frame(self, *args, **kwargs) - if(frame != None): + if(frame != None and display): frame.grid(row=0, column=0, sticky="nsew") self.current_frame = frame return frame diff --git a/UI/multi.py b/UI/multi.py index c1746dd..6b286cb 100644 --- a/UI/multi.py +++ b/UI/multi.py @@ -17,13 +17,16 @@ from .statistics import Statistics class Join(base_frame): - def __init__(self, master, *args, opponent=player_type.unknown, local_players, **kwargs): + def __init__(self, master, *args, opponent=player_type.unknown, local_players, quiet=False, **kwargs): super().__init__(master) + self.quiet = quiet self._create_widgets(opponent) - self._display_widgets() + if(not quiet): + self._display_widgets() self.playerlist = [] self.local_players = local_players self.ai_players = [] + #self.bind('<>', self._update_lobby) #self.bind('<>', self._start_game) self.master.network_events['lobby/status'] = self._update_lobby @@ -83,9 +86,10 @@ def _update_lobby(self, queue): self.btnRdy.config(text="not Ready") else: self.btnRdy.config(text="Ready") - for i, player in enumerate(self.playerlist): - for j, object in enumerate(player): - object.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2+j, row=4+i) + if(not self.quiet): + for i, player in enumerate(self.playerlist): + for j, object in enumerate(player): + object.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2+j, row=4+i) def _start_game(self, queue): @@ -172,7 +176,7 @@ def _start_game(self): client_thread(self.master.dummy, in_queue=self.master.out_queue[player2.uuid], out_queue=Queue(), player=player2, ip='localhost') self.master.out_queue[player1.uuid].put({'message_type': 'lobby/ready', 'args' : {'ready': True}}) self.master.out_queue[player2.uuid].put({'message_type': 'lobby/ready', 'args' : {'ready': True}}) - self.master.show(Join, opponent=player_type.local, local_players=[player1.uuid, player2.uuid]) + self.master.show(Join, display=False, opponent=player_type.local, local_players=[player1.uuid, player2.uuid],quiet=True) class Lobby_Overview(tk.Container): def __init__(self, master): From d4c7a06e78afcf5158e1cf4db774fccf05001516 Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Mon, 18 Mar 2024 18:53:59 +0100 Subject: [PATCH 70/77] feat(UI-UX): added scaling for tic tac toe symboles --- UI/gamefield.py | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/UI/gamefield.py b/UI/gamefield.py index 014a8a3..25f8a75 100644 --- a/UI/gamefield.py +++ b/UI/gamefield.py @@ -30,12 +30,14 @@ def __init__(self, view: gamefield, starting_uuid: UUID, symbol_colors, **kwargs self.view = view self.currentplayer = starting_uuid self.symbol_colors = symbol_colors + self.current_font = ('Helvetica', 20, 'bold') self.input_methods = {input_methods.mouse: [], input_methods.qeyc: [], input_methods.uom: []} for uuid, input_method in kwargs.items(): self.input_methods[input_method].append(UUID(uuid)) self._bind() def _bind(self): + self.view.fields[(0,0)].bind('', self._update_font) for position, button in self.view.fields.items(): button.config(command=lambda e=position: self._game_input(e, input_methods.mouse)) for position, button in zip(self.view.fields.keys(),['q', 'w', 'e', 'a', 's', 'd', 'y', 'x', 'c']): @@ -43,6 +45,21 @@ def _bind(self): for position, button in zip(self.view.fields.keys(),['u', 'i', 'o', 'j', 'k', 'l', 'm', ',', '.']): self.view.bind(f'', lambda e=position: self._game_input(e, input_methods.uom)) + def _update_font(self, event): + label_width = event.width + label_height = event.height + + # Calculate the new font size (you might need to adjust this formula) + new_font_size = min(label_width//2, label_height//2) # Example ratio, adjust as needed + + # Update the label's font size + if((tmp := abs(new_font_size-self.current_font[1]) )> 1): + print(tmp, new_font_size) + new_font = (self.current_font[0], new_font_size, self.current_font[2]) + for position, button in self.view.fields.items(): + button.config(font=new_font) + self.current_font = new_font + def draw_field(self, matrix=None, position=None, value=None): #either matrix as a 3x3 list or position and value need to be provided if matrix != None: for i, row in enumerate(matrix): @@ -53,7 +70,7 @@ def draw_field(self, matrix=None, position=None, value=None): #either matrix as case 2: f='O' case _: - f = '' + f = ' ' self.view.fields[(i, j)].config(text=f) if(e != 0): self.view.fields[(i, j)].config( fg=self.symbol_colors[e-1]) From b7544baef535c3bed6a17e39e3db2887ce034e33 Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Mon, 18 Mar 2024 19:03:38 +0100 Subject: [PATCH 71/77] chore(UI-UX): removal of debug prints, depricated imports, outcommented code --- UI/chat.py | 1 - UI/credits.py | 1 - UI/customLobby.py | 1 - UI/endscreen.py | 1 - UI/field_frame.py | 5 +---- UI/gamefield.py | 5 ++--- UI/multi.py | 4 ---- UI/profile.py | 10 +--------- UI/statistics.py | 1 - 9 files changed, 4 insertions(+), 25 deletions(-) diff --git a/UI/chat.py b/UI/chat.py index 4a3ebe4..c8aac1f 100644 --- a/UI/chat.py +++ b/UI/chat.py @@ -43,7 +43,6 @@ def _chat_receive(self, queue): self.txtChat.config(state=tk.DISABLED) def _on_destroy(self): - print(self.root.network_events) del self.root.network_events['chat/receive'] def _enter(self): diff --git a/UI/credits.py b/UI/credits.py index cf832ca..3f5c0b6 100644 --- a/UI/credits.py +++ b/UI/credits.py @@ -2,7 +2,6 @@ from .lib import tttk_tk as tk from .base_frame import base_frame -from .field_frame import Field class Credits(base_frame): def __init__(self, master, *args, **kwargs): diff --git a/UI/customLobby.py b/UI/customLobby.py index 76ca5f0..d4f00a5 100644 --- a/UI/customLobby.py +++ b/UI/customLobby.py @@ -19,7 +19,6 @@ def reload(self): for entry in self.widgets: for widget in entry: widget.forget() - #reload self.network for i, client in enumerate(self.local + self.ai + self.network): name = tk.Label(self, text=client) button0 = tk.Button(self, text="Slot1") diff --git a/UI/endscreen.py b/UI/endscreen.py index 95567f5..f766d82 100644 --- a/UI/endscreen.py +++ b/UI/endscreen.py @@ -1,7 +1,6 @@ #import tkinter as tk from .lib import tttk_tk as tk from .base_frame import base_frame -#from .field_frame import Field def too_dark(hex_color: str): r = int(hex_color[1:3], 16) diff --git a/UI/field_frame.py b/UI/field_frame.py index a0b0b44..16e4841 100644 --- a/UI/field_frame.py +++ b/UI/field_frame.py @@ -78,7 +78,7 @@ def _display_widgets(self): self.columnconfigure([3], weight=2) self.rowconfigure([0, 11], weight=1) self.rowconfigure([2], weight=2) - self.rowconfigure([4, 6, 10, 12], weight=4) + self.rowconfigure([4, 6, 8, 10, 12], weight=4) self.rowconfigure([3, 5, 7, 11, 13], weight=2) # display the buttons created in the _create_widgets method self.lblTitle.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=2, row=2, columnspan=3) @@ -130,9 +130,6 @@ def __init__(self, master, chat, *args, starting_player, player1, player1_symbol self._create_widgets(chat, display_chat=not local_mp) self.controller = field_controller(self, [player1, player2], [player1_symbol, player2_symbol], starting_player.uuid, local_mp, **kwargs) self._display_widgets() - #self.bind("<>", self.controller.sub_controller.turn) - #self.bind("<>", self.controller.end) - #self.bind("<>", self.controller.error) self.master.network_events['game/turn'] = self.controller.sub_controller.turn self.master.network_events['game/end'] = self.controller.end self.master.network_events['game/error'] = self.controller.error diff --git a/UI/gamefield.py b/UI/gamefield.py index 25f8a75..863ac87 100644 --- a/UI/gamefield.py +++ b/UI/gamefield.py @@ -50,11 +50,10 @@ def _update_font(self, event): label_height = event.height # Calculate the new font size (you might need to adjust this formula) - new_font_size = min(label_width//2, label_height//2) # Example ratio, adjust as needed + new_font_size = min(label_width//2, label_height//2) # Update the label's font size - if((tmp := abs(new_font_size-self.current_font[1]) )> 1): - print(tmp, new_font_size) + if(abs(new_font_size-self.current_font[1]) )> 1: new_font = (self.current_font[0], new_font_size, self.current_font[2]) for position, button in self.view.fields.items(): button.config(font=new_font) diff --git a/UI/multi.py b/UI/multi.py index 6b286cb..1ab505f 100644 --- a/UI/multi.py +++ b/UI/multi.py @@ -26,9 +26,6 @@ def __init__(self, master, *args, opponent=player_type.unknown, local_players, q self.playerlist = [] self.local_players = local_players self.ai_players = [] - - #self.bind('<>', self._update_lobby) - #self.bind('<>', self._start_game) self.master.network_events['lobby/status'] = self._update_lobby self.master.network_events['game/start'] = self._start_game self.master.network_events['lobby/kick'] = self._lobby_kick @@ -76,7 +73,6 @@ def _update_lobby(self, queue): rdy = '\u2611' if player.ready else '' buffer = [] buffer.append(tk.Label(self, text=rdy + ' ' + player.display_name)) - print(player.uuid, self.local_players + self.ai_players) if(player.uuid not in (self.local_players + self.ai_players)): buffer.append(tk.Button(self, text='Kick', command=lambda uuid=player.uuid, *args: list(self.master.out_queue.values())[0].put({'message_type': 'lobby/kick', 'args' : {'player_to_kick': uuid}}))) self.playerlist.append(buffer) diff --git a/UI/profile.py b/UI/profile.py index af0bc21..fd7a7dd 100644 --- a/UI/profile.py +++ b/UI/profile.py @@ -1,10 +1,8 @@ #import tkinter as tk from .lib import tttk_tk as tk from tkinter import colorchooser -from uuid import UUID, uuid4 from .base_frame import base_frame -from .field_frame import Field from Server.player import Player from Client.profile_save import Profile as ProfileIO from .messages import messages @@ -78,8 +76,6 @@ def _create(self): self.master.players[i] = Player.with_color_str(self.etrName.val, self.color_str) else: self.master.players.append(Player.with_color_str(self.etrName.val, self.color_str)) - #self.master.player = Player(self.etrName.val, 0) - print([o.uuid for o in self.master.players]) ProfileIO.set_profiles(self.master.players, self.master.player) self.master.show(self.next) @@ -88,7 +84,6 @@ def _color(self): if(color_str[1] == None): return self.color_str = color_str[1] - print(self.color_str) self.btnColor.config(bg=self.color_str) class Profile(base_frame): @@ -101,16 +96,13 @@ def __init__(self, master, *args, **kwargs): super().__init__(master) self._create_widgets() self._display_widgets() - self.address_toogle = False def _create_widgets(self): self.lblTitle = tk.Label(self, text='Multiplayer', font=self.master.title_font) self.lblName = tk.Label(self, text='Name') - #self.lblNameValue = tk.Label(self, text=self.master.player.display_name) self.lblvar = tk.StringVar(self, self.master.players[self.master.player].display_name) if self.master.player != None else tk.StringVar(self, "Please select a profile") self.lblvar.trace_add('write', self._dropdown_changed) - #print([o.display_name for o in self.master.players]) - self.lblNameValue = tk.OptionMenu(self, self.lblvar, *[o.display_name for o in self.master.players]) #[o.attr for o in objs] + self.lblNameValue = tk.OptionMenu(self, self.lblvar, *[o.display_name for o in self.master.players]) self.lblUUDI = tk.Label(self, text='User ID') self.lblUUIDValue = tk.Label(self, text=self.master.players[self.master.player].uuid) if self.master.player != None else tk.Label(self) self.lblColor = tk.Label(self, text='Color') diff --git a/UI/statistics.py b/UI/statistics.py index 863f187..a9fe8a9 100644 --- a/UI/statistics.py +++ b/UI/statistics.py @@ -9,7 +9,6 @@ def __init__(self, master, *args, **kwargs): super().__init__(master) def _update_statistics(self, queue): - print(queue) heading = {Player('Player', 0): {'wins': 'Wins', 'losses': 'Losses', 'draws': 'Draws', 'moves': 'Moves', 'emojis': 'Emojis'}} for i, (player, values) in enumerate((heading | queue['statistics']).items()): tk.Label(self, text=player.display_name).grid(sticky=tk.E+tk.W+tk.N+tk.S, column=0, row=i) From f93b79f24228f4892de8da6e251e6a0d9526ac42 Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Mon, 18 Mar 2024 19:16:15 +0100 Subject: [PATCH 72/77] doc(UI-UX): add class header for documentation purposes --- UI/base_frame.py | 3 +++ UI/chat.py | 4 ++++ UI/credits.py | 5 +++++ UI/customLobby.py | 4 ++++ UI/endscreen.py | 3 +++ UI/field_frame.py | 15 +++++++++++++++ UI/gamefield.py | 6 ++++++ UI/main.py | 4 ++++ UI/menu.py | 3 +++ UI/messages.py | 3 +++ UI/multi.py | 13 +++++++++++++ UI/profile.py | 6 ++++++ UI/single.py | 3 +++ UI/statistics.py | 6 ++++++ 14 files changed, 78 insertions(+) diff --git a/UI/base_frame.py b/UI/base_frame.py index 3074ede..6c94065 100644 --- a/UI/base_frame.py +++ b/UI/base_frame.py @@ -1,5 +1,8 @@ import tkinter as tk class base_frame(tk.Frame): + """ + Base class for all frames in the game. This class is used to set the background color of all frames to white. + """ def __init__(self, parent): tk.Frame.__init__(self, parent, bg='#FFFFFF') \ No newline at end of file diff --git a/UI/chat.py b/UI/chat.py index c8aac1f..fed3014 100644 --- a/UI/chat.py +++ b/UI/chat.py @@ -1,6 +1,10 @@ from .lib import tttk_tk as tk class Chat(tk.Frame): + """ + Chat window for the game. This class is used to display the chat window and send messages to the server. + It also listens for incoming chat messages from the server. + """ def __init__(self, master, root, chat='', *args, **kwargs): super().__init__(master) self.root = root diff --git a/UI/credits.py b/UI/credits.py index 3f5c0b6..1a36849 100644 --- a/UI/credits.py +++ b/UI/credits.py @@ -4,6 +4,11 @@ from .base_frame import base_frame class Credits(base_frame): + """ + The credits screen of the game. This screen is used to display the names of the developers of the game. + It also contains a hidden developer options menu. This menu can be accessed by clicking the "created by" label + three times. This menu is used to test the game and is not intended for the end user. + """ def __init__(self, master, *args, **kwargs): super().__init__(master) self._create_widgets() diff --git a/UI/customLobby.py b/UI/customLobby.py index d4f00a5..5b240da 100644 --- a/UI/customLobby.py +++ b/UI/customLobby.py @@ -71,6 +71,10 @@ def clear(self): self.master.clear(self.slot) class CustomLobby(base_frame): + """ + Custom lobby for the game. This class is used to display the custom lobby window and send messages to the server. + It is only used for internal testing. + """ def __init__(self, master, *args): super().__init__(master) self.player = list() diff --git a/UI/endscreen.py b/UI/endscreen.py index f766d82..d1ca4a6 100644 --- a/UI/endscreen.py +++ b/UI/endscreen.py @@ -9,6 +9,9 @@ def too_dark(hex_color: str): return (r+g+b)/3 < 85 class EndScreen(base_frame): + """ + The end screen is displayed after a game has ended. It shows the winner and the final playfield. + """ def __init__(self, master, win:bool, winner, final_playfield, *args, local_mp=False, **kwargs): super().__init__(master) self._create_widgets(win, winner, final_playfield, local_mp) diff --git a/UI/field_frame.py b/UI/field_frame.py index 16e4841..6d96c1d 100644 --- a/UI/field_frame.py +++ b/UI/field_frame.py @@ -11,6 +11,9 @@ from .statistics import Statistics class player_type(Enum): + """ + Enum for the different player types + """ local = auto() ai_weak = auto() ai_strong = auto() @@ -18,6 +21,9 @@ class player_type(Enum): unknown = auto() class player(tk.Container): + """ + Class for the player labels in the gamefield + """ def __init__(self, master, number, uuid=None): super().__init__(master) self.uuid = uuid @@ -59,6 +65,9 @@ def _display_widgets(self): self.symbol.grid(row=1, column=1) class game_menu(base_frame): + """ + The game menu can be accessed from the gamefield. It allows the user to return to the main menu, the statistics or to exit the game. + """ def __init__(self, master, *args, **kwargs): super().__init__(master) self._create_widgets() @@ -93,6 +102,9 @@ def _menu(self): self.master.show_menu() class field_controller(): + """ + The controller for the gamefield. It is used to control the gamefield and access the game menu. + """ def __init__(self, view, players, player_symbols, starting_uuid, local_mp, **kwargs): self.view = view self.local_mp = local_mp @@ -124,6 +136,9 @@ def _close(self): self.view.master.show_menu() class Field(base_frame): + """ + The field frame is used to display the gamefield and the player labels. + """ def __init__(self, master, chat, *args, starting_player, player1, player1_symbol, player2, player2_symbol, **kwargs): super().__init__(master) local_mp = not (len(kwargs)==1) diff --git a/UI/gamefield.py b/UI/gamefield.py index 863ac87..8204340 100644 --- a/UI/gamefield.py +++ b/UI/gamefield.py @@ -8,6 +8,9 @@ class input_methods(Enum): uom= auto() class gamefield(tk.Frame): + """ + Class for the gamefield in the game. This class is used to display the gamefield + """ def __init__(self, master): super().__init__(master) self._create_widgets() @@ -26,6 +29,9 @@ def _display_widgets(self): field.grid(sticky=tk.N+tk.S+tk.E+tk.W, row=position[0], column=position[1]) class gamefield_controller: + """ + Class for the gamefield controller in the game. This class is used to control the gamefield + """ def __init__(self, view: gamefield, starting_uuid: UUID, symbol_colors, **kwargs): self.view = view self.currentplayer = starting_uuid diff --git a/UI/main.py b/UI/main.py index cc2987c..4725523 100644 --- a/UI/main.py +++ b/UI/main.py @@ -8,6 +8,10 @@ from Client.profile_save import Profile as ProfileIO class Root(tk.Tk): + """ + Root class for the application. This class is the main window and handles the switching of frames. + It also handles the network events and the queues for the network events. + """ def __init__(self): super().__init__() start_width = 500 diff --git a/UI/menu.py b/UI/menu.py index fd7dbb0..dafefa5 100644 --- a/UI/menu.py +++ b/UI/menu.py @@ -9,6 +9,9 @@ from .credits import Credits class Menu(base_frame): + """ + The main menu of the game. This screen is used to navigate to the different parts of the game. + """ def __init__(self, master, *args, **kwargs): super().__init__(master) self._create_widgets() diff --git a/UI/messages.py b/UI/messages.py index df47467..3d861ac 100644 --- a/UI/messages.py +++ b/UI/messages.py @@ -2,6 +2,9 @@ class messages: + """ + Class for the different message boxes + """ def __init__(self, type : str = "error", message : str = None): """" Constructor for the messages class diff --git a/UI/multi.py b/UI/multi.py index 1ab505f..9ece69d 100644 --- a/UI/multi.py +++ b/UI/multi.py @@ -17,6 +17,9 @@ from .statistics import Statistics class Join(base_frame): + """ + Class for the join screen. This screen is used to join a lobby and to display the players in the lobby. + """ def __init__(self, master, *args, opponent=player_type.unknown, local_players, quiet=False, **kwargs): super().__init__(master) self.quiet = quiet @@ -105,6 +108,9 @@ def _show_statistics(self): self.master.show(Statistics, return_to=Join) class LocalProfileSelection(base_frame): + """ + Class for the local profile selection screen. This screen is used to select the local profiles for the game. + """ def __init__(self, master, *args, **kwargs): super().__init__(master) self._create_widgets() @@ -175,6 +181,9 @@ def _start_game(self): self.master.show(Join, display=False, opponent=player_type.local, local_players=[player1.uuid, player2.uuid],quiet=True) class Lobby_Overview(tk.Container): + """ + Class for the lobby overview. This screen is part of the multiplayer screen and used to display the available lobbies and to join them. + """ def __init__(self, master): super().__init__(master) self._create_widgets() @@ -235,6 +244,10 @@ def on_destroy(self): del self.master.master.network_events['lobby/connect_error'] class Multiplayer(base_frame): + """ + Class for the multiplayer screen. This screen is used to select the multiplayer mode and to create or join lobbies. + It houses the lobby overview. + """ def __new__(cls, master, *args, **kwargs): if(len(master.players) == 0 or master.player == None): return Profile(master, *args, return_to=Multiplayer, **kwargs) diff --git a/UI/profile.py b/UI/profile.py index fd7a7dd..879efd4 100644 --- a/UI/profile.py +++ b/UI/profile.py @@ -8,6 +8,9 @@ from .messages import messages class NewProfile(base_frame): + """ + Class for the creation of a new profile. This class is used to create a new profile or edit an existing one. + """ def __init__(self, master, *args, **kwargs): super().__init__(master) self.color_str = None @@ -87,6 +90,9 @@ def _color(self): self.btnColor.config(bg=self.color_str) class Profile(base_frame): + """ + Class for the profile selection. This class is used to select a profile for the game. + """ def __new__(cls, master, *args, **kwargs): if len(master.players) == 0: return NewProfile(master, *args, **kwargs) diff --git a/UI/single.py b/UI/single.py index 72ef7cb..d0d44fb 100644 --- a/UI/single.py +++ b/UI/single.py @@ -9,6 +9,9 @@ from .profile import Profile class Singleplayer(base_frame): + """ + The singleplayer menu. This screen is used to choose the opponent for a singleplayer game. + """ def __new__(cls, master, *args, **kwargs): if(len(master.players) == 0 or master.player == None): return Profile(master, *args, return_to=Singleplayer, **kwargs) diff --git a/UI/statistics.py b/UI/statistics.py index a9fe8a9..653b804 100644 --- a/UI/statistics.py +++ b/UI/statistics.py @@ -5,6 +5,9 @@ from Server.player import Player class Statistics_data(tk.Container): + """ + Class for the statistics data in the statistics frame. This class is used to display the statistics of the players. + """ def __init__(self, master, *args, **kwargs): super().__init__(master) @@ -16,6 +19,9 @@ def _update_statistics(self, queue): tk.Label(self, text=value).grid(sticky=tk.E+tk.W+tk.N+tk.S, column=j+1, row=i) class Statistics(base_frame): + """ + The statistics menu. This screen is used to display the statistics of the players. + """ def __init__(self, master, return_to, *args, **kwargs): super().__init__(master) self.return_to = return_to From fabe04b655030527071a0f10768ed2aca286d01c Mon Sep 17 00:00:00 2001 From: Petzys <87223648+Petzys@users.noreply.github.com> Date: Tue, 19 Mar 2024 10:04:46 +0100 Subject: [PATCH 73/77] Fix(client): Added connect try again for general client --- Client/client.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/Client/client.py b/Client/client.py index 16c67f5..fcd2179 100644 --- a/Client/client.py +++ b/Client/client.py @@ -81,7 +81,14 @@ def __init__(self, ip:str, port:int, player:Player) -> None: self._json_schema = json.load(f) async def connect(self): - self._websocket = await connect(f"ws://{self._ip}:{str(self._port)}") + # Try 5 times to connect to the server + for i in range(5): + try: + self._websocket = await connect(f"ws://{self._ip}:{str(self._port)}") + break + except Exception as e: + logger.error(f"Could not connect to server. Attempt {i+1}/5. Retrying in 0.5 seconds...") + await asyncio.sleep(0.5) @classmethod async def create_game(cls, player: Player, port:int = 8765) -> tuple[GameClient, asyncio.Task, Thread]: From 1a78e83dc7bcf2c852f2b53a9d106842e7357b81 Mon Sep 17 00:00:00 2001 From: Hauke Platte Date: Mon, 18 Mar 2024 19:16:15 +0100 Subject: [PATCH 74/77] doc(UI-UX): add class header for documentation purposes --- UI/base_frame.py | 3 +++ UI/chat.py | 4 ++++ UI/credits.py | 5 +++++ UI/customLobby.py | 4 ++++ UI/endscreen.py | 3 +++ UI/field_frame.py | 15 +++++++++++++++ UI/gamefield.py | 6 ++++++ UI/main.py | 4 ++++ UI/menu.py | 3 +++ UI/messages.py | 3 +++ UI/multi.py | 13 +++++++++++++ UI/profile.py | 6 ++++++ UI/single.py | 3 +++ UI/statistics.py | 6 ++++++ 14 files changed, 78 insertions(+) diff --git a/UI/base_frame.py b/UI/base_frame.py index 3074ede..6c94065 100644 --- a/UI/base_frame.py +++ b/UI/base_frame.py @@ -1,5 +1,8 @@ import tkinter as tk class base_frame(tk.Frame): + """ + Base class for all frames in the game. This class is used to set the background color of all frames to white. + """ def __init__(self, parent): tk.Frame.__init__(self, parent, bg='#FFFFFF') \ No newline at end of file diff --git a/UI/chat.py b/UI/chat.py index c8aac1f..fed3014 100644 --- a/UI/chat.py +++ b/UI/chat.py @@ -1,6 +1,10 @@ from .lib import tttk_tk as tk class Chat(tk.Frame): + """ + Chat window for the game. This class is used to display the chat window and send messages to the server. + It also listens for incoming chat messages from the server. + """ def __init__(self, master, root, chat='', *args, **kwargs): super().__init__(master) self.root = root diff --git a/UI/credits.py b/UI/credits.py index 3f5c0b6..1a36849 100644 --- a/UI/credits.py +++ b/UI/credits.py @@ -4,6 +4,11 @@ from .base_frame import base_frame class Credits(base_frame): + """ + The credits screen of the game. This screen is used to display the names of the developers of the game. + It also contains a hidden developer options menu. This menu can be accessed by clicking the "created by" label + three times. This menu is used to test the game and is not intended for the end user. + """ def __init__(self, master, *args, **kwargs): super().__init__(master) self._create_widgets() diff --git a/UI/customLobby.py b/UI/customLobby.py index d4f00a5..5b240da 100644 --- a/UI/customLobby.py +++ b/UI/customLobby.py @@ -71,6 +71,10 @@ def clear(self): self.master.clear(self.slot) class CustomLobby(base_frame): + """ + Custom lobby for the game. This class is used to display the custom lobby window and send messages to the server. + It is only used for internal testing. + """ def __init__(self, master, *args): super().__init__(master) self.player = list() diff --git a/UI/endscreen.py b/UI/endscreen.py index f766d82..d1ca4a6 100644 --- a/UI/endscreen.py +++ b/UI/endscreen.py @@ -9,6 +9,9 @@ def too_dark(hex_color: str): return (r+g+b)/3 < 85 class EndScreen(base_frame): + """ + The end screen is displayed after a game has ended. It shows the winner and the final playfield. + """ def __init__(self, master, win:bool, winner, final_playfield, *args, local_mp=False, **kwargs): super().__init__(master) self._create_widgets(win, winner, final_playfield, local_mp) diff --git a/UI/field_frame.py b/UI/field_frame.py index 16e4841..6d96c1d 100644 --- a/UI/field_frame.py +++ b/UI/field_frame.py @@ -11,6 +11,9 @@ from .statistics import Statistics class player_type(Enum): + """ + Enum for the different player types + """ local = auto() ai_weak = auto() ai_strong = auto() @@ -18,6 +21,9 @@ class player_type(Enum): unknown = auto() class player(tk.Container): + """ + Class for the player labels in the gamefield + """ def __init__(self, master, number, uuid=None): super().__init__(master) self.uuid = uuid @@ -59,6 +65,9 @@ def _display_widgets(self): self.symbol.grid(row=1, column=1) class game_menu(base_frame): + """ + The game menu can be accessed from the gamefield. It allows the user to return to the main menu, the statistics or to exit the game. + """ def __init__(self, master, *args, **kwargs): super().__init__(master) self._create_widgets() @@ -93,6 +102,9 @@ def _menu(self): self.master.show_menu() class field_controller(): + """ + The controller for the gamefield. It is used to control the gamefield and access the game menu. + """ def __init__(self, view, players, player_symbols, starting_uuid, local_mp, **kwargs): self.view = view self.local_mp = local_mp @@ -124,6 +136,9 @@ def _close(self): self.view.master.show_menu() class Field(base_frame): + """ + The field frame is used to display the gamefield and the player labels. + """ def __init__(self, master, chat, *args, starting_player, player1, player1_symbol, player2, player2_symbol, **kwargs): super().__init__(master) local_mp = not (len(kwargs)==1) diff --git a/UI/gamefield.py b/UI/gamefield.py index 863ac87..8204340 100644 --- a/UI/gamefield.py +++ b/UI/gamefield.py @@ -8,6 +8,9 @@ class input_methods(Enum): uom= auto() class gamefield(tk.Frame): + """ + Class for the gamefield in the game. This class is used to display the gamefield + """ def __init__(self, master): super().__init__(master) self._create_widgets() @@ -26,6 +29,9 @@ def _display_widgets(self): field.grid(sticky=tk.N+tk.S+tk.E+tk.W, row=position[0], column=position[1]) class gamefield_controller: + """ + Class for the gamefield controller in the game. This class is used to control the gamefield + """ def __init__(self, view: gamefield, starting_uuid: UUID, symbol_colors, **kwargs): self.view = view self.currentplayer = starting_uuid diff --git a/UI/main.py b/UI/main.py index cc2987c..4725523 100644 --- a/UI/main.py +++ b/UI/main.py @@ -8,6 +8,10 @@ from Client.profile_save import Profile as ProfileIO class Root(tk.Tk): + """ + Root class for the application. This class is the main window and handles the switching of frames. + It also handles the network events and the queues for the network events. + """ def __init__(self): super().__init__() start_width = 500 diff --git a/UI/menu.py b/UI/menu.py index fd7dbb0..dafefa5 100644 --- a/UI/menu.py +++ b/UI/menu.py @@ -9,6 +9,9 @@ from .credits import Credits class Menu(base_frame): + """ + The main menu of the game. This screen is used to navigate to the different parts of the game. + """ def __init__(self, master, *args, **kwargs): super().__init__(master) self._create_widgets() diff --git a/UI/messages.py b/UI/messages.py index df47467..3d861ac 100644 --- a/UI/messages.py +++ b/UI/messages.py @@ -2,6 +2,9 @@ class messages: + """ + Class for the different message boxes + """ def __init__(self, type : str = "error", message : str = None): """" Constructor for the messages class diff --git a/UI/multi.py b/UI/multi.py index 1ab505f..9ece69d 100644 --- a/UI/multi.py +++ b/UI/multi.py @@ -17,6 +17,9 @@ from .statistics import Statistics class Join(base_frame): + """ + Class for the join screen. This screen is used to join a lobby and to display the players in the lobby. + """ def __init__(self, master, *args, opponent=player_type.unknown, local_players, quiet=False, **kwargs): super().__init__(master) self.quiet = quiet @@ -105,6 +108,9 @@ def _show_statistics(self): self.master.show(Statistics, return_to=Join) class LocalProfileSelection(base_frame): + """ + Class for the local profile selection screen. This screen is used to select the local profiles for the game. + """ def __init__(self, master, *args, **kwargs): super().__init__(master) self._create_widgets() @@ -175,6 +181,9 @@ def _start_game(self): self.master.show(Join, display=False, opponent=player_type.local, local_players=[player1.uuid, player2.uuid],quiet=True) class Lobby_Overview(tk.Container): + """ + Class for the lobby overview. This screen is part of the multiplayer screen and used to display the available lobbies and to join them. + """ def __init__(self, master): super().__init__(master) self._create_widgets() @@ -235,6 +244,10 @@ def on_destroy(self): del self.master.master.network_events['lobby/connect_error'] class Multiplayer(base_frame): + """ + Class for the multiplayer screen. This screen is used to select the multiplayer mode and to create or join lobbies. + It houses the lobby overview. + """ def __new__(cls, master, *args, **kwargs): if(len(master.players) == 0 or master.player == None): return Profile(master, *args, return_to=Multiplayer, **kwargs) diff --git a/UI/profile.py b/UI/profile.py index fd7a7dd..879efd4 100644 --- a/UI/profile.py +++ b/UI/profile.py @@ -8,6 +8,9 @@ from .messages import messages class NewProfile(base_frame): + """ + Class for the creation of a new profile. This class is used to create a new profile or edit an existing one. + """ def __init__(self, master, *args, **kwargs): super().__init__(master) self.color_str = None @@ -87,6 +90,9 @@ def _color(self): self.btnColor.config(bg=self.color_str) class Profile(base_frame): + """ + Class for the profile selection. This class is used to select a profile for the game. + """ def __new__(cls, master, *args, **kwargs): if len(master.players) == 0: return NewProfile(master, *args, **kwargs) diff --git a/UI/single.py b/UI/single.py index 72ef7cb..d0d44fb 100644 --- a/UI/single.py +++ b/UI/single.py @@ -9,6 +9,9 @@ from .profile import Profile class Singleplayer(base_frame): + """ + The singleplayer menu. This screen is used to choose the opponent for a singleplayer game. + """ def __new__(cls, master, *args, **kwargs): if(len(master.players) == 0 or master.player == None): return Profile(master, *args, return_to=Singleplayer, **kwargs) diff --git a/UI/statistics.py b/UI/statistics.py index a9fe8a9..653b804 100644 --- a/UI/statistics.py +++ b/UI/statistics.py @@ -5,6 +5,9 @@ from Server.player import Player class Statistics_data(tk.Container): + """ + Class for the statistics data in the statistics frame. This class is used to display the statistics of the players. + """ def __init__(self, master, *args, **kwargs): super().__init__(master) @@ -16,6 +19,9 @@ def _update_statistics(self, queue): tk.Label(self, text=value).grid(sticky=tk.E+tk.W+tk.N+tk.S, column=j+1, row=i) class Statistics(base_frame): + """ + The statistics menu. This screen is used to display the statistics of the players. + """ def __init__(self, master, return_to, *args, **kwargs): super().__init__(master) self.return_to = return_to From b5f87e71d81b4ac335a2e39895d5f317eed69944 Mon Sep 17 00:00:00 2001 From: Petzys <87223648+Petzys@users.noreply.github.com> Date: Tue, 19 Mar 2024 14:04:47 +0100 Subject: [PATCH 75/77] Fix(kick): Fixed server termination and kicking Co-authored-by: Hauke Platte --- Client/client.py | 1 + Client/ui_client.py | 2 -- Server/websocket_server.py | 15 +++++++++------ UI/multi.py | 15 ++++++++++++++- 4 files changed, 24 insertions(+), 9 deletions(-) diff --git a/Client/client.py b/Client/client.py index fcd2179..b44b1da 100644 --- a/Client/client.py +++ b/Client/client.py @@ -317,6 +317,7 @@ async def chat_message(self, message:str): async def close(self): await self._websocket.close() + exit() async def terminate(self): msg = { diff --git a/Client/ui_client.py b/Client/ui_client.py index 0f19c74..340fe66 100644 --- a/Client/ui_client.py +++ b/Client/ui_client.py @@ -127,8 +127,6 @@ async def await_commands(self): if message: match message["message_type"]: - case "lobby/join": - pass case "lobby/ready": await self.lobby_ready(**message["args"]) case "lobby/kick": diff --git a/Server/websocket_server.py b/Server/websocket_server.py index b4b3cec..5b46d60 100644 --- a/Server/websocket_server.py +++ b/Server/websocket_server.py @@ -16,7 +16,7 @@ class Lobby: def __init__(self, admin:Player, port: int = 8765) -> None: self._players = {} - #self._players = {admin.uuid : admin} + self._admin = admin self._game = None self._inprogress = False self._port = port @@ -73,7 +73,6 @@ async def handler(self, websocket): "message_type": "lobby/kick", "kick_player_uuid": message_json["kick_player_uuid"], })) - case "lobby/ready": @@ -137,9 +136,10 @@ async def handler(self, websocket): case "server/terminate": - logger.info("Server Termination Requested") + logger.info("Server Termination Requested. Checking if game is in progress.") if self._inprogress: + logger.info("Game in progress. Terminating game.") if self._game.players.index(self._players[message_json["player_uuid"]]) == 1: self._game.state.set_winner(2) elif self._game.players.index(self._players[message_json["player_uuid"]]) == 2: @@ -149,15 +149,18 @@ async def handler(self, websocket): await self._end_game() - else: + elif message_json["player_uuid"] == str(self._admin.uuid): # still in lobby, can terminate without game end. + logger.info("Not in game. Sender was host. Terminating server.") websockets.broadcast(self._connections, json.dumps({ "message_type": "game/error", "error_message": "Server terminated.", })) + exit() - + else: + logger.info("Not in game. Sender was not host. Ignoring termination request.") case _: await websocket.send(json.dumps({"message_type": "error", "error": "Unknown message type"})) @@ -172,10 +175,10 @@ async def handler(self, websocket): else: # request a ping from everyone and delete player list to wait for join messages. + self._players = {} websockets.broadcast(self._connections, json.dumps({ "message_type": "lobby/ping", })) - self._players = {} # End this connection loop break diff --git a/UI/multi.py b/UI/multi.py index 9ece69d..7360c00 100644 --- a/UI/multi.py +++ b/UI/multi.py @@ -32,6 +32,7 @@ def __init__(self, master, *args, opponent=player_type.unknown, local_players, q self.master.network_events['lobby/status'] = self._update_lobby self.master.network_events['game/start'] = self._start_game self.master.network_events['lobby/kick'] = self._lobby_kick + self.master.network_events['lobby/connect_error'] = lambda *args: self._connect_error() #error is thrown when the server is not reachable anymore self.bind('', lambda *args: self.on_destroy()) self.ready = False if opponent not in [player_type.unknown, player_type.local]: @@ -66,11 +67,21 @@ def _display_widgets(self): self.btnExit.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=5, row=1) self.btnStatistics.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=5, row=2) + def _connect_error(self): + self.master.show_menu() + msg = messages(type='info', message=f'Connection to the server was lost!') + msg.display() + def _menu(self): list(self.master.out_queue.values())[0].put({'message_type': 'server/terminate', 'args' :{} }) self.master.show_menu() def _update_lobby(self, queue): + if(len(self.playerlist) > 0): + for player in self.playerlist: + for object in player: + object.grid_forget() + object.destroy() self.playerlist = [] for player in queue['player']: rdy = '\u2611' if player.ready else '' @@ -97,11 +108,13 @@ def _start_game(self, queue): def on_destroy(self): del self.master.network_events['lobby/status'] del self.master.network_events['game/start'] + del self.master.network_events['lobby/kick'] + del self.master.network_events['lobby/connect_error'] def _lobby_kick(self, queue): + self._menu() msg = messages(type='info', message=f'You have been kicked from the lobby by the host') msg.display() - self._menu() def _show_statistics(self): self.master.cache_current_frame() From b73041a5ab4767b10b6ebc8345e4ffe5b873bc59 Mon Sep 17 00:00:00 2001 From: Petzys <87223648+Petzys@users.noreply.github.com> Date: Tue, 19 Mar 2024 14:22:44 +0100 Subject: [PATCH 76/77] Feat(UI statistics): Highlighting best statistics --- UI/statistics.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/UI/statistics.py b/UI/statistics.py index 653b804..8eca475 100644 --- a/UI/statistics.py +++ b/UI/statistics.py @@ -13,11 +13,21 @@ def __init__(self, master, *args, **kwargs): def _update_statistics(self, queue): heading = {Player('Player', 0): {'wins': 'Wins', 'losses': 'Losses', 'draws': 'Draws', 'moves': 'Moves', 'emojis': 'Emojis'}} + + highscores = {'wins': 0, 'losses': 0, 'draws': 0, 'moves': 0, 'emojis': 0} + for player, values in queue['statistics'].items(): + for headline, value in values.items(): + if value > highscores[headline]: + highscores[headline] = value + for i, (player, values) in enumerate((heading | queue['statistics']).items()): tk.Label(self, text=player.display_name).grid(sticky=tk.E+tk.W+tk.N+tk.S, column=0, row=i) for j, (headline, value) in enumerate(values.items()): - tk.Label(self, text=value).grid(sticky=tk.E+tk.W+tk.N+tk.S, column=j+1, row=i) - + lbl = tk.Label(self, text=value) + if value == highscores[headline]: + lbl.config(fg='green') + lbl.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=j+1, row=i) + class Statistics(base_frame): """ The statistics menu. This screen is used to display the statistics of the players. From 799f6c7f7d93ea774f8cc18f196912a0463b0140 Mon Sep 17 00:00:00 2001 From: Petzys <87223648+Petzys@users.noreply.github.com> Date: Tue, 19 Mar 2024 14:29:23 +0100 Subject: [PATCH 77/77] Fix(Error Message): Now showing server error messages to user --- UI/messages.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/UI/messages.py b/UI/messages.py index 3d861ac..ce20d7e 100644 --- a/UI/messages.py +++ b/UI/messages.py @@ -55,9 +55,7 @@ def display_move(self): Displays a message box for a move, if no message provided, it will display a default message :param move: """ - message = "Illegal Move!" - - mb.showinfo("Move", message) + mb.showinfo("Move", self.message) def set_message(self, message : str):