mirror of
https://github.com/tcsenpai/CodeForHonor-Streamline.git
synced 2025-06-04 08:50:04 +00:00
314 lines
12 KiB
Python
314 lines
12 KiB
Python
# 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 |