Custom Actions ~ Existing Code Review - uchicago-cs/chiventure GitHub Wiki

Documentation of Existing Action Management Code

Aims

The following documentation aims to provide a thorough account of the existing structures and implementation related to action management in Chiventure. It does not attempt to provide commentary on the design, but rather to succinctly lay out the existing codebase to provide the rest of our team with a good foundation of understanding before we move forward with the design and implementation of custom actions.

Relevant Files

The current action management implementation referred to in this document can be found in the following files.

action_management

  • include/action_management
    • action_structs.h
    • actionmanagement.h
  • src/action_management/src
    • actionmanagement.c
    • get_actions.c

game-state

  • include/game-state
    • game_action.h
    • item.h
    • room.h
    • game.h

Understanding Actions

Note: All of the data structures in this section can be found in include/game-state/item.h

An action is currently constituted by the following information:

  • A hash handle for lookup
  • The name of the action (a string)
  • A list of conditions required by the action (a linked list of condition structures)
  • A list of effects of the action (a linked list of effect structures)
  • A string to return if action is successful
  • A string to return if action fails
typedef struct game_action {
    UT_hash_handle hh;
    char* action_name; //consider this the “primary key” used to search for this action
    action_condition_list_t *conditions; //type: game_action_condition
    action_effect_list_t *effects; //type: game_action_effect
    char* success_str; 
    char* fail_str;
} game_action_t;

We will attempt to further decompose this game_action structure in order to develop a complete understanding of how actions are defined and start to get a sense of how the representation supports the necessary functionality.

Items

The item data structure is also extremely important in understanding actions in chiventure. An item is constituted by the following:

  • A hash handle for lookup
  • The name of the item (a string)
  • A short and a long description of the item (strings)
  • A hashtable of actions compatible with the item (hashtable of actions)
  • A hashtable of attributes possessed by the item (hashtable of attribute structs)
typedef struct item {
    UT_hash_handle hh; 
    char *item_id;
    char *short_desc;
    char *long_desc;
    game_action_hash_t *actions; //type: action
    attribute_hash_t *attributes; //type: attribute
} item_t;

The most important things to understand about the item structure are that

  • Items “have” actions. Intuitively this is strange, but for an item to have an action, it simply means that it is valid for that action to be performed on the item
  • Items have attributes, which I will get into next

Attributes

Attributes specify details about an item and features possessed by it. They are used to define the conditions and effects of an action:

  • A condition checks whether a certain attribute is present
  • An effect is defined by the attribute it is meant to change

The value of an attribute is implemented as a union, and thus the value of an attribute can be given by any of the following primitive datatypes:

typedef union attribute_value {
    double double_val;
    char char_val;
    bool bool_val;
    char* str_val;
    int int_val;
} attribute_value_t;

The attribute struct then contains the following:

  • A hash handle for lookup
  • The name of the attribute (a string)
  • A tag indicating the datatype of the attribute value (enum)
  • The attribute value (union attribute_value)
typedef struct attribute {
    UT_hash_handle hh;
    char* attribute_key; // “primary key” to search for attribute
    enum attribute_tag attribute_tag;
    attribute_value_t attribute_value;
} attribute_t;

Conditions

A condition is simply an attribute that must be possessed by a certain item in order for an action to be valid. A condition is implemented as a linked list and is composed of the following:

  • The item to be checked (pointer to an item struct)
  • The attribute to be checked (pointer to an attribute struct)
  • The attribute value that should be met (an attribute_value)
  • The next condition to be checked (a pointer to condition struct)
typedef struct game_action_condition{
    item_t *item; //type: item
    attribute_t* attribute_to_check; //type: attribute
    attribute_value_t expected_value;
    struct game_action_condition *next;
} game_action_condition_t;

Effects

A valid action results in an effect. An effect simply changes a particular attribute value of the appropriate item. An effect is also implemented as a linked list and is composed of the following:

  • The item to be modified (pointer to an item struct)
  • The attribute to be modified (pointer to an attribute struct)
  • The new attribute value (an attribute_value)
  • The next effect to be executed (a pointer to an effect struct)
typedef struct game_action_effect{
    item_t *item; //type: item
    attribute_t* attribute_to_modify; //type: attribute
    attribute_value_t new_value;
    struct game_action_effect *next; 
} game_action_effect_t;

