Skip to content

Commit

Permalink
Merge pull request #21 from TINF21CS1/TTTK-53
Browse files Browse the repository at this point in the history
Tttk 53
  • Loading branch information
Petzys authored Mar 20, 2024
2 parents 6576d17 + 0c12d4e commit 139c058
Show file tree
Hide file tree
Showing 4 changed files with 119 additions and 15 deletions.
17 changes: 14 additions & 3 deletions Server/websocket_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
import logging
import json
from jsonschema import validate, ValidationError
from uuid import UUID
import uuid
from zeroconf import ServiceInfo, Zeroconf
import socket

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
Expand All @@ -26,6 +28,14 @@ def __init__(self, admin:Player, port: int = 8765) -> None:
with open("./json_schema/client_to_server.json", "r") as f:
self._json_schema = json.load(f)

# MDNS
#https://stackoverflow.com/a/74633230
self._mdns = Zeroconf()
ip = socket.inet_aton(socket.gethostbyname(socket.gethostname())) # this is a dirty hack and also doesnt work dual stack :'(
wsInfo = ServiceInfo(type_ = '_tictactoe._tcp.local.', name = admin.display_name+'s-Server'+'._tictactoe._tcp.local.', port = self._port, addresses = [ip])
self._mdns.register_service(wsInfo)


async def handler(self, websocket):

self._connections.add(websocket)
Expand All @@ -50,7 +60,7 @@ async def handler(self, websocket):
await websocket.send("Game in progress, cannot join") # TODO jsonify
break

self._players[message_json["profile"]["uuid"]] = Player(uuid=UUID(message_json["profile"]["uuid"]), display_name=message_json["profile"]["display_name"], color=message_json["profile"]["color"])
self._players[message_json["profile"]["uuid"]] = Player(uuid=uuid.UUID(message_json["profile"]["uuid"]), display_name=message_json["profile"]["display_name"], color=message_json["profile"]["color"])

