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:

ss0

The wealth differences soon look substantial:

ss1

Very quickly, we see immense differences:

ss2

After only a few minutes, the differences are profound!

ss3

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!