Simulation or Sermon?
We recover quickly from a mistake last night, and get a nice histogram. I’m surely reading too much into this little simulation. Or am I?
- Full Disclosure
- There is a serious defect in the code, which I’ll discover in the next aerticle. 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.
Last night I was just fiddling with some parameters and then rolled them back. Unfortunately, I had not committed the morning’s final code. Bad Ron, no biscuit.
Right, so now I’ve lost the good changes. Lesson re-learned, commit on green. Fortunately, PyCharm has this Local History thing which may be just the ticket.
Yes, it is. Essentially it shows the history of every change back for some insane amount of time, certainly all the changes I made yesterday around 8 AM. It displays a diff, and, as with any PYCharm diff, you can just click “»” and apply a past change. So, basically, I backed up to 0803 yesterday, which was when I finished yesterday’s morning work.
I’ve just shared the project on GitHub, and will put the link at the top of the article.
Changes of note:
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
v = v * random.uniform(0.8, 1.2)
theta = random.uniform(0, 2*math.pi)
self.vel = 4 * Vector2(v * math.cos(theta), v * math.sin(theta))
self.adjust_radius = 10 / math.sqrt(1000/math.pi)
The initial velocity is now set to a random speed between 0.8 and 1.2, either positive or negative and it is then rotated to a random angle between zero and two pi. That’s more random than we need, isn’t it? We can eliminate the pos/neg, since the angle handles that.
class PersonView:
def __init__(self, person, x, y):
self.person = person
self.pos = Vector2(x, y)
v = random.uniform(0.8, 1.2)
theta = random.uniform(0, 2*math.pi)
self.vel = 4 * Vector2(v * math.cos(theta), v * math.sin(theta))
self.adjust_radius = 10 / math.sqrt(1000/math.pi)
A quick run tells me that works fine. Commit: simplify random direction.
Further on down, we have this:
def draw(self, screen):
wealth = self.person.wealth
if wealth < 250:
color = "red"
elif wealth < 500:
color = "yellow"
else:
color = "cyan"
pygame.draw.circle(screen, color, self.pos, max(4, int(self.radius)), 0)
That just gives them the nice bright colors. We have some changes in Game:
class Game:
def main_loop(self):
running = True
moving = False
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
keys = pygame.key.get_pressed()
if keys[pygame.K_SPACE]:
moving = True
screen = self.screen
screen.fill("midnightblue")
if moving:
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()
Here we have the change to only start moving when I hit space. That lets me set up for recording the screen, which I might do again, although at 20 meg a crack, I suspect folks won’t be looking often. Maybe I can find a smaller way to do that.
def check_collisions(self):
pairs = itertools.combinations(self.people, 2)
for p1, p2 in pairs:
if p1.colliding(p2) and p1.person.wealth > 100 and p2.person.wealth > 100:
p1.person.transact(p2.person)
p1.vel = -p1.vel
p2.vel = -p2.vel
p1.move()
p2.move()
- Added in Post
- We get an idea, think, try, and change direction. Change is good when it is collaborative.
Here is the code that makes the folx with 100 or lower wealth drop out of the game. I think that this morning’s exercise will be to have them literally drop out.
Now a cute idea. When a Person’s wealth gets to 100 or less, they no longer participate in exchanges. I did that so that the oligarchs would interact more frequently with each other. With a half dozen oligarchs and over ninety little guys, the little guys seemed to keep the oligarchs from interacting with each other.
Turning that off and viewing it again, I don’t think that makes much difference. Belay that change and commit: poor people still transact.
def check_collisions(self):
pairs = itertools.combinations(self.people, 2)
for p1, p2 in pairs:
if p1.colliding(p2): # and p1.person.wealth > 100 and p2.person.wealth > 100:
p1.person.transact(p2.person)
p1.vel = -p1.vel
p2.vel = -p2.vel
p1.move()
p2.move()
Let’s add more randomness in moves. To support that, let’s extract a method to set a random velocity, from the init:
class PersonView:
def __init__(self, person, x, y):
self.person = person
self.pos = Vector2(x, y)
v = random.uniform(0.8, 1.2)
theta = random.uniform(0, 2*math.pi)
self.vel = 4 * Vector2(v * math.cos(theta), v * math.sin(theta))
self.adjust_radius = 10 / math.sqrt(1000/math.pi)
Extract method:
class PersonView:
def __init__(self, person, x, y):
self.vel = None
self.person = person
self.pos = Vector2(x, y)
self.set_random_velocity()
self.adjust_radius = 10 / math.sqrt(1000/math.pi)
def set_random_velocity(self):
v = random.uniform(0.8, 1.2)
theta = random.uniform(0, 2 * math.pi)
self.vel = 4 * Vector2(v * math.cos(theta), v * math.sin(theta))
PyCharm is happier if you make a space for a member in init, so I let it add the None thing.
Now we can call that method at will. And my will is that we’d like each little dude to change direction every few seconds. We’re running at 60 frames per second, so let’s say a one in 200 chance of a direction change.
def move(self):
if random.random() < 0.005:
self.set_random_velocity()
self.pos += self.vel
if self.pos.x < self.radius or self.pos.x > screen_size - self.radius:
self.vel = Vector2(-self.vel.x, self.vel.y)
if self.pos.y < self.radius or self.pos.y > screen_size - self.radius:
self.vel = Vector2(self.vel.x, -self.vel.y)
I like the look of that. Commit: folx change direction occasionally.
Did you notice that I have completely dropped the idea of having the little folx drop out? They continue to scurry around, trying to make a living. Most of them will find their wealth getting smaller and smaller. Very rarely, I suppose, one might hit a lucky streak and get back to yellow, though I have never seen it happen.
- Added in Post
- We see here the hatching of a small idea that quickly turns very nice.
What might we do now? We could extract some summary information. Can we have more than one screen in our game? Quick test tells me no, and a search confirms. OK, we’ll do this more cleverly.
I am feeling the need for some tests on stats, but let’s hold off. This is still just a toy script after all.
I bang this in, just to get the hang of it all:
...
for pv in self.people:
pv.draw(screen)
self.statistics(self.people, screen)
self.clock.tick(60)
pygame.display.flip()
pygame.quit()
def statistics(self, views, screen):
richest = views[0].person.wealth
poorest = richest
for v in views:
w = v.person.wealth
richest = max(richest, w)
poorest = min(poorest, w)
text = f'Min: {poorest:.0f} Max: {richest:.0f} ({richest/1000:.0f}%)'
score_surface = self.score_font.render(text, True, "green")
screen.blit(score_surface, (100, 300))
With this result:
Swell, after a while, one dude has 46 percent of all the money in the world. Does this seem fair to you? It doesn’t seem fair to me.
- Added in Post
- This simple next step makes some space on the screen. The space attracts a good idea below.
OK, we see roughly how to do this. Let’s partition off part of the screen to provide this kind of info.
screen_size = 750
stats_space = 100
class Game:
def __init__(self):
pygame.init()
pygame.display.set_caption("Wealth")
self.screen = pygame.display.set_mode((screen_size, screen_size + stats_space))
self.clock = pygame.time.Clock()
self.people = []
self.populate()
if pygame.get_init():
self.score_font = pygame.font.SysFont("arial", 32)
def statistics(self, views, screen):
pygame.draw.line(screen, "green", (0, screen_size), (screen_size, screen_size))
richest = views[0].person.wealth
poorest = richest
for v in views:
w = v.person.wealth
richest = max(richest, w)
poorest = min(poorest, w)
text = f'Min: {poorest:.0f} Max: {richest:.0f} ({richest/1000:.0f}%)'
score_surface = self.score_font.render(text, True, "white")
screen.blit(score_surface, (20, screen_size + stats_space*0.5))
Give us this:
Let’s commit this as a reasonable save point: initial stats at bottom of screen.
- Added in Post
- A histogram might have seemed difficult earlier on today. Now that there is this nice empty space on the screen, it seems easier.
I think what I’d really like to have, though, is a histogram showing the distribution of wealth from low to high.
Let’s begin by just extracting all the wealth values, as that is what we need. This will improve the method a bit.
def statistics(self, views, screen):
pygame.draw.line(screen, "green", (0, screen_size), (screen_size, screen_size))
wealths = sorted([view.person.wealth for view in views])
richest = wealths[-1]
poorest = wealths[0]
for v in views:
w = v.person.wealth
richest = max(richest, w)
poorest = min(poorest, w)
text = f'Min: {poorest:.0f} Max: {richest:.0f} ({richest/1000:.0f}%)'
score_surface = self.score_font.render(text, True, "white")
screen.blit(score_surface, (20, screen_size + stats_space*0.5))
Commit: refactoring.
I plan to use the full 200 height of the stats window for my histogram. Then we’ll add a bit more space to it to make it look good and to parameterize some magic numbers that we will have. Hm, but with only 750 across, I don’t have room for all the folx. For now we’ll skip the smallest 750. The picture will be the same.
What a dolt I am. I was thinking there were 1000 points: there are 100, not 1000. Here’s what I’ve got:
- Aside
- The code that follows was, I freely grant, created by just hammering until I got a picture that I can barely accept. I have not found it productive to do much hand calculation of things like graphs. YMMV: a sketch and some numbers might be just what you need. Me, I usually just start flinging lines on the screen.
def statistics(self, views, screen):
...
scale = stats_space / richest
x = 0
min_height = 2
top = screen_size
for w in wealths:
x_pos = x
x += 7
height = w * scale
if height < min_height: height = min_height
y = screen_size + stats_space - height
pygame.draw.rect(screen, "white", (x_pos, y, 5, height))
This needs work to make it nice but the big picture is pretty clear: a very few people have all the money.
One interesting thing might be to show on the histogram where 90 percent or 95 percent of all the money is. We are, of course, perilously close to converting our not particularly precise little simulation into a sermon, but, hey, tomorrow is Easter and from where I sit, the world needs a lot of work on inequality and inequity.
- Lessons Learned?
- Well, how about checking to see the commit status before starting a new session, on the off chance (p > 0.4) that I’ve forgotten to commit something. But yay, PyCharm, Local History.
-
And small steps. Drawing the numbers in the middle of the screen gave me a short path to some statistics. Making a place for the numbers made an eye-caching area just right for a graph. And with that space partitioned off, it wasn’t terribly hard to draw things.
-
And believe me, if I ever create a game platform, the y coordinate is going to increase upward, not downward. Remind me to think about how to transform the graph coordinates to make it easier to draw graphs.
In any case, we’ll call a break this morning. I have some science fiction to read.
See you next time!