Initial commit

This commit is contained in:
thecookingsenpai 2023-12-25 13:26:15 +01:00
commit 16ad2902e6
12 changed files with 538 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
.DS_Store

11
README.md Normal file
View File

@ -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

7
engine/.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,7 @@
{
"editor.quickSuggestions": {
"other": "on",
"comments": true,
"strings": "off"
}
}

BIN
engine/CFO.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 MiB

BIN
engine/CFO_midsize.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

61
engine/cfo.py Normal file
View File

@ -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

Binary file not shown.

Binary file not shown.

314
engine/gamelib.py Normal file
View File

@ -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

BIN
engine/players.pickle Normal file

Binary file not shown.

143
engine/streamline.py Normal file
View File

@ -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