Exercise in Perl - The Magic Shop

I wanted to refresh my knowledge of Perl since I haven't used it in some time, and it's fairly useful for more complex scripting needs.   I needed a project rather than just some random reading, thus I decided to combine it with my interest in RPGs.

I wanted a script that would give me a dynamic inventory for a magic shop.   My requirements were that the current store inventory would be calculated from a list containing
  • All the potential items a magic shop would carry
  • The chance that the item would be in stock 
  • The maximum number in the shop.
  • Item categorization
This isn't developed as efficiently as it could be, I was more focused on experimenting with arrays and iterations over them, etc. 

Initialization and command line options

I'm doing this in Bash for Windows 10 so using the standard UNIX header.   I'll need a couple of modules to output a nicely formatted table, ease processing of CSV files, and debugging.


#!/usr/bin/perl
use strict;
use warnings;

use Text::CSV;
use Text::ASCIITable;
use Getopt::Long 'HelpMessage';
use Data::Dumper;

I like to declare and initialize variables right at the top so I know they're "globally" available.

# Variable Delcarations and initialization
my $csvFile="";     # File with Store's stock type information
my %itemTypes;      # Hash aray of Item Types

# Initize table for output
my $tb = Text::ASCIITable->new();
$tb->setOptions('headingText',"Store Inventory");
$tb->setCols('Item Type','Item','Count');

The  Getopt module allows more clear argument processing.  It's overkill here as there's really only one option, but I prefer it for practice.   The HelpMessage displays the text appended to the bottom of the file (see later in document).  I added a check to ensure the file exists and was specified.

# use GetOptions to process command line arguments and print out help
GetOptions(
    "file=s"    => \$csvFile,
    "help"      => sub { HelpMessage(0) },
) or HelpMessage(1);

# Checkt that data file was specified and that it exists
die "Must supply name of store inventory csv file\n" if $csvFile eq "";
die "Input file doesn't exist\n" if !-e $csvFile;

Read in the data

I used a function, readCSVFile to process the data file.   This was both to make the program cleaner looking and to alow re-use should I modify this later to process more than one store.  The store data is stored in a hash.

# Read the data file into a hash array
my %stockInfo = readCSVFile(
    {
        filename => $csvFile,
    }
);

Processing the data

Because I want the types or categorizations to be dynamic so data files can be generated without the need of knowing the program, I gather them from what's in the data file, ensuring only unique categories

# Go through the hash aray and create a list of unique types for categorization
foreach my $type ( keys %stockInfo ) {
    $itemTypes{$stockInfo{$type}->{Type}} = 1; 
}

Next, we iterate through my list of categories and see if the items associated with the category are in stock, and generate a random number of them.  Here we make use of a local hash to store each item, and it's calculated count

# Get inventory for each category
foreach my $itemType (sort keys %itemTypes) {

    # Local variable declaration
    my %items;

    # Iterate through the stock info
    foreach my $item (keys %stockInfo) {

        # Only care about stock that matches current inventory
        if ($stockInfo{$item}->{Type} eq $itemType) {
   
            # Generate random percentage and check against the stock chance
            if ( ((int(rand 100) + 1) / 100) <= $stockInfo{$item}->{StockChance} ) {
                # If in stock, determine random number of them between 1 and the maximum stock
                $items{$item}= int(rand($stockInfo{$item}->{MaxStock})) + 1
            }

        }
    };

    # Add the inventory count to the table output
    foreach my $row (sort keys %items) {
        $tb->addRow($itemType,$row,$items{$row});
    }

}

Finally print the table out
  

# Print the table
print $tb;

# -- End Main Perl Script

readCSVFile Function

Using a funciton allows for reuse and keeps the code a bit more readable.

################################################################################
# Sub Routines                                                                 #
################################################################################

sub readCSVFile {

    my ($args) = @_ ;

    my $csv = Text::CSV_XS->new(
        {
            binary      => 1,
            allow_loose_quotes  => 1,
            allow_loose_escapes => 1,
            allow_unquoted_escape   => 1,
            allow_whitespace    => 1,
            auto_diag       => 1,
            sep_char        => ','
        }
    );
   

    my $inventoryFile;

Because this may be reused, I  recheck to see if the file exists, and if it was supplied to the function at all.   We could put a default value here if desired, but I don't feel that's useful.

    if (exists $args->{filename}){
        $inventoryFile = $args->{filename};
        die "$inventoryFile doesn't exist" if !-e $inventoryFile;
    } else {
        die "File Name not supplied to function"
    }
   
    my %myInventory;

    open(my $myData, "<", $inventoryFile) or die "Could not open $inventoryFile file\n";

We read the data and store it into a hash and return the hash.   Essentially, I'm creating a data structure for each item from the column data.   The structure would be by the item, not the type, with characteristics:  Type, Maximum count for the store, the chance of it being in the store.
   
    my @headers = @{ $csv->getline($myData) };
    $csv->column_names(@headers);
   
    while (my $iItem = $csv->getline_hr($myData)) {
   
        my $itemType = "";

        $itemType = $iItem->{Item};

        foreach my $key (keys %{$iItem}) {

            if ($key ne "Item") {
                $myInventory{$itemType}{$key} = $iItem->{$key};
            }

        }
       
    }

    close($inventoryFile);

    return %myInventory;
}

The Help Message

This is appended to the ned of the file for GetOptions to print out for the -h/--help option or if missing required parameters.

__END__

=head1 SYNOPSIS

    magic_shop_stock [arguments]

    Reads a CSV file in format of  itemType, Item, Chance of Stock and calculates a store's current inventory based off a set of parameters defined at the top of the script

    Arguments
        -f, --file  File containing items and their chance of stock

=cut

The data file

For ease of use, the data file is a simple CSV file with the following fields:  Type, MaxStock, Item, StockChance.   For example:

Type,MaxStock,Item,StockChance
Potions,5,Healing,.75
Potions,2,Invisibility,.10
Swords,2,Short Sword +1,.10
Mundane,5,Backpack,1
Mundane,10,Travel Rations,1

The first line, the header, is required.   The above data would indicate we potentially have Potions, Swords, and Mundane items.   There is a 75% chance of having healing potions, 10% of having Invisibility potions and +1 Short Swords, and finally 100% chance of having backpacks and Travel Rations.  At any one time, we would have a maximum of 5 healing potions, 2 invisibility potions, two +1 short swords, 5 backpacks, and 10 travel rations.

Sample Output

$ ./magic_shop_stock -f ../../myStockTypes.csv
.------------------------------------.
|           Store Inventory          |
+-----------+----------------+-------+
| Item Type | Item           | Count |
+-----------+----------------+-------+
| Mundane   | Backpack       |     2 |
| Mundane   | Travel Rations |    10 |
| Potions   | Healing        |     5 |
| Potions   | Invisibility   |     2 |
'-----------+----------------+-------'

Things left to do

There are a number of things I'd like to add
  • I'd rather a more dynamic help message that doesn't have the program name hardcoded
  • Adding a maxium inventory for store across all items might be good.
  • Store the current inventory and add options to items and have the generation of new inventory limited by current inventory and maximum.
  • Change some of the variable names as this was developed somewhat dynamically without thought to real requirements...yeah, shame on me. 😜

Final Thoughts

I don't know if I'd ever really use this, nor how handy it would be for anyone, but it was helpful in experimenting with arrays using Perl.   If you do find it handy and would like to ping me in the comments for the file (or just copy the text above, although that could get ugly).   Feel free to request features in the comments as well.   I don't know if I'd spend much time, but if it's interesting or makes it more useful, I just might.

Comments

Popular Posts