Let’s Program A Prisoner’s Dilemma Part 2: Crime And Punishment

Last time we learned about a game theory thought experiment called the “Iterated Group Prisoner’s Dilemma”. Basically players get randomly paired up and have to decide to either cooperate by accepting a small penalty for themselves or defect by shoving a big penalty onto the other player. The players then get shuffled around and repeat the game with a new partner. After a few hundred rounds (or more) the game comes to a stop and the winner is whoever has lost the fewest points.

This time we’re going to start programing a simulator for that game so we can watch how different strategies play out.

I will be writing this project in Ruby. It’s not really “better” for this program than any of the various other high level languages we could choose from, but I’ve been using it a lot as part of my game programming hobby and figured I might as well stick with it while it was in my head.

So if you want to follow along with my exact code open up a file named prisonerdilemma.rb in your favorite text editor and get ready for some Ruby. Of course, rewriting the project in your favorite language is always an option.

Introducing Prisoner 24601

The most important part of the prisoner’s dilemma is the prisoners. So let’s make a list of what our little digital prisoners need to be able to do in order for this simulation to work.

1) They need to have a unique ID so that other prisoners (and us humans) can recognize them.

2) They need to be able to decide whether to cooperate or defect with other prisoners based on ID.

3) They need to be able to see whether their opponent decided to cooperate or defect.

4) They need to be able to report what strategy they are using so we can keep track of how different strategies perform.

5) They need to be able to keep track of their score so we can calculate who is winning and losing.

Based on all this I’ve thrown together a very simple prisoner class for us to start working with. It can’t really do much except book keeping though. It has variables to keep track of it’s own ID, score and strategy and even exposes the score to outside code so we can update it during the game. It also has a convenient “report” function that will print out all that information in human friendly format.

As for actual game playing code… it does technically have a “cooperate?” function for deciding how to play the game along with a “learnResults” function for keeping track of its opponents. But at the moment both of these functions are empty because this is just skeleton code; it isn’t meant to actually do anything. In fact, if you try to call the “cooperate?” function it will throw an error!

class Prisoner
   attr_reader :id, :strategy, :score
   attr_writer :score

   def initialize(id)
      @id = id;
      @score = 0;
      @strategy = "Prisoner Parent Class"
   end

   def cooperate?(opponentID)
      throw "Prisoner child class must implement a cooperate? method"
   end

   def learnResults(opponentID,oponentCooperated)
   end

   def report
      puts "ID: #{@id} Score: #{@score} Strategy: #{@strategy}"
   end
end

Still, even with two incomplete functions we can still give this a test run. If you were to add some code like this to your project:

testprisoner = Prisoner.new(6)
testprisoner.score = -10
testprisoner.report

You would get some output like this:

ID: 6 Score: -10 Strategy: Prisoner Parent Class

Ye Not Guilty – The “Saint” Strategy

Now that we’ve got a nice generic class up and running we can move on to writing a prisoner with actual dilemma solving code!

For our first attempt we’re going to be writing the simplest possible of all prisoner algorithms: A player who always always chooses to cooperate. We’ll be calling this the “Saint” strategy.

Now the actual code for this will be virtually identical to the generic prisoner. It will just have a different name inside of the “strategy” variable and its “cooperate” function will return true instead of throwing an error.

But we all know that copy pasting code is horrible and hard to maintain, so we’ll be using the miracle of inheritance to avoid any copy pasting.

class Saint < Prisoner
   def initialize(id)
      super(id)
      @strategy = "Saint"
   end

   def cooperate?(opponentID)
      return true
   end
end

That “Saint < Prisoner” tells the compiler that Saint is a type of Prisoner and should get a free copy of all of its code. Then instead of writing an entire new class from scratch we only have to write the few bits that are different.

First up we write a new initialize function so we can set the strategy variable to “Saint” instead of the default from the Prisoner class. But we still want the Prisoner class to handle things like setting our ID and initializing our score so we start out by calling the generic Prisoner initialize function with a call to “super”.

The super call checks whether the parent has a function with the same name as the current function and then calls it. It’s really useful for when you want a child function to do everything the parent did and then a little extra. Just start with super to get the parent’s behavior and then add in the unique child logic afterwards.

Next we write a new cooperate? function, which for our super trusting saint just involves returning “true”. In this case we want to completely override the parent’s version of the function so we leave out the call to super and just write new code.

Father of Lies

That was pretty easy, so let’s keep going by programming a second type of prisoner that perfectly mirrors our first. This time we’ll create a player that always always chooses to defect. Let’s call this the “Devil” strategy.

class Devil < Prisoner
   def initialize(id)
      super(id)
      @strategy = "Devil"
   end

   def cooperate?(opponentID)
      return false
   end
end

Just like with the “Saint” we use inheritance to grab our basic Prionser code and then just add in our new strategy name and cooperate? Algorithm. Couldn’t be easier.

A Not So Epic Clash Between Good And Evil

And now here’s a little code snippet you can use for testing that both of our new classes work:

testSaint = Saint.new(1)
testSaint.report
testDevil = Devil.new(2)
testDevil.report
puts "Saint Cooperates?"
puts testSaint.cooperate?(testDevil.id)
puts "Devil Cooperates?"
puts testDevil.cooperate?(testSaint.id)

Which results in this output:

ID: 1 Score: 0 Strategy: Saint

ID: 2 Score: 0 Strategy: Devil

Saint Cooperates?

true

Devil Cooperates?

False

Let’s Get Ready To Rumble

Now that we have some players assembled our next task will be setting up an actual game for them to play in. But that will have to wait until the next post.