NAME

Game::PseudoRand - pseudo random distribution functions

SYNOPSIS

use Game::PseudoRand qw(prd_step prd_table);

( $coinflip, $reset ) = prd_step( start => 0.25, step => 0.41675 );

$coinflip->() and say "maybe heads";      # 25%
$coinflip->() and say "likely heads";     # 25% + 42%
$coinflip->() and say "heads";            # 25% + 42% * 2
$coinflip->() and say "maybe heads";      # 25%
$reset->();
$coinflip->() and say "maybe heads";      # 25%

( $randfn, undef ) = 
  prd_table( start => 0.12, table => [ 0.05, 0.05, 0.1 ] );

$food = ( prd_bistep(
  start     =>  0.5,
  step_hit  => -0.4,
  step_miss =>  0.1
) )[0]; 

... = prd_bitable(
  start      => 0.5,
  table_hit  => [ -0.1, -0.2 ],
  table_miss => [  0.1,  0.2 ]
)

DESCRIPTION

This module creates Pseudo Random Distribution (PRD) random functions. For example one may construct a function that returns true 25% of the time but one with a rather different distribution than from a typical random function: typically a distribution with much lower odds of long runs of misses or (maybe) long runs of hits.

Random values are assumed to be in the same range as the rand builtin accepts, or

0.0 <= r < 1.0

though the odds used in the functions generated below can go above or equal to 1.0 (always true or a hit) or below 0.0 (always false or a miss).

FUNCTION GENERATING FUNCTIONS

These return a pair of functions, the first being the random function to call for a true or false value (1 or 0 to be exact), and the second a function that will reset the internal state of the closure back to the starting values. If something goes wrong an exception will be thrown.

prd_step

Increases the starting odds by step-odds each miss, resets to reset or start odds on hit. Optional keys rand (for a custom random function, should return values in the same range as the builtin rand) and reset (odds to reset to, if different from the start odds).

Use this for effects that need to be frequently activated but where a predictable cool-down timer is unsuitable.

Players may quickly realize and become annoyed by things that always miss on the first try, so maybe only make start slightly lower than the reset value when imposing a first use penalty. On the other hand, beginner's luck can be a thing. One way to (maybe) apply beginner's luck would be to randomize the starting odds.

prd_step( start => rand(), step => 0.1, reset => 0.0 );
prd_bistep

Bidirectional step odds function generator. On hit adds the (ideally negative) step_hit value to the odds. On miss adds the (ideally positive) step_miss value to the odds.

prd_bistep( start => 0.5, step_hit => -0.1, step_miss => 0.1 )

This produces "rubber band" odds that increase during periods of bad luck but decrease during periods of good luck; this may suit the generation of critical resources such as food that there should not be too much or too little of.

Also accepts rand for a custom RNG function. There is no reset as the odds are always adjusted by the step values after hit or miss.

prd_table

Increases the odds by the value given in table at the optional index index on miss; resets to reset or start on hit, and also resets the index to 0 on hit. Optional keys index (custom starting index, default is 0) and as for prd_step rand and reset.

prd_table( start => 0.12, table => [ 0.05, 0.05, 0.1, 0.1 ] )

Unlike prd_step the table allows for non-linear odds changes, and can wrap around back to the beginning instead of always eventually adding up to a value greater than 100%; this means prd_table can permit longer runs of misses, depending on the table used.

prd_bitable

Uses a different table lookup for when there is a hit or a miss, and adjusts the odds by that value. Resets the index pointer of the miss or hit to 0 on a hit or miss.

... = prd_bitable(
  start      => 0.5,
  table_hit  => [ -0.1, -0.2 ],
  table_miss => [  0.1,  0.2 ]
)

Accepts a custom rand function.

STATISTICS 101

prd_step( start => 0.25, step => 0.41675 )

was above claimed without evidence to be (roughly) a coinflip; what this tests for is whether a random value is below 0.25, 0.66, and then 1.08. This means the distribution of runs will be rather different from the output of a random function:

$ cd eg
$ ./trial | ./runstats
49.9610
1  53309
2  19578
3  1670
4  435
5  109
6  27
7  11
$ perl -E 'say rand > 0.5 ? 1 : 0 for 1..1e5' | ./runstats          
1  24967
2  12459
3  6180
4  3238
5  1597
6  742
7  380
8  197
9  88
10 53
11 25
12 11
13 8
14 1
15 1
16 2
17 2
21 1

The trial coinflip only managed a run of 7 (and it was a run of activations, not of misses) while the unbiased coinflip using the same rand had a run of 21 hits or misses in a row, and greater odds of runs of two to seven hits or misses in a row compared to the PRD.

Note that for prd_step an prd_table if the start odds are set too high there may be runs of activations; prd_step and prd_table typically only minimize runs of misses and thus may activate too often. This can be avoided (especially with prd_table) by setting low initial odds so that the odds of a miss are very high after a hit, though this then may require a large increase in odds to keep the variance low, if that is also desired.

BUGS

Please report any bugs or feature requests to bug-game-pseudorand at rt.cpan.org, or through the web interface at http://rt.cpan.org/NoAuth/ReportBug.html?Queue=Game-PseudoRand.

Patches might best be applied towards:

https://github.com/thrig/Game-PseudoRand

SEE ALSO

https://github.com/thrig/ministry-of-silly-vaults/ contains a discussion and example code under the pseudo-random-dist directory.

Various alternative random modules to the (bad, but fast) builtin rand call exist on CPAN, e.g. Math::Random::PCG32, among others.

AUTHOR

thrig - Jeremy Mates (cpan:JMATES) <jmates at cpan.org>

COPYRIGHT AND LICENSE

Copyright (C) 2019 by Jeremy Mates

This program is distributed under the (Revised) BSD License: http://www.opensource.org/licenses/BSD-3-Clause