Richer
I’ve been pecking away at the little simulation. Here’s a report on status. We get some early results: the rich get richer.
- Full Disclosure
- There is a serious defect in the code, which I’ll discover three articles from now. The effect of the defect is that certain players are more likely to win than others: the game isn’t fair. As we’ll see, all that really does is speed up the rich getting richer. They still win all the chips even with the defect fixed.
-
For extra credit, see if you can spot the defect before I did. With that hint it should be easy.
I was just fiddling a bit while half-listening to a conversation. Here’s where I am now:
screen_size = 1024
class PersonView:
def __init__(self, x, y):
self.pos = Vector2(x, y)
v = 1.0 if random.random() > 0.5 else -1.0
theta = random.uniform(0, 2*math.pi)
self.vel = Vector2(v * math.cos(theta), v * math.sin(theta))
self.radius = 10
self.width = 10
def colliding(self, aPersonView):
dist = self.pos.distance_to(aPersonView.pos)
return dist <= self.radius + aPersonView.radius
def move(self):
self.pos += self.vel
if self.pos.x < self.width or self.pos.x > screen_size - self.width:
self.vel = Vector2(-self.vel.x, self.vel.y)
if self.pos.y < self.width or self.pos.y > screen_size - self.width:
self.vel = Vector2(self.vel.x, -self.vel.y)
def draw(self, screen):
pygame.draw.circle(screen, "red", self.pos, self.radius, 0)
class Game:
def __init__(self):
pygame.init()
pygame.display.set_caption("Wealth")
self.screen = pygame.display.set_mode((screen_size, screen_size))
self.clock = pygame.time.Clock()
self.people = []
self.populate()
def populate(self):
for i in range(100):
self.add_person(self.people)
def add_person(self, people):
safe = False
margin = 20
while not safe:
safe = True
x = random.uniform(0 + margin, screen_size - margin)
y = random.uniform(0 + margin, screen_size - margin)
trial = PersonView(x, y)
for person in people:
if person.colliding(trial):
safe = False
people.append(trial)
def main_loop(self):
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
screen = self.screen
screen.fill("midnightblue")
self.check_collisions()
for pv in self.people:
pv.move()
for pv in self.people:
pv.draw(screen)
self.clock.tick(60)
pygame.display.flip()
pygame.quit()
def check_collisions(self):
pairs = itertools.combinations(self.people, 2)
for p1, p2 in pairs:
if p1.colliding(p2):
p1.vel = -p1.vel
p2.vel = -p2.vel
I’ve doubled the screen size, have the balls moving and detecting collisions. They’re not doing the right thing about collisions, they just reverse direction. It looks like this:
The Next Day …
The correct deflection angles are an interesting momentum equation that I’ll have to look up and code up. For purposes of the simulation, that doesn’t matter. What matters is that on each collision, the Persons do a transaction, and the radius of each person should reflect his wealth. I think that, ideally, we’d have area proportional to wealth, and radius would be proportional to the square root of wealth. If that doesn’t look good, we’ll fix it up.
Let’s associate a Person with each PersonView:
We currently have this:
class Game:
def add_person(self, people):
safe = False
margin = 20
while not safe:
safe = True
x = random.uniform(0 + margin, screen_size - margin)
y = random.uniform(0 + margin, screen_size - margin)
trial = PersonView(x, y)
for person in people:
if person.colliding(trial):
safe = False
people.append(trial)
Let’s just create a Person right here and give it to the PersonView:
class Game:
def add_person(self, people):
safe = False
margin = 20
person = Person()
while not safe:
safe = True
x = random.uniform(0 + margin, screen_size - margin)
y = random.uniform(0 + margin, screen_size - margin)
trial = PersonView(person, x, y)
for person in people:
if person.colliding(trial):
safe = False
people.append(trial)
class PersonView:
def __init__(self, person, x, y):
self.person = person
self.pos = Vector2(x, y)
v = 1.0 if random.random() > 0.5 else -1.0
theta = random.uniform(0, 2*math.pi)
self.vel = Vector2(v * math.cos(theta), v * math.sin(theta))
self.radius = 10
self.width = 10
I have no idea why I’m storing both radius and width. This is the sort of thing that happens when we are just banging out an experimental little program, and I am inclined to clean it up a bit as I go. I find that even with a one-off program, I often come back to it, and even if I don’t debugging a messy program takes me longer than cleaning up the mess.
I’ll run the program once this morning, just because I haven’t seen it yet. It looks the same. So far so good.
We want the View to ask the Person for something, from which it will compute the radius. The Person knows its wealth, so that’s what we’d better ask. Let’s first fix up the radius/width issue. We’ll settle on radius.
However, we no longer what radius to be a member of the view, we want to compute it. In Python we might make it a property. Let’s do that.
@property
def radius(self):
return 10
Still good. Now we need to do some arithmetic. Better yet, let’s do some testing.
def test_radius(self):
p = Person()
v = PersonView(p, 0, 0)
assert v.radius == 10
This passes, of course. Now let’s do some arithmetic. We intend Person.wealth
to equate to area of the ball. Area, if I recall kindergarten, equals PI times radius squared. So if area is 1000, radius, obviously, is 17.84. We could let it come up with 17.84 and then scale it.
Change the test:
def test_radius(self):
p = Person()
v = PersonView(p, 0, 0)
assert v.radius == pytest.approx(10.0)
We’re going to be using floats, so we need to do the approx thing.
@property
def radius(self):
wealth = self.person.wealth
r = math.sqrt(wealth/math.pi)
return r
This had better fail saying that 17.84 isn’t 10.
Expected :10.0 ± 1.0e-05
Actual :17.841241161527712
Don’t you just love it when a plan comes together?
Fix the code.
class PersonView:
def __init__(self, person, x, y):
self.person = person
self.pos = Vector2(x, y)
v = 1.0 if random.random() > 0.5 else -1.0
theta = random.uniform(0, 2*math.pi)
self.vel = Vector2(v * math.cos(theta), v * math.sin(theta))
self.adjust_radius = 1 / math.sqrt(1000/math.pi)
@property
def radius(self):
wealth = self.person.wealth
r = math.sqrt(wealth/math.pi)
return r * 10 * self.adjust_radius
We can clean this up a bunch:
class PersonView:
def __init__(self, person, x, y):
self.person = person
self.pos = Vector2(x, y)
v = 1.0 if random.random() > 0.5 else -1.0
theta = random.uniform(0, 2*math.pi)
self.vel = Vector2(v * math.cos(theta), v * math.sin(theta))
self.adjust_radius = 10 / math.sqrt(1000/math.pi)
@property
def radius(self):
return math.sqrt(self.person.wealth / math.pi) * self.adjust_radius
So that’s good. Now, when we find two balls colliding, let’s have their persons do the transaction.
class Game:
def check_collisions(self):
pairs = itertools.combinations(self.people, 2)
for p1, p2 in pairs:
if p1.colliding(p2):
p1.vel = -p1.vel
p2.vel = -p2.vel
Just the right spot.
def check_collisions(self):
pairs = itertools.combinations(self.people, 2)
for p1, p2 in pairs:
if p1.colliding(p2):
p1.transact(p2)
p1.vel = -p1.vel
p2.vel = -p2.vel
I think this should have the effect we want. Run it. Something bad happens.
When I init the views, I have a confusion between the person and the view:
def add_person(self, people):
safe = False
margin = 20
person = Person()
while not safe:
safe = True
x = random.uniform(0 + margin, screen_size - margin)
y = random.uniform(0 + margin, screen_size - margin)
trial = PersonView(person, x, y)
for person in people:
if person.colliding(trial):
safe = False
people.append(trial)
I wish you had pointed that out when I wrote it. I fix that and then another defect:
def add_person(self, person_views):
safe = False
margin = 20
person = Person()
while not safe:
safe = True
x = random.uniform(0 + margin, screen_size - margin)
y = random.uniform(0 + margin, screen_size - margin)
trial = PersonView(person, x, y)
for view in person_views:
if view.colliding(trial):
safe = False
person_views.append(trial)
def check_collisions(self):
pairs = itertools.combinations(self.people, 2)
for p1, p2 in pairs:
if p1.colliding(p2):
p1.person.transact(p2.person)
p1.vel = -p1.vel
p2.vel = -p2.vel
That should be enough to show the effect, and it is. Here’s a series of screen shots taken over a few minutes of the program running:
Wealth differences appear quickly:
The wealth differences soon look substantial:
Very quickly, we see immense differences:
After only a few minutes, the differences are profound!
Reflection
The Rich Get Richer
Well, first of all in this simulated market where we all just go around doing two-party bets of ten percent of the lesser wealth of the two parties, that is sufficient to create super-rich and super-poor individuals. The rich really do get richer, at least in this over-simplified world. At the end of this series, I’ll try to think about this, but believe me, my knowledge of how to fix the economics of the world is not up to the task.
I’ll wait before adding another video, because the process is slower than we’d like to see here, in part because there are lots of very tiny dots floating around gaining and losing a tiny amount of wealth, with the effect that the larger objects don’t collide often. I’m not sure how to deal with that, and there are other issues:
When a transaction takes place, it appears that sometimes the growth of the winner is enough to cause it to collide immediately with the loser, with the effect that the winner stays where it is. I’m not sure why that happens, but the effect certainly is that the velocity of a player sometimes goes to zero or nearly zero. If asked, I would have said that we only ever reverse the velocity of the players, so something must be wrong with my understanding.
As you can see in at least one of the pictures, the larger players can overlap the edge of the screen. I’m pretty sure that’s because we are checking against a fixed distance from the edge, instead of considering the radius of the player.
And, as one would expect in a little exercise like this, the code is pretty ragged. Before we do our final publication, we’ll want to improve the code as much as we can.
For now, I think I’ll take a well-deserved break and make my morning chai. I was so excited to get here that I put that off.
Summary
The rich get richer, clearly unfairly so. This little simulation certainly isn’t a complete simulation of the real economy, but it is certainly evocative enough to make us tilt our heads and wonder what could be done to make things more equitable. I’m pretty sure that communism isn’t the answer: it’s far too subject to corruption of its own type. But somehow, as a society, we might well want to counter this tendency for a few people to accrue all the goodies. There should be goodies for everyone, more or less equitably, it seems to me.
For now, that’s all I’ve got.
See you next time!