# send new lobby status
websockets.broadcast(self._connections, json.dumps({
Expand Down Expand Up @@ -90,6 +100,7 @@ async def handler(self, websocket):
self._game = Game(player1 = list(self._players.values())[0], player2 = list(self._players.values())[1], rule_base = rulebase)

self._inprogress = True
self._mdns.unregister_all_services()

websockets.broadcast(self._connections, json.dumps({
"message_type": "game/start",
Expand Down Expand Up @@ -207,5 +218,5 @@ def run(self):
asyncio.run(self.start_server())

if __name__ == "__main__":
lobby = Lobby(port = 8765, admin = Player(uuid=UUID("c4f0eccd-a6a4-4662-999c-17669bc23d5e"), display_name="admin", color=0xffffff, ready=True))
lobby = Lobby(port = 8765, admin = Player(uuid=uuid.UUID("c4f0eccd-a6a4-4662-999c-17669bc23d5e"), display_name="admin", color=0xffffff, ready=True))
lobby.run()
45 changes: 45 additions & 0 deletions UI/autodiscovery.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from zeroconf import ServiceBrowser, ServiceListener, Zeroconf
import logging
import time

logger = logging.getLogger(__name__)

def discover_games(timeout: int = 5) -> dict:
"""
Returns a dictionary of games that are available to play.
The key is the name of the game and the value is the IP address of the server as string.
param: timeout. The time in seconds to wait for responses. Default is 5 seconds.
return: dict. A dictionary of games on the network that are available to play. Key is the name, Value is the IP as string.
"""

class MyListener(ServiceListener):
def __init__(self):
self.results = {}

def add_service(self, zc: Zeroconf, type_: str, name: str) -> None:
info = zc.get_service_info(type_, name)
if info:
logger.info(f"Service {name} found, service address: {info.parsed_addresses()[0]}") # TODO only gets first address, so dual stack is not possible
self.results.update({name.removesuffix('._tictactoe._tcp.local.'): info.parsed_addresses()[0]})
else:
logger.error(f"Service {name} found, no info")

def remove_service(self, zc: Zeroconf, type_: str, name: str) -> None:
logger.info(f"Service {name} removed")


zeroconf = Zeroconf()
listener = MyListener()
ServiceBrowser(zeroconf, "_tictactoe._tcp.local.", listener)

time.sleep(timeout)

zeroconf.close()
return listener.results

if __name__ == "__main__":
while True:
print(discover_games())
time.sleep(5)
71 changes: 59 additions & 12 deletions UI/multi.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
#import tkinter as tk
from .lib import tttk_tk as tk
from uuid import UUID
from threading import Thread
from queue import Queue

from sys import exit
from .base_frame import base_frame
from .field_frame import Field
from .profile import Profile, NewProfile
Expand All @@ -11,6 +11,7 @@
from Server.main import server_thread
from AI.ai_context import AIContext
from AI.ai_strategy import WeakAIStrategy, AdvancedAIStrategy
from .autodiscovery import discover_games
from .chat import Chat
from .messages import messages
from .gamefield import input_methods
Expand Down Expand Up @@ -193,6 +194,21 @@ def _start_game(self):
self.master.out_queue[player2.uuid].put({'message_type': 'lobby/ready', 'args' : {'ready': True}})
self.master.show(Join, display=False, opponent=player_type.local, local_players=[player1.uuid, player2.uuid],quiet=True)


def reload(tkinter_obj: tk.Widget, queue: Queue):
for i in range(10):
servers = discover_games(i+1)
queue.put(servers)
try:
tkinter_obj.event_generate('<<lobby/reload>>')
except tk.TclError:
pass
try:
tkinter_obj.event_generate('<<thread/exit>>')
except tk.TclError:
pass
exit()

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.
Expand All @@ -201,44 +217,52 @@ def __init__(self, master):
super().__init__(master)
self._create_widgets()
self._display_widgets()
self.queue = Queue()
self.thread = False
self.servers = {}
self.master.master.network_events['lobby/connect'] = self._lobby_connect
self.master.master.network_events['lobby/connect_error'] = self._connect_error
self.bind('<<lobby/reload>>', lambda *args: self._finish_reload())
self.bind('<<thread/exit>>', lambda *args: self._thread_reset())
self.bind('<Destroy>', lambda *args: self.on_destroy())
self._start_reload()

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.btnReload = tk.Button(self.innerframe, text="\u21BB", command=lambda *args: self._start_reload(), border=0)
self.wrapper = tk.Container(self.innerframe)
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.btnConnect = tk.Button(self.innerframe, text="Connect", command=lambda ip=self.etrAddress.get(), *args: self._connect(ip))
self.master.master.bind('<Return>', 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([0, 2, 4, 5], 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)
self.btnReload.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=4, row=0)
self.btnManual.grid(sticky=tk.E+tk.W+tk.N+tk.S, column=1, row=4, columnspan=4)

def manually(self):
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.btnConnect.grid(column=3, row=10, columnspan=2, 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):
def _connect(self, ip):
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.network_client = client_thread(root, in_queue=list(root.out_queue.values())[0], out_queue=root.in_queue, player=root.players[root.player], ip=ip)
self.etrAddress.config(state=tk.DISABLED)
self.btnConnect.config(text="Connecting...", state=tk.DISABLED)

Expand All @@ -256,6 +280,29 @@ def on_destroy(self):
del self.master.master.network_events['lobby/connect']
del self.master.master.network_events['lobby/connect_error']

def _start_reload(self):
if(self.thread):
return
Thread(target=reload, args=(self, self.queue), daemon=True).start()
self.thread = True

def _finish_reload(self):
servers = self.queue.get()
if(servers == self.servers):
return
self.servers = servers
self.wrapper.destroy()
self.wrapper = tk.Container(self.innerframe)
self.wrapper.columnconfigure([0], weight=1)
self.wrapper.columnconfigure([1], weight=0)
for i, (server, ip) in enumerate(self.servers.items()):
tk.Label(self.wrapper, text=server).grid(column=0, row=i, sticky=tk.W+tk.N+tk.S)
tk.Button(self.wrapper, text="Join", command=lambda ip=ip, *args: self._connect(ip)).grid(column=1, row=i, sticky=tk.E+tk.W+tk.N+tk.S)
self.wrapper.grid(column=1, row=2, columnspan=4, sticky=tk.E+tk.W+tk.N+tk.S)

def _thread_reset(self):
self.thread = False

class Multiplayer(base_frame):
"""
Class for the multiplayer screen. This screen is used to select the multiplayer mode and to create or join lobbies.
Expand All @@ -271,7 +318,7 @@ def __init__(self, master, *args):
self.master.out_queue = {}
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)
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
websockets==12.0
jsonschema==4.21.1
emoji==2.10.1
zeroconf

0 comments on commit 139c058

Please sign in to comment.