Summary

Combining these components, you can understand how actions are currently represented in chiventure, and we can start to imagine how they provide the necessary functionality. This is the big picture:

Actions are performed on items. Items include a list of valid actions that can be performed with/on them. For an action to be successful, certain conditions must be met. A condition is simply an attribute that is possessed by an item. If all necessary conditions are met, an action has effect(s). These effects simply modify attributes of particular items.

Note: The functions for manipulating the game_action struct can be found in include/game-state/game_action.h while those for manipulating items can be found in include/game-state/item.h

Performing Actions

While the data structures that define actions are actually found in the game state module (particularly item.h), the code for actually executing actions falls under the action management module.

action_structs.h

Action management executes different actions by classifying them into 3 types, which are represented as an enum:

  • Type 1 (action ): actions that are performed directly on a single item
  • Type 2 (action ): actions involving a path as opposed to an item
  • Type 3 (action ): actions that use an item to act upon another item
// Each enum corresponds to a different "KIND" of action
enum action_kind {
    ITEM = 1, // ACTION <item> i.e. Action Type 1
    PATH = 2, // ACTION <path i.e. Action Type 2
    ITEM_ITEM = 3 // ACTION <item> <item> i.e. Action Type 3
};

This enumeration is then included in an action_type struct, which includes:

  • The name of the action (a string)
  • The type of action (enum action_kind)
typedef struct {
    char *c_name; // e.g. "eat"
    enum action_kind kind; // e.g. KIND_1
} action_type_t;

The current actions supported by chiventure with their corresponding kind are outlined in the following static array of action types from src/action_management/src/get_actions.c:

static action_type_t valid_actions[] =
{
    // KIND 1
    {"OPEN", ITEM},
    {"CLOSE", ITEM},
    {"PUSH", ITEM},
    {"PULL", ITEM},
    {"TURNON", ITEM},
    {"TURNOFF", ITEM},
    {"TAKE", ITEM},
    {"DROP", ITEM},
    {"CONSUME", ITEM},
    // KIND 2
    {"GO", PATH},
    // KIND 3
    {"USE", ITEM_ITEM},
    {"PUT", ITEM_ITEM}
};

actionmanagement.h

The main takeaway from the action management functions is that the procedure of executing an action is split up by the type of action, yielding the following three functions:

  • do_item_action
  • do_path_action
  • do_item_item_action

These functions return 0 if the action was successful, and if not, they return the reason the action could not be executed using the encoding shown below (from actionmanagement.c). If the action is successful, the success string from the action struct is returned as an outparameter, and if the action fails, the failure string is returned as an outparameter.

#define WRONG_KIND (2)
#define NOT_ALLOWED_DIRECT (3)
#define NOT_ALLOWED_INDIRECT (4)
#define NOT_ALLOWED_PATH (5)
#define CONDITIONS_NOT_MET (6)
#define EFFECT_NOT_APPLIED (7)

Executing Actions of Type 1 and Type 3

The procedure of executing actions of these types can be pretty easily deduced from the current structure of actions outlined in the previous section. The functions for executing these types of actions take in the following parameters:

  • A pointer to the appropriate action type structure
  • A pointer to the item(s) to be acted upon
  • An outparameter string, indicating whether the action was successful

The functions include the following steps:

  • Check if action is of the appropriate kind
  • Check if the action can be performed on the given item(s)
  • Check if the conditions of the action have been met
  • Apply the appropriate effects

These functions make pretty heavy use of validation helper functions to handle the steps given above. These helper functions can be found in include/game-state/game_action.h:

  • possible_action: can an action be performed on an item?
  • get_action: retrieves game_action struct from hashtable using the name of the action
  • all_conditions_met: checks that all of the action’s conditions are met
  • do_all_effects: applies effects of the action, returning whether they were successful

Executing Actions of Type 2

Type 2 actions are obviously quite different from Types 1 and 3. Rather than acting upon item(s), they involve movement along a path, which is currently only the “go” action. You can get a pretty good understanding of how these actions are executed by looking at the path data structure defined in include/game-state/room.h. A path is composed of:

  • A hash handle for lookup
  • A cardinal direction (a string)
  • The destination room (a pointer to the room struct)
  • The door item in the path which must be set to open (pointer to the item struct)
