The original post: /r/php by /u/2019-01-03 on 2024-12-25 16:13:53.

https://github.com/hopeseekr/PrisonersDilemma

It uses phpexperts/php-evolver, which is a drop-dead simple way to create and use genetic algorithms in your own application.

What is the Prisoner’s Dilemma?

  • Two criminals commit a crime but there’s no evidence tying either to it.
  • Cops interrogate both separately.
  • If one rats on the other, they get no jail time and the other gets 5 years.
  • If both of them rat, they both get 2 years each.
  • If none of them rat, they will get the default 1 year.

In neither case do they know this beforehand, in most experiments.

I used phpexperts/php-evolver to test this with genetic algorithms, and it shows that overtime, populations will tend towards cooperation after about 120 generations.

Your partner's decision: They said nothing.  
Minimum Wage: $10 Outcomes:  
 \- You were convicted for 1 years.                Income: $40,000  
 \- Your partner was convicted for 1 years.        Income: $40,000  
Total Income: $80,000  
=== Generation 140 ===  
Protagonist Fitness: 120000

So how do you make a genetic algorithm in PHP? Well, most solutions are super hard, the phpexperts/php-evolver is pretty straight-forward.

First, you need to create the Creature (DNA interpretrations of the Genome) for your creatures. In Genetic Algorithm parlance, this is called “A Solution”.

class PeopleFitnessSolution extends Solution
{
    const TEMPERMENT_NAIVE = 0;
    const TEMPERMENT_SELFISH = 100;

    public function genome()
    {
        // Like all real-world genomes, most GA genomes aren't semantically indexed either.
        return [
            ['integer', -200, 200], // Action/Decision Weight; upper and lower bounds
        ];
    }

    public function evaluate($fitness)
    {
        if (!($fitness instanceof PersonFitnessScore)) {
            dd($fitness);
            throw new \LogicException('Something is messed up. Corrupted entity.');
        }

        return $fitness->getFitnessScore();
    }
}

You define the genome with weight ranges (in this case, where they are from an angel (-200) to psychopath (200)).

And then you tie in the genome with the FitnessScore evaluator.

I decided to solve the prisoner’s dilemma based upon how much minimum wage income could be gained (or lost) depending on how many years each convict spent in prison, basically $10,000/year free.

I used phpexperts/simple-dto to store the individual’s selfishness range, the collective’s earnings, and the individual’s earnings.

use PHPExperts\SimpleDTO\SimpleDTO;

/**
 * u/property float $individualWages
 * u/property float $communityWages
 * @property float $selfishWeight    1.0 == 100% selfish, don't care about community.
 *                                   > 1.0 == Sociopathic. Antipathy.
 *                                   < 0.0 == Pathologically altruistic (Group > Self)
 */
class PersonFitnessScore extends SimpleDTO
{
    public function getFitnessScore()
    {
        return $this->individualWages + ($this->communityWages -  ($this->communityWages * $this->selfishWeight));
    }
}

Next you need to design the game itself, or what the Creatures are going to do over and over again, generation after generation. These are called “Decisions” in GA parlance.

Each decision should produce a tangible, quantifiable result in the overall envrionment of the game, so that the creatures can be evaluated at each term as closer or further from the goal.

class SuspectDecision
{
    // @todo ADD support for "Lawyering up".
    public const CONFESS = 0;
    public const TESTIFY = 1;
    public const SILENCE = 2;

    // @todo: Research whether this should be a constant or evolvable.
    public const POSSIBLE_DECISIONS = [
        self::CONFESS => 'Confess',
        self::TESTIFY => 'Testify against your partner',
        self::SILENCE => 'Say nothing',
    ];

    public static function isValidDecision(int $decisionId): bool
    {
        return array_key_exists($decisionId, self::POSSIBLE_DECISIONS);
    }

    /**
     * Returns a third-person response for a chosen decision.
     */
    public static function getThirdPartyResponse(int $decisionId): string
    {
        $DECISIONS = [
            self::CONFESS => 'They confessed to everything',
            self::TESTIFY => 'They testified against you',
            self::SILENCE => 'They said nothing',
        ];

        if (!array_key_exists($decisionId, $DECISIONS)) {
            throw new \LogicException("Invalid Partner Decision: $decisionId");
        }

        return $DECISIONS[$decisionId];
    }
}

