diff --git a/Assets/Arenas/default.png b/Assets/Arenas/default.png new file mode 100644 index 0000000..ba5da22 Binary files /dev/null and b/Assets/Arenas/default.png differ diff --git a/Assets/Cards/Arena/field.png b/Assets/Cards/Arena/field.png new file mode 100644 index 0000000..472f0a1 Binary files /dev/null and b/Assets/Cards/Arena/field.png differ diff --git a/Assets/Cards/MonsterCards/testmonstercard/card.json b/Assets/Cards/MonsterCards/testmonstercard/card.json new file mode 100644 index 0000000..0df2ece --- /dev/null +++ b/Assets/Cards/MonsterCards/testmonstercard/card.json @@ -0,0 +1,22 @@ +{ + "id": 1, + "name": "Test Monster", + "image": "Assets/Cards/testmonstercard/cards.png", + "description": "can attack other monsters", + "costs": 30, + "defense": 40, + "attacks":[ + { + "id": 1, + "name":"test attack", + "description": "can attack another Monster", + "damage": 80 + }, + { + "id": 2, + "name":"test attack", + "description": "can attack another Monster", + "damage": 80 + } + ] +} diff --git a/Assets/Cards/MonsterCards/testmonstercard/card.png b/Assets/Cards/MonsterCards/testmonstercard/card.png new file mode 100644 index 0000000..320d60d Binary files /dev/null and b/Assets/Cards/MonsterCards/testmonstercard/card.png differ diff --git a/Assets/Cards/SpeelCards/testspellcard/artworkjson.png b/Assets/Cards/SpeelCards/testspellcard/artworkjson.png new file mode 100644 index 0000000..9955a0c Binary files /dev/null and b/Assets/Cards/SpeelCards/testspellcard/artworkjson.png differ diff --git a/Assets/Cards/SpeelCards/testspellcard/testspellcard.json b/Assets/Cards/SpeelCards/testspellcard/testspellcard.json new file mode 100644 index 0000000..51d4083 --- /dev/null +++ b/Assets/Cards/SpeelCards/testspellcard/testspellcard.json @@ -0,0 +1,6 @@ +{ + "name": "testspell", + "image":"Assets/Cards/testspelltcard/artwork.png", + "costs": 30, + "description":"this is a test spell card" +} \ No newline at end of file diff --git a/Assets/Cards/TrapCards/testtrapcard/artworkjson.png b/Assets/Cards/TrapCards/testtrapcard/artworkjson.png new file mode 100644 index 0000000..9955a0c Binary files /dev/null and b/Assets/Cards/TrapCards/testtrapcard/artworkjson.png differ diff --git a/Assets/Cards/TrapCards/testtrapcard/testtrapcard.json b/Assets/Cards/TrapCards/testtrapcard/testtrapcard.json new file mode 100644 index 0000000..6d1cd5a --- /dev/null +++ b/Assets/Cards/TrapCards/testtrapcard/testtrapcard.json @@ -0,0 +1,6 @@ +{ + "name": "testtrap", + "image":"Assets/Cards/testtrapcard/artwork.png", + "costs": 30, + "description":"this is a test tryp card" +} \ No newline at end of file diff --git a/Classes/Game/Player.py b/Classes/Game/Player.py new file mode 100644 index 0000000..2d337be --- /dev/null +++ b/Classes/Game/Player.py @@ -0,0 +1,57 @@ +import random + + +class Player: + __id:int + __hp:int + __mana:int + __name:str + __handCards:list + __deck:list + + def __init__(self, name:str, deck:list, hp:int=1000, mana:int=0): + self.__hp = hp + self.__mana = mana + self.__name = name + self.__handCards = [] + self.__deck = deck + self.__id = random.randint(3, 99999) + + def shuffleDeck(self): + self.__deck = random.shuffle(self.__deck) + + def getDeck(self) -> list: + return self.__deck + + def getName(self) -> str: + return self.__name + + def getHP(self) -> int: + return self.__hp + + def adjustHP(self, hp:int) -> int: + self.__hp = self.__hp + hp + + def getID(self) -> int: + return self.__id + + def getHand(self) -> list: + return self.__handCards + + def getMana(self) -> int: + return self.__mana + + def addMana(self, amount) -> int: + self.__mana + amount + return self.__mana + + def AddToHand(self, card) -> list: + self.__handCards.append(card) + return self.__handCards + + def setHand(self, hand:list): + self.__handCards = hand + + def removeFromHand(self, pos:int) -> list: + self.__handCards.remove(pos) + return self.__handCards \ No newline at end of file diff --git a/Classes/System/GameManager.py b/Classes/System/GameManager.py new file mode 100644 index 0000000..3930a80 --- /dev/null +++ b/Classes/System/GameManager.py @@ -0,0 +1,153 @@ +import json +import socket +import time +from Classes.Game.Player import Player + + +class GameManager: + __players:dict + __playingPlayer:Player + __state:str + __round:str + __cards:list + + def __init__(self, logger): + self.__players = {} + self.__playingPlayer = None + self.__state = "waiting" + self.__round = "none" + self.logger = logger + self.__cards = [] + pass + + def getLogger(self): + return self.logger + + # card management + def spawnCard(self, card, owner, x, y): + # self.logger.info("spawning card") + + self.__cards.append(card) + + payload = { + "event":"PlacedCard", + "owner": owner, + "card": card, + "x": x, + "y": y, + } + + for userAddr in self.__players.keys(): + try: + self.logger.info(f"send to client {self.__players[userAddr]['player'].getID() != owner}") + if self.__players[userAddr]["player"].getID() != owner: + self.__players[userAddr]["socket"].send(json.dumps(payload).encode()) + except: + pass + + # game round management + # this section manages the flow of rounds this should inherit itself + # ============================================================================= + + # this function iterates all + def progressRound(self): + # phases + # - playerPrep => playing player switches, gets a mana point and gets verified + if self.__playingPlayer != None: + for player in self.__players: + if self.__playingPlayer != player: + self.__playingPlayer = player + else: + self.__playingPlayer = next(iter(self.__players)) + # - playerDraw => player draws a card + # - playerPlay => player can place cards and active effects + # - playerEnd => player ends his turn and the code reiterates with the remaining player + pass + + # game state management + # this section mostly only used by the networking and event handling classes + # other parts should never need to interface with this unless really required + # ============================================================================= + def startGame(self): + self.__state = "running" + + players = list(self.__players.values()) + + print("game starts") + self.logger.info("game manager is starting the game") + for userAddr, player_data in self.__players.items(): + try: + user = self.__players[userAddr]["player"] + user.addMana(1000) + user.adjustHP(1000) + user.shuffleDeck() + cards = self.__players[userAddr]["deck"] + user.setHand(cards[:5]) + + # iterates until the enemy player is not anymore equal to current player + enemy = next(player_data["player"] for player_data in players if player_data["player"] != user) + + payload = { + "event": "startgame", + "player": { + "mana": user.getMana(), + "hp": user.getHP(), + "hand": user.getHand() + }, + "enemy": { + "id": enemy.getID(), + "name": enemy.getName(), + "hp": enemy.getHP(), + }, + } + + print(f"user {player_data["socket"]}") + player_data["socket"].send(json.dumps(payload).encode()) + except Exception as e: + self.logger.error(f"failed to start game due to error: {e}") + pass + # handles notifying all players that the game starts + pass + + def stopGame(self): + # handles notifying all players that the game stops + # handles stoping the game itself and notifies server to stop itself + pass + + # player management + # the network manager will create a player instance + # ============================================================= + + # gets all player known to the game manager and returns them + def getPlayers(self) -> dict: + return self.__players + + # creates a player and handles counting all players and if conditions met starting the game + # returns the new dict in which the new player now is added + def addPlayers(self, player:Player, socket:socket, clientAddr, deck) -> dict: + + self.logger.info(f"creating user with id: {player.getID}") + self.__players[clientAddr] = { + "player": player, + "deck": deck, + "socket":socket + } + self.logger.info(f"new length of user dictionary: {len(self.__players)}") + + payload = { + "event":"loginresponse", + "status": "success", + "id": player.getID(), + "name": player.getName() + } + + socket.send(json.dumps(payload).encode()) + + + # counts participating players and starts the game if enough have joined + if len(self.__players) >= 2: + time.sleep(1) + self.logger.info("2 players have join game starts") + self.startGame() + + return self.__players \ No newline at end of file diff --git a/Classes/System/Logger.py b/Classes/System/Logger.py new file mode 100644 index 0000000..b548160 --- /dev/null +++ b/Classes/System/Logger.py @@ -0,0 +1,18 @@ +import logging + + +class Logger: + def __init__(self, filename): + logging.basicConfig(filename=filename, + filemode='a', + format='%(asctime)s,%(msecs)d %(name)s %(levelname)s %(message)s', + datefmt='%H:%M:%S', + level=logging.DEBUG) + + def info(self, message): + print(message) + logging.info(message) + + def error(self, message): + print(message) + logging.error(message) \ No newline at end of file diff --git a/Classes/System/Network/EventHandler.py b/Classes/System/Network/EventHandler.py new file mode 100644 index 0000000..6cca201 --- /dev/null +++ b/Classes/System/Network/EventHandler.py @@ -0,0 +1,32 @@ +import socket +from Classes.Game.Player import Player +from Classes.System.GameManager import GameManager + +from Classes.System.World import World + + +class TCPEventHandler: + __tcpSocket:socket + + def __init__(self, socket:socket): + self.__tcpSocket = socket + + # handles passing of event data to the right functions + def handleTCPEvents(self, event, gameManager:GameManager, address): + gameManager.getLogger().info(f"incommingevent {event}") + if event["event"] == "placecard": + gameManager.spawnCard(event["card"], event["user"], event["x"], event["y"]) + pass + elif event["event"] == "MoveCard": + pass + elif event["event"] == "RemoveCard": + pass + elif event["event"] == "AttackCard": + pass + elif event["event"] == "AttackPlayer": + pass + elif event["event"] == "ActivateEffectCard": + pass + elif event["event"] == "ActivateMonsterCard": + pass + pass \ No newline at end of file diff --git a/Classes/System/Network/NetworkManger.py b/Classes/System/Network/NetworkManger.py new file mode 100644 index 0000000..60a6a3f --- /dev/null +++ b/Classes/System/Network/NetworkManger.py @@ -0,0 +1,137 @@ +import json +import signal +import socket +import sys +import threading +from Classes.Game.Player import Player +from Classes.System.GameManager import GameManager + +from Classes.System.Network.EventHandler import TCPEventHandler +from Classes.System.World import World + +class NetworkManager: + class TCP: + __Addr:str + __Port:str + __BufferSize:int = 1024 + __tcpSocket:socket + __eventHandler: dict + __users: dict + __TCPClientThread:threading.Thread + __gameManager:GameManager + + def __init__(self, Addr:str, Port:str, gameManager:GameManager): + gameManager.getLogger().info("starting up network manager") + + self.running = True + self.__Addr = Addr + self.__Port = int(Port) + self.__gameManager = gameManager + self.__eventHandler = {} + + gameManager.getLogger().info("starting up tcp server") + self.__tcpSocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.__tcpSocket.bind((self.__Addr, self.__Port)) + self.__tcpSocket.listen() + + gameManager.getLogger().info("starting up thread for client socket accepting") + self.__TCPClientThread = threading.Thread(target=self.accept_connections) + self.__TCPClientThread.daemon = True + self.__TCPClientThread.start() + + def accept_connections(self): + while self.running: + try: + client_tcp_socket, client_address = self.__tcpSocket.accept() + self.__gameManager.getLogger().info(f"Connected with {client_address}") + self.__gameManager.getPlayers()[client_address] = client_tcp_socket + self.__eventHandler[client_address] = TCPEventHandler(client_tcp_socket) + + client_handler_thread = threading.Thread( + target=self.receive, + args=(client_tcp_socket, client_address) + ) + + self.__gameManager.getLogger().info(f"starting client handler thread for client at address {client_address}") + client_handler_thread.daemon = True + client_handler_thread.start() + + except Exception as e: + self.__gameManager.getLogger().error(f"tcp socket failed to accept connection due to error: {e}") + pass + client_handler_thread.join() + + def receive(self, client_socket, client_address): + while self.running: + try: + data = client_socket.recv(self.__BufferSize) + if not data: + self.__gameManager.getLogger().info(f"Connection with {client_address} closed.") + break + + try: + + message = data.decode() + messageJson = json.loads(message) + self.__gameManager.getLogger().info(f"decoded message {messageJson}") + user = messageJson.get("user") + self.__gameManager.getLogger().info(f"user in message {user}") + + except Exception as ex: + self.__gameManager.getLogger().info(f"decoding incoming packet failed due to exception: {ex}") + + # creates a user and counts how many currently are connected to the server + # if enough users for a round are connected the server has to start the game + if user not in self.__gameManager.getPlayers(): + if messageJson["event"] == "login": + self.__gameManager.getLogger().info("user logging in") + self.__gameManager.getLogger().info("task passed off to gameManager") + user = self.__gameManager.addPlayers(Player(messageJson["username"], messageJson["deck"]), client_socket, client_address, messageJson["deck"]) + self.__gameManager.getLogger().info(f"connected users {len(self.__gameManager.getPlayers())}") + + self.__gameManager.getLogger().info(f"confirming login for user") + + self.__eventHandler[client_address].handleTCPEvents(messageJson, self.__gameManager, client_address) + self.__gameManager.getLogger().info(f"Received message from {client_address}: {message}") + + except socket.error as e: + + if e.errno == 10054: + self.__gameManager.getLogger().error(f"Connection with {client_address} forcibly closed by remote host.") + self.__gameManager.getPlayers()[client_address] = None + break + + self.__gameManager.getLogger().error(f"Socket error receiving data from {client_address}: {e}") + + except json.JSONDecodeError as e: + self.__gameManager.getLogger().error(f"JSON decoding error receiving data from {client_address}: {e}") + + # except Exception as e: + # self.__gameManager.getLogger().error(f"UknownError receiving data from {client_address} due to error: {e}") + + def broadcast(self, payload:dict): + for user in self.__gameManager.getPlayers().values(): + user["socket"].send(json.dumps(payload).encode()) + + def send(self, payload: dict, user: str): + players = self.__gameManager.getPlayers() + + if user in players and "socket" in players[user]: + players[user]["socket"].send(json.dumps(payload).encode()) + else: + self.__gameManager.getLogger().error(f"user '{user}' or socket was not found 'socket' failed to send data.") + + def stop(self): + self.__TCPClientThread.join() # Wait for the thread to finish before exiting + + tcp: TCP + # udp: UDP + + def __init__(self, Addr:str, TCPPort:str, UDPPort:str, gameManager:GameManager): + self.tcp = self.TCP(Addr, TCPPort, gameManager) + signal.signal(signal.SIGINT, self.handle_interrupt) # Register the signal handler + + def handle_interrupt(self, signum, frame): + self.__gameManager.getLogger().info("Received keyboard interrupt. Stopping the server.") + self.tcp().stop() + sys.exit(0) \ No newline at end of file diff --git a/Classes/System/PlayerManager.py b/Classes/System/PlayerManager.py new file mode 100644 index 0000000..e149622 --- /dev/null +++ b/Classes/System/PlayerManager.py @@ -0,0 +1,23 @@ +import json +class Player: + + def createUser(self, user:json): + self.__users.append(user) + + def createUser(self, user:json): + if self.getUser(user["username"]) == None: + self.__users.append(Player(user["username"])) + + def removeUser(self, user:int): + self.__users.remove(user) + + def removeUser(self, user:str): + self.__users.remove(user) + + def getUsers(self) -> list: + return self.__users + + def getUser(self, user:int): + for user in self.__users: + if int(user["id"]) == user: + return user \ No newline at end of file diff --git a/Classes/System/QueueManager.py b/Classes/System/QueueManager.py new file mode 100644 index 0000000..be82662 --- /dev/null +++ b/Classes/System/QueueManager.py @@ -0,0 +1,34 @@ + + +from Classes.Game.Player import Player + + +class QueueManager: + __queue:list + + def __init__(self): + self.__queue = [] + + def getQueue(self) -> list: + return self.__queue + + def addToQueue(self, user) -> list: + if self.isInQueue(user["id"]): + self.__queue.append(user) + return self.__queue + + def removeFromQueue(self, player:Player) -> list: + self.__queue.remove(player) + return self.__queue + + def isInQueue(self, user:int) -> bool: + for user in self.__queue: + if int(user["id"]) == user: + return True + return False + + def isInQueue(self, user:str) -> bool: + for user in self.__queue: + if user["username"] == user: + return True + return False \ No newline at end of file diff --git a/Classes/System/Server.py b/Classes/System/Server.py new file mode 100644 index 0000000..c3485db --- /dev/null +++ b/Classes/System/Server.py @@ -0,0 +1,40 @@ +import json +import socket +import threading +from Classes.System.GameManager import GameManager + +from Classes.System.Network.NetworkManger import NetworkManager +from Classes.System.PlayerManager import Player +from Classes.System.World import World +from Classes.System.Logger import Logger + +class Server: + + __address:str + __tcpPort:str + __udpPort:str + __world:World + __gameManager:GameManager + + networkManager:NetworkManager + + def __init__(self, address:str, tcpPort:str, udpPort:str, logger:Logger): + self.__address = address + self.__tcpPort = tcpPort + self.__udpPort = udpPort + self.__world = World() + self.logger = logger + + self.logger.info("starting up game manager") + self.__gameManager = GameManager(logger) + + self.logger.info("preparing to start server") + self.startServer(self.__gameManager) + + # handles starting the server and assigning socket values to the local reference + def startServer(self, gameManager:GameManager): + self.logger.info("starting up network manager") + self.__networkManager = NetworkManager(self.__address, self.__tcpPort, self.__udpPort, gameManager) + + def getNetworkManager(self) -> NetworkManager: + return self.__networkManager \ No newline at end of file diff --git a/Classes/System/Utils/Path.py b/Classes/System/Utils/Path.py new file mode 100644 index 0000000..a274f43 --- /dev/null +++ b/Classes/System/Utils/Path.py @@ -0,0 +1,6 @@ +import os + + +class PathUtil: + def getAbsolutePathTo(notAbsolutPath:str) -> str: + return os.path.abspath("OLD_Server/" + notAbsolutPath) \ No newline at end of file diff --git a/Classes/System/Utils/StringUtils.py b/Classes/System/Utils/StringUtils.py new file mode 100644 index 0000000..e4d02a1 --- /dev/null +++ b/Classes/System/Utils/StringUtils.py @@ -0,0 +1,11 @@ +import random +import string + + +class StringUtils: + def get_random_string(length) -> str: + # choose from all lowercase letter + letters = string.ascii_lowercase + result_str = ''.join(random.choice(letters) for i in range(length)) + print("Random string of length", length, "is:", result_str) + return result_str diff --git a/Classes/System/World.py b/Classes/System/World.py new file mode 100644 index 0000000..f6b2957 --- /dev/null +++ b/Classes/System/World.py @@ -0,0 +1,20 @@ +import socket +from Classes.Game.Player import Player + + +class World: + __players:dict + + def __init__(self): + self.__players = {} + + def getPlayers(self) -> list: + return self.__players + + def addPlayers(self, player:Player, socket:socket, clientAddr) -> list: + self.__players[clientAddr] = { + player: player, + socket:socket + } + + return self.__players \ No newline at end of file diff --git a/index.py b/index.py new file mode 100644 index 0000000..27fd861 --- /dev/null +++ b/index.py @@ -0,0 +1,31 @@ +import logging +import os +import random +import string +import sys + +from Classes.System.Server import Server +from Classes.System.Logger import Logger +from Classes.System.Utils.Path import PathUtil + +def get_random_string(length) -> str: + # choose from all lowercase letter + letters = string.ascii_lowercase + result_str = ''.join(random.choice(letters) for i in range(length)) + print("Random string of length", length, "is:", result_str) + return result_str + +def main(): + # retrieves host data from environment + HOST = "127.0.0.1" + TCPPORT = "54322" + UDPPORT = "54323" + + logger = Logger(PathUtil.getAbsolutePathTo("log/"+get_random_string(8)+".log")) + logger.info("starting up server") + server = Server(HOST, TCPPORT, UDPPORT, logging) + server.getNetworkManager().tcp.stop() + sys.exit(0) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/server logic notes.md b/server logic notes.md new file mode 100644 index 0000000..7370d4a --- /dev/null +++ b/server logic notes.md @@ -0,0 +1,42 @@ +# validation for placing cards: +- is the game still running +- is it the players turn +- does the card exist +- does the player have that card in his deck +- does the player have this card in his hand +- is the type of card allowed in that type of field +- is the field already blocked by another card + +# validation for attacking another player +- is the game still running +- is it the players turn +- does the card exist +- does the player have that card in his deck +- is that card played +- does the enemy have remaining monster cards on his side + - if yes a direct attack would only be possible if a effect allows it +- can this card attack + - is the card of correct type + - is it blocked by effects (will be implemented after card effects are implemented) + +# player death management (win condition) +- does a players hp go to 0? + - make the other player the winner + - if an effect affects the playing player card priority comes first + +# handle a player leaving +- check if game still runs + - make remaining player win if yes + +# turn management +- server keeps track of each turn + - whos turn is it + - what turn state is currently active + - draw state + - place state + - is the player trying to do actions not allowed in the given state + +# drawing cards: +- ensure the player only can have 7 cards + - if limit exceeds the player payes lifepoints and drops a card +- ensure the drawn card for sure still can be in the players deck \ No newline at end of file