typedef struct path {
    UT_hash_handle hh;
    /* direction (north/south/etc) as key */
    char *direction; // *letter case matters*
    struct room *dest;
    /* the door item in the path, which has to be
    open (attribute open is set true) to let through */
    item_t *through;
} path_t;

room.h provides a function path_search, which takes in a direction and a destination room and returns a pointer to the corresponding path if it is valid. The function for executing actions of type 2 makes use of this function to validate that the given path is valid. We can now understand the procedure for executing actions of type 2:

  • Check that the action is of the correct type
  • Check that the destination room and path exist (using path_search)
  • Move the player from to the destination (using move_room function from include/game-state/game.h)

Preliminary Analysis of Existing Action Management Code

Aims

The purpose of the following analysis is to attempt to understand how the action management system currently works in chiventure with the intent of finding hooks to implement both action blocks and action sequences.

Relevant Files

  • action_management
    • action_structs.h
    • actionmanagement.h
  • game-state
    • game_action.h
    • item.h
    • player.h

Example game (See here for how functions are used):

  • src/sample_game.c

Code Walkthrough

Note: There seems to be a weird split in structs between the action_management and game-state folders

  • Disregard this note--it was before I figured out that effects, attributes, and actions were actually owned by items and not the other way around

Player “attribute” fields are primitive values, which are going to be a PITA to validate effect targets for

When a verb (i.e. action) is applied to a noun (i.e. item), the item is searched for and then the following things are checked

  • Whether the action is associated with the item (i.e. the item HAS the action)
  • Whether those action’s conditions are met

Restating the above, items contain the valid actions that can be performed upon/by them and their requirements/effects

The attribute struct seems very flexible with regards to linking different systems (battle, RPG, etc.) together without tons of validation code--a shame it’s tucked away in items.h

Actions are somewhat rigidly restricted to 3 types, though adding a new action to those three existing types is borderline trivial.

  • Type 2 (path) is somewhat rigid though--it’s fixed on not executing an effect but explicitly changing the player’s position to the given room (after validating it, of course)

Though adding more actions than the already predefined ones would involve changing the valid_actions array in get_actions.c to be non-static (and also probably not an array)

  • While we’re at it, we might as well take that out of the code altogether and fit it into WDL
  • There’s no reason the predefined actions have to be fixed in the code

It seems that action types (item, path, item-item) are not stored in the actual game_action struct itself, but in a hash table where type is returned after a lookup by game_action_t->action_name

  • This makes sense considering game_action_t are stored per item, implying duplicates across certain items and it’d be redundant to have an action_type field
  • Though this would make it much nicer for implementation >>

The lists of action conditions and action effects in a game_action is actually initialized to NULL at first and added to as the WDL file is loaded

Questions and Research to do

  • Why is it that after getting supported actions via get_supported_actions pointers to specific actions used in add_action still need to be explicitly defined? Aren’t actions associated with items via their name rather than direct pointers?
    • Same deal with attributes
  • Is search_supported_actions supposed to be in the action_management interface? It’s only defined in sample_game. Should it be?

Glaring Hurdles

  • Current effects only support modifying attributes, which a player currently does not have
  • Effects that use temporary variables (e.g. generating a random number to use in an action sequence) can store them in attributes, but they need some way to easily retrieve them for later deletion
  • Effects need a way to refer to “entity” objects (i.e. players, targets, etc.) as opposed to just items

Potential Ideas/Hooks

  • Abstract out player/entity primitive values to be full attribute structs
  • Develop a new struct that contains a list of effects and temporary attributes (some sort of action_sequence type of thing?)
  • Pseudo-inheritance by making a new entity struct that can either contain a player or a target? Need to think more on this

Summary of Struct Relationships

Effects:

  • HAVE: attributes to modify (single)
  • ASSOCIATED WITH: item

Actions:

  • HAVE: conditions (mult.)
  • HAVE: effects (mult.)

Items:

  • HAVE: actions (mult.)
    • i.e. The actions that can “use” this item
  • HAVE: attributes (mult.)

Attributes:

  • HAVE: values

Conditions

  • ASSOCIATED WITH: attribute
  • HAVE: expected values

Everyone thank Zak for both blessing us with his artistic ability and imparting invaluable understanding of everything that is going on here:

⚠️ **GitHub.com Fallback** ⚠️