+1(978)310-4246 credencewriters@gmail.com
  

Unit 10
Learn something new
1. Imagine a game world populated by players who encounter one another at random.
When an encounter occurs, each of the two players must decide whether to be ‘nice’
or ‘nasty’. If both are nice, they work together cooperatively to earn points and
receive 30 each. If both are nasty, they earn nothing. But if one is nice and the other
is nasty, the nasty player takes advantage and receives 50 points, while the nice one
​
loses 70 points.
What would you do in an encounter?
If the other player is going to be nice, you can be nice too and earn 30 points. On the
other hand, if you’re nasty in this case you’ll earn 50.
If the other player is going to be nasty, you’re in danger of losing 70 points. In this
case, too, it’s better to be nasty. You won’t score points, but at least you won’t lose
any.
It seems that, no matter what the other player does, you’re better off being nasty. On
the other hand, if he or she reasons the same way, you’ll both be nasty and earn
nothing. If only you’d both been nice, you could both have been 30 points better off.
This is only a game, but it reflects real conditions in a biological environment. Pure
self­interest would seem to be the best strategy for survival. An organism that tries to
work together with others is vulnerable to exploitation. But then, how can
cooperation evolve? Wouldn’t selfish individuals have a competitive advantage and
reproduce more successfully, choking out the cooperators over the course of multiple
generations?
In fact, though, it isn’t always true that evolution breeds selfishness. Dogs hunt
together in packs and bees work for the good of the hive. Individuals do cooperate
and get a larger payoff than they would have if each acted only for itself. So we have
a paradox. It doesn’t seem possible to account for cooperative behavior through the
evolutionary model of competitive advantages and yet evolution does seem to
produce it.
We can unravel the paradox by reproducing a famous set of computer simulations in
which players interact in the point­scoring encounters described above.1
2. We’ll be creating lots of players, so we’ll start with a class defining them.
1
For the classic report, see Robert
​
Axelrod’s ​The Evolution of Cooperation.
class Player:
idCounter = 0
def __init__(self):
self.score = 0
self.memory = {}
Player.idCounter += 1
self.name = ‘Player {0}’.format(Player.idCounter)
def processResult(self,otherName,myResponse,otherResponse):
result = [myResponse, otherResponse]
if otherName in self.memory:
self.memory[otherName].append(result)
else:
self.memory[otherName] = [result]
if myResponse == ‘nice’ and otherResponse == ‘nice’:
self.score += 30
elif myResponse == ‘nice’ and otherResponse == ‘nasty’:
self.score -= 70
elif myResponse == ‘nasty’ and otherResponse == ‘nice’:
self.score += 50
else:
self.score += 0
We’d like each player we create to have a unique name. For this purpose, we use a
class name, ​idCounter​, that starts at 0 and is increased by one for each new player.
The unique value of i​ dCounter​ at the time a player is created is incorporated into the
player’s name in the _​ _init__​ function, ensuring that no two players will have the
same name.
The ​__init__​ function also initializes the player’s score to 0 and creates an empty
dictionary that will serve as its memory of encounters with other players. The names
of the other players will be keys in the dictionary and the associated values will be
lists like the following example:
[[‘nice’, ‘nasty’], [‘nasty’, ‘nasty’]]
This means:
The first time I met this player I was nice and he was nasty.
The next time I was nasty and he was also nasty.
Whenever there is an encounter between two players, we’ll make a call to the
processResult​ method of each, passing in the name of the other player and how each
acted. The method then takes care of updating the score and memory. In this
function we’ve used an extended version of i​ f-else​ called the i​ f-elif-else​ statement.
This statement is handy when we want to choose between more than two possibilities
without using deeply nested i​ f-else​ statements. Here’s how the code would look
2
without ​elif​. This version also makes it easy to see that ​elif​ is a kind of contraction of
else​ and ​if​.
if myResponse == ‘nice’ and otherResponse == ‘nice’:
self.score += 30
else:
if myResponse == ‘nice’ and otherResponse == ‘nasty’:
self.score -= 70
else:
if myResponse == ‘nasty’ and otherResponse == ‘nice’:
self.score += 50
else:
self.score += 0
3. Our ​Player​ class is purposely missing an essential function—the one that determines
how an instance will act when it meets another player. Since this response function
will be different for different kinds of players, we’ll create specializations of our
generic P​ layer​ class for each kind. Instances will always be based on these
subclasses​, never on P​ layer​ itself. The purpose of P​ layer​ is to centralize code shared
by all of the different kinds of players we’ll be creating.
Our first subclass is for a ‘friendly’ player, one who is always nice in every
encounter.
class FriendlyPlayer(Player):
def __init__(self):
Player.__init__(self)
self.name += ‘ (friendly)’
def respondsTo(self, otherName):
return ‘nice’
The name for a friendly player is created in two steps. The call to ​Player.__init__
assigns a generic name like ‘Player 7’. Then the ​__init__​ function for ​FriendlyPlayer
appends to this, making the name ‘Player 7 (friendly)’.
The ​respondsTo​ method for every kind of player will take as an argument the name
of the encountered player, in case the response depends on previous experience with
this player. A friendly player, however, ignores the name passed in and always just
returns ‘​ nice’​.
Our second subclass of Player defines a ‘mean’ player, who is nasty in every
encounter. This is an easy variation of the friendly player and requires only
straightforward editing.
3
class ​Mean​Player(Player):
def __init__(self):
Player.__init__(self)
self.name += ‘ (​mean​)’
def respondsTo(self, otherName):
return ​’nasty’
4. Clearly we’ll want to introduce players based on more sophisticated strategies, but
already we can run some interesting experiments. In general, all of our experiments
will follow the same pattern. First we’ll create a population, a collection of players of
various kinds. Then we’ll allow them to encounter one another at random for some
time. At the end of this time, we’ll see how they scored. This gives us the results for
a single generation of players.
The next generation will be based—as in biological evolution—on the most
successful individuals of the previous generation. We’ll allow each of the players
scoring in the top half to produce two new players of the same kind for the next
generation. In that way, the total number of players in each generation will remain
the same, but the most successful ​kinds of players will proliferate. As an example,
suppose the scores in one generation are as follows:
Player 1 (mean)
700
Player 4 (friendly) 840
Player 6 (mean) 1200
Player 5 (friendly) 1350
Player 2 (friendly) 1620
Player 3 (mean) 1780
In this case, only the highlighted players will reproduce and the next generation will
contain four friendly players (two each from Players 5 and 2) and only two mean
players (from Player 3). Friendly players were more successful in one generation, so
there are more of them in the next.
We’ll need facilities for producing populations given specifications of the number of
each kind of player. Here’s a function that takes in a specification in this form
[[FriendlyPlayer, 4], [MeanPlayer, 8]]
and returns a population, in this case one containing four friendly players and eight
mean ones.
def makePopulation(specs):
population = []
for playerType, number in specs:
for player in range(number):
population.append(playerType())
4
return population
The header of the ​for​ statement uses multiple assignment. In Python we can write
x, y = 3, 4
to assign ​x​ to 3 and ​y​ to 4 at the same time. We can do the same thing with the
following code:
numbers = [3, 4]
x, y = numbers
If, as in this case, there is a single collection on the right side of an assignment and
multiple items separated by commas on the left side, Python will assign the left­hand
items to ​elements of the collection. Here, it assigns x
​ ​ and y
​ ​ to elements in the list
called n
​ umbers​.
In the case of our ​makePopulation​ function, items are taken one at a time from the
specs​ list. Then this item is used element­by­element in the multiple assignment.
For example, if
[MeanPlayer, 8]
is the current element taken from ​specs​, then ​playerType​ will refer to the class
MeanPlayer​ and n
​ umber​ to the integer 8​ ​. In the next­to­last line of the function,
playerType()
will have the same effect as if the code read
MeanPlayer()
That is, it will create a new mean player.
5. Next we need a function for carrying out one encounter between two players. The
code is just a matter of calling methods we have already defined. The first line of the
function body again uses multiple assignment.
def encounter(player1, player2):
name1, name2 = player1.name, player2.name
response1 = player1.respondsTo(name2)
response2 = player2.respondsTo(name1)
player1.processResult(name2, response1, response2)
player2.processResult(name1, response2, response1)
5
We also need a function for carrying out a large number of random encounters
between players in a population. This one uses a function called ​sample​ from the
random​ module that selects any given number of items at random from a collection.
In our case, we pick two members of the population at random for an encounter.
def doGeneration(population, numberOfEncounters):
for encounterNumber in range(numberOfEncounters):
players = random.sample(population, 2)
encounter(players[0], players[1])
Once all encounters in a single generation are complete, we’d like to get a report of
the results. Since these ought to be sorted by performance and since we’ll need the
same ordering to produce the next generation, we’ll write one function to do the
sorting and another to use the result to generate a report.
def sortPopulation(population):
scoreList = [[player.score, player.name, type(player)]
for player in population]
scoreList.sort()
return scoreList
def report(scoreList):
pattern = ‘{0:23s}{1:6d}’
for score, name, playerType in scoreList:
print(pattern.format(name, score))
In the first function, we start by producing a list of three­element lists, one for each
player. The first two elements are the player’s score and name. When we sort the
whole collection of three­element lists, the first element—the score—is used to
determine the order. Sometimes, though, two players will have the same score. In
this case, Python will use the second element—the name—to break the tie.
The third element is not of use to us yet, but we’ll need it in a moment when we want
to produce the next generation of players. The built­in function ​type​ returns the class
of its argument. So if p
​ layer​ is a friendly player, then t​ ype(player)​ will be
FriendlyPlayer​. This in turn can be used to create a new instance of the same kind.
If p
​ layer​ is a friendly player, then writing
type(player)()
is the same as writing
FriendlyPlayer()
6
which produces a new friendly player.
If we pass the result returned by ​sortPopulation​ to the ​report​ function, we get a
neatly formatted table of the player scores, from lowest to highest. In ​report​, we
once again use multiple assignment in the header of a f​ or​ statement.
6. Our last task before running an experiment is to write a function for producing the
next generation, based on the performance of players in the current one. The
sortPopulation​ function produces a list that contains all the information we need. We
just pass this list to the following function
def makeNextGeneration(scoreList):
nextGeneration = []
populationSize = len(scoreList)
scoreList = scoreList[int(populationSize/2):]
for score, name, playerType in scoreList:
for number in range(2):
nextGeneration.append(playerType())
return nextGeneration
This makes a new population, with two new individuals corresponding to—and of the
same type as—each individual in the top half of the current population. The only
tricky point is in the third line of the body of the function where we slice off the top
half of the list passed in. Dividing the current population size by two may yield a
floating point number—e.g. 15/2 is 7.5—but we need an integer to use in our slice.
We use the i​ nt​ function to make the appropriate conversion. Note that i​ nt​ is defined
so that i​ nt(7.5)​ is 7​ ​.2
7. Here now is complete code for our first experiment. We create a population of nine
friendly players and nine mean ones and watch what happens over five generations,
assuming there are 2,000 encounters in each generation.
import random
class Player:
idCounter = 0
def __init__(self):
self.score = 0
self.memory = {}
Player.idCounter += 1
2
We’ll use populations of even size to avoid this kind of rounding. Otherwise the next generation wouldn’t
be exactly the same size as the current one. For example, 15/2 is 7.5, making the next generation of size
2*7 or 14.
7
self.name = ‘Player {0}’.format(Player.idCounter)
def processResult(self, otherName,myResponse,otherResponse):
result = [myResponse, otherResponse]
if otherName in self.memory:
self.memory[otherName].append(result)
else:
self.memory[otherName] = [result]
if myResponse == ‘nice’ and otherResponse == ‘nice’:
self.score += 30
elif myResponse == ‘nice’ and otherResponse == ‘nasty’:
self.score -= 70
elif myResponse == ‘nasty’ and otherResponse == ‘nice’:
self.score += 50
else:
self.score += 0
class FriendlyPlayer(Player):
def __init__(self):
Player.__init__(self)
self.name += ‘ (friendly)’
def respondsTo(self, otherName):
return ‘nice’
class MeanPlayer(Player):
def __init__(self):
Player.__init__(self)
self.name += ‘ (mean)’
def respondsTo(self, otherName):
return ‘nasty’
def encounter(player1, player2):
name1, name2 = player1.name, player2.name
response1 = player1.respondsTo(name2)
response2 = player2.respondsTo(name1)
player1.processResult(name2, response1, response2)
player2.processResult(name1, response2, response1)
def makePopulation(specs):
population = []
for playerType, number in specs:
for player in range(number):
population.append(playerType())
return population
def doGeneration(population, numberOfEncounters):
for encounterNumber in range(numberOfEncounters):
players = random.sample(population, 2)
encounter(players[0], players[1])
def sortPopulation(population):
scoreList = [[player.score, player.name, type(player)]
for player in population]
scoreList.sort()
8
return scoreList
def report(scoreList):
pattern = ‘{0:23s}{1:6d}’
for score, name, playerType in scoreList:
print(pattern.format(name, score))
def makeNextGeneration(scoreList):
nextGeneration = []
populationSize = len(scoreList)
scoreList = scoreList[int(populationSize/2):]
for score, name, playerType in scoreList:
for number in range(2):
nextGeneration.append(playerType())
return nextGeneration
allPlayers = makePopulation([[FriendlyPlayer, 9],
[MeanPlayer, 9]
])
pattern = ‘*** Generation: {0} ***n’
for generationNumber in range(5):
doGeneration(allPlayers, 2000)
sortedResults = sortPopulation(allPlayers)
print(pattern.format(generationNumber+1))
report(sortedResults)
allPlayers = makeNextGeneration(sortedResults)
print()
The results are exactly what we’d expect. Mean players take advantage of the
friendly ones, outperform them and, in one generation, take over the world.
In a second experiment, we start with 17 friendly players and just one mean player.
The only change in the code is the specification of the initial population:
allPlayers = makePopulation([[FriendlyPlayer, 17],
[MeanPlayer, 1]
])
Now the process is slower, but the trend is still clear. By the end of five generations,
friendliness is on the brink of extinction. Starting with just one mean player is
enough to turn the whole world selfish.
Note also how the high scores decrease from generation to generation. As the pool of
friendly players shrinks, mean players have fewer opportunities to take advantage,
which is their only way of scoring. In the sixth generation, all players will be mean
9
and no one will ever score. Mean players take over the world, but the world they
dominate is a nasty one—it doesn’t profit even the winners.
8. Now let’s introduce a third kind of player. We’ll call this kind a ‘mirror’ player,
because it reflects the behavior of the players it encounters. If you’re nice this time to
a mirror player, it will be nice to you next time. If you’re nasty, it will be nasty back
next time. The first time it meets you, it gives you the benefit of the doubt and is
automatically nice. Here’s the code.
class MirrorPlayer(Player):
def __init__(self):
Player.__init__(self)
self.name += ‘ (mirror)’
def respondsTo(self, otherName):
if otherName in self.memory:
return self.memory[otherName][-1][1]
else:
return ‘nice’
As usual the name of the encountered player is passed to the ​respondsTo​ function.
This time, though, it’s used to look up the record of previous encounters with this
player. Recall that the value s​ elf.memory[otherName]​ will be a list like this:
[[‘nice’, ‘nasty’], [‘nasty’, ‘nasty’]]
The last element of this list, ​self.memory[otherName]​[-1]​, will be a record of the most
recent encounter, e.g. ​[‘nasty’, ‘nasty’]​. And the second element of this,
self.memory[otherName][-1]​[1]​,​ is how the other player acted in this encounter.
As a first experiment with this new kind of player, we’ll start with an initial
population consisting of six friendly players, six mean ones and six of the new mirror
players:
allPlayers = makePopulation([[FriendlyPlayer, 6],
[MeanPlayer, 6],
[MirrorPlayer, 6]
])
The results are striking. When the mirror player meets either a friendly player or
another mirror player, both are nice and they cooperate to win 30 points each. When
a mirror player meets a mean player, the mean player takes advantage of it in the first
encounter, but thereafter the mirror player is nasty back and doesn’t continue to lose
points.
10
Mirror players do well with nice players and mirror players—two­thirds of the
population. Mean players do well only with nice players. As these disappear from
the population, they have no one left to exploit and they themselves die out.
What’s left is a population of mirror players who are nice to each other. They work
together rather than relying on exploitation. Evolution favors cooperation!
We’ve already seen that just one mean player can spoil a world of friendly ones.
How about if we introduce a mirror player as well as the one bad apple? Here’s a
second experiment.
allPlayers = makePopulation([[FriendlyPlayer, 16],
[MeanPlayer, 1],
[MirrorPlayer, 1]
])
Again, cooperation wins out.
Finally, suppose the world is initially full of mean players. What happens if we
introduce just a few mirror players?
allPlayers = makePopulation([[MeanPlayer, 14],
[MirrorPlayer, 4]
])
This is an especially interesting experiment. Try running it at least 10 times. You
should find that it’s possible for two very different worlds to result from the same
initial conditions.
11

Purchase answer to see full
attachment

  
error: Content is protected !!