Learning C With Game Concepts/Inventory System

Inventory System

edit

There is nothing quite so satisfying as equipping one's character. Most of the time, you start out at level 1, wearing rags and shackles. After being released from the local dungeon for one reason or another, you meet a Haberdasher with a heart of gold. He gives you some odd jobs so you can earn enough copper to eat. After weeks of punching things, fetching things, running from things, and still without two copper pieces to scratch together, you at last come across the "Unremarkable Sword of Rusty Might". Huzzahǃ It nothing compared to what the City Guard is using, but now those sewer rats shall feel your wrath. So let's get to itǃ

An Inventory Shouldː

  • Contain a list of items.
  • We should be able to ADD and REMOVE items from the list, as well as FIND items.
  • Similar items should be aggregated. When a similar item is added, quantity is increased, not list size.
  • Players should have their own inventory, as should chests and other containers.

An Item Shouldː

  • Have a name and description.
  • Health and mana.
    • These are the restorative properties of potions. 20 Health heals 20 health points if consumed.
  • Similar items should be aggregated. When a similar item is added, quantity is increased, not list size.
  • Have the option of multiple uses.
  • Have an ID that uniquely identifies it.

Putting the above specification into practice, we start by defining the kinds of items available, via an enumeration. In the future, we may want to import a list of items from a file, rather than keeping all of them with our source code. However, it's best to keep things small for testing purposes.

// 0 based.
enum itemNumber {
   HEALTH_POTION,
   MANA_POTION
};

typedef struct ItemStructure {
   char name[50];
   char description [100];
   int health;
   int mana;
   int quantity;
   int usesLeft;
   int id;
} item;

// Doubly linked list for items.
typedef struct itemNodeStructure {
   item *current;	                 // Pointer to current item.
   struct itemNodeStructure *previous;   // Pointer to previous item in the list.
   struct itemNodeStructure *next;	 // Pointer to the next item in the list.
} itemNode;


Each item that we create will be embedded in an item node. A node is a single unit which can be linked to other nodes in order to form a data structures such as lists and trees. In this case, the data structure is a doubly linked list. As you can see, the itemNodeStructure points forwards, backwards, and to its own item. When defining the itemNodeStructure, it is necessary to use "struct itemNodeStructure" to declare the pointers because the itemNode typedef is not yet in effect and the compiler will not understand.


Going back to ye olde playerStructure datatype, we add a new value to it.

 itemNode *inventory;

Because the functions required for a basic inventory setup are somewhat involved, it cannot be broken up into more digestible pieces. My code is very likely not the most elegant solution to the problem, so there is room for improving the clarity. For now, if you want to understand how the functions for the inventory work, you'll need to read through the comments until you can identify what each piece of code is doing.


inventory.c

#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <stdlib.h>
#include "gameProperties.h"
#include "players.h"
#include "fightSys.h"

// 0 based.
enum itemNumber {
   HEALTH_POTION,
   MANA_POTION
};

typedef struct ItemStructure {
   char name[50];
   char description [100];
   int health;
   int mana;
   int quantity;
   int usesLeft;
   int id;
} item;

// Doubly linked list for items.
typedef struct itemNodeStructure {
   item *current;	                 // Pointer to current item.
   struct itemNodeStructure *previous;   // Pointer to previous item in the list.
   struct itemNodeStructure *next;	 // Pointer to the next item in the list.
} itemNode;

// Function Prototypes
int DisplayInventory(itemNode *node);
int AddItem(itemNode *inventory, enum itemNumber number);

int main () {
  player *Hero = NewPlayer(WARRIOR, "Sir Leeroy");
  player *Villain = NewPlayer(WARRIOR, "Sir Jenkins");

  AddItem(Hero->inventory, HEALTH_POTION); 
  AddItem(Hero->inventory, HEALTH_POTION); 
  AddItem(Hero->inventory, MANA_POTION);
  DisplayInventory(Hero->inventory);  

  return(0);
}

// Exampleː DisplayInventory(Player->inventory);
int DisplayInventory(itemNode *node) {
  // While there is an item present, print said item.
  while (node->current != NULL)  {
    printf("Name: %s\n", node->current->name);
    printf("Description: %s\n", node->current->description);
    printf("Health: %d\n", node->current->health);
    printf("Mana: %d\n", node->current->mana);
    printf("Uses Left: %d\n", node->current->usesLeft);
    printf("Quantity: %d\n\n", node->current->quantity);

    // If the node points to another node, go to it and print it's item. Otherwise, end loop.
    if (node->next != NULL) {
      node = node->next;   // Move to next node.
    } else {
      return(0);           // Loop ends
    }
  }
  // Inventory pointer is NULL, there are no items.
  printf("Inventory is empty.\n");
  return(0);
}

