pyGameAIFoundation 0.1.1
pyGameAIFoundation is a python package that provides classes for implementing AI in games
Requires Python: >=3.12
Keywords: framework, turn based, game, goal-oriented, action, blackboard architecture

# pyGameAIFoundation

Source code: [GitHub](https://github.com/KevinRGeurts/pyGameAIFoundation)
---
pyGameAIFoundation is a python package that provides classes for implementing decision making AI in games. It
includes classes for goal-oriented behavior, action execution, and blackboard architecture. A develper can use
these foundation classes to implement game goals and actions specific to their game, and to utilize
those objects to create goal-based behavior in a game.

## Credit where credit is due

- The package generally implements pseudo code from the book "Artificial Intelligence for Games",
  Second Edtion, by Ian Millington and John Funge, Chapter 5, 2009.

## Gob class

The Gob, which is an implementation of Goal-Oriented Behavior. Specifically, it implements
the Simple Selection mechanisim for choosing a game action from a set of goals and actions. It 
implements pseudo code from Section 5.7.2 of "Artificial Intelligence for Games".

## GameGoal class

GameGoal is an abstract base class. Concrete implementations of this class represent goals that
can be achieved in a game. It implements pseudo code from Section 5.7.2-3 of "Artificial Intelligence for Games".

A concrete implementation must implement the method ```getInsistence()```, which is called to get the
insistence (urgency or importance) value of the goal. The higher the insistence value, the more urgent or important the goal is.

## ActionManager class

The ActionManager class manages the scheduling and execution of actions in a game.
It implements pseudo code from Section 5.11.3 of "Artificial Intelligence for Games".

## GameAction class

GameAction is an abstract base class, GameAction. Concrete implementations of this class represent actions that
can be performed in a game. It generally implements pseudo code from Section 5.11.4 of "Artificial Intelligence for Games",
The possibility of actions being able to read and write data to a blackboard is additionally included, following the blackboard
architecture pattern described in Section 5.9 of the same book.

Concrete implementation child classes of GameAction must:
- Implement the method ```execute()```, which is called to enact the specific behavior of the action.
- Override the method ```isComplete()```, which is called to determine if the action is completed.

Concrete implementation child classes of GameAction also:
- May override the method ```canInterrupt()```, which is called to determine if this action can interrupt
  other actions, e.g., in the active collection of an ActionManager object.
- May override the method ```canDoBoth(...)```, which is called to determine of this action can be executed
  at the same time as another action, e.g., in the active collection of an ActionManager object.
- Should override the method ```getGoalChange(...)```, which is called to determine how this action affects
  the insistence of a specific goal, e.g., in the goal collection maintained by a Gob object. The method
  should return a negative integer, indicating a reduction in the insistence of the goal when the action is executed.

### GameActionCombination class (child of GameAction)

The GameActonCombination class is a child of GameAction, and it represents a combination of game actions
that can be performed together (in parallel, at least in the sense of game logic).

### GameActionSequence class (child of GameAction)

The GameActionSequence class is a child of GameAction, and it represents a sequence of game actions that must be performed
in order in the sense of game logic.

## Usage

```python
# Local imports
from pyGameAIFoundation.gob import Gob
from pyGameAIFoundation.game_goal import GameGoal, GoalInsistence
from pyGameAIFoundation.action_manager import ActionManager
from pyGameAIFoundation.game_action import GameAction, GameActionCombination, GameActionSequence


# Create a couple of concrete GameGoal subclasses with different insistence values.

class GoalOne(GameGoal):
    def getInsistence(self):
        """
        Return the insistence value of the goal.
        :return: The insistence value of the goal, int
        """
        return GoalInsistence.LOW


class GoalTwo(GameGoal):
    def getInsistence(self):
        """
        Return the insistence value of the goal.
        :return: The insistence value of the goal, int
        """
        return GoalInsistence.MEDIUM


# Create a pair of GameAction subclasses with different implementations of getGoalChange(...)

class ActionOne(GameAction):
    def __init__(self, expiry_time=0, priority=0, read_blackboard=None, write_blackboard=None):
        """
        :parameter expiry_time: The time in an arbitrary count-up from zero until the action expires, as int
        :parameter priority: The priority of the action. Higher numbers indicate higher priority. As int
        :parameter read_blackboard: A function to read from the blackboard, as callable
            Signature: read_blackboard(key: str) -> any
        :parameter write_blackboard: A function to write to the blackboard, as callable
            Signature: write_blackboard(key: str, value: any) -> None
        """
        super().__init__(expiry_time, priority, read_blackboard, write_blackboard)
        self._canInterrupt = False
        self._canDoBoth = False
        self._completed=False

    def canInterrupt(self):
        """
        Return whether this action can interrupt any other action.
        :return: True if the action can interrupt any other action, False otherwise, as boolean.
        """
        return self._canInterrupt

    def canDoBoth(self, other_action):
        """
        Return whether this action can be done at the same time as another action.
        :param other_action: The other action to check against, as GameAction object.
        :return: True if both actions can be done at the same time, False otherwise, as boolean.
        """
        assert(isinstance(other_action, GameAction))
        return self._canDoBoth

    def isComplete(self):
        """
        Return whether this action is complete.
        :return: True if the action is complete, False otherwise, as boolean.
        """
        return self._completed

    def execute(self):
        """
        Execute the action. In this case, by writing to the blackboard (if we have one) and
        setting the completed flag to True.
        :return: None
        """
        print("Executing ActionOne")
        if self._write_blackboard:
            self._write_blackboard('ActionOne Output', 15)
            print("Wrote to blackboard: ActionOne Output = 15")
        self._completed = True
        return None

    def getGoalChange(self, goal=None):
        """
        Return the goal insistence change associated with this action.
        :param goal: The goal to check against, as GameGoal object.
        :return: The goal insistence change associated with this action, as int.
        """
        assert(isinstance(goal, GameGoal))
        if isinstance(goal, GoalOne):
            return -GoalInsistence.MEDIUM
        else:
            return -GoalInsistence.LOW


class ActionTwo(GameAction):
    def __init__(self, expiry_time=0, priority=0, read_blackboard=None, write_blackboard=None):
        """
        :parameter expiry_time: The time in an arbitrary count-up from zero until the action expires, as int
        :parameter priority: The priority of the action. Higher numbers indicate higher priority. As int
        :parameter read_blackboard: A function to read from the blackboard, as callable
            Signature: read_blackboard(key: str) -> any
        :parameter write_blackboard: A function to write to the blackboard, as callable
            Signature: write_blackboard(key: str, value: any) -> None
        """
        super().__init__(expiry_time, priority, read_blackboard, write_blackboard)
        self._canInterrupt = False
        self._canDoBoth = False
        self._completed=False

    def canInterrupt(self):
        """
        Return whether this action can interrupt any other action.
        :return: True if the action can interrupt any other action, False otherwise, as boolean.
        """
        return self._canInterrupt

    def canDoBoth(self, other_action):
        """
        Return whether this action can be done at the same time as another action.
        :param other_action: The other action to check against, as GameAction object.
        :return: True if both actions can be done at the same time, False otherwise, as boolean.
        """
        assert(isinstance(other_action, GameAction))
        return self._canDoBoth

    def isComplete(self):
        """
        Return whether this action is complete.
        :return: True if the action is complete, False otherwise, as boolean.
        """
        return self._completed

    def execute(self):
        """
        Execute the action. In this case, by reading from the blackboard (if we have one) and
        setting the completed flag to True.
        :return: None
        """
        print("Executing ActionTwo")
        if self._read_blackboard:
            value = self._read_blackboard('ActionOne Output')
            print(f"Read from blackboard: ActionOne Output = {value}")
        self._completed = True
        return None

    def getGoalChange(self, goal=None):
        """
        Return the goal insistence change associated with this action.
        :param goal: The goal to check against, as GameGoal object.
        :return: The goal insistence change associated with this action, as int.
        """
        assert(isinstance(goal, GameGoal))
        if isinstance(goal, GoalTwo):
            return -GoalInsistence.MEDIUM
        else:
            return -GoalInsistence.LOW

        
# Create the Gob (goal-oriented behavior) object. 
gob = Gob()
# Add goals to the Gob.
gob.add_goal(GoalOne('goal_1'))
gob.add_goal(GoalTwo('goal_2'))
# Create actions and add them to the Gob.
act1 = ActionOne(expiry_time=10)
act2 = ActionTwo(expiry_time=10)
gob.add_action(act1)
gob.add_action(act2)
    
# Create the ActionManager.
mgr = ActionManager()

# Choose the best action to execute.
(bestAct, topGoal) = gob.chooseAction()
bestAct.expiry_time = mgr.currentTime + bestAct.expiry_time  # Set the expiry time for the action.
print(f'Chose action with priority {bestAct.priority} to fulfill goal "{topGoal.name}" with insistence {topGoal.getInsistence()}')

# Schedule the best action in the ActionManager.
mgr.scheduleAction(bestAct)

# Output the action manager state.
print(f'Action Manager State before execution:\n{mgr}')

# Execute the action manager, which will activate the scheduled action and execute it
mgr.execute()
# Output the action manager state.
print(f'Action Manager State after first execution:\n{mgr}')
# Execute the action manager again, which will remove the completed action from the active list.
mgr.execute()
# Output the action manager state.
print(f'Action Manager State after second execution:\n{mgr}')

# Create a GameActionCombination and demonstrate its use in the Gob and ActionManager.
combo = GameActionCombination(expiry_time=10, combo_acts=[act1, act2])
gob.add_action(combo)
(bestAct, topGoal) = gob.chooseAction()
bestAct.expiry_time = mgr.currentTime + bestAct.expiry_time  # Set the expiry time for the action.
print(f'\nChose action with priority {bestAct.priority} to fulfill goal "{topGoal.name}" with insistence {topGoal.getInsistence()}')
mgr.scheduleAction(bestAct)
print(f'Action Manager State before execution:\n{mgr}')
# Activate the scheduled combination action, and execute both actions in the combo.
mgr.execute()
print(f'Action Manager State after first execution:\n{mgr}')
# Remove the completed combination action from the active list.
mgr.execute()
print(f'Action Manager State after second execution:\n{mgr}')
# Remove the combination action from the Gob's action list, so it doesn't interfere with the rest of the demo,
# since it's insistence change is the same as the sequence action created next.
gob.remove_action(combo)

# Create a GameActionSequence, demonstrate its use in the Gob and ActionManager, and use of the
# blackboard to pass information between actions in a sequence.
seq = GameActionSequence(expiry_time=10)
act1 = ActionOne(expiry_time=10, read_blackboard=seq.readFromBlackBoard, write_blackboard=seq.writeToBlackBoard)
act2 = ActionTwo(expiry_time=10, read_blackboard=seq.readFromBlackBoard, write_blackboard=seq.writeToBlackBoard)
seq.addAction(act1)
seq.addAction(act2)
gob.add_action(seq)
(bestAct, topGoal) = gob.chooseAction()
bestAct.expiry_time = mgr.currentTime + bestAct.expiry_time  # Set the expiry time for the action.
print(f'\nChose action with priority {bestAct.priority} to fulfill goal "{topGoal.name}" with insistence {topGoal.getInsistence()}')
mgr.scheduleAction(bestAct)
print(f'Action Manager State before execution:\n{mgr}')
# Activate the scheduled sequence action, and execute the first action in the sequence.
mgr.execute()
print(f'Action Manager State after first execution:\n{mgr}')
# Execute the second action in the sequence.
mgr.execute()
print(f'Action Manager State after second execution:\n{mgr}')
# Remove the completed sequence action from the active list.
mgr.execute()
print(f'Action Manager State after third execution:\n{mgr}')
```

## Demonstration

To run the demonstration, type ```python -m pyGameAIFoundation.main``` in a terminal window. Note, that this assumes that the
pyGameAIFoundation package has been installed in your Python environment. The demonstration is essentially the
same as the usage example above.

## Unittests

Unittests for the pyGameAIFoundation are in the tests directory, with filenames starting with test_. To run the unittests,
type ```python -m unittest discover -s ..\..\tests -v``` in a terminal window in the src\pyGameAIFoundation directory.

## License
MIT License. See the LICENSE file for details