mirror of
https://github.com/tcsenpai/CodeForHonor-Streamline.git
synced 2025-06-02 16:10:03 +00:00
Initial commit
This commit is contained in:
commit
16ad2902e6
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
|
||||
.DS_Store
|
11
README.md
Normal file
11
README.md
Normal 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
7
engine/.vscode/settings.json
vendored
Normal file
@ -0,0 +1,7 @@
|
||||
{
|
||||
"editor.quickSuggestions": {
|
||||
"other": "on",
|
||||
"comments": true,
|
||||
"strings": "off"
|
||||
}
|
||||
}
|
BIN
engine/CFO.png
Normal file
BIN
engine/CFO.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.4 MiB |
BIN
engine/CFO_midsize.png
Normal file
BIN
engine/CFO_midsize.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.6 MiB |
BIN
engine/__pycache__/cfo.cpython-39.pyc
Normal file
BIN
engine/__pycache__/cfo.cpython-39.pyc
Normal file
Binary file not shown.
61
engine/cfo.py
Normal file
61
engine/cfo.py
Normal 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
|
||||
|
BIN
engine/data/characters/default/character.cfo
Normal file
BIN
engine/data/characters/default/character.cfo
Normal file
Binary file not shown.
BIN
engine/data/characters/default/weapon
Normal file
BIN
engine/data/characters/default/weapon
Normal file
Binary file not shown.
314
engine/gamelib.py
Normal file
314
engine/gamelib.py
Normal 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
BIN
engine/players.pickle
Normal file
Binary file not shown.
143
engine/streamline.py
Normal file
143
engine/streamline.py
Normal 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
|
Loading…
x
Reference in New Issue
Block a user