The Gaming Hideout
The Gaming Hideout

Reputation: 584

Is it better to have multiple files for scripts or just one large script file with every function?

I started a project about a year ago involving a simple terminal-based RPG with Python 3. Without really thinking about it I just jumped into it. I started with organizing multiple scripts for each.. well, function. But halfway into the project, for the end goal I'm not sure if it's easier/more efficient to just have one very large script file or multiple files.

Since I'm using the cmd module for the terminal, I'm realizing getting the actual app running to be a looping game might be challenging with all these external files, but at the same time I have a __init__.py file to combine all the functions for the main run script. Here's the file structure.

File Structure

To clarify I'm not the greatest programmer, and I'm a novice in Python. I'm not sure of the compatibility issues yet with the cmd module.

So my question is this; Should I keep this structure and it should work as intended? Or should I combine all those assets scripts into one file? Or even put them apart of the start.py that uses cmd? Here's the start function, plus some snippets of various scripts.

start.py

from assets import *
from cmd import Cmd
import pickle
from test import TestFunction
import time
import sys
import os.path
import base64

class Grimdawn(Cmd):

    def do_start(self, args):
        """Start a new game with a brand new hero."""
        #fill
    def do_test(self, args):
        """Run a test script. Requires dev password."""
        password = str(base64.b64decode("N0tRMjAxIEJSRU5ORU1BTg=="))
        if len(args) == 0:
            print("Please enter the password for accessing the test script.")
        elif args == password:
            test_args = input('> Enter test command.\n> ')
            try:
                TestFunction(test_args.upper())
            except IndexError:
                print('Enter a command.')
        else:
            print("Incorrect password.")
    def do_quit(self, args):
        """Quits the program."""
        print("Quitting.")
        raise SystemExit


if __name__ == '__main__':

    prompt = Grimdawn()
    prompt.prompt = '> '
    #ADD VERSION SCRIPT TO PULL VERSION FROM FOR PRINT
    prompt.cmdloop('Joshua B - Grimdawn v0.0.3 |')

test.py

from assets import *
def TestFunction(args):
    player1 = BaseCharacter()
    player2 = BerserkerCharacter('Jon', 'Snow')
    player3 = WarriorCharacter('John', 'Smith')
    player4 = ArcherCharacter('Alexandra', 'Bobampkins')
    shop = BaseShop()
    item = BaseItem()
    #//fix this to look neater, maybe import switch case function
    if args == "BASE_OFFENSE":
        print('Base Character: Offensive\n-------------------------\n{}'.format(player1.show_player_stats("offensive")))
        return
    elif args == "BASE_DEFENSE":
        print('Base Character: Defensive\n-------------------------\n{}'.format(player1.show_player_stats("defensive")))
        return

 *   *   *

player.py

#import functions used by script
#random is a math function used for creating random integers
import random
#pickle is for saving/loading/writing/reading files
import pickle
#sys is for system-related functions, such as quitting the program
import sys
#create a class called BaseCharacter, aka an Object()
class BaseCharacter:
    #define what to do when the object is created, or when you call player = BaseCharacter()
    def __init__(self):
        #generate all the stats. these are the default stats, not necessarily used by the final class when player starts to play.
        #round(random.randint(25,215) * 2.5) creates a random number between 25 and 215, multiplies it by 2.5, then roudns it to the nearest whole number
        self.gold = round(random.randint(25, 215) * 2.5)
        self.currentHealth = 100
        self.maxHealth = 100
        self.stamina = 10
        self.resil = 2
        self.armor = 20
        self.strength = 15
        self.agility = 10
        self.criticalChance = 25
        self.spellPower = 15
        self.intellect = 5
        self.speed = 5
        self.first_name = 'New'
        self.last_name = 'Player'
        self.desc = "Base Description"
        self.class_ = None
        self.equipment = [None] * 6
    #define the function to update stats when the class is set
    def updateStats(self, attrs, factors):
        #try to do a function
        try:
            #iterate, or go through data
            for attr, fac in zip(attrs, factors):
                val = getattr(self, attr)
                setattr(self, attr, val * fac)
        #except an error with a value given or not existing values
        except:
            raise("Error updating stats.")
    #print out the stats when called
    #adding the category line in between the ( ) makes it require a parameter when called
    def show_player_stats(self, category):
 *   *   *

