This Perl script is used to test "procedural generation" techniques for MonkeyNet, a game about hacking to be released for mobile.
It is basically a "microscope" for the MonkeyNet game world, allowing me to ask "what is X object, and what is it doing right now?"
MonkeyNet is entirely procedural: The engine is not required to keep any "state" at all concerning the game world. At any given time, any game object can be inspected and its properties generated, on-the-fly. This is the only way it is possible to create a game with 10,000 apparently active objects, without relying on lots of storage (on or off the device).
It is used by simply providing it with a MonkeyNet-standard, 4-digit "node address". In MonkeyNet, the 1000's place holds the "region" number, the 100's place the "network" inside that region, and the 10s place the actual node (with 100 per network).
So the address "2438" indicates "Node 38, which is in Network 4, which is in Region 2".
When a node is given, the details of the agent assigned to that node are also shown. This includes their current state (which changes over time, driven by wave functions) and whether they any "winnable" game assets.
Here are some example runs
And here is the code!
#!/usr/bin/perl
# MonkeyNet Procedural generation test rig in Perl
# Related project: MonkeyNet
# @garyd 20150206
# --------------------------------------------------------------------------------
# Notes
#
# # This is simply the formula for calculating any MonkeyNet node address
# # ---------------------------------------------------------------------
# $addr = $region * 1000 + $net * 100 + $node;
#
# NEVER F with schedule formula! # Clocksec is 'x' axis
#
# About Seeds
# # Note: For really random-per-node props (none atm), seed with $lDiceSeed
#
# $fScheduleWave = $fWaveAmp * sin ($fWaveLength * $ClockSec);
#
# $lClockMS Always leave at system max time resolution, for realism.
# $lClockEvents Occurs only as often as 'iEventResolution'. For game events.
# $lWorldSeed = 31337; # Default seed. Not too big, we add to.
# $fOnFreq = 12.0; # Sine wave length of online schedule
# $iOnJitter # How much schedule varies, low=reliable/hi=chaotic
# Note: We only do this -v in two steps as perl needs re-seeding. Java will be 1 step.
#
# $fOnTrigVal = sin ($fOnFreq * $lClockEvents);
# (later...)
# srand($lEventSeed);
# $fOnTrigVal += rand($fJitter);
# $iOnline = 1 if ($fOnTrigVal > $fOnTrigger);
#
# About clocks
# Debug display: # "\n| Clocks: Mn %2.2d S %2.2d Ms %2.2d Mcs %2.2d"
# $fAmplitude = 1.0; # Normally unchanged, results in -1.0 to 1.0
# $fFrequency = 0.05; # Smaller = smoother, range -1.0 to 1.0#
# $fDutyCycle = 0.80 -- 1 - this becomes a floor, such that the wave will only
# be gte 0 for this pct of the time see also -v
#
# Use a different duty cycle to have some things fire more/less often, avoiding
# of a bunch of 'threshholds' and tests
#
# # Moves whole wave down, making easier "if (wave)" tests
# # set it to this to have objects "80% active" (> 0)
#
# # Milliseconds replacement for Perl's slow 1-sec "time()"
# # Get millisec clock via weird fp HiRes *microsec* clock) (thus 100 * then int)
# $ClockMicroSec = int (gettimeofday * 1000);
# --------------------------------------------------------------------------------
use Time::HiRes qw(gettimeofday); # Notes
# Settings and pre-calc
# -------------------------------------------------------------------------
my @Regions, @RegionPeers, @Nets, @Types, @NetStates;
my $iNet, $iRegion, $iNode, $iAddress = 0; # Ex: 3, 7, 16, and "3716"
my $iRegionalGateway, $iNetGateway; # Reg. GW generative, Net GW fixed 'xx00'
my $iNetState = 0;
my $sNetName, $sRegionName, $sPeerList; # Strings for disp only
my $sNetStateText = "Unknown";
my $sNetText = "Unknown";
my $lWorldSeed = 9846274;
my ($lRegionSeed, $lNetSeed, $lNodeSeed, $lEventSeed, $lDiceSeed);
my ($lEventSeedSeconds, $lEventSeedMinutes, $lEventSeedHours, $lEventSeedDays);
my $ClockMicroSec = int (gettimeofday * 10000); # Notes. Perl-specific.
my $ClockMilliSec = int($ClockMicroSec/100);
my $ClockSec = int($ClockMilliSec/100); # Convenient
my $ClockMin = int ($ClockSec/60);
my $ClockHour = int ($ClockMin/60);
# Q: Are these backwards? Is ticks/1000 actually 'closest to normal'?
my $fClockTicksPerDay = 1000 * 60 * 60 * 24; # If system ticks microseconds or 'fast fwd' (best)
# my $fClockTicksPerDay = 100 * 60 * 60 * 24; # If system ticks milliseconds (preferred)
# my $fClockTicksPerDay = 60 * 60 * 24; # If system ticks are seconds (normal?)
my $fWaveAmp = 1.0; # Amplitude of schedule waves (why?)
my $fWaveCycle = 3.1415 * 2; # Normal -1 to +1 wave
my $fWaveTime = $fClockTicksPerDay; # How many ticks for a '1 day long' cycle
my $fPctAwake = 0.90; # Pct time spent awake (gt 0)
my $fWakeThreshold, $fOnlineThreshold, $fActionThreshold;
my $fJitter = 0;
my $fJitterMax = 0.05; # To use: target *= 1-jitter
my $Awake, $Online, $Acting;
my $sState = "Sleeping"; # For disp only, but default state
my @UserActivities;
my $iUserActivity = 0; # If acting, what doing
my $sUserActionText = "Uninit";
# -------------------------------------------------------------------------
# Mainline
# -------------------------------------------------------------------------
# Handle args
$iAddress = shift (@ARGV) or die ("\n\nPass a single 4-digit node number\n");
# Setup
$iNode = $iAddress % 100;
$iNet = int ($iAddress % 1000 - $iNode) / 100;
$iRegion = int($iAddress / 1000);
fnLoadTestData();
fnGenerateSeeds();
# Generate all region, network and node properties
fnGenerateRegionProperties();
fnGenerateRegionPeers();
fnGenerateRegionEvents();
fnGenerateNetworkProperties();
fnGenerateNodeProperties();
# Now that it's known, figure if address is region, net, gateway or node for convenience
fnGetAddressType();
# Generate all schedules and user activity
fnGenerateSchedules();
fnGenerateUserActivity();
# Cheap dev output
# -------------------------------------------------------------------------
$sPeerList = "No peers";
if ($iAddress == $iRegionalGateway) { $sPeerList = join (", ", @RegionPeers); }
$sWorldText = "\nWorld | Ws $lWorldSeed Rs $lRegionSeed Nes $lNetSeed Nos $lNodeSeed Ems $lEventSeedSeconds Ds $lDiceSeed \n";
$sRegionText = "\nNet | $sRegionName Region ($iRegion) - $sNetName Net ($iNet) - Reg Gw $iRegionalGateway - Net Gw $iNetGateway";
$sNetText = "\nGate | $sRegionName Region ($iRegion) - $sNetName Net ($iNet) - Reg Gw $iRegionalGateway - Net Gw $iNetGateway - Peers $sPeerList";
print ($sWorldText);
print ($sRegionText);
print ($sNetText);
printf (
"\n %4.4d"
."| %2.2d:%2.2d:%2.2d:%2.2d.%2.3d" # Crammed clock
."| Sched:%+2.2f Wake:%+2.2f On:%+2.2f Act:%+2.2f"
."| $sState ($sUserActionText)",
$iAddress, # Mod 4 pretty
$ClockHour % 24, $ClockMin % 60, $ClockSec % 60, $ClockMilliSec % 100, $ClockMicroSec % 1000,
$fScheduleWave, $fWakeTreshold, $fOnlineThreshold, $fActionThreshold);
# -------------------------------------------------------------------------
# The End
# -------------------------------------------------------------------------
print ("\n");
exit;
# ---------------------------------------------------------------------
# Generate all seeds based on world seed, timers, and node number
# ---------------------------------------------------------------------
sub fnGenerateSeeds() {
# $lRegionSeed = $lWorldSeed + $iRegion * 1000; # Use "3000" form of region addy
srand($lWorldSeed);
$lRegionSeed = $iRegion * int(rand(65536)) + $iRegion;
$lNetSeed = $iRegion + int(rand(65536)) * $iNet + int(rand(65536));
$lNodeSeed = $iNet + int(rand(65536)) * $iAddress + int(rand(65536));
$lEventSeedSeconds = $ClockSec + $lNodeSeed * int(rand(65536)); # WARN: Is set high for fast forward testing!
$lEventSeedMinutes = int($lEventSeedSeconds / 60); # changes every 60 'seconds'
$lEventSeedHours = int($lEventSeedMinutes / 60); # changes every 60 'minutes'
$lEventSeedDays = int($lEventSeedHours / 24); # changes every 60 'minutes'
$lDiceSeed = $lWorldSeed * $ClockMicroSec; # For 'true' random
}
# ---------------------------------------------------------------------
# Simply determine address type via masks (just like tcpip sorta)
# ---------------------------------------------------------------------
sub fnGetAddressType() {
$iType = 3; # Assume node
$iType = 0 if ($iAddress == 0); # Reserve 'The Core'
$iType = 1 if ($iAddress == $iRegion * 1000 + $iNet * 100); # Type network ie '3700', always network gw
$iType = 2 if ($iAddress == $iRegionalGateway); # Regional gateway
$sTypeText = @Types[$iType]; # Disp only
}
# ---------------------------------------------------------------------
# Generate Region Properties
# ---------------------------------------------------------------------
sub fnGenerateRegionProperties() {
# Determine the 'regional megahub' in this region
# ---------------------------------------------------------------------
srand ($lRegionSeed);
$iTemp = int(rand(10)); # Pick one network (0-9) in region to act as regional gateway
$iRegionalGateway = $iRegion * 1000 + $iTemp * 100; # Results in full 4-dig address of reg gw
$sRegionName = @Regions[$iRegion];
# Determine regional "object density", influences props of its networks
$fRegionDensity = rand(0.44) + 0.33; # Density range 0.33 to 0.99 (treat as "percent")
$iNetNameIndex = $iRegion * 10 + $iNet; # Calc index into netnames array, ex 2*10+3 = 23 =
$sNetName = @Nets[$iNetNameIndex]; # Pick from flat array, Java will differ
}
# ---------------------------------------------------------------------
# Generate Region Peerings
# ---------------------------------------------------------------------
sub fnGenerateRegionPeers() {
# Generate list of regional peers, influenced by density
# ---------------------------------------------------------------------
srand ($lRegionSeed);
@RegionPeers = (); # reset
$iNumRegionPeers = rand(3.5) + 2.5; # Very carefully tuned!
$iNumRegionPeers *= $fRegionDensity; # Btw won't work in Java
$iMandatoryPeer = $iRegion + 1; # 'next region up' peer (WARN: 1-2 dig version!)
$iMandatoryPeer = 0 if ($iMandatoryPeer > 9); # wraparound
$bAdded = 0; # Ensure, below, mandatory peer in list
for ($i = 0; $i < int($iNumRegionPeers); $i++) { # Note clever formula avoids dupes
$iTemp = $i;
if (int(rand(2)) == 0) { # Possibly flip direction
$iTemp *= -1;
}
$iTemp += $iRegion;
$iTemp = abs (10 - $iTemp) if ($iTemp > 9); # Handle wraparound: 10=0, 11=1, etc
$iTemp = 10 - abs($iTemp) if ($iTemp < 0); # 10=0, 11=1, etc
next if ($iTemp == $iRegion); # Ignore (sad) peer-to-self
$bAdded = 1 if ($iTemp == $iMandatoryPeer); # Set true if this was the mandatory peer
push (@RegionPeers, $iTemp); # Add it to the list
}
push (@RegionPeers, $iMandatoryPeer) if ($bAdded != 1); # If not already
}
# ---------------------------------------------------------------------
# Generate regional events (outages, etc)
# ---------------------------------------------------------------------
sub fnGenerateRegionEvents() {
# Generate "region events": The modulus on clock limits events frequency
# Possibility of state change increases with network density (Carefully tuned!)
# 0th state 'normal', all others considered down, later more subtlety - disp strings only
# ---------------------------------------------------------------------
srand ($lEventSeedSeconds); # New: Use this, as full node + time salts it
$iNetState = 0;
$iNetState = int(rand(scalar(@NetStates))) if (0.70 + rand(0.2372) < $fNetworkDensity);
$sNetStateText = @NetStates[$iNetState];
}
# ---------------------------------------------------------------------
# Generate network properties
# ---------------------------------------------------------------------
sub fnGenerateNetworkProperties() {
# Generate "Network" properties
# Network obj density is region density +/- some pct
# ---------------------------------------------------------------------
# srand ($sRandSeed + $region + $net); # seed+region+net (IMPORTANTE?)
srand($lNetSeed);
$fNetworkDensity = $fRegionDensity + rand(0.036) - 0.018; # Carefully tuned for minor variation!
$fNetworkDensity = 0 if ($fNetworkDensity < 0); # Clampdown
$fNetworkDensity = 1 if ($fNetworkDensity > 1);
# Network gateways (unlike region GWs) are always fixed: nn00 i.e. '3300'
$iNetGateway = $iRegion * 1000 + $iNet * 100; # Note is full xxxx addy
}
# ---------------------------------------------------------------------
# Generate node properties
# ---------------------------------------------------------------------
sub fnGenerateNodeProperties() {
# Generate (additional) specifics about node
# Figure 'node prop density' which varies much more but the variance doesn't change over time
# ---------------------------------------------------------------------
srand($lNetSeed);
$fNodePropertyDensity = $fNetworkDensity * 1 + rand(0.56) - 0.28; # Density of apps, events, etc
$fNodePropertyDensity = 1 if ($fNodePropertyDensity > 1);
# ^-- Add other non-time-changing node props here, before seeding with time
# Node properties which require the 'time seed'
# Determine whether (arg) node is enabled. Roll < network's density to win.
# ---------------------------------------------------------------------
srand ($lEventSeedSeconds);
$iNodeEnabled = 0;
$sEnabledText = "Disabled"; # Pretty output only
$iNodeEnabled = 1 if (rand(1) < $fNetworkDensity);
$sEnabledText = "Enabled " if ($iNodeEnabled == 1);
}
# ---------------------------------------------------------------------
# Generate all schedules
# ---------------------------------------------------------------------
sub fnGenerateSchedules() {
# Gamble jitter, which changes but is consistent for any given timestamp
srand($lEventSeedSeconds);
$fJitter = rand ($fJitterMax); # Notes
# Calculate thresholds, which jitter slightly
$fWakeThreshold = -0.33; # ie 66 pct 'duty cycle'
$fOnlineThreshold = 0.01; # pct of wake time online
$fActionThreshold = 0.13; # pct of online time acting
# Calculate 'master' schedule curve, with jitter
$fScheduleWave = $fJitter + $fWaveAmp * sin ($ClockHour / 12); # Norm: clockHour / 24
# Prep (crude sample) NPC schedule (These curves need serious work)
$fWakeWave = $fWakeThreshold ; # cludge for disp tho no longer waves
$fOnlineWave = $fOnlineThreshold;
$fActionWave = $fActionTreshold;
# New: Tests simple thresholds instead of waves
if ($fScheduleWave > $fWakeThreshold) { $Awake = 1; $sState = "Life"; } # Perl specific
if ($fScheduleWave > $fOnlineThreshold) { $Online = 1; $sState = "Online"; }
if ($fScheduleWave > $fActionThreshold) { $Acting = 1; $sState = "Action"; }
}
# ---------------------------------------------------------------------
# Generate only node/npc 'user' activity (in Java, this would be methods)
# ---------------------------------------------------------------------
sub fnGenerateUserActivity() {
srand (int($lEventSeedMinutes/2)); # Cludge: not hours, instead 'change apps no faster than 3m'
$sUserActivityText = "zzzz";
if ($Acting == 1) { # Only if user is supposed to be 'doing something'
$iUserActivity = 3 * int(rand(3)) + int(rand(3)); # 3 groups of 3
$sUserActionText = @UserActivities[$iUserActivity];
} else {
$iUserActivity = 0;
$sUserActionText = "na"; # Default
}
}
# ---------------------------------------------------------------------
# Perl-only test data
# ---------------------------------------------------------------------
sub fnLoadTestData() {
# Activities it is possible for NPCs/nodes to 'engage in'
# 3 types with 3 each - you responsible for unpacking properly
# Later: Users researched when/what, deliver as bounties, or blackmail!
# ---------------------------------------------------------------
@UserActivities = (
"CalcLater", # Mundane activities (note cool app name!)
"MrOffice",
"PainPoint",
"Facehole", # Questionable activities
"YouNoob",
"Cryptick",
"ShowUMine", # Blackmailable activities
"BackSniff",
"HireOut"
);
# Network types and states for convenient display
my @Types = ("Core", "Network", "Regional Gateway", "User Node");
my @NetStates = ("Up", "Fubar", "Fires", "Aliens", "Floods", "DOSed ");
# Network regions, 10-elm array parallel to @Nets (A perl test structure only)
@Regions = (
"Far East",
"Europe",
"Space",
"Monkey",
"Darknet",
"So Amer",
"No Amer",
"Near East",
"Aus",
"Arctic"
);
# Regional networks
@Nets = ( # Must be arranged parallel to Regions, in groups of 10
# Begin Far East network
"Tokyo, Japan", "Yokohama, Japan", "Beijing, China", "Dim Sum Heaven, China",
"Hong Kong, China", "Noodle Bar, Japan", "Thailand", "Awesome Nightclub",
"Taipei", "Korea",
# Begin Europe
"Paris", "Germany", "Bonn", "Amsterdam", "Prague", "Minsk", "Rome", "Belgium",
"Mark and Jeremy's", "London",
# Begin Space
"Mercury", "Venus", "Just Above Earth", "Mars", "Jupiter", "Venus", "Saturn",
"Uranus", "A Planetoid", "Not Pluto",
# Begin Monkey
"MR Labs 1", "MR Labs 2", "MR Labs 3", "MR Labs 4", "Kitchen 1", "Kitchen 2",
"Kitchen 3", "Security 1", "Security 2", "Gary's Office",
# Begin Darknet
"The Metal Shop", "The Horde", "The Hive", "CardersNet", "Spooksville", "D7",
"Nulled", "Anon", "D3adc0d3", "Treaclenet",
# Begin So Amer
"Brazil", "Peru", "Cabo", "Venezuela", "Santiago", "Panama", "Orozco",
"Mexico City", "The Farm", "Location 21",
# Begin No Amer
"New York", "Chicago", "Dallas", "New Orleans", "Denver", "San Francisco",
"Palo Alto", "Los Angeles", "San Diego", "Baja",
# Begin Near East
"Padagonia", "Sierra Leone", "North Africa", "Bangalore", "Bank of Leone", "411 Net",
"Guests of Nigerian Prince", "Bodhi Net", "Curry Heaven", "Awesome Beach",
# Begin Aus
"Wellington", "Sydnet", "Aukland", "Bartertown", "Queenstown", "Melbourne", "Thunderdome",
"Mel Gibson Theme Park", "Holden Dealership",
# Begin Arctic
"ColdNet 1", "ColdNet 2", "Under the Ice", "Outside Alien Crash", "Inside Alien Craft!",
"Very Cold Net", "Disused Barbeque", "Lost Sock Heaven", "BrrrrrrNet", "Winternet"
); # End array declaration
}