commit 16ad2902e65bf88b4b92ca92198e77f20b3bbd1f Author: thecookingsenpai Date: Mon Dec 25 13:26:15 2023 +0100 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9bea433 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ + +.DS_Store diff --git a/README.md b/README.md new file mode 100644 index 0000000..260faeb --- /dev/null +++ b/README.md @@ -0,0 +1,11 @@ +# CodeForHonor Streamline + +A CLI based game demonstrating the cfo engine capabilities + +## cfo engine + +Powers a great pletora of possible games in pure python by using non blocking sockets for multiplayer communications and intersecated classes + +## Status + +This project is totally WIP and lacks documentation \ No newline at end of file diff --git a/engine/.vscode/settings.json b/engine/.vscode/settings.json new file mode 100644 index 0000000..c53effe --- /dev/null +++ b/engine/.vscode/settings.json @@ -0,0 +1,7 @@ +{ + "editor.quickSuggestions": { + "other": "on", + "comments": true, + "strings": "off" + } +} \ No newline at end of file diff --git a/engine/CFO.png b/engine/CFO.png new file mode 100644 index 0000000..545da4f Binary files /dev/null and b/engine/CFO.png differ diff --git a/engine/CFO_midsize.png b/engine/CFO_midsize.png new file mode 100644 index 0000000..21fa492 Binary files /dev/null and b/engine/CFO_midsize.png differ diff --git a/engine/__pycache__/cfo.cpython-39.pyc b/engine/__pycache__/cfo.cpython-39.pyc new file mode 100644 index 0000000..4ad9b11 Binary files /dev/null and b/engine/__pycache__/cfo.cpython-39.pyc differ diff --git a/engine/cfo.py b/engine/cfo.py new file mode 100644 index 0000000..17a48bf --- /dev/null +++ b/engine/cfo.py @@ -0,0 +1,61 @@ + +# INFO +# This library contains the classes and the relative +# methods that will be used in the main code and in game library + +# ANCHOR Character class +class Character: + + def __init__(self): + self.name = "" # Name of the character + self.weapons = [] + self.max_ep = 100 # Energy points + self.max_hp = 100 # Max health points + self.protection = 0 # Protection points + self.equipment = { + "head": None, + "arms": None, + "body": None, + "legs": None, + "feet": None, + "weapon_dx": None, + "weapon_sx": None + } + + +# ANCHOR Weapon code class +class WeaponCode: + + def __init__(self): + self.name = "" # Name of the weapon + self.invocation = "" # Invocation of the weapon + # True if the weapon is offensive, False if it's defensive or tool + self.is_offensive = None + self.is_tool = None # True if the weapon is a tool + # If the weapon is a tool, the following attributes are not needed + self.damage = 0 # Damage of the weapon + self.accuracy = 0 # Accuracy of the weapon + self.range = 0 # Range of the weapon + self.is_ranged = None # True if the weapon is ranged, False if it's melee + # Number of ammo rounds (if the weapon is ranged or use energy); -1 means infinite + self.rounds_max = 0 + self.rounds = 0 # Actual rounds loaded + self.rounds_per_shoot = 0 # Number of ammo used when shooting + # The following attributes are generic + self.resistance = 100 # Resistance of the weapon, -1 means infinite + self.speed = 0 # Speed of the weapon + + +# ANCHOR Player class +class Player: + + def __init__(self): + self.username = "" # Username + self.score = 0 # Score of the player + # NOTE Character prototype + # [character_object, expiration_time] + self.characters = [] # List of characters owned by the player + self.is_banned = False # True if the player is banned + self.is_in_match = False # True if the player is in a match + self.last_match = 0 # ID of the last match played or playing + diff --git a/engine/data/characters/default/character.cfo b/engine/data/characters/default/character.cfo new file mode 100644 index 0000000..6d09261 Binary files /dev/null and b/engine/data/characters/default/character.cfo differ diff --git a/engine/data/characters/default/weapon b/engine/data/characters/default/weapon new file mode 100644 index 0000000..eaf70e0 Binary files /dev/null and b/engine/data/characters/default/weapon differ diff --git a/engine/gamelib.py b/engine/gamelib.py new file mode 100644 index 0000000..967836f --- /dev/null +++ b/engine/gamelib.py @@ -0,0 +1,314 @@ +# INFO +# This library defines the methods used to manage +# and work with game sessions and fights +import os +import sys +import json +import pickle +import time +import socket +import fcntl +import errno +from requests import get +import cfo + +# ANCHOR Game class +class Game: + + def __init__(self): + # NOTE Fight server status + self.busy = False # True if the player is in a fight + self.connected_ip = None # IP of the connected player + self.connected_port = None # Port of the connected player + # NOTE Network communication variables + self.socket = None + self.ip = get('https://api.ipify.org').content.decode('utf8') + self.port = 9999 + # NOTE Load the default values for new players and + # ensure that the folders are created + self.default_character = None + self.loaded_characters = [] # List of loaded characters + self.default_weapon = None + self.loaded_weapons = [] # List of loaded weapons + self.initialize() + # NOTE Loads players if any or creates a new dictionary + if os.path.exists("players.pickle"): + with open("players.pickle", "rb") as f: + self.players = pickle.load(f) + else: + self.players = {} + # NOTE Current state + self.current_player = None + self.loaded_character = None + # NOTE Match dictionary + # { + # "match_id" { + # "players": [player1, player2], + # "characters": [character1, character2], + # "turn": 0, # 0 = player1, 1 = player2 + # "round": 0, + # "winner": None + # "starting_time": starting_time, + # "ending_time": ending_time, + # "scores": [score1, score2] + # } + # } + self.matches = {} + + # ANCHOR First time initalization / loading of defaults + # NOTE This method is called at every restart and creates or loads + # the default starting values + def initialize(self): + # NOE IP and Port initialization + self.ip = "127.0.0.1" + self.port = 9999 + # NOTE Folders creation + if not (os.path.exists("data")): + os.mkdir("data") + if not (os.path.exists("data/weapons")): + os.mkdir("data/weapons") + if not (os.path.exists("data/characters")): + os.mkdir("data/characters") + if not (os.path.exists("data/players")): + os.mkdir("data/players") + if not (os.path.exists("data/characters/default")): + os.mkdir("data/characters/default") + # NOTE Database of objects loading + if not (os.path.exists("data/weapons/db.cfo")): + with open("data/weapons/db.cfo", "wb") as f: + pickle.dump([basic_weapon], f) + else: + with open("data/weapons/db.cfo", "rb") as f: + self.loaded_weapons = pickle.load(f) + if not (os.path.exists("data/characters/db.cfo")): + with open("data/characters/db.cfo", "wb") as f: + pickle.dump([basic_character], f) + else: + with open("data/characters/db.cfo", "rb") as f: + self.loaded_characters = pickle.load(f) + # NOTE Default weapon creation + if not (os.path.exists("data/characters/default/weapon")): + basic_weapon = cfo.WeaponCode() + basic_weapon.name = "fist" + basic_weapon.invocation = "punch(TARGET)" + basic_weapon.is_offensive = True + basic_weapon.is_tool = False + basic_weapon.resistance = -1 + basic_weapon.damage = 1 + basic_weapon.accuracy = 50 + basic_weapon.is_ranged = False + basic_weapon.rounds_max = -1 + basic_weapon.speed = 1 + with open("data/characters/default/weapon", "wb") as f: + pickle.dump(basic_weapon, f) + else: + with open("data/characters/default/weapon", "rb") as f: + basic_weapon = pickle.load(f) + # NOTE Default character creation or loading + if not (os.path.exists("data/characters/default/character.cfo")): + basic_character = cfo.Character() + basic_character.name = "Eddie" + basic_character.weapons = [basic_weapon] + basic_character.equipment["weapon_dx"] = basic_weapon + with open("data/characters/default/character.cfo", "wb") as f: + pickle.dump(basic_character, f) + else: + with open("data/characters/default/character.cfo", "rb") as f: + basic_character = pickle.load(f) + + # NOTE Setting the default character and the default weapon + self.default_character = basic_character + self.default_weapon = basic_weapon + + # ANCHOR Player creation + # NOTE A new player has the default character and the default weapon + def new_player(self, username): + player = cfo.Player() + player.username = username + player.score = 0 + player.characters = [self.default_character] + player.is_banned = False + self.players[username] = player + + # SECTION Utils + def execute_function(self, method_name, *args): + method = getattr(self, method_name) + return method(*args) + # !SECTION Utils + + # SECTION Fights + def init_fight(self, match_id): + # NOTE Environment + self.fight_id = match_id + # NOTE Match dictionary so we have both + # players, both characters and their properties + # NOTE Actions dictionary + self.actions = { + "punch": ["take_damage", 2], # weapon, damage + "shield": ["increase_protection", 2], # tool, quantity + "shoot": ["take_damage", 2], # weapon, damage + "reload": ["reload_weapon", 1], # weapon + "heal": ["increase_health", 2], # tool, quantity + } + + # SECTION Base events + + def take_damage(self, damage): + # NOTE Calculate protection and damage + protection = self.current_player.characters[self.loaded_character].protection + real_damage = damage - (protection/10) + self.current_player.characters[self.loaded_character].hp -= real_damage + + def cure(self, cure): + self.current_player.characters[self.loaded_character].hp += cure + + def increase_protection(self, quantity): + self.current_player.characters[self.loaded_character].protection += quantity + + def reload_weapon(self, weapon): + max_load = self.current_player.characters[self.loaded_character].weapons[weapon].rounds_max + # NOTE If the weapon is infinite, we don't reload + if max_load == -1: + return True + # NOTE If the weapon is not infinite, we reload + self.current_player.characters[self.loaded_character].weapons[weapon].rounds = max_load + + def increase_health(self, quantity): + current_health = self.current_player.characters[self.loaded_character].hp + new_health = current_health + quantity + max_health = self.current_player.characters[self.loaded_character].max_hp + # NOTE If the new health is higher than the max health, we set it to the max health + if new_health > max_health: + new_health = max_health + self.current_player.characters[self.loaded_character].hp = new_health + + # !SECTION Base events + + # TODO Code sender and receiver + def send_action(self, action): + pass + + def reply_action(self, action): + pass + + # NOTE The order of the actions is important + # check the below example, receive_action takes + # its parameter from listen_to_socket in main cycle + def receive_action(self, action): + # NOTE Dividing action + # REVIEW Ensure that the action is valid + is_valid = ( + action.endswith(")") and + "(" in action and not + action.startswith("(") + ) + if not is_valid: + return False + # Actions are composed of a word and two round brackets + action = action.split("(")[0] + try: + arguments = action.split("(")[1].split(")")[0].split(",") + if len(arguments) == 0: + raise Exception + except: + arguments = [] + # Normalizing arguments + counter = 0 + for argument in arguments: + argument = argument.strip() + arguments[counter] = argument + # REVIEW How actions work + # Actions are evalued as: + # dictionary.get(action) + # has [0] = callback, [...] + # callback will be then called with arguments + # using call-string as a function + # TODO To ensure integrity all the items are checked + # against our database to get the correct item properties + if not action in self.actions: + return False + result = self.execute_function(self.actions[action][0], *arguments) + self.send_to_socket(result) + + # NOTE ip and port are specified in the messages sent + def send_to_socket(self, data): + if not self.connected_ip or not self.connected_port: + return False, "No IP or port specified for the connected player" + data = "[" + self.ip + ":" + self.port + "]> " + data + sendsocket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + sendsocket.connect((self.connected_ip, self.connected_port)) + sendsocket.send(data.encode()) + + # NOTE This method is called on streamline main file and manages + # the whole listen, action and repeat for the duration of + # the fight + def listen_to_socket(self): + while True: + try: + msg = self.socket.recv(4096) + except socket.error as e: + err = e.args[0] + if err == errno.EAGAIN or err == errno.EWOULDBLOCK: + time.sleep(1) + # Returns with no data + return False, "" + else: + # a "real" error occurred + print(e) + sys.exit(1) + else: + decoded = msg.decode() + # REVIEW Ensure we can receive one connection per time (same ip) + # NOTE You can do this by locking the IP and refusing anything + # that is not from that IP + # + # NOTE Returning this method means the fight is over + if not self.busy: + self.connected_ip = decoded.split(">")[0].split("[")[ + 1].split(":")[0] + self.connected_port = decoded.split( + ">")[0].split("[")[1].split(":")[1] + self.busy = True + else: + if not self.connected_ip in decoded: + # Ignore the message if it's not from the connected player + continue + # NOTE Decoding the message + msg = decoded.split("> ")[1] + # Basic actions + # REVIEW Implement a method to disconnect clean and not clean to free the slot + if msg == "disconnect": + self.busy = False + self.connected_ip = "" + self.connected_port = "" + # FIXME the second parameter is to be real + return True, 0, "Disconnected" + # Execute complex actions + self.receive_action(msg) + + def start_socket(self): + # Non blocking socket + self.socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + self.socket.connect(self.ip, self.port) + fcntl.fcntl(self.socket, fcntl.F_SETFL, os.O_NONBLOCK) + self.listen_to_socket() + + # Example to run and listen in a main cycle + # + # sock = self.start_socket() + # while True: + # is_data, data = self.listen_to_socket(sock) + # if is_data: + # self.receive_action(data) + # else: + # continue + # + # # do game stuff + + # SECTION Utils + def execute_function(self, method_name, *args): + method = getattr(self, method_name) + return method(*args) + # !SECTION Utils + + # !SECTION Fights \ No newline at end of file diff --git a/engine/players.pickle b/engine/players.pickle new file mode 100644 index 0000000..eb6309a Binary files /dev/null and b/engine/players.pickle differ diff --git a/engine/streamline.py b/engine/streamline.py new file mode 100644 index 0000000..c72cfb4 --- /dev/null +++ b/engine/streamline.py @@ -0,0 +1,143 @@ +import os +import pickle +import gamelib + +# SECTION Debug methods + +# SECTION Players selection menu +def player_menu(players): + valid = False + while not valid: + print("\n") + counter = 0 + chosable = [] + for player in players: + counter += 1 + print(str(counter) + ": " + players.get(player).username) + chosable.append(players.get(player)) + print("\n") + choice = input("Please select a player: ") + # NOTE Sanity check + if choice.isdigit(): + choice = int(choice) + if choice <= len(players): + current = chosable[choice - 1] + valid = True + else: + print("Invalid choice!") + else: + print("Invalid choice!") + print("\nYou are playing as " + current.username) + return current +# !SECTION Players selection menu + +# SECTION Character display list +def display_characters(current_player, to_choose=False): + print("\nAvailable characters:") + counter = 0 + for character in current_player.characters: + counter += 1 + print(str(counter) + ": " + character.name) + print("\n===============================================\n") + if to_choose: + valid = False + while not valid: + choice = input("Select a character: ") + if choice.isdigit(): + if choice <= len(current_player.characters): + choice = int(choice) + valid = True + else: + print("Invalid choice!") + else: + print("Invalid choice!") + return current_player.characters[choice - 1] +# !SECTION Character display list + +# !SECTION Debug methods + +# SECTION Helper methods +def search_fight(): + # TODO network game search + pass + + +def load_fight(match_id): + fight = game.init_fight(match_id) + # TODO +# !SECTION Helper methods + +# SECTION Main routine + +# ANCHOR Global variables +game = None + +if __name__ == '__main__': + game = gamelib.cfo.Game() + # NOTE Creating a new player or loading an existing one + if len(game.players) == 0: + username = input("Chose an username: ") + game.new_player(username) + # NOTE Saving the players + with open("players.pickle", "wb") as f: + pickle.dump(game.players, f) + + # ANCHOR Debug CLI game instance + # NOTE This is a debug CLI game instance + # NOTE Presenting the user with the main menu + print("Welcome to the CLI game instance") + print("|===============================================|") + print("|=============== Code Of Honor =================|") + print("|=============== Streamline =================|") + print("|===============================================|") + game.current_player = player_menu(game.players) + display_characters(game.current_player) + + # SECTION Choice menu + print("1) Load a character") + print("2) Find a fight") + print("3) Logout") + print("4) Exit") + valid = False + # NOTE Choice loop + while not exiting: + exiting = True + choice = input("-\nChoose what to do: ") + if (choice == "1"): + print("Loading a character") + game.loaded_character = display_characters( + game.current_player, True) + exiting = False + elif (choice == "2"): + print("Finding a fight") + # NOTE Start the server and look for avail + # servers abroad + # TODO match_id determination + fight = game.init_fight(0) + game.start_socket() + # NOTE Here starts the whole while loop managed by + # listen_to_socket() + success, end_result, message = game.listen_to_socket() + if success: + if end_result: + print("You won the fight!") + else: + print("You lost the fight!") + else: + print("ERROR: " + message) + exiting = False + elif (choice == "3"): + print("Logging out") + game.current_player = None + player_menu(game.players) + # Returning to the menu + exiting = False + elif (choice == "4"): + print("Exiting") + os._exit(0) + else: + print("Invalid choice!") + exiting = False + # !SECTION Choice menu + +# !SECTION Main routine \ No newline at end of file