// FIND ITEM
// Used in the functionsː AddItem and RemoveItem.
// Can't return 0 because it's interpreted as an int. Return NULL for functions
// that are supposed to return pointers.
itemNode* findItem (itemNode *node, enum itemNumber number) {
  
  // If the node is NULL, it's an empty list. End function.
  if (node == NULL) {
    return(NULL);
  }

  // While the current node has an item.
  while (node->current != NULL) {
     
     // Compare that item's id to our number.
     // If it is a match, return the memory address of that node.
     if (node->current->id == number) {
       return(node);
     } 

     // If the current item doesn't match and there
     // is another node to look examine, move to it.
     if (node->next != NULL) {
       node = node->next;
     } else {
       return(NULL);   // List ends.
     }
  }
  return(NULL);  // List is empty.
}

// Use Exampleː AddItem(Hero->inventory, HEALTH_POTION);
int AddItem(itemNode *node, enum itemNumber number) {
  itemNode *previousNode;
  itemNode *searchResult;

  // See if item already exists.
  searchResult = findItem(node, number);
  if (searchResult != NULL) {
    searchResult->current->quantity += 1;    // Increase quantity by one and end function.
    return(0);
  }
  
  // Generate item if it doesn't exist.
  // This requires allocating memory and increasing
  // the size of the linked list.
  item *object = malloc(sizeof(item));          // Allocating memory for item.

  // Just like our class enumeration, our item names are variables
  // that stand for numbers. Because cases in C can't use strings,
  // this method makes them much more readable.
  switch(number) {
  case HEALTH_POTION:
    strcpy(object->name, "Health Potion");
    strcpy(object->description, "Drinkable item that heals 20 health points.");
    object->health = 20;
    object->usesLeft = 1;
    object->quantity = 1;
    object->id = number;          // ID and ItemNumber are the same.
    break;
  case MANA_POTION:
    strcpy(object->name, "Mana Potion");
    strcpy(object->description, "Drinkable item that heals 20 mana.");
    object->usesLeft = 1;
    object->quantity = 1;
    object->mana = 20;
    object->id = number;
    break;
  }

  // Now that our object has been created, we must find free space for it.

  // If the current node is unused allocate memory and assign item.
  if (node->current == NULL) {
    node->current = object;

  // If the current node is occupied, check the next node.
  // If the next node doesn't exist, then we must allocate memory
  // to the next pointer.  
  } else if (node->next == NULL) {
    node->next = malloc(sizeof(itemNode));        // Allocate memory to the next pointer.
    previousNode = node;                          // Store location of current node.
    node = node->next;                            // Move to the next node.
    node->previous = previousNode;                // Link the current node to the previous one.
    node->current = object;                       // Assign item to the current node.
  } else {
    // If current and next node are occupied, search for the last node.
    // The last node will have an empty "next" spot.
    while (node->next != NULL) {
      node = node->next;
    }

    node->next = malloc(sizeof(itemNode));        // Allocate memory to the next pointer.
    previousNode = node;                          // Store location of current node.
    node = node->next;                            // Move to the next node.
    node->previous = previousNode;                // Link the current node to the previous one.
    node->current = object;                       // Assign item to the current node.
  }
  return(0);
}




inventoryFinished.c

#include <stdio.h>
#include <string.h>
#include <stdbool.h>
#include <stdlib.h>

// 0 based.
enum itemNumber {
   HEALTH_POTION,
   MANA_POTION
};

typedef struct ItemStructure {
   char name[50];
   char description [100];
   int health;
   int mana;
   int quantity;
   int usesLeft;
   int id;
} item;

// Doubly linked list for items.
typedef struct itemNodeStructure {
   item *current;	 // Pointer to current item.
   struct itemNodeStructure *previous;   // Pointer to previous item in the list.
   struct itemNodeStructure *next;	 // Pointer to the next item in the list.
} itemNode;

typedef struct playerStructure {
   char name[50];
   int health;
   int mana;
   int attack;
   int defense;
   bool autoPilot;
   itemNode *inventory;
} player;

// Function Prototype
void DisplayStats(player *target);
int DisplayInventory(itemNode *node);
int AddItem(itemNode *inventory, enum itemNumber number);
int RemoveItem(itemNode *inventory, enum itemNumber number);
itemNode* findItem(itemNode *node, enum itemNumber number);