note

The purpose of the scripts is to show what kind of structure they have so it helps support the question of whether I should combine or not

Upvotes: 3

Views: 14399

Answers (4)

bruno desthuilliers
bruno desthuilliers

Reputation: 77942

First a bit of terminology:

  • a "script" is python (.py) file that is intended to be directly executed (python myscript.py)
  • a "module" is a python file (usually containing mostly functions and classes definitions) that is intended to be imported by a script or another module.
  • a "package" is a directory eventually containing modules and (mandatory in py2, not in py3) an __init__.py file.

You can check the tutorial for more on modules and packages.

Basically, what you want is to organize your code in coherent units (packages / modules / scripts).

For a complete application, you will typically have a "main" module (doesn't have to be named "main.py" - actually it's often named as the application itself) that will only import some definitions (from the stdlib, from 3rd part libs and from your own modules), setup things and run the application's entry point. In your example that would be the "start.py" script.

For the remaining code, what you want is that each module has strong cohesion (functions and classes defined in it are closely related and concur to implement the same feature) and low coupling (each module is as independant as possible of other modules). You can technically put as much functions and classes as you want in a single module, but a too huge module can become a pain to maintain, so if after a first reorganisation based on high cohesion / low coupling you find yourself with a 5000+klocs module you'll probably want to turn it into a package with more specialized submodules.

If you still have a couple utilitie functions that clearly don't fit in any of your modules, the usual solution is to put them together in an "utils.py" (or "misc.py" or "helpers.py" etc) module.

Two things that you absolutely want to avoid are:

  1. circular dependencies, either direct (module A depends on module B, module B depends on module A) or indirect (module A depends on module B which depends on module A). If you find you have such case, it means you should either merge two modules together or extract some definitions to a third module.

  2. wildcard import ("from module import *"), which are a major PITA wrt/ maintainability (you can't tell from the import where some names are imported from) and make the code subject to unexpected - and sometimes not obvious - breakage

As you can see, this is still quite very a generic guideline, but deciding what belongs together can not be automated and in the end depends on your own judgement.

Upvotes: 10

Luke Brady
Luke Brady

Reputation: 185

There are several ways to approach organizing your code and it ultimately comes down to:

  1. Personal Preference
  2. Team coding standards for your Project
  3. Naming / Structure / Architecture Conventions for your Company

They way I organize my Python code is by creating several directories:

Folder Structure for Code Organization

  • class_files (Reusable code)
  • input_files (Files read by scripts)
  • output_files (Files written by scripts)
  • scripts (Code executed)

This has served me pretty well. Import your paths relatively so the code can be run from any place it is cloned. Here's how I handle the imports in my script files:

import sys
# OS Compatibility for importing Class Files
if(sys.platform.lower().startswith('linux')):
  sys.path.insert(0,'../class_files/')
elif(sys.platform.lower().startswith('win')):
  sys.path.insert(0,'..\\class_files\\')

from some_class_file import my_reusable_method

This approach also makes it possible to make your code run in various version of Python and your code can detect and import as necessary.

if(sys.version.find('3.4') == 0):
  if(sys.platform.lower().startswith('linux') or sys.platform.lower().startswith('mac')):
            sys.path.insert(0,'../modules/Python34/')
            sys.path.insert(0,'../modules/Python34/certifi/')
            sys.path.insert(0,'../modules/Python34/chardet/')
            sys.path.insert(0,'../modules/Python34/idna/')
            sys.path.insert(0,'../modules/Python34/requests/')
            sys.path.insert(0,'../modules/Python34/urllib3/')
    elif(sys.platform.lower().startswith('win')):
            sys.path.insert(0,'..\\modules\\Python34\\')
            sys.path.insert(0,'..\\modules\\Python34\\certifi\\')
            sys.path.insert(0,'..\\modules\\Python34\\chardet\\')
            sys.path.insert(0,'..\\modules\\Python34\\idna\\')
            sys.path.insert(0,'..\\modules\\Python34\\requests\\')
            sys.path.insert(0,'..\\modules\\Python34\\urllib3\\')
    else:
            print('OS ' + sys.platform + ' is not supported')
elif(sys.version.find('2.6') == 0):
    if(sys.platform.lower().startswith('linux') or sys.platform.lower().startswith('mac')):
            sys.path.insert(0,'../modules/Python26/')
            sys.path.insert(0,'../modules/Python26/certifi/')
            sys.path.insert(0,'../modules/Python26/chardet/')
            sys.path.insert(0,'../modules/Python26/idna/')
            sys.path.insert(0,'../modules/Python26/requests/')
            sys.path.insert(0,'../modules/Python26/urllib3/')
    elif(sys.platform.lower().startswith('win')):
            sys.path.insert(0,'..\\modules\\Python26\\')
            sys.path.insert(0,'..\\modules\\Python26\\certifi\\')
            sys.path.insert(0,'..\\modules\\Python26\\chardet\\')
            sys.path.insert(0,'..\\modules\\Python26\\idna\\')
            sys.path.insert(0,'..\\modules\\Python26\\requests\\')
            sys.path.insert(0,'..\\modules\\Python26\\urllib3\\')
    else:
            print('OS ' + sys.platform + ' is not supported')
else:
    print("Your OS and Python Version combination is not yet supported")

Upvotes: -1

Peter
Peter

Reputation: 3495

The way you have it currently is fine, personally I much prefer a lot of files as it's a lot easier to maintain. The main issue I see is that all of your code is going under assets, so either you'll end up with everything dumped there (defeating the point of calling it that), or you'll eventually end up with a bit of a mess of folders once you start coding other bits such as the world/levels and so on.

A quite common way of designing projects is your root would be Grimdawn, which contians one file to call your code, then all your actual code goes in Grimdawn/grimdawn. I would personally forget the assets folder and instead put everything at the root of that folder, and only go deeper if some of the files get more complex or could be grouped.

I would suggest something like this (put in a couple of additions as an example):

Grimdawn/characters/Jon_Snow
Grimdawn/characters/New_Player
Grimdawn/start.py
Grimdawn/grimdawn/utils/(files containing generic functions that are not game specific)
Grimdawn/grimdawn/classes.py
Grimdawn/grimdawn/combat.py
Grimdawn/grimdawn/items.py
Grimdawn/grimdawn/mobs/generic.py
Grimdawn/grimdawn/mobs/bosses.py
Grimdawn/grimdawn/player.py
Grimdawn/grimdawn/quests/quest1.py
Grimdawn/grimdawn/quests/quest2.py
Grimdawn/grimdawn/shops.py

Upvotes: 4

Xander YzWich
Xander YzWich

Reputation: 141

The pythonic approach to what goes into a single file (I'll discuss as it applies largely to classes) is that a single file is a module (not a package as I said previously).

A number of tools will typically exist in a single package, but all of the tools in a single module should remain centered around a single theme. With that said, a very small project I will typically keep in a single file with several functions and maybe a few classes inside. I would then use if main to contain the script as I want it run in its entirety.

if __name__== '__main__': 

I would break logic down into functions as much as makes sense so that the main body of the script is readable as higher level logic.

Short answer: A file for every function is not manageable on any scale. You should put things together into files (modules) with related functionality. It is up to you as to whether the current functions should be clustered together into modules or not.

Upvotes: 1

Related Questions