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
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.
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
Finally print the table out
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.
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.
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.
.------------------------------------.
| Store Inventory |
+-----------+----------------+-------+
| Item Type | Item | Count |
+-----------+----------------+-------+
| Mundane | Backpack | 2 |
| Mundane | Travel Rations | 10 |
| Potions | Healing | 5 |
| Potions | Invisibility | 2 |
'-----------+----------------+-------'
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
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');
# 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;
}
# 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. 😜
Comments
Post a Comment