// MAIN
int main () {

  player *Hero = malloc(sizeof (player));

  // Hero
  strcpy(Hero->name, "Sir Leeroy");
  Hero->health = 60;
  Hero->mana = 30;
  Hero->attack = 5;
  Hero->defense = 1;
  Hero->autoPilot = false;
  Hero->inventory = malloc(sizeof(itemNode)); // It's necessary to initialize the inventory property with a
                                              // memory address. That way we can pass the address instead of the pointer address.
					      // Then our function would need to accept a pointer to a pointer to an itemNode
					      // as an argument and that's too much overhead.

  AddItem(Hero->inventory, HEALTH_POTION); 
  AddItem(Hero->inventory, HEALTH_POTION); 
  AddItem(Hero->inventory, MANA_POTION);
  RemoveItem(Hero->inventory, MANA_POTION);
  DisplayInventory(Hero->inventory);

  return(0);
}

int DisplayInventory(itemNode *node) {
  // While there is an item present, print said item.
  while (node->current != NULL)  {
    printf("Name: %s\n", node->current->name);
    printf("Description: %s\n", node->current->description);
    printf("Health: %d\n", node->current->health);
    printf("Mana: %d\n", node->current->mana);
    printf("Uses Left: %d\n", node->current->usesLeft);
    printf("Quantity: %d\n\n", node->current->quantity);

    // If there is another item in the list, go to it, else, stop.
    if (node->next != NULL) {
      node = node->next;
    } else {
      return(0);
    }
  }
  printf("Inventory is empty.\n");
  return(0);
}

// FIND ITEM
// Can't return 0 because it's interpreted as an int. Return NULL for functions
// that are supposed to return pointers.
itemNode* findItem (itemNode *node, enum itemNumber number) {
  if (node == NULL) {
    return(NULL);
  }

  // Avoid unitialized or unassigned nodes.
  while (node->current != NULL) {
     if (node->current->id == number) {
       return(node);
     } 

     if (node->next != NULL) {
       node = node->next;
     } else {
       return(NULL);
     }
  }
  return(NULL);
}

int AddItem(itemNode *node, enum itemNumber number) {
  itemNode *previousNode;
  itemNode *searchResult;

  // See if item already exists.
  searchResult = findItem(node, number);
  if (searchResult != NULL) {
    searchResult->current->quantity += 1;
    return(0);
  }
  
  // Generate item if it doesn't exist.
  item *object = malloc(sizeof(item)); // Item.
  switch(number) {
  case 0:
    strcpy(object->name, "Health Potion");
    strcpy(object->description, "Drinkable item that heals 20 health points.");
    object->health = 20;
    object->usesLeft = 1;
    object->quantity = 1;
    object->id = number;
    break;
  case 1:
    strcpy(object->name, "Mana Potion");
    strcpy(object->description, "Drinkable item that heals 20 mana.");
    object->usesLeft = 1;
    object->quantity = 1;
    object->mana = 20;
    object->id = number;
    break;
  }

  // If node is unused allocate memory and assign item.
  if (node->current == NULL) {
    node->current = object;
  // If node is occupied, check next node.
  } else if (node->next == NULL) {
    node->next = malloc(sizeof(itemNode));
    previousNode = node;
    node = node->next;
    node->previous = previousNode;
    node->current = object;
  // If current and next node are occupied, search for the last node.
  // The last node will have an empty "next" spot.
  } else {
    while (node->next != NULL) {
      node = node->next;
    }

    node->next = malloc(sizeof(itemNode));
    previousNode = node;
    node = node->next;
    node->previous = previousNode;
    node->current = object;
  }
  return(0);
}

int RemoveItem(itemNode *node, enum itemNumber number) {
  itemNode *searchResult;
  itemNode *previous;
  itemNode *next;

  // See if item already exists.
  searchResult = findItem(node, number);

  // If item exists, and reduce quantity by 1.
  if (searchResult != NULL) {
    searchResult->current->quantity -= 1;

    // If reduction results in 0 quantity, remove item entirely.
    if (searchResult->current->quantity <= 0) {
      previous = searchResult->previous;
      next = searchResult->next;

      // Free the item and then the node containing it.
      free(searchResult->current);
      free(searchResult);

      // Switch linked list together.
      // We can't assign the next/previous members if the itemNode is null.
      if (previous != NULL) {
        searchResult = previous;
        searchResult->next = next;
      }
      if (next != NULL) {
        searchResult = next;
	searchResult->previous = previous;
      }
    }
  }  
  return(0);
}