Then you need to create a Genome for each type of Creature. This will determine how the genes each express themselves. In this case, we only have one gene, selfishness, so it’s pretty simple.

class SuspectGenome
{
    public array $actions = [];
    public int $actionWeight = 0;
    public float $selfishWeight = 0;

    public function __construct(array $actions = [], int $actionWeight = 0, int $selfishWeight = 0)
    {
        $this->actions = $actions;
        $this->actionWeight = $actionWeight;
        $this->selfishWeight = $selfishWeight;
    }

    public function rememberAction(int $actionId, string $actionDescription)
    {
        // @todo: Should probably add error handling if the action doesn't actually exist.
        $this->actions[$actionId] = $actionDescription;
    }
}

The rest of the code, in the State directory, basically just is the programming of the game. Good Genetic algorithms will be playable games, usually, first. You can actually play the Prisoner’s Dilemma yourself using this app.

Putting it all into action is the evolve:classic Laravel command:

All beautiful code resembles stories, and the most beautiful code can be easily read and understood like a novel by people with no coding experience. This Command is a good example.

Three actors in the story:

        $interrogator = new Interrogator();
        $protagonist = new Suspect($protagonistGenome);
        $partner = new Suspect($antagonistGenome);

Two separate interrogations and their outcomes:

        $protagonistChoice = $interrogator->interrogate($protagonist);
        $partnerChoice = $interrogator->interrogate($partner);

A fourth actor comes in, the Judge.

        $criminalLaw = new PrisonSentence();
        $judge = new Adjudicator($criminalLaw);

Both convicts can be tried either individually or together, just like our current legal system.

        $convictions = $judge->issueSentence($protagonistChoice, $partnerChoice);

The judge reads out to the court the maximum possible sentence.

        $maxPossibleSentence = $criminalLaw->getMaxSentence();

Then someone in accounting calculates how much income you both would earn individually based upon your prison sentences of 0, 1, or 5 years.

        $yourIncome = IncomeCalculator::calculate($hourlyWage, $maxPossibleSentence - $convictions['you']);
        $antagonistIncome = IncomeCalculator::calculate($hourlyWage, $maxPossibleSentence - $convictions['partner']);

This is packaged into a GA FitnessScore:

        $yourFitnessDTO = new PersonFitnessScore([
            'individualWages' => $yourIncome,
            'communityWages'  => $yourIncome + $antagonistIncome,
            'selfishWeight'   => $protagonistGenome->selfishWeight,
        ]);

        $theirFitnessDTO = new PersonFitnessScore([
            'individualWages' => $yourIncome,
            'communityWages'  => $yourIncome + $antagonistIncome,
            'selfishWeight'   => $antagonistGenome->selfishWeight,
        ]);

SimpleDTO acts as a sort of police agent, in that it assures that the prison sentence and community wages and psycho profile are all carried out through the simulation.

Finally, this entire play is done again, and again and again for 500 generations, pausing 222 ms between rounds so the human proctor (you!) can observe it in action.

    for ($generation = 1; $generation <= 500; ++$generation) {
        /** @var PersonFitnessScore $yourFitnessDTO */
        /** @var PersonFitnessScore $theirFitnessDTO */
        [$convictions, $yourFitnessDTO, $theirFitnessDTO] = $this->runRound($protagonistGenome, $antagonistGenome);

        $myFitness = $yourFitnessDTO->getFitnessScore();
        $theirFitness = $theirFitnessDTO->getFitnessScore();

        $cp = new ConsolePainter();
        $this->line($cp->onRed()->white()->bold("  === Generation $generation ===  "));
        $this->line($cp->onDarkGray()->white()->bold("  Protagonist Fitness: $myFitness\n" . json_encode($protagonistGenome, JSON_PRETTY_PRINT)));
        $this->line($cp->onDarkGray()->red()->bold("  Antagonist Fitness: $theirFitness\n" . json_encode($antagonistGenome, JSON_PRETTY_PRINT)));

        usleep(222222);
    }

It uses phpexperts/console-painter to create very nice colorized displays.