Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Tttk 53 #21

Merged
merged 5 commits into from
Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading