miron / NeonCore

Terminal based Cyberpunk Tabletop RPG with Nostr as database and openAI API compatible commands
3 stars 1 forks source link

structure code into classes and modules #10

Open miron opened 1 year ago

miron commented 1 year ago
miron commented 1 year ago

That's a good summary of the different components of your game design. It sounds like you have a good understanding of how the different modules and classes interact and how to organize your code in a modular way. It's good that you are thinking about how to organize your code in a way that is easy to understand and maintain, it's good to keep in mind that for your game design, the number of modules is not as important as how well they are organized and how easy it is to understand and maintain the code.

It's also a good idea to keep in mind about separating the concerns of each module and class. For example, the Combat module should focus on combat mechanics and the Inventory module should focus on inventory management.

It's also a good idea to think about how you will handle saving and loading game data, and how you will interact with the map and player/NPC location data and in-game events. The GameManager class could handle those responsibilities, it could be a good idea to have one central class that handles all the saving and loading, updating the game world state, interacting with the map and player/NPC location data and in-game events.

You have also done a good job in considering different ways to handle the storage of the player and NPC data, such as using shelve or a database.

Overall, it looks like you have a solid plan for the design of your game, keep up the good work!

miron commented 1 year ago

You're correct that some functionality in the ActionManager class may overlap with functionality in the GameManager class, and it's worth considering if it would make more sense to move some of the functionality from the ActionManager to the GameManager. For example, the GameManager class could handle updating the game world state and interacting with the map and player/NPC location data, while the ActionManager class could handle processing player input and calling the appropriate methods in the GameManager and other classes to update the game state accordingly. This approach would help to keep the responsibilities of each class more focused and make it easier to make changes to one class without affecting the other.

miron commented 1 year ago

In your case, the ActionManager class is responsible for handling user input and executing commands, while the GameManager class is responsible for managing the game state, loading and saving data, and interacting with the map and NPCs. These are two distinct responsibilities, and it might make sense to have them as separate classes.

miron commented 1 year ago

A RangedCombatCommand class could differ from NPCEncounterCommand in that it would be specifically for handling ranged combat encounters. It would likely have different methods for handling ranged attacks and determining hit chances, damage, etc. compared to the methods in NPCEncounterCommand which are specific to close-combat encounters. You could also consider having RangedCombatCommand inherit from NPCEncounterCommand and just adds methods for ranged combat. This way it will also have the command functionality. It's also worth noting that you can also have a single class that handles both ranged and close combat encounters and methods for determining hit chances and damage based on the type of encounter.

miron commented 1 year ago

The RangedCombatCommand class could still inherit from the SkillCheckCommand class, as it is still performing a skill check. However, the calculation of the difficulty value would be different as it is based on the weapon type and distance, rather than a fixed difficulty level. So, it makes sense to separate the RangedCombatCommand class from the SkillCheckCommand class and have it handle the specific calculation of the difficulty value for ranged combat. Additionally, it could have methods to handle the different weapon types and distance ranges, so that it can properly calculate the appropriate difficulty value based on the input provided.

miron commented 1 year ago

Another option could be to have a GameFlow manager class, that will handle the flow of the game and communication between classes. It's important to keep in mind the Single Responsibility Principle, and make sure that each class has a single, well-defined responsibility, and that the flow of the game is well-organized and easy to understand.

miron commented 1 year ago

It depends on how you want to structure your code and handle the flow of the game. One option is to keep the completenames override in the ActionManager, and have it check the current game state before determining which commands to show. Another option is to move the completenames method to the PerceptionCheckCommand class, and have it only show the perception_check command when the game state is 'before_perception_check' . This would keep all the logic for handling the flow of the game and determining which commands to show in the PerceptionCheckCommand class, but would require some changes in the way you handle the flow of the game in the ActionManager class. Another option could be to have a GameFlow manager class, that will handle the flow of the game and communication between classes. It's important to keep in mind the Single Responsibility Principle, and make sure that each class has a single, well-defined responsibility, and that the flow of the game is well-organized and easy to understand.

miron commented 1 year ago

Solution without using CommandManager, maybe better and more maintanable when do_* commands are registered in the ActionManager instead of the respective classes. greet i.e. could still call another class

def precmd(self, line):
    # Add some methods based on condition
    if some_condition:
        setattr(self, 'do_bye', self.bye)
        setattr(self, 'do_greet', self.greet)
    return line

def postcmd(self, stop, line):
    # Remove some methods based on condition
    if not some_condition:
        delattr(self, 'do_bye')
        delattr(self, 'do_greet')
    return stop

def bye(self, arg):
    # Do something here
    pass

def greet(self, arg):
    # Do something here
    pass

Another Idea:

import types
import cmd

class MyCmd(cmd.Cmd):
    pass

def add_do_methods(obj):
    if some_condition:
        def do_bye(self, arg):
            # Do something here
            pass

        def do_greet(self, arg):
            # Do something here
            pass

        do_methods = {'do_bye': do_bye, 'do_greet': do_greet}

        new_class_dict = dict(obj.__class__.__dict__)
        new_class_dict.update(do_methods)
        new_class = types.new_class(obj.__class__.__name__, (obj.__class__,), {}, lambda ns: ns.update(new_class_dict))

        obj.__class__ = new_class

def remove_do_methods(obj):
    if not some_condition:
        new_class = types.new_class(obj.__class__.__name__, (obj.__class__,), {})
        new_class_dict = dict(new_class.__dict__)

        for method_name in ['do_bye', 'do_greet']:
            if method_name in new_class_dict:
                del new_class_dict[method_name]

        new_class = types.new_class(obj.__class__.__name__, (obj.__class__,), {}, lambda ns: ns.update(new_class_dict))
        obj.__class__ = new_class

# Create an instance of MyCmd
my_cmd = MyCmd()

# Add the do_* methods
add_do_methods(my_cmd)

# Call the methods
my_cmd.do_bye('arg')
my_cmd.do_greet('arg')

# Remove the do_* methods
remove_do_methods(my_cmd)

In this example, the add_do_methods() function adds the do_bye() and do_greet() methods to the class of the specified object if the some_condition is true. It does this by creating a new dictionary that includes the existing class dictionary as well as the new methods, and then creating a new class that derives from the existing class with the updated dictionary. It then sets the class of the specified object to the new class.

The remove_do_methods() function does the opposite, removing the do_bye() and do_greet() methods from the class of the specified object if the some_condition is false. It does this by creating a new dictionary that includes the existing class dictionary with the methods removed, and then creating a new class that derives from the existing class with the updated dictionary. It then sets the class of the specified object to the new class.

By using dynamic class creation and setting the class of the object to the new class, you can add and remove methods from an existing class at runtime.