package Games::Traveller::UWP;
use 5.008003;
use strict;
use warnings;
require Exporter;
our @ISA = qw(Exporter);
our @EXPORT_OK = qw( readUwp toString );
our $VERSION = '0.94';
###############################################################
#
# Package Logic
#
###############################################################
{
my @hex = ( 0..9, 'A'..'H', 'J'..'N', 'P'..'Z' );
my %hex2dec = ();
for( my $i=0; $i<@hex; $i++ )
{
$hex2dec{$hex[$i]} = "$i";
}
my %yaml = ();
sub new { bless{}, shift }
###########################################################
#
# Essential methods
#
# readUwp() - parses in the given UWP line data
#
# toString() - prints data out in standard UWP format
#
###########################################################
sub readUwp
{
my $self = shift;
my $line = shift;
$self->_data = ();
$self->_oldUWP( $line )
unless $self->_standardUWP( $line );
return $self;
}
sub toString
{
my $self = shift;
my $routes = '';
$routes = ' :' . join( ',', @{$self->routes} ) if $self->routes;
return sprintf( "%-15s%04d %8s %1s %-20s %2s %3s %2s %s%s",
$self->name,
$self->loc,
$self->uwp,
$self->bases,
$self->codes,
$self->zone,
$self->pbg,
$self->allegiance,
$self->stars,
$routes );
}
###########################################################
#
# Read-write methods
#
###########################################################
sub _yaml :lvalue { $yaml{+shift} }
sub _data :lvalue { $yaml{+shift}->{data} }
sub _src :lvalue { $yaml{+shift}->{data}->{src} }
sub name :lvalue { $yaml{+shift}->{data}->{Name} }
sub loc :lvalue { $yaml{+shift}->{data}->{Hex} }
sub starport :lvalue { $yaml{+shift}->{data}->{Starport} }
sub size :lvalue { $yaml{+shift}->{data}->{Size} }
sub atmosphere :lvalue { $yaml{+shift}->{data}->{Atmosphere} }
sub hydrographics :lvalue { $yaml{+shift}->{data}->{Hydrographics}}
sub popDigit :lvalue { $yaml{+shift}->{data}->{Population} }
sub government :lvalue { $yaml{+shift}->{data}->{Government} }
sub law :lvalue { $yaml{+shift}->{data}->{Law} }
sub tl :lvalue { $yaml{+shift}->{data}->{TL} }
sub bases :lvalue { $yaml{+shift}->{data}->{Bases} }
sub codes :lvalue { $yaml{+shift}->{data}->{Codes} }
sub zone :lvalue { $yaml{+shift}->{data}->{Zone} }
sub popMult :lvalue { $yaml{+shift}->{data}->{PM} }
sub belts :lvalue { $yaml{+shift}->{data}->{Belts} }
sub ggs :lvalue { $yaml{+shift}->{data}->{GGs} }
sub allegiance :lvalue { $yaml{+shift}->{data}->{Allegiance} }
sub starData :lvalue { $yaml{+shift}->{data}->{Stellar} }
sub primary :lvalue { $yaml{+shift}->{data}->{Stellar}->[0] }
sub companion :lvalue { $yaml{+shift}->{data}->{Stellar}->[1] }
sub far :lvalue { $yaml{+shift}->{data}->{Stellar}->[2] }
sub farCompanion :lvalue { $yaml{+shift}->{data}->{Stellar}->[3] }
sub routes :lvalue { $yaml{+shift}->{data}->{Routes} }
###########################################################
#
# Read-only methods
#
###########################################################
sub isBa($) { (population(shift) == 0) && 'Ba ' }
sub isLo($) { (population(shift) =~ /[0-4]/) && 'Lo ' }
sub isHi($) { (population(shift) =~ /[9A]/) && 'Hi ' }
sub isAg($) { (uwp(shift) =~ /^..[4-9][4-8][5-7]/) && 'Ag ' }
sub isNa($) { (uwp(shift) =~ /^..[0-3][0-3][6-A]/) && 'Na ' }
sub isIn($) { (uwp(shift) =~ /^..[012479].[9A]/) && 'In ' }
sub isNi($) { (uwp(shift) =~ /^....[1-6]/) && 'Ni ' }
sub isRi($) { (uwp(shift) =~ /^..[6-8].[6-8][4-9]/) && 'Ri ' }
sub isPo($) { (uwp(shift) =~ /^..[2-5][0-3][^0]/) && 'Po ' }
sub isWa($) { (hydrographics(shift) eq 'A') && 'Wa ' }
sub isDe($) { (uwp(shift) =~ /^..[2-A]0/) && 'De ' }
sub isAs($) { (size(shift) eq '0') && 'As ' }
sub isVa($) { (uwp(shift) =~ /^.[1-A]0/) && 'Va ' }
sub isIc($) { (uwp(shift) =~ /^..[01][1-A]/) && 'Ic ' }
sub isFl($) { (uwp(shift) =~ /^..A[1-A]/) && 'Fl ' }
sub isCp($) { (codes(shift) =~ /cp/) && 'Cp ' }
sub isCx($) { (codes(shift) =~ /cx/) && 'Cx ' }
sub isNice($)
{
my $self = shift;
return 1 if $self->tl =~ /[12345]/
|| $self->atmosphere =~ /[456789]/
|| $self->population >= 100_000_000;
}
sub isGassy($)
{
my $self = shift;
return 1 if $self->atmosphere ne '0';
}
sub isaRock($)
{
my $self = shift;
return 1 if $self->size ne '0'
&& $self->atmosphere eq '0'
&& $self->hydrographics eq '0';
}
############################################################
#
# Returns the calculated population of the world.
#
############################################################
sub population($)
{
my $self = shift;
return $self->popMult * (10 ** $hex2dec{ $self->popDigit } );
}
############################################################
#
# Returns the hex-row or hex-col coordinates of the world.
#
############################################################
sub col($) { (loc(shift) =~ /^(..)/)[0] }
sub row($) { (loc(shift) =~ /(..)$/)[0] }
############################################################
#
# Returns the core UWP, i.e. 'A123456-7'
#
############################################################
sub uwp($)
{
my $self = shift;
no strict;
return $self->starport . ($self->size || '0')
. ($self->atmosphere || '0')
. ($self->hydrographics || '0')
. ($self->popDigit || '0')
. ($self->government || '0')
. ($self->law || '0')
. '-'
. ($self->tl || '0');
}
############################################################
#
# Returns the PBG, i.e. '323'
#
############################################################
sub pbg($)
{
my $self = shift;
no strict;
return ($self->popMult || '0')
. ($self->belts || '0')
. ($self->ggs || '0');
}
############################################################
#
# Converts star data back to string format.
#
############################################################
sub stars($)
{
my $self = shift;
return '' unless $self->starData;
my @primary = @{$self->primary};
my @companion = @{$self->companion};
my @far = @{$self->far};
my @farcmp = @{$self->farCompanion};
my $primary = $primary[0];
$primary = '(' . join( ' ', @primary ) . ')' if @primary > 1;
$primary = $primary . ' ';
my $companion = '';
$companion = join( ' ', @companion ) . ' '
if @companion > 0;
my $far = '';
if ( @far > 0)
{
$far = $far[0];
$far = '(' . join( ' ', @far ) . ')' if @far > 1;
$far .= ' ' . join( ' ', @farcmp ) if @farcmp;
$far = "[$far]";
}
return $primary . $companion . $far;
}
###########################################################
#
# Estimate the importance of this world in the big
# scheme of things.
#
###########################################################
sub importance($)
{
my $self = shift;
my $importance = 0;
$importance++ if $self->starport =~ /[AB]/;
$importance-- if $self->starport =~ /[EX]/;
$importance++ if $self->tl !~ /\d/;
$importance-- if $self->tl =~ /[01235]/;
$importance++ if $self->isHi();
$importance-- if $self->isLo();
$importance++ if $self->isRi();
$importance-- if $self->isPo();
$importance++ if $self->isAg();
$importance++ if $self->isIn();
$importance++ if $self->isCp() || $self->isCx();
$importance = 0 if $importance < 0;
return $importance;
}
###########################################################
#
# Return the number of billions of people here,
# or fraction thereof.
#
###########################################################
sub countBillionsOfPeople($)
{
my $self = shift;
my $pm = $self->popMult() || 1;
return $pm * 10 if $self->popul eq 'A';
return $pm if $self->popul eq '9';
return $pm/10 if $self->popul eq '8';
return $pm/100 if $self->popul eq '7';
return 0;
}
###########################################################
#
# Sort trade codes.
#
###########################################################
sub alphabetizeTradeCodes($)
{
my $self = shift;
$self->codes = join( ' ', sort split( ' ', $self->codes ) );
}
###########################################################
#
# Determine trade codes.
#
# Note: this will only potentially change trade codes
# that can be determined via the UWP. Other codes are
# kept as-is.
#
###########################################################
sub regenerateTradeCodes($)
{
my $self = shift;
my $s = '';
$self->codes =~ s/(Ba|Lo|Hi|Ag|Na|In|Ni|Ri|Po|Wa|De|As|Va|Ic|Fl)\s*//g;
$s .= $self->isBa || $self->isLo || $self->isHi || '';
$s .= $self->isAg || $self->isNa || '';
$s .= $self->isIn || $self->isNi || '';
$s .= $self->isRi || $self->isPo || '';
$s .= $self->isWa || $self->isDe || '';
$s .= $self->isAs || $self->isVa || '';
$s .= $self->isIc || $self->isFl || '';
$self->codes = $s . $self->codes;
return $self;
}
###########################################################
#
# Internal functions
#
###########################################################
sub DESTROY { delete $yaml{+shift} }
sub _standardUWP($$)
{
my $self = shift;
my $line = shift;
if ( $line =~ /^\s*(\S.+\S)? # $1 name
\s*(\d{4}) # $2 hex
\s+(\w\w{6}-\w) # $3 uwp
\s+(.*) # $4 codes
\s+(\d{3}) # $5 PBG
(.*) # $6 etc
$/x )
{
$self->_src = 'Std';
$self->name = $1;
$self->loc = $2;
$self->_loadUwp( $3 );
$self->_loadCodes( $4 );
$self->_loadPBG( $5 );
my $etc = $6;
if ( $etc =~ /(\w\w)\s*(.*)/ )
{
$self->allegiance = $1;
$self->_loadStars( $2 );
}
else
{
$self->allegiance = 'Na';
}
return $self;
}
return ();
}
sub _oldUWP($$)
{
my $self = shift;
my $line = shift;
if ( $line =~ /^\s*(\S.*\S)? # name
\s*(\d{4}) # hex
\s+(\w\w{6}-\w) # uwp
\s*(.*) # codes
/x )
{
$self->_src = 'Old';
$self->name = $1;
$self->loc = $2;
$self->_loadUwp( $3 );
my @codes = split( ' ', $4 );
my $gg = pop @codes if @codes && $codes[-1] eq 'G';
$self->_loadCodes( join( ' ', @codes ) );
$self->popMult = 1;
$self->popMult = 0 if $self->popDigit == 0;
$self->belts = 0;
$self->ggs = 0;
$self->ggs = 1 if $gg;
$self->allegiance = 'Na';
}
return ();
}
sub _loadUwp
{
my $self = shift;
my $uwp = shift;
my $dash;
($self->starport,
$self->size,
$self->atmosphere,
$self->hydrographics,
$self->popDigit,
$self->government,
$self->law,
$dash,
$self->tl) = split( '', $uwp );
}
sub _loadCodes
{
my $self = shift;
my $codes = shift;
my @codes = split( ' ', $codes );
my $bases = shift @codes if @codes && length( $codes[0] ) == 1;
my $zone = pop @codes if @codes && length( $codes[-1] ) == 1;
$self->bases = $bases || '';
$self->codes = join( ' ', @codes );
$self->zone = $zone || '';
}
sub _loadPBG
{
my $self = shift;
my $pbg = shift;
($self->popMult,
$self->belts,
$self->ggs) = split( '', $pbg );
}
sub _loadStars
{
my $self = shift;
my $stuff = shift;
my ($stars, $routes) = split( ':', $stuff );
$self->_loadRoutes( $routes ) if $routes;
return unless $stars;
# force all primary stars to be nested
$stars =~ s/ \[(\w+ \w+)/ [[$1]/;
$stars =~ s/^(\w+ \w+)/[$1]/;
# translate all parens to brackets
$stars =~ tr/\(\)/\[\]/;
# add commas
$stars =~ s/\] /\], /g;
$stars =~ s/(\w+ \w+\*?) /$1, /g;
$stars =~ s/,\s*$//;
# rip it all apart
my ($junk1, $pri, $comp,
$junk2, $far, $farcomp) = split( /[\[\]]/, $stars );
$comp =~ s/^,\s*//;
$comp =~ s/,\s*$//;
$farcomp =~ s/^,\s*// if $farcomp;
my @pri = split( /\s*,\s*/, $pri ) if $pri;
my @comp = split( /\s*,\s*/, $comp ) if $comp;
my @far = split( /\s*,\s*/, $far ) if $far;
my @fcmp = split( /\s*,\s*/, $farcomp ) if $farcomp;
$self->starData =
[
\@pri, \@comp, \@far, \@fcmp
];
}
sub _loadRoutes
{
my $self = shift;
my $routes = shift;
my @routes = split( ',', $routes );
$self->routes = \@routes;
}
}
1;
__END__
=head1 NAME
Games::Traveller::UWP - The Universal World Profile parser for the Traveller role-playing game.
=head1 SYNOPSIS
use Games::Traveller::UWP;
print "This is UWP $Games::Traveller::UWP::VERSION\n";
my $uwp = new Games::Traveller::UWP;
my $line = "My World 0980 X123456-8 N Ri Ag Cp R G";
$uwp->readUwp( $line );
print $uwp->toString();
$uwp->readUwp( 'Foo 1010 A123456-7 B Ri In Da Na R 232 Im K0 V [(G5 D G6 D)] :1010, 1011, 1012' );
print $uwp->toString();
=head1 DESCRIPTION
The UWP package is a module that provides access to UWP data by parsing
a valid UWP line, stored in a scalar string. The data is parsed and made
available to the user via a rich set of accessors, some of which are usable
as L-values (but most are read-only).
=head1 OVERVIEW OF CLASS AND METHODS
To create an instance of a UWP:
my $uwp = new Games::Traveller::UWP;
The following accessors can be either RValues (read) or LValues (write):
=over 3
$uwp->name
$uwp->loc
$uwp->starport
$uwp->size
$uwp->atmosphere
$uwp->hydrographics
$uwp->popDigit
$uwp->government
$uwp->law
$uwp->tl
$uwp->bases
$uwp->codes
$uwp->zone
$uwp->popMult
$uwp->belts
$uwp->ggs
$uwp->allegiance
$uwp->starData (an array ref)
$uwp->routes (an array ref)
starData() returns a four-element array reference, each element of which
contains another array reference to a group of stars:
my $aref = $uwp->starData();
my @array = @$aref;
print $aref[0]->[0] # primary star. always present.
, $aref[0]->[1] # binary companion to primary, if there is one.
, $aref[1]->[0] # first 'near' companion star
, $aref[1]->[1] # second 'near' companion star
, $aref[2]->[0] # far primary star.
, $aref[2]->[1] # binary companion to far primary, if there is one.
, $aref[3]->[0] # first 'near' companion star to far primary
, $aref[3]->[1]; # second 'near' companion star to far primary
These elements (primary, companion, far, far companion) are individually
accessible via these read-write methods:
$uwp->primary
$uwp->companion
$uwp->far
$uwp->farCompanion
These all return array references.
print $uwp->primary->[0], "\n"; # primary star only
print "@{$uwp->primary}\n"; # primary with its binary companion, if any.
print $uwp->companion->[0], "\n"; # first near companion
print "@{$uwp->companion}\n"; # all near companions
print $uwp->far->[0], "\n"; # far primary only
print "@{$uwp->far}\n"; # far primary with binary companion, if any.
print $uwp->farCompanion->[0], "\n"; # first near companion to far primary
print "@{$uwp->farCompanion}\n"; # all near companions to far primary
=back
In addition to the above, there is a large body of read-only accessors:
=over 3
$uwp->population # calculates the population from the popDigit and popMult
$uwp->col # returns the column component of the hex location
$uwp->row # ibid for the row
$uwp->isNice # returns '1' if the TL < 7, OR
# the atmosphere is pleasant, OR
# the population >= 100 million
$uwp->isGassy # returns '1' if the atmosphere isn't 0 (zero)
$uwp->isaRock # returns '1' if the size is not 0 (zero)
# AND the atmosphere and hydrographics ARE 0 (zero)
$uwp->uwp # returns the core UWP (i.e. "A123456-7")
$uwp->pbg # returns the PBG string (i.e. "323")
$uwp->stars # returns the standard star data string
$uwp->importance # calculates how important the world probably is
$uwp->countBillionsOfPeople # returns the population in billions
$uwp->alphabetizeTradeCodes
$uwp->regenerateTradeCodes # re-does trade codes
The previous method is useful if you've been changing the UWP values around.
$uwp->isBa # returns 'Ba' if the world is Barren
$uwp->isLo # returns 'Lo' if the world is Low-Pop
$uwp->isHi # high pop
$uwp->isAg # agricultural
$uwp->isNa # non-agri
$uwp->isIn # industrial
$uwp->isNi # non-ind
$uwp->isRi # rich
$uwp->isPo # poor
$uwp->isWa # water world
$uwp->isDe # desert
$uwp->isAs # mainworld is asteroid
$uwp->isVa # vacuum world
$uwp->isIc # all water is ice
$uwp->isFl # non-water fluid oceans
$uwp->isCp # subsector capital
$uwp->isCx # sector capital
=back
Finally,
=over 3
$uwp->toString
=back
returns the UWP data encapsulated in a string, suitable for writing to
an output stream.
=head1 AUTHOR
Pasuuli Immuguna
=head1 COPYRIGHT
This library is free software; you can redistribute it and/or
modify it under the same terms as Perl itself.
=head1 AVAILABILITY
The latest version of this library is likely to be available from CPAN.
=cut